<!-- canonical: https://efficientnewlanguage.org/eml-p/projects/P050-connect-four/ | updated: 2026-10-10 -->

# P050 Connect Four

Drop discs into a grid of 7 columns and 6 rows and be the first with four in a row - across, down or diagonally. Play a friend, or the computer, going first or second: it wins when it can, blocks when it must, and looks one move ahead so as not to set up your win. A running score is kept.

EML-P project `projects/connect-four` in the EML language repo: 3 module(s), entry `main.eml`, terminal UI. There, `eml project run projects/connect-four` runs it and `eml project verify projects/connect-four` 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
0
x
1
0
8
x
44
1
1
1
1
1
1
1

3

4
5
```

Screen:

```text
== Connect Four ==
Drop discs into the columns; four in a row wins - across, down or diagonally.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 0
Pick a number from 1 to 5.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> x
Pick a number from 1 to 5.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 1
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
X, column (1 to 7)> 0
Type a column from 1 to 7.
X, column (1 to 7)> 8
Type a column from 1 to 7.
X, column (1 to 7)> x
Type a column from 1 to 7.
X, column (1 to 7)> 44
Type a column from 1 to 7.
X, column (1 to 7)> 1
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  X . . . . . .
O, column (1 to 7)> 1
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  O . . . . . .
  X . . . . . .
X, column (1 to 7)> 1
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  X . . . . . .
  O . . . . . .
  X . . . . . .
O, column (1 to 7)> 1
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  O . . . . . .
  X . . . . . .
  O . . . . . .
  X . . . . . .
X, column (1 to 7)> 1
  1 2 3 4 5 6 7
  . . . . . . .
  X . . . . . .
  O . . . . . .
  X . . . . . .
  O . . . . . .
  X . . . . . .
O, column (1 to 7)> 1
  1 2 3 4 5 6 7
  O . . . . . .
  X . . . . . .
  O . . . . . .
  X . . . . . .
  O . . . . . .
  X . . . . . .
X, column (1 to 7)> 1
Column 1 is full.
X, column (1 to 7)> 
Game left.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 3
The computer plays X and goes first; you play O.
The computer drops in column 4.
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . X . . .
O, column (1 to 7)> 
Game left.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 4
X wins: 0, O wins: 0, draws: 0.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 5
Bye.
```

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

Input:

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

Screen:

```text
== Connect Four ==
Drop discs into the columns; four in a row wins - across, down or diagonally.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 2
You play X and go first.
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
X, column (1 to 7)> 4
The computer drops in column 4.
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . O . . .
  . . . X . . .
X, column (1 to 7)> 2
The computer drops in column 4.
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . O . . .
  . . . O . . .
  . X . X . . .
X, column (1 to 7)> 4
The computer drops in column 4.
  1 2 3 4 5 6 7
  . . . . . . .
  . . . O . . .
  . . . X . . .
  . . . O . . .
  . . . O . . .
  . X . X . . .
X, column (1 to 7)> 2
The computer drops in column 4.
  1 2 3 4 5 6 7
  . . . O . . .
  . . . O . . .
  . . . X . . .
  . . . O . . .
  . X . O . . .
  . X . X . . .
X, column (1 to 7)> 1
The computer drops in column 3.
  1 2 3 4 5 6 7
  . . . O . . .
  . . . O . . .
  . . . X . . .
  . . . O . . .
  . X . O . . .
  X X O X . . .
X, column (1 to 7)> 6
The computer drops in column 5.
  1 2 3 4 5 6 7
  . . . O . . .
  . . . O . . .
  . . . X . . .
  . . . O . . .
  . X . O . . .
  X X O X O X .
X, column (1 to 7)> 2
The computer drops in column 2.
  1 2 3 4 5 6 7
  . . . O . . .
  . . . O . . .
  . O . X . . .
  . X . O . . .
  . X . O . . .
  X X O X O X .
