jkh-c4 0.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- jkh_c4-0.0.1/GETTING_STARTED.md +203 -0
- jkh_c4-0.0.1/LICENSE +21 -0
- jkh_c4-0.0.1/PKG-INFO +221 -0
- jkh_c4-0.0.1/bot/__init__.py +57 -0
- jkh_c4-0.0.1/bot/invalid.py +7 -0
- jkh_c4-0.0.1/bot/leftmost.py +10 -0
- jkh_c4-0.0.1/bot/rand.py +12 -0
- jkh_c4-0.0.1/bot/rightmost.py +10 -0
- jkh_c4-0.0.1/bot/test_bot.py +147 -0
- jkh_c4-0.0.1/client.py +272 -0
- jkh_c4-0.0.1/game.py +390 -0
- jkh_c4-0.0.1/jkh_c4.egg-info/PKG-INFO +221 -0
- jkh_c4-0.0.1/jkh_c4.egg-info/SOURCES.txt +20 -0
- jkh_c4-0.0.1/jkh_c4.egg-info/dependency_links.txt +1 -0
- jkh_c4-0.0.1/jkh_c4.egg-info/entry_points.txt +4 -0
- jkh_c4-0.0.1/jkh_c4.egg-info/top_level.txt +7 -0
- jkh_c4-0.0.1/local.py +144 -0
- jkh_c4-0.0.1/message.py +227 -0
- jkh_c4-0.0.1/protocol.py +33 -0
- jkh_c4-0.0.1/pyproject.toml +43 -0
- jkh_c4-0.0.1/server.py +391 -0
- jkh_c4-0.0.1/setup.cfg +4 -0
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
A terminal Connect Four game in Python. This guide gets you playing locally and
|
|
4
|
+
shows the **mechanics** of writing your own bot. (For playing over a network
|
|
5
|
+
with a server and clients, see [NETWORKED_PLAY.md](NETWORKED_PLAY.md).)
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Requirements
|
|
10
|
+
|
|
11
|
+
- **Python 3.13 or newer** — check with `python3 --version`.
|
|
12
|
+
- **No packages to install** — it uses only the standard library.
|
|
13
|
+
|
|
14
|
+
Run all commands from the project folder (the one with `local.py` and the
|
|
15
|
+
`bot/` directory). If `python3` isn't found, try `python`.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Play a local game
|
|
20
|
+
|
|
21
|
+
Local play runs everything in one program. Start a human-vs-human game:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
python3 local.py
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
You'll enter a name, then see the board:
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
0 1 2 3 4 5 6
|
|
31
|
+
+-+-+-+-+-+-+-+
|
|
32
|
+
| | | | | | | |
|
|
33
|
+
| | | | | | | |
|
|
34
|
+
| | | | | | | |
|
|
35
|
+
| | | | | | | |
|
|
36
|
+
| | | | | | | |
|
|
37
|
+
| | | | | | | |
|
|
38
|
+
+-+-+-+-+-+-+-+
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
The numbers on top are **column numbers, starting at 0**. On your turn, type a
|
|
42
|
+
column number and press Enter to drop your piece; it falls to the lowest open
|
|
43
|
+
slot. Get `win_length` pieces in a row (horizontal, vertical, or diagonal) to
|
|
44
|
+
win. **Ctrl-C** quits.
|
|
45
|
+
|
|
46
|
+
### Choosing players
|
|
47
|
+
|
|
48
|
+
RED always moves first, then YELLOW. Use `--red` and `--yellow` to set each
|
|
49
|
+
color to either `human` or a bot name:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
python3 local.py # human vs human (default)
|
|
53
|
+
python3 local.py --yellow leftmost # you (RED) vs leftmost bot
|
|
54
|
+
python3 local.py --red leftmost --yellow rightmost # bot vs bot
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
The bots included so far are deliberately simple — they're your competition to
|
|
58
|
+
beat:
|
|
59
|
+
|
|
60
|
+
| Name | Strategy |
|
|
61
|
+
| ----------- | ------------------------------------------------------------------- |
|
|
62
|
+
| `leftmost` | Plays the left-most column with room. |
|
|
63
|
+
| `rightmost` | Plays the right-most column with room. |
|
|
64
|
+
| `rand` | Plays a random column with room. |
|
|
65
|
+
| `invalid` | Always plays an out-of-bounds column (for testing error handling). |
|
|
66
|
+
|
|
67
|
+
### Changing the board and win condition
|
|
68
|
+
|
|
69
|
+
| Option | Default | Meaning |
|
|
70
|
+
| -------------- | ------- | ----------------------------- |
|
|
71
|
+
| `--cols` | `7` | Board width |
|
|
72
|
+
| `--rows` | `6` | Board height |
|
|
73
|
+
| `--win-length` | `4` | Pieces in a row needed to win |
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
# Same as the default:
|
|
77
|
+
python3 local.py --cols 7 --rows 6 --win-length 4
|
|
78
|
+
|
|
79
|
+
# A small, fast "three in a row" board — handy while testing a bot:
|
|
80
|
+
python3 local.py --cols 5 --rows 4 --win-length 3 --red leftmost --yellow rand
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
`win_length` must be at least 1 and can't exceed *both* the width and height
|
|
84
|
+
(otherwise no win is possible); the game refuses impossible combinations.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Write your own bot
|
|
89
|
+
|
|
90
|
+
A bot is just **one function** that, given the current board and which piece
|
|
91
|
+
you're playing, returns the **column number to play**. The rest of this section
|
|
92
|
+
is the plumbing; the *strategy* is up to you.
|
|
93
|
+
|
|
94
|
+
### 1. Create a file in `bot/`
|
|
95
|
+
|
|
96
|
+
Add a file `bot/<name>.py`. Any file you drop in the `bot/` folder is picked up
|
|
97
|
+
automatically when the program starts (except files beginning with `_` or
|
|
98
|
+
`test_`). The name you `register` is the name you'll use on the command line.
|
|
99
|
+
|
|
100
|
+
Here is the entire `leftmost` bot (`bot/leftmost.py`) — the simplest complete
|
|
101
|
+
example to model yours on:
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
from game import Board, Piece
|
|
105
|
+
from . import register
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@register("leftmost")
|
|
109
|
+
def leftmost(board: Board, piece: Piece) -> int:
|
|
110
|
+
for c in range(board.cols):
|
|
111
|
+
if board.is_column_playable(c):
|
|
112
|
+
return c
|
|
113
|
+
raise ValueError("Board appears full")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
The pieces that make it a bot:
|
|
117
|
+
|
|
118
|
+
- `from . import register` — pulls in the registration helper from the `bot`
|
|
119
|
+
package.
|
|
120
|
+
- `@register("leftmost")` — registers the function under a name. Use a unique
|
|
121
|
+
name for yours, e.g. `@register("mybot")`.
|
|
122
|
+
- The function signature **must** be `(board: Board, piece: Piece) -> int`.
|
|
123
|
+
- It **returns a column number** (an `int`). It does *not* place the piece
|
|
124
|
+
itself — the game does that.
|
|
125
|
+
|
|
126
|
+
### 2. What your function gets, and what it returns
|
|
127
|
+
|
|
128
|
+
- **`board`** — the current position. Read it to decide your move (see the
|
|
129
|
+
toolbox below). Treat it as read-only.
|
|
130
|
+
- **`piece`** — `Piece.RED` or `Piece.YELLOW`, whichever you are playing.
|
|
131
|
+
- **Return** — the column number to drop into, `0` to `board.cols - 1`. Pick a
|
|
132
|
+
column that still has room. If you return a full or out-of-range column the
|
|
133
|
+
server counts it as an illegal move, so check first.
|
|
134
|
+
|
|
135
|
+
### 3. The board toolbox
|
|
136
|
+
|
|
137
|
+
These are the methods and values you'll use to inspect the position:
|
|
138
|
+
|
|
139
|
+
| Tool | What it gives you |
|
|
140
|
+
| --------------------------------- | ---------------------------------------------------------- |
|
|
141
|
+
| `board.cols`, `board.rows` | Board dimensions. |
|
|
142
|
+
| `board.win_length` | Pieces in a row needed to win (may not be 4!). |
|
|
143
|
+
| `board.is_column_playable(c)` | `True` if column `c` has room. |
|
|
144
|
+
| `board.get_next_open_row(c)` | Row a new piece in column `c` would land on (`None` if full). |
|
|
145
|
+
| `board[c][r]` | The `Piece` at column `c`, row `r` (`Piece.EMPTY` if empty). Row 0 is the bottom. |
|
|
146
|
+
| `board.check_win(piece)` | `True` if `piece` already has a winning line. |
|
|
147
|
+
| `board.is_full()` | `True` if no moves remain. |
|
|
148
|
+
| `piece.opponent()` | The other player's piece. |
|
|
149
|
+
| `board.copy()` | A separate copy you can experiment on. |
|
|
150
|
+
| `board.move_piece(c, piece)` | Drops `piece` into column `c` **on that board**. |
|
|
151
|
+
|
|
152
|
+
The last two are how you can *try out* a move without disturbing the real game:
|
|
153
|
+
copy the board, call `move_piece` on the copy, and inspect the result (for
|
|
154
|
+
example with `check_win`). The real `board` the game hands you is unchanged.
|
|
155
|
+
|
|
156
|
+
### 4. Run and test your bot
|
|
157
|
+
|
|
158
|
+
Once `bot/mybot.py` exists, use it like any other bot:
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
python3 local.py --red mybot --yellow leftmost
|
|
162
|
+
python3 local.py --red mybot --yellow rand --cols 5 --rows 4 --win-length 3
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
You can also call the function directly to test specific situations. Set up a
|
|
166
|
+
position with `move_piece`, then assert your bot picks the column you expect:
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
import unittest
|
|
170
|
+
from game import Board, Piece
|
|
171
|
+
import bot
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
class TestMyBot(unittest.TestCase):
|
|
175
|
+
def test_picks_a_playable_column(self):
|
|
176
|
+
mybot = bot.strict_lookup("mybot") # raises if "mybot" never registered
|
|
177
|
+
board = Board()
|
|
178
|
+
board.move_piece(3, Piece.RED) # set up any position you like
|
|
179
|
+
choice = mybot(board, Piece.YELLOW)
|
|
180
|
+
self.assertTrue(board.is_column_playable(choice))
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
if __name__ == "__main__":
|
|
184
|
+
unittest.main()
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
Run your tests with:
|
|
188
|
+
|
|
189
|
+
```bash
|
|
190
|
+
python3 -m unittest discover -p "test_*.py"
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
### 5. Now it's your turn
|
|
194
|
+
|
|
195
|
+
The example bots never look at where the pieces are — that's the bar to clear.
|
|
196
|
+
Some questions to get you thinking (no code provided on purpose):
|
|
197
|
+
|
|
198
|
+
- Can your bot notice when *it* has a move that wins right now, and take it?
|
|
199
|
+
- Can it notice when the *opponent* is about to win, and block?
|
|
200
|
+
- Which column is worth more when nothing urgent is happening?
|
|
201
|
+
- How would you handle a board whose `win_length` isn't 4?
|
|
202
|
+
|
|
203
|
+
Start small, play it against `leftmost` and `rand`, and grow it from there.
|
jkh_c4-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Joseph Anttila Hall
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
jkh_c4-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: jkh-c4
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: Connect Four with local play and a networked server/clients.
|
|
5
|
+
Author-email: Joseph Anttila Hall <joseph.hall@gmail.com>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: connect-four,game,asyncio,teaching
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Classifier: Intended Audience :: Education
|
|
12
|
+
Classifier: Topic :: Games/Entertainment :: Board Games
|
|
13
|
+
Classifier: Operating System :: OS Independent
|
|
14
|
+
Requires-Python: >=3.11
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
19
|
+
# Getting Started
|
|
20
|
+
|
|
21
|
+
A terminal Connect Four game in Python. This guide gets you playing locally and
|
|
22
|
+
shows the **mechanics** of writing your own bot. (For playing over a network
|
|
23
|
+
with a server and clients, see [NETWORKED_PLAY.md](NETWORKED_PLAY.md).)
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Requirements
|
|
28
|
+
|
|
29
|
+
- **Python 3.13 or newer** — check with `python3 --version`.
|
|
30
|
+
- **No packages to install** — it uses only the standard library.
|
|
31
|
+
|
|
32
|
+
Run all commands from the project folder (the one with `local.py` and the
|
|
33
|
+
`bot/` directory). If `python3` isn't found, try `python`.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Play a local game
|
|
38
|
+
|
|
39
|
+
Local play runs everything in one program. Start a human-vs-human game:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
python3 local.py
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
You'll enter a name, then see the board:
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
0 1 2 3 4 5 6
|
|
49
|
+
+-+-+-+-+-+-+-+
|
|
50
|
+
| | | | | | | |
|
|
51
|
+
| | | | | | | |
|
|
52
|
+
| | | | | | | |
|
|
53
|
+
| | | | | | | |
|
|
54
|
+
| | | | | | | |
|
|
55
|
+
| | | | | | | |
|
|
56
|
+
+-+-+-+-+-+-+-+
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The numbers on top are **column numbers, starting at 0**. On your turn, type a
|
|
60
|
+
column number and press Enter to drop your piece; it falls to the lowest open
|
|
61
|
+
slot. Get `win_length` pieces in a row (horizontal, vertical, or diagonal) to
|
|
62
|
+
win. **Ctrl-C** quits.
|
|
63
|
+
|
|
64
|
+
### Choosing players
|
|
65
|
+
|
|
66
|
+
RED always moves first, then YELLOW. Use `--red` and `--yellow` to set each
|
|
67
|
+
color to either `human` or a bot name:
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
python3 local.py # human vs human (default)
|
|
71
|
+
python3 local.py --yellow leftmost # you (RED) vs leftmost bot
|
|
72
|
+
python3 local.py --red leftmost --yellow rightmost # bot vs bot
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The bots included so far are deliberately simple — they're your competition to
|
|
76
|
+
beat:
|
|
77
|
+
|
|
78
|
+
| Name | Strategy |
|
|
79
|
+
| ----------- | ------------------------------------------------------------------- |
|
|
80
|
+
| `leftmost` | Plays the left-most column with room. |
|
|
81
|
+
| `rightmost` | Plays the right-most column with room. |
|
|
82
|
+
| `rand` | Plays a random column with room. |
|
|
83
|
+
| `invalid` | Always plays an out-of-bounds column (for testing error handling). |
|
|
84
|
+
|
|
85
|
+
### Changing the board and win condition
|
|
86
|
+
|
|
87
|
+
| Option | Default | Meaning |
|
|
88
|
+
| -------------- | ------- | ----------------------------- |
|
|
89
|
+
| `--cols` | `7` | Board width |
|
|
90
|
+
| `--rows` | `6` | Board height |
|
|
91
|
+
| `--win-length` | `4` | Pieces in a row needed to win |
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# Same as the default:
|
|
95
|
+
python3 local.py --cols 7 --rows 6 --win-length 4
|
|
96
|
+
|
|
97
|
+
# A small, fast "three in a row" board — handy while testing a bot:
|
|
98
|
+
python3 local.py --cols 5 --rows 4 --win-length 3 --red leftmost --yellow rand
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`win_length` must be at least 1 and can't exceed *both* the width and height
|
|
102
|
+
(otherwise no win is possible); the game refuses impossible combinations.
|
|
103
|
+
|
|
104
|
+
---
|
|
105
|
+
|
|
106
|
+
## Write your own bot
|
|
107
|
+
|
|
108
|
+
A bot is just **one function** that, given the current board and which piece
|
|
109
|
+
you're playing, returns the **column number to play**. The rest of this section
|
|
110
|
+
is the plumbing; the *strategy* is up to you.
|
|
111
|
+
|
|
112
|
+
### 1. Create a file in `bot/`
|
|
113
|
+
|
|
114
|
+
Add a file `bot/<name>.py`. Any file you drop in the `bot/` folder is picked up
|
|
115
|
+
automatically when the program starts (except files beginning with `_` or
|
|
116
|
+
`test_`). The name you `register` is the name you'll use on the command line.
|
|
117
|
+
|
|
118
|
+
Here is the entire `leftmost` bot (`bot/leftmost.py`) — the simplest complete
|
|
119
|
+
example to model yours on:
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from game import Board, Piece
|
|
123
|
+
from . import register
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@register("leftmost")
|
|
127
|
+
def leftmost(board: Board, piece: Piece) -> int:
|
|
128
|
+
for c in range(board.cols):
|
|
129
|
+
if board.is_column_playable(c):
|
|
130
|
+
return c
|
|
131
|
+
raise ValueError("Board appears full")
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
The pieces that make it a bot:
|
|
135
|
+
|
|
136
|
+
- `from . import register` — pulls in the registration helper from the `bot`
|
|
137
|
+
package.
|
|
138
|
+
- `@register("leftmost")` — registers the function under a name. Use a unique
|
|
139
|
+
name for yours, e.g. `@register("mybot")`.
|
|
140
|
+
- The function signature **must** be `(board: Board, piece: Piece) -> int`.
|
|
141
|
+
- It **returns a column number** (an `int`). It does *not* place the piece
|
|
142
|
+
itself — the game does that.
|
|
143
|
+
|
|
144
|
+
### 2. What your function gets, and what it returns
|
|
145
|
+
|
|
146
|
+
- **`board`** — the current position. Read it to decide your move (see the
|
|
147
|
+
toolbox below). Treat it as read-only.
|
|
148
|
+
- **`piece`** — `Piece.RED` or `Piece.YELLOW`, whichever you are playing.
|
|
149
|
+
- **Return** — the column number to drop into, `0` to `board.cols - 1`. Pick a
|
|
150
|
+
column that still has room. If you return a full or out-of-range column the
|
|
151
|
+
server counts it as an illegal move, so check first.
|
|
152
|
+
|
|
153
|
+
### 3. The board toolbox
|
|
154
|
+
|
|
155
|
+
These are the methods and values you'll use to inspect the position:
|
|
156
|
+
|
|
157
|
+
| Tool | What it gives you |
|
|
158
|
+
| --------------------------------- | ---------------------------------------------------------- |
|
|
159
|
+
| `board.cols`, `board.rows` | Board dimensions. |
|
|
160
|
+
| `board.win_length` | Pieces in a row needed to win (may not be 4!). |
|
|
161
|
+
| `board.is_column_playable(c)` | `True` if column `c` has room. |
|
|
162
|
+
| `board.get_next_open_row(c)` | Row a new piece in column `c` would land on (`None` if full). |
|
|
163
|
+
| `board[c][r]` | The `Piece` at column `c`, row `r` (`Piece.EMPTY` if empty). Row 0 is the bottom. |
|
|
164
|
+
| `board.check_win(piece)` | `True` if `piece` already has a winning line. |
|
|
165
|
+
| `board.is_full()` | `True` if no moves remain. |
|
|
166
|
+
| `piece.opponent()` | The other player's piece. |
|
|
167
|
+
| `board.copy()` | A separate copy you can experiment on. |
|
|
168
|
+
| `board.move_piece(c, piece)` | Drops `piece` into column `c` **on that board**. |
|
|
169
|
+
|
|
170
|
+
The last two are how you can *try out* a move without disturbing the real game:
|
|
171
|
+
copy the board, call `move_piece` on the copy, and inspect the result (for
|
|
172
|
+
example with `check_win`). The real `board` the game hands you is unchanged.
|
|
173
|
+
|
|
174
|
+
### 4. Run and test your bot
|
|
175
|
+
|
|
176
|
+
Once `bot/mybot.py` exists, use it like any other bot:
|
|
177
|
+
|
|
178
|
+
```bash
|
|
179
|
+
python3 local.py --red mybot --yellow leftmost
|
|
180
|
+
python3 local.py --red mybot --yellow rand --cols 5 --rows 4 --win-length 3
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
You can also call the function directly to test specific situations. Set up a
|
|
184
|
+
position with `move_piece`, then assert your bot picks the column you expect:
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
import unittest
|
|
188
|
+
from game import Board, Piece
|
|
189
|
+
import bot
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
class TestMyBot(unittest.TestCase):
|
|
193
|
+
def test_picks_a_playable_column(self):
|
|
194
|
+
mybot = bot.strict_lookup("mybot") # raises if "mybot" never registered
|
|
195
|
+
board = Board()
|
|
196
|
+
board.move_piece(3, Piece.RED) # set up any position you like
|
|
197
|
+
choice = mybot(board, Piece.YELLOW)
|
|
198
|
+
self.assertTrue(board.is_column_playable(choice))
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
if __name__ == "__main__":
|
|
202
|
+
unittest.main()
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Run your tests with:
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
python3 -m unittest discover -p "test_*.py"
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
### 5. Now it's your turn
|
|
212
|
+
|
|
213
|
+
The example bots never look at where the pieces are — that's the bar to clear.
|
|
214
|
+
Some questions to get you thinking (no code provided on purpose):
|
|
215
|
+
|
|
216
|
+
- Can your bot notice when *it* has a move that wins right now, and take it?
|
|
217
|
+
- Can it notice when the *opponent* is about to win, and block?
|
|
218
|
+
- Which column is worth more when nothing urgent is happening?
|
|
219
|
+
- How would you handle a board whose `win_length` isn't 4?
|
|
220
|
+
|
|
221
|
+
Start small, play it against `leftmost` and `rand`, and grow it from there.
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import importlib
|
|
2
|
+
import pkgutil
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Callable
|
|
5
|
+
|
|
6
|
+
from game import Board, Piece
|
|
7
|
+
|
|
8
|
+
_BOTS: dict[str, Callable[[Board, Piece], int]] = {}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def register(name: str) -> Callable:
|
|
12
|
+
def decorator(fn: Callable[[Board, Piece], int]) -> Callable[[Board, Piece], int]:
|
|
13
|
+
if name in _BOTS:
|
|
14
|
+
raise ValueError(
|
|
15
|
+
f"Bot name {name!r} is already registered by "
|
|
16
|
+
f"{_BOTS[name].__module__}; choose a different name.")
|
|
17
|
+
_BOTS[name] = fn
|
|
18
|
+
return fn
|
|
19
|
+
return decorator
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def lookup(name: str) -> Callable[[Board, Piece], int] | None:
|
|
23
|
+
"""Return the bot registered under name, or None if there is none.
|
|
24
|
+
|
|
25
|
+
The lenient counterpart to strict_lookup() (which raises KeyError). Use it
|
|
26
|
+
where the name might legitimately be unknown, e.g. validating user input.
|
|
27
|
+
"""
|
|
28
|
+
return _BOTS.get(name)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def strict_lookup(name: str) -> Callable[[Board, Piece], int]:
|
|
32
|
+
"""Return the bot registered under name, raising KeyError if there is none.
|
|
33
|
+
|
|
34
|
+
The strict counterpart to lookup() (which returns None). Use it where the
|
|
35
|
+
name is already known valid and its absence would be a bug, e.g. in tests.
|
|
36
|
+
"""
|
|
37
|
+
return _BOTS[name]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_bot_module(name: str) -> None:
|
|
41
|
+
"""Import one bot module, skipping it (with a warning) if it can't load.
|
|
42
|
+
|
|
43
|
+
A syntax error or a duplicate name in one student's bot file must not
|
|
44
|
+
break the whole package — and with it local.py, client.py, and server.py.
|
|
45
|
+
"""
|
|
46
|
+
try:
|
|
47
|
+
importlib.import_module(f".{name}", package=__name__)
|
|
48
|
+
except Exception as exc:
|
|
49
|
+
print(f"⚠️ Skipping bot module {name!r}: {exc}", file=sys.stderr)
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
for _mod_info in pkgutil.iter_modules(__path__):
|
|
53
|
+
if not _mod_info.name.startswith(("_", "test_")):
|
|
54
|
+
_load_bot_module(_mod_info.name)
|
|
55
|
+
|
|
56
|
+
# Don't expose bots as module attributes: one named after a framework symbol
|
|
57
|
+
# (e.g. "lookup") would shadow it.
|
jkh_c4-0.0.1/bot/rand.py
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import random
|
|
2
|
+
|
|
3
|
+
from game import Board, Piece
|
|
4
|
+
from . import register
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@register("rand")
|
|
8
|
+
def rand(board: Board, piece: Piece) -> int:
|
|
9
|
+
choices = [c for c in range(board.cols) if board.is_column_playable(c)]
|
|
10
|
+
if not choices:
|
|
11
|
+
raise ValueError("Board appears full")
|
|
12
|
+
return random.choice(choices)
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from game import Board, Piece
|
|
2
|
+
from . import register
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
@register("rightmost")
|
|
6
|
+
def rightmost(board: Board, piece: Piece) -> int:
|
|
7
|
+
for c in reversed(range(board.cols)):
|
|
8
|
+
if board.is_column_playable(c):
|
|
9
|
+
return c
|
|
10
|
+
raise ValueError("Board appears full")
|