Project 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.

3 modules · 2 recorded sessionstext-menu UI in the terminalupdated 2026-10-10

Every screen below was recorded under CPython. When this page was built, the EML interpreter replayed each session from the same input and printed the same bytes.

About

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).

Recorded sessions

What the screen shows while someone uses the program. Each typed line appears after its prompt, the way a terminal shows it.

bad-input

interpreter: byte-equal
== 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.
What was typed (19 lines)
0
x
1
0
8
x
44
1
1
1
1
1
1
1

3

4
5

basic

interpreter: byte-equal
== 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.
What was typed (23 lines)
2
4
2
4
2
1
6
2
3
1
1
2
2
3
3
4
3
4
7
4
4
4
5

Modules

The program as written, entry module first. Each module transpiles to its own Python file, which is what eml project run executes.

main.eml(entry)

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 (main.py)
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 (grid.py)
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 (robot.py)
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

Built on these corpus cases