magmacrunch-chess 0.1.0__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.
@@ -0,0 +1,9 @@
1
+ # The link tools/link-shared.mjs makes to the website's arcade/shared/.
2
+ /shared
3
+ __pycache__/
4
+ *.pyc
5
+ .pytest_cache/
6
+ node_modules/
7
+ .DS_Store
8
+ .ruff_cache/
9
+ *.egg-info/
@@ -0,0 +1,75 @@
1
+ Required Notice: Copyright 2026 magmacrunch media (https://magmacrunch.com)
2
+
3
+ # PolyForm Noncommercial License 1.0.0
4
+
5
+ <https://polyformproject.org/licenses/noncommercial/1.0.0>
6
+
7
+ ## Acceptance
8
+
9
+ In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses.
10
+
11
+ ## Copyright License
12
+
13
+ The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license).
14
+
15
+ ## Distribution License
16
+
17
+ The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license).
18
+
19
+ ## Notices
20
+
21
+ You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example:
22
+
23
+ > Required Notice: Copyright Yoyodyne, Inc. (http://example.com)
24
+
25
+ ## Changes and New Works License
26
+
27
+ The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose.
28
+
29
+ ## Patent License
30
+
31
+ The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software.
32
+
33
+ ## Noncommercial Purposes
34
+
35
+ Any noncommercial purpose is a permitted purpose.
36
+
37
+ ## Personal Uses
38
+
39
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose.
40
+
41
+ ## Noncommercial Organizations
42
+
43
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding.
44
+
45
+ ## Fair Use
46
+
47
+ You may have "fair use" rights for the software under the law. These terms do not limit them.
48
+
49
+ ## No Other Rights
50
+
51
+ These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses.
52
+
53
+ ## Patent Defense
54
+
55
+ If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company.
56
+
57
+ ## Violations
58
+
59
+ The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately.
60
+
61
+ ## No Liability
62
+
63
+ ***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.***
64
+
65
+ ## Definitions
66
+
67
+ The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms.
68
+
69
+ **You** refers to the individual or entity agreeing to these terms.
70
+
71
+ **Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect.
72
+
73
+ **Your licenses** are all the licenses granted to you for the software under these terms.
74
+
75
+ **Use** means anything you do with the software requiring one of your licenses.
@@ -0,0 +1,30 @@
1
+ chess
2
+ Copyright 2026 magmacrunch media
3
+
4
+
5
+ THE LICENCE
6
+
7
+ Everything in this repository -- the game in web/, its terminal version in
8
+ tui/, its tests, the tools and the documentation -- is licensed under the
9
+ PolyForm Noncommercial License 1.0.0, in the LICENSE file beside this one.
10
+
11
+ Read it, learn from it, build on it, play with it. Any noncommercial purpose is
12
+ permitted, and that includes personal projects, teaching and research. What is not
13
+ permitted is commercial use: selling the game or a derivative of it, shipping it inside
14
+ a product, or running it as part of a business.
15
+
16
+ Chess is a traditional game and nobody's property; this licence does not pretend
17
+ otherwise. What is licensed is this implementation: the rules engine, the board
18
+ renderer, the server and the arcade's presentation of them.
19
+
20
+ What is reserved: the magmacrunch name and marks and the visual design of the arcade.
21
+ If you want to use any of it, ask.
22
+
23
+
24
+ THIRD-PARTY COMPONENTS
25
+
26
+ Fonts are loaded from Google Fonts under the SIL Open Font License 1.1 rather than
27
+ shipped here as files.
28
+
29
+ The page loads adenosine-multiplayer.js, arcade-base.css, board-game-template.js, mp-server.js from the website's arcade/shared/, and the server imports server_base from arcade/shared/multiplayer/.
30
+ Those are part of the magmacrunch.com repository and adenosine, and are not copied here.
@@ -0,0 +1,249 @@
1
+ Metadata-Version: 2.5
2
+ Name: magmacrunch-chess
3
+ Version: 0.1.0
4
+ Summary: Chess against the computer. Terminal version.
5
+ Project-URL: Homepage, https://magmacrunch.com/arcade/chess/
6
+ Author: magmacrunch media
7
+ License-Expression: PolyForm-Noncommercial-1.0.0
8
+ License-File: LICENSE
9
+ License-File: NOTICE
10
+ Keywords: arcade,board,chess,games,terminal,tui
11
+ Requires-Python: >=3.10
12
+ Requires-Dist: magmacrunch>=0.7.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest>=7.0; extra == 'dev'
15
+ Requires-Dist: ruff>=0.6; extra == 'dev'
16
+ Requires-Dist: websockets>=14.0; extra == 'dev'
17
+ Provides-Extra: online
18
+ Requires-Dist: websockets>=14.0; extra == 'online'
19
+ Description-Content-Type: text/markdown
20
+
21
+ # Chess (terminal)
22
+
23
+ Chess against the computer, in a terminal. It is the second version of the
24
+ game, beside `web/`. It runs on the
25
+ [magmacrunch](https://github.com/magmacrunch-media/magmacrunch) engine's
26
+ terminal backend, either as a cabinet in the magmacrunch arcade or as its own
27
+ command, and plays the same either way.
28
+
29
+ ```
30
+ pipx install magmacrunch-chess
31
+ chess # the title, where the level and clock are set
32
+ chess --play --level hard --clock blitz # straight into a game
33
+ ```
34
+
35
+ For working on it, from this folder:
36
+
37
+ ```
38
+ pip install -e ".[dev]"
39
+ python -m neon_chess
40
+ ```
41
+
42
+ The command is `chess`. The import package is `neon_chess`, after the page's
43
+ Neon Edition, because `chess` is python-chess's and a player may well have it
44
+ installed.
45
+
46
+ ## Keys
47
+
48
+ | key | |
49
+ |---|---|
50
+ | `←` `↑` `↓` `→` | move the cursor |
51
+ | `ENTER` | pick up the piece under the cursor; on a marked square, put it down |
52
+ | `Q` `R` `B` `N` | when a pawn promotes, what it becomes (or `←` `→` and `ENTER`) |
53
+ | `Esc` | put the piece back; with nothing picked up, the title (and from there, the arcade) |
54
+ | `R` | new game, same level and clock |
55
+ | `Q` | quit |
56
+
57
+ Your pieces are cyan capitals, the computer's pink small letters, as notation
58
+ writes them: K Q R B N P. So the two sides stay apart with no colour at all,
59
+ and under `--ascii`. Once a piece is picked up, a green `·` marks every empty
60
+ square it can go to, and a square it can take on is tinted green. The last
61
+ move is tinted yellow and a king in check red. The cursor is bracketed as well
62
+ as shaded, so it shows under `NO_COLOR`. On the title, `←` `→` change the level
63
+ (Easy, Medium, Hard) and the clock (none, Blitz 5, Rapid 15, Classical 30).
64
+ The panel shows the clocks, whose turn it is, and the move list in the page's
65
+ notation.
66
+
67
+ The computer starts thinking 0.3 seconds after your move, as on the page. It
68
+ thinks on a thread of its own, so the screen and the clocks carry on while it
69
+ does: under a second at Easy and Medium, one or two at Hard. The page freezes
70
+ while it thinks, and its clock does too, so there the computer's clock hardly
71
+ moves. Here it runs.
72
+
73
+ Needs a 68x19 terminal.
74
+
75
+ ## Playing people online (a prototype)
76
+
77
+ PLAY ONLINE on the title, or `chess --online`, connects to the arcade's own
78
+ multiplayer server, the one behind the page's PLAY ONLINE button, so a
79
+ terminal can play a browser. Type a name, then press `C` to create a room and
80
+ read its four-letter code to your opponent, or `J` to join one. The first in
81
+ plays white; the host presses Enter to start once both are in. Black sees the
82
+ board from black's side, as on the page. You move as against the computer, a
83
+ promotion asks for its piece, and `X` then `Y` resigns.
84
+
85
+ ```
86
+ pip install "magmacrunch-chess[online]" # websockets, only for this
87
+ chess --online
88
+ chess --online --server ws://127.0.0.1:8769 # a web/server.py on this machine
89
+ ```
90
+
91
+ The server keeps the rules. This client sends the square a piece leaves and
92
+ the square it lands on, and the piece for a promotion straight after, and
93
+ draws the board the server sends back. It works out the legal moves only to
94
+ mark them, with `rules.py`, the page's own rules, which
95
+ `tests/test_server_parity.py` holds to
96
+ the server's own, piece by piece, in over 20,000 positions with castling, en
97
+ passant and promotion. The server sends no notation, so the move list is
98
+ written here, from the positions before and after, as chess writes it.
99
+ `neon_chess/online.py` holds the protocol, split into `Table` (the server's
100
+ state, with no I/O) and `Connection` (the WebSocket, on its own thread).
101
+ `online_scenes.py` reuses the computer game's screen with the server's state
102
+ in place of a local game.
103
+
104
+ `tests/test_online.py` runs `web/server.py` in-process on a free port and
105
+ plays it over real sockets. It covers:
106
+
107
+ - **a hundred moves of random play**, promotions to every piece included.
108
+ After every move both clients' boards, castling rights and en passant must
109
+ match a shadow kept with `rules.py`, and the notation must match;
110
+ - checkmate, a promotion to a knight, a resignation, out-of-turn and illegal
111
+ moves, a wrong room code, a player leaving and a dead address;
112
+ - **two complete terminal sessions**, by keys alone: the lobby, a move each
113
+ (black's on the turned board) and a resignation.
114
+
115
+ It never contacts the live server. By hand, the real page in headless Chromium
116
+ played a terminal client both ways through a local `web/server.py`: as white
117
+ for eight moves, and as black for eleven with a capture, a check and both
118
+ kings moving. Every board on both sides matched the position the moves give.
119
+
120
+ What the page gets wrong online, seen in that run:
121
+
122
+ - **The page used to castle across a swept square, and then hang.** Fixed in
123
+ both; see below.
124
+ - **The page's move list online has only your own moves.** It writes a move
125
+ when you make it and nothing when the server reports your opponent's: the
126
+ page as white listed `e4 Nf3 g3 Bh3 O-O`.
127
+ - **The host's START button shows before anyone has joined.** Pressed early,
128
+ it starts nothing, and says nothing.
129
+
130
+ The server's rules, which a prototype can only follow:
131
+
132
+ - **The lobby's clock does nothing.** The host may pick a time control; the
133
+ server stores it and runs no clock, and neither does the page. So this
134
+ client offers none.
135
+ - **Nobody can take a leaver's place.** A player leaving ends the game, and
136
+ the one left is told they win, as on the page; the server records no result.
137
+
138
+ What a prototype has not done yet:
139
+
140
+ - **Nobody has played the live server from a terminal**, only a copy of it run
141
+ locally. The default address is `wss://magmacrunch.duckdns.org/chess`, which
142
+ is what `mpServerFor('chess', 8769)` gives the public site.
143
+ - **No reconnect.** A dropped connection says so, and the game is lost.
144
+ - **No chat, and no watching**, which the browser has.
145
+
146
+ ## It is the browser's game, and the browser's opponent
147
+
148
+ `neon_chess/rules.py` is `web/js/config.js`, `board.js`, `ai.js` and
149
+ `game.js`, ported function for function: the squares searched in the same
150
+ order, the moves listed in the same order, the same evaluation and the same
151
+ alpha-beta, so the computer picks the same move as the page's in every
152
+ position. Two tests hold it there:
153
+
154
+ - **`tests/test_oracle.py`.** `tools/js_oracle.mjs` loads those files
155
+ unmodified and plays two kinds of game through them:
156
+ - **12 games against the computer**, through `Game`'s own API as `main.js`
157
+ drives it: 6 at Easy, 4 at Medium and 2 at Hard. Half have a random player;
158
+ half a careful one, who opens at random and then takes the biggest capture
159
+ it can. The computer won every game that finished.
160
+ - **200 games of random moves for both sides**, through `Board` alone, up to
161
+ 300 moves each. They end in 21 checkmates and 12 stalemates, and cover 360
162
+ promotions to every piece, 15 castlings and en passant.
163
+
164
+ The test replays all 56,000 moves through the port. After each one it
165
+ requires the same board, castling rights, en passant square and check, and
166
+ the same list of legal moves in the page's order. In the computer games the
167
+ game state, turn, move list and result must match too.
168
+ Changing the bishop's value from 330 to 320 fails it, at Easy, move 34.
169
+ - **`tests/test_rules.py`.** It sets up by hand the cases worth pinning,
170
+ including the page's own rules below, so that fixing one shows as a change.
171
+
172
+ The port is quicker than a straight copy. It looks outward from a square for
173
+ an attacker rather than asking every enemy piece, undoes a trial move rather
174
+ than copying the board, and adds up the evaluation tables once. Each gives
175
+ the page's answer, which the oracle checks.
176
+
177
+ ## The computer's promotions, fixed in both
178
+
179
+ **The page's computer would not promote a free pawn, at any level.** Inside its
180
+ search a pawn reaching the last row stayed a pawn: `Board.applyMove` promotes
181
+ only to a piece the move names, and a move names none until it is played. The
182
+ pawn table rates a pawn one row short at 50 and one on the last row at 0, so
183
+ the search preferred to hold a pawn back. With a pawn on h2 and nothing to stop
184
+ it, Easy, Medium and Hard all moved their king instead. Nor could it see your
185
+ promotions coming: with your pawn on b7, Easy played Kg8 and let it queen for
186
+ nothing.
187
+
188
+ Now a promotion tried inside the search is a queen, which is what the computer
189
+ plays when it makes the move. All three levels queen the h2 pawn, and every
190
+ level stops yours, Easy with a check and Medium and Hard with a rook behind it.
191
+ This is fixed in `web/js/ai.js` (`searchMove`) and here (`_search_move`)
192
+ together. `tests/test_oracle.py` asks the page's own `ai.js` both positions at
193
+ every level and requires the port's answers. The `ai.js` from before the fix
194
+ fails that test: it moves its king. In headless Chromium, the fixed page played
195
+ a game opening with no errors, and its `AI.chooseMove` queened the pawn at all
196
+ three levels.
197
+
198
+ ## Castling across an attacked square, fixed in both
199
+
200
+ **The page's bishops, rooks and queens attacked only occupied squares.**
201
+ `getSlidingAttacks` in `board.js` listed the first piece in each direction and
202
+ none of the empty squares before it. Check was unaffected, since a king stands
203
+ on its square, but you or the computer could castle across a square a slider
204
+ swept. `web/server.py` gets this right and refuses that castle.
205
+
206
+ **Online, that hung the game.** The page moves its king before the server
207
+ answers, and the server ignores a move it refuses without a word, so the page
208
+ waited on its opponent for good while the server and the opponent waited on
209
+ it. In headless Chromium against a terminal client, after 1.e4 b6 2.Nf3 Ba6
210
+ 3.g3 Nc6 4.Bh3 Nf6, the page offered O-O across f1, which the bishop on a6
211
+ sweeps. It drew its king on g1 and said "Waiting for opponent...", with the
212
+ server's king still on e1 and white to move.
213
+
214
+ Now `getSlidingAttacks` lists every square up to and including the first
215
+ piece. In the same game the fixed page does not offer the castle. Clicking g1
216
+ leaves it on "Select a piece to move", with its board still the server's and
217
+ white still to move. The port matches it (`Board.attacked`), so against the
218
+ computer the rule is chess's too, and the online client needs no rules of its
219
+ own: `tests/test_server_parity.py` compares the port with the server directly.
220
+ `tests/test_oracle.py` asks the page's own `board.js` for the king's moves with
221
+ a rook sweeping f1, and the old `board.js` fails it by offering O-O.
222
+ `tests/test_rules.py` covers a rook, a bishop and a queen sweeping f1, and a
223
+ knight covering it.
224
+
225
+ ## The page's rules
226
+
227
+ These are the page's own, kept because the oracle requires them. Each is a
228
+ place where the page is not chess:
229
+
230
+ - **The computer's moves are written without their piece.** `formatMove`
231
+ looks for the piece on the square it has just left, so the move list has
232
+ `c6` for `Nc6` and `xe5` for `Bxe5`. Your moves are never disambiguated,
233
+ because it looks for a second piece that could reach the square after yours
234
+ has taken it: two rooks to d1 is written `Rd1`.
235
+ - **There is no draw but stalemate.** No repetition, no fifty-move rule, no
236
+ insufficient material: a king against a king plays on until `R`.
237
+ - **Game over is asked of you first.** After your move, if you would have no
238
+ legal move, the game ends in stalemate, though it is the computer's turn.
239
+
240
+ Two of the page's clicks work differently here. On the page, a second piece
241
+ can only be picked up once the first is put back; here Enter on it picks it
242
+ up. And the page's clock stops while the computer thinks, as above.
243
+
244
+ There is no high-score table, because the page keeps none.
245
+
246
+ ## Licence
247
+
248
+ PolyForm Noncommercial 1.0.0, as for the rest of this repository. See `LICENSE`
249
+ and `NOTICE`.
@@ -0,0 +1,229 @@
1
+ # Chess (terminal)
2
+
3
+ Chess against the computer, in a terminal. It is the second version of the
4
+ game, beside `web/`. It runs on the
5
+ [magmacrunch](https://github.com/magmacrunch-media/magmacrunch) engine's
6
+ terminal backend, either as a cabinet in the magmacrunch arcade or as its own
7
+ command, and plays the same either way.
8
+
9
+ ```
10
+ pipx install magmacrunch-chess
11
+ chess # the title, where the level and clock are set
12
+ chess --play --level hard --clock blitz # straight into a game
13
+ ```
14
+
15
+ For working on it, from this folder:
16
+
17
+ ```
18
+ pip install -e ".[dev]"
19
+ python -m neon_chess
20
+ ```
21
+
22
+ The command is `chess`. The import package is `neon_chess`, after the page's
23
+ Neon Edition, because `chess` is python-chess's and a player may well have it
24
+ installed.
25
+
26
+ ## Keys
27
+
28
+ | key | |
29
+ |---|---|
30
+ | `←` `↑` `↓` `→` | move the cursor |
31
+ | `ENTER` | pick up the piece under the cursor; on a marked square, put it down |
32
+ | `Q` `R` `B` `N` | when a pawn promotes, what it becomes (or `←` `→` and `ENTER`) |
33
+ | `Esc` | put the piece back; with nothing picked up, the title (and from there, the arcade) |
34
+ | `R` | new game, same level and clock |
35
+ | `Q` | quit |
36
+
37
+ Your pieces are cyan capitals, the computer's pink small letters, as notation
38
+ writes them: K Q R B N P. So the two sides stay apart with no colour at all,
39
+ and under `--ascii`. Once a piece is picked up, a green `·` marks every empty
40
+ square it can go to, and a square it can take on is tinted green. The last
41
+ move is tinted yellow and a king in check red. The cursor is bracketed as well
42
+ as shaded, so it shows under `NO_COLOR`. On the title, `←` `→` change the level
43
+ (Easy, Medium, Hard) and the clock (none, Blitz 5, Rapid 15, Classical 30).
44
+ The panel shows the clocks, whose turn it is, and the move list in the page's
45
+ notation.
46
+
47
+ The computer starts thinking 0.3 seconds after your move, as on the page. It
48
+ thinks on a thread of its own, so the screen and the clocks carry on while it
49
+ does: under a second at Easy and Medium, one or two at Hard. The page freezes
50
+ while it thinks, and its clock does too, so there the computer's clock hardly
51
+ moves. Here it runs.
52
+
53
+ Needs a 68x19 terminal.
54
+
55
+ ## Playing people online (a prototype)
56
+
57
+ PLAY ONLINE on the title, or `chess --online`, connects to the arcade's own
58
+ multiplayer server, the one behind the page's PLAY ONLINE button, so a
59
+ terminal can play a browser. Type a name, then press `C` to create a room and
60
+ read its four-letter code to your opponent, or `J` to join one. The first in
61
+ plays white; the host presses Enter to start once both are in. Black sees the
62
+ board from black's side, as on the page. You move as against the computer, a
63
+ promotion asks for its piece, and `X` then `Y` resigns.
64
+
65
+ ```
66
+ pip install "magmacrunch-chess[online]" # websockets, only for this
67
+ chess --online
68
+ chess --online --server ws://127.0.0.1:8769 # a web/server.py on this machine
69
+ ```
70
+
71
+ The server keeps the rules. This client sends the square a piece leaves and
72
+ the square it lands on, and the piece for a promotion straight after, and
73
+ draws the board the server sends back. It works out the legal moves only to
74
+ mark them, with `rules.py`, the page's own rules, which
75
+ `tests/test_server_parity.py` holds to
76
+ the server's own, piece by piece, in over 20,000 positions with castling, en
77
+ passant and promotion. The server sends no notation, so the move list is
78
+ written here, from the positions before and after, as chess writes it.
79
+ `neon_chess/online.py` holds the protocol, split into `Table` (the server's
80
+ state, with no I/O) and `Connection` (the WebSocket, on its own thread).
81
+ `online_scenes.py` reuses the computer game's screen with the server's state
82
+ in place of a local game.
83
+
84
+ `tests/test_online.py` runs `web/server.py` in-process on a free port and
85
+ plays it over real sockets. It covers:
86
+
87
+ - **a hundred moves of random play**, promotions to every piece included.
88
+ After every move both clients' boards, castling rights and en passant must
89
+ match a shadow kept with `rules.py`, and the notation must match;
90
+ - checkmate, a promotion to a knight, a resignation, out-of-turn and illegal
91
+ moves, a wrong room code, a player leaving and a dead address;
92
+ - **two complete terminal sessions**, by keys alone: the lobby, a move each
93
+ (black's on the turned board) and a resignation.
94
+
95
+ It never contacts the live server. By hand, the real page in headless Chromium
96
+ played a terminal client both ways through a local `web/server.py`: as white
97
+ for eight moves, and as black for eleven with a capture, a check and both
98
+ kings moving. Every board on both sides matched the position the moves give.
99
+
100
+ What the page gets wrong online, seen in that run:
101
+
102
+ - **The page used to castle across a swept square, and then hang.** Fixed in
103
+ both; see below.
104
+ - **The page's move list online has only your own moves.** It writes a move
105
+ when you make it and nothing when the server reports your opponent's: the
106
+ page as white listed `e4 Nf3 g3 Bh3 O-O`.
107
+ - **The host's START button shows before anyone has joined.** Pressed early,
108
+ it starts nothing, and says nothing.
109
+
110
+ The server's rules, which a prototype can only follow:
111
+
112
+ - **The lobby's clock does nothing.** The host may pick a time control; the
113
+ server stores it and runs no clock, and neither does the page. So this
114
+ client offers none.
115
+ - **Nobody can take a leaver's place.** A player leaving ends the game, and
116
+ the one left is told they win, as on the page; the server records no result.
117
+
118
+ What a prototype has not done yet:
119
+
120
+ - **Nobody has played the live server from a terminal**, only a copy of it run
121
+ locally. The default address is `wss://magmacrunch.duckdns.org/chess`, which
122
+ is what `mpServerFor('chess', 8769)` gives the public site.
123
+ - **No reconnect.** A dropped connection says so, and the game is lost.
124
+ - **No chat, and no watching**, which the browser has.
125
+
126
+ ## It is the browser's game, and the browser's opponent
127
+
128
+ `neon_chess/rules.py` is `web/js/config.js`, `board.js`, `ai.js` and
129
+ `game.js`, ported function for function: the squares searched in the same
130
+ order, the moves listed in the same order, the same evaluation and the same
131
+ alpha-beta, so the computer picks the same move as the page's in every
132
+ position. Two tests hold it there:
133
+
134
+ - **`tests/test_oracle.py`.** `tools/js_oracle.mjs` loads those files
135
+ unmodified and plays two kinds of game through them:
136
+ - **12 games against the computer**, through `Game`'s own API as `main.js`
137
+ drives it: 6 at Easy, 4 at Medium and 2 at Hard. Half have a random player;
138
+ half a careful one, who opens at random and then takes the biggest capture
139
+ it can. The computer won every game that finished.
140
+ - **200 games of random moves for both sides**, through `Board` alone, up to
141
+ 300 moves each. They end in 21 checkmates and 12 stalemates, and cover 360
142
+ promotions to every piece, 15 castlings and en passant.
143
+
144
+ The test replays all 56,000 moves through the port. After each one it
145
+ requires the same board, castling rights, en passant square and check, and
146
+ the same list of legal moves in the page's order. In the computer games the
147
+ game state, turn, move list and result must match too.
148
+ Changing the bishop's value from 330 to 320 fails it, at Easy, move 34.
149
+ - **`tests/test_rules.py`.** It sets up by hand the cases worth pinning,
150
+ including the page's own rules below, so that fixing one shows as a change.
151
+
152
+ The port is quicker than a straight copy. It looks outward from a square for
153
+ an attacker rather than asking every enemy piece, undoes a trial move rather
154
+ than copying the board, and adds up the evaluation tables once. Each gives
155
+ the page's answer, which the oracle checks.
156
+
157
+ ## The computer's promotions, fixed in both
158
+
159
+ **The page's computer would not promote a free pawn, at any level.** Inside its
160
+ search a pawn reaching the last row stayed a pawn: `Board.applyMove` promotes
161
+ only to a piece the move names, and a move names none until it is played. The
162
+ pawn table rates a pawn one row short at 50 and one on the last row at 0, so
163
+ the search preferred to hold a pawn back. With a pawn on h2 and nothing to stop
164
+ it, Easy, Medium and Hard all moved their king instead. Nor could it see your
165
+ promotions coming: with your pawn on b7, Easy played Kg8 and let it queen for
166
+ nothing.
167
+
168
+ Now a promotion tried inside the search is a queen, which is what the computer
169
+ plays when it makes the move. All three levels queen the h2 pawn, and every
170
+ level stops yours, Easy with a check and Medium and Hard with a rook behind it.
171
+ This is fixed in `web/js/ai.js` (`searchMove`) and here (`_search_move`)
172
+ together. `tests/test_oracle.py` asks the page's own `ai.js` both positions at
173
+ every level and requires the port's answers. The `ai.js` from before the fix
174
+ fails that test: it moves its king. In headless Chromium, the fixed page played
175
+ a game opening with no errors, and its `AI.chooseMove` queened the pawn at all
176
+ three levels.
177
+
178
+ ## Castling across an attacked square, fixed in both
179
+
180
+ **The page's bishops, rooks and queens attacked only occupied squares.**
181
+ `getSlidingAttacks` in `board.js` listed the first piece in each direction and
182
+ none of the empty squares before it. Check was unaffected, since a king stands
183
+ on its square, but you or the computer could castle across a square a slider
184
+ swept. `web/server.py` gets this right and refuses that castle.
185
+
186
+ **Online, that hung the game.** The page moves its king before the server
187
+ answers, and the server ignores a move it refuses without a word, so the page
188
+ waited on its opponent for good while the server and the opponent waited on
189
+ it. In headless Chromium against a terminal client, after 1.e4 b6 2.Nf3 Ba6
190
+ 3.g3 Nc6 4.Bh3 Nf6, the page offered O-O across f1, which the bishop on a6
191
+ sweeps. It drew its king on g1 and said "Waiting for opponent...", with the
192
+ server's king still on e1 and white to move.
193
+
194
+ Now `getSlidingAttacks` lists every square up to and including the first
195
+ piece. In the same game the fixed page does not offer the castle. Clicking g1
196
+ leaves it on "Select a piece to move", with its board still the server's and
197
+ white still to move. The port matches it (`Board.attacked`), so against the
198
+ computer the rule is chess's too, and the online client needs no rules of its
199
+ own: `tests/test_server_parity.py` compares the port with the server directly.
200
+ `tests/test_oracle.py` asks the page's own `board.js` for the king's moves with
201
+ a rook sweeping f1, and the old `board.js` fails it by offering O-O.
202
+ `tests/test_rules.py` covers a rook, a bishop and a queen sweeping f1, and a
203
+ knight covering it.
204
+
205
+ ## The page's rules
206
+
207
+ These are the page's own, kept because the oracle requires them. Each is a
208
+ place where the page is not chess:
209
+
210
+ - **The computer's moves are written without their piece.** `formatMove`
211
+ looks for the piece on the square it has just left, so the move list has
212
+ `c6` for `Nc6` and `xe5` for `Bxe5`. Your moves are never disambiguated,
213
+ because it looks for a second piece that could reach the square after yours
214
+ has taken it: two rooks to d1 is written `Rd1`.
215
+ - **There is no draw but stalemate.** No repetition, no fifty-move rule, no
216
+ insufficient material: a king against a king plays on until `R`.
217
+ - **Game over is asked of you first.** After your move, if you would have no
218
+ legal move, the game ends in stalemate, though it is the computer's turn.
219
+
220
+ Two of the page's clicks work differently here. On the page, a second piece
221
+ can only be picked up once the first is put back; here Enter on it picks it
222
+ up. And the page's clock stops while the computer thinks, as above.
223
+
224
+ There is no high-score table, because the page keeps none.
225
+
226
+ ## Licence
227
+
228
+ PolyForm Noncommercial 1.0.0, as for the rest of this repository. See `LICENSE`
229
+ and `NOTICE`.
File without changes
@@ -0,0 +1,37 @@
1
+ """``python -m neon_chess`` -- play chess against the computer in a terminal."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+
7
+ from neon_chess import rules as R
8
+
9
+
10
+ def main() -> None:
11
+ parser = argparse.ArgumentParser(prog="chess", description="Chess against the computer.")
12
+ parser.add_argument("--play", action="store_true", help="skip the title screen")
13
+ parser.add_argument("--level", choices=list(R.DIFFICULTY), default="medium",
14
+ help="how deep the computer searches: easy 2, medium 3, hard 4 moves")
15
+ parser.add_argument("--clock", choices=list(R.TIME_CONTROLS), default="none",
16
+ help="minutes each: blitz 5, rapid 15, classical 30")
17
+ parser.add_argument("--online", action="store_true",
18
+ help="go straight to PLAY ONLINE (needs the [online] extra)")
19
+ parser.add_argument("--server", default=None,
20
+ help="the multiplayer server for PLAY ONLINE, e.g. "
21
+ "ws://127.0.0.1:8769 for a web/server.py on this machine")
22
+ parser.add_argument(
23
+ "--ascii", action="store_true", dest="ascii_only",
24
+ help="draw with plain ASCII instead of block and box glyphs. Detected "
25
+ "automatically from the terminal's encoding; this forces it. "
26
+ "MAGMACRUNCH_ASCII=1 says the same for every cabinet.",
27
+ )
28
+ args = parser.parse_args()
29
+
30
+ from neon_chess.app import run
31
+
32
+ run(args.play, args.ascii_only, level=args.level, clock=args.clock,
33
+ server=args.server, online=args.online)
34
+
35
+
36
+ if __name__ == "__main__":
37
+ main()