X, column (1 to 7)> 3
The computer drops in column 3.
  1 2 3 4 5 6 7
  . . . O . . .
  . . . O . . .
  . o . X . . .
  . X o O . . .
  . X X o . . .
  X X O X o X .
The computer wins with disc 16 - the winning line is in lower case.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 1
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
X, column (1 to 7)> 1
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  X . . . . . .
O, column (1 to 7)> 2
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  X O . . . . .
X, column (1 to 7)> 2
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . X . . . . .
  X O . . . . .
O, column (1 to 7)> 3
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . X . . . . .
  X O O . . . .
X, column (1 to 7)> 3
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . X X . . . .
  X O O . . . .
O, column (1 to 7)> 4
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . X X . . . .
  X O O O . . .
X, column (1 to 7)> 3
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . X . . . .
  . X X . . . .
  X O O O . . .
O, column (1 to 7)> 4
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . X . . . .
  . X X O . . .
  X O O O . . .
X, column (1 to 7)> 7
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . X . . . .
  . X X O . . .
  X O O O . . X
O, column (1 to 7)> 4
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . . . . .
  . . X O . . .
  . X X O . . .
  X O O O . . X
X, column (1 to 7)> 4
  1 2 3 4 5 6 7
  . . . . . . .
  . . . . . . .
  . . . x . . .
  . . x O . . .
  . x X O . . .
  x O O O . . X
X wins with disc 11 - the winning line is in lower case.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 4
X wins: 1, O wins: 1, draws: 0.

1) two players  2) you first against the computer  3) the computer first  4) score  5) quit
choice> 5
Bye.
```

## Modules

### main.eml

```eml
# P050 Connect Four: drop discs into a grid of 7 columns and 6 rows and be
# the first with four in a row - across, down or diagonally. Play a friend,
# or the computer, which wins when it can, blocks when it must and does
# not set up your win.
import grid
import robot

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 show(g, marked):
    for line in grid.drawn(g, marked):
        line ^0

def ask_column(g, who):
    # A column from 0 to 6 with room in it, or -1 when the answer is empty.
    while True:
        trim(input(who + ", column (1 to 7)> ")) => answer
        if answer == "":
            return -1
        if len(answer) == 1 and answer >= "1" and answer <= "7":
            int(answer) - 1 => c
            if grid.landing_row(g, c) >= 0:
                return c
            ("Column " + answer + " is full.") ^0
        else:
            "Type a column from 1 to 7." ^0

def play(computer):
    # One game; computer is "", "O" or "X" - the side the computer plays.
    # Returns "X", "O", "draw", or "" when the game was left.
    grid.empty_grid() => g
    "X" => who
    0 => discs
    if computer == "O":
        "You play X and go first." ^0
    elif computer == "X":
        "The computer plays X and goes first; you play O." ^0
    while True:
        "O" => other
        if who == "O":
            "X" => other
        if who == computer:
            robot.choose(g, who, other) => c
            ("The computer drops in column " + str(c + 1) + ".") ^0
        else:
            show(g, [])
            ask_column(g, who) => c
            if c < 0:
                "Game left." ^0
                return ""
        grid.drop(g, c, who) => r
        discs + 1 => discs
        grid.run_through(g, r, c) => line
        if len(line) > 0:
            show(g, line)
            if computer == "":
                (who + " wins with disc " + str(discs) + " - the winning line is in lower case.") ^0
            elif who == computer:
                ("The computer wins with disc " + str(discs) + " - the winning line is in lower case.") ^0
            else:
                ("You win with disc " + str(discs) + " - the winning line is in lower case.") ^0
            return who
        if grid.full(g):
            show(g, [])
            "The grid is full: a draw." ^0
            return "draw"
        other => who

