Skip to content

Your first game

In this chapter you will read a complete game, tic-tac-toe, from top to bottom and see how each piece works. It is the whole language in 130 lines: a class, a list you change in place, loops, a computer opponent, input and drawing. It compiles to a 21,233-byte compiled program file, .dbc, and plays in the playground.

The state

# fragment: the class and its state
class Game:
    def __init__(self):
        self.board = [0, 0, 0, 0, 0, 0, 0, 0, 0]
        self.reset()

    def reset(self):
        for i in range(9):
            self.board[i] = 0
        self.moves = 0
        self.ended = False
        self.message = "Your turn"

The board is a list of nine integers: 0 for empty, 1 for you, 2 for the computer. Notice that reset writes into the existing list instead of assigning a new one. Either works, and both keep the promise Python makes about aliasing: two names for the same list stay the same list.

The rules

# fragment: a method of Game
    def wins(self, player):
        for i in range(3):
            row = i * 3
            if self.board[row] == player and self.board[row + 1] == player and self.board[row + 2] == player:
                return True
            if self.board[i] == player and self.board[i + 3] == player and self.board[i + 6] == player:
                return True
        if self.board[0] == player and self.board[4] == player and self.board[8] == player:
            return True
        if self.board[2] == player and self.board[4] == player and self.board[6] == player:
            return True
        return False

    def place(self, index, player):
        self.board[index] = player
        self.moves += 1
        if self.wins(player):
            self.ended = True
            self.message = "You win!" if player == 1 else "Computer wins!"
        elif self.moves == 9:
            self.ended = True
            self.message = "A draw!"

A conditional expression (a if test else b) picks one of two strings. Both sides have the same type, which is what the compiler needs to generate specialized code for it.

The opponent

# fragment: a method of Game
    def computer(self):
        # Finish a winning line, block a threat, then prefer center and corners.
        for player in [2, 1]:
            for i in range(9):
                if self.board[i] == 0:
                    self.board[i] = player
                    win = self.wins(player)
                    self.board[i] = 0
                    if win:
                        self.place(i, 2)
                        return
        for i in [4, 0, 2, 6, 8, 1, 3, 5, 7]:
            if self.board[i] == 0:
                self.place(i, 2)
                return

Try each empty square, first for the computer and then for you; if either move would win, take that square. Otherwise take the center, then the corners. The tests for this program check its rules against boards written out independently and against CPython, and the native tests cover held buttons, occupied squares, input after the game is over and repeated resets.

Input and drawing

# fragment: the handlers
game = Game()
pointer_down = False

@dos.on("ON_KEY")
def on_key(key):
    if key == 82 or key == 114:
        game.reset()
    elif key >= 49 and key <= 57:
        game.play(key - 49)
    dos.service("INVALIDATE")

@dos.on("ON_POINTER")
def on_pointer(x, y, buttons):
    global pointer_down
    down = buttons % 2 == 1
    if down and not pointer_down:
        if x >= 16 and x < 138 and y >= 48 and y < 170:
            column = (x - 16) // 42
            row = (y - 48) // 42
            if (x - 16) % 42 < 38 and (y - 48) % 42 < 38:
                game.play(row * 3 + column)
                dos.service("INVALIDATE")
    pointer_down = down

Keys arrive as codes (R is 82 or 114, the digits 1 to 9 are 49 to 57). The pointer handler acts only at the moment of the press and works out which square was hit with // and %. Drawing, in Game.draw, is one frame: a background, nine squares, a label per square, the message and the legend; see the full source.

What it costs

On macOS, the native measurement of 11 September 2026 recorded 17,968 bytes of application code, and these per-turn costs: 157,694 instructions to open the window, 806,604 for the first move and 322,130 to reset. Five games in a row return to 11 live objects at each reset, and closing the window releases everything. Sharing method bodies as cached, typed compiled procedures took the program from 67,275 bytes to 17,968 without changing a rule or a reviewed pixel. The browser build's compiled program is larger because the compiler's front end has grown since; the size on the examples page is measured from this bundle.

Make it yours

Change the opponent's preference list, add a score line, or replace the labels. Every edit recompiles right in the tab. If you reach for something the compiler does not have, it will stop and tell you at the exact line; the compatibility table tells you why.