The classic Connect Four game in Python, playable in the terminal or in a Pygame window, against a friend or against a CPU opponent that uses the minimax algorithm.
| Game mode menu | Player wins |
|---|---|
![]() |
![]() |
| CPU wins on a diagonal | Draw |
![]() |
![]() |
- Two interfaces: a text version for the terminal and a graphical version with Pygame.
- CPU opponent with three difficulty levels, the hardest one using minimax search with alpha-beta pruning.
- Pygame interface with a game mode menu, falling piece animation, mouse and keyboard controls, and the winning line highlighted.
- Game rules separated from the interfaces: both the terminal and the Pygame window use the same
JudgeandBoardclasses. - Automated tests for the rules, the CPU and both interfaces, run on every push by GitHub Actions on Python 3.10 to 3.14.
You need Python 3.10 or newer.
git clone https://github.com/renanss4/Connect4.git
cd Connect4
python -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[gui,dev]"The extras are optional: gui installs Pygame, which is only needed for the graphical version, and dev installs the testing and linting tools. The terminal version has no dependencies, so pip install -e . is enough to play it.
On Ubuntu and other Linux distributions you may need to use
python3instead ofpython.
Two players take turns dropping pieces into one of the seven columns. The piece falls to the lowest empty row. The first player to line up four pieces in a row, horizontally, vertically or diagonally, wins. If the board fills up first, the game is a draw.
python -m connect4 # or simply: connect4Choose 1 or 2 players (and the CPU difficulty for 1 player), then type the number of the column for each move. Press Ctrl+C to quit at any time.
python -m connect4.gui # or simply: connect4-gui| Action | Mouse | Keyboard |
|---|---|---|
| Choose a game mode | Click an option | 1 to 4 |
| Aim at a column | Move the mouse | ← → |
| Drop the piece | Click | Enter, Space or ↓, or 1 to 7 for that column |
| Restart the game | R |
|
| Back to the menu (or quit, from the menu) | Esc |
| Level | Strategy |
|---|---|
| Easy | Plays a random column. |
| Medium | Wins if it can, otherwise blocks your winning move, otherwise plays at random. |
| Hard | Looks 6 moves ahead with the minimax algorithm. |
How the hard level thinks: for each possible move, it simulates every reply of the opponent, every answer to that reply, and so on, 6 moves deep. It assumes that both players always choose their best option: the CPU picks the move with the highest score and the opponent the one with the lowest. Positions where nobody has won yet are scored by a heuristic that rewards pieces in the center column and lines of 2 or 3 pieces that can still become 4.
Alpha-beta pruning skips branches that cannot change the decision. It chooses the same move as a plain minimax, about 50 times faster in this project's tests. Trying the center columns first makes the pruning even more effective.
The implementation is in connect4/cpu.py.
connect4/
├── board.py # Board: the grid, dropping pieces, finding four in a row
├── player.py # Player: a human player's name and symbol
├── cpu.py # CpuPlayer and Difficulty: the three CPU strategies
├── judge.py # Judge: turns, moves, winner and draw (no input or output)
├── cli.py # Terminal interface
├── gui.py # Pygame interface
└── __main__.py # Makes `python -m connect4` start the terminal version
tests/ # pytest test suite
classDiagram
direction LR
class Board {
+int rows
+int columns
+cell(row, column) str
+is_valid_move(column) bool
+valid_columns() list
+landing_row(column) int
+drop_piece(column, symbol) int
+remove_piece(column)
+copy() Board
+lines() tuple
+winning_line(symbol) list
+has_winner(symbol) bool
+is_full() bool
}
class Player {
+str name
+str symbol
+bool is_human
}
class CpuPlayer {
+int SEARCH_DEPTH
+Difficulty difficulty
+choose_column(board) int
-_minimax(board, depth, alpha, beta, maximizing) float
-_evaluate(board) int
}
class Difficulty {
<<enumeration>>
EASY
MEDIUM
HARD
}
class Judge {
+Board board
+Player current_player
+Player winner
+bool is_draw
+bool is_over
+play(column) int
+reset()
}
class cli {
<<module>>
+main()
+play_game(judge)
+render(board) str
+ask_int(prompt, valid) int
}
class App {
+Judge judge
+run()
+handle_event(event)
+update(dt)
+draw()
}
Player <|-- CpuPlayer
CpuPlayer --> Difficulty
Judge *-- Board
Judge o-- "2" Player
cli ..> Judge : uses
App --> Judge : uses
CpuPlayer ..> Board : simulates moves on a copy
The interfaces never change the board directly: every move goes through Judge.play(), which enforces the rules. This is why the terminal and the Pygame window share the same flow.
sequenceDiagram
actor Human
participant UI as Interface (cli or App)
participant Judge
participant Board
participant CPU as CpuPlayer
Human->>UI: chooses a column
UI->>Judge: play(column)
Judge->>Board: drop_piece(column, symbol)
Judge->>Board: has_winner(symbol)
Board-->>Judge: False
Judge->>Board: is_full()
Board-->>Judge: False
Note over Judge: no winner and no draw: the turn passes to the CPU
UI->>Judge: current_player
Judge-->>UI: CpuPlayer
UI->>CPU: choose_column(board)
CPU->>Board: copy()
Note over CPU: minimax simulates moves on the copy
CPU-->>UI: column
UI->>Judge: play(column)
Note over UI,Judge: repeats until judge.is_over
pytest # run the test suite
pytest -v # one line per test
ruff check . # lint
ruff format . # format the codeThe Pygame tests run without opening a window. If Pygame is not installed, they are skipped.
Contributions are welcome! If you find a bug or have an idea, open an issue or a pull request:
- Fork the repository and clone your fork.
- Create a branch:
git checkout -b my-contribution. - Make your changes and check that
pytestandruff check .pass. - Commit, push to your fork and open a pull request.
GitHub Actions runs the tests and the linter on every pull request.
- Stronger CPU: a transposition table to avoid evaluating the same position twice, and iterative deepening to search as deep as a time limit allows.
- More variety from the CPU: choose randomly between equally good moves, so it does not always play the same game.
- Sound effects when a piece lands and when the game ends.
- Score board across games, alternating who starts.
This project is licensed under the MIT License, which means you have complete freedom to use it as you prefer. Enjoy playing and contributing!



