Skip to main content

Connect Four with local play and a networked server/clients.

Project description

Getting Started

A terminal Connect Four game in Python. This guide gets you playing locally and shows the mechanics of writing your own bot. (For playing over a network with a server and clients, see NETWORKED_PLAY.md.)


Requirements

  • Python 3.13 or newer — check with python3 --version.
  • No packages to install — it uses only the standard library.

Run all commands from the project folder (the one with local.py and the bot/ directory). If python3 isn't found, try python.


Play a local game

Local play runs everything in one program. Start a human-vs-human game:

python3 local.py

You'll enter a name, then see the board:

 0 1 2 3 4 5 6
+-+-+-+-+-+-+-+
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
| | | | | | | |
+-+-+-+-+-+-+-+

The numbers on top are column numbers, starting at 0. On your turn, type a column number and press Enter to drop your piece; it falls to the lowest open slot. Get win_length pieces in a row (horizontal, vertical, or diagonal) to win. Ctrl-C quits.

Choosing players

RED always moves first, then YELLOW. Use --red and --yellow to set each color to either human or a bot name:

python3 local.py                                    # human vs human (default)
python3 local.py --yellow leftmost                  # you (RED) vs leftmost bot
python3 local.py --red leftmost --yellow rightmost  # bot vs bot

The bots included so far are deliberately simple — they're your competition to beat:

Name Strategy
leftmost Plays the left-most column with room.
rightmost Plays the right-most column with room.
rand Plays a random column with room.
invalid Always plays an out-of-bounds column (for testing error handling).

Changing the board and win condition

Option Default Meaning
--cols 7 Board width
--rows 6 Board height
--win-length 4 Pieces in a row needed to win
# Same as the default:
python3 local.py --cols 7 --rows 6 --win-length 4

# A small, fast "three in a row" board — handy while testing a bot:
python3 local.py --cols 5 --rows 4 --win-length 3 --red leftmost --yellow rand

win_length must be at least 1 and can't exceed both the width and height (otherwise no win is possible); the game refuses impossible combinations.


How a game flows (the rules as a state machine)

Every game — local or networked — is run by one referee (Game.play() in game.py). It is the single source of truth for the rules: turn order, what makes a move legal, and how a game ends. Both the local game and the server are thin shells that just ask play() what happens next, so the rules can't drift apart between the two modes.

play() loops over one turn at a time. Each turn asks the current player (RED first, then alternating) for a column, then branches on what comes back:

stateDiagram-v2
    direction TB
    [*] --> AskCurrentPlayer: RED moves first

    AskCurrentPlayer --> ApplyMove: legal column
    AskCurrentPlayer --> IllegalMove: illegal / malformed column
    AskCurrentPlayer --> GameOver: forfeit / timeout

    IllegalMove --> AskCurrentPlayer: ask again (same player)
    IllegalMove --> GameOver: 5th illegal move

    ApplyMove --> GameOver: four in a row, or board full
    ApplyMove --> AskCurrentPlayer: otherwise, other player's turn

    GameOver --> [*]

Reading the same thing in plain text, if your terminal doesn't render the diagram above:

        +------>+-----------------------------+
        |       |  Ask current player         |-- forfeit / timeout --> GAME OVER
        |       |  for a column               |
        |       +-----------------------------+
        |            |                  |
        |       legal column      illegal / malformed
        |            |                  |
        |            v                  v
        |       +-----------+      +--------------+
        |       | Apply move|      | Illegal move |-- 5th illegal move --> GAME OVER
        |       +-----------+      +--------------+
        |            |                  |
        |       win / board full        |
        |            |                  |
        |            v                  |
        |        GAME OVER              |
        +--- no win: other player's     +-- still your turn: ask again
             turn (back to top)             (back to top)

