chesspal 0.0.0-stage → 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 (226) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +138 -2
  3. package/dist/cli/chat-hook.d.ts +2 -0
  4. package/dist/cli/chat-hook.js +33 -0
  5. package/dist/cli/cli.d.ts +2 -0
  6. package/dist/cli/cli.js +28 -0
  7. package/dist/cli/command-line.d.ts +8 -0
  8. package/dist/cli/command-line.js +178 -0
  9. package/dist/commands/ai-move.d.ts +37 -0
  10. package/dist/commands/ai-move.js +92 -0
  11. package/dist/commands/ai-say.d.ts +20 -0
  12. package/dist/commands/ai-say.js +40 -0
  13. package/dist/commands/analyse-game.d.ts +61 -0
  14. package/dist/commands/analyse-game.js +238 -0
  15. package/dist/commands/annotate-game.d.ts +35 -0
  16. package/dist/commands/annotate-game.js +78 -0
  17. package/dist/commands/clock.d.ts +15 -0
  18. package/dist/commands/clock.js +45 -0
  19. package/dist/commands/context.d.ts +56 -0
  20. package/dist/commands/context.js +36 -0
  21. package/dist/commands/draw-offer.d.ts +10 -0
  22. package/dist/commands/draw-offer.js +43 -0
  23. package/dist/commands/drawings.d.ts +15 -0
  24. package/dist/commands/drawings.js +23 -0
  25. package/dist/commands/end-game.d.ts +23 -0
  26. package/dist/commands/end-game.js +64 -0
  27. package/dist/commands/export-game.d.ts +14 -0
  28. package/dist/commands/export-game.js +26 -0
  29. package/dist/commands/game-summary.d.ts +95 -0
  30. package/dist/commands/game-summary.js +239 -0
  31. package/dist/commands/index.d.ts +3 -0
  32. package/dist/commands/index.js +19 -0
  33. package/dist/commands/init.d.ts +15 -0
  34. package/dist/commands/init.js +73 -0
  35. package/dist/commands/levels.d.ts +9 -0
  36. package/dist/commands/levels.js +25 -0
  37. package/dist/commands/look.d.ts +31 -0
  38. package/dist/commands/look.js +61 -0
  39. package/dist/commands/new-game.d.ts +69 -0
  40. package/dist/commands/new-game.js +215 -0
  41. package/dist/commands/opening-book.d.ts +13 -0
  42. package/dist/commands/opening-book.js +84 -0
  43. package/dist/commands/page-link.d.ts +11 -0
  44. package/dist/commands/page-link.js +28 -0
  45. package/dist/commands/past-games.d.ts +11 -0
  46. package/dist/commands/past-games.js +47 -0
  47. package/dist/commands/play-move.d.ts +44 -0
  48. package/dist/commands/play-move.js +102 -0
  49. package/dist/commands/player-move.d.ts +17 -0
  50. package/dist/commands/player-move.js +44 -0
  51. package/dist/commands/player-say.d.ts +5 -0
  52. package/dist/commands/player-say.js +16 -0
  53. package/dist/commands/ponder.d.ts +42 -0
  54. package/dist/commands/ponder.js +167 -0
  55. package/dist/commands/position.d.ts +14 -0
  56. package/dist/commands/position.js +128 -0
  57. package/dist/commands/profiles.d.ts +60 -0
  58. package/dist/commands/profiles.js +129 -0
  59. package/dist/commands/registry.d.ts +54 -0
  60. package/dist/commands/registry.js +61 -0
  61. package/dist/commands/say.d.ts +16 -0
  62. package/dist/commands/say.js +84 -0
  63. package/dist/commands/status.d.ts +29 -0
  64. package/dist/commands/status.js +103 -0
  65. package/dist/commands/study-games.d.ts +5 -0
  66. package/dist/commands/study-games.js +40 -0
  67. package/dist/commands/study.d.ts +38 -0
  68. package/dist/commands/study.js +122 -0
  69. package/dist/commands/view.d.ts +14 -0
  70. package/dist/commands/view.js +125 -0
  71. package/dist/core/board-text.d.ts +7 -0
  72. package/dist/core/board-text.js +18 -0
  73. package/dist/core/book.json +1 -0
  74. package/dist/core/clock.d.ts +45 -0
  75. package/dist/core/clock.js +109 -0
  76. package/dist/core/facts.d.ts +55 -0
  77. package/dist/core/facts.js +175 -0
  78. package/dist/core/game.d.ts +93 -0
  79. package/dist/core/game.js +348 -0
  80. package/dist/core/illegal.d.ts +19 -0
  81. package/dist/core/illegal.js +141 -0
  82. package/dist/core/index.d.ts +19 -0
  83. package/dist/core/index.js +11 -0
  84. package/dist/core/levels.d.ts +20 -0
  85. package/dist/core/levels.js +33 -0
  86. package/dist/core/material.d.ts +9 -0
  87. package/dist/core/material.js +12 -0
  88. package/dist/core/moods.d.ts +23 -0
  89. package/dist/core/moods.js +45 -0
  90. package/dist/core/openings.d.ts +37 -0
  91. package/dist/core/openings.js +98 -0
  92. package/dist/core/openings.json +1 -0
  93. package/dist/core/pgn.d.ts +10 -0
  94. package/dist/core/pgn.js +213 -0
  95. package/dist/core/profile.d.ts +117 -0
  96. package/dist/core/profile.js +232 -0
  97. package/dist/core/record.d.ts +21 -0
  98. package/dist/core/record.js +25 -0
  99. package/dist/core/review.d.ts +74 -0
  100. package/dist/core/review.js +126 -0
  101. package/dist/games/ai-model.d.ts +5 -0
  102. package/dist/games/ai-model.js +15 -0
  103. package/dist/games/analysis.d.ts +12 -0
  104. package/dist/games/analysis.js +26 -0
  105. package/dist/games/archive.d.ts +42 -0
  106. package/dist/games/archive.js +168 -0
  107. package/dist/games/chat-start.d.ts +16 -0
  108. package/dist/games/chat-start.js +26 -0
  109. package/dist/games/current-game.d.ts +54 -0
  110. package/dist/games/current-game.js +128 -0
  111. package/dist/games/files.d.ts +9 -0
  112. package/dist/games/files.js +52 -0
  113. package/dist/games/index.d.ts +10 -0
  114. package/dist/games/index.js +11 -0
  115. package/dist/games/plans.d.ts +9 -0
  116. package/dist/games/plans.js +23 -0
  117. package/dist/games/table-talk.d.ts +12 -0
  118. package/dist/games/table-talk.js +25 -0
  119. package/dist/games/talk.d.ts +40 -0
  120. package/dist/games/talk.js +131 -0
  121. package/dist/games/thoughts.d.ts +15 -0
  122. package/dist/games/thoughts.js +60 -0
  123. package/dist/home/data-folder.d.ts +12 -0
  124. package/dist/home/data-folder.js +55 -0
  125. package/dist/home/index.d.ts +2 -0
  126. package/dist/home/index.js +3 -0
  127. package/dist/home/open-folder.d.ts +2 -0
  128. package/dist/home/open-folder.js +14 -0
  129. package/dist/host-files/host-files.d.ts +10 -0
  130. package/dist/host-files/host-files.js +167 -0
  131. package/dist/host-files/index.d.ts +2 -0
  132. package/dist/host-files/index.js +3 -0
  133. package/dist/host-files/ponder-agents.d.ts +12 -0
  134. package/dist/host-files/ponder-agents.js +40 -0
  135. package/dist/index.d.ts +4 -0
  136. package/dist/index.js +2 -0
  137. package/dist/page/app.js +576 -0
  138. package/dist/page/avatars/beanie.webp +0 -0
  139. package/dist/page/avatars/beard.webp +0 -0
  140. package/dist/page/avatars/bun.webp +0 -0
  141. package/dist/page/avatars/cat.webp +0 -0
  142. package/dist/page/avatars/curls.webp +0 -0
  143. package/dist/page/avatars/glasses.webp +0 -0
  144. package/dist/page/avatars/moustache.webp +0 -0
  145. package/dist/page/avatars/owl.webp +0 -0
  146. package/dist/page/avatars/short-hair.webp +0 -0
  147. package/dist/page/avatars/silver-bob.webp +0 -0
  148. package/dist/page/avatars.js +16 -0
  149. package/dist/page/badges.js +50 -0
  150. package/dist/page/board.js +566 -0
  151. package/dist/page/browse.js +17 -0
  152. package/dist/page/clock-time.js +12 -0
  153. package/dist/page/dialog.js +26 -0
  154. package/dist/page/ending.js +73 -0
  155. package/dist/page/explain.js +58 -0
  156. package/dist/page/fold-tab.js +9 -0
  157. package/dist/page/folds.js +43 -0
  158. package/dist/page/game-column.js +44 -0
  159. package/dist/page/header.js +27 -0
  160. package/dist/page/helps-view.js +51 -0
  161. package/dist/page/helps.js +90 -0
  162. package/dist/page/main.js +8 -0
  163. package/dist/page/model-name.js +20 -0
  164. package/dist/page/move-list.js +68 -0
  165. package/dist/page/new-game-dialog.js +42 -0
  166. package/dist/page/opening-book.js +14 -0
  167. package/dist/page/pieces/CREDITS.md +36 -0
  168. package/dist/page/pieces/cburnett-small.svg +20 -0
  169. package/dist/page/pieces/cburnett.svg +20 -0
  170. package/dist/page/pieces/nitida-small.svg +19 -0
  171. package/dist/page/pieces/nitida.svg +20 -0
  172. package/dist/page/pieces/torretta-small.svg +19 -0
  173. package/dist/page/pieces/torretta.svg +19 -0
  174. package/dist/page/pieces.js +28 -0
  175. package/dist/page/player-card.js +60 -0
  176. package/dist/page/plies.js +62 -0
  177. package/dist/page/profile-dialogs.js +123 -0
  178. package/dist/page/profile.js +95 -0
  179. package/dist/page/settings-panel.js +32 -0
  180. package/dist/page/settings.js +76 -0
  181. package/dist/page/sounds/capture.ogg +0 -0
  182. package/dist/page/sounds/check.ogg +0 -0
  183. package/dist/page/sounds/draw.ogg +0 -0
  184. package/dist/page/sounds/flag.ogg +0 -0
  185. package/dist/page/sounds/mate.ogg +0 -0
  186. package/dist/page/sounds/move.ogg +0 -0
  187. package/dist/page/sounds/offer.ogg +0 -0
  188. package/dist/page/sounds/resign.ogg +0 -0
  189. package/dist/page/sounds.js +105 -0
  190. package/dist/page/studied.js +14 -0
  191. package/dist/page/study-dialog.js +58 -0
  192. package/dist/page/style.css +4398 -0
  193. package/dist/page/talk.js +172 -0
  194. package/dist/page/text.js +593 -0
  195. package/dist/page/torretta.js +38 -0
  196. package/dist/page/voice.js +38 -0
  197. package/dist/profiles/avatar.d.ts +23 -0
  198. package/dist/profiles/avatar.js +84 -0
  199. package/dist/profiles/front-matter.d.ts +9 -0
  200. package/dist/profiles/front-matter.js +33 -0
  201. package/dist/profiles/index.d.ts +3 -0
  202. package/dist/profiles/index.js +5 -0
  203. package/dist/profiles/profiles.d.ts +58 -0
  204. package/dist/profiles/profiles.js +237 -0
  205. package/dist/profiles/settings.d.ts +12 -0
  206. package/dist/profiles/settings.js +46 -0
  207. package/dist/server/analyses.d.ts +17 -0
  208. package/dist/server/analyses.js +46 -0
  209. package/dist/server/bell.d.ts +60 -0
  210. package/dist/server/bell.js +209 -0
  211. package/dist/server/caller.d.ts +10 -0
  212. package/dist/server/caller.js +99 -0
  213. package/dist/server/http.d.ts +16 -0
  214. package/dist/server/http.js +57 -0
  215. package/dist/server/mcp.d.ts +14 -0
  216. package/dist/server/mcp.js +179 -0
  217. package/dist/server/page.d.ts +46 -0
  218. package/dist/server/page.js +657 -0
  219. package/dist/server/profile-routes.d.ts +19 -0
  220. package/dist/server/profile-routes.js +186 -0
  221. package/dist/version.d.ts +2 -0
  222. package/dist/version.js +3 -0
  223. package/instructions/analyst.md +8 -0
  224. package/instructions/opponent.md +15 -0
  225. package/instructions/ponder.md +9 -0
  226. package/package.json +68 -4
