chesspal 0.1.0 → 0.2.1

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 (211) hide show
  1. package/README.md +45 -82
  2. package/dist/cli/chat-hook.js +1 -1
  3. package/dist/cli/command-line.d.ts +1 -2
  4. package/dist/cli/command-line.js +2 -3
  5. package/dist/commands/agents.d.ts +26 -0
  6. package/dist/commands/agents.js +72 -0
  7. package/dist/commands/ai-move.d.ts +5 -14
  8. package/dist/commands/ai-move.js +28 -35
  9. package/dist/commands/ai-say.d.ts +3 -1
  10. package/dist/commands/ai-say.js +24 -6
  11. package/dist/commands/analyse-game.d.ts +4 -0
  12. package/dist/commands/analyse-game.js +43 -16
  13. package/dist/commands/annotate-game.d.ts +4 -0
  14. package/dist/commands/annotate-game.js +65 -12
  15. package/dist/commands/answer-lines.d.ts +28 -0
  16. package/dist/commands/answer-lines.js +144 -0
  17. package/dist/commands/coach.d.ts +45 -0
  18. package/dist/commands/coach.js +344 -0
  19. package/dist/commands/context.d.ts +16 -5
  20. package/dist/commands/context.js +1 -0
  21. package/dist/commands/delete-game.d.ts +7 -0
  22. package/dist/commands/delete-game.js +30 -0
  23. package/dist/commands/delete-review.d.ts +8 -0
  24. package/dist/commands/delete-review.js +34 -0
  25. package/dist/commands/drawings.d.ts +0 -2
  26. package/dist/commands/drawings.js +5 -3
  27. package/dist/commands/end-game.js +5 -2
  28. package/dist/commands/export-game.d.ts +1 -1
  29. package/dist/commands/export-game.js +4 -3
  30. package/dist/commands/game-summary.d.ts +21 -38
  31. package/dist/commands/game-summary.js +30 -167
  32. package/dist/commands/index.d.ts +15 -0
  33. package/dist/commands/index.js +47 -2
  34. package/dist/commands/init.d.ts +3 -3
  35. package/dist/commands/init.js +4 -4
  36. package/dist/commands/lead-input.d.ts +8 -0
  37. package/dist/commands/lead-input.js +21 -0
  38. package/dist/commands/look.js +4 -1
  39. package/dist/commands/new-game.d.ts +2 -3
  40. package/dist/commands/new-game.js +7 -4
  41. package/dist/commands/play-move.d.ts +8 -15
  42. package/dist/commands/play-move.js +5 -13
  43. package/dist/commands/player-move.d.ts +2 -6
  44. package/dist/commands/player-move.js +13 -6
  45. package/dist/commands/player-say.d.ts +1 -1
  46. package/dist/commands/player-say.js +1 -1
  47. package/dist/commands/ponder.d.ts +10 -0
  48. package/dist/commands/ponder.js +33 -21
  49. package/dist/commands/position.d.ts +1 -1
  50. package/dist/commands/position.js +3 -1
  51. package/dist/commands/profiles.d.ts +13 -4
  52. package/dist/commands/profiles.js +49 -15
  53. package/dist/commands/registry.d.ts +8 -3
  54. package/dist/commands/registry.js +12 -11
  55. package/dist/commands/say.d.ts +1 -1
  56. package/dist/commands/say.js +16 -16
  57. package/dist/commands/speech.d.ts +8 -0
  58. package/dist/commands/speech.js +20 -0
  59. package/dist/commands/status.d.ts +9 -3
  60. package/dist/commands/status.js +40 -15
  61. package/dist/commands/study-games.js +1 -1
  62. package/dist/commands/study.js +3 -19
  63. package/dist/core/facts.js +15 -15
  64. package/dist/core/game.d.ts +3 -1
  65. package/dist/core/game.js +8 -4
  66. package/dist/core/illegal.js +1 -1
  67. package/dist/core/index.d.ts +5 -4
  68. package/dist/core/index.js +5 -4
  69. package/dist/core/lead.d.ts +15 -0
  70. package/dist/core/lead.js +40 -0
  71. package/dist/core/levels.d.ts +1 -1
  72. package/dist/core/levels.js +1 -1
  73. package/dist/core/moods.d.ts +0 -2
  74. package/dist/core/moods.js +2 -2
  75. package/dist/core/openings.d.ts +0 -2
  76. package/dist/core/openings.js +1 -1
  77. package/dist/core/profile.js +1 -1
  78. package/dist/core/record.d.ts +1 -1
  79. package/dist/core/record.js +1 -1
  80. package/dist/core/review.d.ts +18 -3
  81. package/dist/core/review.js +52 -9
  82. package/dist/games/ai-model.d.ts +4 -0
  83. package/dist/games/ai-model.js +9 -0
  84. package/dist/games/analysis.d.ts +1 -1
  85. package/dist/games/archive.d.ts +25 -4
  86. package/dist/games/archive.js +99 -25
  87. package/dist/games/coach.d.ts +57 -0
  88. package/dist/games/coach.js +144 -0
  89. package/dist/games/current-game.d.ts +2 -0
  90. package/dist/games/current-game.js +7 -1
  91. package/dist/games/files.js +1 -0
  92. package/dist/games/index.d.ts +5 -4
  93. package/dist/games/index.js +5 -4
  94. package/dist/games/talk.d.ts +1 -5
  95. package/dist/games/talk.js +14 -14
  96. package/dist/home/open-folder.js +1 -1
  97. package/dist/host-files/{ponder-agents.d.ts → agents.d.ts} +10 -2
  98. package/dist/host-files/agents.js +53 -0
  99. package/dist/host-files/host-files.d.ts +7 -2
  100. package/dist/host-files/host-files.js +27 -12
  101. package/dist/host-files/index.d.ts +2 -2
  102. package/dist/host-files/index.js +3 -3
  103. package/dist/index.d.ts +1 -3
  104. package/dist/index.js +1 -0
  105. package/dist/page/app/anchor.js +88 -0
  106. package/dist/page/{app.js → app/app.js} +116 -231
  107. package/dist/page/app/keys.js +94 -0
  108. package/dist/page/app/phase.js +30 -0
  109. package/dist/page/app/served.js +73 -0
  110. package/dist/page/avatars/avatars.js +19 -0
  111. package/dist/page/avatars/thought.js +10 -0
  112. package/dist/page/base.css +160 -0
  113. package/dist/page/board/board.css +959 -0
  114. package/dist/page/board/board.js +125 -0
  115. package/dist/page/{explain.js → board/explain.js} +1 -1
  116. package/dist/page/board/marks.js +160 -0
  117. package/dist/page/board/motion.js +71 -0
  118. package/dist/page/board/pointer.js +239 -0
  119. package/dist/page/board/squares.js +26 -0
  120. package/dist/page/dialogs/analyses.js +195 -0
  121. package/dist/page/{dialog.js → dialogs/dialog.js} +1 -1
  122. package/dist/page/dialogs/dialogs.css +1228 -0
  123. package/dist/page/{new-game-dialog.js → dialogs/new-game.js} +3 -3
  124. package/dist/page/{profile-dialogs.js → dialogs/profiles.js} +40 -11
  125. package/dist/page/dialogs/search.js +31 -0
  126. package/dist/page/dialogs/settings.js +42 -0
  127. package/dist/page/dialogs/studied.js +39 -0
  128. package/dist/page/dialogs/study.js +51 -0
  129. package/dist/page/{badges.js → game/badges.js} +1 -1
  130. package/dist/page/{browse.js → game/browse.js} +1 -1
  131. package/dist/page/game/card-fold.js +40 -0
  132. package/dist/page/game/column.js +55 -0
  133. package/dist/page/{ending.js → game/ending.js} +2 -2
  134. package/dist/page/game/game.css +577 -0
  135. package/dist/page/{move-list.js → game/move-list.js} +10 -6
  136. package/dist/page/{plies.js → game/plies.js} +30 -10
  137. package/dist/page/game/result-tile.js +14 -0
  138. package/dist/page/helps/helps.css +689 -0
  139. package/dist/page/helps/helps.js +220 -0
  140. package/dist/page/helps/view.js +127 -0
  141. package/dist/page/layout/fold-tab.js +11 -0
  142. package/dist/page/{folds.js → layout/folds.js} +1 -1
  143. package/dist/page/{header.js → layout/header.js} +5 -4
  144. package/dist/page/layout/layout.css +834 -0
  145. package/dist/page/lead/lead.js +82 -0
  146. package/dist/page/lead/view.js +38 -0
  147. package/dist/page/main.js +2 -2
  148. package/dist/page/pieces/CREDITS.md +12 -29
  149. package/dist/page/pieces/capablanca-small.svg +20 -0
  150. package/dist/page/pieces/capablanca.svg +22 -0
  151. package/dist/page/pieces/nitida-small.svg +3 -2
  152. package/dist/page/pieces/nitida.svg +3 -2
  153. package/dist/page/{pieces.js → pieces/pieces.js} +2 -2
  154. package/dist/page/pieces/torretta-small.svg +15 -14
  155. package/dist/page/pieces/torretta.svg +17 -15
  156. package/dist/page/{player-card.js → players/card.js} +2 -6
  157. package/dist/page/{clock-time.js → players/clock-time.js} +2 -2
  158. package/dist/page/{profile.js → profile/profile.js} +9 -5
  159. package/dist/page/{settings.js → profile/settings.js} +5 -4
  160. package/dist/page/style.css +9 -4398
  161. package/dist/page/talk/talk.css +958 -0
  162. package/dist/page/{talk.js → talk/talk.js} +39 -46
  163. package/dist/page/text.js +137 -19
  164. package/dist/page/theme.css +292 -0
  165. package/dist/profiles/index.d.ts +1 -1
  166. package/dist/profiles/profiles.d.ts +15 -4
  167. package/dist/profiles/profiles.js +24 -7
  168. package/dist/server/bell.d.ts +10 -2
  169. package/dist/server/bell.js +28 -12
  170. package/dist/server/caller.d.ts +14 -3
  171. package/dist/server/caller.js +44 -11
  172. package/dist/server/mcp.d.ts +2 -3
  173. package/dist/server/mcp.js +53 -19
  174. package/dist/server/page/analyses.d.ts +23 -0
  175. package/dist/server/page/analyses.js +65 -0
  176. package/dist/server/page/files.d.ts +5 -0
  177. package/dist/server/page/files.js +92 -0
  178. package/dist/server/page/game.d.ts +6 -0
  179. package/dist/server/page/game.js +381 -0
  180. package/dist/server/{http.d.ts → page/http.d.ts} +13 -0
  181. package/dist/server/{http.js → page/http.js} +11 -0
  182. package/dist/server/{page.d.ts → page/page.d.ts} +8 -13
  183. package/dist/server/page/page.js +229 -0
  184. package/dist/server/{profile-routes.d.ts → page/profiles.d.ts} +2 -2
  185. package/dist/server/{profile-routes.js → page/profiles.js} +47 -18
  186. package/instructions/analysis.md +4 -0
  187. package/instructions/analyst-agent.md +8 -0
  188. package/instructions/analyst.md +3 -7
  189. package/instructions/coach.md +11 -0
  190. package/instructions/opponent.md +4 -4
  191. package/package.json +1 -1
  192. package/dist/host-files/ponder-agents.js +0 -40
  193. package/dist/page/avatars.js +0 -16
  194. package/dist/page/board.js +0 -566
  195. package/dist/page/fold-tab.js +0 -9
  196. package/dist/page/game-column.js +0 -44
  197. package/dist/page/helps-view.js +0 -51
  198. package/dist/page/helps.js +0 -90
  199. package/dist/page/pieces/cburnett-small.svg +0 -20
  200. package/dist/page/pieces/cburnett.svg +0 -20
  201. package/dist/page/settings-panel.js +0 -32
  202. package/dist/page/studied.js +0 -14
  203. package/dist/page/study-dialog.js +0 -58
  204. package/dist/server/analyses.d.ts +0 -17
  205. package/dist/server/analyses.js +0 -46
  206. package/dist/server/page.js +0 -657
  207. /package/dist/page/{torretta.js → avatars/torretta.js} +0 -0
  208. /package/dist/page/{opening-book.js → game/opening-book.js} +0 -0
  209. /package/dist/page/{model-name.js → players/model-name.js} +0 -0
  210. /package/dist/page/{sounds.js → sounds/sounds.js} +0 -0
  211. /package/dist/page/{voice.js → talk/voice.js} +0 -0
