Metadata-Version: 2.4
Name: jkh-c4
Version: 0.0.3
Summary: Connect Four with local play and a networked server/clients.
Author-email: Joseph Anttila Hall <joseph.hall@gmail.com>
License-Expression: MIT
Keywords: connect-four,game,asyncio,teaching
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Intended Audience :: Education
Classifier: Topic :: Games/Entertainment :: Board Games
Classifier: Operating System :: OS Independent
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Dynamic: license-file

# 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](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:

```bash
python3 local.py
```

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

```
　０　１　２　３　４　５　６
＋－＋－＋－＋－＋－＋－＋－＋
｜　｜　｜　｜　｜　｜　｜　｜
｜　｜　｜　｜　｜　｜　｜　｜
｜　｜　｜　｜　｜　｜　｜　｜
｜　｜　｜　｜　｜　｜　｜　｜
｜　｜　｜　｜　｜　｜　｜　｜
｜　｜　｜　｜　｜　｜　｜　｜
＋－＋－＋－＋－＋－＋－＋－＋
```

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:

```bash
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 |

```bash
# 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.

---

## 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:

```python
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.
- **`piece`** — `Piece.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:

```bash
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:

```python
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:

```bash
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.
