chesspal 0.0.1 → 0.2.0

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 (273) hide show
  1. package/README.md +99 -2
  2. package/dist/cli/chat-hook.d.ts +2 -0
  3. package/dist/cli/chat-hook.js +33 -0
  4. package/dist/cli/cli.d.ts +2 -0
  5. package/dist/cli/cli.js +28 -0
  6. package/dist/cli/command-line.d.ts +7 -0
  7. package/dist/cli/command-line.js +177 -0
  8. package/dist/commands/agents.d.ts +26 -0
  9. package/dist/commands/agents.js +72 -0
  10. package/dist/commands/ai-move.d.ts +28 -0
  11. package/dist/commands/ai-move.js +85 -0
  12. package/dist/commands/ai-say.d.ts +22 -0
  13. package/dist/commands/ai-say.js +58 -0
  14. package/dist/commands/analyse-game.d.ts +65 -0
  15. package/dist/commands/analyse-game.js +265 -0
  16. package/dist/commands/annotate-game.d.ts +39 -0
  17. package/dist/commands/annotate-game.js +131 -0
  18. package/dist/commands/answer-lines.d.ts +28 -0
  19. package/dist/commands/answer-lines.js +144 -0
  20. package/dist/commands/clock.d.ts +15 -0
  21. package/dist/commands/clock.js +45 -0
  22. package/dist/commands/coach.d.ts +45 -0
  23. package/dist/commands/coach.js +344 -0
  24. package/dist/commands/context.d.ts +67 -0
  25. package/dist/commands/context.js +37 -0
  26. package/dist/commands/delete-game.d.ts +7 -0
  27. package/dist/commands/delete-game.js +30 -0
  28. package/dist/commands/delete-review.d.ts +8 -0
  29. package/dist/commands/delete-review.js +34 -0
  30. package/dist/commands/draw-offer.d.ts +10 -0
  31. package/dist/commands/draw-offer.js +43 -0
  32. package/dist/commands/drawings.d.ts +13 -0
  33. package/dist/commands/drawings.js +25 -0
  34. package/dist/commands/end-game.d.ts +23 -0
  35. package/dist/commands/end-game.js +67 -0
  36. package/dist/commands/export-game.d.ts +14 -0
  37. package/dist/commands/export-game.js +27 -0
  38. package/dist/commands/game-summary.d.ts +78 -0
  39. package/dist/commands/game-summary.js +102 -0
  40. package/dist/commands/index.d.ts +18 -0
  41. package/dist/commands/index.js +64 -0
  42. package/dist/commands/init.d.ts +15 -0
  43. package/dist/commands/init.js +73 -0
  44. package/dist/commands/lead-input.d.ts +8 -0
  45. package/dist/commands/lead-input.js +21 -0
  46. package/dist/commands/levels.d.ts +9 -0
  47. package/dist/commands/levels.js +25 -0
  48. package/dist/commands/look.d.ts +31 -0
  49. package/dist/commands/look.js +64 -0
  50. package/dist/commands/new-game.d.ts +68 -0
  51. package/dist/commands/new-game.js +218 -0
  52. package/dist/commands/opening-book.d.ts +13 -0
  53. package/dist/commands/opening-book.js +84 -0
  54. package/dist/commands/page-link.d.ts +11 -0
  55. package/dist/commands/page-link.js +28 -0
  56. package/dist/commands/past-games.d.ts +11 -0
  57. package/dist/commands/past-games.js +47 -0
  58. package/dist/commands/play-move.d.ts +37 -0
  59. package/dist/commands/play-move.js +94 -0
  60. package/dist/commands/player-move.d.ts +13 -0
  61. package/dist/commands/player-move.js +51 -0
  62. package/dist/commands/player-say.d.ts +5 -0
  63. package/dist/commands/player-say.js +16 -0
  64. package/dist/commands/ponder.d.ts +52 -0
  65. package/dist/commands/ponder.js +179 -0
  66. package/dist/commands/position.d.ts +14 -0
  67. package/dist/commands/position.js +130 -0
  68. package/dist/commands/profiles.d.ts +69 -0
  69. package/dist/commands/profiles.js +163 -0
  70. package/dist/commands/registry.d.ts +59 -0
  71. package/dist/commands/registry.js +62 -0
  72. package/dist/commands/say.d.ts +16 -0
  73. package/dist/commands/say.js +84 -0
  74. package/dist/commands/speech.d.ts +8 -0
  75. package/dist/commands/speech.js +20 -0
  76. package/dist/commands/status.d.ts +35 -0
  77. package/dist/commands/status.js +128 -0
  78. package/dist/commands/study-games.d.ts +5 -0
  79. package/dist/commands/study-games.js +40 -0
  80. package/dist/commands/study.d.ts +38 -0
  81. package/dist/commands/study.js +106 -0
  82. package/dist/commands/view.d.ts +14 -0
  83. package/dist/commands/view.js +125 -0
  84. package/dist/core/board-text.d.ts +7 -0
  85. package/dist/core/board-text.js +18 -0
  86. package/dist/core/book.json +1 -0
  87. package/dist/core/clock.d.ts +45 -0
  88. package/dist/core/clock.js +109 -0
  89. package/dist/core/facts.d.ts +55 -0
  90. package/dist/core/facts.js +175 -0
  91. package/dist/core/game.d.ts +95 -0
  92. package/dist/core/game.js +352 -0
  93. package/dist/core/illegal.d.ts +19 -0
  94. package/dist/core/illegal.js +141 -0
  95. package/dist/core/index.d.ts +20 -0
  96. package/dist/core/index.js +12 -0
  97. package/dist/core/lead.d.ts +15 -0
  98. package/dist/core/lead.js +40 -0
  99. package/dist/core/levels.d.ts +20 -0
  100. package/dist/core/levels.js +33 -0
  101. package/dist/core/material.d.ts +9 -0
  102. package/dist/core/material.js +12 -0
  103. package/dist/core/moods.d.ts +21 -0
  104. package/dist/core/moods.js +45 -0
  105. package/dist/core/openings.d.ts +35 -0
  106. package/dist/core/openings.js +98 -0
  107. package/dist/core/openings.json +1 -0
  108. package/dist/core/pgn.d.ts +10 -0
  109. package/dist/core/pgn.js +213 -0
  110. package/dist/core/profile.d.ts +117 -0
  111. package/dist/core/profile.js +232 -0
  112. package/dist/core/record.d.ts +21 -0
  113. package/dist/core/record.js +25 -0
  114. package/dist/core/review.d.ts +80 -0
  115. package/dist/core/review.js +146 -0
  116. package/dist/games/ai-model.d.ts +5 -0
  117. package/dist/games/ai-model.js +15 -0
  118. package/dist/games/analysis.d.ts +12 -0
  119. package/dist/games/analysis.js +26 -0
  120. package/dist/games/archive.d.ts +63 -0
  121. package/dist/games/archive.js +242 -0
  122. package/dist/games/chat-start.d.ts +16 -0
  123. package/dist/games/chat-start.js +26 -0
  124. package/dist/games/coach.d.ts +57 -0
  125. package/dist/games/coach.js +144 -0
  126. package/dist/games/current-game.d.ts +56 -0
  127. package/dist/games/current-game.js +134 -0
  128. package/dist/games/files.d.ts +9 -0
  129. package/dist/games/files.js +53 -0
  130. package/dist/games/index.d.ts +11 -0
  131. package/dist/games/index.js +12 -0
  132. package/dist/games/plans.d.ts +9 -0
  133. package/dist/games/plans.js +23 -0
  134. package/dist/games/table-talk.d.ts +12 -0
  135. package/dist/games/table-talk.js +25 -0
  136. package/dist/games/talk.d.ts +36 -0
  137. package/dist/games/talk.js +131 -0
  138. package/dist/games/thoughts.d.ts +15 -0
  139. package/dist/games/thoughts.js +60 -0
  140. package/dist/home/data-folder.d.ts +12 -0
  141. package/dist/home/data-folder.js +55 -0
  142. package/dist/home/index.d.ts +2 -0
  143. package/dist/home/index.js +3 -0
  144. package/dist/home/open-folder.d.ts +2 -0
  145. package/dist/home/open-folder.js +14 -0
  146. package/dist/host-files/agents.d.ts +20 -0
  147. package/dist/host-files/agents.js +53 -0
  148. package/dist/host-files/host-files.d.ts +15 -0
  149. package/dist/host-files/host-files.js +182 -0
  150. package/dist/host-files/index.d.ts +2 -0
  151. package/dist/host-files/index.js +3 -0
  152. package/dist/index.d.ts +2 -0
  153. package/dist/index.js +3 -0
  154. package/dist/page/app/anchor.js +88 -0
  155. package/dist/page/app/app.js +458 -0
  156. package/dist/page/app/keys.js +94 -0
  157. package/dist/page/app/phase.js +30 -0
  158. package/dist/page/app/served.js +73 -0
  159. package/dist/page/avatars/avatars.js +19 -0
  160. package/dist/page/avatars/beanie.webp +0 -0
  161. package/dist/page/avatars/beard.webp +0 -0
  162. package/dist/page/avatars/bun.webp +0 -0
  163. package/dist/page/avatars/cat.webp +0 -0
  164. package/dist/page/avatars/curls.webp +0 -0
  165. package/dist/page/avatars/glasses.webp +0 -0
  166. package/dist/page/avatars/moustache.webp +0 -0
  167. package/dist/page/avatars/owl.webp +0 -0
  168. package/dist/page/avatars/short-hair.webp +0 -0
  169. package/dist/page/avatars/silver-bob.webp +0 -0
  170. package/dist/page/avatars/thought.js +10 -0
  171. package/dist/page/avatars/torretta.js +38 -0
  172. package/dist/page/base.css +160 -0
  173. package/dist/page/board/board.css +959 -0
  174. package/dist/page/board/board.js +125 -0
  175. package/dist/page/board/explain.js +58 -0
  176. package/dist/page/board/marks.js +160 -0
  177. package/dist/page/board/motion.js +71 -0
  178. package/dist/page/board/pointer.js +239 -0
  179. package/dist/page/board/squares.js +26 -0
  180. package/dist/page/dialogs/analyses.js +195 -0
  181. package/dist/page/dialogs/dialog.js +26 -0
  182. package/dist/page/dialogs/dialogs.css +1228 -0
  183. package/dist/page/dialogs/new-game.js +42 -0
  184. package/dist/page/dialogs/profiles.js +152 -0
  185. package/dist/page/dialogs/search.js +31 -0
  186. package/dist/page/dialogs/settings.js +42 -0
  187. package/dist/page/dialogs/studied.js +39 -0
  188. package/dist/page/dialogs/study.js +51 -0
  189. package/dist/page/game/badges.js +50 -0
  190. package/dist/page/game/browse.js +17 -0
  191. package/dist/page/game/card-fold.js +40 -0
  192. package/dist/page/game/column.js +51 -0
  193. package/dist/page/game/ending.js +73 -0
  194. package/dist/page/game/game.css +577 -0
  195. package/dist/page/game/move-list.js +72 -0
  196. package/dist/page/game/opening-book.js +14 -0
  197. package/dist/page/game/plies.js +82 -0
  198. package/dist/page/game/result-tile.js +14 -0
  199. package/dist/page/helps/helps.css +689 -0
  200. package/dist/page/helps/helps.js +220 -0
  201. package/dist/page/helps/view.js +127 -0
  202. package/dist/page/layout/fold-tab.js +11 -0
  203. package/dist/page/layout/folds.js +43 -0
  204. package/dist/page/layout/header.js +28 -0
  205. package/dist/page/layout/layout.css +834 -0
  206. package/dist/page/lead/lead.js +82 -0
  207. package/dist/page/lead/view.js +38 -0
  208. package/dist/page/main.js +8 -0
  209. package/dist/page/pieces/CREDITS.md +19 -0
  210. package/dist/page/pieces/capablanca-small.svg +20 -0
  211. package/dist/page/pieces/capablanca.svg +22 -0
  212. package/dist/page/pieces/nitida-small.svg +20 -0
  213. package/dist/page/pieces/nitida.svg +21 -0
  214. package/dist/page/pieces/pieces.js +28 -0
  215. package/dist/page/pieces/torretta-small.svg +20 -0
  216. package/dist/page/pieces/torretta.svg +21 -0
  217. package/dist/page/players/card.js +56 -0
  218. package/dist/page/players/clock-time.js +12 -0
  219. package/dist/page/players/model-name.js +20 -0
  220. package/dist/page/profile/profile.js +99 -0
  221. package/dist/page/profile/settings.js +77 -0
  222. package/dist/page/sounds/capture.ogg +0 -0
  223. package/dist/page/sounds/check.ogg +0 -0
  224. package/dist/page/sounds/draw.ogg +0 -0
  225. package/dist/page/sounds/flag.ogg +0 -0
  226. package/dist/page/sounds/mate.ogg +0 -0
  227. package/dist/page/sounds/move.ogg +0 -0
  228. package/dist/page/sounds/offer.ogg +0 -0
  229. package/dist/page/sounds/resign.ogg +0 -0
  230. package/dist/page/sounds/sounds.js +105 -0
  231. package/dist/page/style.css +9 -0
  232. package/dist/page/talk/talk.css +958 -0
  233. package/dist/page/talk/talk.js +165 -0
  234. package/dist/page/talk/voice.js +38 -0
  235. package/dist/page/text.js +711 -0
  236. package/dist/page/theme.css +292 -0
  237. package/dist/profiles/avatar.d.ts +23 -0
  238. package/dist/profiles/avatar.js +84 -0
  239. package/dist/profiles/front-matter.d.ts +9 -0
  240. package/dist/profiles/front-matter.js +33 -0
  241. package/dist/profiles/index.d.ts +3 -0
  242. package/dist/profiles/index.js +5 -0
  243. package/dist/profiles/profiles.d.ts +69 -0
  244. package/dist/profiles/profiles.js +254 -0
  245. package/dist/profiles/settings.d.ts +12 -0
  246. package/dist/profiles/settings.js +46 -0
  247. package/dist/server/bell.d.ts +68 -0
  248. package/dist/server/bell.js +225 -0
  249. package/dist/server/caller.d.ts +21 -0
  250. package/dist/server/caller.js +132 -0
  251. package/dist/server/mcp.d.ts +13 -0
  252. package/dist/server/mcp.js +207 -0
  253. package/dist/server/page/analyses.d.ts +23 -0
  254. package/dist/server/page/analyses.js +65 -0
  255. package/dist/server/page/files.d.ts +5 -0
  256. package/dist/server/page/files.js +92 -0
  257. package/dist/server/page/game.d.ts +6 -0
  258. package/dist/server/page/game.js +381 -0
  259. package/dist/server/page/http.d.ts +29 -0
  260. package/dist/server/page/http.js +68 -0
  261. package/dist/server/page/page.d.ts +41 -0
  262. package/dist/server/page/page.js +229 -0
  263. package/dist/server/page/profiles.d.ts +19 -0
  264. package/dist/server/page/profiles.js +215 -0
  265. package/dist/version.d.ts +2 -0
  266. package/dist/version.js +3 -0
  267. package/instructions/analysis.md +4 -0
  268. package/instructions/analyst-agent.md +8 -0
  269. package/instructions/analyst.md +4 -0
  270. package/instructions/coach.md +11 -0
  271. package/instructions/opponent.md +15 -0
  272. package/instructions/ponder.md +9 -0
  273. package/package.json +66 -5
