<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P009-tic-tac-toe/ | updated: 2026-09-29 -->

# P009 Tic-tac-toe

Play tic-tac-toe against a friend or against the computer, which wins when it can and blocks when it must; wins and draws are found and a running score is kept - from a text menu.

EML-P project `projects/tic-tac-toe` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/tic-tac-toe` runs it and `eml project verify projects/tic-tac-toe` replays every session under CPython (two hash seeds) and in the interpreter; the site build replays every session in the interpreter again and publishes a session only if its screen matches.

Built on verified corpus cases: mvp-tic-tac-toe (https://efficientnewlanguage.org/cases/007-mvp-tic-tac-toe/).

## Sessions

### bad-input - interpreter: byte-equal to the golden

Input:

```text
9
3
1
0
10
x

5
5
q
3
4
```

Screen:

```text

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 9
Pick a number from 1 to 4.

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 3
X wins: 0, O wins: 0, draws: 0

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 1

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 0
Pick a free square from 1 to 9.
X to play, square 1-9 (q to leave)> 10
Pick a free square from 1 to 9.
X to play, square 1-9 (q to leave)> x
Pick a free square from 1 to 9.
X to play, square 1-9 (q to leave)> 
Pick a free square from 1 to 9.
X to play, square 1-9 (q to leave)> 5

 1 | 2 | 3
---+---+---
 4 | X | 6
---+---+---
 7 | 8 | 9
O to play, square 1-9 (q to leave)> 5
That square is taken.
O to play, square 1-9 (q to leave)> q
Game left.

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 3
X wins: 0, O wins: 0, draws: 0

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 4
Bye.
```

### basic - interpreter: byte-equal to the golden

Input:

```text
1
1
4
2
5
3
2
1
9
7
4
2
2
8
3
1
1
5
9
2
8
7
3
6
4
3
4
```

Screen:

```text

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 1

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 1

 X | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9
O to play, square 1-9 (q to leave)> 4

 X | 2 | 3
---+---+---
 O | 5 | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 2

 X | X | 3
---+---+---
 O | 5 | 6
---+---+---
 7 | 8 | 9
O to play, square 1-9 (q to leave)> 5

 X | X | 3
---+---+---
 O | O | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 3

 X | X | X
---+---+---
 O | O | 6
---+---+---
 7 | 8 | 9
X wins.

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 2

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 1
Computer plays 5.

 X | 2 | 3
---+---+---
 4 | O | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 9
Computer plays 3.

 X | 2 | O
---+---+---
 4 | O | 6
---+---+---
 7 | 8 | X
X to play, square 1-9 (q to leave)> 7
Computer plays 8.

 X | 2 | O
---+---+---
 4 | O | 6
---+---+---
 X | O | X
X to play, square 1-9 (q to leave)> 4

 X | 2 | O
---+---+---
 X | O | 6
---+---+---
 X | O | X
You win.

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 2

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 2
Computer plays 5.

 1 | X | 3
---+---+---
 4 | O | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 8
Computer plays 1.

 O | X | 3
---+---+---
 4 | O | 6
---+---+---
 7 | X | 9
X to play, square 1-9 (q to leave)> 3
Computer plays 9.

 O | X | X
---+---+---
 4 | O | 6
---+---+---
 7 | X | O
The computer wins.

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 1

 1 | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 1

 X | 2 | 3
---+---+---
 4 | 5 | 6
---+---+---
 7 | 8 | 9
O to play, square 1-9 (q to leave)> 5

 X | 2 | 3
---+---+---
 4 | O | 6
---+---+---
 7 | 8 | 9
X to play, square 1-9 (q to leave)> 9

 X | 2 | 3
---+---+---
 4 | O | 6
---+---+---
 7 | 8 | X
O to play, square 1-9 (q to leave)> 2

 X | O | 3
---+---+---
 4 | O | 6
---+---+---
 7 | 8 | X
X to play, square 1-9 (q to leave)> 8

 X | O | 3
---+---+---
 4 | O | 6
---+---+---
 7 | X | X
O to play, square 1-9 (q to leave)> 7

 X | O | 3
---+---+---
 4 | O | 6
---+---+---
 O | X | X
X to play, square 1-9 (q to leave)> 3

 X | O | X
---+---+---
 4 | O | 6
---+---+---
 O | X | X
O to play, square 1-9 (q to leave)> 6

 X | O | X
---+---+---
 4 | O | O
---+---+---
 O | X | X
X to play, square 1-9 (q to leave)> 4

 X | O | X
---+---+---
 X | O | O
---+---+---
 O | X | X
Draw.

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 3
X wins: 2, O wins: 1, draws: 1

