chesspal 0.1.0 → 0.2.1

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.
Files changed (211) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +18 -3
  81. package/dist/core/review.js +52 -9
  82. package/dist/games/ai-model.d.ts +4 -0
  83. package/dist/games/ai-model.js +9 -0
  84. package/dist/games/analysis.d.ts +1 -1
  85. package/dist/games/archive.d.ts +25 -4
  86. package/dist/games/archive.js +99 -25
  87. package/dist/games/coach.d.ts +57 -0
  88. package/dist/games/coach.js +144 -0
  89. package/dist/games/current-game.d.ts +2 -0
  90. package/dist/games/current-game.js +7 -1
  91. package/dist/games/files.js +1 -0
  92. package/dist/games/index.d.ts +5 -4
  93. package/dist/games/index.js +5 -4
  94. package/dist/games/talk.d.ts +1 -5
  95. package/dist/games/talk.js +14 -14
  96. package/dist/home/open-folder.js +1 -1
  97. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  98. package/dist/host-files/agents.js +53 -0
  99. package/dist/host-files/host-files.d.ts +7 -2
  100. package/dist/host-files/host-files.js +27 -12
  101. package/dist/host-files/index.d.ts +2 -2
  102. package/dist/host-files/index.js +3 -3
  103. package/dist/index.d.ts +1 -3
  104. package/dist/index.js +1 -0
  105. package/dist/page/app/anchor.js +88 -0
  106. package/dist/page/{app.js → app/app.js} +116 -231
  107. package/dist/page/app/keys.js +94 -0
  108. package/dist/page/app/phase.js +30 -0
  109. package/dist/page/app/served.js +73 -0
  110. package/dist/page/avatars/avatars.js +19 -0
  111. package/dist/page/avatars/thought.js +10 -0
  112. package/dist/page/base.css +160 -0
  113. package/dist/page/board/board.css +959 -0
  114. package/dist/page/board/board.js +125 -0
  115. package/dist/page/{explain.js → board/explain.js} +1 -1
  116. package/dist/page/board/marks.js +160 -0
  117. package/dist/page/board/motion.js +71 -0
  118. package/dist/page/board/pointer.js +239 -0
  119. package/dist/page/board/squares.js +26 -0
  120. package/dist/page/dialogs/analyses.js +195 -0
  121. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  122. package/dist/page/dialogs/dialogs.css +1228 -0
  123. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  124. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  125. package/dist/page/dialogs/search.js +31 -0
  126. package/dist/page/dialogs/settings.js +42 -0
  127. package/dist/page/dialogs/studied.js +39 -0
  128. package/dist/page/dialogs/study.js +51 -0
  129. package/dist/page/{badges.js → game/badges.js} +1 -1
  130. package/dist/page/{browse.js → game/browse.js} +1 -1
  131. package/dist/page/game/card-fold.js +40 -0
  132. package/dist/page/game/column.js +55 -0
  133. package/dist/page/{ending.js → game/ending.js} +2 -2
  134. package/dist/page/game/game.css +577 -0
  135. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  136. package/dist/page/{plies.js → game/plies.js} +30 -10
  137. package/dist/page/game/result-tile.js +14 -0
  138. package/dist/page/helps/helps.css +689 -0
  139. package/dist/page/helps/helps.js +220 -0
  140. package/dist/page/helps/view.js +127 -0
  141. package/dist/page/layout/fold-tab.js +11 -0
  142. package/dist/page/{folds.js → layout/folds.js} +1 -1
  143. package/dist/page/{header.js → layout/header.js} +5 -4
  144. package/dist/page/layout/layout.css +834 -0
  145. package/dist/page/lead/lead.js +82 -0
  146. package/dist/page/lead/view.js +38 -0
  147. package/dist/page/main.js +2 -2
  148. package/dist/page/pieces/CREDITS.md +12 -29
  149. package/dist/page/pieces/capablanca-small.svg +20 -0
  150. package/dist/page/pieces/capablanca.svg +22 -0
  151. package/dist/page/pieces/nitida-small.svg +3 -2
  152. package/dist/page/pieces/nitida.svg +3 -2
  153. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  154. package/dist/page/pieces/torretta-small.svg +15 -14
  155. package/dist/page/pieces/torretta.svg +17 -15
  156. package/dist/page/{player-card.js → players/card.js} +2 -6
  157. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  158. package/dist/page/{profile.js → profile/profile.js} +9 -5
  159. package/dist/page/{settings.js → profile/settings.js} +5 -4
  160. package/dist/page/style.css +9 -4398
  161. package/dist/page/talk/talk.css +958 -0
  162. package/dist/page/{talk.js → talk/talk.js} +39 -46
  163. package/dist/page/text.js +137 -19
  164. package/dist/page/theme.css +292 -0
  165. package/dist/profiles/index.d.ts +1 -1
  166. package/dist/profiles/profiles.d.ts +15 -4
  167. package/dist/profiles/profiles.js +24 -7
  168. package/dist/server/bell.d.ts +10 -2
  169. package/dist/server/bell.js +28 -12
  170. package/dist/server/caller.d.ts +14 -3
  171. package/dist/server/caller.js +44 -11
  172. package/dist/server/mcp.d.ts +2 -3
  173. package/dist/server/mcp.js +53 -19
  174. package/dist/server/page/analyses.d.ts +23 -0
  175. package/dist/server/page/analyses.js +65 -0
  176. package/dist/server/page/files.d.ts +5 -0
  177. package/dist/server/page/files.js +92 -0
  178. package/dist/server/page/game.d.ts +6 -0
  179. package/dist/server/page/game.js +381 -0
  180. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  181. package/dist/server/{http.js → page/http.js} +11 -0
  182. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  183. package/dist/server/page/page.js +229 -0
  184. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  185. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  186. package/instructions/analysis.md +4 -0
  187. package/instructions/analyst-agent.md +8 -0
  188. package/instructions/analyst.md +3 -7
  189. package/instructions/coach.md +11 -0
  190. package/instructions/opponent.md +4 -4
  191. package/package.json +1 -1
  192. package/dist/host-files/ponder-agents.js +0 -40
  193. package/dist/page/avatars.js +0 -16
  194. package/dist/page/board.js +0 -566
  195. package/dist/page/fold-tab.js +0 -9
  196. package/dist/page/game-column.js +0 -44
  197. package/dist/page/helps-view.js +0 -51
  198. package/dist/page/helps.js +0 -90
  199. package/dist/page/pieces/cburnett-small.svg +0 -20
  200. package/dist/page/pieces/cburnett.svg +0 -20
  201. package/dist/page/settings-panel.js +0 -32
  202. package/dist/page/studied.js +0 -14
  203. package/dist/page/study-dialog.js +0 -58
  204. package/dist/server/analyses.d.ts +0 -17
  205. package/dist/server/analyses.js +0 -46
  206. package/dist/server/page.js +0 -657
  207. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  208. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  209. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  210. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  211. /package/dist/page/{voice.js → talk/voice.js} +0 -0