@@ -0,0 +1,31 @@
1
+ import { z } from "zod";
2
+ import { type Facts, type Side } from "../core/index.ts";
3
+ import { type GameSummary } from "./game-summary.ts";
4
+ /** The game with the facts of its position, whoever is to move. */
5
+ type Viewed = GameSummary & {
6
+ facts: Facts;
7
+ };
8
+ /** A position of a game studied (D-68): where it stands, and its facts. */
9
+ interface StudyView {
10
+ where: string;
11
+ turn: Side;
12
+ fen: string;
13
+ facts: Facts;
14
+ }
15
+ /** The whole view (D-26), when the compact one after the player's move is not enough; or the board for the player, where the host does not show it (D-36). */
16
+ export declare const look: import("./registry.ts").Command<z.ZodObject<{
17
+ game: z.ZodOptional<z.ZodString>;
18
+ move: z.ZodOptional<z.ZodString>;
19
+ only: z.ZodOptional<z.ZodEnum<{
20
+ board: "board";
21
+ }>>;
22
+ }, z.core.$strip>, {
23
+ board: string;
24
+ } | {
25
+ game: Viewed;
26
+ words: string[];
27
+ } | {
28
+ study: StudyView;
29
+ words: string[];
30
+ }>;
31
+ export {};
@@ -0,0 +1,61 @@
1
+ import { z } from "zod";
2
+ import { boardText, materialOf } from "../core/index.js";
3
+ import { gameFolder } from "../profiles/index.js";
4
+ import { gameAt } from "./clock.js";
5
+ import { headLines, summaryOf, wordLines } from "./game-summary.js";
6
+ import { refuseUnseen } from "./levels.js";
7
+ import { defineCommand } from "./registry.js";
8
+ import { newWords } from "./say.js";
9
+ import { gameToStudy, positionAfter, positionLine, studyInput } from "./study.js";
10
+ import { fullView, materialText } from "./view.js";
11
+ /** The whole view (D-26), when the compact one after the player's move is not enough; or the board for the player, where the host does not show it (D-36). */
12
+ export const look = defineCommand({
13
+ name: "look",
14
+ description: "Shows the whole view of the position, from the club level up and at every level once the game is over: the FEN and, for every piece, what attacks and defends it, then the pins, the checks and the legal "
15
+ + "moves of each piece. The views of player_move, status and new_game give attackers and defenders only for the pieces attacked: call look "
16
+ + 'before you attack or capture a piece that is not attacked yet, or plan an exchange. With only: "board", the board as text for the player, to '
17
+ + "copy into your message where they do not see it. With move, and game for a game of the archive, the same of any position of a game over, to "
18
+ + 'analyse it. Example: look {}, look {move: "12..."}.',
19
+ input: z.object({ only: z.enum(["board"]).optional().describe("board: only the board as text, for the player"), ...studyInput }),
20
+ // Read only for the host: a flag that has fallen is written, as the time already decided.
21
+ readOnly: true,
22
+ idempotent: true,
23
+ run: ({ only, game, move }, context) => {
24
+ if (game !== undefined || move !== undefined) {
25
+ return studyLook(context, only, game, move);
26
+ }
27
+ const current = gameAt(gameFolder(context.home()), context.now());
28
+ if (current === undefined) {
29
+ throw new Error("There is no game to look at: start one with new_game.");
30
+ }
31
+ if (only !== "board") {
32
+ refuseUnseen(current, "tactics", 'look without only: "board"');
33
+ }
34
+ return only === "board" ? { board: boardText(current.game.position.fen) } : { game: { ...summaryOf(current, context.now()), facts: current.game.facts }, words: newWords(context) };
35
+ },
36
+ format: (look) => {
37
+ if ("board" in look) {
38
+ return look.board;
39
+ }
40
+ return [...("game" in look ? [...headLines(look.game), ...fullView(look.game.facts, look.game.turn)] : studyLines(look.study)), ...wordLines(look.words)].join("\n");
41
+ },
42
+ });
43
+ /** A position of a game over, as the view of the game going on gives it. */
44
+ function studyLook(context, only, name, move) {
45
+ const studied = gameToStudy(context, name);
46
+ const at = positionAfter(studied.record, move);
47
+ const { position } = at.game;
48
+ if (only === "board") {
49
+ return { board: boardText(position.fen) };
50
+ }
51
+ return { study: { where: positionLine(at, studied.name), turn: position.turn, fen: position.fen, facts: at.game.facts }, words: newWords(context) };
52
+ }
53
+ /** Where the position stands, the material, the FEN, and the whole view. */
54
+ function studyLines({ where, turn, fen, facts }) {
55
+ return [
56
+ where,
57
+ `Material: ${materialText(materialOf(fen))}`,
58
+ `FEN: ${fen}`,
59
+ ...fullView(facts, turn),
60
+ ];
61
+ }
@@ -0,0 +1,69 @@
1
+ import { z } from "zod";
2
+ import type { Context } from "./context.ts";
3
+ import { type GameSummary, type Speech } from "./game-summary.ts";
4
+ interface NewGame {
5
+ game: GameSummary;
6
+ /** Where the game before went: one going on, unfinished; one over, when its archive could not be written at its end. */
7
+ archived?: {
8
+ name: string;
9
+ finished: boolean;
10
+ };
11
+ /** The last games played with this player, for a chat that starts clean (D-57). */
12
+ past: string[];
13
+ /** Who plays, from the profile in use (D-23), once the player has completed it (D-71). */
14
+ player?: string;
15
+ /** The player's language when it is not English. */
16
+ speech?: Speech;
17
+ /** The board page's address when no browser has it open: the player needs it first. */
18
+ closedPage?: string;
19
+ }
20
+ /** The player chooses the game in the board page's window instead: open in a browser, or to be opened first at its address. */
21
+ interface Asked {
22
+ window: "opened" | "closed";
23
+ page?: string;
24
+ }
25
+ declare const newGameInput: z.ZodObject<{
26
+ ai_colour: z.ZodOptional<z.ZodEnum<{
27
+ white: "white";
28
+ black: "black";
29
+ random: "random";
30
+ }>>;
31
+ from: z.ZodOptional<z.ZodString>;
32
+ clock: z.ZodOptional<z.ZodString>;
33
+ level: z.ZodOptional<z.ZodEnum<{
34
+ beginner: "beginner";
35
+ club: "club";
36
+ strong: "strong";
37
+ master: "master";
38
+ }>>;
39
+ replace: z.ZodDefault<z.ZodBoolean>;
40
+ }, z.core.$strip>;
41
+ /** A new game against the player, from the standard position or from a FEN or PGN; a game going on is never lost (D-41). */
42
+ export declare const newGame: import("./registry.ts").Command<z.ZodObject<{
43
+ ai_colour: z.ZodOptional<z.ZodEnum<{
44
+ white: "white";
45
+ black: "black";
46
+ random: "random";
47
+ }>>;
48
+ from: z.ZodOptional<z.ZodString>;
49
+ clock: z.ZodOptional<z.ZodString>;
50
+ level: z.ZodOptional<z.ZodEnum<{
51
+ beginner: "beginner";
52
+ club: "club";
53
+ strong: "strong";
54
+ master: "master";
55
+ }>>;
56
+ replace: z.ZodDefault<z.ZodBoolean>;
57
+ }, z.core.$strip>, NewGame | Asked>;
58
+ /**
59
+ * Starts the new game with the player's choices: those the AI asked in the chat, or the page's window gave (D-76). Only the chat's is refused on a game
60
+ * changed since it saw it (D-71): the player who presses New game on the page sees the game there (Federico, round of D-85).
61
+ */
62
+ export declare function startNewGame({ ai_colour: colour, from, clock, level: chosen, replace }: z.output<typeof newGameInput>, context: Context, by: "chat" | "page"): NewGame;
63
+ /** The refusal of a new game in a chat that still holds the game before (D-57): the board page says it in the player's language. */
64
+ export declare class ChatNotClear extends Error {
65
+ constructor();
66
+ }
67
+ /** From how many plies a game played in a chat asks for a clean one before the next (D-57): one left after a few moves does not. */
68
+ export declare const pliesBeforeClearing = 20;
69
+ export {};
@@ -0,0 +1,215 @@
1
+ import { z } from "zod";
2
+ import { aiLevels, Game, levelTag, pgnDate, readPgn, startClock, timeControlOf, timeControlTag, withoutMoods } from "../core/index.js";
3
+ import { archivedName, archiveGame, cleanSince, endAnalysis, endOf, endTableTalk, lastChatStart, loadGame, saveGame, startOfNewGame, withoutPlans } from "../games/index.js";
4
+ import { gameFolder, gameFolders } from "../profiles/index.js";
5
+ import { gameAt } from "./clock.js";
6
+ import { nextStep, positionLines, sidesLine, speechLine, speechOf, summaryOf, withThought } from "./game-summary.js";
7
+ import { levelText, levelToPlay } from "./levels.js";
8
+ import { closedPage, pageGiven, pageLine } from "./page-link.js";
9
+ import { pastGameLines } from "./past-games.js";
10
+ import { refuseUnseenPosition } from "./play-move.js";
11
+ import { playerLine } from "./profiles.js";
12
+ import { defineCommand } from "./registry.js";
13
+ const newGameInput = z.object({
14
+ ai_colour: z.enum(["white", "black", "random"]).optional().describe("the colour you play: random draws it; left out with the rest, the page's window asks"),
15
+ from: z.string().optional().describe("a FEN or a PGN to go on from, instead of the standard position"),
16
+ clock: z.string().optional().describe("the time control, minutes each and seconds added after each move, such as 10+5 or 3+0; none, or left out, for a game without a clock"),
17
+ level: z.enum(aiLevels).optional().describe("the facts you, not the player, get: beginner the board and the legal moves, club also the tactical facts, strong also the positional ones, master "
18
+ + "also the opening book's moves; left out, the last game's, or for the first game the one that suits the player's profile"),
19
+ replace: z.boolean().default(false).describe("start even though a game is going on: it goes to the archive unfinished"),
20
+ });
21
+ /** A new game against the player, from the standard position or from a FEN or PGN; a game going on is never lost (D-41). */
22
+ export const newGame = defineCommand({
23
+ name: "new_game",
24
+ description: "Starts a new game against the player. Without choices it opens the board page's New game window, where the player chooses the colour, the clock "
25
+ + "and the level; where there is no board page, it tells you to ask them in the chat. With choices it starts the game: only where there is no board page, "
26
+ + 'or from a FEN or PGN the player gives. Example: new_game {}; new_game {ai_colour: "black", from: "<FEN or PGN>", level: "club"}.',
27
+ input: newGameInput,
28
+ alwaysLoad: true,
29
+ showsGame: true,
30
+ positionals: ["ai_colour"],
31
+ run: (input, context) => {
32
+ const { ai_colour: colour, from, clock, level } = input;
33
+ if (colour === undefined && from === undefined && clock === undefined && level === undefined) {
34
+ return askedOnPage(context);
35
+ }
36
+ // The game is played on the board page, and chosen in its window (D-85); a position to go on from is what the window cannot set.
37
+ if (context.page !== undefined && from === undefined) {
38
+ throw new Error("The player chooses a new game in the board page's New game window: call new_game without choices, which opens it, or gives you its address "
39
+ + "when nobody has the page open. Nothing changed.");
40
+ }
41
+ return startNewGame(input, context, "chat");
42
+ },
43
+ format: (result) => "window" in result ? askedLine(result) : startedLines(result),
44
+ });
45
+ /**
46
+ * Starts the new game with the player's choices: those the AI asked in the chat, or the page's window gave (D-76). Only the chat's is refused on a game
47
+ * changed since it saw it (D-71): the player who presses New game on the page sees the game there (Federico, round of D-85).
48
+ */
49
+ export function startNewGame({ ai_colour: colour, from, clock, level: chosen, replace }, context, by) {
50
+ if (colour === undefined) {
51
+ throw new Error(from === undefined
52
+ ? "ai_colour: say the colour you play, white, black or random, with the other choices; or leave them all out for the page's window. Nothing changed."
53
+ : "ai_colour: say the colour you play from this position, white, black or random. Nothing changed.");
54
+ }
55
+ const control = clock === undefined || /^\s*none\s*$/i.test(clock) ? undefined : timeControlFrom(clock);
56
+ const now = context.now();
57
+ const folder = gameFolder(context.home());
58
+ const before = gameAt(folder, now);
59
+ const going = before !== undefined && endOf(before.game) === undefined ? before : undefined;
60
+ if (going !== undefined && !replace) {
61
+ const { moveNumber } = going.game.position;
62
+ throw new Error(`A game is going on, at move ${moveNumber}: end it with end_game, or call new_game with replace: true, which archives it unfinished. Nothing changed.`);
63
+ }
64
+ if (going !== undefined && by === "chat") {
65
+ refuseUnseenPosition(context, folder);
66
+ }
67
+ if (chatToClear(context)) {
68
+ throw new ChatNotClear();
69
+ }
70
+ const ai = colour === "random" ? (context.random() < 0.5 ? "white" : "black") : colour;
71
+ const level = chosen ?? levelToPlay(context, before);
72
+ const started = startOfNewGame(folder, now, before);
73
+ const game = startingGame(from, { ai, level }, started, control);
74
+ const current = control === undefined
75
+ ? { game, ai, started: started.toISOString(), talk: [] }
76
+ : { game, ai, started: started.toISOString(), talk: [], clock: startClock(control, now, context.pageOpen?.() === true) };
77
+ const archived = before === undefined || (going === undefined && archivedName(folder, before) !== undefined) ? undefined : archiveBefore(folder, before);
78
+ saveGame(folder, current);
79
+ endAnalysis(folder);
80
+ endTableTalk(folder);
81
+ const past = pastGameLines(folder, remembered);
82
+ const closed = closedPage(context);
83
+ const player = playerLine(context);
84
+ const speech = speechOf(folder, context.language);
85
+ if (closed !== undefined) {
86
+ pageGiven(context, current.started);
87
+ }
88
+ return {
89
+ game: withThought(summaryOf(current, now), folder, current),
90
+ ...(archived === undefined ? {} : { archived: { name: archived, finished: going === undefined } }),
91
+ past,
92
+ ...(player === undefined ? {} : { player }),
93
+ ...(speech === undefined ? {} : { speech }),
94
+ ...(closed === undefined ? {} : { closedPage: closed }),
95
+ };
96
+ }
97
+ /** The new game's lines for the AI. */
98
+ function startedLines({ game, archived, past, player, speech, closedPage: closed }) {
99
+ return [
100
+ `New game: ${sidesLine(game.ai)}, ${levelText(game.level)}.`,
101
+ ...(player === undefined ? [] : [player]),
102
+ ...(speech === undefined ? [] : [speechLine(speech)]),
103
+ ...(archived === undefined ? [] : [`The game before is in ${archived.name}${archived.finished ? "" : ", unfinished"}.`]),
104
+ ...(past.length === 0 ? [] : ["Last games with this player (past_games gives more, and their moves):", ...past]),
105
+ ...positionLines(game),
106
+ ...(closed === undefined ? [] : [pageLine(closed, true)]),
107
+ nextStep(game),
108
+ ].join("\n");
109
+ }
110
+ /**
111
+ * The New game window opened on the board page, or its address for the player to open it first; a chat to clear refuses it as a new game does (D-57), and
112
+ * without the page the player is asked in the chat.
113
+ */
114
+ function askedOnPage(context) {
115
+ if (chatToClear(context)) {
116
+ throw new ChatNotClear();
117
+ }
118
+ if (context.askNewGame?.() === true) {
119
+ return { window: "opened" };
120
+ }
121
+ if (context.page !== undefined) {
122
+ return { window: "closed", page: context.page };
123
+ }
124
+ throw new Error("There is no board page here: ask the player which colour they want, whether they want a clock and at which level you play, then call new_game with "
125
+ + "them. Nothing changed.");
126
+ }
127
+ function askedLine({ window, page }) {
128
+ return window === "opened"
129
+ ? "The New game window is open on the board page: the player chooses the game there. Don't ask in the chat; end your turn, the doorbell wakes you when "
130
+ + "the game starts."
131
+ : `Nobody has the board page open: give the player its address, ${page}, and tell them to start the game there with its New game button. End your turn.`;
132
+ }
133
+ /** The refusal of a new game in a chat that still holds the game before (D-57): the board page says it in the player's language. */
134
+ export class ChatNotClear extends Error {
135
+ constructor() {
136
+ super("The game before was played in this chat: ask the player to write /clear here, which empties the chat and keeps the games on disk, then "
137
+ + "start the new game. A clean chat plays faster and costs less. Nothing changed.");
138
+ }
139
+ }
140
+ /** How many of the last games new_game recalls. */
141
+ const remembered = 3;
142
+ /** From how many plies a game played in a chat asks for a clean one before the next (D-57): one left after a few moves does not. */
143
+ export const pliesBeforeClearing = 20;
144
+ // A FEN's board: eight ranks of pieces and empty counts.
145
+ const fenPattern = /^[pnbrqkPNBRQK1-8]+(?:\/[pnbrqkPNBRQK1-8]+){7}(?:\s|$)/;
146
+ /**
147
+ * Whether the chat must be cleared first: the last game begun, in any profile, was played in it at some length, and the host says how chats start (the hook
148
+ * of Claude Code and Codex, T-85, T-104). Another profile does not clear the chat (D-66), nor a game another chat played, which this one was never shown (D-71).
149
+ */
150
+ function chatToClear(context) {
151
+ const start = lastChatStart(context.folder);
152
+ if (!hostsNotingChats.has(context.host?.() ?? "") || start === undefined) {
153
+ return false;
154
+ }
155
+ const last = gameFolders(context.home()).flatMap((folder) => loadGame(folder) ?? []).sort((one, other) => Date.parse(other.started) - Date.parse(one.started))[0];
156
+ return last !== undefined && last.started === context.positionSeen?.started && last.game.record.moves.length >= pliesBeforeClearing && !cleanSince(start, last.started);
157
+ }
158
+ // The hosts whose SessionStart hook, written by init, notes how each chat starts: Claude Code and Codex, by the names they give the server.
159
+ const hostsNotingChats = new Set(["claude-code", "codex-mcp-client"]);
160
+ function archiveBefore(folder, before) {
161
+ try {
162
+ return archiveGame(folder, before);
163
+ }
164
+ catch (error) {
165
+ throw new Error(`${error.message} Nothing changed.`, { cause: error });
166
+ }
167
+ }
168
+ /**
169
+ * The new game: the standard position, or the one a FEN or PGN gives, which must still be going on. Its tags are Chesspal's, the AI's level among them:
170
+ * an imported game gives its position only.
171
+ */
172
+ function startingGame(from, { ai, level }, started, control) {
173
+ const tags = {
174
+ Event: "Chesspal",
175
+ Date: pgnDate(started),
176
+ White: ai === "white" ? "AI" : "Player",
177
+ Black: ai === "black" ? "AI" : "Player",
178
+ [levelTag]: level,
179
+ ...(control === undefined ? {} : { TimeControl: timeControlTag(control) }),
180
+ };
181
+ let game;
182
+ try {
183
+ const record = from === undefined ? { tags: {}, moves: [] } : importedRecord(from.trim());
184
+ const fen = record.tags.FEN === undefined ? {} : { SetUp: "1", FEN: record.tags.FEN };
185
+ game = new Game({ tags: { ...tags, ...fen }, moves: record.moves });
186
+ }
187
+ catch (error) {
188
+ throw new Error(`from: ${error.message} Nothing changed.`);
189
+ }
190
+ const end = endOf(game);
191
+ if (end !== undefined) {
192
+ throw new Error(`from holds a game already over (${end.result}): give one still going on, or leave from out for a new game. Nothing changed.`);
193
+ }
194
+ return game;
195
+ }
196
+ function timeControlFrom(clock) {
197
+ try {
198
+ return timeControlOf(clock);
199
+ }
200
+ catch (error) {
201
+ throw new Error(`clock: ${error.message} Nothing changed.`);
202
+ }
203
+ }
204
+ function importedRecord(from) {
205
+ if (fenPattern.test(from)) {
206
+ return { tags: { SetUp: "1", FEN: from }, moves: [] };
207
+ }
208
+ const records = readPgn(from);
209
+ const [record] = records;
210
+ if (record === undefined || records.length > 1) {
211
+ throw new Error(`it holds ${records.length} games: give one, in PGN, or a FEN.`);
212
+ }
213
+ // Another game's [%plan] and [%mood] comments are not the AI's.
214
+ return withoutMoods(withoutPlans(record));
215
+ }
@@ -0,0 +1,13 @@
1
+ import { type GameRecord, type Opening } from "../core/index.ts";
2
+ /** 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. */
3
+ export declare function openingLine(record: GameRecord): string | undefined;
4
+ /** 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. */
5
+ export declare function knownMoves(fen: string): readonly string[];
6
+ /** How many plies from the start the book knows, to the last before the game left it (D-69); none from another position. */
7
+ export declare function bookPlies(record: GameRecord): number;
8
+ /** Whether a line of Lichess's openings reaches a position, by any order of moves (D-68); never where the build made no book. */
9
+ export declare function bookReaches(fen: string): boolean;
10
+ /** The opening a game reached, ECO and name, without where it left the book: for the games in the archive. */
11
+ export declare function openingName(record: GameRecord): string | undefined;
12
+ /** The last named opening a game reached, and at which ply; none outside the book, or where the build made no index. */
13
+ export declare function bookOpening(record: GameRecord): Opening | undefined;
@@ -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,44 @@
1
+ import { type MoveReview } from "../core/index.ts";
2
+ import type { Context } from "./context.ts";
3
+ import { type MovePlayed } from "./game-summary.ts";
4
+ /** Who plays a move: the AI, or the player through the AI. */
5
+ export type Mover = "ai" | "player";
6
+ /** What comes with a move: the AI's comment, or the player's words from the board page, saved with it (T-65), and a draw offer (D-65). */
7
+ export interface WithMove {
8
+ comment?: string;
9
+ offerDraw?: boolean;
10
+ said?: {
11
+ text: string;
12
+ at: Date;
13
+ };
14
+ /** Milliseconds of the mover's turn that were not theirs, not taken off their clock (D-36). */
15
+ allowance?: number;
16
+ /** The AI's badge and note on the player's last move, kept in its comment as the analyst keeps them (D-78). */
17
+ judged?: MoveReview;
18
+ }
19
+ /**
20
+ * 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
21
+ * with it, change nothing: it throws the message `refused` writes from the core's.
22
+ */
23
+ export declare function playMove(folder: string, now: Date, mover: Mover, text: string, refused: (message: string, legal: readonly string[]) => string, { comment, said, offerDraw, allowance, judged }?: WithMove): MovePlayed;
24
+ /**
25
+ * Where the game a chat sees stands: the game by its start, none when there is none, and how many plies it has. Not by its folder, which a profile
26
+ * completed renames (D-71); a game in another profile started at another time.
27
+ */
28
+ export interface Position {
29
+ readonly started?: string;
30
+ readonly plies: number;
31
+ }
32
+ /** A chat's move, resignation or game replaced on a position it has not seen (D-71): it was decided on another. */
33
+ export declare class PositionChanged extends Error {
34
+ constructor();
35
+ }
36
+ /** The position of the profile's game now. */
37
+ export declare function positionIn(folder: string): Position;
38
+ /**
39
+ * 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
40
+ * (D-71). Words and the clock change no position; the command line and the page, which have no chat, are never refused.
41
+ */
42
+ export declare function refuseUnseenPosition(context: Context, folder: string): void;
43
+ /** 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. */
44
+ export declare function refuseOtherGame(context: Context, folder: string): void;