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,57 @@
|
|
|
1
|
+
import { type Badge, type DrawnArrow, type DrawnSquare, type GameRecord, type Lead } from "../core/index.ts";
|
|
2
|
+
import type { CurrentGame } from "./current-game.ts";
|
|
3
|
+
/** The coach's word on a move of the player's: the move by the plies up to it, its badge and note, and the moves of theirs its note names. */
|
|
4
|
+
export interface CoachJudgment {
|
|
5
|
+
readonly ply: number;
|
|
6
|
+
readonly badge?: Badge;
|
|
7
|
+
readonly note?: string;
|
|
8
|
+
/** The moves in SAN its note names that the player could play: the page shows it only once none is legal on their turn (Anatolij). */
|
|
9
|
+
readonly held?: string[];
|
|
10
|
+
/** Only as shown, never kept: a held note the player's last move let go, which the card shows now (Zoe). */
|
|
11
|
+
readonly released?: true;
|
|
12
|
+
}
|
|
13
|
+
/** The coach's hint for the player's turn, for the position it was given in, with what it draws on the board to show it. */
|
|
14
|
+
export interface CoachHint {
|
|
15
|
+
readonly position: number;
|
|
16
|
+
readonly text: string;
|
|
17
|
+
readonly arrows?: DrawnArrow[];
|
|
18
|
+
readonly squares?: DrawnSquare[];
|
|
19
|
+
}
|
|
20
|
+
/** Who the coach sees ahead in a position the player was to move in (D-90), by the plies up to it. */
|
|
21
|
+
export interface CoachLead {
|
|
22
|
+
readonly position: number;
|
|
23
|
+
readonly lead: Lead;
|
|
24
|
+
}
|
|
25
|
+
/** What the coach wrote in a game. */
|
|
26
|
+
export interface CoachWords {
|
|
27
|
+
readonly judgments: readonly CoachJudgment[];
|
|
28
|
+
readonly hint?: CoachHint;
|
|
29
|
+
readonly leads?: readonly CoachLead[];
|
|
30
|
+
}
|
|
31
|
+
/** What the coach wrote in the game going on: nothing when the file is another game's, or missing. */
|
|
32
|
+
export declare function coachWords(folder: string, current: CurrentGame): CoachWords;
|
|
33
|
+
/** Keeps the coach's word on a move of the player's, in place of one it wrote on the same move. */
|
|
34
|
+
export declare function keepJudgment(folder: string, current: CurrentGame, judgment: CoachJudgment): void;
|
|
35
|
+
/** Keeps the coach's hint for the player's turn, in place of the one before. */
|
|
36
|
+
export declare function keepHint(folder: string, current: CurrentGame, hint: CoachHint): void;
|
|
37
|
+
/** Keeps who the coach sees ahead in a position, in place of what it said there before. */
|
|
38
|
+
export declare function keepLead(folder: string, current: CurrentGame, { position, lead }: CoachLead): void;
|
|
39
|
+
/** Since when the coach has waited for the player's move in a position: the first time it asked, `now` then. */
|
|
40
|
+
export declare function coachWaitingSince(folder: string, current: CurrentGame, position: number, now: Date): Date;
|
|
41
|
+
/** Whether a coach has started on the game as it is now: until one has, the chat is told again to start it (Priya). */
|
|
42
|
+
export declare function coachStarted(folder: string, current: CurrentGame): boolean;
|
|
43
|
+
/** Notes that a coach starts on the game as it is now: true for the first, false for another the chat started on it too, which stops. */
|
|
44
|
+
export declare function startCoach(folder: string, current: CurrentGame): boolean;
|
|
45
|
+
/** Forgets the coach's words on the game over, when its review is deleted (D-92): they would go back into its PGN. */
|
|
46
|
+
export declare function forgetCoach(folder: string): void;
|
|
47
|
+
/**
|
|
48
|
+
* What the player may see of it in the game as it is: the judgments but those that name a move the player may play, held while the opponent is to move
|
|
49
|
+
* too (Anatolij), and the hint only in its own position. A held one that the last pair of moves let go is marked, for the card to show it
|
|
50
|
+
* now, however far back its move (Zoe).
|
|
51
|
+
*/
|
|
52
|
+
export declare function coachShown({ judgments, hint, leads }: CoachWords, { game, ai }: CurrentGame): CoachWords;
|
|
53
|
+
/**
|
|
54
|
+
* The record with the coach's judgments in the comments of the moves they judge, as the analyst writes its own: for the game over. A move the analyst has
|
|
55
|
+
* reviewed already keeps its review (Federico); the book's badge alone is no review, and stays under a note alone, as annotate_game keeps it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function withCoach(record: GameRecord, { judgments, leads }: CoachWords): GameRecord;
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// What the coach writes during a game with the helps on (D-88): its judgments of the player's moves, its hint for their turn and who it sees ahead (D-90),
|
|
2
|
+
// in coach.json beside game.json, where no command of the opponent's reads them; the judgments and the leads go into the game's PGN once it is over.
|
|
3
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { badges, Game, leadOf, leadText, readLead, reviewed, reviewOf, withLead } from "../core/index.js";
|
|
7
|
+
import { writeAtomically } from "./files.js";
|
|
8
|
+
/** What the coach wrote in the game going on: nothing when the file is another game's, or missing. */
|
|
9
|
+
export function coachWords(folder, current) {
|
|
10
|
+
const { judgments, hint, leads = [] } = keptFor(folder, current);
|
|
11
|
+
const read = leads.flatMap(({ position, lead }) => {
|
|
12
|
+
const known = readLead(lead);
|
|
13
|
+
return known === undefined ? [] : [{ position, lead: known }];
|
|
14
|
+
});
|
|
15
|
+
return { judgments, ...(hint === undefined ? {} : { hint }), ...(read.length === 0 ? {} : { leads: read }) };
|
|
16
|
+
}
|
|
17
|
+
/** Keeps the coach's word on a move of the player's, in place of one it wrote on the same move. */
|
|
18
|
+
export function keepJudgment(folder, current, judgment) {
|
|
19
|
+
const kept = keptFor(folder, current);
|
|
20
|
+
const judgments = [...kept.judgments.filter((one) => one.ply !== judgment.ply), judgment].sort((one, other) => one.ply - other.ply);
|
|
21
|
+
write(folder, { ...kept, judgments });
|
|
22
|
+
}
|
|
23
|
+
/** Keeps the coach's hint for the player's turn, in place of the one before. */
|
|
24
|
+
export function keepHint(folder, current, hint) {
|
|
25
|
+
write(folder, { ...keptFor(folder, current), hint });
|
|
26
|
+
}
|
|
27
|
+
/** Keeps who the coach sees ahead in a position, in place of what it said there before. */
|
|
28
|
+
export function keepLead(folder, current, { position, lead }) {
|
|
29
|
+
const kept = keptFor(folder, current);
|
|
30
|
+
const leads = [...(kept.leads ?? []).filter((one) => one.position !== position), { position, lead: leadText(lead) }].sort((one, other) => one.position - other.position);
|
|
31
|
+
write(folder, { ...kept, leads });
|
|
32
|
+
}
|
|
33
|
+
/** Since when the coach has waited for the player's move in a position: the first time it asked, `now` then. */
|
|
34
|
+
export function coachWaitingSince(folder, current, position, now) {
|
|
35
|
+
const kept = keptFor(folder, current);
|
|
36
|
+
if (kept.waiting?.position === position) {
|
|
37
|
+
return new Date(kept.waiting.since);
|
|
38
|
+
}
|
|
39
|
+
write(folder, { ...kept, waiting: { position, since: now.toISOString() } });
|
|
40
|
+
return now;
|
|
41
|
+
}
|
|
42
|
+
/** Whether a coach has started on the game as it is now: until one has, the chat is told again to start it (Priya). */
|
|
43
|
+
export function coachStarted(folder, current) {
|
|
44
|
+
return keptFor(folder, current).begun === current.game.record.moves.length;
|
|
45
|
+
}
|
|
46
|
+
/** Notes that a coach starts on the game as it is now: true for the first, false for another the chat started on it too, which stops. */
|
|
47
|
+
export function startCoach(folder, current) {
|
|
48
|
+
const kept = keptFor(folder, current);
|
|
49
|
+
const plies = current.game.record.moves.length;
|
|
50
|
+
if (kept.begun === plies) {
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
write(folder, { ...kept, begun: plies });
|
|
54
|
+
return true;
|
|
55
|
+
}
|
|
56
|
+
/** Forgets the coach's words on the game over, when its review is deleted (D-92): they would go back into its PGN. */
|
|
57
|
+
export function forgetCoach(folder) {
|
|
58
|
+
rmSync(join(folder, fileName), { force: true });
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* What the player may see of it in the game as it is: the judgments but those that name a move the player may play, held while the opponent is to move
|
|
62
|
+
* too (Anatolij), and the hint only in its own position. A held one that the last pair of moves let go is marked, for the card to show it
|
|
63
|
+
* now, however far back its move (Zoe).
|
|
64
|
+
*/
|
|
65
|
+
export function coachShown({ judgments, hint, leads = [] }, { game, ai }) {
|
|
66
|
+
const plies = game.record.moves.length;
|
|
67
|
+
const legal = game.position.turn === ai ? undefined : legalMoves(game);
|
|
68
|
+
const shown = judgments.filter(({ held }) => held === undefined || (legal !== undefined && !names(held, legal)));
|
|
69
|
+
const before = legal === undefined || plies < 2 || shown.every(({ held }) => held === undefined)
|
|
70
|
+
? undefined
|
|
71
|
+
: legalMoves(new Game({ tags: game.record.tags, moves: game.record.moves.slice(0, plies - 2) }));
|
|
72
|
+
const marked = before === undefined ? shown : shown.map((judgment) => judgment.held !== undefined && names(judgment.held, before) ? { ...judgment, released: true } : judgment);
|
|
73
|
+
return { judgments: marked, ...(hint === undefined || hint.position !== plies ? {} : { hint }), ...(leads.length === 0 ? {} : { leads }) };
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The record with the coach's judgments in the comments of the moves they judge, as the analyst writes its own: for the game over. A move the analyst has
|
|
77
|
+
* reviewed already keeps its review (Federico); the book's badge alone is no review, and stays under a note alone, as annotate_game keeps it.
|
|
78
|
+
*/
|
|
79
|
+
export function withCoach(record, { judgments, leads = [] }) {
|
|
80
|
+
if (judgments.length === 0 && leads.length === 0) {
|
|
81
|
+
return record;
|
|
82
|
+
}
|
|
83
|
+
const moves = record.moves.map((move, index) => {
|
|
84
|
+
const judgment = judgments.find((one) => one.ply === index + 1);
|
|
85
|
+
const review = reviewOf(move.comment);
|
|
86
|
+
if (judgment === undefined || (review !== undefined && (review.badge !== "book" || review.note !== undefined))) {
|
|
87
|
+
return move;
|
|
88
|
+
}
|
|
89
|
+
const badge = judgment.badge ?? review?.badge;
|
|
90
|
+
return reviewed(move, { ...(badge === undefined ? {} : { badge }), ...(judgment.note === undefined ? {} : { note: judgment.note }) });
|
|
91
|
+
});
|
|
92
|
+
return { ...record, moves: withLeads(moves, leads) };
|
|
93
|
+
}
|
|
94
|
+
/** The moves with who the coach saw ahead after each (D-90), where nobody has said it there since: on the move that led to the position it was given in. */
|
|
95
|
+
function withLeads(moves, leads) {
|
|
96
|
+
return moves.map((move, index) => {
|
|
97
|
+
const lead = leads.find((one) => one.position === index + 1)?.lead;
|
|
98
|
+
return lead === undefined || leadOf(move.comment) !== undefined ? move : withLead(move, lead);
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
/** The legal moves in a position, without their checks. */
|
|
102
|
+
function legalMoves(game) {
|
|
103
|
+
return new Set(game.legalMoves().map((move) => withoutCheck(move.san)));
|
|
104
|
+
}
|
|
105
|
+
/** Whether a note's moves name one of the legal moves. */
|
|
106
|
+
function names(held, legal) {
|
|
107
|
+
return held.some((san) => legal.has(withoutCheck(san)));
|
|
108
|
+
}
|
|
109
|
+
/** A move in SAN without its check or mate, which the position it is played in decides: Qxf7# then is Qxf7+ now. */
|
|
110
|
+
function withoutCheck(san) {
|
|
111
|
+
return san.replace(/[+#]$/, "");
|
|
112
|
+
}
|
|
113
|
+
const fileName = "coach.json";
|
|
114
|
+
const colours = ["green", "red", "yellow", "blue"];
|
|
115
|
+
const coachFile = z.object({
|
|
116
|
+
started: z.string(),
|
|
117
|
+
judgments: z.array(z.object({ ply: z.int().min(1), badge: z.enum(badges).optional(), note: z.string().optional(), held: z.array(z.string()).optional() })),
|
|
118
|
+
leads: z.array(z.object({ position: z.int().min(0), lead: z.string() })).optional(),
|
|
119
|
+
hint: z.object({
|
|
120
|
+
position: z.int().min(0),
|
|
121
|
+
text: z.string(),
|
|
122
|
+
arrows: z.array(z.object({ colour: z.enum(colours), from: z.string(), to: z.string() })).optional(),
|
|
123
|
+
squares: z.array(z.object({ colour: z.enum(colours), square: z.string() })).optional(),
|
|
124
|
+
}).optional(),
|
|
125
|
+
waiting: z.object({ position: z.int().min(0), since: z.iso.datetime() }).optional(),
|
|
126
|
+
begun: z.int().min(0).optional(),
|
|
127
|
+
});
|
|
128
|
+
/** What is kept for a game, by when it started: nothing yet when the file is another game's, or missing. */
|
|
129
|
+
function keptFor(folder, current) {
|
|
130
|
+
const kept = read(folder);
|
|
131
|
+
return kept?.started === current.started ? kept : { started: current.started, judgments: [] };
|
|
132
|
+
}
|
|
133
|
+
function read(folder) {
|
|
134
|
+
try {
|
|
135
|
+
return coachFile.parse(JSON.parse(readFileSync(join(folder, fileName), "utf8")));
|
|
136
|
+
}
|
|
137
|
+
catch {
|
|
138
|
+
// None yet, or one Chesspal cannot read: the coach's words are only a help, and the next one written replaces it.
|
|
139
|
+
return undefined;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
function write(folder, kept) {
|
|
143
|
+
writeAtomically(join(folder, fileName), `${JSON.stringify(kept, null, 2)}\n`);
|
|
144
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
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
|
+
/** Removes game.json, when the game over it holds is deleted from the archive (D-92): a new game would archive it again. */
|
|
45
|
+
export declare function removeGame(folder: string): void;
|
|
46
|
+
/** A line of the talk as game.json and talk.json keep it. */
|
|
47
|
+
export declare const savedLine: z.ZodObject<{
|
|
48
|
+
who: z.ZodEnum<{
|
|
49
|
+
player: "player";
|
|
50
|
+
ai: "ai";
|
|
51
|
+
}>;
|
|
52
|
+
text: z.ZodString;
|
|
53
|
+
ply: z.ZodInt;
|
|
54
|
+
at: z.ZodISODateTime;
|
|
55
|
+
agent: z.ZodOptional<z.ZodLiteral<true>>;
|
|
56
|
+
}, z.core.$strip>;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
// The player's current game on disk (D-27, D-41): game.json, its PGN and the talk on the board page inside, written whole at every change, and kept in
|
|
2
|
+
// memory while nobody else changes it.
|
|
3
|
+
import { readFileSync, rmSync, statSync } from "node:fs";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { Game, readPgn, writePgn } from "../core/index.js";
|
|
7
|
+
import { writeAtomically } from "./files.js";
|
|
8
|
+
/** 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. */
|
|
9
|
+
export function loadGame(folder) {
|
|
10
|
+
const path = join(folder, fileName);
|
|
11
|
+
const stamp = stampOf(path);
|
|
12
|
+
if (stamp === undefined) {
|
|
13
|
+
kept.delete(path);
|
|
14
|
+
return undefined;
|
|
15
|
+
}
|
|
16
|
+
const known = kept.get(path);
|
|
17
|
+
if (known?.stamp === stamp) {
|
|
18
|
+
return known.current;
|
|
19
|
+
}
|
|
20
|
+
const current = parsed(readFileSync(path, "utf8"), path);
|
|
21
|
+
kept.set(path, { stamp, current });
|
|
22
|
+
return current;
|
|
23
|
+
}
|
|
24
|
+
/** Writes the current game to game.json, whole or not at all; from then on it is the one kept in memory. */
|
|
25
|
+
export function saveGame(folder, current) {
|
|
26
|
+
const path = join(folder, fileName);
|
|
27
|
+
const file = {
|
|
28
|
+
format: 1,
|
|
29
|
+
ai: current.ai,
|
|
30
|
+
started: current.started,
|
|
31
|
+
pgn: writePgn(current.game.record),
|
|
32
|
+
...(current.talk.length === 0 ? {} : { talk: current.talk }),
|
|
33
|
+
...(current.clock === undefined ? {} : { clock: savedClockOf(current.clock) }),
|
|
34
|
+
...(drawOfferOf(current) === undefined ? {} : { drawOffer: current.drawOffer }),
|
|
35
|
+
};
|
|
36
|
+
try {
|
|
37
|
+
writeAtomically(path, `${JSON.stringify(file, null, 2)}\n`);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
// The live game may hold a move the file does not: the next call reads the file again.
|
|
41
|
+
kept.delete(path);
|
|
42
|
+
throw new Error(`Chesspal could not save the game: ${error.message} Nothing changed.`, { cause: error });
|
|
43
|
+
}
|
|
44
|
+
kept.set(path, { stamp: stampOf(path) ?? "", current });
|
|
45
|
+
}
|
|
46
|
+
/** How the game ended, or undefined while it goes on: by the board, or by what the record says, such as a resignation. */
|
|
47
|
+
export function endOf(game) {
|
|
48
|
+
const { tags } = game.record;
|
|
49
|
+
const { outcome } = game;
|
|
50
|
+
if (tags.Result === undefined || tags.Result === "*") {
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
const termination = tags.Termination ?? (outcome.over ? outcome.ending : undefined);
|
|
54
|
+
return termination === undefined ? { result: tags.Result } : { result: tags.Result, termination };
|
|
55
|
+
}
|
|
56
|
+
/** The draw offer that stands: none once the game is over (D-65). */
|
|
57
|
+
export function drawOfferOf(current) {
|
|
58
|
+
return endOf(current.game) === undefined ? current.drawOffer : undefined;
|
|
59
|
+
}
|
|
60
|
+
/** Whether each side has made a move in the game, before which no draw is agreed (FIDE 5.2.3, D-65). */
|
|
61
|
+
export function bothSidesMoved(game) {
|
|
62
|
+
return game.record.moves.length >= 2;
|
|
63
|
+
}
|
|
64
|
+
/** A stamp of the folder's game.json that changes whenever it is written, whoever writes it; undefined when there is none. */
|
|
65
|
+
export function gameStamp(folder) {
|
|
66
|
+
return stampOf(join(folder, fileName));
|
|
67
|
+
}
|
|
68
|
+
/** Removes game.json, when the game over it holds is deleted from the archive (D-92): a new game would archive it again. */
|
|
69
|
+
export function removeGame(folder) {
|
|
70
|
+
const path = join(folder, fileName);
|
|
71
|
+
kept.delete(path);
|
|
72
|
+
rmSync(path, { force: true });
|
|
73
|
+
}
|
|
74
|
+
const fileName = "game.json";
|
|
75
|
+
/** A line of the talk as game.json and talk.json keep it. */
|
|
76
|
+
export const savedLine = z.object({ who: z.enum(["player", "ai"]), text: z.string(), ply: z.int().min(0), at: z.iso.datetime(), agent: z.literal(true).optional() });
|
|
77
|
+
const savedClock = z.object({
|
|
78
|
+
base: z.int().positive(),
|
|
79
|
+
increment: z.int().min(0),
|
|
80
|
+
white: z.number().min(0),
|
|
81
|
+
black: z.number().min(0),
|
|
82
|
+
since: z.iso.datetime(),
|
|
83
|
+
waiting: z.literal(true).optional(),
|
|
84
|
+
});
|
|
85
|
+
const gameFile = z.object({
|
|
86
|
+
format: z.literal(1),
|
|
87
|
+
ai: z.enum(["white", "black"]),
|
|
88
|
+
started: z.iso.datetime(),
|
|
89
|
+
pgn: z.string(),
|
|
90
|
+
talk: z.array(savedLine).optional(),
|
|
91
|
+
clock: savedClock.optional(),
|
|
92
|
+
drawOffer: z.enum(["player", "ai"]).optional(),
|
|
93
|
+
});
|
|
94
|
+
// The games read or written in this process, by their file, with the file's stamp then: a call reads a file again only when another process changed it.
|
|
95
|
+
const kept = new Map();
|
|
96
|
+
function stampOf(path) {
|
|
97
|
+
// The file's id changes at every rename: a write within the same tick of the clock, and of the same size, is seen too.
|
|
98
|
+
const stats = statSync(path, { bigint: true, throwIfNoEntry: false });
|
|
99
|
+
return stats === undefined ? undefined : `${stats.ino}:${stats.mtimeNs}:${stats.size}`;
|
|
100
|
+
}
|
|
101
|
+
function parsed(text, path) {
|
|
102
|
+
try {
|
|
103
|
+
const file = gameFile.parse(JSON.parse(text));
|
|
104
|
+
const records = readPgn(file.pgn);
|
|
105
|
+
const [record] = records;
|
|
106
|
+
if (record === undefined || records.length > 1) {
|
|
107
|
+
throw new Error(`its PGN holds ${records.length} games`);
|
|
108
|
+
}
|
|
109
|
+
return {
|
|
110
|
+
game: new Game(record),
|
|
111
|
+
ai: file.ai,
|
|
112
|
+
started: file.started,
|
|
113
|
+
talk: file.talk ?? [],
|
|
114
|
+
...(file.clock === undefined ? {} : { clock: clockOf(file.clock) }),
|
|
115
|
+
...(file.drawOffer === undefined ? {} : { drawOffer: file.drawOffer }),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (error) {
|
|
119
|
+
throw new Error(`${path} is not a game Chesspal can read (${problemOf(error)}): ask the player to fix it or move it away, since Chesspal never overwrites it.`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
/** The clock as game.json keeps it, flat, in milliseconds. */
|
|
123
|
+
function savedClockOf({ control, left, since, waiting }) {
|
|
124
|
+
return { base: control.base, increment: control.increment, white: left.white, black: left.black, since, ...(waiting === true ? { waiting } : {}) };
|
|
125
|
+
}
|
|
126
|
+
function clockOf({ base, increment, white, black, since, waiting }) {
|
|
127
|
+
return { control: { base, increment }, left: { white, black }, since, ...(waiting === true ? { waiting } : {}) };
|
|
128
|
+
}
|
|
129
|
+
function problemOf(error) {
|
|
130
|
+
if (error instanceof z.ZodError) {
|
|
131
|
+
return error.issues.map((issue) => `${issue.path.join(".") || "the file"}: ${issue.message}`).join("; ");
|
|
132
|
+
}
|
|
133
|
+
return error.message;
|
|
134
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** Writes a file whole or not at all: a temporary file beside it, then renamed over it (D-27). A failure says what to do about it. */
|
|
2
|
+
export declare function writeAtomically(path: string, text: string | Uint8Array): void;
|
|
3
|
+
/** Makes a folder, and those above it, when missing. A failure says what to do about it. */
|
|
4
|
+
export declare function makeFolder(path: string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Renames a file or a folder. Windows refuses while another program holds a file in it open (an editor, an antivirus, a sync client): Chesspal waits a
|
|
7
|
+
* little and tries again, half a second at most.
|
|
8
|
+
*/
|
|
9
|
+
export declare function renameOver(from: string, to: string): void;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Writing the player's files safely: whole or not at all, with failures that say what to do (D-27).
|
|
2
|
+
import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
/** Writes a file whole or not at all: a temporary file beside it, then renamed over it (D-27). A failure says what to do about it. */
|
|
4
|
+
export function writeAtomically(path, text) {
|
|
5
|
+
const temporary = `${path}.${process.pid}.tmp`;
|
|
6
|
+
try {
|
|
7
|
+
writeFileSync(temporary, text);
|
|
8
|
+
renameOver(temporary, path);
|
|
9
|
+
}
|
|
10
|
+
catch (error) {
|
|
11
|
+
throw new Error(problemOf(error, path), { cause: error });
|
|
12
|
+
}
|
|
13
|
+
finally {
|
|
14
|
+
rmSync(temporary, { force: true });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
/** Makes a folder, and those above it, when missing. A failure says what to do about it. */
|
|
18
|
+
export function makeFolder(path) {
|
|
19
|
+
try {
|
|
20
|
+
mkdirSync(path, { recursive: true });
|
|
21
|
+
}
|
|
22
|
+
catch (error) {
|
|
23
|
+
throw new Error(`${path} could not be made (${error.message}): check that it is a folder, and can be written.`, { cause: error });
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const attempts = 10;
|
|
27
|
+
/**
|
|
28
|
+
* Renames a file or a folder. Windows refuses while another program holds a file in it open (an editor, an antivirus, a sync client): Chesspal waits a
|
|
29
|
+
* little and tries again, half a second at most.
|
|
30
|
+
*/
|
|
31
|
+
export function renameOver(from, to) {
|
|
32
|
+
for (let attempt = 1;; attempt++) {
|
|
33
|
+
try {
|
|
34
|
+
renameSync(from, to);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
if (attempt === attempts || !isHeld(error)) {
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10 * attempt);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function isHeld(error) {
|
|
46
|
+
return error.syscall === "rename" && (error.code === "EPERM" || error.code === "EACCES" || error.code === "EBUSY");
|
|
47
|
+
}
|
|
48
|
+
function problemOf(error, path) {
|
|
49
|
+
if (isHeld(error)) {
|
|
50
|
+
return `${path} is held open by another program, or is read-only (${error.code}, ${attempts} tries): close that program, or make the file writable.`;
|
|
51
|
+
}
|
|
52
|
+
return `${path} could not be written (${error.message}): check that its folder can be written.`;
|
|
53
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { aiModelOf, noteAiModel } from "./ai-model.ts";
|
|
2
|
+
export { type Analysis, endAnalysis, gameInAnalysis, startAnalysis } from "./analysis.ts";
|
|
3
|
+
export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.ts";
|
|
4
|
+
export { type ChatStart, cleanSince, lastChatStart, saveChatStart } from "./chat-start.ts";
|
|
5
|
+
export { type CoachHint, type CoachJudgment, type CoachLead, coachShown, coachStarted, coachWaitingSince, type CoachWords, coachWords, forgetCoach, keepHint, keepJudgment, keepLead, startCoach, withCoach } from "./coach.ts";
|
|
6
|
+
export { bothSidesMoved, type CurrentGame, drawOfferOf, endOf, type GameEnd, gameStamp, loadGame, removeGame, saveGame, type Speaker, type TalkLine } from "./current-game.ts";
|
|
7
|
+
export { makeFolder, renameOver, writeAtomically } from "./files.ts";
|
|
8
|
+
export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.ts";
|
|
9
|
+
export { endTableTalk, loadTableTalk, saveTableTalk, type TableTalk } from "./table-talk.ts";
|
|
10
|
+
export { type Heard, heardLines, type Sayer, type Talk, talkLimits, talkLine, unheardWords, wakesTheChat, type Words, WordsRefused } from "./talk.ts";
|
|
11
|
+
export { keepThought, type Thought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.ts";
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// The player's games on disk: the current one in game.json, the finished ones in games/ (D-27, D-41), the games kept to study in study/ (D-68). Below the commands, above the core.
|
|
2
|
+
export { aiModelOf, noteAiModel } from "./ai-model.js";
|
|
3
|
+
export { endAnalysis, gameInAnalysis, startAnalysis } from "./analysis.js";
|
|
4
|
+
export { archiveCoached, archivedGame, archivedGames, archivedName, archivedWhen, archiveEnded, archiveGame, archiveName, deleteGame, gameStemOf, isGameName, isOverOnTable, keepToStudy, keptGame, keptGames, listedName, movesPgn, playerPgn, rewriteGame, startOfNewGame, } from "./archive.js";
|
|
5
|
+
export { cleanSince, lastChatStart, saveChatStart } from "./chat-start.js";
|
|
6
|
+
export { coachShown, coachStarted, coachWaitingSince, coachWords, forgetCoach, keepHint, keepJudgment, keepLead, startCoach, withCoach } from "./coach.js";
|
|
7
|
+
export { bothSidesMoved, drawOfferOf, endOf, gameStamp, loadGame, removeGame, saveGame } from "./current-game.js";
|
|
8
|
+
export { makeFolder, renameOver, writeAtomically } from "./files.js";
|
|
9
|
+
export { commentWithPlan, currentPlan, planOf, withoutPlans } from "./plans.js";
|
|
10
|
+
export { endTableTalk, loadTableTalk, saveTableTalk } from "./table-talk.js";
|
|
11
|
+
export { heardLines, talkLimits, talkLine, unheardWords, wakesTheChat, WordsRefused } from "./talk.js";
|
|
12
|
+
export { keepThought, thoughtForLastMove, thoughtKey, waitingSince } from "./thoughts.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { GameRecord } from "../core/index.ts";
|
|
2
|
+
/** The comment of the AI's move, when it has one, with its plan. */
|
|
3
|
+
export declare function commentWithPlan(comment: string | undefined, plan: string): string;
|
|
4
|
+
/** The plan a move's comment carries, if it carries one. */
|
|
5
|
+
export declare function planOf(comment: string | undefined): string | undefined;
|
|
6
|
+
/** The plan the AI wrote last, if it wrote one. */
|
|
7
|
+
export declare function currentPlan(record: GameRecord): string | undefined;
|
|
8
|
+
/** The record without the AI's plans: what the player may see while the game goes on. */
|
|
9
|
+
export declare function withoutPlans(record: GameRecord): GameRecord;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/** The comment of the AI's move, when it has one, with its plan. */
|
|
2
|
+
export function commentWithPlan(comment, plan) {
|
|
3
|
+
return comment === undefined ? `[%plan ${plan}]` : `${comment} [%plan ${plan}]`;
|
|
4
|
+
}
|
|
5
|
+
/** The plan a move's comment carries, if it carries one. */
|
|
6
|
+
export function planOf(comment) {
|
|
7
|
+
return planPattern.exec(comment ?? "")?.[1];
|
|
8
|
+
}
|
|
9
|
+
/** The plan the AI wrote last, if it wrote one. */
|
|
10
|
+
export function currentPlan(record) {
|
|
11
|
+
for (let index = record.moves.length - 1; index >= 0; index--) {
|
|
12
|
+
const plan = planOf(record.moves[index]?.comment);
|
|
13
|
+
if (plan !== undefined) {
|
|
14
|
+
return plan;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
/** The record without the AI's plans: what the player may see while the game goes on. */
|
|
20
|
+
export function withoutPlans(record) {
|
|
21
|
+
return { ...record, moves: record.moves.map((move) => ({ ...move, comment: move.comment?.replace(planPattern, "") })) };
|
|
22
|
+
}
|
|
23
|
+
const planPattern = /\s*\[%plan ([^\]]*)\]/;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { type TalkLine } from "./current-game.ts";
|
|
2
|
+
/** What the player and the AI said with no game on the page, and since when: it keys the player's lines apart from any game's. */
|
|
3
|
+
export interface TableTalk {
|
|
4
|
+
readonly started: string;
|
|
5
|
+
readonly talk: readonly TalkLine[];
|
|
6
|
+
}
|
|
7
|
+
/** The talk with no game: empty when there is none, or talk.json cannot be read, since it is only words. */
|
|
8
|
+
export declare function loadTableTalk(folder: string): TableTalk;
|
|
9
|
+
/** Writes the talk with no game, whole or not at all. */
|
|
10
|
+
export declare function saveTableTalk(folder: string, table: TableTalk): void;
|
|
11
|
+
/** Forgets the talk with no game, when a game starts. */
|
|
12
|
+
export declare function endTableTalk(folder: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The talk on the board page before the first game (D-77): talk.json, in the folder of the profile's games, until a game starts and its talk goes in game.json.
|
|
2
|
+
import { readFileSync, rmSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { savedLine } from "./current-game.js";
|
|
6
|
+
import { writeAtomically } from "./files.js";
|
|
7
|
+
/** The talk with no game: empty when there is none, or talk.json cannot be read, since it is only words. */
|
|
8
|
+
export function loadTableTalk(folder) {
|
|
9
|
+
try {
|
|
10
|
+
return tableFile.parse(JSON.parse(readFileSync(join(folder, fileName), "utf8")));
|
|
11
|
+
}
|
|
12
|
+
catch {
|
|
13
|
+
return { started: "", talk: [] };
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
/** Writes the talk with no game, whole or not at all. */
|
|
17
|
+
export function saveTableTalk(folder, table) {
|
|
18
|
+
writeAtomically(join(folder, fileName), `${JSON.stringify(table, null, 2)}\n`);
|
|
19
|
+
}
|
|
20
|
+
/** Forgets the talk with no game, when a game starts. */
|
|
21
|
+
export function endTableTalk(folder) {
|
|
22
|
+
rmSync(join(folder, fileName), { force: true });
|
|
23
|
+
}
|
|
24
|
+
const fileName = "talk.json";
|
|
25
|
+
const tableFile = z.object({ started: z.iso.datetime(), talk: z.array(savedLine) });
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { type CurrentGame, type Speaker, type TalkLine } from "./current-game.ts";
|
|
2
|
+
import type { TableTalk } from "./table-talk.ts";
|
|
3
|
+
/** The most the player may write in a move, in all their lines, and the most a line of the AI's may hold (D-54); with the helps on, as an analyst's note (D-78). */
|
|
4
|
+
export declare const talkLimits: {
|
|
5
|
+
readonly player: 200;
|
|
6
|
+
readonly ai: 300;
|
|
7
|
+
readonly aiHelps: 700;
|
|
8
|
+
};
|
|
9
|
+
/** Who says a line: the player, the AI in the chat, or the AI's agent thinking on the player's time (D-72), whose lines are the AI's on the page. */
|
|
10
|
+
export type Sayer = Speaker | "agent";
|
|
11
|
+
/** A talk: a game's, going on or over, or the one with no game, where both talk freely as between games (D-74, D-77). */
|
|
12
|
+
export type Talk = CurrentGame | TableTalk;
|
|
13
|
+
/**
|
|
14
|
+
* A new line of the talk, said when the game has `ply` moves: refused when it is empty or too long. The player writes 200 characters a move in all, the
|
|
15
|
+
* words sent with the move included; the AI speaks once a move of theirs, on their time, and its agent's word leaves it an answer to what they write after (D-54, D-72).
|
|
16
|
+
*/
|
|
17
|
+
export declare function talkLine(current: Talk, sayer: Sayer, text: string, at: Date, ply?: number, aiMost?: number): TalkLine;
|
|
18
|
+
/** Words refused, and why, for the page to say it in the player's language: none, the move's characters spent, more than a line or what is left holds. */
|
|
19
|
+
export declare class WordsRefused extends Error {
|
|
20
|
+
readonly reason: "empty" | "spent" | "long" | "longer-than-left";
|
|
21
|
+
readonly most: number;
|
|
22
|
+
constructor(message: string, reason: WordsRefused["reason"], most?: number);
|
|
23
|
+
}
|
|
24
|
+
/** Whether the player's last line wakes the chat: the first of a move, and between games the first since the AI last spoke (D-54, D-74). */
|
|
25
|
+
export declare function wakesTheChat(current: Talk): boolean;
|
|
26
|
+
/** Words of the player's, with the key that tells them apart across games. */
|
|
27
|
+
export interface Words {
|
|
28
|
+
readonly key: string;
|
|
29
|
+
readonly text: string;
|
|
30
|
+
}
|
|
31
|
+
/** What the player said that the AI has not answered, but for the words whose keys are in `given`. */
|
|
32
|
+
export declare function unheardWords(current: Talk, given: ReadonlySet<string>): Words[];
|
|
33
|
+
/** How far a line of the player's got: written, read by the AI in this chat, or answered by its words or its move. */
|
|
34
|
+
export type Heard = "sent" | "read" | "answered";
|
|
35
|
+
/** How far each line of the talk got: the player's by `Heard`, the AI's undefined (D-53). */
|
|
36
|
+
export declare function heardLines(current: Talk, given: ReadonlySet<string>): (Heard | undefined)[];
|