chesspal 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (225) hide show
  1. package/README.md +136 -2
  2. package/dist/cli/chat-hook.d.ts +2 -0
  3. package/dist/cli/chat-hook.js +33 -0
  4. package/dist/cli/cli.d.ts +2 -0
  5. package/dist/cli/cli.js +28 -0
  6. package/dist/cli/command-line.d.ts +8 -0
  7. package/dist/cli/command-line.js +178 -0
  8. package/dist/commands/ai-move.d.ts +37 -0
  9. package/dist/commands/ai-move.js +92 -0
  10. package/dist/commands/ai-say.d.ts +20 -0
  11. package/dist/commands/ai-say.js +40 -0
  12. package/dist/commands/analyse-game.d.ts +61 -0
  13. package/dist/commands/analyse-game.js +238 -0
  14. package/dist/commands/annotate-game.d.ts +35 -0
  15. package/dist/commands/annotate-game.js +78 -0
  16. package/dist/commands/clock.d.ts +15 -0
  17. package/dist/commands/clock.js +45 -0
  18. package/dist/commands/context.d.ts +56 -0
  19. package/dist/commands/context.js +36 -0
  20. package/dist/commands/draw-offer.d.ts +10 -0
  21. package/dist/commands/draw-offer.js +43 -0
  22. package/dist/commands/drawings.d.ts +15 -0
  23. package/dist/commands/drawings.js +23 -0
  24. package/dist/commands/end-game.d.ts +23 -0
  25. package/dist/commands/end-game.js +64 -0
  26. package/dist/commands/export-game.d.ts +14 -0
  27. package/dist/commands/export-game.js +26 -0
  28. package/dist/commands/game-summary.d.ts +95 -0
  29. package/dist/commands/game-summary.js +239 -0
  30. package/dist/commands/index.d.ts +3 -0
  31. package/dist/commands/index.js +19 -0
  32. package/dist/commands/init.d.ts +15 -0
  33. package/dist/commands/init.js +73 -0
  34. package/dist/commands/levels.d.ts +9 -0
  35. package/dist/commands/levels.js +25 -0
  36. package/dist/commands/look.d.ts +31 -0
  37. package/dist/commands/look.js +61 -0
  38. package/dist/commands/new-game.d.ts +69 -0
  39. package/dist/commands/new-game.js +215 -0
  40. package/dist/commands/opening-book.d.ts +13 -0
  41. package/dist/commands/opening-book.js +84 -0
  42. package/dist/commands/page-link.d.ts +11 -0
  43. package/dist/commands/page-link.js +28 -0
  44. package/dist/commands/past-games.d.ts +11 -0
  45. package/dist/commands/past-games.js +47 -0
  46. package/dist/commands/play-move.d.ts +44 -0
  47. package/dist/commands/play-move.js +102 -0
  48. package/dist/commands/player-move.d.ts +17 -0
  49. package/dist/commands/player-move.js +44 -0
  50. package/dist/commands/player-say.d.ts +5 -0
  51. package/dist/commands/player-say.js +16 -0
  52. package/dist/commands/ponder.d.ts +42 -0
  53. package/dist/commands/ponder.js +167 -0
  54. package/dist/commands/position.d.ts +14 -0
  55. package/dist/commands/position.js +128 -0
  56. package/dist/commands/profiles.d.ts +60 -0
  57. package/dist/commands/profiles.js +129 -0
  58. package/dist/commands/registry.d.ts +54 -0
  59. package/dist/commands/registry.js +61 -0
  60. package/dist/commands/say.d.ts +16 -0
  61. package/dist/commands/say.js +84 -0
  62. package/dist/commands/status.d.ts +29 -0
  63. package/dist/commands/status.js +103 -0
  64. package/dist/commands/study-games.d.ts +5 -0
  65. package/dist/commands/study-games.js +40 -0
  66. package/dist/commands/study.d.ts +38 -0
  67. package/dist/commands/study.js +122 -0
  68. package/dist/commands/view.d.ts +14 -0
  69. package/dist/commands/view.js +125 -0
  70. package/dist/core/board-text.d.ts +7 -0
  71. package/dist/core/board-text.js +18 -0
  72. package/dist/core/book.json +1 -0
  73. package/dist/core/clock.d.ts +45 -0
  74. package/dist/core/clock.js +109 -0
  75. package/dist/core/facts.d.ts +55 -0
  76. package/dist/core/facts.js +175 -0
  77. package/dist/core/game.d.ts +93 -0
  78. package/dist/core/game.js +348 -0
  79. package/dist/core/illegal.d.ts +19 -0
  80. package/dist/core/illegal.js +141 -0
  81. package/dist/core/index.d.ts +19 -0
  82. package/dist/core/index.js +11 -0
  83. package/dist/core/levels.d.ts +20 -0
  84. package/dist/core/levels.js +33 -0
  85. package/dist/core/material.d.ts +9 -0
  86. package/dist/core/material.js +12 -0
  87. package/dist/core/moods.d.ts +23 -0
  88. package/dist/core/moods.js +45 -0
  89. package/dist/core/openings.d.ts +37 -0
  90. package/dist/core/openings.js +98 -0
  91. package/dist/core/openings.json +1 -0
  92. package/dist/core/pgn.d.ts +10 -0
  93. package/dist/core/pgn.js +213 -0
  94. package/dist/core/profile.d.ts +117 -0
  95. package/dist/core/profile.js +232 -0
  96. package/dist/core/record.d.ts +21 -0
  97. package/dist/core/record.js +25 -0
  98. package/dist/core/review.d.ts +74 -0
  99. package/dist/core/review.js +126 -0
  100. package/dist/games/ai-model.d.ts +5 -0
  101. package/dist/games/ai-model.js +15 -0
  102. package/dist/games/analysis.d.ts +12 -0
  103. package/dist/games/analysis.js +26 -0
  104. package/dist/games/archive.d.ts +42 -0
  105. package/dist/games/archive.js +168 -0
  106. package/dist/games/chat-start.d.ts +16 -0
  107. package/dist/games/chat-start.js +26 -0
  108. package/dist/games/current-game.d.ts +54 -0
  109. package/dist/games/current-game.js +128 -0
  110. package/dist/games/files.d.ts +9 -0
  111. package/dist/games/files.js +52 -0
  112. package/dist/games/index.d.ts +10 -0
  113. package/dist/games/index.js +11 -0
  114. package/dist/games/plans.d.ts +9 -0
  115. package/dist/games/plans.js +23 -0
  116. package/dist/games/table-talk.d.ts +12 -0
  117. package/dist/games/table-talk.js +25 -0
  118. package/dist/games/talk.d.ts +40 -0
  119. package/dist/games/talk.js +131 -0
  120. package/dist/games/thoughts.d.ts +15 -0
  121. package/dist/games/thoughts.js +60 -0
  122. package/dist/home/data-folder.d.ts +12 -0
  123. package/dist/home/data-folder.js +55 -0
  124. package/dist/home/index.d.ts +2 -0
  125. package/dist/home/index.js +3 -0
  126. package/dist/home/open-folder.d.ts +2 -0
  127. package/dist/home/open-folder.js +14 -0
  128. package/dist/host-files/host-files.d.ts +10 -0
  129. package/dist/host-files/host-files.js +167 -0
  130. package/dist/host-files/index.d.ts +2 -0
  131. package/dist/host-files/index.js +3 -0
  132. package/dist/host-files/ponder-agents.d.ts +12 -0
  133. package/dist/host-files/ponder-agents.js +40 -0
  134. package/dist/index.d.ts +4 -0
  135. package/dist/index.js +2 -0
  136. package/dist/page/app.js +576 -0
  137. package/dist/page/avatars/beanie.webp +0 -0
  138. package/dist/page/avatars/beard.webp +0 -0
  139. package/dist/page/avatars/bun.webp +0 -0
  140. package/dist/page/avatars/cat.webp +0 -0
  141. package/dist/page/avatars/curls.webp +0 -0
  142. package/dist/page/avatars/glasses.webp +0 -0
  143. package/dist/page/avatars/moustache.webp +0 -0
  144. package/dist/page/avatars/owl.webp +0 -0
  145. package/dist/page/avatars/short-hair.webp +0 -0
  146. package/dist/page/avatars/silver-bob.webp +0 -0
  147. package/dist/page/avatars.js +16 -0
  148. package/dist/page/badges.js +50 -0
  149. package/dist/page/board.js +566 -0
  150. package/dist/page/browse.js +17 -0
  151. package/dist/page/clock-time.js +12 -0
  152. package/dist/page/dialog.js +26 -0
  153. package/dist/page/ending.js +73 -0
  154. package/dist/page/explain.js +58 -0
  155. package/dist/page/fold-tab.js +9 -0
  156. package/dist/page/folds.js +43 -0
  157. package/dist/page/game-column.js +44 -0
  158. package/dist/page/header.js +27 -0
  159. package/dist/page/helps-view.js +51 -0
  160. package/dist/page/helps.js +90 -0
  161. package/dist/page/main.js +8 -0
  162. package/dist/page/model-name.js +20 -0
  163. package/dist/page/move-list.js +68 -0
  164. package/dist/page/new-game-dialog.js +42 -0
  165. package/dist/page/opening-book.js +14 -0
  166. package/dist/page/pieces/CREDITS.md +36 -0
  167. package/dist/page/pieces/cburnett-small.svg +20 -0
  168. package/dist/page/pieces/cburnett.svg +20 -0
  169. package/dist/page/pieces/nitida-small.svg +19 -0
  170. package/dist/page/pieces/nitida.svg +20 -0
  171. package/dist/page/pieces/torretta-small.svg +19 -0
  172. package/dist/page/pieces/torretta.svg +19 -0
  173. package/dist/page/pieces.js +28 -0
  174. package/dist/page/player-card.js +60 -0
  175. package/dist/page/plies.js +62 -0
  176. package/dist/page/profile-dialogs.js +123 -0
  177. package/dist/page/profile.js +95 -0
  178. package/dist/page/settings-panel.js +32 -0
  179. package/dist/page/settings.js +76 -0
  180. package/dist/page/sounds/capture.ogg +0 -0
  181. package/dist/page/sounds/check.ogg +0 -0
  182. package/dist/page/sounds/draw.ogg +0 -0
  183. package/dist/page/sounds/flag.ogg +0 -0
  184. package/dist/page/sounds/mate.ogg +0 -0
  185. package/dist/page/sounds/move.ogg +0 -0
  186. package/dist/page/sounds/offer.ogg +0 -0
  187. package/dist/page/sounds/resign.ogg +0 -0
  188. package/dist/page/sounds.js +105 -0
  189. package/dist/page/studied.js +14 -0
  190. package/dist/page/study-dialog.js +58 -0
  191. package/dist/page/style.css +4398 -0
  192. package/dist/page/talk.js +172 -0
  193. package/dist/page/text.js +593 -0
  194. package/dist/page/torretta.js +38 -0
  195. package/dist/page/voice.js +38 -0
  196. package/dist/profiles/avatar.d.ts +23 -0
  197. package/dist/profiles/avatar.js +84 -0
  198. package/dist/profiles/front-matter.d.ts +9 -0
  199. package/dist/profiles/front-matter.js +33 -0
  200. package/dist/profiles/index.d.ts +3 -0
  201. package/dist/profiles/index.js +5 -0
  202. package/dist/profiles/profiles.d.ts +58 -0
  203. package/dist/profiles/profiles.js +237 -0
  204. package/dist/profiles/settings.d.ts +12 -0
  205. package/dist/profiles/settings.js +46 -0
  206. package/dist/server/analyses.d.ts +17 -0
  207. package/dist/server/analyses.js +46 -0
  208. package/dist/server/bell.d.ts +60 -0
  209. package/dist/server/bell.js +209 -0
  210. package/dist/server/caller.d.ts +10 -0
  211. package/dist/server/caller.js +99 -0
  212. package/dist/server/http.d.ts +16 -0
  213. package/dist/server/http.js +57 -0
  214. package/dist/server/mcp.d.ts +14 -0
  215. package/dist/server/mcp.js +179 -0
  216. package/dist/server/page.d.ts +46 -0
  217. package/dist/server/page.js +657 -0
  218. package/dist/server/profile-routes.d.ts +19 -0
  219. package/dist/server/profile-routes.js +186 -0
  220. package/dist/version.d.ts +2 -0
  221. package/dist/version.js +3 -0
  222. package/instructions/analyst.md +8 -0
  223. package/instructions/opponent.md +15 -0
  224. package/instructions/ponder.md +9 -0
  225. package/package.json +66 -5
