Metadata-Version: 2.5
Name: magmacrunch-chess
Version: 0.1.0
Summary: Chess against the computer. Terminal version.
Project-URL: Homepage, https://magmacrunch.com/arcade/chess/
Author: magmacrunch media
License-Expression: PolyForm-Noncommercial-1.0.0
License-File: LICENSE
License-File: NOTICE
Keywords: arcade,board,chess,games,terminal,tui
Requires-Python: >=3.10
Requires-Dist: magmacrunch>=0.7.0
Provides-Extra: dev
Requires-Dist: pytest>=7.0; extra == 'dev'
Requires-Dist: ruff>=0.6; extra == 'dev'
Requires-Dist: websockets>=14.0; extra == 'dev'
Provides-Extra: online
Requires-Dist: websockets>=14.0; extra == 'online'
Description-Content-Type: text/markdown

# Chess (terminal)

Chess against the computer, in a terminal. It is the second version of the
game, beside `web/`. It runs on the
[magmacrunch](https://github.com/magmacrunch-media/magmacrunch) engine's
terminal backend, either as a cabinet in the magmacrunch arcade or as its own
command, and plays the same either way.

```
pipx install magmacrunch-chess
chess                                   # the title, where the level and clock are set
chess --play --level hard --clock blitz # straight into a game
```

For working on it, from this folder:

```
pip install -e ".[dev]"
python -m neon_chess
```

The command is `chess`. The import package is `neon_chess`, after the page's
Neon Edition, because `chess` is python-chess's and a player may well have it
installed.

## Keys

| key | |
|---|---|
| `←` `↑` `↓` `→` | move the cursor |
| `ENTER` | pick up the piece under the cursor; on a marked square, put it down |
| `Q` `R` `B` `N` | when a pawn promotes, what it becomes (or `←` `→` and `ENTER`) |
| `Esc` | put the piece back; with nothing picked up, the title (and from there, the arcade) |
| `R` | new game, same level and clock |
| `Q` | quit |

Your pieces are cyan capitals, the computer's pink small letters, as notation
writes them: K Q R B N P. So the two sides stay apart with no colour at all,
and under `--ascii`. Once a piece is picked up, a green `·` marks every empty
square it can go to, and a square it can take on is tinted green. The last
move is tinted yellow and a king in check red. The cursor is bracketed as well
as shaded, so it shows under `NO_COLOR`. On the title, `←` `→` change the level
(Easy, Medium, Hard) and the clock (none, Blitz 5, Rapid 15, Classical 30).
The panel shows the clocks, whose turn it is, and the move list in the page's
notation.

The computer starts thinking 0.3 seconds after your move, as on the page. It
thinks on a thread of its own, so the screen and the clocks carry on while it
does: under a second at Easy and Medium, one or two at Hard. The page freezes
while it thinks, and its clock does too, so there the computer's clock hardly
moves. Here it runs.

Needs a 68x19 terminal.

## Playing people online (a prototype)

PLAY ONLINE on the title, or `chess --online`, connects to the arcade's own
multiplayer server, the one behind the page's PLAY ONLINE button, so a
terminal can play a browser. Type a name, then press `C` to create a room and
read its four-letter code to your opponent, or `J` to join one. The first in
plays white; the host presses Enter to start once both are in. Black sees the
board from black's side, as on the page. You move as against the computer, a
promotion asks for its piece, and `X` then `Y` resigns.

```
pip install "magmacrunch-chess[online]"     # websockets, only for this
chess --online
chess --online --server ws://127.0.0.1:8769  # a web/server.py on this machine
```

The server keeps the rules. This client sends the square a piece leaves and
the square it lands on, and the piece for a promotion straight after, and
draws the board the server sends back. It works out the legal moves only to
mark them, with `rules.py`, the page's own rules, which
`tests/test_server_parity.py` holds to
the server's own, piece by piece, in over 20,000 positions with castling, en
passant and promotion. The server sends no notation, so the move list is
written here, from the positions before and after, as chess writes it.
`neon_chess/online.py` holds the protocol, split into `Table` (the server's
state, with no I/O) and `Connection` (the WebSocket, on its own thread).
`online_scenes.py` reuses the computer game's screen with the server's state
in place of a local game.

`tests/test_online.py` runs `web/server.py` in-process on a free port and
plays it over real sockets. It covers:

- **a hundred moves of random play**, promotions to every piece included.
  After every move both clients' boards, castling rights and en passant must
  match a shadow kept with `rules.py`, and the notation must match;
- checkmate, a promotion to a knight, a resignation, out-of-turn and illegal
  moves, a wrong room code, a player leaving and a dead address;
- **two complete terminal sessions**, by keys alone: the lobby, a move each
  (black's on the turned board) and a resignation.

It never contacts the live server. By hand, the real page in headless Chromium
played a terminal client both ways through a local `web/server.py`: as white
for eight moves, and as black for eleven with a capture, a check and both
kings moving. Every board on both sides matched the position the moves give.

What the page gets wrong online, seen in that run:

- **The page used to castle across a swept square, and then hang.** Fixed in
  both; see below.
- **The page's move list online has only your own moves.** It writes a move
  when you make it and nothing when the server reports your opponent's: the
  page as white listed `e4 Nf3 g3 Bh3 O-O`.
- **The host's START button shows before anyone has joined.** Pressed early,
  it starts nothing, and says nothing.

The server's rules, which a prototype can only follow:

- **The lobby's clock does nothing.** The host may pick a time control; the
  server stores it and runs no clock, and neither does the page. So this
  client offers none.
- **Nobody can take a leaver's place.** A player leaving ends the game, and
  the one left is told they win, as on the page; the server records no result.

What a prototype has not done yet:

- **Nobody has played the live server from a terminal**, only a copy of it run
  locally. The default address is `wss://magmacrunch.duckdns.org/chess`, which
  is what `mpServerFor('chess', 8769)` gives the public site.
- **No reconnect.** A dropped connection says so, and the game is lost.
- **No chat, and no watching**, which the browser has.

## It is the browser's game, and the browser's opponent

`neon_chess/rules.py` is `web/js/config.js`, `board.js`, `ai.js` and
`game.js`, ported function for function: the squares searched in the same
order, the moves listed in the same order, the same evaluation and the same
alpha-beta, so the computer picks the same move as the page's in every
position. Two tests hold it there:

- **`tests/test_oracle.py`.** `tools/js_oracle.mjs` loads those files
  unmodified and plays two kinds of game through them:
  - **12 games against the computer**, through `Game`'s own API as `main.js`
    drives it: 6 at Easy, 4 at Medium and 2 at Hard. Half have a random player;
    half a careful one, who opens at random and then takes the biggest capture
    it can. The computer won every game that finished.
  - **200 games of random moves for both sides**, through `Board` alone, up to
    300 moves each. They end in 21 checkmates and 12 stalemates, and cover 360
    promotions to every piece, 15 castlings and en passant.

  The test replays all 56,000 moves through the port. After each one it
  requires the same board, castling rights, en passant square and check, and
  the same list of legal moves in the page's order. In the computer games the
  game state, turn, move list and result must match too.
  Changing the bishop's value from 330 to 320 fails it, at Easy, move 34.
- **`tests/test_rules.py`.** It sets up by hand the cases worth pinning,
  including the page's own rules below, so that fixing one shows as a change.

The port is quicker than a straight copy. It looks outward from a square for
an attacker rather than asking every enemy piece, undoes a trial move rather
than copying the board, and adds up the evaluation tables once. Each gives
the page's answer, which the oracle checks.

## The computer's promotions, fixed in both

**The page's computer would not promote a free pawn, at any level.** Inside its
search a pawn reaching the last row stayed a pawn: `Board.applyMove` promotes
only to a piece the move names, and a move names none until it is played. The
pawn table rates a pawn one row short at 50 and one on the last row at 0, so
the search preferred to hold a pawn back. With a pawn on h2 and nothing to stop
it, Easy, Medium and Hard all moved their king instead. Nor could it see your
promotions coming: with your pawn on b7, Easy played Kg8 and let it queen for
nothing.

Now a promotion tried inside the search is a queen, which is what the computer
plays when it makes the move. All three levels queen the h2 pawn, and every
level stops yours, Easy with a check and Medium and Hard with a rook behind it.
This is fixed in `web/js/ai.js` (`searchMove`) and here (`_search_move`)
together. `tests/test_oracle.py` asks the page's own `ai.js` both positions at
every level and requires the port's answers. The `ai.js` from before the fix
fails that test: it moves its king. In headless Chromium, the fixed page played
a game opening with no errors, and its `AI.chooseMove` queened the pawn at all
three levels.

## Castling across an attacked square, fixed in both

**The page's bishops, rooks and queens attacked only occupied squares.**
`getSlidingAttacks` in `board.js` listed the first piece in each direction and
none of the empty squares before it. Check was unaffected, since a king stands
on its square, but you or the computer could castle across a square a slider
swept. `web/server.py` gets this right and refuses that castle.

**Online, that hung the game.** The page moves its king before the server
answers, and the server ignores a move it refuses without a word, so the page
waited on its opponent for good while the server and the opponent waited on
it. In headless Chromium against a terminal client, after 1.e4 b6 2.Nf3 Ba6
3.g3 Nc6 4.Bh3 Nf6, the page offered O-O across f1, which the bishop on a6
sweeps. It drew its king on g1 and said "Waiting for opponent...", with the
server's king still on e1 and white to move.

Now `getSlidingAttacks` lists every square up to and including the first
piece. In the same game the fixed page does not offer the castle. Clicking g1
leaves it on "Select a piece to move", with its board still the server's and
white still to move. The port matches it (`Board.attacked`), so against the
computer the rule is chess's too, and the online client needs no rules of its
own: `tests/test_server_parity.py` compares the port with the server directly.
`tests/test_oracle.py` asks the page's own `board.js` for the king's moves with
a rook sweeping f1, and the old `board.js` fails it by offering O-O.
`tests/test_rules.py` covers a rook, a bishop and a queen sweeping f1, and a
knight covering it.

## The page's rules

These are the page's own, kept because the oracle requires them. Each is a
place where the page is not chess:

- **The computer's moves are written without their piece.** `formatMove`
  looks for the piece on the square it has just left, so the move list has
  `c6` for `Nc6` and `xe5` for `Bxe5`. Your moves are never disambiguated,
  because it looks for a second piece that could reach the square after yours
  has taken it: two rooks to d1 is written `Rd1`.
- **There is no draw but stalemate.** No repetition, no fifty-move rule, no
  insufficient material: a king against a king plays on until `R`.
- **Game over is asked of you first.** After your move, if you would have no
  legal move, the game ends in stalemate, though it is the computer's turn.

Two of the page's clicks work differently here. On the page, a second piece
can only be picked up once the first is put back; here Enter on it picks it
up. And the page's clock stops while the computer thinks, as above.

There is no high-score table, because the page keeps none.

## Licence

PolyForm Noncommercial 1.0.0, as for the rest of this repository. See `LICENSE`
and `NOTICE`.
