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.

Accessible display

The board is drawn with emoji disks by default. If that's hard to read — you use a screen reader, can't tell the colors apart, or your terminal renders the fullwidth grid poorly — pick a different style with --display:

--display Looks like
emoji The colored-disk grid above (the default).
ascii A plain grid using letters — R, Y, and . for empty — with | + - borders and ordinary digit headers. Pieces are told apart by letter, not color.
text No grid at all: each column is described in words, bottom to top — read aloud cleanly by a screen reader.
python3 local.py --display ascii
python3 local.py --display text --yellow leftmost

The same --display flag works for networked play (python3 client.py --display text). It only changes what you see; both players can choose different styles in the same game.

ascii shows the board above as:

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

and text describes each column, e.g. Column 3: red, yellow. (listed from the bottom up, the order pieces were dropped).


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.1.2.tar.gz (69.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.1.2-py3-none-any.whl (42.9 kB view details)

Uploaded Python 3

File details

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

File metadata

  • Download URL: jkh_c4-0.1.2.tar.gz
  • Upload date:
  • Size: 69.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.1.2.tar.gz
Algorithm Hash digest
SHA256 7db44fd632412a3f40aed86e13cc97fdd855f19ab68bb66cc14f92169d099780
MD5 aa3b43ea9e03a4cbeb3ce05497da34e1
BLAKE2b-256 548c7675efa693dbb4c47b41e290af54ba8b4fc948c6213c9f2e44919747a2f1

See more details on using hashes here.

Provenance

The following attestation bundles were made for jkh_c4-0.1.2.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.1.2-py3-none-any.whl.

File metadata

  • Download URL: jkh_c4-0.1.2-py3-none-any.whl
  • Upload date:
  • Size: 42.9 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.1.2-py3-none-any.whl
Algorithm Hash digest
SHA256 84b5764f8a0523069eedd46e83eba557629f62a8d0a37c3001caf9b103919fe7
MD5 91e025240642f42041999b53f27bcf85
BLAKE2b-256 89bcbf3132355331e64a2f77975287e17f6df2cebc9c165b6b0506173ed4ae6b

See more details on using hashes here.

Provenance

The following attestation bundles were made for jkh_c4-0.1.2-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