@@ -0,0 +1,74 @@
1
+ import { type GameRecord, type RecordedMove } from "./record.ts";
2
+ /** The badges of a move, as chess.com has them, from the best to the worst. */
3
+ export declare const badges: readonly ["brilliant", "great", "best", "excellent", "good", "book", "inaccuracy", "mistake", "miss", "blunder"];
4
+ /** A move's badge. */
5
+ export type Badge = (typeof badges)[number];
6
+ /** The colours the AI draws with on the board, by the letters PGN's [%cal] and [%csl] write them with. */
7
+ export declare const drawColours: {
8
+ readonly G: "green";
9
+ readonly R: "red";
10
+ readonly Y: "yellow";
11
+ readonly B: "blue";
12
+ };
13
+ /** A colour the AI draws with. */
14
+ export type DrawColour = (typeof drawColours)[keyof typeof drawColours];
15
+ /** An arrow the AI draws to show its note: from a square to another, in a colour. */
16
+ export interface DrawnArrow {
17
+ readonly colour: DrawColour;
18
+ readonly from: string;
19
+ readonly to: string;
20
+ }
21
+ /** A square the AI colours to show its note. */
22
+ export interface DrawnSquare {
23
+ readonly colour: DrawColour;
24
+ readonly square: string;
25
+ }
26
+ /** The AI's word on a move: its badge, its note, or both, and the arrows and squares it draws to show it. */
27
+ export interface MoveReview {
28
+ readonly badge?: Badge;
29
+ readonly note?: string;
30
+ readonly arrows?: readonly DrawnArrow[];
31
+ readonly squares?: readonly DrawnSquare[];
32
+ }
33
+ /** 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. */
34
+ export declare function reviewOf(comment: string | undefined): MoveReview | undefined;
35
+ /** The arrows and squares a move's comment carries, as the AI draws them with its move when the helps are on (D-78); none without any. */
36
+ export declare function drawingsIn(comment: string | undefined): Pick<MoveReview, "arrows" | "squares">;
37
+ /** An arrow as PGN's [%cal] writes it, a colour's letter and two squares (Gf6e4); none when it is not one. */
38
+ export declare function arrowOf(text: string): DrawnArrow | undefined;
39
+ /** A square as PGN's [%csl] writes it, a colour's letter and the square (Rf2); none when it is not one. */
40
+ export declare function squareOf(text: string): DrawnSquare | undefined;
41
+ /** What the AI wrote about the whole game, when it wrote something; its points start with " • ". */
42
+ export declare function gameReviewOf(record: GameRecord): string | undefined;
43
+ /** The AI's last word on how the game ended, when it wrote one. */
44
+ export declare function closingOf(record: GameRecord): string | undefined;
45
+ /** 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. */
46
+ export declare function reviewed(move: RecordedMove, review: MoveReview): RecordedMove;
47
+ /** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
48
+ export declare function drawingsText({ arrows, squares }: Pick<MoveReview, "arrows" | "squares">): string;
49
+ /** The record with who reviewed it (D-81): the model, else the host, as PGN's Annotator, and the effort only when known; an effort no longer known goes. */
50
+ export declare function withAnnotator(record: GameRecord, { annotator, effort }: {
51
+ annotator: string;
52
+ effort?: string;
53
+ }): GameRecord;
54
+ /** The AI's model, as the host writes it, and its effort when the host gives it (D-82). */
55
+ export interface AiModel {
56
+ readonly model: string;
57
+ readonly effort?: string;
58
+ }
59
+ /** The record with the model the AI played it with (D-82), and the effort only when known; with none known, as it is. */
60
+ export declare function withAiModel(record: GameRecord, played: AiModel | undefined): GameRecord;
61
+ /** Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator. */
62
+ export declare function annotatorOf(record: GameRecord): {
63
+ annotator: string;
64
+ effort?: string;
65
+ } | undefined;
66
+ /** The model the AI played the game with, as its tags say (D-82), and the effort when known; undefined without one. */
67
+ export declare function aiModelIn(record: GameRecord): AiModel | undefined;
68
+ /** 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. */
69
+ export declare function withGameReview(record: GameRecord, { summary, closing }: {
70
+ summary?: string;
71
+ closing?: string;
72
+ }): GameRecord;
73
+ /** Whether a word is one of the badges. */
74
+ export declare function isBadge(word: string | undefined): word is Badge;
@@ -0,0 +1,126 @@
1
+ // The AI's review of a game (D-69): a badge and a note on the moves that matter, as chess.com marks them, and its word on the whole game. They ride in the
2
+ // comments as [%badge …] and [%review …], beside the NAG of the badges that have one, which other programs show.
3
+ import { recordedMove } from "./record.js";
4
+ /** The badges of a move, as chess.com has them, from the best to the worst. */
5
+ export const badges = ["brilliant", "great", "best", "excellent", "good", "book", "inaccuracy", "mistake", "miss", "blunder"];
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" };
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
+ export function reviewOf(comment) {
10
+ const found = badgePattern.exec(comment ?? "")?.[1];
11
+ const badge = isBadge(found) ? found : undefined;
12
+ const note = notePattern.exec(comment ?? "")?.[1]?.trim() || undefined;
13
+ return badge === undefined && note === undefined ? undefined : { ...(badge === undefined ? {} : { badge }), ...(note === undefined ? {} : { note }), ...drawingsIn(comment) };
14
+ }
15
+ /** The arrows and squares a move's comment carries, as the AI draws them with its move when the helps are on (D-78); none without any. */
16
+ export function drawingsIn(comment) {
17
+ const arrows = drawn(arrowPattern, comment, arrowOf);
18
+ const squares = drawn(squarePattern, comment, squareOf);
19
+ return { ...(arrows.length === 0 ? {} : { arrows }), ...(squares.length === 0 ? {} : { squares }) };
20
+ }
21
+ /** An arrow as PGN's [%cal] writes it, a colour's letter and two squares (Gf6e4); none when it is not one. */
22
+ export function arrowOf(text) {
23
+ const [, colour, from, to] = /^([GRYB])([a-h][1-8])([a-h][1-8])$/.exec(text.trim()) ?? [];
24
+ return colour === undefined || from === undefined || to === undefined || from === to ? undefined : { colour: drawColours[colour], from, to };
25
+ }
26
+ /** A square as PGN's [%csl] writes it, a colour's letter and the square (Rf2); none when it is not one. */
27
+ export function squareOf(text) {
28
+ const [, colour, square] = /^([GRYB])([a-h][1-8])$/.exec(text.trim()) ?? [];
29
+ return colour === undefined || square === undefined ? undefined : { colour: drawColours[colour], square };
30
+ }
31
+ /** What the AI wrote about the whole game, when it wrote something; its points start with " • ". */
32
+ export function gameReviewOf(record) {
33
+ return notePattern.exec(record.comment ?? "")?.[1]?.trim() || undefined;
34
+ }
35
+ /** The AI's last word on how the game ended, when it wrote one. */
36
+ export function closingOf(record) {
37
+ return closingPattern.exec(record.comment ?? "")?.[1]?.trim() || undefined;
38
+ }
39
+ /** 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
+ export function reviewed(move, review) {
41
+ const nag = review.badge === undefined ? undefined : nagOfBadge[review.badge];
42
+ const nags = (move.nags ?? []).filter((one) => !judgmentNags.has(one));
43
+ const note = review.note === undefined ? "" : noteText(review.note);
44
+ const comment = [
45
+ withoutReview(move.comment),
46
+ review.badge === undefined ? "" : `[%badge ${review.badge}]`,
47
+ note === "" ? "" : `[%review ${note}]`,
48
+ drawingsText(review),
49
+ ].filter((part) => part !== "").join(" ");
50
+ return recordedMove(move.san, nag === undefined ? nags : [nag, ...nags], comment);
51
+ }
52
+ /** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
53
+ export function drawingsText({ arrows = [], squares = [] }) {
54
+ const arrowList = arrows.map(({ colour, from, to }) => `${letterOf(colour)}${from}${to}`).join(",");
55
+ const squareList = squares.map(({ colour, square }) => `${letterOf(colour)}${square}`).join(",");
56
+ return [arrowList === "" ? "" : `[%cal ${arrowList}]`, squareList === "" ? "" : `[%csl ${squareList}]`].filter((part) => part !== "").join(" ");
57
+ }
58
+ /** The record with who reviewed it (D-81): the model, else the host, as PGN's Annotator, and the effort only when known; an effort no longer known goes. */
59
+ export function withAnnotator(record, { annotator, effort }) {
60
+ return { ...record, tags: signed(record.tags, ["Annotator", effortTag], annotator, effort) };
61
+ }
62
+ /** The record with the model the AI played it with (D-82), and the effort only when known; with none known, as it is. */
63
+ export function withAiModel(record, played) {
64
+ return played === undefined ? record : { ...record, tags: signed(record.tags, ["AIModel", "AIEffort"], played.model, played.effort) };
65
+ }
66
+ /** Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator. */
67
+ export function annotatorOf(record) {
68
+ const found = signerOf(record.tags, ["Annotator", effortTag]);
69
+ return found === undefined ? undefined : { annotator: found.model, ...(found.effort === undefined ? {} : { effort: found.effort }) };
70
+ }
71
+ /** The model the AI played the game with, as its tags say (D-82), and the effort when known; undefined without one. */
72
+ export function aiModelIn(record) {
73
+ return signerOf(record.tags, ["AIModel", "AIEffort"]);
74
+ }
75
+ // The effort has no tag of its own in PGN: Chesspal's sits beside the Annotator.
76
+ const effortTag = "AnnotatorEffort";
77
+ /** A model and its effort as the tags say, each only when not blank; undefined without the model. */
78
+ function signerOf(tags, [modelTag, modelEffortTag]) {
79
+ const model = tags[modelTag]?.trim();
80
+ const effort = tags[modelEffortTag]?.trim();
81
+ if (model === undefined || model === "") {
82
+ return undefined;
83
+ }
84
+ return effort === undefined || effort === "" ? { model } : { model, effort };
85
+ }
86
+ /** The tags with a model and its effort, last: an effort no longer known goes. */
87
+ function signed(tags, [modelTag, modelEffortTag], model, effort) {
88
+ const others = Object.entries(tags).filter(([name]) => name !== modelTag && name !== modelEffortTag);
89
+ return { ...Object.fromEntries(others), [modelTag]: model, ...(effort === undefined ? {} : { [modelEffortTag]: effort }) };
90
+ }
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
+ /** Whether a word is one of the badges. */
99
+ export function isBadge(word) {
100
+ return badges.includes(word ?? "");
101
+ }
102
+ // The NAGs that judge a move ($1 to $6): a new badge takes their place.
103
+ const judgmentNags = new Set([1, 2, 3, 4, 5, 6]);
104
+ // The badges PGN has a NAG for: !! ! ?! ? ??.
105
+ const nagOfBadge = { brilliant: 3, great: 1, inaccuracy: 6, mistake: 2, blunder: 4 };
106
+ const badgePattern = /\[%badge (\w+)\]/;
107
+ const notePattern = /\[%review ([^\]]*)\]/;
108
+ const closingPattern = /\[%closing ([^\]]*)\]/;
109
+ const arrowPattern = /\[%cal ([^\]]*)\]/;
110
+ const squarePattern = /\[%csl ([^\]]*)\]/;
111
+ /** A comment without the review it carried, its drawings with it. */
112
+ function withoutReview(comment) {
113
+ return (comment ?? "").replace(/\s*\[%(badge|review|closing|cal|csl) [^\]]*\]/g, "").trim();
114
+ }
115
+ /** The drawings of one kind a comment carries, those that read. */
116
+ function drawn(pattern, comment, read) {
117
+ return (pattern.exec(comment ?? "")?.[1] ?? "").split(",").map(read).filter((one) => one !== undefined);
118
+ }
119
+ /** A colour's letter in [%cal] and [%csl]. */
120
+ function letterOf(colour) {
121
+ return Object.entries(drawColours).find(([, name]) => name === colour)[0];
122
+ }
123
+ /** A note as a comment's command can hold it: on one line, without brackets or braces, a point of a list starting with " • ". */
124
+ function noteText(text) {
125
+ return text.replace(/[[\]{}]/g, "").replace(/^[ \t]*[-*•][ \t]+/gm, " • ").replace(/\s+/g, " ").trim();
126
+ }
@@ -0,0 +1,5 @@
1
+ import type { AiModel } from "../core/index.ts";
2
+ /** Notes the AI's model for the games of a folder, after its move: a change shows from the next move. Whether it changed. */
3
+ export declare function noteAiModel(folder: string, noted: AiModel): boolean;
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
+ export declare function aiModelOf(folder: string): AiModel | undefined;
@@ -0,0 +1,15 @@
1
+ /** Notes the AI's model for the games of a folder, after its move: a change shows from the next move. Whether it changed. */
2
+ export function noteAiModel(folder, noted) {
3
+ const known = models.get(folder);
4
+ if (known?.model === noted.model && known.effort === noted.effort) {
5
+ return false;
6
+ }
7
+ models.set(folder, noted);
8
+ return true;
9
+ }
10
+ /** 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. */
11
+ export function aiModelOf(folder) {
12
+ return models.get(folder);
13
+ }
14
+ // By the game's folder, the profile's, where the games are archived.
15
+ const models = new Map();
@@ -0,0 +1,12 @@
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). */
2
+ export interface Analysis {
3
+ readonly game: string;
4
+ readonly at: string;
5
+ readonly brought?: true;
6
+ }
7
+ /** Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. */
8
+ export declare function startAnalysis(folder: string, game: string, now: Date, brought?: boolean): void;
9
+ /** The game in analysis, or none when there is none, or analysis.json cannot be read. */
10
+ export declare function gameInAnalysis(folder: string): Analysis | undefined;
11
+ /** Ends the analysis, when a new game starts. */
12
+ export declare function endAnalysis(folder: string): void;
@@ -0,0 +1,26 @@
1
+ // The game in analysis (D-69): analysis.json, in the folder of the profile's games, names the game analyse_game and annotate_game went over last, for the
2
+ // board page to show it once no game goes on; a new game ends it.
3
+ import { readFileSync, rmSync } from "node:fs";
4
+ import { join } from "node:path";
5
+ import { writeAtomically } from "./files.js";
6
+ /** Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. */
7
+ export function startAnalysis(folder, game, now, brought = false) {
8
+ writeAtomically(join(folder, fileName), `${JSON.stringify({ game, at: now.toISOString(), ...(brought ? { brought } : {}) }, null, 2)}\n`);
9
+ }
10
+ /** The game in analysis, or none when there is none, or analysis.json cannot be read. */
11
+ export function gameInAnalysis(folder) {
12
+ try {
13
+ const analysis = JSON.parse(readFileSync(join(folder, fileName), "utf8"));
14
+ return typeof analysis.game === "string" && typeof analysis.at === "string"
15
+ ? { game: analysis.game, at: analysis.at, ...(analysis.brought === true ? { brought: true } : {}) }
16
+ : undefined;
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /** Ends the analysis, when a new game starts. */
23
+ export function endAnalysis(folder) {
24
+ rmSync(join(folder, fileName), { force: true });
25
+ }
26
+ const fileName = "analysis.json";
@@ -0,0 +1,42 @@
1
+ import { type GameRecord } from "../core/index.ts";
2
+ import { type CurrentGame } from "./current-game.ts";
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
+ export declare function archiveName(current: CurrentGame): string;
5
+ /**
6
+ * 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
+ * games started in the same second, when summer time ends or with the clock set back would otherwise share a name.
8
+ */
9
+ export declare function startOfNewGame(folder: string, now: Date, before?: CurrentGame): Date;
10
+ /**
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
12
+ * same file.
13
+ */
14
+ export declare function archiveGame(folder: string, current: CurrentGame): string;
15
+ /** Archives a game that has ended: its name in the archive or, when that fails, a warning, since the game is saved in game.json all the same. */
16
+ export declare function archiveEnded(folder: string, current: CurrentGame): {
17
+ archive?: string;
18
+ warning?: string;
19
+ };
20
+ /** The name of a game over in the archive, when it is there. */
21
+ export declare function archivedName(folder: string, current: CurrentGame): string | undefined;
22
+ /** The names of the games in the archive, the latest first: games/2026-11-02-153012.pgn. */
23
+ export declare function archivedGames(folder: string): string[];
24
+ /** A game of the archive by its name, as listed; refused when it is not there, or cannot be read. */
25
+ export declare function archivedGame(folder: string, name: string): GameRecord;
26
+ /** The names of the games kept to study in a profile's folder, the latest kept first: study/2026-11-04-153012.pgn. */
27
+ export declare function keptGames(root: string): string[];
28
+ /** A game kept to study by its name, as listed; refused when it is not there, or cannot be read. */
29
+ export declare function keptGame(root: string, name: string): GameRecord;
30
+ /** When an archived game started, from its name, in the player's time: 2026-11-02 15:30. */
31
+ export declare function archivedWhen(name: string): string;
32
+ /**
33
+ * 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`:
34
+ * study/2026-11-04-153012.pgn; the same game again keeps the name it has.
35
+ */
36
+ export declare function keepToStudy(root: string, record: GameRecord, now: Date): string;
37
+ /** 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
+ 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). */
42
+ export declare function movesPgn(current: CurrentGame): string;
@@ -0,0 +1,168 @@
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";
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";
7
+ import { makeFolder, writeAtomically } from "./files.js";
8
+ import { withoutPlans } from "./plans.js";
9
+ /** 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
+ export function archiveName(current) {
11
+ return nameAt(new Date(current.started));
12
+ }
13
+ /**
14
+ * 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
+ * games started in the same second, when summer time ends or with the clock set back would otherwise share a name.
16
+ */
17
+ export function startOfNewGame(folder, now, before) {
18
+ const taken = before === undefined ? undefined : archiveName(before);
19
+ let start = now;
20
+ while (nameAt(start) === taken || existsSync(join(folder, nameAt(start)))) {
21
+ start = new Date(start.getTime() + 1_000);
22
+ }
23
+ return start;
24
+ }
25
+ /**
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
27
+ * same file.
28
+ */
29
+ export function archiveGame(folder, current) {
30
+ const name = archiveName(current);
31
+ const path = join(folder, name);
32
+ try {
33
+ makeFolder(dirname(path));
34
+ writeAtomically(path, writePgn(withAiModel(endedRecord(current), aiModelOf(folder))));
35
+ }
36
+ catch (error) {
37
+ throw new Error(`Chesspal could not archive the game: ${error.message}`, { cause: error });
38
+ }
39
+ return name;
40
+ }
41
+ /** Archives a game that has ended: its name in the archive or, when that fails, a warning, since the game is saved in game.json all the same. */
42
+ export function archiveEnded(folder, current) {
43
+ try {
44
+ return { archive: archiveGame(folder, current) };
45
+ }
46
+ catch (error) {
47
+ return { warning: `${error.message} The game is saved in game.json: it goes to the archive when a new game starts.` };
48
+ }
49
+ }
50
+ /** The name of a game over in the archive, when it is there. */
51
+ export function archivedName(folder, current) {
52
+ if (endOf(current.game) === undefined) {
53
+ return undefined;
54
+ }
55
+ const name = archiveName(current);
56
+ return existsSync(join(folder, name)) ? name : undefined;
57
+ }
58
+ /** The names of the games in the archive, the latest first: games/2026-11-02-153012.pgn. */
59
+ export function archivedGames(folder) {
60
+ return gameNames(folder, "games");
61
+ }
62
+ /** A game of the archive by its name, as listed; refused when it is not there, or cannot be read. */
63
+ export function archivedGame(folder, name) {
64
+ return gameNamed(folder, "games", name);
65
+ }
66
+ /** The names of the games kept to study in a profile's folder, the latest kept first: study/2026-11-04-153012.pgn. */
67
+ export function keptGames(root) {
68
+ return gameNames(root, "study");
69
+ }
70
+ /** A game kept to study by its name, as listed; refused when it is not there, or cannot be read. */
71
+ export function keptGame(root, name) {
72
+ return gameNamed(root, "study", name);
73
+ }
74
+ /** When an archived game started, from its name, in the player's time: 2026-11-02 15:30. */
75
+ 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}`;
78
+ }
79
+ /**
80
+ * 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`:
81
+ * study/2026-11-04-153012.pgn; the same game again keeps the name it has.
82
+ */
83
+ export function keepToStudy(root, record, now) {
84
+ const text = writePgn(record);
85
+ // The same game, whatever the review and the book's badges written on it since.
86
+ const [written] = readPgn(text);
87
+ const kept = keptGames(root).find((name) => sameGame(readPgn(readFileSync(join(root, name), "utf8"))[0], written));
88
+ if (kept !== undefined) {
89
+ return kept;
90
+ }
91
+ let name = nameAt(now, "study");
92
+ for (let second = 1; existsSync(join(root, name)); second++) {
93
+ name = nameAt(new Date(now.getTime() + second * 1_000), "study");
94
+ }
95
+ try {
96
+ makeFolder(join(root, "study"));
97
+ writeAtomically(join(root, name), text);
98
+ }
99
+ catch (error) {
100
+ throw new Error(`Chesspal could not keep the game to study: ${error.message}`, { cause: error });
101
+ }
102
+ return name;
103
+ }
104
+ /** 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
+ 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
+ }
110
+ try {
111
+ writeAtomically(join(folder, name), writePgn(record));
112
+ }
113
+ catch (error) {
114
+ throw new Error(`Chesspal could not write ${name}: ${error.message}`, { cause: error });
115
+ }
116
+ }
117
+ 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$/;
119
+ function gameNames(folder, shelf) {
120
+ try {
121
+ return readdirSync(join(folder, shelf)).filter((name) => namePattern.test(name)).sort().reverse().map((name) => `${shelf}/${name}`);
122
+ }
123
+ catch {
124
+ return [];
125
+ }
126
+ }
127
+ function gameNamed(folder, shelf, name) {
128
+ // The name as listed, or without its folder or its .pgn.
129
+ const file = `${(name.startsWith(`${shelf}/`) ? name.slice(shelf.length + 1) : name).trim().replace(/\.pgn$/, "")}.pgn`;
130
+ const { what, list } = shelves[shelf];
131
+ if (!namePattern.test(file)) {
132
+ throw new Error(`${name} is no game ${what}: give a name as ${list} writes it, such as ${shelf}/2026-11-02-153012.pgn.`);
133
+ }
134
+ let text;
135
+ try {
136
+ text = readFileSync(join(folder, shelf, file), "utf8");
137
+ }
138
+ catch {
139
+ throw new Error(`There is no ${name} among the games ${what}: give a name as ${list} writes it.`);
140
+ }
141
+ const [record] = readPgn(text);
142
+ if (record === undefined) {
143
+ throw new Error(`${name} holds no game Chesspal can read.`);
144
+ }
145
+ return record;
146
+ }
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
+ /** Whether two records are the same game: the same tags and the same moves, whatever their comments say. */
156
+ function sameGame(one, other) {
157
+ const shape = (record) => JSON.stringify([record?.tags, record?.moves.map((move) => move.san)]);
158
+ return one !== undefined && shape(one) === shape(other);
159
+ }
160
+ function nameAt(date, shelf = "games") {
161
+ const time = date.toTimeString().slice(0, 8).replaceAll(":", "");
162
+ return `${shelf}/${pgnDate(date).replaceAll(".", "-")}-${time}.pgn`;
163
+ }
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;
167
+ return outcome.over && record.tags.Termination === undefined ? { ...record, tags: { ...record.tags, Termination: outcome.ending } } : record;
168
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * How a chat started, as the host's SessionStart hook says it: new, cleared, resumed or compacted; when, as an ISO date; its session, for the logs; and the
3
+ * file where the host writes the chat, where Claude Code tells which model made each tool call (D-81).
4
+ */
5
+ export interface ChatStart {
6
+ readonly source: string;
7
+ readonly at: string;
8
+ readonly session?: string;
9
+ readonly transcript?: string;
10
+ }
11
+ /** Writes how the chat started, over the last one. */
12
+ export declare function saveChatStart(folder: string, start: ChatStart): void;
13
+ /** How the chat last started, or undefined when no hook wrote it, or what it wrote cannot be read. */
14
+ export declare function lastChatStart(folder: string): ChatStart | undefined;
15
+ /** Whether the chat started clean (new or cleared) after a moment: a resumed or compacted one still holds what came before. */
16
+ export declare function cleanSince(start: ChatStart | undefined, moment: string): boolean;
@@ -0,0 +1,26 @@
1
+ // How the player's chat last started (D-57): chat.json, written by the host's hook when a chat starts, is cleared or resumed; a new game asks for a clean one.
2
+ import { readFileSync } from "node:fs";
3
+ import { join } from "node:path";
4
+ import { writeAtomically } from "./files.js";
5
+ /** Writes how the chat started, over the last one. */
6
+ export function saveChatStart(folder, start) {
7
+ writeAtomically(join(folder, fileName), `${JSON.stringify(start, null, 2)}\n`);
8
+ }
9
+ /** How the chat last started, or undefined when no hook wrote it, or what it wrote cannot be read. */
10
+ export function lastChatStart(folder) {
11
+ try {
12
+ const start = JSON.parse(readFileSync(join(folder, fileName), "utf8"));
13
+ if (typeof start.source !== "string" || typeof start.at !== "string") {
14
+ return undefined;
15
+ }
16
+ return { source: start.source, at: start.at, ...(typeof start.transcript === "string" ? { transcript: start.transcript } : {}) };
17
+ }
18
+ catch {
19
+ return undefined;
20
+ }
21
+ }
22
+ /** Whether the chat started clean (new or cleared) after a moment: a resumed or compacted one still holds what came before. */
23
+ export function cleanSince(start, moment) {
24
+ return start !== undefined && (start.source === "startup" || start.source === "clear") && Date.parse(start.at) > Date.parse(moment);
25
+ }
26
+ const fileName = "chat.json";
@@ -0,0 +1,54 @@
1
+ import { z } from "zod";
2
+ import { type Clock, Game, type Side } from "../core/index.ts";
3
+ /** The player's current game, going on or over: the live game, and what game.json keeps beside its PGN. */
4
+ export interface CurrentGame {
5
+ readonly game: Game;
6
+ /** The side the AI plays. */
7
+ readonly ai: Side;
8
+ /** When the game started, as an ISO date: it names the game in the archive. */
9
+ readonly started: string;
10
+ /** What the player and the AI said on the board page, beside the PGN and never in the archive (D-51). */
11
+ readonly talk: TalkLine[];
12
+ /** The clock, in a game with a time control (D-13). */
13
+ readonly clock?: Clock;
14
+ /** Who offered a draw, while the offer stands: until the other side accepts it, declines it or moves (D-65). */
15
+ readonly drawOffer?: Speaker;
16
+ }
17
+ /** Who speaks on the board page. */
18
+ export type Speaker = "player" | "ai";
19
+ /** A line said on the board page: who said it, its words, how many moves the game had then, and when, as an ISO date; the AI's agent on the player's time (D-72). */
20
+ export interface TalkLine {
21
+ readonly who: Speaker;
22
+ readonly text: string;
23
+ readonly ply: number;
24
+ readonly at: string;
25
+ readonly agent?: true;
26
+ }
27
+ /** How a game ended: its result, and why when the record says it or the board decided it. */
28
+ export interface GameEnd {
29
+ readonly result: string;
30
+ readonly termination?: string;
31
+ }
32
+ /** The current game of a folder, as it is on disk, or undefined when there is none. A game.json Chesspal cannot read throws: nothing overwrites it. */
33
+ export declare function loadGame(folder: string): CurrentGame | undefined;
34
+ /** Writes the current game to game.json, whole or not at all; from then on it is the one kept in memory. */
35
+ export declare function saveGame(folder: string, current: CurrentGame): void;
36
+ /** How the game ended, or undefined while it goes on: by the board, or by what the record says, such as a resignation. */
37
+ export declare function endOf(game: Game): GameEnd | undefined;
38
+ /** The draw offer that stands: none once the game is over (D-65). */
39
+ export declare function drawOfferOf(current: CurrentGame): Speaker | undefined;
40
+ /** Whether each side has made a move in the game, before which no draw is agreed (FIDE 5.2.3, D-65). */
41
+ export declare function bothSidesMoved(game: Game): boolean;
42
+ /** A stamp of the folder's game.json that changes whenever it is written, whoever writes it; undefined when there is none. */
43
+ export declare function gameStamp(folder: string): string | undefined;
44
+ /** A line of the talk as game.json and talk.json keep it. */
45
+ export declare const savedLine: z.ZodObject<{
46
+ who: z.ZodEnum<{
47
+ player: "player";
48
+ ai: "ai";
49
+ }>;
50
+ text: z.ZodString;
51
+ ply: z.ZodInt;
52
+ at: z.ZodISODateTime;
53
+ agent: z.ZodOptional<z.ZodLiteral<true>>;
54
+ }, z.core.$strip>;