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,35 @@
1
+ import { z } from "zod";
2
+ import { type GameSummary } from "./game-summary.ts";
3
+ import { type Speech } from "./speech.ts";
4
+ interface Status {
5
+ version: string;
6
+ folder: string;
7
+ /** The board page's address, where the player plays when the chat serves it. */
8
+ page?: string;
9
+ /** No browser has the board page open: the player needs its address first. */
10
+ pageClosed?: true;
11
+ /** Who plays, from the profile in use (D-23), once the player has completed it (D-71). */
12
+ player?: string;
13
+ /** The player's language when it is not English. */
14
+ speech?: Speech;
15
+ game?: GameSummary;
16
+ /** Why the game on disk cannot be read, when it cannot. */
17
+ problem?: string;
18
+ /** What the player wrote on the board page that the AI has not answered yet, all of it. */
19
+ words?: string[];
20
+ /** The game in analysis on the board page once none goes on (D-69), which the player may have brought there. */
21
+ analysis?: {
22
+ game: string;
23
+ brought?: true;
24
+ analyst?: true;
25
+ };
26
+ /** A game going on from before this chat, told once a chat: the player says whether to go on with it. */
27
+ fromBefore?: true;
28
+ /** The coach to start, once, for the player's move that ended the game (D-88). */
29
+ coach?: true;
30
+ /** The files an older init wrote in the player's folder, which this version would write otherwise. */
31
+ outdated?: string[];
32
+ }
33
+ /** What Chesspal sees now, the first call of a chat: a dashboard under 150 tokens while the player is to move, with the next step. */
34
+ export declare const status: import("./registry.ts").Command<z.ZodObject<{}, z.core.$strip>, Status>;
35
+ export {};
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import { archivedName, cleanSince, endOf, gameInAnalysis, lastChatStart } from "../games/index.js";
3
+ import { coachAgent, outdatedHostFiles } from "../host-files/index.js";
4
+ import { gameFolder } from "../profiles/index.js";
5
+ import { version } from "../version.js";
6
+ import { agentsLine, analystLine, analystRuns, coachOnEnd, coachRuns, reviewedLine, withAnalyst } from "./agents.js";
7
+ import { closingLines, newGameStep, positionLines, sidesLine, wordLines } from "./answer-lines.js";
8
+ import { gameAt } from "./clock.js";
9
+ import { summaryOf, withThought } from "./game-summary.js";
10
+ import { levelText } from "./levels.js";
11
+ import { closedPage, pageGiven, pageLine } from "./page-link.js";
12
+ import { playerLine } from "./profiles.js";
13
+ import { defineCommand } from "./registry.js";
14
+ import { unansweredWords } from "./say.js";
15
+ import { speechLine, speechOf } from "./speech.js";
16
+ /** What Chesspal sees now, the first call of a chat: a dashboard under 150 tokens while the player is to move, with the next step. */
17
+ export const status = defineCommand({
18
+ name: "status",
19
+ description: "Shows what Chesspal sees now: its version, the profile's folder and the game, with your next step. Call it at the start of every chat, "
20
+ + "before anything else.",
21
+ input: z.object({}),
22
+ // Read only for the host: a flag that has fallen is written, as the time already decided.
23
+ readOnly: true,
24
+ alwaysLoad: true,
25
+ showsGame: true,
26
+ run: (input, context) => {
27
+ // The folder is the profile's, in the data folder (D-71): the names of its games are relative to it.
28
+ const folder = gameFolder(context.home());
29
+ const player = playerLine(context);
30
+ const speech = speechOf(folder, context.language);
31
+ const outdated = outdatedHostFiles(context.folder, context.platform);
32
+ const status = { version, folder, ...(player === undefined ? {} : { player }), ...(speech === undefined ? {} : { speech }), ...(outdated.length === 0 ? {} : { outdated }) };
33
+ if (context.page !== undefined) {
34
+ status.page = context.page;
35
+ }
36
+ if (closedPage(context) !== undefined) {
37
+ status.pageClosed = true;
38
+ }
39
+ const first = firstInChat(context);
40
+ try {
41
+ const current = gameAt(folder, context.now());
42
+ // With no game the player may have written before the first (D-77).
43
+ const words = unansweredWords(context);
44
+ if (words.length > 0) {
45
+ status.words = words;
46
+ }
47
+ if (current !== undefined) {
48
+ status.game = withAnalyst(withThought(summaryOf(current, context.now(), archivedName(folder, current)), folder, current, coachRuns(context)), context, current);
49
+ if (status.pageClosed === true) {
50
+ pageGiven(context, current.started);
51
+ }
52
+ if (first && endOf(current.game) === undefined && fromBeforeChat(context, current.started)) {
53
+ status.fromBefore = true;
54
+ }
55
+ // The player's move on the board page that ended the game: the coach judges it too (D-88).
56
+ if (coachOnEnd(context, folder, current)) {
57
+ status.coach = true;
58
+ }
59
+ }
60
+ const analysis = current === undefined || status.game?.end !== undefined ? gameInAnalysis(folder) : undefined;
61
+ if (analysis !== undefined) {
62
+ // The analyst agent goes over a game the player brought, where it runs, and the chat over what it reviewed (D-89).
63
+ status.analysis = { game: analysis.game, ...(analysis.brought === true ? { brought: true } : {}), ...(analystRuns(context) ? { analyst: true } : {}) };
64
+ }
65
+ }
66
+ catch (error) {
67
+ status.problem = error.message;
68
+ }
69
+ return status;
70
+ },
71
+ format: ({ version, folder, page, pageClosed, player, speech, game, problem, words = [], analysis, fromBefore, coach, outdated }) => [
72
+ `Chesspal ${version}. Folder: ${folder}`,
73
+ ...(outdated === undefined ? [] : [outdatedLine(outdated)]),
74
+ ...(page === undefined ? [] : [pageLine(page, pageClosed === true)]),
75
+ ...(player === undefined ? [] : [player]),
76
+ ...(speech === undefined ? [] : [speechLine(speech)]),
77
+ ...(fromBefore === true ? [fromBeforeLine] : []),
78
+ ...(problem !== undefined ? [problem] : gameLines(game, words, analysis?.brought === true, pageClosed === true, coach === true)),
79
+ ...(analysis === undefined ? [] : [analysisLine(analysis)]),
80
+ ].join("\n"),
81
+ });
82
+ /** An older init's files in the player's folder: the player runs init again, with the chat closed, since the host reads them as a chat starts. */
83
+ function outdatedLine(files) {
84
+ return `This folder was set up by an older Chesspal (${files.join(", ")}): tell the player, in their language, to close this chat, run "chesspal init" `
85
+ + "in this folder and open a new chat. Until then the agents and your instructions are the old ones.";
86
+ }
87
+ // The game going on as a chat starts: the player may have come back for it, or want another.
88
+ const fromBeforeLine = "A game from before this chat is going on: ask the player whether to go on with it. For a new one, " + newGameStep;
89
+ /**
90
+ * Whether this is the chat's first status: once a chat where the MCP server keeps what it told, again in a chat cleared or summarized, since its start keys
91
+ * it (D-57); never on the command line.
92
+ */
93
+ function firstInChat(context) {
94
+ const key = `game before ${lastChatStart(context.folder)?.at ?? ""}`;
95
+ if (context.told === undefined || context.told.has(key)) {
96
+ return false;
97
+ }
98
+ context.told.add(key);
99
+ return true;
100
+ }
101
+ /** Whether a game began before the chat: one a summarized or resumed chat still holds is its own; without the host's word, any game is. */
102
+ function fromBeforeChat(context, started) {
103
+ const start = lastChatStart(context.folder);
104
+ return start === undefined || cleanSince(start, started);
105
+ }
106
+ /** The game in analysis; the next step when the player has just brought it on the board page. */
107
+ function analysisLine({ game, brought, analyst }) {
108
+ if (brought === true) {
109
+ return analyst === true
110
+ ? `The player brought ${game} on the board page to analyse it. Next: ${analystLine(game)}`
111
+ : `The player brought ${game} on the board page to analyse it. Next: call analyse_game {"game": "${game}"}, then keep your review on that page with annotate_game, and go over it with them.`;
112
+ }
113
+ return analyst === true
114
+ ? `In analysis on the board page: ${game}. ${reviewedLine(game)}`
115
+ : `In analysis on the board page: ${game}; analyse_game {"game": "${game}"} goes over it with the player.`;
116
+ }
117
+ /**
118
+ * The game's lines; a game the player brought to analyse takes the place of the next step. The coach to start goes just before the next step, where
119
+ * the AI acts on it (Priya).
120
+ */
121
+ function gameLines(game, words, brought, pageClosed, coach) {
122
+ const closing = [...(coach ? [agentsLine([coachAgent])] : []), ...(brought ? wordLines(words) : closingLines(game, words, pageClosed))];
123
+ if (game === undefined) {
124
+ return ["No game.", ...closing];
125
+ }
126
+ const moves = game.lastMoves === "" ? [] : [`Moves: ${game.lastMoves}`];
127
+ return [`Game: ${sidesLine(game.ai)}, ${levelText(game.level)}.`, ...moves, ...positionLines(game), ...closing];
128
+ }
@@ -0,0 +1,5 @@
1
+ import { z } from "zod";
2
+ /** The games kept to study, one line each, the latest kept first. */
3
+ export declare const studyGames: import("./registry.ts").Command<z.ZodObject<{}, z.core.$strip>, {
4
+ games: string[];
5
+ }>;
@@ -0,0 +1,40 @@
1
+ // The games kept to study (D-68): played elsewhere and brought to analyse_game as a PGN, in the folder of the profile that brought them.
2
+ import { z } from "zod";
3
+ import { keptGame, keptGames } 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
+ import { knownTag } from "./study.js";
8
+ /** The games kept to study, one line each, the latest kept first. */
9
+ export const studyGames = defineCommand({
10
+ name: "study_games",
11
+ description: "Lists the games kept to study, which the player brought from elsewhere, the latest kept first: who played, when, "
12
+ + "the result, the opening, how long, and its name. analyse_game with pgn keeps a game here; analyse_game, look and position take its name as game. "
13
+ + "Example: study_games {}.",
14
+ input: z.object({}),
15
+ readOnly: true,
16
+ idempotent: true,
17
+ run: (input, context) => {
18
+ const folder = gameFolder(context.home());
19
+ return { games: keptGames(folder).flatMap((name) => studyLines(folder, name)) };
20
+ },
21
+ format: ({ games }) => (games.length === 0 ? "No game kept to study yet: analyse_game with the PGN of a game played elsewhere keeps it here." : games.join("\n")),
22
+ });
23
+ /** Ann vs Bob, 2026.10.01: 1-0, C23 Bishop's Opening, 4 moves. study/2026-11-04-153012.pgn; none for a game that cannot be read. */
24
+ function studyLines(root, name) {
25
+ let record;
26
+ try {
27
+ record = keptGame(root, name);
28
+ }
29
+ catch {
30
+ // A game that cannot be read stays out of the list.
31
+ return [];
32
+ }
33
+ const { tags, moves } = record;
34
+ const date = knownTag(tags.Date);
35
+ const result = tags.Result === undefined || tags.Result === "*" ? "result unknown" : tags.Result;
36
+ const opening = openingName(record);
37
+ const count = Math.ceil(moves.length / 2);
38
+ const players = `${tags.White ?? "?"} vs ${tags.Black ?? "?"}${date === undefined ? "" : `, ${date}`}`;
39
+ return [`${players}: ${result}${opening === undefined ? "" : `, ${opening}`}, ${count} ${count === 1 ? "move" : "moves"}. ${name}`];
40
+ }
@@ -0,0 +1,38 @@
1
+ import { z } from "zod";
2
+ import { Game, type GameRecord } from "../core/index.ts";
3
+ import { type CurrentGame } from "../games/index.ts";
4
+ import type { Context } from "./context.ts";
5
+ /** What look and position take to show any position of a game that is over. */
6
+ export declare const studyInput: {
7
+ game: z.ZodOptional<z.ZodString>;
8
+ move: z.ZodOptional<z.ZodString>;
9
+ };
10
+ /** A game to study: its record, and its name in the archive when it is there. */
11
+ export interface Studied {
12
+ readonly record: GameRecord;
13
+ readonly name?: string;
14
+ }
15
+ /** A position of a game: the game as it stood after a move, and that move as the analysis writes it (12...Kf8); none at the start. */
16
+ export interface StudiedPosition {
17
+ readonly game: Game;
18
+ readonly move?: string;
19
+ }
20
+ /** The current game when it is over, or none; refused while it goes on, for every game: games are analysed once the one being played is over. */
21
+ export declare function noGameGoingOn(context: Context): CurrentGame | undefined;
22
+ /** The game played or kept to study by that name; without one, the current game over, or else the latest of the archive; refused while a game goes on. */
23
+ export declare function gameToStudy(context: Context, name?: string): Studied;
24
+ /** A game played or kept to study by its name as the lists write it, or without its folder or its .pgn; refused when it is not there. */
25
+ export declare function namedGame(context: Context, name: string): Required<Studied>;
26
+ /** The index of a move in the game's record, given as analyse_game numbers it: 12. for White's, 12... for Black's, 12...Kf8 too. */
27
+ export declare function moveIndex(record: GameRecord, move: string): number;
28
+ /** The number of each move of a game as the analysis writes it: 1. for White's first move, 1... for Black's. */
29
+ export declare function moveNumbers(record: GameRecord): string[];
30
+ /**
31
+ * The game as it stood after a move, given by its number (12. for White's, 12... for Black's) and, if wanted, the move itself (12...Kf8), which decides
32
+ * the side when the dots do not; after the last move without one.
33
+ */
34
+ export declare function positionAfter(record: GameRecord, move?: string): StudiedPosition;
35
+ /** A tag PGN leaves unknown with ? or -, as nothing. */
36
+ export declare function knownTag(tag: string | undefined): string | undefined;
37
+ /** Where a position of a game studied stands: After 12...Kf8 in games/2026-11-02-153012.pgn: White to move. */
38
+ export declare function positionLine({ game, move }: StudiedPosition, name?: string): string;
@@ -0,0 +1,106 @@
1
+ // A game to study (D-68): one that is over, played here or kept to study, and any position of it, by its moves as the analysis numbers them. While a game
2
+ // goes on, nothing is studied: the analysis would help the game being played (D-60, D-62).
3
+ import { z } from "zod";
4
+ import { Game } from "../core/index.js";
5
+ import { archivedGame, archivedGames, archivedName, endOf, keptGame, listedName } from "../games/index.js";
6
+ import { gameFolder } from "../profiles/index.js";
7
+ import { gameAt } from "./clock.js";
8
+ import { colourName } from "./view.js";
9
+ /** What look and position take to show any position of a game that is over. */
10
+ export const studyInput = {
11
+ game: z.string().optional().describe("a game's name, as past_games, study_games or analyse_game writes it: one of its positions"),
12
+ move: z.string().optional().describe("the position after this move of a game over, as analyse_game numbers it: 12. for White's, 12... for Black's"),
13
+ };
14
+ /** The current game when it is over, or none; refused while it goes on, for every game: games are analysed once the one being played is over. */
15
+ export function noGameGoingOn(context) {
16
+ const current = gameAt(gameFolder(context.home()), context.now());
17
+ if (current !== undefined && endOf(current.game) === undefined) {
18
+ throw new Error("A game is going on: games are analysed once it is over, this one and the others, and not by you from a PGN or a file either. Tell the player, and go "
19
+ + "over it when it ends. Nothing changed.");
20
+ }
21
+ return current;
22
+ }
23
+ /** The game played or kept to study by that name; without one, the current game over, or else the latest of the archive; refused while a game goes on. */
24
+ export function gameToStudy(context, name) {
25
+ const current = noGameGoingOn(context);
26
+ const folder = gameFolder(context.home());
27
+ if (name !== undefined) {
28
+ return namedGame(context, name);
29
+ }
30
+ if (current !== undefined) {
31
+ // The archive's record, which keeps the AI's review (D-69).
32
+ const archived = archivedName(folder, current);
33
+ return archived === undefined ? { record: current.game.record } : { record: archivedGame(folder, archived), name: archived };
34
+ }
35
+ const [latest] = archivedGames(folder);
36
+ if (latest === undefined) {
37
+ throw new Error("There is no game to study: play one with new_game, or give analyse_game the PGN of a game played elsewhere.");
38
+ }
39
+ return { record: archivedGame(folder, latest), name: latest };
40
+ }
41
+ /** A game played or kept to study by its name as the lists write it, or without its folder or its .pgn; refused when it is not there. */
42
+ export function namedGame(context, name) {
43
+ const folder = gameFolder(context.home());
44
+ const full = listedName(folder, name);
45
+ return { record: full.startsWith("study/") ? keptGame(folder, full) : archivedGame(folder, full), name: full };
46
+ }
47
+ /** The index of a move in the game's record, given as analyse_game numbers it: 12. for White's, 12... for Black's, 12...Kf8 too. */
48
+ export function moveIndex(record, move) {
49
+ return plyOf(record, moveNumbers(record), move);
50
+ }
51
+ /** The number of each move of a game as the analysis writes it: 1. for White's first move, 1... for Black's. */
52
+ export function moveNumbers(record) {
53
+ const start = new Game({ tags: record.tags, moves: [] }).position;
54
+ const blackFirst = start.turn === "black" ? 1 : 0;
55
+ return record.moves.map((move, index) => {
56
+ const ply = index + blackFirst;
57
+ return `${start.moveNumber + Math.floor(ply / 2)}${ply % 2 === 0 ? "." : "..."}`;
58
+ });
59
+ }
60
+ /**
61
+ * The game as it stood after a move, given by its number (12. for White's, 12... for Black's) and, if wanted, the move itself (12...Kf8), which decides
62
+ * the side when the dots do not; after the last move without one.
63
+ */
64
+ export function positionAfter(record, move) {
65
+ const numbers = moveNumbers(record);
66
+ const ply = move === undefined ? numbers.length - 1 : plyOf(record, numbers, move);
67
+ if (ply < 0) {
68
+ return { game: new Game(record) };
69
+ }
70
+ return { game: new Game({ ...record, moves: record.moves.slice(0, ply + 1) }), move: `${numbers[ply]}${record.moves[ply].san}` };
71
+ }
72
+ /** A tag PGN leaves unknown with ? or -, as nothing. */
73
+ export function knownTag(tag) {
74
+ return tag === undefined || /^[?.\-\s]*$/.test(tag) ? undefined : tag;
75
+ }
76
+ /** Where a position of a game studied stands: After 12...Kf8 in games/2026-11-02-153012.pgn: White to move. */
77
+ export function positionLine({ game, move }, name) {
78
+ const where = `${move === undefined ? "At the start" : `After ${move}`}${name === undefined ? "" : ` in ${name}`}`;
79
+ const { outcome, position } = game;
80
+ return `${where}: ${outcome.over ? `the board ends the game by ${outcome.ending}` : `${colourName(position.turn)} to move`}.`;
81
+ }
82
+ function plyOf(record, numbers, move) {
83
+ const [, number, dots, san] = movePattern.exec(move) ?? [];
84
+ if (number === undefined) {
85
+ throw new Error(`"${move}" is not a move of the game: give its number as analyse_game writes it, 12. for White's move and 12... for Black's, such as 12...Kf8.`);
86
+ }
87
+ if (numbers.length === 0) {
88
+ throw new Error("This game has no moves: look and position without move show its position.");
89
+ }
90
+ const white = numbers.indexOf(`${Number(number)}.`);
91
+ const black = numbers.indexOf(`${Number(number)}...`);
92
+ const plies = (dots === "..." || dots === "…" ? [black] : [white, black]).filter((ply) => ply >= 0);
93
+ const found = san === undefined ? plies[0] : plies.find((ply) => isMove(record, ply, san));
94
+ if (found === undefined) {
95
+ const played = [white, black].filter((ply) => ply >= 0).map((ply) => `${numbers[ply]}${record.moves[ply].san}`);
96
+ const there = played.length === 0 ? `its moves go from ${numbers[0]}${record.moves[0].san} to ${numbers.at(-1)}${record.moves.at(-1).san}` : `move ${number} is ${played.join(" ")}`;
97
+ throw new Error(`There is no ${move.trim()} in this game: ${there}.`);
98
+ }
99
+ return found;
100
+ }
101
+ /** Whether the move written is the one the game played at that ply, however it is written: Nf3, g1f3, Nf3+. */
102
+ function isMove(record, ply, written) {
103
+ const played = new Game({ ...record, moves: record.moves.slice(0, ply) }).playRecorded(written);
104
+ return played.played && played.move.san === record.moves[ply].san;
105
+ }
106
+ const movePattern = /^\s*(\d+)\s*(\.\.\.|\.|…)?\s*(\S+)?\s*$/;
@@ -0,0 +1,14 @@
1
+ import type { Facts, Material, Side } from "../core/index.ts";
2
+ /** The view after the player's move: the check, the pieces attacked, then what is at risk, the pins, the checks and the moves of each piece. */
3
+ export declare function compactView(facts: Facts, turn: Side): string[];
4
+ /** The view of a level without the tactical facts (D-60): the check and the legal moves by piece, which are the rules (D-17). */
5
+ export declare function boardView(facts: Facts, turn: Side): string[];
6
+ /** The whole view, for look: every piece with its attackers and defenders, then the same lines as the compact view. */
7
+ export declare function fullView(facts: Facts, turn: Side): string[];
8
+ /** What is at risk, in one line for a move of the analysis (D-68): undefended Black Nf6; by a cheaper piece White Qd1; outnumbered Black Pe5; none when nothing is. */
9
+ export declare function riskLine(facts: Facts): string | undefined;
10
+ export declare function colourName(side: Side): string;
11
+ /** The material as the player sees it beside the board (D-59): who is ahead and by how much, and the pieces each side took. */
12
+ export declare function materialText(material: Material): string;
13
+ /** Who is ahead in material, and by how many pawns: White +2, or even. */
14
+ export declare function balanceText(balance: number): string;
@@ -0,0 +1,125 @@
1
+ /** The view after the player's move: the check, the pieces attacked, then what is at risk, the pins, the checks and the moves of each piece. */
2
+ export function compactView(facts, turn) {
3
+ const names = namesOf(facts);
4
+ const attacked = officersOf(facts).filter((piece) => piece.attackers.length > 0);
5
+ const pieces = attacked.length === 0 ? ["Attacked pieces: none"] : ["Attacked pieces (attackers / defenders):", ...attacked.map((piece) => pieceLine(piece, names, true))];
6
+ return [...checkLines(facts, turn, names), ...pieces, ...tacticLines(facts, turn, names)];
7
+ }
8
+ /** The view of a level without the tactical facts (D-60): the check and the legal moves by piece, which are the rules (D-17). */
9
+ export function boardView(facts, turn) {
10
+ const names = namesOf(facts);
11
+ return [...checkLines(facts, turn, names), ...legalLines(facts, names)];
12
+ }
13
+ /** The whole view, for look: every piece with its attackers and defenders, then the same lines as the compact view. */
14
+ export function fullView(facts, turn) {
15
+ const names = namesOf(facts);
16
+ const sides = sidesInOrder.flatMap((side) => [
17
+ `${colourName(side)} pieces (attackers / defenders):`,
18
+ ...officersOf(facts).filter((piece) => piece.side === side).map((piece) => pieceLine(piece, names, false)),
19
+ ]);
20
+ return [...checkLines(facts, turn, names), ...sides, ...tacticLines(facts, turn, names)];
21
+ }
22
+ /** What is at risk, in one line for a move of the analysis (D-68): undefended Black Nf6; by a cheaper piece White Qd1; outnumbered Black Pe5; none when nothing is. */
23
+ export function riskLine(facts) {
24
+ const names = namesOf(facts);
25
+ const kinds = [["undefended", facts.undefended], ["by a cheaper piece", facts.attackedByCheaper], ["outnumbered", facts.outnumbered]];
26
+ const text = kinds.filter(([, squares]) => squares.length > 0).map(([kind, squares]) => `${kind} ${bySide(facts, squares, names)}`).join("; ");
27
+ return text === "" ? undefined : text;
28
+ }
29
+ export function colourName(side) {
30
+ return side === "white" ? "White" : "Black";
31
+ }
32
+ /** The material as the player sees it beside the board (D-59): who is ahead and by how much, and the pieces each side took. */
33
+ export function materialText(material) {
34
+ const ahead = balanceText(material.balance);
35
+ if (material.taken.white.length === 0 && material.taken.black.length === 0) {
36
+ return `${ahead}, nothing taken`;
37
+ }
38
+ return `${ahead} (White took ${takenText(material.taken.white)}, Black took ${takenText(material.taken.black)})`;
39
+ }
40
+ /** Who is ahead in material, and by how many pawns: White +2, or even. */
41
+ export function balanceText(balance) {
42
+ return balance === 0 ? "even" : `${balance > 0 ? "White" : "Black"} +${Math.abs(balance)}`;
43
+ }
44
+ /** The pieces taken, counted by kind: Q 2R 7P. */
45
+ function takenText(taken) {
46
+ const kinds = [...new Set(taken)];
47
+ return kinds.map((piece) => {
48
+ const count = taken.filter((other) => other === piece).length;
49
+ return count === 1 ? piece : `${count}${piece}`;
50
+ }).join(" ") || "nothing";
51
+ }
52
+ const sidesInOrder = ["white", "black"];
53
+ /** The check the side to move is in, from which pieces; none when it is not. */
54
+ function checkLines(facts, turn, names) {
55
+ return facts.checkedBy === undefined ? [] : [`${colourName(turn)} is in check from ${facts.checkedBy.map(names.piece).join(" and ")}.`];
56
+ }
57
+ /** What is at risk, loose and pinned, the checks of both sides, and the legal moves of the side to move by piece. */
58
+ function tacticLines(facts, turn, names) {
59
+ const list = (squares) => bySide(facts, squares, names) || "none";
60
+ const atRisk = facts.undefended.length === 0 && facts.attackedByCheaper.length === 0 && facts.outnumbered.length === 0
61
+ ? "At risk: none"
62
+ : `Attacked and undefended: ${list(facts.undefended)}; defended, but attacked by a cheaper piece: ${list(facts.attackedByCheaper)}; `
63
+ + `attacked more times than defended: ${list(facts.outnumbered)}`;
64
+ const pins = facts.pins.map((pin) => `${names.sided(pin.square)} to ${names.piece(pin.to)} by ${names.piece(pin.by)}`);
65
+ return [
66
+ atRisk,
67
+ `Loose: ${list(facts.loose)}`,
68
+ `Pinned: ${pins.join(", ") || "none"}`,
69
+ ...checksLines(facts, turn),
70
+ ...legalLines(facts, names),
71
+ ];
72
+ }
73
+ /** The checks the side to move can give, and those the other side could give if it were to move; one line when neither has any. */
74
+ function checksLines(facts, turn) {
75
+ if (facts.checks.length === 0 && facts.otherChecks?.length === 0) {
76
+ return ["Checks: none for either side"];
77
+ }
78
+ const other = colourName(turn === "white" ? "black" : "white");
79
+ const theirs = facts.otherChecks === undefined ? [] : [`Checks ${other} could give if it were to move: ${facts.otherChecks.join(" ") || "none"}`];
80
+ return [`Checks ${colourName(turn)} (to move) can give: ${facts.checks.join(" ") || "none"}`, ...theirs];
81
+ }
82
+ /** The legal moves of the side to move by piece, with how many, and its pieces that have none; nothing when it has no move, the game over. */
83
+ function legalLines(facts, names) {
84
+ const mine = facts.pieces.filter((piece) => piece.moves !== undefined);
85
+ const moving = mine.filter((piece) => piece.moves.length > 0);
86
+ const stuck = mine.filter((piece) => piece.moves.length === 0).map((piece) => names.piece(piece.square));
87
+ const count = moving.reduce((total, piece) => total + piece.moves.length, 0);
88
+ if (count === 0) {
89
+ return [];
90
+ }
91
+ const byPiece = moving.map((piece) => `${names.piece(piece.square)} (${piece.moves.length}) ${piece.moves.join(" ")}`);
92
+ return [`Legal by piece (${count}): ${[...byPiece, ...(stuck.length === 0 ? [] : [`no moves: ${stuck.join(" ")}`])].join("; ")}`];
93
+ }
94
+ /** A piece with the pieces that attack it and those that defend it, the pieces lined up behind them in brackets: Pf7: Bc4 Ng5 / Ke8; Qd2(+Rd1). */
95
+ function pieceLine(piece, names, withSide) {
96
+ const list = (squares) => squares.map((square) => `${names.piece(square)}${behindOf(piece, square, names)}`).join(" ") || "-";
97
+ const name = withSide ? names.sided(piece.square) : names.piece(piece.square);
98
+ return `${name}: ${list(piece.attackers)} / ${list(piece.defenders)}`;
99
+ }
100
+ function behindOf(piece, square, names) {
101
+ const behind = piece.behind?.[square];
102
+ return behind === undefined ? "" : `(+${behind.map(names.piece).join("+")})`;
103
+ }
104
+ /** Pieces by side, White's first: White Ra1 Pb2, Black Ra8. */
105
+ function bySide(facts, squares, names) {
106
+ const sideAt = new Map(facts.pieces.map((piece) => [piece.square, piece.side]));
107
+ const groups = sidesInOrder.map((side) => [side, squares.filter((square) => sideAt.get(square) === side)]).filter(([, group]) => group.length > 0);
108
+ return groups.map(([side, group]) => `${colourName(side)} ${group.map(names.piece).join(" ")}`).join(", ");
109
+ }
110
+ /** The pieces but the kings: a king attacked is a check, said in its own line, and a king is never defended. */
111
+ function officersOf(facts) {
112
+ return facts.pieces.filter((piece) => piece.piece !== "K");
113
+ }
114
+ function namesOf(facts) {
115
+ const pieces = new Map(facts.pieces.map((piece) => [piece.square, piece]));
116
+ const piece = (square) => {
117
+ const found = pieces.get(square);
118
+ return found === undefined ? square : `${found.piece}${square}`;
119
+ };
120
+ const sided = (square) => {
121
+ const found = pieces.get(square);
122
+ return found === undefined ? square : `${colourName(found.side)} ${found.piece}${square}`;
123
+ };
124
+ return { piece, sided };
125
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The board of a FEN as text: White at the bottom, the pieces as Unicode figurines, the empty squares and the files as signs a browser draws as wide as the
3
+ * figurines and a terminal in one cell each, so that the columns stay in line in Claude's desktop app and in a terminal alike.
4
+ */
5
+ export declare function boardText(fen: string): string;
6
+ /** A move in SAN as the player is shown it, with the pieces' solid figurines for either side (D-86): Nxe4 is ♞xe4 and e8=Q is e8=♛. */
7
+ export declare function figurinesOf(san: string): string;
@@ -0,0 +1,18 @@
1
+ // The board and the moves as text for the player in the chat (D-36), with the pieces' figurines (D-86): the moves for the AI to copy, never its view of the board (D-26).
2
+ /**
3
+ * The board of a FEN as text: White at the bottom, the pieces as Unicode figurines, the empty squares and the files as signs a browser draws as wide as the
4
+ * figurines and a terminal in one cell each, so that the columns stay in line in Claude's desktop app and in a terminal alike.
5
+ */
6
+ export function boardText(fen) {
7
+ const ranks = (fen.split(" ")[0] ?? "").split("/").map((rank) => rank.replace(/\d/g, (empty) => ".".repeat(Number(empty))));
8
+ const files = ` ${fileSigns.join(" ")}`;
9
+ return [files, ...ranks.map((rank, index) => `${8 - index} ${[...rank].map((square) => figurines[square] ?? emptySquare).join(" ")} ${8 - index}`), files].join("\n");
10
+ }
11
+ /** A move in SAN as the player is shown it, with the pieces' solid figurines for either side (D-86): Nxe4 is ♞xe4 and e8=Q is e8=♛. */
12
+ export function figurinesOf(san) {
13
+ return san.replace(/^[KQRBN]|(?<==)[QRBN]/g, (letter) => figurines[letter.toLowerCase()] ?? letter);
14
+ }
15
+ // The pieces' figurines by their FEN letters; the black pawn's asks for its text form, since alone it shows as an emoji.
16
+ const figurines = { K: "♔", Q: "♕", R: "♖", B: "♗", N: "♘", P: "♙", k: "♚", q: "♛", r: "♜", b: "♝", n: "♞", p: "♟︎" };
17
+ const emptySquare = "⊙";
18
+ const fileSigns = [..."ⓐⓑⓒⓓⓔⓕⓖⓗ"];