"== Connect Four ==" ^0
"Drop discs into the columns; four in a row wins - across, down or diagonally." ^0
[0, 0, 0] => score
True => running
while running:
    "" ^0
    "1) two players  2) you first against the computer  3) the computer first  4) score  5) quit" ^0
    trim(input("choice> ")) => choice
    "" => result
    if choice == "1":
        play("") => result
    elif choice == "2":
        play("O") => result
    elif choice == "3":
        play("X") => result
    elif choice == "4":
        ("X wins: " + str(score[0]) + ", O wins: " + str(score[1]) + ", draws: " + str(score[2]) + ".") ^0
    elif choice == "5":
        False => running
    else:
        "Pick a number from 1 to 5." ^0
    if result == "X":
        score[0] + 1 => score[0]
    elif result == "O":
        score[1] + 1 => score[1]
    elif result == "draw":
        score[2] + 1 => score[2]
"Bye." ^0
```

Python projection of main.eml:

```python
import grid
import robot

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 show(g, marked):
    for line in grid.drawn(g, marked):
        print(line)

def ask_column(g, who):
    while True:
        answer = trim(input(who + ", column (1 to 7)> "))
        if answer == "":
            return -1
        if len(answer) == 1 and answer >= "1" and answer <= "7":
            c = int(answer) - 1
            if grid.landing_row(g, c) >= 0:
                return c
            print("Column " + answer + " is full.")
        else:
            print("Type a column from 1 to 7.")

def play(computer):
    g = grid.empty_grid()
    who = "X"
    discs = 0
    if computer == "O":
        print("You play X and go first.")
    elif computer == "X":
        print("The computer plays X and goes first; you play O.")
    while True:
        other = "O"
        if who == "O":
            other = "X"
        if who == computer:
            c = robot.choose(g, who, other)
            print("The computer drops in column " + str(c + 1) + ".")
        else:
            show(g, [])
            c = ask_column(g, who)
            if c < 0:
                print("Game left.")
                return ""
        r = grid.drop(g, c, who)
        discs = discs + 1
        line = grid.run_through(g, r, c)
        if len(line) > 0:
            show(g, line)
            if computer == "":
                print(who + " wins with disc " + str(discs) + " - the winning line is in lower case.")
            elif who == computer:
                print("The computer wins with disc " + str(discs) + " - the winning line is in lower case.")
            else:
                print("You win with disc " + str(discs) + " - the winning line is in lower case.")
            return who
        if grid.full(g):
            show(g, [])
            print("The grid is full: a draw.")
            return "draw"
        who = other

print("== Connect Four ==")
print("Drop discs into the columns; four in a row wins - across, down or diagonally.")
score = [0, 0, 0]
running = True
while running:
    print("")
    print("1) two players  2) you first against the computer  3) the computer first  4) score  5) quit")
    choice = trim(input("choice> "))
    result = ""
    if choice == "1":
        result = play("")
    elif choice == "2":
        result = play("O")
    elif choice == "3":
        result = play("X")
    elif choice == "4":
        print("X wins: " + str(score[0]) + ", O wins: " + str(score[1]) + ", draws: " + str(score[2]) + ".")
    elif choice == "5":
        running = False
    else:
        print("Pick a number from 1 to 5.")
    if result == "X":
        score[0] = score[0] + 1
    elif result == "O":
        score[1] = score[1] + 1
    elif result == "draw":
        score[2] = score[2] + 1
print("Bye.")
```

### grid.eml

```eml
# P050 Connect Four - the grid: 6 rows of 7 cells, row 0 at the top, each
# cell ".", "X" or "O". A disc dropped into a column falls to the lowest
# empty cell.

def empty_grid():
    [] => g
    for r in [1:6]:
        g + [["."] * 7] => g
    return g

def landing_row(g, c):
    # The row a disc dropped into column c would land in, or -1 when the
    # column is full.
    5 => r
    while r >= 0 and g[r][c] != ".":
        r - 1 => r
    return r

def drop(g, c, who):
    landing_row(g, c) => r
    if r >= 0:
        who => g[r][c]
    return r