package/README.md CHANGED
@@ -1,99 +1,71 @@
1
1
  # Chesspal
2
2
 
3
- Play chess against an AI, or learn with one.
3
+ Play chess against an AI on a real board, or learn with one.
4
4
 
5
- You can already play chess with an AI in a chat, but it forgets the position, plays illegal moves, sees no board and remembers nothing of yesterday's
6
- game. Chesspal is what it lacks: it **keeps the rules, the board, the clock, the games and the memory**; the AI **plays, reasons, judges and talks**. A
7
- move is legal because Chesspal checked it, not because the AI says so.
5
+ ![The board page during a game: the AI's words on the left, the board in the middle, the opening's name and the moves on the right](https://raw.githubusercontent.com/stefanocaronia/chesspal/main/docs/screenshot.png)
8
6
 
9
- Chesspal does not use a chess engine such as Stockfish, in any way: every judgment of a move or a position is the AI's own. Chesspal hands it facts
10
- instead, which a program finds in microseconds and an AI only by reasoning at length: the legal moves, the pieces attacked and defended, the pins and
11
- the checks, the positional facts, the opening's name.
7
+ ## Get started
12
8
 
13
- ## What you can do
9
+ You need [Node.js](https://nodejs.org) 22.13 or newer and an AI agent on your computer, [Claude Code](https://code.claude.com/docs/en/setup) or [Codex](https://developers.openai.com/codex): see [what you need](#what-you-need). Open a new terminal, after installing Node.js (one opened before does not find `npm`), and type:
14
10
 
15
- - **Play against the AI** on the board page in your browser, and talk with it there or in the chat.
16
- - **Choose its level**, from beginner to master: the higher, the more facts of the position the AI sees, and the master knows the opening book.
17
- - **Play with a clock**, with an AI that knows how much time it has left; offer a draw, resign, export the game as PGN.
18
- - **Keep a profile** for each player at the same computer, with their games.
19
-
20
- Coming later: an AI coach that helps you grow, the discursive analysis of your games, exercises chosen on your weak points, a rating worked out from your
21
- games, and an opponent with a character and moods of its own.
22
-
23
- ## Status
24
-
25
- Chesspal is in early development: the version on npm is a first one to try, and it may change from one release to the next. It works with any AI agent that uses MCP tools and can work with the files on your computer;
26
- we develop it with Claude Code, and Codex is connected too. Chesspal never uses an API key: the AI runs on your agent's own plan, or on your computer
27
- with a local model.
28
-
29
- ## Play
30
-
31
- **What you need**
32
-
33
- - [Node.js](https://nodejs.org) 22.13 or newer: download the version marked LTS and install it like any program.
34
- - An AI agent on your computer: [Claude Code](https://code.claude.com/docs/en/setup), in a terminal or in the Code tab of the Claude desktop app, or
35
- [Codex](https://developers.openai.com/codex). The AI runs on what you already pay for your agent, such as your Claude or ChatGPT subscription:
36
- Chesspal asks for no API key.
37
- - A terminal, opened after installing Node.js: on Windows, Command Prompt (press Start, type `cmd`, press Enter), since PowerShell refuses npm's
38
- commands until its scripts are allowed (there, `npm.cmd` and `chesspal.cmd` work in their place); on a Mac, Terminal, in Applications › Utilities.
39
-
40
- **Your first game**
11
+ ```bash
12
+ npm install -g chesspal
13
+ chesspal init chess
14
+ cd chess
15
+ claude
16
+ ```
41
17
 
42
- 1. **Install Chesspal**:
18
+ - `npm install -g` installs the `chesspal` command for the whole computer, where your agent finds it.
19
+ - `chesspal init chess` makes the folder `chess` to play in. Any name will do; your games are kept [elsewhere](#where-your-games-are).
20
+ - `claude` opens Claude Code in that folder (in the Claude desktop app, choose the folder in the Code tab). With Codex, type `codex`, and when it asks about Chesspal's two hooks, choose **Trust all and continue**.
21
+ - The first time, your agent asks whether you trust the folder: say yes.
43
22
 
44
- ```bash
45
- npm install -g chesspal
46
- ```
23
+ Then **say hello**. The AI answers with the link to the board page: open it in your browser, tell it who you are, and ask for a game. You choose your colour, the clock and the AI's level, and play on the board.
47
24
 
48
- 2. **Make a folder to play in**: this makes the folder `chess`, with the few files that tell your agent where Chesspal is and how to play. Any name
49
- will do; your games are not kept there but in [one folder for all](#where-your-games-are).
25
+ On Windows, use Command Prompt (press Start, type `cmd`, press Enter). PowerShell, on a new computer, refuses npm's commands with "running scripts is disabled on this system"; there, type `Set-ExecutionPolicy -Scope CurrentUser RemoteSigned` once and answer yes, or use Command Prompt. Codex on Windows also offers to set up its sandbox the first time: choose the first option, and answer yes when Windows asks for permission.
50
26
 
51
- ```bash
52
- chesspal init chess
53
- ```
27
+ ## What it does
54
28
 
55
- 3. **Open your agent in that folder**: type `cd chess`, which takes the terminal into it, then `claude` or `codex`; in the Claude desktop app, choose
56
- the folder in the Code tab. The first time, it asks whether to trust the folder: say yes. In Codex, also type `/hooks` and allow Chesspal's two
57
- hooks: small helpers that let it notice your moves on the board page.
29
+ An AI in a plain chat forgets the position, plays illegal moves and sees no board. Chesspal is what it lacks: it **keeps the rules, the board, the clock and your games**; the AI **plays, reasons and talks**. Every move is checked by Chesspal, and no chess engine plays for the AI: every judgment is its own, made from the facts Chesspal hands it, such as the legal moves, the pieces attacked and defended, the pins and the opening's name.
58
30
 
59
- 4. **Say hello**: the AI gives you the link to the board page, a page on your own computer; open it in your browser. No link? Ask the AI for the
60
- board. The first time, the page asks who you are: your name, a picture, how well you play.
31
+ - **Play on a board** in your browser, and talk with the AI there or in the chat.
32
+ - **Choose its level**, from beginner to master: the higher, the more of the position the AI sees.
33
+ - **Play with a clock**, against an AI that knows how much time it has left; offer a draw, resign, save the game as PGN.
34
+ - **Go over a game** with the AI when it is over: its notes show on the board page, in Analyses. Games from other sites come in as PGN files.
35
+ - **Keep a profile** for each player at the same computer, with their games.
61
36
 
62
- 5. **Ask for a game**: the New game window opens on the page, where you choose your colour, the clock and the AI's level. Play on the board: that is
63
- where the game is. A move written in the chat counts too, `e4`, `Nf3`, in the international notation (K Q R B N in every language), or in words.
37
+ Coming later: an AI coach that helps you grow, exercises chosen on your weak points, a rating worked out from your games, an opponent with a character and moods of its own, and local models.
64
38
 
65
- 6. **For the next game**, type `/clear` in the chat, or start a new chat: a long chat makes the AI slower and costs you more.
39
+ ## What you need
66
40
 
67
- When a game is over, ask the AI to go over it with you: its notes show on the board page, in Analyses. A game you played on another site comes in
68
- there too: download it from that site as a PGN file and open it in Analyses.
41
+ - **Node.js 22.13** or newer: download the version marked LTS and install it like any program.
42
+ - **An AI agent** with your own subscription: Claude Code, with a paid Claude plan, or Codex, with a ChatGPT account. Chesspal never uses an API key, which is paid by the call: sign your agent in with your account.
43
+ - **A web browser**, for the board page.
69
44
 
70
- **To update Chesspal**, close the chat, run `npm install -g chesspal` again, then `chesspal init` in your folder: it updates its own lines and keeps
71
- what you wrote there. In Codex, when `chesspal init` says so, allow Chesspal's hooks again in `/hooks`.
45
+ Chesspal works with any agent that supports MCP tools and can work with the files on your computer: we develop it with Claude Code, and Codex is connected too.
72
46
 
73
- ## How it works
47
+ ## Good to know
74
48
 
75
- Chesspal is an MCP server, a command line, `chesspal`, and a board page, all made from the same commands. `chesspal init` sets up a folder to play in:
76
- the few files that tell the AI agent where Chesspal is, and its instructions as an opponent. Your profiles, games and analyses live in one folder for
77
- every folder you play in ([where your games are](#where-your-games-are)), or in the folder `CHESSPAL_HOME` names, for tests. Only Chesspal's commands
78
- change them: they check every move with [chess.js](https://github.com/jhlywa/chess.js) and write atomically.
49
+ - **For the next game**, type `/clear` in the chat or start a new one: a long chat makes the AI slower and costs you more.
50
+ - **A move written in the chat** counts too: `e4`, `Nf3`, in the international notation (K Q R B N in every language), or in words.
51
+ - **To update**, close the chat, run `npm install -g chesspal` again, then `chesspal init` in your folder: it updates its own lines and keeps what you wrote there. In Codex, allow the hooks again when it says so.
79
52
 
80
53
  ## Where your games are
81
54
 
82
- Your players and their games are in one folder, the same whatever folder you open the chat in:
55
+ In one folder for every folder you play in:
56
+
57
+ - Windows: `Documents › My Games › Chesspal`, inside OneDrive if you use it;
58
+ - macOS: `Documents › Chesspal`; Linux: `Chesspal` in your Documents folder.
83
59
 
84
- - on Windows, `Documents › My Games › Chesspal`. Explorer may show Documents in your language, such as Documenti. With OneDrive, it is inside
85
- OneDrive, which also keeps a copy online;
86
- - on macOS, `Documents › Chesspal`; on Linux, `Chesspal` in your Documents folder, whatever its name in your language.
60
+ The board page shows where, and its **Open the folder** button opens it. To back up your games, copy that whole folder; to move them to another computer, put the copy in the same place there. Do it with the chat and the board page closed.
87
61
 
88
- The board page says where it is, on its welcome page and in "Who's playing?", and its **Open the folder** button opens it.
62
+ ## Try it, and tell us
89
63
 
90
- **A backup** is a copy of the whole `Chesspal` folder, on a USB stick for example. **To move to another computer**, install Chesspal there and put the
91
- copied `Chesspal` folder in the same place, replacing the one Chesspal may have made already: your players, their games and a game left unfinished all
92
- come with it. Copy it with the chat and the board page closed.
64
+ Chesspal is in early development: the version on npm is a first one to try, and it may change from one release to the next. When something breaks, confuses you or could work better, open an [issue on GitHub](https://github.com/stefanocaronia/chesspal/issues): what you did, what you expected, what happened.
93
65
 
94
66
  ## Development
95
67
 
96
- You need Node.js 22.13 or newer.
68
+ Chesspal is an MCP server, a command line and a board page, all made from the same commands, which check every move with [chess.js](https://github.com/jhlywa/chess.js) and write the games atomically.
97
69
 
98
70
  ```bash
99
71
  git clone https://github.com/stefanocaronia/chesspal.git
@@ -103,28 +75,19 @@ npm run build
103
75
  npm test
104
76
  ```
105
77
 
106
- - `npm run lint` checks the formatting and the types, as CI does; `npm run format` formats the code.
107
- - `npm run page -- <folder>` serves the board page of a player's folder, with the data of `CHESSPAL_HOME` or Documents, to look at it while it is built.
108
- - `npm run endings` shows the ends of a game on the board: checkmate, resignation, a flag fallen, the draws.
109
- - `npm run bench` times the commands against their budgets.
110
- - `npm run smoke`, after the build, packs the package, installs it in a temporary folder and runs it as a player would: its version, `chesspal init`
111
- and its MCP server, as CI does.
112
-
113
- [AGENTS.md](AGENTS.md) is the guide for development: the layout, the architecture's rules, performance, the coding guidelines. It is written for AI
114
- coding agents and people alike.
78
+ [The guide for development](https://github.com/stefanocaronia/chesspal/blob/main/AGENTS.md), for people and AI coding agents alike, holds the other scripts, the layout, the architecture's rules and the coding guidelines.
115
79
 
116
80
  ## Credits
117
81
 
118
82
  - The rules: [chess.js](https://github.com/jhlywa/chess.js), under the BSD 2-Clause license.
119
83
  - The openings' names: Lichess's [chess-openings](https://github.com/lichess-org/chess-openings), under CC0.
120
- - The pieces: the Torretta and Nitida sets are Chesspal's own; cburnett is by Colin M.L. Burnett, under the BSD 3-Clause license.
121
- [The piece sets' credits](src/page/pieces/CREDITS.md) name every author, licence and source.
84
+ - The pieces: Nitida, Torretta and Capablanca are Chesspal's own, the last after the diagrams of Capablanca's *Chess Fundamentals* (1921), in the public domain; they are free to use under CC BY 4.0, crediting "Stefano Caronia, Chesspal". [The piece sets' credits](https://github.com/stefanocaronia/chesspal/blob/main/src/page/pieces/CREDITS.md).
122
85
 
123
86
  Chesspal ships only original or open-licensed content, and no GPL dependency.
124
87
 
125
88
  ## License
126
89
 
127
- [MIT](LICENSE).
90
+ [MIT](https://github.com/stefanocaronia/chesspal/blob/main/LICENSE).
128
91
 
129
92
  ## Development notes
130
93
 
@@ -136,4 +99,4 @@ Fight corporate control of technology, not our right to use it.
136
99
 
137
100
  ---
138
101
 
139
- © 2026 [Stefano Caronia](https://stefanocaronia.it) · [GitHub](https://github.com/stefanocaronia/chesspal)
102
+ © 2026 [Stefano Caronia](https://stefanocaronia.it) · [GitHub](https://github.com/stefanocaronia/chesspal) · [npm](https://www.npmjs.com/package/chesspal)
@@ -1,4 +1,4 @@
1
- // `chesspal chat`, the SessionStart hook of Claude Code and Codex (D-57, T-104): it writes how the chat started, new, cleared, resumed or compacted, for new_game to read.
1
+ // `chesspal chat`, the SessionStart hook of Claude Code and Codex (D-57): it writes how the chat started, new, cleared, resumed or compacted, for new_game to read.
2
2
  import { saveChatStart } from "../games/index.js";
3
3
  /** Reads the hook's input and writes how the chat started; a failure is told on stderr and never stops the chat. */
4
4
  export async function recordChatStart(input = process.stdin, folder = process.cwd(), now = new Date()) {
@@ -1,5 +1,4 @@
1
- import { type Context } from "../commands/context.ts";
2
- import { type AnyCommand } from "../commands/registry.ts";
1
+ import { type AnyCommand, type Context } from "../commands/index.ts";
3
2
  export interface CommandLineResult {
4
3
  output: string;
5
4
  exitCode: number;
@@ -1,7 +1,6 @@
1
+ // The `chesspal` command line: a command's input read from the arguments, its result written for a person.
1
2
  import { parseArgs } from "node:util";
2
- import { defaultContext } from "../commands/context.js";
3
- import { commands } from "../commands/index.js";
4
- import { baseType, commandLineFieldProblem, inputProblems, isBoolean, isList, takesNumbers } from "../commands/registry.js";
3
+ import { baseType, commandLineFieldProblem, commands, defaultContext, inputProblems, isBoolean, isList, takesNumbers } from "../commands/index.js";
5
4
  import { version } from "../version.js";
6
5
  // The commands that exist only on the command line: cli.ts starts the server, the doorbell or the chat's note, when one comes alone.
7
6
  const mcpDescription = "Start the MCP server for an AI host. Through it the host uses these commands as tools; the host starts it, with no options.";
@@ -0,0 +1,26 @@
1
+ import { type CurrentGame } from "../games/index.ts";
2
+ import type { Context } from "./context.ts";
3
+ import type { GameSummary } from "./game-summary.ts";
4
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
5
+ export declare function analystRuns(context: Context): boolean;
6
+ /**
7
+ * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
8
+ * its end: once it is in analysis, by the agent or the chat, or reviewed, the chat goes over that review and never starts the agent again (Stefano).
9
+ */
10
+ export declare function withAnalyst(summary: GameSummary, context: Context, over?: CurrentGame): GameSummary;
11
+ /** How the chat has a game over gone over by the analyst agent, on the player's yes or for the game they brought (D-89). */
12
+ export declare function analystLine(game?: string): string;
13
+ /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
14
+ export declare function reviewedLine(game?: string): string;
15
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
16
+ export declare function coachRuns(context: Context): boolean;
17
+ /**
18
+ * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
19
+ * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
20
+ */
21
+ export declare function coachOnEnd(context: Context, folder: string, current: CurrentGame): boolean;
22
+ /**
23
+ * How to start the agents, in one message, the one that thinks first, since it needs the time (Priya), and that the player sees nothing of
24
+ * them: their end needs no answer either. The end of the last move's agents, told just after, must not take their place (Priya).
25
+ */
26
+ export declare function agentsLine(agents: readonly string[]): string;
@@ -0,0 +1,72 @@
1
+ // The agents that work beside the chat in Claude Code: the analyst that goes over a game over (D-89) and the coach that judges the player's moves (D-88).
2
+ import { isReviewed } from "../core/index.js";
3
+ import { archivedGame, archivedName, coachStarted, gameInAnalysis } from "../games/index.js";
4
+ import { analystAgent, coachAgent, hasAgent } from "../host-files/index.js";
5
+ import { gameFolder, helpsIn } from "../profiles/index.js";
6
+ /** Whether an analyst agent goes over a game over when the player wants: in Claude Code, with its agent in the player's folder (D-89). */
7
+ export function analystRuns(context) {
8
+ return context.host?.() === "claude-code" && hasAgent(context.folder, analystAgent);
9
+ }
10
+ /**
11
+ * The game over, with the analyst agent that goes over it when the player wants, where it runs (D-89); over, the game from status and ai_say, which come after
12
+ * its end: once it is in analysis, by the agent or the chat, or reviewed, the chat goes over that review and never starts the agent again (Stefano).
13
+ */
14
+ export function withAnalyst(summary, context, over) {
15
+ const { end } = summary;
16
+ if (end === undefined || !analystRuns(context)) {
17
+ return summary;
18
+ }
19
+ return { ...summary, end: { ...end, analyst: over !== undefined && goneOver(context, over) ? "done" : "offer" } };
20
+ }
21
+ /** How the chat has a game over gone over by the analyst agent, on the player's yes or for the game they brought (D-89). */
22
+ export function analystLine(game) {
23
+ return `start the chesspal-analyst agent in the background with the prompt "Analyse${game === undefined ? "" : ` ${game}`}. The player speaks <their language>.": it writes its review on the board `
24
+ + "page, which shows it at work; tell the player in a word, and when it ends go over the game with them from what it tells you.";
25
+ }
26
+ /** How the chat goes over a game the analyst agent has reviewed, or is reviewing: from that review, never with another agent (Stefano). */
27
+ export function reviewedLine(game) {
28
+ return "The analyst agent has gone over this game, or is at it: never start it again. When the player wants it gone over, go over it with them from what "
29
+ + `it told you, or, in a chat without it, from its review on the board page, which analyse_game ${game === undefined ? "" : `{"game": "${game}"} `}gives.`;
30
+ }
31
+ /** Whether a coach judges the player's moves when the helps are on: in Claude Code, with its agent in the player's folder (D-80, D-88). */
32
+ export function coachRuns(context) {
33
+ return context.host?.() === "claude-code" && hasAgent(context.folder, coachAgent);
34
+ }
35
+ /**
36
+ * Whether to start the coach after the player's own move has ended the game on the board, with the helps on: it judges that move too (D-88). Told again
37
+ * until it starts, since a chat woken by the page may lose the line on its way to its words (Priya).
38
+ */
39
+ export function coachOnEnd(context, folder, current) {
40
+ const { game, ai } = current;
41
+ return game.outcome.over && game.position.turn === ai && helpsIn(folder) && coachRuns(context) && !coachStarted(folder, current);
42
+ }
43
+ /**
44
+ * How to start the agents, in one message, the one that thinks first, since it needs the time (Priya), and that the player sees nothing of
45
+ * them: their end needs no answer either. The end of the last move's agents, told just after, must not take their place (Priya).
46
+ */
47
+ export function agentsLine(agents) {
48
+ const prompt = (agent) => `the prompt "${agent === coachAgent ? "Coach." : "Ponder."}"`;
49
+ const [first = "", second] = agents;
50
+ if (second === undefined) {
51
+ const why = first === coachAgent ? "Coach the player" : "Think on the player's time";
52
+ return `${why}: start the ${first} agent in the background with ${prompt(first)}; start it even when an agent of the move before has just ended, `
53
+ + "and write nothing about it in the chat, now or when it ends.";
54
+ }
55
+ return `Think on the player's time and coach them: in one message, start two agents in the background, ${first} with ${prompt(first)} and ${second} with `
56
+ + `${prompt(second)}; start them even when an agent of the move before has just ended, and write nothing about them in the chat, now or when they end.`;
57
+ }
58
+ /** Whether the game over is gone over already: in analysis now, by the analyst agent or the chat, or reviewed, whatever the page opened since (Federico). */
59
+ function goneOver(context, over) {
60
+ const folder = gameFolder(context.home());
61
+ const archived = archivedName(folder, over);
62
+ if (archived === undefined || gameInAnalysis(folder)?.game === archived) {
63
+ return archived !== undefined;
64
+ }
65
+ try {
66
+ return isReviewed(archivedGame(folder, archived));
67
+ }
68
+ catch {
69
+ // A game the archive cannot read is offered as it would be: the analyst says what is wrong with it.
70
+ return false;
71
+ }
72
+ }
@@ -5,18 +5,6 @@ export declare const aiMove: import("./registry.ts").Command<z.ZodObject<{
5
5
  move: z.ZodString;
6
6
  plan: z.ZodString;
7
7
  comment: z.ZodOptional<z.ZodString>;
8
- badge: z.ZodOptional<z.ZodEnum<{
9
- brilliant: "brilliant";
10
- great: "great";
11
- best: "best";
12
- excellent: "excellent";
13
- good: "good";
14
- inaccuracy: "inaccuracy";
15
- mistake: "mistake";
16
- miss: "miss";
17
- blunder: "blunder";
18
- }>>;
19
- note: z.ZodOptional<z.ZodString>;
20
8
  arrows: z.ZodOptional<z.ZodArray<z.ZodString>>;
21
9
  squares: z.ZodOptional<z.ZodArray<z.ZodString>>;
22
10
  offer_draw: z.ZodOptional<z.ZodBoolean>;
@@ -29,9 +17,12 @@ export declare const aiMove: import("./registry.ts").Command<z.ZodObject<{
29
17
  sad: "sad";
30
18
  }>>;
31
19
  }, z.core.$strip>, Played>;
32
- /** The AI's move, the agent that thinks on the player's time when the host runs one (D-72), and whether the player sees no board page, so the AI copies the board (D-85). */
20
+ /**
21
+ * The AI's move, the agents to start on the player's time when the host runs them (D-72, D-88), and whether the player sees no board page, so the AI
22
+ * copies the board (D-85).
23
+ */
33
24
  type Played = MovePlayed & {
34
- ponder?: string;
25
+ agents?: string[];
35
26
  board?: true;
36
27
  };
37
28
  export {};
@@ -1,15 +1,15 @@
1
1
  import { z } from "zod";
2
- import { badges, boardText, drawingsText, moods, moodTag } from "../core/index.js";
2
+ import { boardText, drawingsText, moods, moodTag } from "../core/index.js";
3
3
  import { commentWithPlan, talkLimits } from "../games/index.js";
4
- import { hasPonderAgent, ponderAgentFor } from "../host-files/index.js";
4
+ import { coachAgent, hasAgent, ponderAgentFor } from "../host-files/index.js";
5
5
  import { gameFolder, helpsIn } from "../profiles/index.js";
6
- import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
7
- import { closingLines, moveLines, shownMove } from "./game-summary.js";
6
+ import { agentsLine, coachRuns, withAnalyst } from "./agents.js";
7
+ import { closingLines, moveLines, shownMove } from "./answer-lines.js";
8
+ import { arrowsInput, drawingsOf, squaresInput } from "./drawings.js";
9
+ import {} from "./game-summary.js";
8
10
  import { playMove, refuseUnseenPosition } from "./play-move.js";
9
11
  import { defineCommand, sentence } from "./registry.js";
10
12
  import { give, waitingWords } from "./say.js";
11
- // The badges the opponent gives: the book's is Chesspal's, never its own judgment (D-69).
12
- const judgments = badges.filter((badge) => badge !== "book");
13
13
  /** The AI's move, with its plan, kept hidden (D-29), and its comment for the player when it has something to say (D-12, D-54). */
14
14
  export const aiMove = defineCommand({
15
15
  name: "ai_move",
@@ -23,11 +23,8 @@ export const aiMove = defineCommand({
23
23
  comment: sentence(talkLimits.aiHelps).optional().describe(`your words for the player, in one sentence of ${talkLimits.ai} characters at most, only when there is something to say: pieces and moves in words ("your `
24
24
  + `knight takes on f6"), a move you write with the pieces' figurines, as the tools show it (♞xf6+ for Nxf6+); your plans and threats only with the helps on, and then as long as a note of ${talkLimits.aiHelps} `
25
25
  + "characters, to teach; **two asterisks** put a word or two in bold on the page"),
26
- badge: z.enum(judgments).optional().describe("only with the helps on: your badge on the player's last move, as chess.com names them, only on a move that stands out (a mistake, a miss, a fine find), "
27
- + "never on a routine one"),
28
- note: noteInput.optional().describe("only with the helps on: why, on the player's last move, in a sentence or two"),
29
- arrows: arrowsInput.describe("only with the helps on: arrows on the board after your move, to show what your comment says, never your move itself: Rf2e1; G green: the better move or "
30
- + "the good idea; R red: a threat or the mistake; Y yellow: another way; B blue: a plan"),
26
+ arrows: arrowsInput.describe("only with the helps on: arrows on the board after your move, to show what your comment says, never your move itself: Rf2e1; G green: a good idea; "
27
+ + "R red: a threat or the mistake; Y yellow: another way; B blue: a plan"),
31
28
  squares: squaresInput.describe("only with the helps on: squares to mark after your move, as PGN's %csl writes them: Rf2; colours as for arrows"),
32
29
  offer_draw: z.boolean().optional().describe("true offers the player a draw with this move: it stands until their move"),
33
30
  mood: z.enum(moods).optional().describe("how you feel, on a move where the game turns for you: your face on the board page shows it for three of your moves, this one included (a surprise for this one "
@@ -36,25 +33,21 @@ export const aiMove = defineCommand({
36
33
  alwaysLoad: true,
37
34
  showsGame: true,
38
35
  positionals: ["move"],
39
- run: ({ move, plan, comment, offer_draw: offerDraw = false, mood, badge, note, arrows, squares }, context) => {
36
+ run: ({ move, plan, comment, offer_draw: offerDraw = false, mood, arrows, squares }, context) => {
40
37
  const folder = gameFolder(context.home());
41
38
  refuseUnseenPosition(context, folder);
42
- refuseWithoutHelps(folder, { comment, badge, note, arrows, squares });
39
+ refuseWithoutHelps(folder, { comment, arrows, squares });
43
40
  const drawings = drawingsOf(arrows, squares);
44
41
  // The player's words are read before the move, which would count as the answer to them; a move refused leaves them for the next call.
45
42
  const waiting = waitingWords(context);
46
43
  const words = [commentWithPlan(comment, plan), drawingsText(drawings), mood === undefined ? "" : moodTag(mood)].filter((part) => part !== "").join(" ");
47
- const played = playMove(folder, context.now(), "ai", move, (message, legal) => `${message} Legal: ${legal.join(" ")}. Nothing changed.`, {
48
- comment: words,
49
- offerDraw,
50
- ...(badge === undefined && note === undefined ? {} : { judged: { ...(badge === undefined ? {} : { badge }), ...(note === undefined ? {} : { note }) } }),
51
- });
52
- const agent = ponderAgent(context, played);
44
+ const played = playMove(folder, context.now(), "ai", move, (message, legal) => `${message} Legal: ${legal.join(" ")}. Nothing changed.`, { comment: words, offerDraw });
45
+ const agents = agentsAfter(context, folder, played);
53
46
  const board = context.pageOpen?.() === true ? {} : { board: true };
54
- return { ...played, words: give(context, waiting), ...(agent === undefined ? {} : { ponder: agent }), ...board };
47
+ return { ...played, game: withAnalyst(played.game, context), words: give(context, waiting), ...(agents.length === 0 ? {} : { agents }), ...board };
55
48
  },
56
49
  format: (result) => {
57
- // The mood the AI has just given is not told back: player_move and status tell it while it lasts (Priya, round of T-107).
50
+ // The mood the AI has just given is not told back: player_move and status tell it while it lasts (Priya).
58
51
  const game = { ...result.game };
59
52
  delete game.mood;
60
53
  // The board goes before the player's words and the next step, which stays the last line.
@@ -62,15 +55,14 @@ export const aiMove = defineCommand({
62
55
  `Played ${shownMove(result.move)}.`,
63
56
  ...moveLines({ ...result, game }),
64
57
  ...(result.board === true ? ["No board page is open: copy this board into your message.", boardText(result.game.fen)] : []),
65
- ...(result.ponder === undefined ? [] : [ponderLine(result.ponder)]),
58
+ ...(result.agents === undefined ? [] : [agentsLine(result.agents)]),
66
59
  ...closingLines(result.game, result.words),
67
60
  ].join("\n");
68
61
  },
69
62
  });
70
- /** A comment as long as a note, a badge, a note and drawings, only with the helps on (D-78): the player who has them off plays as at a tournament. */
71
- function refuseWithoutHelps(folder, { comment, badge, note, arrows, squares }) {
63
+ /** A comment as long as a note, and drawings, only with the helps on (D-78): the player who has them off plays as at a tournament. */
64
+ function refuseWithoutHelps(folder, { comment, arrows, squares }) {
72
65
  const asked = [
73
- badge === undefined && note === undefined ? "" : "without badge and note",
74
66
  arrows === undefined && squares === undefined ? "" : "without arrows and squares",
75
67
  comment === undefined || comment.length <= talkLimits.ai ? "" : `with the comment in ${talkLimits.ai} characters at most`,
76
68
  ].filter((part) => part !== "");
@@ -78,15 +70,16 @@ function refuseWithoutHelps(folder, { comment, badge, note, arrows, squares }) {
78
70
  throw new Error(`The player has the helps off: play ${asked.join(", ")}. Nothing changed.`);
79
71
  }
80
72
  }
81
- /** The agent to start after the move: in Claude Code, in a folder init set up with it, while the game goes on (D-72). */
82
- function ponderAgent(context, played) {
83
- if (context.host?.() !== "claude-code" || played.game.end !== undefined) {
84
- return undefined;
73
+ /**
74
+ * The agents to start after the move, in Claude Code, in a folder init set up with them: the one that thinks for the AI while the game goes on (D-72), then
75
+ * the coach with the helps on, which judges the player's move even when the AI's ends the game (D-88).
76
+ */
77
+ function agentsAfter(context, folder, played) {
78
+ if (context.host?.() !== "claude-code") {
79
+ return [];
85
80
  }
86
- const agent = ponderAgentFor(played.game.level);
87
- return hasPonderAgent(context.folder, agent) ? agent : undefined;
88
- }
89
- /** How to start the agent, and that the player sees nothing of it: its end needs no answer either. */
90
- function ponderLine(agent) {
91
- return `Think on the player's time: start the ${agent} agent in the background with the prompt "Ponder."; write nothing about it in the chat, now or when it ends.`;
81
+ const ponder = ponderAgentFor(played.game.level);
82
+ const thinks = played.game.end === undefined && hasAgent(context.folder, ponder);
83
+ const coaches = helpsIn(folder) && coachRuns(context);
84
+ return [...(thinks ? [ponder] : []), ...(coaches ? [coachAgent] : [])];
92
85
  }
@@ -4,8 +4,10 @@ interface Said {
4
4
  /** The game, none when the AI spoke before the first (D-77). */
5
5
  game?: GameSummary;
6
6
  words: string[];
7
+ /** The coach yet to start on the player's move that ended the game (D-88). */
8
+ coach?: true;
7
9
  }
8
- /** The AI's words on the board page without a move (T-65), read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
10
+ /** The AI's words on the board page without a move, read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
9
11
  export declare const aiSay: import("./registry.ts").Command<z.ZodObject<{
10
12
  text: z.ZodString;
11
13
  mood: z.ZodOptional<z.ZodEnum<{
@@ -1,12 +1,15 @@
1
1
  import { z } from "zod";
2
2
  import { moods } from "../core/index.js";
3
- import { talkLimits } from "../games/index.js";
3
+ import { archivedName, talkLimits } from "../games/index.js";
4
+ import { coachAgent } from "../host-files/index.js";
4
5
  import { gameFolder } from "../profiles/index.js";
5
- import { newGameStep, nextStep, summaryOf, wordLines } from "./game-summary.js";
6
+ import { agentsLine, analystLine, coachOnEnd, reviewedLine, withAnalyst } from "./agents.js";
7
+ import { newGameStep, nextStep, wordLines } from "./answer-lines.js";
8
+ import { summaryOf } from "./game-summary.js";
6
9
  import { refuseOtherGame } from "./play-move.js";
7
10
  import { defineCommand } from "./registry.js";
8
11
  import { give, say, waitingWords } from "./say.js";
9
- /** The AI's words on the board page without a move (T-65), read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
12
+ /** The AI's words on the board page without a move, read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
10
13
  export const aiSay = defineCommand({
11
14
  name: "ai_say",
12
15
  description: "Says a word on the board page without a move, once while the player is to move: an answer to what they wrote or to their draw offer, or "
@@ -24,14 +27,29 @@ export const aiSay = defineCommand({
24
27
  const waiting = waitingWords(context);
25
28
  const current = say(folder, "ai", text, context.now(), mood);
26
29
  const words = give(context, waiting);
27
- return "game" in current ? { game: summaryOf(current, context.now()), words } : { words };
30
+ if (!("game" in current)) {
31
+ return { words };
32
+ }
33
+ // The chat woken by the player's ending move on the page may have gone to its words without the coach (Priya).
34
+ return { game: withAnalyst(summaryOf(current, context.now(), archivedName(folder, current)), context, current), words, ...(coachOnEnd(context, folder, current) ? { coach: true } : {}) };
28
35
  },
29
- format: ({ game, words }) => ["Said on the board page, where the player reads it: don't write it again in the chat.", ...wordLines(words), afterWords(game, words)].join("\n"),
36
+ format: ({ game, words, coach }) => [
37
+ "Said on the board page, where the player reads it: don't write it again in the chat.",
38
+ ...wordLines(words),
39
+ ...(coach === true ? [agentsLine([coachAgent])] : []),
40
+ afterWords(game, words),
41
+ ].join("\n"),
30
42
  });
31
43
  /** The next step once the AI has spoken: the end of a game over has been told already, and the player's draw offer answered in words. */
32
44
  function afterWords(game, words) {
33
45
  if (words.length === 0 && game?.end !== undefined) {
34
- return `Next: when the player wants another game, ${newGameStep}`;
46
+ // The offer to go over the game, made in the AI's words, waits for the player's yes (D-89).
47
+ if (game.end.analyst === "done") {
48
+ return `Next: ${reviewedLine(game.end.archive)} When they want another game, ${newGameStep}`;
49
+ }
50
+ return game.end.analyst === "offer"
51
+ ? `Next: when the player wants this game gone over, ${analystLine(game.end.archive)} When they want another game, ${newGameStep}`
52
+ : `Next: when the player wants another game, ${newGameStep}`;
35
53
  }
36
54
  if (words.length === 0 && game?.drawOffer === "player" && game.turn !== game.ai) {
37
55
  return "Next: accept the draw with end_game draw_agreed, or wait for the player's move.";
@@ -22,11 +22,15 @@ interface MoveFacts {
22
22
  plan?: string;
23
23
  /** The badge and note the AI gave it with annotate_game (D-69). */
24
24
  review?: MoveReview;
25
+ /** Who is ahead after it, as the AI estimated it (D-90). */
26
+ lead?: string;
25
27
  }
26
28
  /** A game as the analysis gives it. */
27
29
  interface Analysis {
28
30
  /** How to analyse, the first time in a chat (D-68). */
29
31
  instructions?: string;
32
+ /** The player's language, for the analyst's agent, which talks with no one (D-89). */
33
+ speech?: string;
30
34
  white: string;
31
35
  black: string;
32
36
  /** In a game played here: the side the AI played, and its level. */