@@ -0,0 +1,144 @@
1
+ // The lines an AI reads in a tool's answer about the current game: where it stands, the move just played, the player's words and what to do next.
2
+ import { figurinesOf, materialOf, sees } from "../core/index.js";
3
+ import { talkLimits } from "../games/index.js";
4
+ import { onQuickModel } from "../host-files/index.js";
5
+ import { analystLine, reviewedLine } from "./agents.js";
6
+ import { clockLine } from "./clock.js";
7
+ import { endText } from "./game-summary.js";
8
+ import { boardView, colourName, compactView, materialText } from "./view.js";
9
+ /** Where the game stands: the AI's position to decide on, with its last move unless the lines before named it; the player's turn; or how it ended. */
10
+ export function positionLines(game, withLastMove = true) {
11
+ if (game.end !== undefined || game.turn !== game.ai) {
12
+ return [whereLine(game), ...offerLines(game), ...clockLines(game), ...moodLines(game)];
13
+ }
14
+ return [
15
+ ...headLines(game, withLastMove),
16
+ ...(game.facts === undefined ? [] : (sees(game.level, "tactics") ? compactView : boardView)(game.facts, game.turn)),
17
+ ...(game.book === undefined ? [] : [`Book moves, most named lines first: ${game.book.join(", ")}`]),
18
+ ...(game.plan === undefined ? [] : [`Plan: ${game.plan}`]),
19
+ ...(game.thought === undefined ? [] : [thoughtLine(game.thought, game.level)]),
20
+ ...moodLines(game),
21
+ ...(game.helps === "coached" ? [`${helpsLine} ${coachLine}`] : game.helps === "on" ? [helpsLine] : game.helps === "off now" ? [helpsOffLine] : []),
22
+ ];
23
+ }
24
+ /** The helps turned on: the AI may teach as the analyst does (D-62, D-78), never playing for the player. */
25
+ const helpsLine = "Helps on: the player wants to learn. Tell your intentions, never their best move; draw arrows and squares to show what you say, and write up to "
26
+ + `${talkLimits.aiHelps} characters.`;
27
+ /**
28
+ * Where a coach runs, a coach, not the AI, judges the player's moves, and the AI knows it, so that it neither makes up what the coach said nor looks for it
29
+ * (D-88, Priya); where none runs, nobody judges them before the analyst (D-80).
30
+ */
31
+ const coachLine = "A coach, not you, judges their moves and gives them hints on the board page; you don't see what it writes.";
32
+ /** The helps turned off during a game: what the AI said and drew before no longer holds (Priya, round of D-78). */
33
+ const helpsOffLine = `Helps off now: play as at a tournament again: no intentions or arrows, comments of ${talkLimits.ai} characters at most.`;
34
+ /** The head of a view: where the game stands, the last move unless the lines before named it, the opening, the material as the player sees it, and the FEN. */
35
+ export function headLines(game, withLastMove = true) {
36
+ return [
37
+ whereLine(game),
38
+ ...offerLines(game),
39
+ ...(withLastMove && game.lastMove !== undefined ? [`Last move: ${lastMoveText(game.lastMove)}.`] : []),
40
+ ...(game.opening === undefined ? [] : [`Opening: ${game.opening}`]),
41
+ `Material: ${materialText(materialOf(game.fen))}`,
42
+ ...clockLines(game),
43
+ `FEN: ${game.fen}`,
44
+ ];
45
+ }
46
+ /** The last move, as the player is shown it, with its squares: 4.♞g5 (f3-g5). */
47
+ export function lastMoveText({ move, from, to }) {
48
+ return `${shownMove(move)} (${from}-${to})`;
49
+ }
50
+ /**
51
+ * A numbered move as the player is shown it, with the pieces' figurines (D-86): the form alone, without the SAN, which the AI copied into its message
52
+ * (Priya, test of D-86): 4.Ng5 is 4.♞g5.
53
+ */
54
+ export function shownMove(move) {
55
+ const [, number = "", san = move] = /^(\d+\.+)(.+)$/.exec(move) ?? [];
56
+ return `${number}${figurinesOf(san)}`;
57
+ }
58
+ /** The lines after a move, which the line before named: its note, where the game stands, a warning. closingLines end them. */
59
+ export function moveLines({ note, game, warning }) {
60
+ return [...(note === undefined ? [] : [`Note: ${note}`]), ...positionLines(game, false), ...(warning === undefined ? [] : [warning])];
61
+ }
62
+ /** The last lines of a tool's answer: the player's words from the board page, then the next step, which stays the last line. */
63
+ export function closingLines(game, words = [], pageClosed = false) {
64
+ return [...wordLines(words), nextStep(game, words, pageClosed)];
65
+ }
66
+ /** The player's words from the board page, one line each: lines they broke are joined, so no line of theirs reads as one of Chesspal's. */
67
+ export function wordLines(words) {
68
+ return words.map((text) => `Player wrote on the board page: «${text.replace(/\s+/g, " ")}»`);
69
+ }
70
+ /**
71
+ * What the AI does next: the one line every host reads, even where the server's instructions are not shown. Words of the player's ask for an answer. With the
72
+ * board page open in no browser, a new game starts there, never in the chat.
73
+ */
74
+ export function nextStep(game, words = [], pageClosed = false) {
75
+ const step = pageClosed ? closedPageGameStep : newGameStep;
76
+ if (game === undefined) {
77
+ return words.length === 0 ? `Next: when the player wants a game, ${step}` : `Next: answer the player with ai_say. When they want a game, ${step}`;
78
+ }
79
+ if (game.end !== undefined) {
80
+ const analysis = game.end.analyst === "offer"
81
+ ? ` To go over it, on their yes, ${analystLine(game.end.archive)}`
82
+ : game.end.analyst === "done"
83
+ ? ` ${reviewedLine(game.end.archive)}`
84
+ : "";
85
+ return words.length === 0
86
+ ? "Next: tell the player how the game ended, with ai_say and how you feel as its mood if they play on the board page; then offer them, in words, another game or "
87
+ + `to go over this one${analysis === "" ? " (analyse_game)." : `.${analysis}`} For another, ${step}`
88
+ : `Next: answer the player with ai_say.${analysis} When they want another game, ${step}`;
89
+ }
90
+ if (game.turn === game.ai && game.clock?.waiting === true) {
91
+ return "Next: don't move yet: the player starts the game with Start on the board page, and the doorbell wakes you then. End your turn.";
92
+ }
93
+ if (game.turn === game.ai) {
94
+ return words.length === 0
95
+ ? "Next: choose your move, then call ai_move with it, your plan and any comment."
96
+ : "Next: choose your move, then call ai_move with it, your plan and a comment that answers the player.";
97
+ }
98
+ if (game.drawOffer === "player") {
99
+ return "Next: accept the draw with end_game draw_agreed, or answer with ai_say and wait for the player's move.";
100
+ }
101
+ return words.length === 0 ? "Next: wait for the player's move, then call player_move with it." : "Next: answer the player with ai_say, then wait for their move.";
102
+ }
103
+ /** How a new game starts: the player chooses it in the board page's window, which new_game without choices opens (D-85). */
104
+ export const newGameStep = "call new_game without choices: it opens the board page's New game window, where the player chooses the colour, the clock and the level.";
105
+ // With the page open in no browser the window cannot open: the player opens the page and presses New game.
106
+ const closedPageGameStep = "they start it on the board page with its New game button, where they choose the colour, the clock and the level: tell them so with "
107
+ + "the page's address, and never offer to set it up in the chat.";
108
+ /** Who plays what, from the AI's side. */
109
+ export function sidesLine(ai) {
110
+ return `you play ${colourName(ai)}, the player ${colourName(ai === "white" ? "black" : "white")}`;
111
+ }
112
+ /** The draw offer that stands, and how to answer it or what becomes of it (D-65). */
113
+ function offerLines({ drawOffer, turn, ai }) {
114
+ if (drawOffer === "ai") {
115
+ return ["Your draw offer stands until the player moves; if they accept it in the chat, end_game draw_agreed ends the game."];
116
+ }
117
+ if (drawOffer === "player") {
118
+ return [turn === ai ? "The player offers a draw: end_game draw_agreed accepts it; your move declines it." : "The player offers a draw before their move; your next move declines it."];
119
+ }
120
+ return [];
121
+ }
122
+ /** What the AI thought on the player's time, saying by which model and that Chesspal checked only that it is legal: the facts do not cover every blunder (Anatolij). */
123
+ function thoughtLine({ move, why }, level) {
124
+ return `Thought on their time ${onQuickModel(level) ? "by a quicker model" : "by your model"} (legal, not checked): ${move}, ${why}`;
125
+ }
126
+ /** The AI's mood while it lasts, and how long its face shows it (D-70). */
127
+ function moodLines({ mood }) {
128
+ if (mood === undefined) {
129
+ return [];
130
+ }
131
+ const shown = mood.left === 0 ? "until your next move" : `for ${mood.left} more of your moves`;
132
+ return [`Your mood: ${mood.mood}, on your face ${shown}, then neutral by itself.`];
133
+ }
134
+ /** Who is to move, or how the game ended. */
135
+ function clockLines(game) {
136
+ return game.clock === undefined ? [] : [clockLine(game.clock, game.ai)];
137
+ }
138
+ function whereLine(game) {
139
+ if (game.end !== undefined) {
140
+ const archived = game.end.archive === undefined ? "" : ` Archived in ${game.end.archive}.`;
141
+ return `Game over: ${endText(game.end)}.${archived}`;
142
+ }
143
+ return `${game.turn === game.ai ? "You" : "Player"} (${colourName(game.turn)}) to move, move ${game.moveNumber}.`;
144
+ }
@@ -0,0 +1,15 @@
1
+ import { type Side } from "../core/index.ts";
2
+ import { type CurrentGame } from "../games/index.ts";
3
+ /** What the AI reads of the clock: the time control, and the time each side has left when the command ran, in milliseconds. */
4
+ export interface ClockSummary {
5
+ control: string;
6
+ left: Record<Side, number>;
7
+ /** The clocks wait for the player to start them. */
8
+ waiting?: true;
9
+ }
10
+ /** The current game of a folder `now`, or undefined: a game whose side to move has run out of time is over, saved and archived first. */
11
+ export declare function gameAt(folder: string, now: Date): CurrentGame | undefined;
12
+ /** The clock of a game `now`, as the AI reads it; none in a game without one. */
13
+ export declare function clockSummaryOf(current: CurrentGame, now: Date): ClockSummary | undefined;
14
+ /** The clock's line: Clock: you 4:32, player 5:01 (10+5), and whether the clocks still wait for the player. */
15
+ export declare function clockLine({ control, left, waiting }: ClockSummary, ai: Side): string;
@@ -0,0 +1,45 @@
1
+ // The clock in the commands (D-13): a flag that falls ends the game as soon as a command reads it, and the clock's line for the AI.
2
+ import { canMate, clockText, Game, timeControlText, timeLeft } from "../core/index.js";
3
+ import { archiveEnded, endOf, loadGame, saveGame } from "../games/index.js";
4
+ import { colourName } from "./view.js";
5
+ /** The current game of a folder `now`, or undefined: a game whose side to move has run out of time is over, saved and archived first. */
6
+ export function gameAt(folder, now) {
7
+ const current = loadGame(folder);
8
+ if (current?.clock === undefined || endOf(current.game) !== undefined) {
9
+ return current;
10
+ }
11
+ const { turn } = current.game.position;
12
+ if (timeLeft(current.clock, turn, now)[turn] > 0) {
13
+ return current;
14
+ }
15
+ const ended = outOfTime(current, turn, now);
16
+ saveGame(folder, ended);
17
+ // An archive that cannot be written leaves the game in game.json, where status shows it, until a new game archives it.
18
+ archiveEnded(folder, ended);
19
+ return ended;
20
+ }
21
+ /** The clock of a game `now`, as the AI reads it; none in a game without one. */
22
+ export function clockSummaryOf(current, now) {
23
+ const { clock, game } = current;
24
+ if (clock === undefined) {
25
+ return undefined;
26
+ }
27
+ const left = endOf(game) === undefined ? timeLeft(clock, game.position.turn, now) : { ...clock.left };
28
+ return { control: timeControlText(clock.control), left, ...(clock.waiting === true && endOf(game) === undefined ? { waiting: true } : {}) };
29
+ }
30
+ /** The clock's line: Clock: you 4:32, player 5:01 (10+5), and whether the clocks still wait for the player. */
31
+ export function clockLine({ control, left, waiting }, ai) {
32
+ const player = ai === "white" ? "black" : "white";
33
+ const line = `Clock: you ${clockText(left[ai])}, player ${clockText(left[player])} (${control}).`;
34
+ return waiting === true ? `${line} The clocks wait: they start with the player's first move, or with Start on the board page when your move is first.` : line;
35
+ }
36
+ /** The game lost on time by `loser`, or drawn when the other side could not mate with any series of legal moves (FIDE 6.9). */
37
+ function outOfTime(current, loser, now) {
38
+ const winner = loser === "white" ? "black" : "white";
39
+ const ending = canMate(current.game.position.fen, winner)
40
+ ? { Result: loser === "white" ? "0-1" : "1-0", Termination: `${colourName(loser)} lost on time` }
41
+ : { Result: "1/2-1/2", Termination: `${colourName(loser)} ran out of time and ${colourName(winner)} cannot mate` };
42
+ const { record } = current.game;
43
+ const clock = { ...current.clock, left: { ...current.clock.left, [loser]: 0 }, since: now.toISOString() };
44
+ return { ...current, game: new Game({ ...record, tags: { ...record.tags, ...ending } }), clock };
45
+ }
@@ -0,0 +1,45 @@
1
+ import { z } from "zod";
2
+ /** What coach answers: stop, the moves to work on, the words kept and what comes next, or still the player's move; with the game's key and the position. */
3
+ type Coached = {
4
+ stop: "moved" | "waited" | "helps" | "started";
5
+ } | {
6
+ start: string[];
7
+ } | {
8
+ kept: Place;
9
+ then: "lead" | "wait" | "moved" | "over";
10
+ saved: string[];
11
+ hintLeft: boolean;
12
+ } | {
13
+ waiting: Place;
14
+ };
15
+ /** The game, by its key, and the position in it the coach works on. */
16
+ interface Place {
17
+ readonly game: string;
18
+ readonly position: number;
19
+ }
20
+ /**
21
+ * The coach (D-80, D-88), for the agent that judges the player's moves and gives them hints with the helps on: every fact, never the opponent's plan; its
22
+ * words go to the board page alone, never to the opponent. Its calls are not the chat's, and the server refuses them from the chat. Offered to Claude Code alone.
23
+ */
24
+ export declare const coach: import("./registry.ts").Command<z.ZodObject<{
25
+ game: z.ZodOptional<z.ZodString>;
26
+ position: z.ZodOptional<z.ZodInt>;
27
+ ahead: z.ZodOptional<z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>;
28
+ badge: z.ZodOptional<z.ZodEnum<{
29
+ brilliant: "brilliant";
30
+ great: "great";
31
+ best: "best";
32
+ excellent: "excellent";
33
+ good: "good";
34
+ inaccuracy: "inaccuracy";
35
+ mistake: "mistake";
36
+ miss: "miss";
37
+ blunder: "blunder";
38
+ }>>;
39
+ note: z.ZodOptional<z.ZodString>;
40
+ hint: z.ZodOptional<z.ZodString>;
41
+ arrows: z.ZodOptional<z.ZodArray<z.ZodString>>;
42
+ squares: z.ZodOptional<z.ZodArray<z.ZodString>>;
43
+ done: z.ZodOptional<z.ZodBoolean>;
44
+ }, z.core.$strip>, Coached>;
45
+ export {};
@@ -0,0 +1,344 @@
1
+ import { z } from "zod";
2
+ import { badges, Game, leadText, levelOf, materialOf, withSanLetters } from "../core/index.js";
3
+ import { archiveCoached, coachWaitingSince, coachWords, endOf, keepHint, keepJudgment, keepLead, loadGame, startCoach } from "../games/index.js";
4
+ import { coachAgent } from "../host-files/index.js";
5
+ import { activeProfile, gameFolder, helpsIn } from "../profiles/index.js";
6
+ import { clockLine, clockSummaryOf } from "./clock.js";
7
+ import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
8
+ import { lastMoves } from "./game-summary.js";
9
+ import { aheadWords, leadFrom, leadInput } from "./lead-input.js";
10
+ import { knownMoves, openingLine } from "./opening-book.js";
11
+ import { waitForPlayer, wordsOf } from "./ponder.js";
12
+ import { profileLines } from "./position.js";
13
+ import { defineCommand } from "./registry.js";
14
+ import { speechLine, speechOf } from "./speech.js";
15
+ import { colourName, compactView, materialText, riskLine } from "./view.js";
16
+ // The badges the coach gives: the book's is Chesspal's, never a judgment (D-69).
17
+ const judgments = badges.filter((badge) => badge !== "book");
18
+ /**
19
+ * The coach (D-80, D-88), for the agent that judges the player's moves and gives them hints with the helps on: every fact, never the opponent's plan; its
20
+ * words go to the board page alone, never to the opponent. Its calls are not the chat's, and the server refuses them from the chat. Offered to Claude Code alone.
21
+ */
22
+ export const coach = defineCommand({
23
+ name: "coach",
24
+ description: "For the chesspal-coach agent only, never from the chat: Chesspal's answers say when to start it. coach {} gives the player's move to judge and the position "
25
+ + "they are to move in; coach {game, position, ahead, badge, note, hint, arrows, squares} keeps who is ahead there, while the game goes on, and your judgment and your hint, "
26
+ + "each only when there is something to say; coach {game, position, done: true} waits for their move, once who is ahead is kept. Whenever it says stop, stop. "
27
+ + 'Example: coach {game: "t3k9qa", position: 24, ahead: -0.8, badge: "mistake", note: "It leaves the d5 pawn without a defender.", hint: "Look at what defends their king now."}.',
28
+ input: z.object({
29
+ game: z.string().optional().describe("the game's key, as coach {} gives it"),
30
+ position: z.int().min(0).optional().describe("the position's number, as coach {} gives it"),
31
+ ahead: leadInput.optional().describe("who is ahead in the position the player is to move in, for the page's bar: pawns from White's side, 1.5 for White, -3 for Black, 0 for even, or a mate "
32
+ + 'you see in moves of the side that mates, "#3" for White\'s, "#-3" for Black\'s'),
33
+ badge: z.enum(judgments).optional().describe("your badge on the player's move, as chess.com names them, only on a move that stands out, never on a routine one"),
34
+ note: noteInput.optional().describe("why, on the player's move, in a sentence or two, in their language"),
35
+ hint: noteInput.optional().describe("a hint for their turn, in a sentence or two, in their language, only when there is something to find: what is on the board (a piece, a square, a file, a "
36
+ + "threat of the opponent's, a goal), never what to play"),
37
+ arrows: arrowsInput.describe("only with a hint: arrows on the board as it is now, never a move the player can play: Rf2e1; R red: a threat of the opponent's; B blue: a plan or a target; "
38
+ + "Y yellow: a line; G green: a good idea"),
39
+ squares: squaresInput.describe("only with a hint: squares to mark on the board as it is now: Rf2; colours as for arrows"),
40
+ done: z.boolean().optional().describe("true once you have kept your words, who is ahead always while the game goes on: it waits for the player's move"),
41
+ }),
42
+ byAgents: [coachAgent],
43
+ host: "claude-code",
44
+ run: async ({ game, position, ahead, badge, note, hint, arrows, squares, done = false }, context) => {
45
+ const folder = gameFolder(context.home());
46
+ const current = loadGame(folder);
47
+ const words = { ahead, badge, note, hint, arrows, squares };
48
+ const writes = Object.values(words).some((word) => word !== undefined);
49
+ if (!helpsIn(folder)) {
50
+ return { stop: "helps" };
51
+ }
52
+ if (game === undefined || position === undefined) {
53
+ if (writes || done || game !== undefined || position !== undefined) {
54
+ throw new Error("Give the game's key and the position's number with it: call coach {} first, which gives them. Nothing changed.");
55
+ }
56
+ const start = current === undefined ? undefined : startOf(current, folder, context);
57
+ if (start === undefined || current === undefined) {
58
+ return { stop: "moved" };
59
+ }
60
+ // The chat may start a second coach on the same position, told again before the first called: one judges it (Priya).
61
+ return startCoach(folder, current) ? { start } : { stop: "started" };
62
+ }
63
+ // A new game may have started while the coach thought on the last one: its words never go to another game (Federico).
64
+ if (current === undefined || gameKey(current) !== game || position > current.game.record.moves.length) {
65
+ return { stop: "moved" };
66
+ }
67
+ const place = { game, position };
68
+ if (!done) {
69
+ return keep(folder, current, place, words);
70
+ }
71
+ if (writes) {
72
+ throw new Error(`Keep your words first, then call ${call(place, "done: true")} alone. Nothing changed.`);
73
+ }
74
+ // The bar on the page needs who is ahead in every position the player is to move in while the game goes on (D-90, Stefano).
75
+ if (leadMissing(folder, current, position)) {
76
+ throw new Error(`Who is ahead is still missing here: first ${aheadCall(place)}, then ${call(place, "done: true")}. Nothing changed.`);
77
+ }
78
+ const since = coachWaitingSince(folder, current, position, context.now());
79
+ const waited = await waitForPlayer(context, since, () => !theirTurn(loadGame(folder), place));
80
+ return waited === "waiting" ? { waiting: place } : { stop: waited };
81
+ },
82
+ format: (coached) => {
83
+ if ("stop" in coached) {
84
+ return coached.stop === "helps"
85
+ ? "Stop now: the player has turned the helps off. End with one word."
86
+ : coached.stop === "started"
87
+ ? "Stop now: another coach works on this position. End with one word."
88
+ : coached.stop === "moved"
89
+ ? "Stop now: the player has moved, or the game is over. End with one word."
90
+ : "Stop now: you have waited long enough. End with one word.";
91
+ }
92
+ if ("start" in coached) {
93
+ return coached.start.join("\n");
94
+ }
95
+ if ("kept" in coached) {
96
+ return keptText(coached);
97
+ }
98
+ return `Still the player's move: call ${call(coached.waiting, "done: true")} again.`;
99
+ },
100
+ });
101
+ /**
102
+ * What a call kept, and what comes next: who is ahead when it is still missing, then wait for the player's move; or stop, as they have moved or the game is
103
+ * over, with the hint that came too late.
104
+ */
105
+ function keptText({ kept, then, saved, hintLeft }) {
106
+ const what = saved.length === 0 ? "nothing" : saved.join(", ");
107
+ if (then === "lead") {
108
+ return `Kept on the board page: ${what}. Who is ahead is still missing: now ${aheadCall(kept)}, then ${call(kept, "done: true")} alone.`;
109
+ }
110
+ if (then === "wait") {
111
+ return `Kept on the board page: ${what}. Now call ${call(kept, "done: true")} alone.`;
112
+ }
113
+ const late = hintLeft ? ", so your hint is not kept" : "";
114
+ return then === "moved"
115
+ ? `Kept: ${what}; the player has moved${late}. Stop now: end with one word.`
116
+ : `Kept for the game's record: ${what}; the game is over${late}. Stop now: end with one word.`;
117
+ }
118
+ /** A call of coach on a game's position, with what else it gives. */
119
+ function call({ game, position }, rest) {
120
+ return `coach {game: "${game}", position: ${position}, ${rest}}`;
121
+ }
122
+ /** The call that gives who is ahead, with how to write it. */
123
+ function aheadCall(place) {
124
+ return `call ${call(place, "ahead")}, in pawns from White's side (1.5, -3, 0) or a mate you see ("#3" White's, "#-3" Black's)`;
125
+ }
126
+ /** Whether who is ahead is still to give in a position the player is to move in, while the game goes on. */
127
+ function leadMissing(folder, current, position) {
128
+ const { leads = [] } = coachWords(folder, current);
129
+ return endOf(current.game) === undefined && sideToMoveAt(current, position) !== current.ai && !leads.some((lead) => lead.position === position);
130
+ }
131
+ /** The game's short key, from its start: the coach names the game it works on, so that its words never go to the next one. */
132
+ function gameKey(current) {
133
+ return Math.floor(Date.parse(current.started) / 1000).toString(36);
134
+ }
135
+ /** How many plies of the game the coach shows: enough to follow it, few enough for its budget. */
136
+ const pliesShown = 40;
137
+ /**
138
+ * What the coach works on, from the position the player is to move in, or the game over: the game, the player's move the opponent answered, or their own
139
+ * that ended the game, with the position they played it in and the book there, and the position now with the hint it gave last; none while the opponent is
140
+ * to move.
141
+ */
142
+ function startOf(current, folder, context) {
143
+ const { game, ai } = current;
144
+ const { record } = game;
145
+ const plies = record.moves.length;
146
+ const over = endOf(game);
147
+ const player = ai === "white" ? "black" : "white";
148
+ const judged = judgedPly(current, plies);
149
+ if ((game.position.turn === ai && over === undefined) || (over !== undefined && judged === undefined)) {
150
+ return undefined;
151
+ }
152
+ const place = { game: gameKey(current), position: plies };
153
+ const ender = game.position.turn === ai ? "the player's" : "the opponent's";
154
+ const head = over === undefined
155
+ ? `Game "${place.game}", position ${plies}: the player (${colourName(player)}) to move, move ${game.position.moveNumber}; the opponent plays ${colourName(ai)} at the `
156
+ + `${levelOf(record)} level.`
157
+ : `Game "${place.game}", position ${plies}: the game is over, ${over.result}${over.termination === undefined ? "" : `, ${over.termination}`}, with ${ender} move.`;
158
+ const speech = speechOf(folder, context.language);
159
+ const clock = clockSummaryOf(current, context.now());
160
+ const keepLine = over === undefined
161
+ ? `Keep your words: ${call(place, "ahead, badge, note, hint, arrows, squares")}, who is ahead always, the rest only when there is something to say; then `
162
+ + `${call(place, "done: true")}.`
163
+ : `No hint: the game is over. Keep your judgment, when their move stands out: ${call(place, "badge, note")}; then end with one word.`;
164
+ return [
165
+ head,
166
+ speech === undefined ? "The player speaks English." : speechLine(speech),
167
+ ...playerLines(context),
168
+ ...movesLines(record),
169
+ ...(clock === undefined ? [] : [clockLine(clock, ai)]),
170
+ ...(judged === undefined ? [] : judgedLines(current, judged, player)),
171
+ ...(over === undefined ? nowLines(current, folder, player) : []),
172
+ keepLine,
173
+ ];
174
+ }
175
+ /** The game's moves as the coach reads them, with the position a game set up began in, the last ones of a long game, and its opening by the book (Anatolij). */
176
+ function movesLines(record) {
177
+ const opening = openingLine(record);
178
+ return [
179
+ ...(record.tags.FEN === undefined ? [] : [`The game began from a set position: ${record.tags.FEN}`]),
180
+ `${record.moves.length > pliesShown ? `Moves (the last ${pliesShown} plies)` : "Moves"}: ${lastMoves(record, pliesShown)}`,
181
+ ...(opening === undefined ? [] : [`Opening: ${opening}`]),
182
+ ];
183
+ }
184
+ /**
185
+ * The plies up to the player's move a judgment at `position` is for: the one the opponent answered, or the player's own when the opponent is to move, as
186
+ * after their move that ended the game (D-88); none before their first move.
187
+ */
188
+ function judgedPly(current, position) {
189
+ const ply = sideToMoveAt(current, position) === current.ai ? position : position - 1;
190
+ return ply >= 1 ? ply : undefined;
191
+ }
192
+ /**
193
+ * The player's move to judge, the position they played it in with the book there, and, when the opponent answered it, what it left at risk and the checks
194
+ * it let the opponent give (Anatolij), and the answer with its words.
195
+ */
196
+ function judgedLines({ game }, ply, player) {
197
+ const { record } = game;
198
+ const before = new Game({ tags: record.tags, moves: record.moves.slice(0, ply - 1) });
199
+ const played = { tags: record.tags, moves: record.moves.slice(0, ply) };
200
+ const book = knownMoves(before.position.fen);
201
+ const lines = [
202
+ `Their move to judge: ${lastMoves(played, 1)}. The position they played it in:`,
203
+ `FEN: ${before.position.fen}`,
204
+ `Material: ${materialText(materialOf(before.position.fen))}`,
205
+ ...compactView(before.facts, player),
206
+ ...(book.length === 0 ? [] : [`Book moves there, most named lines first: ${book.join(", ")}`]),
207
+ ];
208
+ if (ply === record.moves.length) {
209
+ return lines;
210
+ }
211
+ const after = new Game(played).facts;
212
+ const answer = lastMoves({ tags: record.tags, moves: record.moves.slice(0, ply + 1) }, 1);
213
+ const said = wordsOf(record.moves[ply]?.comment);
214
+ return [
215
+ ...lines,
216
+ `After their move: at risk ${riskLine(after) ?? "none"}; the opponent's checks: ${after.checks.join(" ") || "none"}.`,
217
+ said === undefined ? `The opponent answered ${answer} and said nothing.` : `The opponent answered ${answer} and said: «${said}»`,
218
+ ];
219
+ }
220
+ /**
221
+ * The position the player is to move in, for who is ahead and the hint, with its positional facts too (D-90), and what the coach said on their turn
222
+ * before.
223
+ */
224
+ function nowLines(current, folder, player) {
225
+ const { game } = current;
226
+ const plies = game.record.moves.length;
227
+ const { hint, leads = [] } = coachWords(folder, current);
228
+ const lead = leads.find(({ position }) => position === plies - 2)?.lead;
229
+ return [
230
+ "Now, for who is ahead and your hint:",
231
+ `FEN: ${game.position.fen}`,
232
+ ...compactView(game.facts, player),
233
+ ...profileLines(game.profile),
234
+ ...(lead === undefined ? [] : [`Who you saw ahead on their turn before: ${leadText(lead)}, ${aheadWords(lead)}`]),
235
+ ...(hint === undefined || hint.position !== plies - 2 ? [] : [`Your hint on their turn before: «${hint.text}»`]),
236
+ ];
237
+ }
238
+ /** How the player says they play, and their rating, when their profile says so. */
239
+ function playerLines(context) {
240
+ const { level, rating } = activeProfile(context.home());
241
+ const parts = [level === undefined ? undefined : `plays at the ${level} level`, rating === undefined ? undefined : `is rated ${rating.value} on ${rating.site}`];
242
+ const said = parts.filter((part) => part !== undefined);
243
+ return said.length === 0 ? [] : [`The player ${said.join(" and ")}.`];
244
+ }
245
+ /** Keeps the judgment of the player's move the opponent answered at `position`, or that ended the game, and the hint for their turn while it is still theirs. */
246
+ function keep(folder, current, place, { ahead, badge, note, hint, arrows, squares }) {
247
+ const { position } = place;
248
+ const judging = badge !== undefined || note !== undefined;
249
+ const over = endOf(current.game) !== undefined;
250
+ const toMove = theirTurn(current, place);
251
+ if (hint === undefined && (arrows !== undefined || squares !== undefined)) {
252
+ throw new Error("Arrows and squares show your hint: give them with it. Nothing changed.");
253
+ }
254
+ if (!judging && hint === undefined && ahead === undefined) {
255
+ throw new Error(`Give who is ahead (ahead), your judgment (badge, note), your hint (hint, arrows, squares); or ${call(place, "done: true")}. Nothing changed.`);
256
+ }
257
+ // On the position the game ended in there is no turn to help and the result says who won; a position before it keeps who was ahead there, when the game
258
+ // ended while the coach thought (Federico).
259
+ const ended = over && position === current.game.record.moves.length;
260
+ if (ended && (hint !== undefined || ahead !== undefined)) {
261
+ throw new Error("The game is over: no hint and no ahead. Keep your judgment, when their move stands out, or end with one word. Nothing changed.");
262
+ }
263
+ if (ahead !== undefined && sideToMoveAt(current, position) === current.ai) {
264
+ throw new Error("Who is ahead is for a position the player is to move in, as coach {} gives it: here the opponent is to move. Nothing changed.");
265
+ }
266
+ const lead = ahead === undefined ? undefined : leadFrom(ahead);
267
+ // The player's move to judge: the one the opponent answered, or their own that ended the game (D-88).
268
+ const ply = judgedPly(current, position);
269
+ if (judging && (ply === undefined || (ply === position && !ended))) {
270
+ throw new Error("There is no move of the player's to judge here: give only your hint. Nothing changed.");
271
+ }
272
+ const legal = toMove ? current.game.legalMoves() : [];
273
+ if (hint !== undefined && toMove) {
274
+ refuseMoves(hint, arrows ?? [], legal);
275
+ }
276
+ if (judging && ply !== undefined) {
277
+ // A note that names a move the player could play in the position it was given for waits until none of them is legal on their turn: until then it
278
+ // would play for them (Anatolij).
279
+ const then = toMove ? legal : new Game({ tags: current.game.record.tags, moves: current.game.record.moves.slice(0, position) }).legalMoves();
280
+ const held = note === undefined ? [] : [...new Set(namedMoves(note, then))];
281
+ keepJudgment(folder, current, { ply, ...(badge === undefined ? {} : { badge }), ...(note === undefined ? {} : { note }), ...(held.length === 0 ? {} : { held }) });
282
+ }
283
+ if (hint !== undefined && toMove) {
284
+ keepHint(folder, current, { position, text: hint, ...drawingsOf(arrows, squares) });
285
+ }
286
+ // Who is ahead in the position the player was to move in, kept even once they have moved: the bar and the chart show it on that position.
287
+ if (lead !== undefined) {
288
+ keepLead(folder, current, { position, lead });
289
+ }
290
+ const saved = [
291
+ judging ? "your judgment" : undefined,
292
+ lead === undefined ? undefined : `who is ahead, ${aheadWords(lead)}`,
293
+ hint !== undefined && toMove ? "your hint" : undefined,
294
+ ].filter((part) => part !== undefined);
295
+ const hintLeft = hint !== undefined && !toMove;
296
+ if (over) {
297
+ // The game went to the archive with the move that ended it: the judgment goes there too.
298
+ archiveCoached(folder, current);
299
+ return { kept: place, then: "over", saved, hintLeft };
300
+ }
301
+ if (!toMove) {
302
+ return { kept: place, then: "moved", saved, hintLeft };
303
+ }
304
+ return { kept: place, then: leadMissing(folder, current, position) ? "lead" : "wait", saved, hintLeft };
305
+ }
306
+ /** Whether the player is still to move in the position the coach works on, in the same game going on. */
307
+ function theirTurn(current, { game, position }) {
308
+ return current !== undefined && gameKey(current) === game && endOf(current.game) === undefined && current.game.record.moves.length === position
309
+ && current.game.position.turn !== current.ai;
310
+ }
311
+ /** The side to move after `position` plies of the game. */
312
+ function sideToMoveAt({ game }, position) {
313
+ const { turn } = game.position;
314
+ return (game.record.moves.length - position) % 2 === 0 ? turn : turn === "white" ? "black" : "white";
315
+ }
316
+ /** Refuses a hint that plays for the player: a move they may play now written in it, or drawn as an arrow (Anatolij). */
317
+ function refuseMoves(hint, arrows, legal) {
318
+ const drawn = arrows.map((arrow) => legal.find((move) => move.from === arrow.slice(1, 3) && move.to === arrow.slice(3, 5))?.san).filter((san) => san !== undefined);
319
+ const named = [...namedMoves(hint, legal), ...drawn];
320
+ if (named.length > 0) {
321
+ throw new Error(`Your hint gives the player a move they may play now (${[...new Set(named)].join(", ")}): point at what is on the board, a piece, a square, a file, a threat, a goal, `
322
+ + "never what to play, and mark squares rather than draw their move. Nothing changed.");
323
+ }
324
+ }
325
+ /** The moves a text names that the player may play now: a piece's move, a capture, a promotion or a castling, in letters or figurines; a square alone is no move. */
326
+ function namedMoves(text, legal) {
327
+ const sanOf = new Map(legal.map((move) => [moveKey(move.san), move.san]));
328
+ const written = withSanLetters(text).match(moveWords) ?? [];
329
+ return written.map((word) => sanOf.get(moveKey(word))).filter((san) => san !== undefined);
330
+ }
331
+ // A move as SAN writes it, but a pawn's move to a square, which reads as the square: Nf3, Nbxd2, exd5, e8=Q, O-O; never the opponent's, after "...".
332
+ const moveWords = /(?<![\p{L}\d]|\.\.\.|…)(?:O-O(?:-O)?|0-0(?:-0)?|[KQRBN][a-h]?[1-8]?x?[a-h][1-8]|[a-h]x[a-h][1-8](?:=[QRBN])?|[a-h][18]=[QRBN])(?![\p{L}\d])/gu;
333
+ /**
334
+ * A move as the coach may write it and Chesspal lists it, alike: its piece and capture, the square it goes to and its promotion; a castling as it is. A
335
+ * piece on a square without the capture names that piece, never a move taking it (Priya and Anatolij).
336
+ */
337
+ function moveKey(san) {
338
+ const bare = san.replace(/[+#!?]+$/, "").replaceAll("0", "O");
339
+ if (bare.startsWith("O-O")) {
340
+ return bare;
341
+ }
342
+ const [, piece = "", capture = "", to = "", promotion = ""] = /^([KQRBN])?[a-h]?[1-8]?(x?)([a-h][1-8])(=[QRBN])?$/.exec(bare) ?? [];
343
+ return `${piece}${capture}${to}${promotion}`;
344
+ }