chesspal 0.0.1 → 0.1.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 (225) hide show
  1. package/README.md +136 -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 +8 -0
  7. package/dist/cli/command-line.js +178 -0
  8. package/dist/commands/ai-move.d.ts +37 -0
  9. package/dist/commands/ai-move.js +92 -0
  10. package/dist/commands/ai-say.d.ts +20 -0
  11. package/dist/commands/ai-say.js +40 -0
  12. package/dist/commands/analyse-game.d.ts +61 -0
  13. package/dist/commands/analyse-game.js +238 -0
  14. package/dist/commands/annotate-game.d.ts +35 -0
  15. package/dist/commands/annotate-game.js +78 -0
  16. package/dist/commands/clock.d.ts +15 -0
  17. package/dist/commands/clock.js +45 -0
  18. package/dist/commands/context.d.ts +56 -0
  19. package/dist/commands/context.js +36 -0
  20. package/dist/commands/draw-offer.d.ts +10 -0
  21. package/dist/commands/draw-offer.js +43 -0
  22. package/dist/commands/drawings.d.ts +15 -0
  23. package/dist/commands/drawings.js +23 -0
  24. package/dist/commands/end-game.d.ts +23 -0
  25. package/dist/commands/end-game.js +64 -0
  26. package/dist/commands/export-game.d.ts +14 -0
  27. package/dist/commands/export-game.js +26 -0
  28. package/dist/commands/game-summary.d.ts +95 -0
  29. package/dist/commands/game-summary.js +239 -0
  30. package/dist/commands/index.d.ts +3 -0
  31. package/dist/commands/index.js +19 -0
  32. package/dist/commands/init.d.ts +15 -0
  33. package/dist/commands/init.js +73 -0
  34. package/dist/commands/levels.d.ts +9 -0
  35. package/dist/commands/levels.js +25 -0
  36. package/dist/commands/look.d.ts +31 -0
  37. package/dist/commands/look.js +61 -0
  38. package/dist/commands/new-game.d.ts +69 -0
  39. package/dist/commands/new-game.js +215 -0
  40. package/dist/commands/opening-book.d.ts +13 -0
  41. package/dist/commands/opening-book.js +84 -0
  42. package/dist/commands/page-link.d.ts +11 -0
  43. package/dist/commands/page-link.js +28 -0
  44. package/dist/commands/past-games.d.ts +11 -0
  45. package/dist/commands/past-games.js +47 -0
  46. package/dist/commands/play-move.d.ts +44 -0
  47. package/dist/commands/play-move.js +102 -0
  48. package/dist/commands/player-move.d.ts +17 -0
  49. package/dist/commands/player-move.js +44 -0
  50. package/dist/commands/player-say.d.ts +5 -0
  51. package/dist/commands/player-say.js +16 -0
  52. package/dist/commands/ponder.d.ts +42 -0
  53. package/dist/commands/ponder.js +167 -0
  54. package/dist/commands/position.d.ts +14 -0
  55. package/dist/commands/position.js +128 -0
  56. package/dist/commands/profiles.d.ts +60 -0
  57. package/dist/commands/profiles.js +129 -0
  58. package/dist/commands/registry.d.ts +54 -0
  59. package/dist/commands/registry.js +61 -0
  60. package/dist/commands/say.d.ts +16 -0
  61. package/dist/commands/say.js +84 -0
  62. package/dist/commands/status.d.ts +29 -0
  63. package/dist/commands/status.js +103 -0
  64. package/dist/commands/study-games.d.ts +5 -0
  65. package/dist/commands/study-games.js +40 -0
  66. package/dist/commands/study.d.ts +38 -0
  67. package/dist/commands/study.js +122 -0
  68. package/dist/commands/view.d.ts +14 -0
  69. package/dist/commands/view.js +125 -0
  70. package/dist/core/board-text.d.ts +7 -0
  71. package/dist/core/board-text.js +18 -0
  72. package/dist/core/book.json +1 -0
  73. package/dist/core/clock.d.ts +45 -0
  74. package/dist/core/clock.js +109 -0
  75. package/dist/core/facts.d.ts +55 -0
  76. package/dist/core/facts.js +175 -0
  77. package/dist/core/game.d.ts +93 -0
  78. package/dist/core/game.js +348 -0
  79. package/dist/core/illegal.d.ts +19 -0
  80. package/dist/core/illegal.js +141 -0
  81. package/dist/core/index.d.ts +19 -0
  82. package/dist/core/index.js +11 -0
  83. package/dist/core/levels.d.ts +20 -0
  84. package/dist/core/levels.js +33 -0
  85. package/dist/core/material.d.ts +9 -0
  86. package/dist/core/material.js +12 -0
  87. package/dist/core/moods.d.ts +23 -0
  88. package/dist/core/moods.js +45 -0
  89. package/dist/core/openings.d.ts +37 -0
  90. package/dist/core/openings.js +98 -0
  91. package/dist/core/openings.json +1 -0
  92. package/dist/core/pgn.d.ts +10 -0
  93. package/dist/core/pgn.js +213 -0
  94. package/dist/core/profile.d.ts +117 -0
  95. package/dist/core/profile.js +232 -0
  96. package/dist/core/record.d.ts +21 -0
  97. package/dist/core/record.js +25 -0
  98. package/dist/core/review.d.ts +74 -0
  99. package/dist/core/review.js +126 -0
  100. package/dist/games/ai-model.d.ts +5 -0
  101. package/dist/games/ai-model.js +15 -0
  102. package/dist/games/analysis.d.ts +12 -0
  103. package/dist/games/analysis.js +26 -0
  104. package/dist/games/archive.d.ts +42 -0
  105. package/dist/games/archive.js +168 -0
  106. package/dist/games/chat-start.d.ts +16 -0
  107. package/dist/games/chat-start.js +26 -0
  108. package/dist/games/current-game.d.ts +54 -0
  109. package/dist/games/current-game.js +128 -0
  110. package/dist/games/files.d.ts +9 -0
  111. package/dist/games/files.js +52 -0
  112. package/dist/games/index.d.ts +10 -0
  113. package/dist/games/index.js +11 -0
  114. package/dist/games/plans.d.ts +9 -0
  115. package/dist/games/plans.js +23 -0
  116. package/dist/games/table-talk.d.ts +12 -0
  117. package/dist/games/table-talk.js +25 -0
  118. package/dist/games/talk.d.ts +40 -0
  119. package/dist/games/talk.js +131 -0
  120. package/dist/games/thoughts.d.ts +15 -0
  121. package/dist/games/thoughts.js +60 -0
  122. package/dist/home/data-folder.d.ts +12 -0
  123. package/dist/home/data-folder.js +55 -0
  124. package/dist/home/index.d.ts +2 -0
  125. package/dist/home/index.js +3 -0
  126. package/dist/home/open-folder.d.ts +2 -0
  127. package/dist/home/open-folder.js +14 -0
  128. package/dist/host-files/host-files.d.ts +10 -0
  129. package/dist/host-files/host-files.js +167 -0
  130. package/dist/host-files/index.d.ts +2 -0
  131. package/dist/host-files/index.js +3 -0
  132. package/dist/host-files/ponder-agents.d.ts +12 -0
  133. package/dist/host-files/ponder-agents.js +40 -0
  134. package/dist/index.d.ts +4 -0
  135. package/dist/index.js +2 -0
  136. package/dist/page/app.js +576 -0
  137. package/dist/page/avatars/beanie.webp +0 -0
  138. package/dist/page/avatars/beard.webp +0 -0
  139. package/dist/page/avatars/bun.webp +0 -0
  140. package/dist/page/avatars/cat.webp +0 -0
  141. package/dist/page/avatars/curls.webp +0 -0
  142. package/dist/page/avatars/glasses.webp +0 -0
  143. package/dist/page/avatars/moustache.webp +0 -0
  144. package/dist/page/avatars/owl.webp +0 -0
  145. package/dist/page/avatars/short-hair.webp +0 -0
  146. package/dist/page/avatars/silver-bob.webp +0 -0
  147. package/dist/page/avatars.js +16 -0
  148. package/dist/page/badges.js +50 -0
  149. package/dist/page/board.js +566 -0
  150. package/dist/page/browse.js +17 -0
  151. package/dist/page/clock-time.js +12 -0
  152. package/dist/page/dialog.js +26 -0
  153. package/dist/page/ending.js +73 -0
  154. package/dist/page/explain.js +58 -0
  155. package/dist/page/fold-tab.js +9 -0
  156. package/dist/page/folds.js +43 -0
  157. package/dist/page/game-column.js +44 -0
  158. package/dist/page/header.js +27 -0
  159. package/dist/page/helps-view.js +51 -0
  160. package/dist/page/helps.js +90 -0
  161. package/dist/page/main.js +8 -0
  162. package/dist/page/model-name.js +20 -0
  163. package/dist/page/move-list.js +68 -0
  164. package/dist/page/new-game-dialog.js +42 -0
  165. package/dist/page/opening-book.js +14 -0
  166. package/dist/page/pieces/CREDITS.md +36 -0
  167. package/dist/page/pieces/cburnett-small.svg +20 -0
  168. package/dist/page/pieces/cburnett.svg +20 -0
  169. package/dist/page/pieces/nitida-small.svg +19 -0
  170. package/dist/page/pieces/nitida.svg +20 -0
  171. package/dist/page/pieces/torretta-small.svg +19 -0
  172. package/dist/page/pieces/torretta.svg +19 -0
  173. package/dist/page/pieces.js +28 -0
  174. package/dist/page/player-card.js +60 -0
  175. package/dist/page/plies.js +62 -0
  176. package/dist/page/profile-dialogs.js +123 -0
  177. package/dist/page/profile.js +95 -0
  178. package/dist/page/settings-panel.js +32 -0
  179. package/dist/page/settings.js +76 -0
  180. package/dist/page/sounds/capture.ogg +0 -0
  181. package/dist/page/sounds/check.ogg +0 -0
  182. package/dist/page/sounds/draw.ogg +0 -0
  183. package/dist/page/sounds/flag.ogg +0 -0
  184. package/dist/page/sounds/mate.ogg +0 -0
  185. package/dist/page/sounds/move.ogg +0 -0
  186. package/dist/page/sounds/offer.ogg +0 -0
  187. package/dist/page/sounds/resign.ogg +0 -0
  188. package/dist/page/sounds.js +105 -0
  189. package/dist/page/studied.js +14 -0
  190. package/dist/page/study-dialog.js +58 -0
  191. package/dist/page/style.css +4398 -0
  192. package/dist/page/talk.js +172 -0
  193. package/dist/page/text.js +593 -0
  194. package/dist/page/torretta.js +38 -0
  195. package/dist/page/voice.js +38 -0
  196. package/dist/profiles/avatar.d.ts +23 -0
  197. package/dist/profiles/avatar.js +84 -0
  198. package/dist/profiles/front-matter.d.ts +9 -0
  199. package/dist/profiles/front-matter.js +33 -0
  200. package/dist/profiles/index.d.ts +3 -0
  201. package/dist/profiles/index.js +5 -0
  202. package/dist/profiles/profiles.d.ts +58 -0
  203. package/dist/profiles/profiles.js +237 -0
  204. package/dist/profiles/settings.d.ts +12 -0
  205. package/dist/profiles/settings.js +46 -0
  206. package/dist/server/analyses.d.ts +17 -0
  207. package/dist/server/analyses.js +46 -0
  208. package/dist/server/bell.d.ts +60 -0
  209. package/dist/server/bell.js +209 -0
  210. package/dist/server/caller.d.ts +10 -0
  211. package/dist/server/caller.js +99 -0
  212. package/dist/server/http.d.ts +16 -0
  213. package/dist/server/http.js +57 -0
  214. package/dist/server/mcp.d.ts +14 -0
  215. package/dist/server/mcp.js +179 -0
  216. package/dist/server/page.d.ts +46 -0
  217. package/dist/server/page.js +657 -0
  218. package/dist/server/profile-routes.d.ts +19 -0
  219. package/dist/server/profile-routes.js +186 -0
  220. package/dist/version.d.ts +2 -0
  221. package/dist/version.js +3 -0
  222. package/instructions/analyst.md +8 -0
  223. package/instructions/opponent.md +15 -0
  224. package/instructions/ponder.md +9 -0
  225. package/package.json +66 -5
