chesspal 0.0.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +99 -2
- package/dist/cli/chat-hook.d.ts +2 -0
- package/dist/cli/chat-hook.js +33 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +28 -0
- package/dist/cli/command-line.d.ts +7 -0
- package/dist/cli/command-line.js +177 -0
- package/dist/commands/agents.d.ts +26 -0
- package/dist/commands/agents.js +72 -0
- package/dist/commands/ai-move.d.ts +28 -0
- package/dist/commands/ai-move.js +85 -0
- package/dist/commands/ai-say.d.ts +22 -0
- package/dist/commands/ai-say.js +58 -0
- package/dist/commands/analyse-game.d.ts +65 -0
- package/dist/commands/analyse-game.js +265 -0
- package/dist/commands/annotate-game.d.ts +39 -0
- package/dist/commands/annotate-game.js +131 -0
- package/dist/commands/answer-lines.d.ts +28 -0
- package/dist/commands/answer-lines.js +144 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/coach.d.ts +45 -0
- package/dist/commands/coach.js +344 -0
- package/dist/commands/context.d.ts +67 -0
- package/dist/commands/context.js +37 -0
- package/dist/commands/delete-game.d.ts +7 -0
- package/dist/commands/delete-game.js +30 -0
- package/dist/commands/delete-review.d.ts +8 -0
- package/dist/commands/delete-review.js +34 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +13 -0
- package/dist/commands/drawings.js +25 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +67 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +27 -0
- package/dist/commands/game-summary.d.ts +78 -0
- package/dist/commands/game-summary.js +102 -0
- package/dist/commands/index.d.ts +18 -0
- package/dist/commands/index.js +64 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/lead-input.d.ts +8 -0
- package/dist/commands/lead-input.js +21 -0
- package/dist/commands/levels.d.ts +9 -0
- package/dist/commands/levels.js +25 -0
- package/dist/commands/look.d.ts +31 -0
- package/dist/commands/look.js +64 -0
- package/dist/commands/new-game.d.ts +68 -0
- package/dist/commands/new-game.js +218 -0
- package/dist/commands/opening-book.d.ts +13 -0
- package/dist/commands/opening-book.js +84 -0
- package/dist/commands/page-link.d.ts +11 -0
- package/dist/commands/page-link.js +28 -0
- package/dist/commands/past-games.d.ts +11 -0
- package/dist/commands/past-games.js +47 -0
- package/dist/commands/play-move.d.ts +37 -0
- package/dist/commands/play-move.js +94 -0
- package/dist/commands/player-move.d.ts +13 -0
- package/dist/commands/player-move.js +51 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +52 -0
- package/dist/commands/ponder.js +179 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +130 -0
- package/dist/commands/profiles.d.ts +69 -0
- package/dist/commands/profiles.js +163 -0
- package/dist/commands/registry.d.ts +59 -0
- package/dist/commands/registry.js +62 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/speech.d.ts +8 -0
- package/dist/commands/speech.js +20 -0
- package/dist/commands/status.d.ts +35 -0
- package/dist/commands/status.js +128 -0
- package/dist/commands/study-games.d.ts +5 -0
- package/dist/commands/study-games.js +40 -0
- package/dist/commands/study.d.ts +38 -0
- package/dist/commands/study.js +106 -0
- package/dist/commands/view.d.ts +14 -0
- package/dist/commands/view.js +125 -0
- package/dist/core/board-text.d.ts +7 -0
- package/dist/core/board-text.js +18 -0
- package/dist/core/book.json +1 -0
- package/dist/core/clock.d.ts +45 -0
- package/dist/core/clock.js +109 -0
- package/dist/core/facts.d.ts +55 -0
- package/dist/core/facts.js +175 -0
- package/dist/core/game.d.ts +95 -0
- package/dist/core/game.js +352 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +20 -0
- package/dist/core/index.js +12 -0
- package/dist/core/lead.d.ts +15 -0
- package/dist/core/lead.js +40 -0
- package/dist/core/levels.d.ts +20 -0
- package/dist/core/levels.js +33 -0
- package/dist/core/material.d.ts +9 -0
- package/dist/core/material.js +12 -0
- package/dist/core/moods.d.ts +21 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +35 -0
- package/dist/core/openings.js +98 -0
- package/dist/core/openings.json +1 -0
- package/dist/core/pgn.d.ts +10 -0
- package/dist/core/pgn.js +213 -0
- package/dist/core/profile.d.ts +117 -0
- package/dist/core/profile.js +232 -0
- package/dist/core/record.d.ts +21 -0
- package/dist/core/record.js +25 -0
- package/dist/core/review.d.ts +80 -0
- package/dist/core/review.js +146 -0
- package/dist/games/ai-model.d.ts +5 -0
- package/dist/games/ai-model.js +15 -0
- package/dist/games/analysis.d.ts +12 -0
- package/dist/games/analysis.js +26 -0
- package/dist/games/archive.d.ts +63 -0
- package/dist/games/archive.js +242 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/coach.d.ts +57 -0
- package/dist/games/coach.js +144 -0
- package/dist/games/current-game.d.ts +56 -0
- package/dist/games/current-game.js +134 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +53 -0
- package/dist/games/index.d.ts +11 -0
- package/dist/games/index.js +12 -0
- package/dist/games/plans.d.ts +9 -0
- package/dist/games/plans.js +23 -0
- package/dist/games/table-talk.d.ts +12 -0
- package/dist/games/table-talk.js +25 -0
- package/dist/games/talk.d.ts +36 -0
- package/dist/games/talk.js +131 -0
- package/dist/games/thoughts.d.ts +15 -0
- package/dist/games/thoughts.js +60 -0
- package/dist/home/data-folder.d.ts +12 -0
- package/dist/home/data-folder.js +55 -0
- package/dist/home/index.d.ts +2 -0
- package/dist/home/index.js +3 -0
- package/dist/home/open-folder.d.ts +2 -0
- package/dist/home/open-folder.js +14 -0
- package/dist/host-files/agents.d.ts +20 -0
- package/dist/host-files/agents.js +53 -0
- package/dist/host-files/host-files.d.ts +15 -0
- package/dist/host-files/host-files.js +182 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/page/app/anchor.js +88 -0
- package/dist/page/app/app.js +458 -0
- package/dist/page/app/keys.js +94 -0
- package/dist/page/app/phase.js +30 -0
- package/dist/page/app/served.js +73 -0
- package/dist/page/avatars/avatars.js +19 -0
- package/dist/page/avatars/beanie.webp +0 -0
- package/dist/page/avatars/beard.webp +0 -0
- package/dist/page/avatars/bun.webp +0 -0
- package/dist/page/avatars/cat.webp +0 -0
- package/dist/page/avatars/curls.webp +0 -0
- package/dist/page/avatars/glasses.webp +0 -0
- package/dist/page/avatars/moustache.webp +0 -0
- package/dist/page/avatars/owl.webp +0 -0
- package/dist/page/avatars/short-hair.webp +0 -0
- package/dist/page/avatars/silver-bob.webp +0 -0
- package/dist/page/avatars/thought.js +10 -0
- package/dist/page/avatars/torretta.js +38 -0
- package/dist/page/base.css +160 -0
- package/dist/page/board/board.css +959 -0
- package/dist/page/board/board.js +125 -0
- package/dist/page/board/explain.js +58 -0
- package/dist/page/board/marks.js +160 -0
- package/dist/page/board/motion.js +71 -0
- package/dist/page/board/pointer.js +239 -0
- package/dist/page/board/squares.js +26 -0
- package/dist/page/dialogs/analyses.js +195 -0
- package/dist/page/dialogs/dialog.js +26 -0
- package/dist/page/dialogs/dialogs.css +1228 -0
- package/dist/page/dialogs/new-game.js +42 -0
- package/dist/page/dialogs/profiles.js +152 -0
- package/dist/page/dialogs/search.js +31 -0
- package/dist/page/dialogs/settings.js +42 -0
- package/dist/page/dialogs/studied.js +39 -0
- package/dist/page/dialogs/study.js +51 -0
- package/dist/page/game/badges.js +50 -0
- package/dist/page/game/browse.js +17 -0
- package/dist/page/game/card-fold.js +40 -0
- package/dist/page/game/column.js +51 -0
- package/dist/page/game/ending.js +73 -0
- package/dist/page/game/game.css +577 -0
- package/dist/page/game/move-list.js +72 -0
- package/dist/page/game/opening-book.js +14 -0
- package/dist/page/game/plies.js +82 -0
- package/dist/page/game/result-tile.js +14 -0
- package/dist/page/helps/helps.css +689 -0
- package/dist/page/helps/helps.js +220 -0
- package/dist/page/helps/view.js +127 -0
- package/dist/page/layout/fold-tab.js +11 -0
- package/dist/page/layout/folds.js +43 -0
- package/dist/page/layout/header.js +28 -0
- package/dist/page/layout/layout.css +834 -0
- package/dist/page/lead/lead.js +82 -0
- package/dist/page/lead/view.js +38 -0
- package/dist/page/main.js +8 -0
- package/dist/page/pieces/CREDITS.md +19 -0
- package/dist/page/pieces/capablanca-small.svg +20 -0
- package/dist/page/pieces/capablanca.svg +22 -0
- package/dist/page/pieces/nitida-small.svg +20 -0
- package/dist/page/pieces/nitida.svg +21 -0
- package/dist/page/pieces/pieces.js +28 -0
- package/dist/page/pieces/torretta-small.svg +20 -0
- package/dist/page/pieces/torretta.svg +21 -0
- package/dist/page/players/card.js +56 -0
- package/dist/page/players/clock-time.js +12 -0
- package/dist/page/players/model-name.js +20 -0
- package/dist/page/profile/profile.js +99 -0
- package/dist/page/profile/settings.js +77 -0
- package/dist/page/sounds/capture.ogg +0 -0
- package/dist/page/sounds/check.ogg +0 -0
- package/dist/page/sounds/draw.ogg +0 -0
- package/dist/page/sounds/flag.ogg +0 -0
- package/dist/page/sounds/mate.ogg +0 -0
- package/dist/page/sounds/move.ogg +0 -0
- package/dist/page/sounds/offer.ogg +0 -0
- package/dist/page/sounds/resign.ogg +0 -0
- package/dist/page/sounds/sounds.js +105 -0
- package/dist/page/style.css +9 -0
- package/dist/page/talk/talk.css +958 -0
- package/dist/page/talk/talk.js +165 -0
- package/dist/page/talk/voice.js +38 -0
- package/dist/page/text.js +711 -0
- package/dist/page/theme.css +292 -0
- package/dist/profiles/avatar.d.ts +23 -0
- package/dist/profiles/avatar.js +84 -0
- package/dist/profiles/front-matter.d.ts +9 -0
- package/dist/profiles/front-matter.js +33 -0
- package/dist/profiles/index.d.ts +3 -0
- package/dist/profiles/index.js +5 -0
- package/dist/profiles/profiles.d.ts +69 -0
- package/dist/profiles/profiles.js +254 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/bell.d.ts +68 -0
- package/dist/server/bell.js +225 -0
- package/dist/server/caller.d.ts +21 -0
- package/dist/server/caller.js +132 -0
- package/dist/server/mcp.d.ts +13 -0
- package/dist/server/mcp.js +207 -0
- package/dist/server/page/analyses.d.ts +23 -0
- package/dist/server/page/analyses.js +65 -0
- package/dist/server/page/files.d.ts +5 -0
- package/dist/server/page/files.js +92 -0
- package/dist/server/page/game.d.ts +6 -0
- package/dist/server/page/game.js +381 -0
- package/dist/server/page/http.d.ts +29 -0
- package/dist/server/page/http.js +68 -0
- package/dist/server/page/page.d.ts +41 -0
- package/dist/server/page/page.js +229 -0
- package/dist/server/page/profiles.d.ts +19 -0
- package/dist/server/page/profiles.js +215 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analysis.md +4 -0
- package/instructions/analyst-agent.md +8 -0
- package/instructions/analyst.md +4 -0
- package/instructions/coach.md +11 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,80 @@
|
|
|
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
|
+
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
|
+
/** 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). */
|
|
46
|
+
export declare function isReviewed(record: GameRecord): boolean;
|
|
47
|
+
/** 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. */
|
|
48
|
+
export declare function reviewed(move: RecordedMove, review: MoveReview): RecordedMove;
|
|
49
|
+
/**
|
|
50
|
+
* 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
|
|
51
|
+
* 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.
|
|
52
|
+
*/
|
|
53
|
+
export declare function unreviewed(record: GameRecord): GameRecord;
|
|
54
|
+
/** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
|
|
55
|
+
export declare function drawingsText({ arrows, squares }: Pick<MoveReview, "arrows" | "squares">): string;
|
|
56
|
+
/** 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. */
|
|
57
|
+
export declare function withAnnotator(record: GameRecord, { annotator, effort }: {
|
|
58
|
+
annotator: string;
|
|
59
|
+
effort?: string;
|
|
60
|
+
}): GameRecord;
|
|
61
|
+
/** The AI's model, as the host writes it, and its effort when the host gives it (D-82). */
|
|
62
|
+
export interface AiModel {
|
|
63
|
+
readonly model: string;
|
|
64
|
+
readonly effort?: string;
|
|
65
|
+
}
|
|
66
|
+
/** The record with the model the AI played it with (D-82), and the effort only when known; with none known, as it is. */
|
|
67
|
+
export declare function withAiModel(record: GameRecord, played: AiModel | undefined): GameRecord;
|
|
68
|
+
/** Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator. */
|
|
69
|
+
export declare function annotatorOf(record: GameRecord): {
|
|
70
|
+
annotator: string;
|
|
71
|
+
effort?: string;
|
|
72
|
+
} | undefined;
|
|
73
|
+
/** The model the AI played the game with, as its tags say (D-82), and the effort when known; undefined without one. */
|
|
74
|
+
export declare function aiModelIn(record: GameRecord): AiModel | undefined;
|
|
75
|
+
/** 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. */
|
|
76
|
+
export declare function withGameReview(record: GameRecord, { summary, closing }: {
|
|
77
|
+
summary?: string;
|
|
78
|
+
closing?: string;
|
|
79
|
+
}): GameRecord;
|
|
80
|
+
export {};
|
|
@@ -0,0 +1,146 @@
|
|
|
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
|
+
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
|
+
/** 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
|
+
}
|
|
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. */
|
|
44
|
+
export function reviewed(move, review) {
|
|
45
|
+
const nag = review.badge === undefined ? undefined : nagOfBadge[review.badge];
|
|
46
|
+
const nags = (move.nags ?? []).filter((one) => !judgmentNags.has(one));
|
|
47
|
+
const note = review.note === undefined ? "" : noteText(review.note);
|
|
48
|
+
const comment = [
|
|
49
|
+
withoutReview(move.comment),
|
|
50
|
+
review.badge === undefined ? "" : `[%badge ${review.badge}]`,
|
|
51
|
+
note === "" ? "" : `[%review ${note}]`,
|
|
52
|
+
drawingsText(review),
|
|
53
|
+
].filter((part) => part !== "").join(" ");
|
|
54
|
+
return recordedMove(move.san, nag === undefined ? nags : [nag, ...nags], comment);
|
|
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
|
+
}
|
|
71
|
+
/** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
|
|
72
|
+
export function drawingsText({ arrows = [], squares = [] }) {
|
|
73
|
+
const arrowList = arrows.map(({ colour, from, to }) => `${letterOf(colour)}${from}${to}`).join(",");
|
|
74
|
+
const squareList = squares.map(({ colour, square }) => `${letterOf(colour)}${square}`).join(",");
|
|
75
|
+
return [arrowList === "" ? "" : `[%cal ${arrowList}]`, squareList === "" ? "" : `[%csl ${squareList}]`].filter((part) => part !== "").join(" ");
|
|
76
|
+
}
|
|
77
|
+
/** 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. */
|
|
78
|
+
export function withAnnotator(record, { annotator, effort }) {
|
|
79
|
+
return { ...record, tags: signed(record.tags, ["Annotator", effortTag], annotator, effort) };
|
|
80
|
+
}
|
|
81
|
+
/** The record with the model the AI played it with (D-82), and the effort only when known; with none known, as it is. */
|
|
82
|
+
export function withAiModel(record, played) {
|
|
83
|
+
return played === undefined ? record : { ...record, tags: signed(record.tags, ["AIModel", "AIEffort"], played.model, played.effort) };
|
|
84
|
+
}
|
|
85
|
+
/** Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator. */
|
|
86
|
+
export function annotatorOf(record) {
|
|
87
|
+
const found = signerOf(record.tags, ["Annotator", effortTag]);
|
|
88
|
+
return found === undefined ? undefined : { annotator: found.model, ...(found.effort === undefined ? {} : { effort: found.effort }) };
|
|
89
|
+
}
|
|
90
|
+
/** The model the AI played the game with, as its tags say (D-82), and the effort when known; undefined without one. */
|
|
91
|
+
export function aiModelIn(record) {
|
|
92
|
+
return signerOf(record.tags, ["AIModel", "AIEffort"]);
|
|
93
|
+
}
|
|
94
|
+
/** 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. */
|
|
95
|
+
export function withGameReview(record, { summary, closing }) {
|
|
96
|
+
const summed = summary === undefined ? gameReviewOf(record) : noteText(summary);
|
|
97
|
+
const closed = closing === undefined ? closingOf(record) : noteText(closing);
|
|
98
|
+
const comment = [withoutReview(record.comment), summed ? `[%review ${summed}]` : "", closed ? `[%closing ${closed}]` : ""].filter((part) => part !== "").join(" ");
|
|
99
|
+
return comment === "" ? { tags: record.tags, moves: record.moves } : { tags: record.tags, comment, moves: record.moves };
|
|
100
|
+
}
|
|
101
|
+
// The effort has no tag of its own in PGN: Chesspal's sits beside the Annotator.
|
|
102
|
+
const effortTag = "AnnotatorEffort";
|
|
103
|
+
/** A model and its effort as the tags say, each only when not blank; undefined without the model. */
|
|
104
|
+
function signerOf(tags, [modelTag, modelEffortTag]) {
|
|
105
|
+
const model = tags[modelTag]?.trim();
|
|
106
|
+
const effort = tags[modelEffortTag]?.trim();
|
|
107
|
+
if (model === undefined || model === "") {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return effort === undefined || effort === "" ? { model } : { model, effort };
|
|
111
|
+
}
|
|
112
|
+
/** The tags with a model and its effort, last: an effort no longer known goes. */
|
|
113
|
+
function signed(tags, [modelTag, modelEffortTag], model, effort) {
|
|
114
|
+
const others = Object.entries(tags).filter(([name]) => name !== modelTag && name !== modelEffortTag);
|
|
115
|
+
return { ...Object.fromEntries(others), [modelTag]: model, ...(effort === undefined ? {} : { [modelEffortTag]: effort }) };
|
|
116
|
+
}
|
|
117
|
+
/** Whether a word is one of the badges. */
|
|
118
|
+
function isBadge(word) {
|
|
119
|
+
return badges.includes(word ?? "");
|
|
120
|
+
}
|
|
121
|
+
// The NAGs that judge a move ($1 to $6): a new badge takes their place.
|
|
122
|
+
const judgmentNags = new Set([1, 2, 3, 4, 5, 6]);
|
|
123
|
+
// The badges PGN has a NAG for: !! ! ?! ? ??.
|
|
124
|
+
const nagOfBadge = { brilliant: 3, great: 1, inaccuracy: 6, mistake: 2, blunder: 4 };
|
|
125
|
+
const badgePattern = /\[%badge (\w+)\]/;
|
|
126
|
+
const reviewMark = /\[%(badge|review) /;
|
|
127
|
+
const notePattern = /\[%review ([^\]]*)\]/;
|
|
128
|
+
const closingPattern = /\[%closing ([^\]]*)\]/;
|
|
129
|
+
const arrowPattern = /\[%cal ([^\]]*)\]/;
|
|
130
|
+
const squarePattern = /\[%csl ([^\]]*)\]/;
|
|
131
|
+
/** A comment without the review it carried, its drawings with it. */
|
|
132
|
+
function withoutReview(comment) {
|
|
133
|
+
return (comment ?? "").replace(/\s*\[%(badge|review|closing|cal|csl) [^\]]*\]/g, "").trim();
|
|
134
|
+
}
|
|
135
|
+
/** The drawings of one kind a comment carries, those that read. */
|
|
136
|
+
function drawn(pattern, comment, read) {
|
|
137
|
+
return (pattern.exec(comment ?? "")?.[1] ?? "").split(",").map(read).filter((one) => one !== undefined);
|
|
138
|
+
}
|
|
139
|
+
/** A colour's letter in [%cal] and [%csl]. */
|
|
140
|
+
function letterOf(colour) {
|
|
141
|
+
return Object.entries(drawColours).find(([, name]) => name === colour)[0];
|
|
142
|
+
}
|
|
143
|
+
/** A note as a comment's command can hold it: on one line, without brackets or braces, a point of a list starting with " • ". */
|
|
144
|
+
function noteText(text) {
|
|
145
|
+
return text.replace(/[[\]{}]/g, "").replace(/^[ \t]*[-*•][ \t]+/gm, " • ").replace(/\s+/g, " ").trim();
|
|
146
|
+
}
|
|
@@ -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. */
|
|
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,63 @@
|
|
|
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
|
+
/** 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;
|
|
7
|
+
/**
|
|
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
|
|
9
|
+
* games started in the same second, when summer time ends or with the clock set back would otherwise share a name.
|
|
10
|
+
*/
|
|
11
|
+
export declare function startOfNewGame(folder: string, now: Date, before?: CurrentGame): Date;
|
|
12
|
+
/**
|
|
13
|
+
* 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
|
|
14
|
+
* same file.
|
|
15
|
+
*/
|
|
16
|
+
export declare function archiveGame(folder: string, current: CurrentGame): string;
|
|
17
|
+
/** 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. */
|
|
18
|
+
export declare function archiveEnded(folder: string, current: CurrentGame): {
|
|
19
|
+
archive?: string;
|
|
20
|
+
warning?: string;
|
|
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
|
+
};
|
|
30
|
+
/** The name of a game over in the archive, when it is there. */
|
|
31
|
+
export declare function archivedName(folder: string, current: CurrentGame): string | undefined;
|
|
32
|
+
/** The names of the games in the archive, the latest first: games/2026-11-02-153012.pgn. */
|
|
33
|
+
export declare function archivedGames(folder: string): string[];
|
|
34
|
+
/** A game of the archive by its name, as listed; refused when it is not there, or cannot be read. */
|
|
35
|
+
export declare function archivedGame(folder: string, name: string): GameRecord;
|
|
36
|
+
/** The names of the games kept to study in a profile's folder, the latest kept first: study/2026-11-04-153012.pgn. */
|
|
37
|
+
export declare function keptGames(root: string): string[];
|
|
38
|
+
/** A game kept to study by its name, as listed; refused when it is not there, or cannot be read. */
|
|
39
|
+
export declare function keptGame(root: string, name: string): GameRecord;
|
|
40
|
+
/** When an archived game started, from its name, in the player's time: 2026-11-02 15:30. */
|
|
41
|
+
export declare function archivedWhen(name: string): string;
|
|
42
|
+
/**
|
|
43
|
+
* 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`:
|
|
44
|
+
* study/2026-11-04-153012.pgn; the same game again keeps the name it has.
|
|
45
|
+
*/
|
|
46
|
+
export declare function keepToStudy(root: string, record: GameRecord, now: Date): string;
|
|
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). */
|
|
48
|
+
export declare function rewriteGame(folder: string, name: string, record: GameRecord): void;
|
|
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. */
|
|
63
|
+
export declare function movesPgn(current: CurrentGame): string;
|
|
@@ -0,0 +1,242 @@
|
|
|
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, rmSync } 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 { coachWords, withCoach } from "./coach.js";
|
|
7
|
+
import { endOf, loadGame } from "./current-game.js";
|
|
8
|
+
import { makeFolder, writeAtomically } from "./files.js";
|
|
9
|
+
import { withoutPlans } from "./plans.js";
|
|
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. */
|
|
11
|
+
export function archiveName(current) {
|
|
12
|
+
return nameAt(new Date(current.started));
|
|
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
|
+
}
|
|
24
|
+
/**
|
|
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
|
|
26
|
+
* games started in the same second, when summer time ends or with the clock set back would otherwise share a name.
|
|
27
|
+
*/
|
|
28
|
+
export function startOfNewGame(folder, now, before) {
|
|
29
|
+
const taken = before === undefined ? undefined : archiveName(before);
|
|
30
|
+
let start = now;
|
|
31
|
+
while (nameAt(start) === taken || existsSync(join(folder, nameAt(start)))) {
|
|
32
|
+
start = new Date(start.getTime() + 1_000);
|
|
33
|
+
}
|
|
34
|
+
return start;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* 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
|
|
38
|
+
* same file.
|
|
39
|
+
*/
|
|
40
|
+
export function archiveGame(folder, current) {
|
|
41
|
+
const name = archiveName(current);
|
|
42
|
+
const path = join(folder, name);
|
|
43
|
+
try {
|
|
44
|
+
makeFolder(dirname(path));
|
|
45
|
+
writeAtomically(path, writePgn(withAiModel(endedRecord(folder, current), aiModelOf(folder))));
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
throw new Error(`Chesspal could not archive the game: ${error.message}`, { cause: error });
|
|
49
|
+
}
|
|
50
|
+
return name;
|
|
51
|
+
}
|
|
52
|
+
/** 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. */
|
|
53
|
+
export function archiveEnded(folder, current) {
|
|
54
|
+
try {
|
|
55
|
+
return { archive: archiveGame(folder, current) };
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return { warning: `${error.message} The game is saved in game.json: it goes to the archive when a new game starts.` };
|
|
59
|
+
}
|
|
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
|
+
}
|
|
78
|
+
/** The name of a game over in the archive, when it is there. */
|
|
79
|
+
export function archivedName(folder, current) {
|
|
80
|
+
if (endOf(current.game) === undefined) {
|
|
81
|
+
return undefined;
|
|
82
|
+
}
|
|
83
|
+
const name = archiveName(current);
|
|
84
|
+
return existsSync(join(folder, name)) ? name : undefined;
|
|
85
|
+
}
|
|
86
|
+
/** The names of the games in the archive, the latest first: games/2026-11-02-153012.pgn. */
|
|
87
|
+
export function archivedGames(folder) {
|
|
88
|
+
return gameNames(folder, "games");
|
|
89
|
+
}
|
|
90
|
+
/** A game of the archive by its name, as listed; refused when it is not there, or cannot be read. */
|
|
91
|
+
export function archivedGame(folder, name) {
|
|
92
|
+
return gameNamed(folder, "games", name);
|
|
93
|
+
}
|
|
94
|
+
/** The names of the games kept to study in a profile's folder, the latest kept first: study/2026-11-04-153012.pgn. */
|
|
95
|
+
export function keptGames(root) {
|
|
96
|
+
return gameNames(root, "study");
|
|
97
|
+
}
|
|
98
|
+
/** A game kept to study by its name, as listed; refused when it is not there, or cannot be read. */
|
|
99
|
+
export function keptGame(root, name) {
|
|
100
|
+
return gameNamed(root, "study", name);
|
|
101
|
+
}
|
|
102
|
+
/** When an archived game started, from its name, in the player's time: 2026-11-02 15:30. */
|
|
103
|
+
export function archivedWhen(name) {
|
|
104
|
+
const stem = gameStemOf(name);
|
|
105
|
+
return stem === undefined ? name : `${stem.slice(0, 10)} ${stem.slice(11, 13)}:${stem.slice(13, 15)}`;
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
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`:
|
|
109
|
+
* study/2026-11-04-153012.pgn; the same game again keeps the name it has.
|
|
110
|
+
*/
|
|
111
|
+
export function keepToStudy(root, record, now) {
|
|
112
|
+
const text = writePgn(record);
|
|
113
|
+
// The same game, whatever the review and the book's badges written on it since.
|
|
114
|
+
const [written] = readPgn(text);
|
|
115
|
+
const kept = keptGames(root).find((name) => sameGame(readPgn(readFileSync(join(root, name), "utf8"))[0], written));
|
|
116
|
+
if (kept !== undefined) {
|
|
117
|
+
return kept;
|
|
118
|
+
}
|
|
119
|
+
let name = nameAt(now, "study");
|
|
120
|
+
for (let second = 1; existsSync(join(root, name)); second++) {
|
|
121
|
+
name = nameAt(new Date(now.getTime() + second * 1_000), "study");
|
|
122
|
+
}
|
|
123
|
+
try {
|
|
124
|
+
makeFolder(join(root, "study"));
|
|
125
|
+
writeAtomically(join(root, name), text);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
throw new Error(`Chesspal could not keep the game to study: ${error.message}`, { cause: error });
|
|
129
|
+
}
|
|
130
|
+
return name;
|
|
131
|
+
}
|
|
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). */
|
|
133
|
+
export function rewriteGame(folder, name, record) {
|
|
134
|
+
const path = listedPath(folder, name, "to write again");
|
|
135
|
+
try {
|
|
136
|
+
writeAtomically(path, writePgn(record));
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
throw new Error(`Chesspal could not write ${name}: ${error.message}`, { cause: error });
|
|
140
|
+
}
|
|
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
|
+
}
|
|
160
|
+
const shelves = { games: { what: "of the archive", list: "past_games" }, study: { what: "kept to study", list: "study_games" } };
|
|
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
|
+
}
|
|
198
|
+
function gameNames(folder, shelf) {
|
|
199
|
+
try {
|
|
200
|
+
return readdirSync(join(folder, shelf)).filter((name) => namePattern.test(name)).sort().reverse().map((name) => `${shelf}/${name}`);
|
|
201
|
+
}
|
|
202
|
+
catch {
|
|
203
|
+
return [];
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
function gameNamed(folder, shelf, name) {
|
|
207
|
+
// The name as listed, or without its folder or its .pgn.
|
|
208
|
+
const file = `${(name.startsWith(`${shelf}/`) ? name.slice(shelf.length + 1) : name).trim().replace(/\.pgn$/, "")}.pgn`;
|
|
209
|
+
const { what, list } = shelves[shelf];
|
|
210
|
+
if (!namePattern.test(file)) {
|
|
211
|
+
throw new Error(`${name} is no game ${what}: give a name as ${list} writes it, such as ${shelf}/2026-11-02-153012.pgn.`);
|
|
212
|
+
}
|
|
213
|
+
let text;
|
|
214
|
+
try {
|
|
215
|
+
text = readFileSync(join(folder, shelf, file), "utf8");
|
|
216
|
+
}
|
|
217
|
+
catch {
|
|
218
|
+
throw new Error(`There is no ${name} among the games ${what}: give a name as ${list} writes it.`);
|
|
219
|
+
}
|
|
220
|
+
const [record] = readPgn(text);
|
|
221
|
+
if (record === undefined) {
|
|
222
|
+
throw new Error(`${name} holds no game Chesspal can read.`);
|
|
223
|
+
}
|
|
224
|
+
return record;
|
|
225
|
+
}
|
|
226
|
+
/** Whether two records are the same game: the same tags and the same moves, whatever their comments say. */
|
|
227
|
+
function sameGame(one, other) {
|
|
228
|
+
const shape = (record) => JSON.stringify([record?.tags, record?.moves.map((move) => move.san)]);
|
|
229
|
+
return one !== undefined && shape(one) === shape(other);
|
|
230
|
+
}
|
|
231
|
+
function nameAt(date, shelf = "games") {
|
|
232
|
+
const time = date.toTimeString().slice(0, 8).replaceAll(":", "");
|
|
233
|
+
return `${shelf}/${pgnDate(date).replaceAll(".", "-")}-${time}.pgn`;
|
|
234
|
+
}
|
|
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) {
|
|
241
|
+
return outcome.over && record.tags.Termination === undefined ? { ...record, tags: { ...record.tags, Termination: outcome.ending } } : record;
|
|
242
|
+
}
|
|
@@ -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";
|