== Tic-tac-toe ==
1) two players  2) against the computer  3) score  4) quit
choice> 4
Bye.
```

## Modules

### main.eml

```eml
# P009 tic-tac-toe: two players, or you (X) against the computer (O). X
# always starts. The score is kept while the program runs.
import board
import ai

def trim(s):
    0 => i
    len(s) => j
    while i < j and s[i] == " ":
        i + 1 => i
    while j > i and s[j - 1] == " ":
        j - 1 => j
    return s[i:j]

def ask_square(b, who):
    # The cell index of a free square for who, or -1 when q leaves the game.
    while True:
        trim(input(who + " to play, square 1-9 (q to leave)> ")) => typed
        if typed == "q":
            return -1
        if len(typed) == 1 and typed in "123456789":
            int(typed) - 1 => i
            if b[i] == "":
                return i
            "That square is taken." ^0
        else:
            "Pick a free square from 1 to 9." ^0

def play(vs_computer):
    # One game. Returns "X", "O", "draw", or "" when it was left.
    board.empty() => b
    "X" => who
    while True:
        if vs_computer and who == "O":
            ai.move(b, "O", "X") => i
            ("Computer plays " + str(i + 1) + ".") ^0
        else:
            board.show(b)
            ask_square(b, who) => i
            if i < 0:
                "Game left." ^0
                return ""
        board.placed(b, i, who) => b
        board.winner(b) => w
        if w != "":
            board.show(b)
            if not vs_computer:
                (w + " wins.") ^0
            elif w == "X":
                "You win." ^0
            else:
                "The computer wins." ^0
            return w
        if board.full(b):
            board.show(b)
            "Draw." ^0
            return "draw"
        if who == "X":
            "O" => who
        else:
            "X" => who

[0, 0, 0] => score
True => running
while running:
    "" ^0
    "== Tic-tac-toe ==" ^0
    "1) two players  2) against the computer  3) score  4) quit" ^0
    trim(input("choice> ")) => choice
    if choice == "1" or choice == "2":
        play(choice == "2") => result
        if result == "X":
            score[0] + 1 => score[0]
        elif result == "O":
            score[1] + 1 => score[1]
        elif result == "draw":
            score[2] + 1 => score[2]
    elif choice == "3":
        ("X wins: " + str(score[0]) + ", O wins: " + str(score[1]) + ", draws: " + str(score[2])) ^0
    elif choice == "4":
        False => running
    else:
        "Pick a number from 1 to 4." ^0
"Bye." ^0
```

Python projection of main.eml:

```python
import board
import ai

def trim(s):
    i = 0
    j = len(s)
    while i < j and s[i] == " ":
        i = i + 1
    while j > i and s[j - 1] == " ":
        j = j - 1
    return s[i:j]

def ask_square(b, who):
    while True:
        typed = trim(input(who + " to play, square 1-9 (q to leave)> "))
        if typed == "q":
            return -1
        if len(typed) == 1 and typed in "123456789":
            i = int(typed) - 1
            if b[i] == "":
                return i
            print("That square is taken.")
        else:
            print("Pick a free square from 1 to 9.")

def play(vs_computer):
    b = board.empty()
    who = "X"
    while True:
        if vs_computer and who == "O":
            i = ai.move(b, "O", "X")
            print("Computer plays " + str(i + 1) + ".")
        else:
            board.show(b)
            i = ask_square(b, who)
            if i < 0:
                print("Game left.")
                return ""
        b = board.placed(b, i, who)
        w = board.winner(b)
        if w != "":
            board.show(b)
            if not vs_computer:
                print(w + " wins.")
            elif w == "X":
                print("You win.")
            else:
                print("The computer wins.")
            return w
        if board.full(b):
            board.show(b)
            print("Draw.")
            return "draw"
        if who == "X":
            who = "O"
        else:
            who = "X"

score = [0, 0, 0]
running = True
while running:
    print("")
    print("== Tic-tac-toe ==")
    print("1) two players  2) against the computer  3) score  4) quit")
    choice = trim(input("choice> "))
    if choice == "1" or choice == "2":
        result = play(choice == "2")
        if result == "X":
            score[0] = score[0] + 1
        elif result == "O":
            score[1] = score[1] + 1
        elif result == "draw":
            score[2] = score[2] + 1
    elif choice == "3":
        print("X wins: " + str(score[0]) + ", O wins: " + str(score[1]) + ", draws: " + str(score[2]))
    elif choice == "4":
        running = False
    else:
        print("Pick a number from 1 to 4.")
print("Bye.")
```

### board.eml

```eml
# P009 tic-tac-toe - the board. It is a list of nine cells, index 0 to 8 for
# squares 1 to 9 in reading order; a cell is "X", "O" or "" when empty.

# The eight lines that win: three rows, three columns, two diagonals.
[
    [0, 1, 2], [3, 4, 5], [6, 7, 8],
    [0, 3, 6], [1, 4, 7], [2, 5, 8],
    [0, 4, 8], [2, 4, 6],
] => lines

