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,84 @@
1
+ // The book of the openings the build indexed (D-16), read once at the first lookup; the book's moves, only for a game at the master level (D-60).
2
+ import { readFileSync } from "node:fs";
3
+ import { bookMoves, Game, inOpenings, openingOf, outOfBookMove } from "../core/index.js";
4
+ // The index of the named openings and the book of their moves, each read at its first lookup; null where the build made none.
5
+ let index;
6
+ let book;
7
+ // Each record is looked up once: the live game keeps its record until a move changes it.
8
+ const lines = new WeakMap();
9
+ /** The game's opening as the AI reads it: ECO and name, and the move it left the book at; none outside the book, or where the build made no index. */
10
+ export function openingLine(record) {
11
+ let line = lines.get(record);
12
+ if (line === undefined) {
13
+ line = lineOf(record) ?? null;
14
+ lines.set(record, line);
15
+ }
16
+ return line ?? undefined;
17
+ }
18
+ /** The moves the book knows from a position (D-15), for the AI at the master level; none out of the book, or where the build made no book. */
19
+ export function knownMoves(fen) {
20
+ const moves = openingBook();
21
+ return moves === undefined ? [] : bookMoves(fen, moves);
22
+ }
23
+ /** How many plies from the start the book knows, to the last before the game left it (D-69); none from another position. */
24
+ export function bookPlies(record) {
25
+ if (record.tags.FEN !== undefined) {
26
+ return 0;
27
+ }
28
+ const game = new Game({ tags: record.tags, moves: [] });
29
+ let plies = 0;
30
+ for (const { san } of record.moves) {
31
+ if (!game.playRecorded(san).played || !bookReaches(game.position.fen)) {
32
+ break;
33
+ }
34
+ plies++;
35
+ }
36
+ return plies;
37
+ }
38
+ /** Whether a line of Lichess's openings reaches a position, by any order of moves (D-68); never where the build made no book. */
39
+ export function bookReaches(fen) {
40
+ const named = openingIndex();
41
+ const moves = openingBook();
42
+ return named !== undefined && moves !== undefined && inOpenings(fen, named, moves);
43
+ }
44
+ /** The opening a game reached, ECO and name, without where it left the book: for the games in the archive. */
45
+ export function openingName(record) {
46
+ const opening = bookOpening(record);
47
+ return opening === undefined ? undefined : `${opening.eco} ${opening.name}`;
48
+ }
49
+ /** The last named opening a game reached, and at which ply; none outside the book, or where the build made no index. */
50
+ export function bookOpening(record) {
51
+ const named = openingIndex();
52
+ return named === undefined ? undefined : openingOf(record, named);
53
+ }
54
+ function lineOf(record) {
55
+ const opening = bookOpening(record);
56
+ if (opening === undefined) {
57
+ return undefined;
58
+ }
59
+ return `${opening.eco} ${opening.name}${opening.inBook ? "" : ` (out of book since move ${outOfBookMove(opening)})`}`;
60
+ }
61
+ function openingBook() {
62
+ if (book === undefined) {
63
+ try {
64
+ book = JSON.parse(readFileSync(new URL("../../dist/core/book.json", import.meta.url), "utf8"));
65
+ }
66
+ catch {
67
+ // Not built yet: the master goes without the book.
68
+ book = null;
69
+ }
70
+ }
71
+ return book ?? undefined;
72
+ }
73
+ function openingIndex() {
74
+ if (index === undefined) {
75
+ try {
76
+ index = JSON.parse(readFileSync(new URL("../../dist/core/openings.json", import.meta.url), "utf8"));
77
+ }
78
+ catch {
79
+ // Not built yet: the views go without the opening.
80
+ index = null;
81
+ }
82
+ }
83
+ return index ?? undefined;
84
+ }
@@ -0,0 +1,11 @@
1
+ import type { Context } from "./context.ts";
2
+ /** The board page's address when the chat serves it and no browser has it open; none on the command line, or with the page open. */
3
+ export declare function closedPage(context: Context): string | undefined;
4
+ /** The board page's address once a game, for a move written in the chat while no browser has the page open: the game is played there (D-85). */
5
+ export declare function pageInvite(context: Context, started: string | undefined): string | undefined;
6
+ /** Notes the board page's address given for a game, by status or new_game: a move in the chat does not give it again (Priya, round of D-85). */
7
+ export declare function pageGiven(context: Context, started: string): void;
8
+ /** The line that has the AI invite the player to the board page from the chat. */
9
+ export declare function inviteLine(url: string): string;
10
+ /** The board page's line: its address, and when nobody has it open, that the player needs it first. */
11
+ export declare function pageLine(url: string, closed: boolean): string;
@@ -0,0 +1,28 @@
1
+ /** The board page's address when the chat serves it and no browser has it open; none on the command line, or with the page open. */
2
+ export function closedPage(context) {
3
+ return context.page !== undefined && context.pageOpen?.() === false ? context.page : undefined;
4
+ }
5
+ /** The board page's address once a game, for a move written in the chat while no browser has the page open: the game is played there (D-85). */
6
+ export function pageInvite(context, started) {
7
+ const page = closedPage(context);
8
+ if (page === undefined || context.told === undefined || context.told.has(inviteKey(started))) {
9
+ return undefined;
10
+ }
11
+ context.told.add(inviteKey(started));
12
+ return page;
13
+ }
14
+ /** Notes the board page's address given for a game, by status or new_game: a move in the chat does not give it again (Priya, round of D-85). */
15
+ export function pageGiven(context, started) {
16
+ context.told?.add(inviteKey(started));
17
+ }
18
+ /** The line that has the AI invite the player to the board page from the chat. */
19
+ export function inviteLine(url) {
20
+ return `The board page is open in no browser, and the game is played there: invite the player, in a few words, to open ${url}; their moves in the chat still count.`;
21
+ }
22
+ /** The board page's line: its address, and when nobody has it open, that the player needs it first. */
23
+ export function pageLine(url, closed) {
24
+ return closed ? `Board page: ${url}, open in no browser: give the player this address before anything else.` : `Board page: ${url}`;
25
+ }
26
+ function inviteKey(started) {
27
+ return `page invite ${started ?? ""}`;
28
+ }
@@ -0,0 +1,11 @@
1
+ import { z } from "zod";
2
+ /** The AI's memory of the games played (D-57): how the latest went, one line each, or one game whole from the archive. */
3
+ export declare const pastGames: import("./registry.ts").Command<z.ZodObject<{
4
+ game: z.ZodOptional<z.ZodString>;
5
+ }, z.core.$strip>, {
6
+ games: string[];
7
+ } | {
8
+ pgn: string;
9
+ }>;
10
+ /** The latest games of the archive, one line each as the AI reads them: none when there is no archive. */
11
+ export declare function pastGameLines(folder: string, count: number): string[];
@@ -0,0 +1,47 @@
1
+ import { z } from "zod";
2
+ import { writePgn } from "../core/index.js";
3
+ import { archivedGame, archivedGames, archivedWhen } from "../games/index.js";
4
+ import { gameFolder } from "../profiles/index.js";
5
+ import { openingName } from "./opening-book.js";
6
+ import { defineCommand } from "./registry.js";
7
+ /** The AI's memory of the games played (D-57): how the latest went, one line each, or one game whole from the archive. */
8
+ export const pastGames = defineCommand({
9
+ name: "past_games",
10
+ description: "Lists the games played with this player, the latest first: when, your colour, the result, the opening, how long, and its name in the "
11
+ + "archive. With game, that game's PGN with your comments and plans; to comment a game with the player, analyse_game. new_game already gives the "
12
+ + "last three. Example: past_games {}.",
13
+ input: z.object({ game: z.string().optional().describe("a game's name, as the list writes it: its PGN") }),
14
+ readOnly: true,
15
+ idempotent: true,
16
+ run: ({ game }, context) => {
17
+ const folder = gameFolder(context.home());
18
+ return game === undefined ? { games: pastGameLines(folder, listed) } : { pgn: writePgn(archivedGame(folder, game)) };
19
+ },
20
+ format: (past) => ("pgn" in past ? past.pgn : past.games.length === 0 ? "No game played yet." : past.games.join("\n")),
21
+ });
22
+ /** The latest games of the archive, one line each as the AI reads them: none when there is no archive. */
23
+ export function pastGameLines(folder, count) {
24
+ return archivedGames(folder).slice(0, count).flatMap((name) => {
25
+ try {
26
+ return [pastGameLine(name, archivedGame(folder, name))];
27
+ }
28
+ catch {
29
+ // A game the archive cannot read stays out of the memory.
30
+ return [];
31
+ }
32
+ });
33
+ }
34
+ const listed = 20;
35
+ /** 2026-11-03 12:04, you (AI) played Black and won 0-1 (White resigns), B50 Sicilian Defense, 15 moves. games/2026-11-03-120423.pgn */
36
+ function pastGameLine(name, record) {
37
+ const ai = record.tags.Black === "AI" ? "black" : "white";
38
+ const result = record.tags.Result ?? "*";
39
+ const outcome = result === "*" ? ", unfinished" : ` and ${resultFor(result, ai)} ${result}`;
40
+ const termination = record.tags.Termination === undefined || result === "*" ? "" : ` (${record.tags.Termination})`;
41
+ const opening = openingName(record);
42
+ const moves = Math.ceil(record.moves.length / 2);
43
+ return `${archivedWhen(name)}, you (AI) played ${ai === "white" ? "White" : "Black"}${outcome}${termination}${opening === undefined ? "" : `, ${opening}`}, ${moves} moves. ${name}`;
44
+ }
45
+ function resultFor(result, ai) {
46
+ return result === "1/2-1/2" ? "drew" : (result === "1-0") === (ai === "white") ? "won" : "lost";
47
+ }
@@ -0,0 +1,37 @@
1
+ import type { Context, SeenPosition } from "./context.ts";
2
+ import { type MovePlayed } from "./game-summary.ts";
3
+ /** Who plays a move: the AI, or the player through the AI. */
4
+ export type Mover = "ai" | "player";
5
+ /** What comes with a move: the AI's comment, or the player's words from the board page, saved with it, and a draw offer (D-65). */
6
+ export interface WithMove {
7
+ comment?: string;
8
+ offerDraw?: boolean;
9
+ said?: {
10
+ text: string;
11
+ at: Date;
12
+ };
13
+ /** Milliseconds of the mover's turn that were not theirs, not taken off their clock (D-36). */
14
+ allowance?: number;
15
+ /** A coach judges the player's moves with the helps on (D-88): the AI reads it after the player's move. */
16
+ coached?: boolean;
17
+ }
18
+ /**
19
+ * Plays a move for one side `now` and saves the game, with the time it took and the clock after it in its comment ([%clk]). A refused move, or words refused
20
+ * with it, change nothing: it throws the message `refused` writes from the core's.
21
+ */
22
+ export declare function playMove(folder: string, now: Date, mover: Mover, text: string, refused: (message: string, legal: readonly string[]) => string, { comment, said, offerDraw, allowance, coached }?: WithMove): MovePlayed;
23
+ /** A chat's move, resignation or game replaced on a position it has not seen (D-71): it was decided on another. */
24
+ export declare class PositionChanged extends Error {
25
+ constructor();
26
+ }
27
+ /** The position of the profile's game now. */
28
+ export declare function positionIn(folder: string): SeenPosition;
29
+ /**
30
+ * Refuses a chat's move or ending on a position it has not seen, changed by another chat or on the board page, or before any tool showed it the game
31
+ * (D-71). Words and the clock change no position; the command line and the page, which have no chat, are never refused.
32
+ */
33
+ export declare function refuseUnseenPosition(context: Context, folder: string): void;
34
+ /** Refuses a chat's words on a game it has not seen, which another chat or the page started: words on a later move of the same game do no harm. */
35
+ export declare function refuseOtherGame(context: Context, folder: string): void;
36
+ /** Whether two positions a chat may see are the same: the same game, as many plies. */
37
+ export declare function samePosition(one: SeenPosition, other: SeenPosition): boolean;
@@ -0,0 +1,94 @@
1
+ // A move of the AI or of the player on the current game: played, timed by the clock, saved, and archived when it ends the game.
2
+ import { clockAfterMove, clockTag } from "../core/index.js";
3
+ import { archiveEnded, endOf, loadGame, saveGame, talkLine } from "../games/index.js";
4
+ import { gameGoingOn, summaryOf, withThought } from "./game-summary.js";
5
+ import { colourName } from "./view.js";
6
+ /**
7
+ * Plays a move for one side `now` and saves the game, with the time it took and the clock after it in its comment ([%clk]). A refused move, or words refused
8
+ * with it, change nothing: it throws the message `refused` writes from the core's.
9
+ */
10
+ export function playMove(folder, now, mover, text, refused, { comment, said, offerDraw = false, allowance = 0, coached = false } = {}) {
11
+ // A flag falls only past the allowance.
12
+ const before = gameToMove(folder, mover, new Date(now.getTime() - allowance));
13
+ const { game } = before;
14
+ const position = game.position;
15
+ // The words are checked before the move, which changes the live game; they count as said after it.
16
+ const line = said === undefined ? undefined : talkLine(before, mover, said.text, said.at, game.record.moves.length + 1);
17
+ if (mover === "ai" && before.clock?.waiting === true) {
18
+ throw new Error("The clocks wait for the player to start the game with Start on the board page: don't move yet, the doorbell wakes you then. Nothing changed.");
19
+ }
20
+ if (offerDraw && game.record.moves.length === 0) {
21
+ throw new Error("A draw can be offered once each side has made a move: play the move without offer_draw. Nothing changed.");
22
+ }
23
+ const played = game.play(text);
24
+ if (!played.played) {
25
+ throw new Error(refused(played.message, played.legal));
26
+ }
27
+ const clock = before.clock === undefined ? undefined : clockAfterMove(before.clock, position.turn, now, allowance, mover === "player");
28
+ // A move declines the other side's draw offer; the mover's own stands, and the move carries it in the record as FIDE writes it, (=) (D-65). A move that
29
+ // ends the game leaves no offer.
30
+ const offers = (offerDraw || before.drawOffer === mover) && endOf(game) === undefined;
31
+ const current = { ...before, drawOffer: offers ? mover : undefined, ...(clock === undefined ? {} : { clock }) };
32
+ const words = [offers ? "(=)" : undefined, comment, clock === undefined ? undefined : clockTag(clock.left[position.turn])].filter((part) => part !== undefined);
33
+ if (words.length > 0) {
34
+ game.annotate(game.record.moves.length, { comment: words.join(" ") });
35
+ }
36
+ if (line !== undefined) {
37
+ current.talk.push(line);
38
+ }
39
+ saveGame(folder, current);
40
+ const { archive, warning } = endOf(game) === undefined ? {} : archiveEnded(folder, current);
41
+ const result = { move: `${position.moveNumber}${position.turn === "white" ? "." : "..."}${played.move.san}`, game: withThought(summaryOf(current, now, archive), folder, current, coached) };
42
+ if (played.note !== undefined) {
43
+ result.note = played.note;
44
+ }
45
+ if (warning !== undefined) {
46
+ result.warning = warning;
47
+ }
48
+ return result;
49
+ }
50
+ /** A chat's move, resignation or game replaced on a position it has not seen (D-71): it was decided on another. */
51
+ export class PositionChanged extends Error {
52
+ constructor() {
53
+ super("The game may have changed since you last saw it, in another chat or on the board page: call status to see it, then decide. Nothing changed.");
54
+ }
55
+ }
56
+ /** The position of the profile's game now. */
57
+ export function positionIn(folder) {
58
+ const current = loadGame(folder);
59
+ return current === undefined ? { plies: 0 } : { started: current.started, plies: current.game.record.moves.length };
60
+ }
61
+ /**
62
+ * Refuses a chat's move or ending on a position it has not seen, changed by another chat or on the board page, or before any tool showed it the game
63
+ * (D-71). Words and the clock change no position; the command line and the page, which have no chat, are never refused.
64
+ */
65
+ export function refuseUnseenPosition(context, folder) {
66
+ const seen = context.positionSeen;
67
+ if (seen === null || (seen !== undefined && !samePosition(seen, positionIn(folder)))) {
68
+ throw new PositionChanged();
69
+ }
70
+ }
71
+ /** Refuses a chat's words on a game it has not seen, which another chat or the page started: words on a later move of the same game do no harm. */
72
+ export function refuseOtherGame(context, folder) {
73
+ const seen = context.positionSeen;
74
+ const now = positionIn(folder);
75
+ if (now.started !== undefined && (seen === null || (seen !== undefined && seen.started !== now.started))) {
76
+ throw new PositionChanged();
77
+ }
78
+ }
79
+ /** Whether two positions a chat may see are the same: the same game, as many plies. */
80
+ export function samePosition(one, other) {
81
+ return one.started === other.started && one.plies === other.plies;
82
+ }
83
+ function gameToMove(folder, mover, now) {
84
+ const current = gameGoingOn(folder, now);
85
+ const { turn } = current.game.position;
86
+ const aiToMove = turn === current.ai;
87
+ if (mover === "player" && aiToMove) {
88
+ throw new Error(`It is your move (${colourName(turn)}): call ai_move. Nothing changed.`);
89
+ }
90
+ if (mover === "ai" && !aiToMove) {
91
+ throw new Error(`It is the player's move (${colourName(turn)}): wait for it, then call player_move. Nothing changed.`);
92
+ }
93
+ return current;
94
+ }
@@ -0,0 +1,13 @@
1
+ import { z } from "zod";
2
+ import { type MovePlayed } from "./game-summary.ts";
3
+ /** The player's move, written in the chat and given by the AI: it answers with the position the AI decides on (D-34). */
4
+ export declare const playerMove: import("./registry.ts").Command<z.ZodObject<{
5
+ move: z.ZodString;
6
+ offer_draw: z.ZodOptional<z.ZodBoolean>;
7
+ }, z.core.$strip>, PlayerMove>;
8
+ /** The player's move, with the board page's address once a game while no browser has it open (D-85), and the coach to start when it ended the game (D-88). */
9
+ interface PlayerMove extends MovePlayed {
10
+ invite?: string;
11
+ coach?: true;
12
+ }
13
+ export {};
@@ -0,0 +1,51 @@
1
+ import { z } from "zod";
2
+ import { loadGame } from "../games/index.js";
3
+ import { coachAgent } from "../host-files/index.js";
4
+ import { gameFolder } from "../profiles/index.js";
5
+ import { agentsLine, coachOnEnd, coachRuns, withAnalyst } from "./agents.js";
6
+ import { closingLines, lastMoveText, moveLines, shownMove } from "./answer-lines.js";
7
+ import {} from "./game-summary.js";
8
+ import { inviteLine, pageInvite } from "./page-link.js";
9
+ import { playMove, positionIn, refuseUnseenPosition } from "./play-move.js";
10
+ import { defineCommand } from "./registry.js";
11
+ import { newWords } from "./say.js";
12
+ /** The player's move, written in the chat and given by the AI: it answers with the position the AI decides on (D-34). */
13
+ export const playerMove = defineCommand({
14
+ name: "player_move",
15
+ description: "Plays the move the player wrote in the chat, and answers with the position for your reply: the FEN, your legal moves by piece and, from "
16
+ + "the club level up, the pieces attacked with their defenders, the pins and the checks; look gives the rest. Give the move in SAN or as squares, and never choose for "
17
+ + 'them: if their words fit two moves, or it is refused, ask them again. Example: player_move {move: "Nf3"}.',
18
+ input: z.object({
19
+ move: z.string().min(1).describe("the player's move, in SAN (Nf3, exd5, O-O, e8=Q) or as squares (g1f3, e7e8q)"),
20
+ offer_draw: z.boolean().optional().describe("true when the player offers a draw with this move: it stands until your move"),
21
+ }),
22
+ alwaysLoad: true,
23
+ showsGame: true,
24
+ positionals: ["move"],
25
+ run: ({ move, offer_draw: offerDraw = false }, context) => {
26
+ const folder = gameFolder(context.home());
27
+ // A chat that has seen no game yet, new or resumed, plays the player's own move: its answer shows the game the AI decides on (D-71).
28
+ if (context.positionSeen !== null) {
29
+ refuseUnseenPosition(context, folder);
30
+ }
31
+ const started = context.positionSeen === null ? positionIn(folder).started : context.positionSeen?.started;
32
+ const played = playMove(folder, context.now(), "player", move, (message) => `${message} Ask the player again. Nothing changed.`, { allowance: chatAllowance, offerDraw, coached: coachRuns(context) });
33
+ // A game over by this move has no more moves to play there: the next game's window gives the address (Federico, round of D-85).
34
+ const invite = played.game.end === undefined ? pageInvite(context, started) : undefined;
35
+ const ended = played.game.end === undefined ? undefined : loadGame(folder);
36
+ const coach = ended !== undefined && coachOnEnd(context, folder, ended);
37
+ return { ...played, game: withAnalyst(played.game, context), words: newWords(context), ...(invite === undefined ? {} : { invite }), ...(coach ? { coach } : {}) };
38
+ },
39
+ format: (result) => [
40
+ `Player: ${result.game.lastMove === undefined ? shownMove(result.move) : lastMoveText(result.game.lastMove)}.`,
41
+ ...moveLines(result),
42
+ ...(result.invite === undefined ? [] : [inviteLine(result.invite)]),
43
+ ...(result.coach === true ? [agentsLine([coachAgent])] : []),
44
+ ...closingLines(result.game, result.words),
45
+ ].join("\n"),
46
+ });
47
+ /**
48
+ * The time a move in the chat leaves off the player's clock (D-36): the AI writing after its move and reading the player's, about 10 s as Priya measured.
49
+ * Claude Code's hooks will time it exactly; the board page's moves need none.
50
+ */
51
+ const chatAllowance = 10_000;
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ /** The player's words to the AI, as the board page sends them: on the command line only, since the AI never writes for the player. */
3
+ export declare const playerSay: import("./registry.ts").Command<z.ZodObject<{
4
+ text: z.ZodString;
5
+ }, z.core.$strip>, string>;
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { talkLimits } from "../games/index.js";
3
+ import { gameFolder } from "../profiles/index.js";
4
+ import { defineCommand } from "./registry.js";
5
+ import { say } from "./say.js";
6
+ /** The player's words to the AI, as the board page sends them: on the command line only, since the AI never writes for the player. */
7
+ export const playerSay = defineCommand({
8
+ name: "player_say",
9
+ description: "Writes the player's words to the AI, as the board page does: the AI reads them at its next call. "
10
+ + 'Example: chesspal player_say "Play faster, please."',
11
+ input: z.object({ text: z.string().trim().min(1, "write what you say").max(talkLimits.player, `write ${talkLimits.player} characters at most`).describe("what the player says to the AI") }),
12
+ cliOnly: true,
13
+ positionals: ["text"],
14
+ run: ({ text }, context) => say(gameFolder(context.home()), "player", text, context.now()).talk.at(-1).text,
15
+ format: (text) => `Written for the AI: «${text}». It reads it at its next call.`,
16
+ });
@@ -0,0 +1,52 @@
1
+ import { z } from "zod";
2
+ import { type Thought } from "../games/index.ts";
3
+ import { type GameSummary } from "./game-summary.ts";
4
+ /** What ponder answers: stop, the position to think on, the position after a reply, the thoughts kept, its words said, or still the player's move. */
5
+ type Pondered = {
6
+ stop: "moved" | "waited";
7
+ } | {
8
+ start: Start;
9
+ } | {
10
+ after: GameSummary;
11
+ position: number;
12
+ reply: string;
13
+ } | {
14
+ kept: readonly Thought[];
15
+ position: number;
16
+ } | {
17
+ waiting: number;
18
+ } | {
19
+ said: number;
20
+ };
21
+ /** The position the player is to move in, as the AI thinks on it. */
22
+ interface Start {
23
+ position: number;
24
+ line: string;
25
+ plan?: string;
26
+ mood?: string;
27
+ comment?: string;
28
+ talk: string[];
29
+ }
30
+ /**
31
+ * The AI's thinking on the player's time (D-72), for the agent that does it: the position, the view after a reply, its answer kept for its move, and its
32
+ * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to Claude Code alone, which runs the agent.
33
+ */
34
+ export declare const ponder: import("./registry.ts").Command<z.ZodObject<{
35
+ position: z.ZodOptional<z.ZodInt>;
36
+ reply: z.ZodOptional<z.ZodString>;
37
+ move: z.ZodOptional<z.ZodString>;
38
+ why: z.ZodOptional<z.ZodString>;
39
+ say: z.ZodOptional<z.ZodString>;
40
+ done: z.ZodOptional<z.ZodBoolean>;
41
+ }, z.core.$strip>, Pondered>;
42
+ /**
43
+ * Waits for the player's move, for an agent that works on their time (D-72, D-88): moved once `gone` says so, waited once the agent has waited long enough in
44
+ * all since `since`, else still waiting after a while.
45
+ */
46
+ export declare function waitForPlayer(context: {
47
+ now: () => Date;
48
+ pause?: (milliseconds: number) => Promise<void>;
49
+ }, since: Date, gone: () => boolean): Promise<"moved" | "waited" | "waiting">;
50
+ /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
51
+ export declare function wordsOf(comment: string | undefined): string | undefined;
52
+ export {};
@@ -0,0 +1,179 @@
1
+ import { z } from "zod";
2
+ import { Game, levelOf, moodAt, sees, timeLeft } from "../core/index.js";
3
+ import { currentPlan, endOf, keepThought, loadGame, talkLimits, thoughtKey, waitingSince } from "../games/index.js";
4
+ import { ponderAgents } from "../host-files/index.js";
5
+ import { gameFolder } from "../profiles/index.js";
6
+ import { positionLines } from "./answer-lines.js";
7
+ import { summaryOf } from "./game-summary.js";
8
+ import { levelText } from "./levels.js";
9
+ import { defineCommand, sentence } from "./registry.js";
10
+ import { say as sayWords } from "./say.js";
11
+ import { boardView, colourName, compactView } from "./view.js";
12
+ /**
13
+ * The AI's thinking on the player's time (D-72), for the agent that does it: the position, the view after a reply, its answer kept for its move, and its
14
+ * words on the board page. Its calls are not the chat's: the doorbell still wakes the chat for the player's move. Offered to Claude Code alone, which runs the agent.
15
+ */
16
+ export const ponder = defineCommand({
17
+ name: "ponder",
18
+ description: "For the chesspal-ponder agents only, never from the chat: ai_move says when to start one. While the player is to move: ponder {} gives the "
19
+ + "position to think on; ponder {position, reply} the position after a likely reply, as you will see it; ponder {position, reply, move, why} keeps your "
20
+ + "answer to it, which you are given when they play it; ponder {position, say} says a word on the board page; ponder {position, done: true} waits for their "
21
+ + 'move. Whenever it says stop, stop. Example: ponder {position: 24, reply: "Nxe5", move: "d6", why: "The knight has to go back."}.',
22
+ input: z.object({
23
+ position: z.int().min(0).optional().describe("the position's number, as ponder {} gives it"),
24
+ reply: z.string().min(1).optional().describe("a move the player may play, in SAN (Nxe5) or as squares (f3e5)"),
25
+ move: z.string().min(1).optional().describe("your answer to that reply, in SAN or as squares"),
26
+ why: sentence(200).optional().describe("why, in one sentence: kept for you, hidden from the player"),
27
+ say: z.string().trim().min(1).max(talkLimits.ai).optional().describe("your words for the player on the board page, once, in one short sentence, of what has happened: pieces and moves in words, never in notation"),
28
+ done: z.boolean().optional().describe("true when you have no more replies to think on: it waits for the player's move"),
29
+ }),
30
+ byAgents: ponderAgents,
31
+ host: "claude-code",
32
+ run: async ({ position, reply, move, why, say, done = false }, context) => {
33
+ const folder = gameFolder(context.home());
34
+ const current = toPonder(folder, context.now(), position);
35
+ if (current === undefined) {
36
+ return { stop: "moved" };
37
+ }
38
+ if (position === undefined) {
39
+ if (reply !== undefined || move !== undefined || why !== undefined || say !== undefined || done) {
40
+ throw new Error("Give the position's number with it: call ponder {} first, which gives it. Nothing changed.");
41
+ }
42
+ context.ponders?.({ started: current.started, plies: current.game.record.moves.length });
43
+ return { start: startOf(current) };
44
+ }
45
+ if ((say !== undefined || done) && (reply !== undefined || move !== undefined || why !== undefined || (say !== undefined && done))) {
46
+ throw new Error("One thing a call: say, done, or a reply with its answer. Nothing changed.");
47
+ }
48
+ if (say !== undefined) {
49
+ sayWords(folder, "agent", say, context.now());
50
+ return { said: position };
51
+ }
52
+ if (done) {
53
+ context.ponders?.(undefined);
54
+ return await waitForMove(folder, context, current);
55
+ }
56
+ if (reply === undefined) {
57
+ throw new Error(`Name a reply of the player's: ponder {position: ${position}, reply}, or ponder {position: ${position}, done: true} when you have no more. Nothing changed.`);
58
+ }
59
+ if (move === undefined && why !== undefined) {
60
+ throw new Error(`Give your move with its why: ponder {position: ${position}, reply, move, why}. Nothing changed.`);
61
+ }
62
+ const after = new Game(current.game.record);
63
+ const played = after.play(reply);
64
+ if (!played.played) {
65
+ throw new Error(`${played.message} The player's legal moves: ${played.legal.join(" ")}. Nothing changed.`);
66
+ }
67
+ if (move === undefined) {
68
+ return { after: summaryOf({ game: after, ai: current.ai, started: current.started, talk: [] }, context.now()), position, reply: played.move.san };
69
+ }
70
+ if (why === undefined) {
71
+ throw new Error(`Say why in one sentence: ponder {position: ${position}, reply, move, why}. Nothing changed.`);
72
+ }
73
+ const answer = after.play(move);
74
+ if (!answer.played) {
75
+ throw new Error(`${answer.message} Your legal moves after ${played.move.san}: ${answer.legal.join(" ")}. Nothing changed.`);
76
+ }
77
+ return { kept: keepThought(folder, thoughtKey(current), { reply: played.move.san, move: answer.move.san, why }), position };
78
+ },
79
+ format: (pondered) => {
80
+ if ("stop" in pondered) {
81
+ return pondered.stop === "moved"
82
+ ? "Stop now: the player has moved, or the game is over. End with one word."
83
+ : "Stop now: you have waited long enough, and your answers are kept. End with one word.";
84
+ }
85
+ if ("start" in pondered) {
86
+ return startLines(pondered.start).join("\n");
87
+ }
88
+ if ("after" in pondered) {
89
+ const { after, position, reply } = pondered;
90
+ return [`If the player plays ${reply}:`, ...positionLines(after), `Keep your answer: ponder {position: ${position}, reply: "${reply}", move, why}.`].join("\n");
91
+ }
92
+ if ("kept" in pondered) {
93
+ const replies = pondered.kept.map((thought) => thought.reply).join(", ");
94
+ return `Kept: your answers to ${replies}. Think on another likely reply, or call ponder {position: ${pondered.position}, done: true} when you have no more.`;
95
+ }
96
+ if ("said" in pondered) {
97
+ return "Said on the board page, where the player reads it.";
98
+ }
99
+ return `Still the player's move: call ponder {position: ${pondered.waiting}, done: true} again.`;
100
+ },
101
+ });
102
+ /**
103
+ * Waits for the player's move, for an agent that works on their time (D-72, D-88): moved once `gone` says so, waited once the agent has waited long enough in
104
+ * all since `since`, else still waiting after a while.
105
+ */
106
+ export async function waitForPlayer(context, since, gone) {
107
+ const pause = context.pause ?? ((milliseconds) => new Promise((done) => setTimeout(done, milliseconds)));
108
+ const start = context.now().getTime();
109
+ const end = Math.min(start + playerWait.most, since.getTime() + playerWait.inAll);
110
+ while (context.now().getTime() < end) {
111
+ await pause(playerWait.every);
112
+ if (gone()) {
113
+ return "moved";
114
+ }
115
+ }
116
+ return end < start + playerWait.most ? "waited" : "waiting";
117
+ }
118
+ /** The words of the AI's move for the player: its comment without the plan, the mood, the clock and the draw offer it carries. */
119
+ export function wordsOf(comment) {
120
+ const words = comment?.replace(/\[%[^\]]*\]/g, "").replace("(=)", "").replace(/\s+/g, " ").trim();
121
+ return words === undefined || words === "" ? undefined : words;
122
+ }
123
+ /**
124
+ * How long one call waits for the player's move at most, under the minute a host may give a tool (Priya), how often it looks, and how long the agent
125
+ * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya).
126
+ */
127
+ const playerWait = { most: 50_000, every: 500, inAll: 10 * 60_000 };
128
+ /** The game whose position the AI thinks on, read and never written: none once the player has moved, the game is over or their flag has fallen, or the position given is another. */
129
+ function toPonder(folder, now, position) {
130
+ const current = loadGame(folder);
131
+ if (current === undefined || endOf(current.game) !== undefined || current.game.position.turn === current.ai) {
132
+ return undefined;
133
+ }
134
+ // A flag fallen is the page's or the chat's to write, so that the doorbell rings it (Federico).
135
+ const { turn } = current.game.position;
136
+ if (current.clock !== undefined && timeLeft(current.clock, turn, now)[turn] <= 0) {
137
+ return undefined;
138
+ }
139
+ return position === undefined || position === current.game.record.moves.length ? current : undefined;
140
+ }
141
+ /** Waits for the player's move: stop once they have moved or the agent has waited long enough in all, else still their move after a while. */
142
+ async function waitForMove(folder, context, current) {
143
+ const position = current.game.record.moves.length;
144
+ const waited = await waitForPlayer(context, waitingSince(folder, thoughtKey(current), context.now()), () => toPonder(folder, context.now(), position) === undefined);
145
+ return waited === "waiting" ? { waiting: position } : { stop: waited };
146
+ }
147
+ function startOf(current) {
148
+ const { game, ai, talk } = current;
149
+ const level = levelOf(game.record);
150
+ const plan = currentPlan(game.record);
151
+ const player = game.position.turn;
152
+ const { mood } = moodAt(game.record, ai);
153
+ const comment = wordsOf(game.record.moves.at(-1)?.comment);
154
+ const line = [
155
+ `Position ${game.record.moves.length}: the player (${colourName(player)}) to move, move ${game.position.moveNumber}; you play ${colourName(ai)} ${levelText(level)}.`,
156
+ `FEN: ${game.position.fen}`,
157
+ ...(sees(level, "tactics") ? compactView : boardView)(game.facts, player),
158
+ ].join("\n");
159
+ const said = talk.slice(-3).map((spoken) => `${spoken.who === "ai" ? "You" : "Player"}: «${spoken.text.replace(/\s+/g, " ")}»`);
160
+ return {
161
+ position: game.record.moves.length,
162
+ line,
163
+ ...(plan === undefined ? {} : { plan }),
164
+ ...(mood === "neutral" ? {} : { mood }),
165
+ ...(comment === undefined ? {} : { comment }),
166
+ talk: said,
167
+ };
168
+ }
169
+ function startLines({ position, line, plan, mood, comment, talk }) {
170
+ return [
171
+ line,
172
+ ...(plan === undefined ? [] : [`Your plan: ${plan}`]),
173
+ ...(mood === undefined ? [] : [`Your mood: ${mood}.`]),
174
+ comment === undefined ? "You said nothing with your move." : `You said with your move: «${comment}»`,
175
+ ...(talk.length === 0 ? [] : ["Last words on the board page:", ...talk]),
176
+ `For each reply you think likely, ponder {position: ${position}, reply} shows the position after it, then ponder {position: ${position}, reply, move, why} keeps `
177
+ + `your answer; ponder {position: ${position}, done: true} when you have no more.`,
178
+ ];
179
+ }