@@ -4,7 +4,7 @@ import { recordedMove } from "./record.js";
4
4
  /** The badges of a move, as chess.com has them, from the best to the worst. */
5
5
  export const badges = ["brilliant", "great", "best", "excellent", "good", "book", "inaccuracy", "mistake", "miss", "blunder"];
6
6
  /** The colours the AI draws with on the board, by the letters PGN's [%cal] and [%csl] write them with. */
7
- export const drawColours = { G: "green", R: "red", Y: "yellow", B: "blue" };
7
+ const drawColours = { G: "green", R: "red", Y: "yellow", B: "blue" };
8
8
  /** The review a move's comment carries: a badge, a note or both, with the arrows and squares drawn to show it; none without a badge or a note. */
9
9
  export function reviewOf(comment) {
10
10
  const found = badgePattern.exec(comment ?? "")?.[1];
@@ -36,6 +36,10 @@ export function gameReviewOf(record) {
36
36
  export function closingOf(record) {
37
37
  return closingPattern.exec(record.comment ?? "")?.[1]?.trim() || undefined;
38
38
  }
39
+ /** Whether the analyst has reviewed a game: its word on the whole game or on how it ended. The badges given during the game with the helps on are not a review (D-78). */
40
+ export function isReviewed(record) {
41
+ return gameReviewOf(record) !== undefined || closingOf(record) !== undefined;
42
+ }
39
43
  /** The move with the AI's review in place of the one it had: the NAG of its badge, and its badge and note in the comment, beside what the comment held. */
40
44
  export function reviewed(move, review) {
41
45
  const nag = review.badge === undefined ? undefined : nagOfBadge[review.badge];
@@ -49,6 +53,21 @@ export function reviewed(move, review) {
49
53
  ].filter((part) => part !== "").join(" ");
50
54
  return recordedMove(move.san, nag === undefined ? nags : [nag, ...nags], comment);
51
55
  }
56
+ /**
57
+ * The record without the AI's review (D-92): the badges with the NAGs they wrote, the notes, drawings and estimates of who is ahead on the moves, the word on
58
+ * the game and who signed it. What the comments held besides stays, as do the NAGs of moves without a badge and the Annotator of a game it never reviewed.
59
+ */
60
+ export function unreviewed(record) {
61
+ const moves = record.moves.map((move) => {
62
+ const nags = badgePattern.test(move.comment ?? "") ? (move.nags ?? []).filter((nag) => !judgmentNags.has(nag)) : move.nags;
63
+ return recordedMove(move.san, nags, withoutReview(move.comment).replace(/\s*\[%lead [^\]]*\]/g, "").trim());
64
+ });
65
+ const comment = withoutReview(record.comment);
66
+ // The signature is the AI's only on a game it reviewed: a game from elsewhere keeps its own Annotator.
67
+ const signedByAi = isReviewed(record) || record.moves.some((move) => reviewMark.test(move.comment ?? ""));
68
+ const tags = signedByAi ? Object.fromEntries(Object.entries(record.tags).filter(([name]) => name !== "Annotator" && name !== effortTag)) : record.tags;
69
+ return comment === "" ? { tags, moves } : { tags, comment, moves };
70
+ }
52
71
  /** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
53
72
  export function drawingsText({ arrows = [], squares = [] }) {
54
73
  const arrowList = arrows.map(({ colour, from, to }) => `${letterOf(colour)}${from}${to}`).join(",");
@@ -72,6 +91,36 @@ export function annotatorOf(record) {
72
91
  export function aiModelIn(record) {
73
92
  return signerOf(record.tags, ["AIModel", "AIEffort"]);
74
93
  }
94
+ /** The record with the host the AI played it in (D-97); with none known, as it is. */
95
+ export function withAiHost(record, host) {
96
+ const named = host?.trim();
97
+ return named === undefined || named === "" ? record : { ...record, tags: { ...record.tags, AIHost: named } };
98
+ }
99
+ /** The host the AI played the game in, as its tags say (D-97); undefined without one. */
100
+ export function aiHostIn(record) {
101
+ const host = record.tags.AIHost?.trim();
102
+ return host === undefined || host === "" ? undefined : host;
103
+ }
104
+ /**
105
+ * The AI's name from its host: Claude Code and the desktop app are Claude, Codex is Codex; without a host it names, from the model, a Claude model is
106
+ * Claude (D-97). Undefined otherwise.
107
+ */
108
+ export function aiNameOf(host, model) {
109
+ if (/claude/i.test(host ?? "")) {
110
+ return "Claude";
111
+ }
112
+ if (/codex/i.test(host ?? "")) {
113
+ return "Codex";
114
+ }
115
+ return /^claude/i.test(model?.trim() ?? "") ? "Claude" : undefined;
116
+ }
117
+ /** The record with the AI's word on the whole game, or its last word on the ending, in place of the one it had: each only when given. */
118
+ export function withGameReview(record, { summary, closing }) {
119
+ const summed = summary === undefined ? gameReviewOf(record) : noteText(summary);
120
+ const closed = closing === undefined ? closingOf(record) : noteText(closing);
121
+ const comment = [withoutReview(record.comment), summed ? `[%review ${summed}]` : "", closed ? `[%closing ${closed}]` : ""].filter((part) => part !== "").join(" ");
122
+ return comment === "" ? { tags: record.tags, moves: record.moves } : { tags: record.tags, comment, moves: record.moves };
123
+ }
75
124
  // The effort has no tag of its own in PGN: Chesspal's sits beside the Annotator.
76
125
  const effortTag = "AnnotatorEffort";
77
126
  /** A model and its effort as the tags say, each only when not blank; undefined without the model. */
@@ -88,15 +137,8 @@ function signed(tags, [modelTag, modelEffortTag], model, effort) {
88
137
  const others = Object.entries(tags).filter(([name]) => name !== modelTag && name !== modelEffortTag);
89
138
  return { ...Object.fromEntries(others), [modelTag]: model, ...(effort === undefined ? {} : { [modelEffortTag]: effort }) };
90
139
  }
91
- /** The record with the AI's word on the whole game, or its last word on the ending, in place of the one it had: each only when given. */
92
- export function withGameReview(record, { summary, closing }) {
93
- const summed = summary === undefined ? gameReviewOf(record) : noteText(summary);
94
- const closed = closing === undefined ? closingOf(record) : noteText(closing);
95
- const comment = [withoutReview(record.comment), summed ? `[%review ${summed}]` : "", closed ? `[%closing ${closed}]` : ""].filter((part) => part !== "").join(" ");
96
- return comment === "" ? { tags: record.tags, moves: record.moves } : { tags: record.tags, comment, moves: record.moves };
97
- }
98
140
  /** Whether a word is one of the badges. */
99
- export function isBadge(word) {
141
+ function isBadge(word) {
100
142
  return badges.includes(word ?? "");
101
143
  }
102
144
  // The NAGs that judge a move ($1 to $6): a new badge takes their place.
@@ -104,6 +146,7 @@ const judgmentNags = new Set([1, 2, 3, 4, 5, 6]);
104
146
  // The badges PGN has a NAG for: !! ! ?! ? ??.
105
147
  const nagOfBadge = { brilliant: 3, great: 1, inaccuracy: 6, mistake: 2, blunder: 4 };
106
148
  const badgePattern = /\[%badge (\w+)\]/;
149
+ const reviewMark = /\[%(badge|review) /;
107
150
  const notePattern = /\[%review ([^\]]*)\]/;
108
151
  const closingPattern = /\[%closing ([^\]]*)\]/;
109
152
  const arrowPattern = /\[%cal ([^\]]*)\]/;