def empty():
    return ["" for i in [0:8]]

def mark(cell, i):
    # What a square shows: its mark, or its number while it is free.
    if cell == "":
        return str(i + 1)
    return cell

def show(b):
    "" ^0
    for r in [0:2]:
        (" " + mark(b[3 * r], 3 * r) + " | " + mark(b[3 * r + 1], 3 * r + 1) + " | " + mark(b[3 * r + 2], 3 * r + 2)) ^0
        if r < 2:
            "---+---+---" ^0

def winner(b):
    # "X" or "O" if that player has a whole line, otherwise "".
    for line in lines:
        b[line[0]] => a
        if a != "" and b[line[1]] == a and b[line[2]] == a:
            return a
    return ""

def full(b):
    for cell in b:
        if cell == "":
            return False
    return True

def placed(b, i, who):
    # A new board with who's mark on cell i.
    return b[0:i] + [who] + b[i + 1:9]
```

Python projection of board.eml:

```python
lines = [[0, 1, 2], [3, 4, 5], [6, 7, 8], [0, 3, 6], [1, 4, 7], [2, 5, 8], [0, 4, 8], [2, 4, 6]]

def empty():
    return ["" for i in range(0, 9)]

def mark(cell, i):
    if cell == "":
        return str(i + 1)
    return cell

def show(b):
    print("")
    for r in range(0, 3):
        print(" " + mark(b[3 * r], 3 * r) + " | " + mark(b[3 * r + 1], 3 * r + 1) + " | " + mark(b[3 * r + 2], 3 * r + 2))
        if r < 2:
            print("---+---+---")

def winner(b):
    for line in lines:
        a = b[line[0]]
        if a != "" and b[line[1]] == a and b[line[2]] == a:
            return a
    return ""

def full(b):
    for cell in b:
        if cell == "":
            return False
    return True

def placed(b, i, who):
    return b[0:i] + [who] + b[i + 1:9]
```

### ai.eml

```eml
# P009 tic-tac-toe - the computer's move, in order of preference: win now if
# it can; otherwise block the square where the other player would win;
# otherwise take the centre, then the first free corner, then the first free
# side. No randomness, so the same moves always get the same answer.
import board

def completing(b, who):
    # The free cell that would give who a whole line, or -1. Lines are tried
    # in order, so the first such cell is the answer.
    for line in board.lines:
        0 => mine
        -1 => free
        for i in line:
            if b[i] == who:
                mine + 1 => mine
            elif b[i] == "":
                i => free
        if mine == 2 and free >= 0:
            return free
    return -1

def move(b, me, other):
    completing(b, me) => i
    if i >= 0:
        return i
    completing(b, other) => i
    if i >= 0:
        return i
    for i in [4, 0, 2, 6, 8, 1, 3, 5, 7]:
        if b[i] == "":
            return i
    return -1
```

Python projection of ai.eml:

```python
import board

def completing(b, who):
    for line in board.lines:
        mine = 0
        free = -1
        for i in line:
            if b[i] == who:
                mine = mine + 1
            elif b[i] == "":
                free = i
        if mine == 2 and free >= 0:
            return free
    return -1

def move(b, me, other):
    i = completing(b, me)
    if i >= 0:
        return i
    i = completing(b, other)
    if i >= 0:
        return i
    for i in [4, 0, 2, 6, 8, 1, 3, 5, 7]:
        if b[i] == "":
            return i
    return -1
```

## README

# P009 - Tic-tac-toe

Tic-tac-toe for two players, or for one player (X) against the computer (O).
X always starts; the board shows the free squares by number; a game ends in a
win or a draw, or when a player leaves it with `q`. The score is kept while
the program runs.

- `main.eml` - the menu, one game, and the checks on each square typed
- `board.eml` - the board as nine cells, how it is drawn, the eight winning
  lines, and whether the board is full
- `ai.eml` - the computer's move, in order of preference: win now if it can;
  otherwise block the square where the other player would win; otherwise the
  centre, then the first free corner, then the first free side. There is no
  randomness, so the same moves always get the same answer.

What is checked: a square must be one of 1 to 9 and still free; anything else
asks again without losing the turn.

Sessions: `sessions/basic.in` plays a two-player game won by X; a game against
the computer in which it takes the centre and a corner and blocks one of two
threats, and X wins with the other; a game the computer wins because winning
comes before blocking; a two-player draw; and the score;
`sessions/bad-input.in` types an unknown choice, squares 0, 10, a letter and
nothing, a taken square, and leaves a game, which does not count.

Built on the verified corpus case `mvp-tic-tac-toe` (a board, moves that
refuse a taken square, and finding a line of three).