@@ -0,0 +1,102 @@
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, reviewed } 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, judged } = {}) {
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
+ if (judged !== undefined && game.record.moves.length === 0) {
24
+ throw new Error("The player has made no move yet: play without badge and note. Nothing changed.");
25
+ }
26
+ const played = game.play(text);
27
+ if (!played.played) {
28
+ throw new Error(refused(played.message, played.legal));
29
+ }
30
+ if (judged !== undefined) {
31
+ // The player's move is the one before the move just played: its badge and note sit in its comment, beside its clock.
32
+ const judging = game.record.moves.length - 1;
33
+ const { nags, comment: judgedComment } = reviewed(game.record.moves[judging - 1], judged);
34
+ game.annotate(judging, { nags, comment: judgedComment ?? "" });
35
+ }
36
+ const clock = before.clock === undefined ? undefined : clockAfterMove(before.clock, position.turn, now, allowance, mover === "player");
37
+ // 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
38
+ // ends the game leaves no offer.
39
+ const offers = (offerDraw || before.drawOffer === mover) && endOf(game) === undefined;
40
+ const current = { ...before, drawOffer: offers ? mover : undefined, ...(clock === undefined ? {} : { clock }) };
41
+ const words = [offers ? "(=)" : undefined, comment, clock === undefined ? undefined : clockTag(clock.left[position.turn])].filter((part) => part !== undefined);
42
+ if (words.length > 0) {
43
+ game.annotate(game.record.moves.length, { comment: words.join(" ") });
44
+ }
45
+ if (line !== undefined) {
46
+ current.talk.push(line);
47
+ }
48
+ saveGame(folder, current);
49
+ const { archive, warning } = endOf(game) === undefined ? {} : archiveEnded(folder, current);
50
+ const result = { move: `${position.moveNumber}${position.turn === "white" ? "." : "..."}${played.move.san}`, game: withThought(summaryOf(current, now, archive), folder, current) };
51
+ if (played.note !== undefined) {
52
+ result.note = played.note;
53
+ }
54
+ if (warning !== undefined) {
55
+ result.warning = warning;
56
+ }
57
+ return result;
58
+ }
59
+ /** A chat's move, resignation or game replaced on a position it has not seen (D-71): it was decided on another. */
60
+ export class PositionChanged extends Error {
61
+ constructor() {
62
+ 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.");
63
+ }
64
+ }
65
+ /** The position of the profile's game now. */
66
+ export function positionIn(folder) {
67
+ const current = loadGame(folder);
68
+ return current === undefined ? { plies: 0 } : { started: current.started, plies: current.game.record.moves.length };
69
+ }
70
+ /**
71
+ * 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
72
+ * (D-71). Words and the clock change no position; the command line and the page, which have no chat, are never refused.
73
+ */
74
+ export function refuseUnseenPosition(context, folder) {
75
+ const seen = context.positionSeen;
76
+ if (seen === null || (seen !== undefined && !samePosition(seen, positionIn(folder)))) {
77
+ throw new PositionChanged();
78
+ }
79
+ }
80
+ /** 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. */
81
+ export function refuseOtherGame(context, folder) {
82
+ const seen = context.positionSeen;
83
+ const now = positionIn(folder);
84
+ if (now.started !== undefined && (seen === null || (seen !== undefined && seen.started !== now.started))) {
85
+ throw new PositionChanged();
86
+ }
87
+ }
88
+ function samePosition(one, other) {
89
+ return one.started === other.started && one.plies === other.plies;
90
+ }
91
+ function gameToMove(folder, mover, now) {
92
+ const current = gameGoingOn(folder, now);
93
+ const { turn } = current.game.position;
94
+ const aiToMove = turn === current.ai;
95
+ if (mover === "player" && aiToMove) {
96
+ throw new Error(`It is your move (${colourName(turn)}): call ai_move. Nothing changed.`);
97
+ }
98
+ if (mover === "ai" && !aiToMove) {
99
+ throw new Error(`It is the player's move (${colourName(turn)}): wait for it, then call player_move. Nothing changed.`);
100
+ }
101
+ return current;
102
+ }
@@ -0,0 +1,17 @@
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). */
9
+ interface PlayerMove extends MovePlayed {
10
+ invite?: string;
11
+ }
12
+ /**
13
+ * 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.
14
+ * Claude Code's hooks will time it exactly (T-53); the board page's moves need none.
15
+ */
16
+ export declare const chatAllowance = 10000;
17
+ export {};
@@ -0,0 +1,44 @@
1
+ import { z } from "zod";
2
+ import { gameFolder } from "../profiles/index.js";
3
+ import { closingLines, lastMoveText, moveLines, shownMove } from "./game-summary.js";
4
+ import { inviteLine, pageInvite } from "./page-link.js";
5
+ import { playMove, positionIn, refuseUnseenPosition } from "./play-move.js";
6
+ import { defineCommand } from "./registry.js";
7
+ import { newWords } from "./say.js";
8
+ /** The player's move, written in the chat and given by the AI: it answers with the position the AI decides on (D-34). */
9
+ export const playerMove = defineCommand({
10
+ name: "player_move",
11
+ 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 "
12
+ + "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 "
13
+ + 'them: if their words fit two moves, or it is refused, ask them again. Example: player_move {move: "Nf3"}.',
14
+ input: z.object({
15
+ move: z.string().min(1).describe("the player's move, in SAN (Nf3, exd5, O-O, e8=Q) or as squares (g1f3, e7e8q)"),
16
+ offer_draw: z.boolean().optional().describe("true when the player offers a draw with this move: it stands until your move"),
17
+ }),
18
+ alwaysLoad: true,
19
+ showsGame: true,
20
+ positionals: ["move"],
21
+ run: ({ move, offer_draw: offerDraw = false }, context) => {
22
+ const folder = gameFolder(context.home());
23
+ // 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, T-129).
24
+ if (context.positionSeen !== null) {
25
+ refuseUnseenPosition(context, folder);
26
+ }
27
+ const started = context.positionSeen === null ? positionIn(folder).started : context.positionSeen?.started;
28
+ const played = playMove(folder, context.now(), "player", move, (message) => `${message} Ask the player again. Nothing changed.`, { allowance: chatAllowance, offerDraw });
29
+ // 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).
30
+ const invite = played.game.end === undefined ? pageInvite(context, started) : undefined;
31
+ return { ...played, words: newWords(context), ...(invite === undefined ? {} : { invite }) };
32
+ },
33
+ format: (result) => [
34
+ `Player: ${result.game.lastMove === undefined ? shownMove(result.move) : lastMoveText(result.game.lastMove)}.`,
35
+ ...moveLines(result),
36
+ ...(result.invite === undefined ? [] : [inviteLine(result.invite)]),
37
+ ...closingLines(result.game, result.words),
38
+ ].join("\n"),
39
+ });
40
+ /**
41
+ * 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.
42
+ * Claude Code's hooks will time it exactly (T-53); the board page's moves need none.
43
+ */
44
+ export 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 (T-65): 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 (T-65): 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,42 @@
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
+ export {};
@@ -0,0 +1,167 @@
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 { gameFolder } from "../profiles/index.js";
5
+ import { positionLines, summaryOf } from "./game-summary.js";
6
+ import { levelText } from "./levels.js";
7
+ import { defineCommand, sentence } from "./registry.js";
8
+ import { say as sayWords } from "./say.js";
9
+ import { boardView, colourName, compactView } from "./view.js";
10
+ /**
11
+ * 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
12
+ * 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.
13
+ */
14
+ export const ponder = defineCommand({
15
+ name: "ponder",
16
+ 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 "
17
+ + "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 "
18
+ + "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 "
19
+ + 'move. Whenever it says stop, stop. Example: ponder {position: 24, reply: "Nxe5", move: "d6", why: "The knight has to go back."}.',
20
+ input: z.object({
21
+ position: z.int().min(0).optional().describe("the position's number, as ponder {} gives it"),
22
+ reply: z.string().min(1).optional().describe("a move the player may play, in SAN (Nxe5) or as squares (f3e5)"),
23
+ move: z.string().min(1).optional().describe("your answer to that reply, in SAN or as squares"),
24
+ why: sentence(200).optional().describe("why, in one sentence: kept for you, hidden from the player"),
25
+ 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"),
26
+ done: z.boolean().optional().describe("true when you have no more replies to think on: it waits for the player's move"),
27
+ }),
28
+ byAgent: true,
29
+ host: "claude-code",
30
+ run: async ({ position, reply, move, why, say, done = false }, context) => {
31
+ const folder = gameFolder(context.home());
32
+ const current = toPonder(folder, context.now(), position);
33
+ if (current === undefined) {
34
+ return { stop: "moved" };
35
+ }
36
+ if (position === undefined) {
37
+ if (reply !== undefined || move !== undefined || why !== undefined || say !== undefined || done) {
38
+ throw new Error("Give the position's number with it: call ponder {} first, which gives it. Nothing changed.");
39
+ }
40
+ return { start: startOf(current) };
41
+ }
42
+ if ((say !== undefined || done) && (reply !== undefined || move !== undefined || why !== undefined || (say !== undefined && done))) {
43
+ throw new Error("One thing a call: say, done, or a reply with its answer. Nothing changed.");
44
+ }
45
+ if (say !== undefined) {
46
+ sayWords(folder, "agent", say, context.now());
47
+ return { said: position };
48
+ }
49
+ if (done) {
50
+ return await waitForMove(folder, context, current);
51
+ }
52
+ if (reply === undefined) {
53
+ 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.`);
54
+ }
55
+ if (move === undefined && why !== undefined) {
56
+ throw new Error(`Give your move with its why: ponder {position: ${position}, reply, move, why}. Nothing changed.`);
57
+ }
58
+ const after = new Game(current.game.record);
59
+ const played = after.play(reply);
60
+ if (!played.played) {
61
+ throw new Error(`${played.message} The player's legal moves: ${played.legal.join(" ")}. Nothing changed.`);
62
+ }
63
+ if (move === undefined) {
64
+ return { after: summaryOf({ game: after, ai: current.ai, started: current.started, talk: [] }, context.now()), position, reply: played.move.san };
65
+ }
66
+ if (why === undefined) {
67
+ throw new Error(`Say why in one sentence: ponder {position: ${position}, reply, move, why}. Nothing changed.`);
68
+ }
69
+ const answer = after.play(move);
70
+ if (!answer.played) {
71
+ throw new Error(`${answer.message} Your legal moves after ${played.move.san}: ${answer.legal.join(" ")}. Nothing changed.`);
72
+ }
73
+ return { kept: keepThought(folder, thoughtKey(current), { reply: played.move.san, move: answer.move.san, why }), position };
74
+ },
75
+ format: (pondered) => {
76
+ if ("stop" in pondered) {
77
+ return pondered.stop === "moved"
78
+ ? "Stop now: the player has moved, or the game is over. End with one word."
79
+ : "Stop now: you have waited long enough, and your answers are kept. End with one word.";
80
+ }
81
+ if ("start" in pondered) {
82
+ return startLines(pondered.start).join("\n");
83
+ }
84
+ if ("after" in pondered) {
85
+ const { after, position, reply } = pondered;
86
+ return [`If the player plays ${reply}:`, ...positionLines(after), `Keep your answer: ponder {position: ${position}, reply: "${reply}", move, why}.`].join("\n");
87
+ }
88
+ if ("kept" in pondered) {
89
+ const replies = pondered.kept.map((thought) => thought.reply).join(", ");
90
+ return `Kept: your answers to ${replies}. Think on another likely reply, or call ponder {position: ${pondered.position}, done: true} when you have no more.`;
91
+ }
92
+ if ("said" in pondered) {
93
+ return "Said on the board page, where the player reads it.";
94
+ }
95
+ return `Still the player's move: call ponder {position: ${pondered.waiting}, done: true} again.`;
96
+ },
97
+ });
98
+ /**
99
+ * How long one call waits for the player's move at most, under the minute a host may give a tool (Priya, T-109), how often it looks, and how long the agent
100
+ * waits in all before it stops: a player who leaves the game open must not keep it asking (Priya, round of T-109).
101
+ */
102
+ const ponderWait = { most: 50_000, every: 500, inAll: 10 * 60_000 };
103
+ /** 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. */
104
+ function toPonder(folder, now, position) {
105
+ const current = loadGame(folder);
106
+ if (current === undefined || endOf(current.game) !== undefined || current.game.position.turn === current.ai) {
107
+ return undefined;
108
+ }
109
+ // A flag fallen is the page's or the chat's to write, so that the doorbell rings it (Federico, round of T-109).
110
+ const { turn } = current.game.position;
111
+ if (current.clock !== undefined && timeLeft(current.clock, turn, now)[turn] <= 0) {
112
+ return undefined;
113
+ }
114
+ return position === undefined || position === current.game.record.moves.length ? current : undefined;
115
+ }
116
+ /** 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. */
117
+ async function waitForMove(folder, context, current) {
118
+ const pause = context.pause ?? ((milliseconds) => new Promise((done) => setTimeout(done, milliseconds)));
119
+ const position = current.game.record.moves.length;
120
+ const start = context.now().getTime();
121
+ const end = Math.min(start + ponderWait.most, waitingSince(folder, thoughtKey(current), context.now()).getTime() + ponderWait.inAll);
122
+ while (context.now().getTime() < end) {
123
+ await pause(ponderWait.every);
124
+ if (toPonder(folder, context.now(), position) === undefined) {
125
+ return { stop: "moved" };
126
+ }
127
+ }
128
+ return end < start + ponderWait.most ? { stop: "waited" } : { waiting: position };
129
+ }
130
+ function startOf(current) {
131
+ const { game, ai, talk } = current;
132
+ const level = levelOf(game.record);
133
+ const plan = currentPlan(game.record);
134
+ const player = game.position.turn;
135
+ const { mood } = moodAt(game.record, ai);
136
+ const comment = wordsOf(game.record.moves.at(-1)?.comment);
137
+ const line = [
138
+ `Position ${game.record.moves.length}: the player (${colourName(player)}) to move, move ${game.position.moveNumber}; you play ${colourName(ai)} ${levelText(level)}.`,
139
+ `FEN: ${game.position.fen}`,
140
+ ...(sees(level, "tactics") ? compactView : boardView)(game.facts, player),
141
+ ].join("\n");
142
+ const said = talk.slice(-3).map((spoken) => `${spoken.who === "ai" ? "You" : "Player"}: «${spoken.text.replace(/\s+/g, " ")}»`);
143
+ return {
144
+ position: game.record.moves.length,
145
+ line,
146
+ ...(plan === undefined ? {} : { plan }),
147
+ ...(mood === "neutral" ? {} : { mood }),
148
+ ...(comment === undefined ? {} : { comment }),
149
+ talk: said,
150
+ };
151
+ }
152
+ function startLines({ position, line, plan, mood, comment, talk }) {
153
+ return [
154
+ line,
155
+ ...(plan === undefined ? [] : [`Your plan: ${plan}`]),
156
+ ...(mood === undefined ? [] : [`Your mood: ${mood}.`]),
157
+ comment === undefined ? "You said nothing with your move." : `You said with your move: «${comment}»`,
158
+ ...(talk.length === 0 ? [] : ["Last words on the board page:", ...talk]),
159
+ `For each reply you think likely, ponder {position: ${position}, reply} shows the position after it, then ponder {position: ${position}, reply, move, why} keeps `
160
+ + `your answer; ponder {position: ${position}, done: true} when you have no more.`,
161
+ ];
162
+ }
163
+ /** 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. */
164
+ function wordsOf(comment) {
165
+ const words = comment?.replace(/\[%[^\]]*\]/g, "").replace("(=)", "").replace(/\s+/g, " ").trim();
166
+ return words === undefined || words === "" ? undefined : words;
167
+ }
@@ -0,0 +1,14 @@
1
+ import { z } from "zod";
2
+ import { type Profile } from "../core/index.ts";
3
+ /** The positional profile of a position: of a game studied, with where it stands. */
4
+ type Described = Profile & {
5
+ where?: string;
6
+ };
7
+ /** The positional profile of the game in progress (D-19, T-12), or of any position of a game over (D-68): separate facts, never a score. */
8
+ export declare const position: import("./registry.ts").Command<z.ZodObject<{
9
+ game: z.ZodOptional<z.ZodString>;
10
+ move: z.ZodOptional<z.ZodString>;
11
+ }, z.core.$strip>, Described>;
12
+ /** The profile as lines for the AI, White's facts before Black's; a line with nothing to say is left out. */
13
+ export declare function profileLines(profile: Profile): string[];
14
+ export {};
@@ -0,0 +1,128 @@
1
+ import { z } from "zod";
2
+ import {} from "../core/index.js";
3
+ import { gameFolder } from "../profiles/index.js";
4
+ import { gameAt } from "./clock.js";
5
+ import { refuseUnseen } from "./levels.js";
6
+ import { defineCommand } from "./registry.js";
7
+ import { gameToStudy, positionAfter, positionLine, studyInput } from "./study.js";
8
+ import { colourName, materialText } from "./view.js";
9
+ /** The positional profile of the game in progress (D-19, T-12), or of any position of a game over (D-68): separate facts, never a score. */
10
+ export const position = defineCommand({
11
+ name: "position",
12
+ description: "Shows the positional facts of the position, from the strong level up and at every level once the game is over: material, pawns (isolated, doubled, backward, passed, islands), open files, rooks and queens on the seventh, bishops, "
13
+ + "outposts, centre, space, control, mobility, development and castling, king safety, the phase, the draw rules and, with only kings and pawns, "
14
+ + "the rule of the square and the opposition. Facts only, never a score: weigh them yourself. Call it when you set or change your plan: out of "
15
+ + "the opening, after a big exchange, in the endgame; not at every move. With move, and game for a game of the archive, the facts of any position of a "
16
+ + 'game over, to analyse it. Example: position {}, position {move: "24."}.',
17
+ input: z.object(studyInput),
18
+ readOnly: true,
19
+ idempotent: true,
20
+ run: ({ game, move }, context) => {
21
+ if (game !== undefined || move !== undefined) {
22
+ const studied = gameToStudy(context, game);
23
+ const at = positionAfter(studied.record, move);
24
+ return { ...at.game.profile, where: positionLine(at, studied.name) };
25
+ }
26
+ const current = gameAt(gameFolder(context.home()), context.now());
27
+ if (current === undefined) {
28
+ throw new Error("There is no game to describe: start one with new_game.");
29
+ }
30
+ refuseUnseen(current, "strategy", "position");
31
+ return current.game.profile;
32
+ },
33
+ format: (described) => [...(described.where === undefined ? [] : [described.where]), ...profileLines(described)].join("\n"),
34
+ });
35
+ /** The profile as lines for the AI, White's facts before Black's; a line with nothing to say is left out. */
36
+ export function profileLines(profile) {
37
+ const sides = [["white", profile.white], ["black", profile.black]];
38
+ const perSide = (line) => sides.map(([side, facts]) => {
39
+ const text = line(facts);
40
+ return text === undefined ? undefined : `${colourName(side)} ${text}`;
41
+ }).filter((text) => text !== undefined).join("; ");
42
+ const lines = [
43
+ `Phase: ${profile.phase}`,
44
+ `Material: ${materialText(profile.material)}`,
45
+ ...sides.map(([side, facts]) => pawnLine(side, facts)),
46
+ filesLine(profile),
47
+ seventhLine(perSide((side) => joined(side.onSeventh))),
48
+ bishopsLine(profile),
49
+ `Outposts: ${perSide((side) => joined(side.outposts.map((outpost) => (outpost.piece === undefined ? outpost.square : `${outpost.piece}${outpost.square}`)))) || "none"}`,
50
+ `Centre (White/Black pieces reaching it): ${profile.centre.map(centreText).join(", ")}`,
51
+ `Space (squares of the other half attacked): White ${profile.white.space}, Black ${profile.black.space}`,
52
+ `Control: only White ${profile.controlled.white}, only Black ${profile.controlled.black}, both ${profile.controlled.contested}`,
53
+ `Mobility: ${perSide((side) => (side.mobility === undefined ? undefined : mobilityText(side.mobility)))}`,
54
+ `Development: ${perSide(developmentText)}`,
55
+ ...sides.flatMap(([side, facts]) => (facts.king === undefined ? [] : [kingLine(side, facts.king)])),
56
+ drawLine(profile),
57
+ ...endingLines(profile),
58
+ ];
59
+ return lines.filter((line) => line !== "");
60
+ }
61
+ const pieceOrder = ["K", "Q", "R", "B", "N", "P"];
62
+ function pawnLine(side, { pawns }) {
63
+ const kinds = [["isolated", pawns.isolated], ["doubled", pawns.doubled], ["backward", pawns.backward], ["passed", pawns.passed]];
64
+ const named = kinds.filter(([, squares]) => squares.length > 0).map(([kind, squares]) => `${kind} ${squares.join(" ")}`);
65
+ return `${colourName(side)} pawns: ${[...named, `islands ${pawns.islands.join(", ") || "none"}`].join("; ")}`;
66
+ }
67
+ function filesLine(profile) {
68
+ const parts = [
69
+ profile.openFiles.length === 0 ? "" : `open ${profile.openFiles.join(" ")}`,
70
+ ...["white", "black"].map((side) => {
71
+ const files = profile[side].halfOpenFiles;
72
+ return files.length === 0 ? "" : `half-open for ${colourName(side)} ${files.join(" ")}`;
73
+ }),
74
+ ...["white", "black"].map((side) => {
75
+ const heavy = profile[side].heavyOnFiles;
76
+ return heavy.length === 0 ? "" : `${colourName(side)} heavy pieces on them ${heavy.join(" ")}`;
77
+ }),
78
+ ].filter((part) => part !== "");
79
+ return `Files: ${parts.join("; ") || "no open or half-open file"}`;
80
+ }
81
+ function seventhLine(pieces) {
82
+ return pieces === "" ? "" : `Rooks and queens on the seventh rank (the second for Black): ${pieces}`;
83
+ }
84
+ function bishopsLine(profile) {
85
+ const parts = ["white", "black"].flatMap((side) => {
86
+ const facts = profile[side];
87
+ const each = facts.bishops.map((bishop) => `B${bishop.square} ${bishop.pawnsOnColour}/${bishop.pawns}`);
88
+ return each.length === 0 ? [] : [`${colourName(side)}${facts.bishopPair ? " (pair)" : ""} ${each.join(", ")}`];
89
+ });
90
+ return parts.length === 0 ? "" : `Bishops (own pawns on its colour/all): ${parts.join("; ")}${profile.oppositeBishops ? "; opposite-coloured" : ""}`;
91
+ }
92
+ function centreText({ square, piece, white, black }) {
93
+ const on = piece === undefined ? "" : ` ${colourName(piece.side)} ${piece.letter}`;
94
+ return `${square}${on} ${white}/${black}`;
95
+ }
96
+ function mobilityText(mobility) {
97
+ const total = Object.values(mobility).reduce((sum, count) => sum + count, 0);
98
+ return total === 0 ? "0" : `${total} (${pieceOrder.filter((letter) => mobility[letter] !== undefined).map((letter) => `${letter}${mobility[letter]}`).join(" ")})`;
99
+ }
100
+ function developmentText(side) {
101
+ const castled = side.castled === undefined ? "" : side.castled === "no" ? "not castled" : `castled ${side.castled}`;
102
+ const rights = side.castlingRights.length === 0 ? "" : `may castle ${side.castlingRights.join(" ")}`;
103
+ const home = side.undeveloped.length === 0 ? "" : `at home ${side.undeveloped.join(" ")}`;
104
+ return [castled, rights, home].filter((part) => part !== "").join(", ") || "nothing to say";
105
+ }
106
+ function kingLine(side, king) {
107
+ const files = king.openFiles.length === 0 ? "" : `, no own pawn on ${king.openFiles.join(" ")}`;
108
+ return `${colourName(side)} K${king.square}: shield ${king.shield.join(" ") || "none"}${files}, enemy attacks around it ${king.attacksNear}`;
109
+ }
110
+ function drawLine({ draw }) {
111
+ const parts = [
112
+ draw.repetitions > 1 ? `position seen ${draw.repetitions} times` : "",
113
+ draw.fiftyMovesLeft <= 20 ? `${draw.fiftyMovesLeft} half-moves left before the fifty-move draw` : "",
114
+ draw.insufficient ? "material insufficient to mate" : "",
115
+ ].filter((part) => part !== "");
116
+ return parts.length === 0 ? "" : `Draw rules: ${parts.join("; ")}`;
117
+ }
118
+ function endingLines({ pawnEnding }) {
119
+ if (pawnEnding === undefined) {
120
+ return [];
121
+ }
122
+ const squares = pawnEnding.squares.map((square) => `${square.pawn} ${square.caught ? "inside" : "outside"} the enemy king's square`);
123
+ const opposition = pawnEnding.opposition === undefined ? "nobody" : colourName(pawnEnding.opposition);
124
+ return [`Pawn ending: ${[...squares, `opposition: ${opposition}`].join("; ")}`];
125
+ }
126
+ function joined(items) {
127
+ return items.length === 0 ? undefined : items.join(" ");
128
+ }
@@ -0,0 +1,60 @@
1
+ import { z } from "zod";
2
+ import { type DrawnAvatar, type PlayerProfile } from "../profiles/index.ts";
3
+ import type { Context } from "./context.ts";
4
+ /** A change of profile refused while a game goes on: the page says it in the player's language. */
5
+ export declare class GameGoingOn extends Error {
6
+ constructor();
7
+ }
8
+ /** The profiles, the one in use first. */
9
+ export declare const profiles: import("./registry.ts").Command<z.ZodObject<{}, z.core.$strip>, {
10
+ profiles: PlayerProfile[];
11
+ active: string;
12
+ }>;
13
+ /** A new profile, put in use once the game going on has ended; the one Chesspal made at the first start, while new, becomes it, game and all (D-71). */
14
+ export declare const newProfile: import("./registry.ts").Command<z.ZodObject<{
15
+ name: z.ZodString;
16
+ level: z.ZodOptional<z.ZodEnum<{
17
+ learning: "learning";
18
+ casual: "casual";
19
+ regular: "regular";
20
+ tournament: "tournament";
21
+ }>>;
22
+ rating: z.ZodOptional<z.ZodInt>;
23
+ site: z.ZodOptional<z.ZodEnum<{
24
+ lichess: "lichess";
25
+ "chess.com": "chess.com";
26
+ fide: "fide";
27
+ }>>;
28
+ avatar: z.ZodOptional<z.ZodEnum<{
29
+ "short-hair": "short-hair";
30
+ bun: "bun";
31
+ glasses: "glasses";
32
+ beanie: "beanie";
33
+ beard: "beard";
34
+ curls: "curls";
35
+ moustache: "moustache";
36
+ "silver-bob": "silver-bob";
37
+ cat: "cat";
38
+ owl: "owl";
39
+ }>>;
40
+ }, z.core.$strip>, PlayerProfile>;
41
+ /** Another profile put in use, once the game going on has ended. */
42
+ export declare const useProfile: import("./registry.ts").Command<z.ZodObject<{
43
+ profile: z.ZodString;
44
+ }, z.core.$strip>, PlayerProfile>;
45
+ /** The picture of the profile in use: a drawn avatar, or an image of the player's, which replaces any before. */
46
+ export declare const setAvatar: import("./registry.ts").Command<z.ZodObject<{
47
+ avatar: z.ZodString;
48
+ }, z.core.$strip>, {
49
+ profile: string;
50
+ avatar: string;
51
+ }>;
52
+ /** A drawn avatar chosen for a profile: the player's own image goes, or it would still show. */
53
+ export declare function chooseDrawnAvatar(context: Context, id: string, avatar: DrawnAvatar): void;
54
+ /**
55
+ * Who plays, as the AI reads it in status and new_game: their name, how they play and their rating; nothing while the profile is new, named after the
56
+ * system's account until the player completes it (D-71).
57
+ */
58
+ export declare function playerLine(context: Context): string | undefined;
59
+ /** Throws while a game goes on in the profile in use. */
60
+ export declare function refuseWhilePlaying(context: Context): void;