The five ways a game can end (the end reason, reported to both players and in the server's [GAME] log):

End reason What happened
WIN A player got win_length in a row — they win.
TIE The board filled up with no winner.
TOO_MANY_INVALID A player made 5 illegal moves in one turn (MAX_INVALID_MOVES); the opponent wins.
TIMEOUT A player ran out of time on a move (networked play only); the opponent wins.
FORFEIT A player couldn't move — disconnected, or a bot crashed; the opponent wins.

What this means for a bot:

  • You're only ever asked for a move on your turn, and you get a fresh copy of the board, so you can't corrupt the real game.
  • An illegal column (out of range, or a full column) isn't fatal on its own — you're simply asked again. But the count resets only after a legal move, so five bad columns in a single turn loses the game. Return a playable column.
  • A bot that raises an exception forfeits that game (the opponent wins) rather than crashing the program; locally you'll also see the traceback to debug.

Write your own bot

A bot is just one function that, given the current board and which piece you're playing, returns the column number to play. The rest of this section is the plumbing; the strategy is up to you.

1. Create a file in bot/

Add a file bot/<name>.py. Any file you drop in the bot/ folder is picked up automatically when the program starts (except files beginning with _ or test_). The name you register is the name you'll use on the command line.

Here is the entire leftmost bot (bot/leftmost.py) — the simplest complete example to model yours on:

from game import Board, Piece
from . import register


@register("leftmost")
def leftmost(board: Board, piece: Piece) -> int:
    for c in range(board.cols):
        if board.is_column_playable(c):
            return c
    raise ValueError("Board appears full")

The pieces that make it a bot:

  • from . import register — pulls in the registration helper from the bot package.
  • @register("leftmost") — registers the function under a name. Use a unique name for yours, e.g. @register("mybot").
  • The function signature must be (board: Board, piece: Piece) -> int.
  • It returns a column number (an int). It does not place the piece itself — the game does that.

2. What your function gets, and what it returns

  • board — the current position. Read it to decide your move (see the toolbox below). Treat it as read-only.
  • piecePiece.RED or Piece.YELLOW, whichever you are playing.
  • Return — the column number to drop into, 0 to board.cols - 1. Pick a column that still has room. If you return a full or out-of-range column the server counts it as an illegal move, so check first.

3. The board toolbox

These are the methods and values you'll use to inspect the position:

Tool What it gives you
board.cols, board.rows Board dimensions.
board.win_length Pieces in a row needed to win (may not be 4!).
board.is_column_playable(c) True if column c has room.
board.get_next_open_row(c) Row a new piece in column c would land on (None if full).
board[c][r] The Piece at column c, row r (Piece.EMPTY if empty). Row 0 is the bottom.
board.check_win(piece) True if piece already has a winning line.
board.is_full() True if no moves remain.
piece.opponent() The other player's piece.
board.copy() A separate copy you can experiment on.
board.move_piece(c, piece) Drops piece into column c on that board.

The last two are how you can try out a move without disturbing the real game: copy the board, call move_piece on the copy, and inspect the result (for example with check_win). The real board the game hands you is unchanged.

4. Run and test your bot

Once bot/mybot.py exists, use it like any other bot:

python3 local.py --red mybot --yellow leftmost
python3 local.py --red mybot --yellow rand --cols 5 --rows 4 --win-length 3

You can also call the function directly to test specific situations. Set up a position with move_piece, then assert your bot picks the column you expect:

import unittest
from game import Board, Piece
import bot


class TestMyBot(unittest.TestCase):
    def test_picks_a_playable_column(self):
        mybot = bot.strict_lookup("mybot")  # raises if "mybot" never registered
        board = Board()
        board.move_piece(3, Piece.RED)   # set up any position you like
        choice = mybot(board, Piece.YELLOW)
        self.assertTrue(board.is_column_playable(choice))


if __name__ == "__main__":
    unittest.main()

Run your tests with:

python3 -m unittest discover -p "test_*.py"

5. Now it's your turn

The example bots never look at where the pieces are — that's the bar to clear. Some questions to get you thinking (no code provided on purpose):

  • Can your bot notice when it has a move that wins right now, and take it?
  • Can it notice when the opponent is about to win, and block?
  • Which column is worth more when nothing urgent is happening?
  • How would you handle a board whose win_length isn't 4?

Start small, play it against leftmost and rand, and grow it from there.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

jkh_c4-0.0.14.tar.gz (60.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

jkh_c4-0.0.14-py3-none-any.whl (36.7 kB view details)

Uploaded Python 3

File details

Details for the file jkh_c4-0.0.14.tar.gz.

File metadata

  • Download URL: jkh_c4-0.0.14.tar.gz
  • Upload date:
  • Size: 60.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for jkh_c4-0.0.14.tar.gz
Algorithm Hash digest
SHA256 62d8d37c10a453dbab78abdc41d1620c6b66b9c44913f200332228c949d5e8a6
MD5 a53cff80d0805967a705b410cd2e3bdd
BLAKE2b-256 bee0aca444b2b55e45f293f948edebe952c409838eb3447fdc17d162ee712da2

See more details on using hashes here.

Provenance

The following attestation bundles were made for jkh_c4-0.0.14.tar.gz:

Publisher: release.yml on jkh52/c4

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file jkh_c4-0.0.14-py3-none-any.whl.

File metadata

  • Download URL: jkh_c4-0.0.14-py3-none-any.whl
  • Upload date:
  • Size: 36.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.13

File hashes

Hashes for jkh_c4-0.0.14-py3-none-any.whl
Algorithm Hash digest
SHA256 f32fe8a64031d575163e8113f554ea62a3ac1c5cadd26164461a3e902f902a45
MD5 6f479f0ba780b28cb6ff2e82f61ebe93
BLAKE2b-256 cfe4ea2145656ffc5eea2745c5d8a8e3a90c25e7f50bb1a164b986fd6510ea8a

See more details on using hashes here.

Provenance

The following attestation bundles were made for jkh_c4-0.0.14-py3-none-any.whl:

Publisher: release.yml on jkh52/c4

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page