def lift(g, c):
    # Takes the top disc out of column c again - the computer tries a move
    # and takes it back.
    0 => r
    while r < 6 and g[r][c] == ".":
        r + 1 => r
    if r < 6:
        "." => g[r][c]

def run_through(g, r, c):
    # The cells of the longest line of four or more through (r, c) holding
    # the same disc - across, down, or along either diagonal - or [] when
    # there is none. Only lines through the last disc can be new, so this
    # is all a move needs to check.
    g[r][c] => who
    for d in [[0, 1], [1, 0], [1, 1], [1, -1]]:
        [[r, c]] => cells
        for sign in [1, -1]:
            r + sign * d[0] => y
            c + sign * d[1] => x
            while y >= 0 and y < 6 and x >= 0 and x < 7 and g[y][x] == who:
                cells + [[y, x]] => cells
                y + sign * d[0] => y
                x + sign * d[1] => x
        if len(cells) >= 4:
            return cells
    return []

def full(g):
    for x in g[0]:
        if x == ".":
            return False
    return True

def drawn(g, marked):
    # The grid with the column numbers on top; the discs of a winning line
    # (the cells in marked) are drawn in lower case.
    ["  1 2 3 4 5 6 7"] => lines
    for r in [0:5]:
        " " => line
        for c in [0:6]:
            g[r][c] => ch
            for cell in marked:
                if cell[0] == r and cell[1] == c:
                    if ch == "X":
                        "x" => ch
                    else:
                        "o" => ch
            line + " " + ch => line
        lines + [line] => lines
    return lines
```

Python projection of grid.eml:

```python
def empty_grid():
    g = []
    for r in range(1, 7):
        g = g + [["."] * 7]
    return g

def landing_row(g, c):
    r = 5
    while r >= 0 and g[r][c] != ".":
        r = r - 1
    return r

def drop(g, c, who):
    r = landing_row(g, c)
    if r >= 0:
        g[r][c] = who
    return r

def lift(g, c):
    r = 0
    while r < 6 and g[r][c] == ".":
        r = r + 1
    if r < 6:
        g[r][c] = "."

def run_through(g, r, c):
    who = g[r][c]
    for d in [[0, 1], [1, 0], [1, 1], [1, -1]]:
        cells = [[r, c]]
        for sign in [1, -1]:
            y = r + sign * d[0]
            x = c + sign * d[1]
            while y >= 0 and y < 6 and x >= 0 and x < 7 and g[y][x] == who:
                cells = cells + [[y, x]]
                y = y + sign * d[0]
                x = x + sign * d[1]
        if len(cells) >= 4:
            return cells
    return []

def full(g):
    for x in g[0]:
        if x == ".":
            return False
    return True

def drawn(g, marked):
    lines = ["  1 2 3 4 5 6 7"]
    for r in range(0, 6):
        line = " "
        for c in range(0, 7):
            ch = g[r][c]
            for cell in marked:
                if cell[0] == r and cell[1] == c:
                    if ch == "X":
                        ch = "x"
                    else:
                        ch = "o"
            line = line + " " + ch
        lines = lines + [line]
    return lines
```

### robot.eml

```eml
# P050 Connect Four - the computer's move, in order of preference:
#  1. win now, if a column does it;
#  2. otherwise block a column where the other player would win next;
#  3. otherwise look one move ahead: keep to columns after which the other
#     player cannot win at once (not under their winning cell);
#  4. among what is left, the column nearest the middle - a disc there
#     belongs to the most lines of four.
# P009's tic-tac-toe player wins and blocks the same way; the look ahead
# is new here. No randomness: the same moves always get the same answer.
import grid

def order():
    return [3, 2, 4, 1, 5, 0, 6]

def winning_column(g, who):
    # The first column, middle first, where a disc of who makes four, or -1.
    for c in order():
        grid.drop(g, c, who) => r
        if r >= 0:
            len(grid.run_through(g, r, c)) > 0 => wins
            grid.lift(g, c)
            if wins:
                return c
    return -1

