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.
- magmacrunch_chess-0.1.0/.gitignore +9 -0
- magmacrunch_chess-0.1.0/LICENSE +75 -0
- magmacrunch_chess-0.1.0/NOTICE +30 -0
- magmacrunch_chess-0.1.0/PKG-INFO +249 -0
- magmacrunch_chess-0.1.0/README.md +229 -0
- magmacrunch_chess-0.1.0/neon_chess/__init__.py +0 -0
- magmacrunch_chess-0.1.0/neon_chess/__main__.py +37 -0
- magmacrunch_chess-0.1.0/neon_chess/app.py +72 -0
- magmacrunch_chess-0.1.0/neon_chess/arcade.py +39 -0
- magmacrunch_chess-0.1.0/neon_chess/online.py +386 -0
- magmacrunch_chess-0.1.0/neon_chess/online_scenes.py +407 -0
- magmacrunch_chess-0.1.0/neon_chess/rules.py +795 -0
- magmacrunch_chess-0.1.0/neon_chess/scenes.py +580 -0
- magmacrunch_chess-0.1.0/neon_chess/theme.py +56 -0
- magmacrunch_chess-0.1.0/pyproject.toml +50 -0
- magmacrunch_chess-0.1.0/tests/conftest.py +51 -0
- magmacrunch_chess-0.1.0/tests/test_app.py +346 -0
- magmacrunch_chess-0.1.0/tests/test_online.py +323 -0
- magmacrunch_chess-0.1.0/tests/test_oracle.py +142 -0
- magmacrunch_chess-0.1.0/tests/test_rules.py +182 -0
- magmacrunch_chess-0.1.0/tests/test_server_parity.py +112 -0
- magmacrunch_chess-0.1.0/tools/js_oracle.mjs +214 -0
|
@@ -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()
|