@@ -3,3 +3,7 @@ import type { AiModel } from "../core/index.ts";
3
3
  export declare function noteAiModel(folder: string, noted: AiModel): boolean;
4
4
  /** The AI's model last noted for the games of a folder in this process; none on the command line, or before the AI's first move. */
5
5
  export declare function aiModelOf(folder: string): AiModel | undefined;
6
+ /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. */
7
+ export declare function noteAiHost(folder: string, host: string): void;
8
+ /** The host the AI played its last move in, for the games of a folder in this process; none on the command line, or before the AI's first move. */
9
+ export declare function aiHostOf(folder: string): string | undefined;
@@ -11,5 +11,14 @@ export function noteAiModel(folder, noted) {
11
11
  export function aiModelOf(folder) {
12
12
  return models.get(folder);
13
13
  }
14
+ /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. */
15
+ export function noteAiHost(folder, host) {
16
+ hosts.set(folder, host);
17
+ }
18
+ /** The host the AI played its last move in, for the games of a folder in this process; none on the command line, or before the AI's first move. */
19
+ export function aiHostOf(folder) {
20
+ return hosts.get(folder);
21
+ }
14
22
  // By the game's folder, the profile's, where the games are archived.
15
23
  const models = new Map();
24
+ const hosts = new Map();
@@ -1,4 +1,4 @@
1
- /** The game in analysis: its name as the lists write it, when the analysis last changed it, and whether the player brought it on the page and the AI has not gone over it yet (T-92). */
1
+ /** The game in analysis: its name as the lists write it, when the analysis last changed it, and whether the player brought it on the page and the AI has not gone over it yet. */
2
2
  export interface Analysis {
3
3
  readonly game: string;
4
4
  readonly at: string;
@@ -2,13 +2,15 @@ import { type GameRecord } from "../core/index.ts";
2
2
  import { type CurrentGame } from "./current-game.ts";
3
3
  /** Where the game goes in the archive, in the player's folder: named by when it started, in the player's time, games/2026-11-02-153012.pgn. */
4
4
  export declare function archiveName(current: CurrentGame): string;
5
+ /** Whether the game over still in game.json is this one of the archive, named as listed; a game.json Chesspal cannot read is no game. */
6
+ export declare function isOverOnTable(folder: string, name: string): boolean;
5
7
  /**
6
8
  * When a new game starts: now, or the first second after it whose name in the archive is free and is not the game before's, which goes there later. Two
7
9
  * games started in the same second, when summer time ends or with the clock set back would otherwise share a name.
8
10
  */
9
11
  export declare function startOfNewGame(folder: string, now: Date, before?: CurrentGame): Date;
10
12
  /**
11
- * Writes the game to the archive, with the plans, how it ended and the AI's model last noted (D-82), and gives its name; the same game again rewrites the
13
+ * Writes the game to the archive, with the plans, how it ended and the AI's model and host last noted (D-82, D-97), and gives its name; the same game again rewrites the
12
14
  * same file.
13
15
  */
14
16
  export declare function archiveGame(folder: string, current: CurrentGame): string;
@@ -17,6 +19,14 @@ export declare function archiveEnded(folder: string, current: CurrentGame): {
17
19
  archive?: string;
18
20
  warning?: string;
19
21
  };
22
+ /**
23
+ * Writes the coach's judgments into a game over already in the archive, beside what the analyst may have written there since (Federico);
24
+ * archives the game when it is not there.
25
+ */
26
+ export declare function archiveCoached(folder: string, current: CurrentGame): {
27
+ archive?: string;
28
+ warning?: string;
29
+ };
20
30
  /** The name of a game over in the archive, when it is there. */
21
31
  export declare function archivedName(folder: string, current: CurrentGame): string | undefined;
22
32
  /** The names of the games in the archive, the latest first: games/2026-11-02-153012.pgn. */
@@ -36,7 +46,18 @@ export declare function archivedWhen(name: string): string;
36
46
  export declare function keepToStudy(root: string, record: GameRecord, now: Date): string;
37
47
  /** Writes again a game of a profile, played or kept to study, by its name as listed: with the AI's review (D-69). */
38
48
  export declare function rewriteGame(folder: string, name: string, record: GameRecord): void;
39
- /** The game as PGN for the player: while it goes on, without the AI's plans (D-29). */
40
- export declare function playerPgn(current: CurrentGame): string;
41
- /** The game as PGN with its moves alone, without comments or plans, for Lichess or chess.com (T-59). */
49
+ /** Deletes a game of a profile, played or kept to study, by its name as listed (D-92), its review with it. */
50
+ export declare function deleteGame(folder: string, name: string): void;
51
+ /** The time a game's name holds, 2026-11-02-153012, wherever it is in the text; undefined when there is none. */
52
+ export declare function gameStemOf(name: string): string | undefined;
53
+ /** Whether a text is a game's name as the lists write it, games/2026-11-02-153012.pgn, with or without its folder. */
54
+ export declare function isGameName(text: string): boolean;
55
+ /**
56
+ * A game's name as the lists write it, from a name without its folder or its .pgn too: 2026-11-04-173054 is games/2026-11-04-173054.pgn, or the game kept
57
+ * to study by that name when no game played has it; any other name as it is, for the archive to refuse.
58
+ */
59
+ export declare function listedName(folder: string, name: string): string;
60
+ /** The game as PGN for the player: while it goes on, without the AI's plans (D-29); once over, with the coach's judgments too (D-88). */
61
+ export declare function playerPgn(folder: string, current: CurrentGame): string;
62
+ /** The game as PGN with its moves alone, without comments or plans, for Lichess or chess.com. */
42
63
  export declare function movesPgn(current: CurrentGame): string;
@@ -1,15 +1,26 @@
1
1
  // The finished games, as PGN in games/ (D-9, D-41), with the AI's plans for the analyst (D-21); and the games played elsewhere, kept to study in study/ (D-68).
2
- import { existsSync, readdirSync, readFileSync } from "node:fs";
2
+ import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { movesOnly, pgnDate, readPgn, withAiModel, writePgn } from "../core/index.js";
5
- import { aiModelOf } from "./ai-model.js";
6
- import { endOf } from "./current-game.js";
4
+ import { movesOnly, pgnDate, readPgn, withAiHost, withAiModel, writePgn } from "../core/index.js";
5
+ import { aiHostOf, aiModelOf } from "./ai-model.js";
6
+ import { coachWords, withCoach } from "./coach.js";
7
+ import { endOf, loadGame } from "./current-game.js";
7
8
  import { makeFolder, writeAtomically } from "./files.js";
8
9
  import { withoutPlans } from "./plans.js";
9
10
  /** Where the game goes in the archive, in the player's folder: named by when it started, in the player's time, games/2026-11-02-153012.pgn. */
10
11
  export function archiveName(current) {
11
12
  return nameAt(new Date(current.started));
12
13
  }
14
+ /** Whether the game over still in game.json is this one of the archive, named as listed; a game.json Chesspal cannot read is no game. */
15
+ export function isOverOnTable(folder, name) {
16
+ try {
17
+ const current = loadGame(folder);
18
+ return current !== undefined && endOf(current.game) !== undefined && archiveName(current) === name;
19
+ }
20
+ catch {
21
+ return false;
22
+ }
23
+ }
13
24
  /**
14
25
  * When a new game starts: now, or the first second after it whose name in the archive is free and is not the game before's, which goes there later. Two
15
26
  * games started in the same second, when summer time ends or with the clock set back would otherwise share a name.
@@ -23,7 +34,7 @@ export function startOfNewGame(folder, now, before) {
23
34
  return start;
24
35
  }
25
36
  /**
26
- * Writes the game to the archive, with the plans, how it ended and the AI's model last noted (D-82), and gives its name; the same game again rewrites the
37
+ * Writes the game to the archive, with the plans, how it ended and the AI's model and host last noted (D-82, D-97), and gives its name; the same game again rewrites the
27
38
  * same file.
28
39
  */
29
40
  export function archiveGame(folder, current) {
@@ -31,7 +42,7 @@ export function archiveGame(folder, current) {
31
42
  const path = join(folder, name);
32
43
  try {
33
44
  makeFolder(dirname(path));
34
- writeAtomically(path, writePgn(withAiModel(endedRecord(current), aiModelOf(folder))));
45
+ writeAtomically(path, writePgn(withAiHost(withAiModel(endedRecord(folder, current), aiModelOf(folder)), aiHostOf(folder))));
35
46
  }
36
47
  catch (error) {
37
48
  throw new Error(`Chesspal could not archive the game: ${error.message}`, { cause: error });
@@ -47,6 +58,23 @@ export function archiveEnded(folder, current) {
47
58
  return { warning: `${error.message} The game is saved in game.json: it goes to the archive when a new game starts.` };
48
59
  }
49
60
  }
61
+ /**
62
+ * Writes the coach's judgments into a game over already in the archive, beside what the analyst may have written there since (Federico);
63
+ * archives the game when it is not there.
64
+ */
65
+ export function archiveCoached(folder, current) {
66
+ const name = archivedName(folder, current);
67
+ if (name === undefined) {
68
+ return archiveEnded(folder, current);
69
+ }
70
+ try {
71
+ writeAtomically(join(folder, name), writePgn(withCoach(archivedGame(folder, name), coachWords(folder, current))));
72
+ return { archive: name };
73
+ }
74
+ catch (error) {
75
+ return { warning: `Chesspal could not write the coach's words into ${name}: ${error.message}` };
76
+ }
77
+ }
50
78
  /** The name of a game over in the archive, when it is there. */
51
79
  export function archivedName(folder, current) {
52
80
  if (endOf(current.game) === undefined) {
@@ -73,8 +101,8 @@ export function keptGame(root, name) {
73
101
  }
74
102
  /** When an archived game started, from its name, in the player's time: 2026-11-02 15:30. */
75
103
  export function archivedWhen(name) {
76
- const [, day, hour, minute] = /(\d{4}-\d{2}-\d{2})-(\d{2})(\d{2})/.exec(name) ?? [];
77
- return day === undefined ? name : `${day} ${hour}:${minute}`;
104
+ const stem = gameStemOf(name);
105
+ return stem === undefined ? name : `${stem.slice(0, 10)} ${stem.slice(11, 13)}:${stem.slice(13, 15)}`;
78
106
  }
79
107
  /**
80
108
  * Keeps a game played elsewhere to study (D-68) in a profile's folder, apart from the games played here, and gives its name, by `now`:
@@ -103,19 +131,70 @@ export function keepToStudy(root, record, now) {
103
131
  }
104
132
  /** Writes again a game of a profile, played or kept to study, by its name as listed: with the AI's review (D-69). */
105
133
  export function rewriteGame(folder, name, record) {
106
- const [, shelf, file] = /^(games|study)\/(.*)$/.exec(name) ?? [];
107
- if (shelf === undefined || !namePattern.test(file ?? "") || !existsSync(join(folder, name))) {
108
- throw new Error(`There is no ${name} to write again: give a game's name as past_games or study_games writes it.`);
109
- }
134
+ const path = listedPath(folder, name, "to write again");
110
135
  try {
111
- writeAtomically(join(folder, name), writePgn(record));
136
+ writeAtomically(path, writePgn(record));
112
137
  }
113
138
  catch (error) {
114
139
  throw new Error(`Chesspal could not write ${name}: ${error.message}`, { cause: error });
115
140
  }
116
141
  }
142
+ /** Deletes a game of a profile, played or kept to study, by its name as listed (D-92), its review with it. */
143
+ export function deleteGame(folder, name) {
144
+ const path = listedPath(folder, name, "to delete");
145
+ try {
146
+ rmSync(path);
147
+ }
148
+ catch (error) {
149
+ throw new Error(`Chesspal could not delete ${name}: ${error.message}`, { cause: error });
150
+ }
151
+ }
152
+ /** The file of a game by its name as listed, games/… or study/…; refused when there is none, saying what it was wanted for. */
153
+ function listedPath(folder, name, wanted) {
154
+ const [, shelf, file] = /^(games|study)\/(.*)$/.exec(name) ?? [];
155
+ if (shelf === undefined || !namePattern.test(file ?? "") || !existsSync(join(folder, name))) {
156
+ throw new Error(`There is no ${name} ${wanted}: give a game's name as past_games or study_games writes it.`);
157
+ }
158
+ return join(folder, name);
159
+ }
117
160
  const shelves = { games: { what: "of the archive", list: "past_games" }, study: { what: "kept to study", list: "study_games" } };
118
- const namePattern = /^\d{4}-\d{2}-\d{2}-\d{6}\.pgn$/;
161
+ // A game's name is the time it started, 2026-11-02-153012, with .pgn, in its shelf's folder (D-41): the only place that writes the format.
162
+ const stemPattern = /\d{4}-\d{2}-\d{2}-\d{6}/;
163
+ const namePattern = new RegExp(`^${stemPattern.source}\\.pgn$`);
164
+ const listedPattern = new RegExp(`^\\s*(?:(games|study)/)?(${stemPattern.source})(\\.pgn)?\\s*$`);
165
+ /** The time a game's name holds, 2026-11-02-153012, wherever it is in the text; undefined when there is none. */
166
+ export function gameStemOf(name) {
167
+ return stemPattern.exec(name)?.[0];
168
+ }
169
+ /** Whether a text is a game's name as the lists write it, games/2026-11-02-153012.pgn, with or without its folder. */
170
+ export function isGameName(text) {
171
+ return new RegExp(listedPattern.source, "i").exec(text)?.[3] !== undefined;
172
+ }
173
+ /**
174
+ * A game's name as the lists write it, from a name without its folder or its .pgn too: 2026-11-04-173054 is games/2026-11-04-173054.pgn, or the game kept
175
+ * to study by that name when no game played has it; any other name as it is, for the archive to refuse.
176
+ */
177
+ export function listedName(folder, name) {
178
+ const [, shelf, stem] = listedPattern.exec(name) ?? [];
179
+ if (stem === undefined) {
180
+ return name;
181
+ }
182
+ if (shelf !== undefined) {
183
+ return `${shelf}/${stem}.pgn`;
184
+ }
185
+ const played = `games/${stem}.pgn`;
186
+ const kept = `study/${stem}.pgn`;
187
+ return !archivedGames(folder).includes(played) && keptGames(folder).includes(kept) ? kept : played;
188
+ }
189
+ /** The game as PGN for the player: while it goes on, without the AI's plans (D-29); once over, with the coach's judgments too (D-88). */
190
+ export function playerPgn(folder, current) {
191
+ return writePgn(endOf(current.game) === undefined ? withoutPlans(current.game.record) : endedRecord(folder, current));
192
+ }
193
+ /** The game as PGN with its moves alone, without comments or plans, for Lichess or chess.com. */
194
+ export function movesPgn(current) {
195
+ const { record, outcome } = current.game;
196
+ return writePgn(movesOnly(endOf(current.game) === undefined ? record : withTermination(record, outcome)));
197
+ }
119
198
  function gameNames(folder, shelf) {
120
199
  try {
121
200
  return readdirSync(join(folder, shelf)).filter((name) => namePattern.test(name)).sort().reverse().map((name) => `${shelf}/${name}`);
@@ -144,14 +223,6 @@ function gameNamed(folder, shelf, name) {
144
223
  }
145
224
  return record;
146
225
  }
147
- /** The game as PGN for the player: while it goes on, without the AI's plans (D-29). */
148
- export function playerPgn(current) {
149
- return writePgn(endOf(current.game) === undefined ? withoutPlans(current.game.record) : endedRecord(current));
150
- }
151
- /** The game as PGN with its moves alone, without comments or plans, for Lichess or chess.com (T-59). */
152
- export function movesPgn(current) {
153
- return writePgn(movesOnly(endOf(current.game) === undefined ? current.game.record : endedRecord(current)));
154
- }
155
226
  /** Whether two records are the same game: the same tags and the same moves, whatever their comments say. */
156
227
  function sameGame(one, other) {
157
228
  const shape = (record) => JSON.stringify([record?.tags, record?.moves.map((move) => move.san)]);
@@ -161,8 +232,11 @@ function nameAt(date, shelf = "games") {
161
232
  const time = date.toTimeString().slice(0, 8).replaceAll(":", "");
162
233
  return `${shelf}/${pgnDate(date).replaceAll(".", "-")}-${time}.pgn`;
163
234
  }
164
- /** The record with its Termination tag, when the board ended the game and the tag is missing. */
165
- function endedRecord({ game }) {
166
- const { record, outcome } = game;
235
+ /** The record of a game over with the coach's judgments (D-88), and its Termination tag when the board ended the game and the tag is missing. */
236
+ function endedRecord(folder, current) {
237
+ const { record, outcome } = current.game;
238
+ return withTermination(withCoach(record, coachWords(folder, current)), outcome);
239
+ }
240
+ function withTermination(record, outcome) {
167
241
  return outcome.over && record.tags.Termination === undefined ? { ...record, tags: { ...record.tags, Termination: outcome.ending } } : record;
168
242
  }
@@ -0,0 +1,57 @@
1
+ import { type Badge, type DrawnArrow, type DrawnSquare, type GameRecord, type Lead } from "../core/index.ts";
2
+ import type { CurrentGame } from "./current-game.ts";
3
+ /** The coach's word on a move of the player's: the move by the plies up to it, its badge and note, and the moves of theirs its note names. */
4
+ export interface CoachJudgment {
5
+ readonly ply: number;
6
+ readonly badge?: Badge;
7
+ readonly note?: string;
8
+ /** The moves in SAN its note names that the player could play: the page shows it only once none is legal on their turn (Anatolij). */
9
+ readonly held?: string[];
10
+ /** Only as shown, never kept: a held note the player's last move let go, which the card shows now (Zoe). */
11
+ readonly released?: true;
12
+ }
13
+ /** The coach's hint for the player's turn, for the position it was given in, with what it draws on the board to show it. */
14
+ export interface CoachHint {
15
+ readonly position: number;
16
+ readonly text: string;
17
+ readonly arrows?: DrawnArrow[];
18
+ readonly squares?: DrawnSquare[];
19
+ }
20
+ /** Who the coach sees ahead in a position the player was to move in (D-90), by the plies up to it. */
21
+ export interface CoachLead {
22
+ readonly position: number;
23
+ readonly lead: Lead;
24
+ }
25
+ /** What the coach wrote in a game. */
26
+ export interface CoachWords {
27
+ readonly judgments: readonly CoachJudgment[];
28
+ readonly hint?: CoachHint;
29
+ readonly leads?: readonly CoachLead[];
30
+ }
31
+ /** What the coach wrote in the game going on: nothing when the file is another game's, or missing. */
32
+ export declare function coachWords(folder: string, current: CurrentGame): CoachWords;
33
+ /** Keeps the coach's word on a move of the player's, in place of one it wrote on the same move. */
34
+ export declare function keepJudgment(folder: string, current: CurrentGame, judgment: CoachJudgment): void;
35
+ /** Keeps the coach's hint for the player's turn, in place of the one before. */
36
+ export declare function keepHint(folder: string, current: CurrentGame, hint: CoachHint): void;
37
+ /** Keeps who the coach sees ahead in a position, in place of what it said there before. */
38
+ export declare function keepLead(folder: string, current: CurrentGame, { position, lead }: CoachLead): void;
39
+ /** Since when the coach has waited for the player's move in a position: the first time it asked, `now` then. */
40
+ export declare function coachWaitingSince(folder: string, current: CurrentGame, position: number, now: Date): Date;
41
+ /** Whether a coach has started on the game as it is now: until one has, the chat is told again to start it (Priya). */
42
+ export declare function coachStarted(folder: string, current: CurrentGame): boolean;
43
+ /** Notes that a coach starts on the game as it is now: true for the first, false for another the chat started on it too, which stops. */
44
+ export declare function startCoach(folder: string, current: CurrentGame): boolean;
45
+ /** Forgets the coach's words on the game over, when its review is deleted (D-92): they would go back into its PGN. */
46
+ export declare function forgetCoach(folder: string): void;
47
+ /**
48
+ * What the player may see of it in the game as it is: the judgments but those that name a move the player may play, held while the opponent is to move
49
+ * too (Anatolij), and the hint only in its own position. A held one that the last pair of moves let go is marked, for the card to show it
50
+ * now, however far back its move (Zoe).
51
+ */
52
+ export declare function coachShown({ judgments, hint, leads }: CoachWords, { game, ai }: CurrentGame): CoachWords;
53
+ /**
54
+ * The record with the coach's judgments in the comments of the moves they judge, as the analyst writes its own: for the game over. A move the analyst has
55
+ * reviewed already keeps its review (Federico); the book's badge alone is no review, and stays under a note alone, as annotate_game keeps it.
56
+ */
57
+ export declare function withCoach(record: GameRecord, { judgments, leads }: CoachWords): GameRecord;
@@ -0,0 +1,144 @@
1
+ // What the coach writes during a game with the helps on (D-88): its judgments of the player's moves, its hint for their turn and who it sees ahead (D-90),
2
+ // in coach.json beside game.json, where no command of the opponent's reads them; the judgments and the leads go into the game's PGN once it is over.
3
+ import { readFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { z } from "zod";
6
+ import { badges, Game, leadOf, leadText, readLead, reviewed, reviewOf, withLead } from "../core/index.js";
7
+ import { writeAtomically } from "./files.js";
8
+ /** What the coach wrote in the game going on: nothing when the file is another game's, or missing. */
9
+ export function coachWords(folder, current) {
10
+ const { judgments, hint, leads = [] } = keptFor(folder, current);
11
+ const read = leads.flatMap(({ position, lead }) => {
12
+ const known = readLead(lead);
13
+ return known === undefined ? [] : [{ position, lead: known }];
14
+ });
15
+ return { judgments, ...(hint === undefined ? {} : { hint }), ...(read.length === 0 ? {} : { leads: read }) };
16
+ }
17
+ /** Keeps the coach's word on a move of the player's, in place of one it wrote on the same move. */
18
+ export function keepJudgment(folder, current, judgment) {
19
+ const kept = keptFor(folder, current);
20
+ const judgments = [...kept.judgments.filter((one) => one.ply !== judgment.ply), judgment].sort((one, other) => one.ply - other.ply);
21
+ write(folder, { ...kept, judgments });
22
+ }
23
+ /** Keeps the coach's hint for the player's turn, in place of the one before. */
24
+ export function keepHint(folder, current, hint) {
25
+ write(folder, { ...keptFor(folder, current), hint });
26
+ }
27
+ /** Keeps who the coach sees ahead in a position, in place of what it said there before. */
28
+ export function keepLead(folder, current, { position, lead }) {
29
+ const kept = keptFor(folder, current);
30
+ const leads = [...(kept.leads ?? []).filter((one) => one.position !== position), { position, lead: leadText(lead) }].sort((one, other) => one.position - other.position);
31
+ write(folder, { ...kept, leads });
32
+ }
33
+ /** Since when the coach has waited for the player's move in a position: the first time it asked, `now` then. */
34
+ export function coachWaitingSince(folder, current, position, now) {
35
+ const kept = keptFor(folder, current);
36
+ if (kept.waiting?.position === position) {
37
+ return new Date(kept.waiting.since);
38
+ }
39
+ write(folder, { ...kept, waiting: { position, since: now.toISOString() } });
40
+ return now;
41
+ }
42
+ /** Whether a coach has started on the game as it is now: until one has, the chat is told again to start it (Priya). */
43
+ export function coachStarted(folder, current) {
44
+ return keptFor(folder, current).begun === current.game.record.moves.length;
45
+ }
46
+ /** Notes that a coach starts on the game as it is now: true for the first, false for another the chat started on it too, which stops. */
47
+ export function startCoach(folder, current) {
48
+ const kept = keptFor(folder, current);
49
+ const plies = current.game.record.moves.length;
50
+ if (kept.begun === plies) {
51
+ return false;
52
+ }
53
+ write(folder, { ...kept, begun: plies });
54
+ return true;
55
+ }
56
+ /** Forgets the coach's words on the game over, when its review is deleted (D-92): they would go back into its PGN. */
57
+ export function forgetCoach(folder) {
58
+ rmSync(join(folder, fileName), { force: true });
59
+ }
60
+ /**
61
+ * What the player may see of it in the game as it is: the judgments but those that name a move the player may play, held while the opponent is to move
62
+ * too (Anatolij), and the hint only in its own position. A held one that the last pair of moves let go is marked, for the card to show it
63
+ * now, however far back its move (Zoe).
64
+ */
65
+ export function coachShown({ judgments, hint, leads = [] }, { game, ai }) {
66
+ const plies = game.record.moves.length;
67
+ const legal = game.position.turn === ai ? undefined : legalMoves(game);
68
+ const shown = judgments.filter(({ held }) => held === undefined || (legal !== undefined && !names(held, legal)));
69
+ const before = legal === undefined || plies < 2 || shown.every(({ held }) => held === undefined)
70
+ ? undefined
71
+ : legalMoves(new Game({ tags: game.record.tags, moves: game.record.moves.slice(0, plies - 2) }));
72
+ const marked = before === undefined ? shown : shown.map((judgment) => judgment.held !== undefined && names(judgment.held, before) ? { ...judgment, released: true } : judgment);
73
+ return { judgments: marked, ...(hint === undefined || hint.position !== plies ? {} : { hint }), ...(leads.length === 0 ? {} : { leads }) };
74
+ }
75
+ /**
76
+ * The record with the coach's judgments in the comments of the moves they judge, as the analyst writes its own: for the game over. A move the analyst has
77
+ * reviewed already keeps its review (Federico); the book's badge alone is no review, and stays under a note alone, as annotate_game keeps it.
78
+ */
79
+ export function withCoach(record, { judgments, leads = [] }) {
80
+ if (judgments.length === 0 && leads.length === 0) {
81
+ return record;
82
+ }
83
+ const moves = record.moves.map((move, index) => {
84
+ const judgment = judgments.find((one) => one.ply === index + 1);
85
+ const review = reviewOf(move.comment);
86
+ if (judgment === undefined || (review !== undefined && (review.badge !== "book" || review.note !== undefined))) {
87
+ return move;
88
+ }
89
+ const badge = judgment.badge ?? review?.badge;
90
+ return reviewed(move, { ...(badge === undefined ? {} : { badge }), ...(judgment.note === undefined ? {} : { note: judgment.note }) });
91
+ });
92
+ return { ...record, moves: withLeads(moves, leads) };
93
+ }
94
+ /** The moves with who the coach saw ahead after each (D-90), where nobody has said it there since: on the move that led to the position it was given in. */
95
+ function withLeads(moves, leads) {
96
+ return moves.map((move, index) => {
97
+ const lead = leads.find((one) => one.position === index + 1)?.lead;
98
+ return lead === undefined || leadOf(move.comment) !== undefined ? move : withLead(move, lead);
99
+ });
100
+ }
101
+ /** The legal moves in a position, without their checks. */
102
+ function legalMoves(game) {
103
+ return new Set(game.legalMoves().map((move) => withoutCheck(move.san)));
104
+ }
105
+ /** Whether a note's moves name one of the legal moves. */
106
+ function names(held, legal) {
107
+ return held.some((san) => legal.has(withoutCheck(san)));
108
+ }
109
+ /** A move in SAN without its check or mate, which the position it is played in decides: Qxf7# then is Qxf7+ now. */
110
+ function withoutCheck(san) {
111
+ return san.replace(/[+#]$/, "");
112
+ }
113
+ const fileName = "coach.json";
114
+ const colours = ["green", "red", "yellow", "blue"];
115
+ const coachFile = z.object({
116
+ started: z.string(),
117
+ judgments: z.array(z.object({ ply: z.int().min(1), badge: z.enum(badges).optional(), note: z.string().optional(), held: z.array(z.string()).optional() })),
118
+ leads: z.array(z.object({ position: z.int().min(0), lead: z.string() })).optional(),
119
+ hint: z.object({
120
+ position: z.int().min(0),
121
+ text: z.string(),
122
+ arrows: z.array(z.object({ colour: z.enum(colours), from: z.string(), to: z.string() })).optional(),
123
+ squares: z.array(z.object({ colour: z.enum(colours), square: z.string() })).optional(),
124
+ }).optional(),
125
+ waiting: z.object({ position: z.int().min(0), since: z.iso.datetime() }).optional(),
126
+ begun: z.int().min(0).optional(),
127
+ });
128
+ /** What is kept for a game, by when it started: nothing yet when the file is another game's, or missing. */
129
+ function keptFor(folder, current) {
130
+ const kept = read(folder);
131
+ return kept?.started === current.started ? kept : { started: current.started, judgments: [] };
132
+ }
133
+ function read(folder) {
134
+ try {
135
+ return coachFile.parse(JSON.parse(readFileSync(join(folder, fileName), "utf8")));
136
+ }
137
+ catch {
138
+ // None yet, or one Chesspal cannot read: the coach's words are only a help, and the next one written replaces it.
139
+ return undefined;
140
+ }
141
+ }
142
+ function write(folder, kept) {
143
+ writeAtomically(join(folder, fileName), `${JSON.stringify(kept, null, 2)}\n`);
144
+ }
@@ -41,6 +41,8 @@ export declare function drawOfferOf(current: CurrentGame): Speaker | undefined;
41
41
  export declare function bothSidesMoved(game: Game): boolean;
42
42
  /** A stamp of the folder's game.json that changes whenever it is written, whoever writes it; undefined when there is none. */
43
43
  export declare function gameStamp(folder: string): string | undefined;
44
+ /** Removes game.json, when the game over it holds is deleted from the archive (D-92): a new game would archive it again. */
45
+ export declare function removeGame(folder: string): void;
44
46
  /** A line of the talk as game.json and talk.json keep it. */
45
47
  export declare const savedLine: z.ZodObject<{
46
48
  who: z.ZodEnum<{
@@ -1,6 +1,6 @@
1
1
  // The player's current game on disk (D-27, D-41): game.json, its PGN and the talk on the board page inside, written whole at every change, and kept in
2
2
  // memory while nobody else changes it.
3
- import { readFileSync, statSync } from "node:fs";
3
+ import { readFileSync, rmSync, statSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { z } from "zod";
6
6
  import { Game, readPgn, writePgn } from "../core/index.js";
@@ -65,6 +65,12 @@ export function bothSidesMoved(game) {
65
65
  export function gameStamp(folder) {
66
66
  return stampOf(join(folder, fileName));
67
67
  }
68
+ /** Removes game.json, when the game over it holds is deleted from the archive (D-92): a new game would archive it again. */
69
+ export function removeGame(folder) {
70
+ const path = join(folder, fileName);
71
+ kept.delete(path);
72
+ rmSync(path, { force: true });
73
+ }
68
74
  const fileName = "game.json";
69
75
  /** A line of the talk as game.json and talk.json keep it. */
70
76
  export const savedLine = z.object({ who: z.enum(["player", "ai"]), text: z.string(), ply: z.int().min(0), at: z.iso.datetime(), agent: z.literal(true).optional() });
@@ -1,3 +1,4 @@
1
+ // Writing the player's files safely: whole or not at all, with failures that say what to do (D-27).
1
2
  import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
3
  /** Writes a file whole or not at all: a temporary file beside it, then renamed over it (D-27). A failure says what to do about it. */
3
4
  export function writeAtomically(path, text) {
@@ -1,10 +1,11 @@
1
- export { aiModelOf, noteAiModel } from "./ai-model.ts";
1
+ export { aiHostOf, aiModelOf, noteAiHost, noteAiModel } from "./ai-model.ts";
2
2
  export { type Analysis, endAnalysis, gameInAnalysis, startAnalysis } from "./analysis.ts";
3
- export { archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, keepToStudy, keptGame, keptGames, movesPgn, playerPgn, rewriteGame, startOfNewGame } from "./archive.ts";
3
+ export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.ts";
4
4
  export { type ChatStart, cleanSince, lastChatStart, saveChatStart } from "./chat-start.ts";
5
- export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, type GameEnd, gameStamp, loadGame, saveGame, type Speaker, type TalkLine } from "./current-game.ts";
5
+ export { type CoachHint, type CoachJudgment, type CoachLead, coachShown, coachStarted, coachWaitingSince, type CoachWords, coachWords, forgetCoach, keepHint, keepJudgment, keepLead, startCoach, withCoach } from "./coach.ts";
6
+ export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, type GameEnd, gameStamp, loadGame, removeGame, saveGame, type Speaker, type TalkLine } from "./current-game.ts";
6
7
  export { makeFolder, renameOver, writeAtomically } from "./files.ts";
7
8
  export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.ts";
8
9
  export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk } from "./table-talk.ts";
9
- export { type Heard, heardLines, playerLinesThisMove, type Sayer, type Talk, talkLimits, talkLine, unheardWords, wakesTheChat, type Words, WordsRefused } from "./talk.ts";
10
+ export { type Heard, heardLines, type Sayer, type Talk, talkLimits, talkLine, unheardWords, wakesTheChat, type Words, WordsRefused } from "./talk.ts";
10
11
  export { keepThought, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";