def safe(g, c, me, other):
    # After my disc in column c, can the other player win with their next
    # disc? Safe when they cannot.
    grid.drop(g, c, me)
    winning_column(g, other) => reply
    grid.lift(g, c)
    return reply == -1

def choose(g, me, other):
    winning_column(g, me) => c
    if c >= 0:
        return c
    winning_column(g, other) => c
    if c >= 0:
        return c
    -1 => fallback
    for c in order():
        if grid.landing_row(g, c) >= 0:
            if fallback == -1:
                c => fallback
            if safe(g, c, me, other):
                return c
    # Every column lets the other player win next: take the middle-most.
    return fallback
```

Python projection of robot.eml:

```python
import grid

def order():
    return [3, 2, 4, 1, 5, 0, 6]

def winning_column(g, who):
    for c in order():
        r = grid.drop(g, c, who)
        if r >= 0:
            wins = len(grid.run_through(g, r, c)) > 0
            grid.lift(g, c)
            if wins:
                return c
    return -1

def safe(g, c, me, other):
    grid.drop(g, c, me)
    reply = winning_column(g, other)
    grid.lift(g, c)
    return reply == -1

def choose(g, me, other):
    c = winning_column(g, me)
    if c >= 0:
        return c
    c = winning_column(g, other)
    if c >= 0:
        return c
    fallback = -1
    for c in order():
        if grid.landing_row(g, c) >= 0:
            if fallback == -1:
                fallback = c
            if safe(g, c, me, other):
                return c
    return fallback
```

## README

# P050 - Connect Four

Two sides drop discs into a grid of 7 columns and 6 rows; a disc falls to
the lowest empty cell of its column. The first side with four discs in a
row - across, down or along a diagonal - wins, and a full grid is a draw.
Play a friend, or the computer, going first or second; a running score is
kept.

- `main.eml` - the menu, one game from the first disc to the last, the
  messages and the score
- `grid.eml` - the grid: where a disc lands, dropping and taking back a
  disc, the line of four through a disc, and the drawing
- `robot.eml` - the computer's choice of column

How each part works:

- Only a line through the disc just dropped can be new, so after each
  disc the game looks along the four directions through it - across, down
  and both diagonals - and counts the same discs on both sides. When the
  game is won, the discs of the winning line are drawn in lower case.
- The computer picks, in this order: a column that wins at once; otherwise
  a column where you would win next, to block it; otherwise it looks one
  move ahead and keeps to columns after which you cannot win at once - not
  right under the cell you need. Among those it takes the column nearest
  the middle, since a disc there belongs to the most lines of four. This
  is the tic-tac-toe player of P009 and the corpus case `mvp-tic-tac-toe`
  with that look ahead added. There is no randomness, so the same moves
  always get the same answers.
- To try a column the computer drops a disc, looks, and takes it back out
  again, so the grid it was given is left exactly as it was.

What is checked: menu choices 1 to 5; a column from 1 to 7 that is not
full. An empty answer leaves the game, which then counts for no one.

Sessions: `sessions/basic.in` plays one game against the computer and one
between two players, then shows the score. In the first game the computer
blocks three in a row along the bottom, then passes over column 3: a disc
there would let you finish the diagonal from column 1 to column 4 on top
of it, so it plays column 5. It blocks three in a column, and when you
fill in column 3 yourself, it drops on top of that disc and wins along a
diagonal with disc 16. In the two-player game, X wins with disc 11 on the
diagonal from the bottom of column 1 to the fourth row of column 4.

`sessions/bad-input.in` gives:
- menu choices 0 and x;
- columns 0, 8, x and 44;
- column 1 filled to the top and tried once more;
- a game left with an empty answer, both between two players and against
  the computer, which plays first in column 4;
- the score, where neither left game counts.

Built on the verified corpus case `mvp-tic-tac-toe` (a real two-player
board game: moves, a taken square refused, and the lines that win).
