chesspal 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -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 +8 -0
- package/dist/cli/command-line.js +178 -0
- package/dist/commands/ai-move.d.ts +37 -0
- package/dist/commands/ai-move.js +92 -0
- package/dist/commands/ai-say.d.ts +20 -0
- package/dist/commands/ai-say.js +40 -0
- package/dist/commands/analyse-game.d.ts +61 -0
- package/dist/commands/analyse-game.js +238 -0
- package/dist/commands/annotate-game.d.ts +35 -0
- package/dist/commands/annotate-game.js +78 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/context.d.ts +56 -0
- package/dist/commands/context.js +36 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +15 -0
- package/dist/commands/drawings.js +23 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +64 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +26 -0
- package/dist/commands/game-summary.d.ts +95 -0
- package/dist/commands/game-summary.js +239 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -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 +61 -0
- package/dist/commands/new-game.d.ts +69 -0
- package/dist/commands/new-game.js +215 -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 +44 -0
- package/dist/commands/play-move.js +102 -0
- package/dist/commands/player-move.d.ts +17 -0
- package/dist/commands/player-move.js +44 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +42 -0
- package/dist/commands/ponder.js +167 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +128 -0
- package/dist/commands/profiles.d.ts +60 -0
- package/dist/commands/profiles.js +129 -0
- package/dist/commands/registry.d.ts +54 -0
- package/dist/commands/registry.js +61 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/status.d.ts +29 -0
- package/dist/commands/status.js +103 -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 +122 -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 +93 -0
- package/dist/core/game.js +348 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +11 -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 +23 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +37 -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 +74 -0
- package/dist/core/review.js +126 -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 +42 -0
- package/dist/games/archive.js +168 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/current-game.d.ts +54 -0
- package/dist/games/current-game.js +128 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +52 -0
- package/dist/games/index.d.ts +10 -0
- package/dist/games/index.js +11 -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 +40 -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/host-files.d.ts +10 -0
- package/dist/host-files/host-files.js +167 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/host-files/ponder-agents.d.ts +12 -0
- package/dist/host-files/ponder-agents.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/page/app.js +576 -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.js +16 -0
- package/dist/page/badges.js +50 -0
- package/dist/page/board.js +566 -0
- package/dist/page/browse.js +17 -0
- package/dist/page/clock-time.js +12 -0
- package/dist/page/dialog.js +26 -0
- package/dist/page/ending.js +73 -0
- package/dist/page/explain.js +58 -0
- package/dist/page/fold-tab.js +9 -0
- package/dist/page/folds.js +43 -0
- package/dist/page/game-column.js +44 -0
- package/dist/page/header.js +27 -0
- package/dist/page/helps-view.js +51 -0
- package/dist/page/helps.js +90 -0
- package/dist/page/main.js +8 -0
- package/dist/page/model-name.js +20 -0
- package/dist/page/move-list.js +68 -0
- package/dist/page/new-game-dialog.js +42 -0
- package/dist/page/opening-book.js +14 -0
- package/dist/page/pieces/CREDITS.md +36 -0
- package/dist/page/pieces/cburnett-small.svg +20 -0
- package/dist/page/pieces/cburnett.svg +20 -0
- package/dist/page/pieces/nitida-small.svg +19 -0
- package/dist/page/pieces/nitida.svg +20 -0
- package/dist/page/pieces/torretta-small.svg +19 -0
- package/dist/page/pieces/torretta.svg +19 -0
- package/dist/page/pieces.js +28 -0
- package/dist/page/player-card.js +60 -0
- package/dist/page/plies.js +62 -0
- package/dist/page/profile-dialogs.js +123 -0
- package/dist/page/profile.js +95 -0
- package/dist/page/settings-panel.js +32 -0
- package/dist/page/settings.js +76 -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.js +105 -0
- package/dist/page/studied.js +14 -0
- package/dist/page/study-dialog.js +58 -0
- package/dist/page/style.css +4398 -0
- package/dist/page/talk.js +172 -0
- package/dist/page/text.js +593 -0
- package/dist/page/torretta.js +38 -0
- package/dist/page/voice.js +38 -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 +58 -0
- package/dist/profiles/profiles.js +237 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/analyses.d.ts +17 -0
- package/dist/server/analyses.js +46 -0
- package/dist/server/bell.d.ts +60 -0
- package/dist/server/bell.js +209 -0
- package/dist/server/caller.d.ts +10 -0
- package/dist/server/caller.js +99 -0
- package/dist/server/http.d.ts +16 -0
- package/dist/server/http.js +57 -0
- package/dist/server/mcp.d.ts +14 -0
- package/dist/server/mcp.js +179 -0
- package/dist/server/page.d.ts +46 -0
- package/dist/server/page.js +657 -0
- package/dist/server/profile-routes.d.ts +19 -0
- package/dist/server/profile-routes.js +186 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analyst.md +8 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type AiLevel, type MoveReview, type PieceLetter, type Side } from "../core/index.ts";
|
|
3
|
+
/** A move as the analysis gives it: what happened, never how good it was (D-6). */
|
|
4
|
+
interface MoveFacts {
|
|
5
|
+
/** The move, numbered: 12...Nxe5. */
|
|
6
|
+
move: string;
|
|
7
|
+
/** The piece it took. */
|
|
8
|
+
took?: PieceLetter;
|
|
9
|
+
/** The time it took and the time left after it, in milliseconds, where the record has the clock. */
|
|
10
|
+
spent?: number;
|
|
11
|
+
left?: number;
|
|
12
|
+
/** Who is ahead in material after it, when it changed: White +1. */
|
|
13
|
+
material?: string;
|
|
14
|
+
/** After it, the pieces at risk of either side when they changed from the line before ("none" once no piece is), and the mates the side to move has; none once the board ends the game. */
|
|
15
|
+
atRisk?: string;
|
|
16
|
+
mates?: readonly string[];
|
|
17
|
+
/** The first move the opening book does not know. */
|
|
18
|
+
outOfBook?: true;
|
|
19
|
+
drawOffer?: true;
|
|
20
|
+
/** In a game played here, what the AI said with its move, and the plan it wrote. */
|
|
21
|
+
said?: string;
|
|
22
|
+
plan?: string;
|
|
23
|
+
/** The badge and note the AI gave it with annotate_game (D-69). */
|
|
24
|
+
review?: MoveReview;
|
|
25
|
+
}
|
|
26
|
+
/** A game as the analysis gives it. */
|
|
27
|
+
interface Analysis {
|
|
28
|
+
/** How to analyse, the first time in a chat (D-68). */
|
|
29
|
+
instructions?: string;
|
|
30
|
+
white: string;
|
|
31
|
+
black: string;
|
|
32
|
+
/** In a game played here: the side the AI played, and its level. */
|
|
33
|
+
ai?: Side;
|
|
34
|
+
level?: AiLevel;
|
|
35
|
+
/** In a game played elsewhere: the side whose name is the player's profile's. */
|
|
36
|
+
player?: Side;
|
|
37
|
+
date?: string;
|
|
38
|
+
timeControl?: string;
|
|
39
|
+
result?: string;
|
|
40
|
+
termination?: string;
|
|
41
|
+
opening?: string;
|
|
42
|
+
/** Whether the game stayed in the opening book to its last move. */
|
|
43
|
+
inBook?: true;
|
|
44
|
+
/** The last move the opening book knows, the end of the moves with the book's badge. */
|
|
45
|
+
bookTo?: string;
|
|
46
|
+
/** The game's name in the archive, a game brought as a PGN too. */
|
|
47
|
+
name?: string;
|
|
48
|
+
/** In a game from a position: that position, and what is at risk and the mates in one there. */
|
|
49
|
+
start?: Omit<MoveFacts, "move"> & {
|
|
50
|
+
fen: string;
|
|
51
|
+
};
|
|
52
|
+
moves: MoveFacts[];
|
|
53
|
+
/** The AI's word on the whole game, from annotate_game. */
|
|
54
|
+
review?: string;
|
|
55
|
+
}
|
|
56
|
+
/** A game over move by move, for the analyst (D-68): the current one, one of the archive or kept to study, or one the player brings as a PGN, kept to study. */
|
|
57
|
+
export declare const analyseGame: import("./registry.ts").Command<z.ZodObject<{
|
|
58
|
+
game: z.ZodOptional<z.ZodString>;
|
|
59
|
+
pgn: z.ZodOptional<z.ZodString>;
|
|
60
|
+
}, z.core.$strip>, Analysis>;
|
|
61
|
+
export {};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
// The analyst's tool (D-21, D-68): a game over, move by move in one call, facts only and never a judgment, with the analyst's instructions once in a chat.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { clockOfComment, clockText, closingOf, Game, gameReviewOf, levelOf, materialOf, readPgn, reviewed, reviewOf, timeControlOfTag, timeControlText, withGameReview, } from "../core/index.js";
|
|
5
|
+
import { keepToStudy, lastChatStart, planOf, rewriteGame, startAnalysis } from "../games/index.js";
|
|
6
|
+
import { activeProfile, gameFolder } from "../profiles/index.js";
|
|
7
|
+
import { bookOpening, bookReaches } from "./opening-book.js";
|
|
8
|
+
import { defineCommand } from "./registry.js";
|
|
9
|
+
import { gameToStudy, knownTag, moveNumbers, namedGame, noGameGoingOn } from "./study.js";
|
|
10
|
+
import { balanceText, colourName, riskLine } from "./view.js";
|
|
11
|
+
// The analyst's instructions, read at the first analysis.
|
|
12
|
+
let instructions;
|
|
13
|
+
/** A game over move by move, for the analyst (D-68): the current one, one of the archive or kept to study, or one the player brings as a PGN, kept to study. */
|
|
14
|
+
export const analyseGame = defineCommand({
|
|
15
|
+
name: "analyse_game",
|
|
16
|
+
description: "Gives a game that is over move by move, to comment it with the player: each move with what it took and the time it took, the material "
|
|
17
|
+
+ "when it changes, the pieces at risk and the mates in one after it, where the game left the opening book, and your comments and plans. Facts "
|
|
18
|
+
+ "only: the judgment is yours. Without game or pgn, the game just over, or else the last one played; game: a name as past_games or study_games "
|
|
19
|
+
+ "writes it; pgn: a game the player brings from elsewhere, kept to study in the profile. Never while a game goes on. The first time in a chat "
|
|
20
|
+
+ "it also says how to analyse. annotate_game then keeps your badges and notes for the board page. Example: analyse_game {}.",
|
|
21
|
+
input: z.object({
|
|
22
|
+
game: z.string().optional().describe("a game's name, as past_games, study_games or analyse_game writes it"),
|
|
23
|
+
pgn: z.string().optional().describe("a game played elsewhere, in PGN: its tags and its moves, with their [%clk] and a review exported from the board page if it has them; other comments and evals can go"),
|
|
24
|
+
}),
|
|
25
|
+
run: ({ game, pgn }, context) => {
|
|
26
|
+
if (game !== undefined && pgn !== undefined) {
|
|
27
|
+
throw new Error("Give game or pgn, not both: game names a game of the archive, pgn brings one played elsewhere.");
|
|
28
|
+
}
|
|
29
|
+
const studied = pgn === undefined ? gameToStudy(context, game) : importedGame(context, pgn);
|
|
30
|
+
const analysis = analysisOf(studied, activeProfile(context.home()).name);
|
|
31
|
+
if (studied.name !== undefined) {
|
|
32
|
+
markBook(context, { record: studied.record, name: studied.name }, analysis.moves);
|
|
33
|
+
startAnalysis(gameFolder(context.home()), studied.name, context.now());
|
|
34
|
+
}
|
|
35
|
+
const told = analystInstructions(context);
|
|
36
|
+
return told === undefined ? analysis : { instructions: told, ...analysis };
|
|
37
|
+
},
|
|
38
|
+
format: (analysis) => [...(analysis.instructions === undefined ? [] : [analysis.instructions, ""]), ...headLines(analysis), ...analysis.moves.map(moveLine)].join("\n"),
|
|
39
|
+
});
|
|
40
|
+
/** A game the player brings as a PGN, checked move by move and kept to study with its clock and Chesspal's own review: no other comment, no engine's evaluation (D-6). */
|
|
41
|
+
function importedGame(context, pgn) {
|
|
42
|
+
noGameGoingOn(context);
|
|
43
|
+
if (/\.pgn\s*$/i.test(pgn)) {
|
|
44
|
+
throw new Error(`"${pgn.trim()}" is a file's name, not a game: pgn takes a game's text, its tags and its moves. A game played here or kept to study goes in game, by `
|
|
45
|
+
+ "its name, such as games/2026-11-02-153012.pgn.");
|
|
46
|
+
}
|
|
47
|
+
const records = readPgn(pgn);
|
|
48
|
+
if (records.length !== 1) {
|
|
49
|
+
throw new Error(records.length === 0 ? "The PGN holds no game: give it whole, its tags and its moves." : `The PGN holds ${records.length} games: give one at a time.`);
|
|
50
|
+
}
|
|
51
|
+
const [{ tags, moves }] = records;
|
|
52
|
+
// A game analysed here and exported comes back with its review (T-106): Chesspal's own marks stay, as the clock; any other comment goes.
|
|
53
|
+
const brought = {
|
|
54
|
+
tags,
|
|
55
|
+
moves: moves.map(({ san, comment }) => {
|
|
56
|
+
const clock = /\[%clk [^\]]*\]/.exec(comment ?? "")?.[0];
|
|
57
|
+
const move = clock === undefined ? { san } : { san, comment: clock };
|
|
58
|
+
const review = reviewOf(comment);
|
|
59
|
+
return review === undefined ? move : reviewed(move, review);
|
|
60
|
+
}),
|
|
61
|
+
};
|
|
62
|
+
const summary = gameReviewOf(records[0]);
|
|
63
|
+
const closing = closingOf(records[0]);
|
|
64
|
+
const record = summary === undefined && closing === undefined
|
|
65
|
+
? brought
|
|
66
|
+
: withGameReview(brought, { ...(summary === undefined ? {} : { summary }), ...(closing === undefined ? {} : { closing }) });
|
|
67
|
+
// A game brought again is the one already kept, with the review written on it since.
|
|
68
|
+
return namedGame(context, keepToStudy(gameFolder(context.home()), record, context.now()));
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* The opening book's moves marked as such in the game kept (D-69): a fact, not a judgment, so Chesspal puts the badge itself on each move from the start to the
|
|
72
|
+
* last one the book knows, where the AI gave none; a game from another position has none.
|
|
73
|
+
*/
|
|
74
|
+
function markBook(context, { record, name }, moves) {
|
|
75
|
+
const inBook = bookPlies(record, moves);
|
|
76
|
+
const unmarked = record.moves.slice(0, inBook).some((move) => reviewOf(move.comment)?.badge === undefined);
|
|
77
|
+
if (!unmarked) {
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const marked = record.moves.map((move, ply) => {
|
|
81
|
+
const review = reviewOf(move.comment);
|
|
82
|
+
return ply < inBook && review?.badge === undefined ? reviewed(move, { ...review, badge: "book" }) : move;
|
|
83
|
+
});
|
|
84
|
+
rewriteGame(gameFolder(context.home()), name, { ...record, moves: marked });
|
|
85
|
+
}
|
|
86
|
+
/** How many plies from the start the opening book knows: to the move before the first out of it; none from another position. */
|
|
87
|
+
function bookPlies(record, moves) {
|
|
88
|
+
const firstOut = moves.findIndex((move) => move.outOfBook);
|
|
89
|
+
return record.tags.FEN !== undefined ? 0 : firstOut >= 0 ? firstOut : moves.length;
|
|
90
|
+
}
|
|
91
|
+
function analysisOf({ record, name }, playerName) {
|
|
92
|
+
const { tags } = record;
|
|
93
|
+
const ai = sideNamed(record, "AI");
|
|
94
|
+
const control = timeControlOfTag(tags.TimeControl ?? "");
|
|
95
|
+
const { start, moves, outcome } = movesOf(record, ai);
|
|
96
|
+
// The board's end before the tags': a record may say what the board does not.
|
|
97
|
+
const ended = outcome.over ? { result: outcome.result, termination: outcome.ending } : tags.Result !== undefined && tags.Result !== "*" ? { result: tags.Result, termination: tags.Termination } : undefined;
|
|
98
|
+
const opening = bookOpening(record);
|
|
99
|
+
return {
|
|
100
|
+
white: tags.White ?? "?",
|
|
101
|
+
black: tags.Black ?? "?",
|
|
102
|
+
ai,
|
|
103
|
+
level: ai === undefined ? undefined : levelOf(record),
|
|
104
|
+
player: ai === undefined && playerName !== undefined ? sideNamed(record, playerName) : undefined,
|
|
105
|
+
date: knownTag(tags.Date),
|
|
106
|
+
timeControl: control === undefined ? knownTag(tags.TimeControl) : timeControlText(control),
|
|
107
|
+
result: ended?.result,
|
|
108
|
+
termination: ended?.termination,
|
|
109
|
+
opening: opening === undefined ? undefined : `${opening.eco} ${opening.name}`,
|
|
110
|
+
inBook: opening !== undefined && !moves.some((move) => move.outOfBook) ? true : undefined,
|
|
111
|
+
bookTo: opening === undefined ? undefined : moves[bookPlies(record, moves) - 1]?.move,
|
|
112
|
+
review: [gameReviewOf(record), closingOf(record)].filter((word) => word !== undefined).join(" ") || undefined,
|
|
113
|
+
name,
|
|
114
|
+
start: tags.FEN === undefined ? undefined : { fen: tags.FEN, ...start },
|
|
115
|
+
moves,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
/** The facts of every move, from a replay of the game: the facts after each move are those before the next. */
|
|
119
|
+
function movesOf(record, ai) {
|
|
120
|
+
const numbers = moveNumbers(record);
|
|
121
|
+
const game = new Game({ tags: record.tags, moves: [] });
|
|
122
|
+
const increment = timeControlOfTag(record.tags.TimeControl ?? "")?.increment;
|
|
123
|
+
// The time each side had left after its last move: the first move of each side spends no time it can tell, as the sites start their clocks apart.
|
|
124
|
+
const left = {};
|
|
125
|
+
let inBook = record.tags.FEN === undefined && bookReaches(game.position.fen);
|
|
126
|
+
let before = game.facts;
|
|
127
|
+
let balance = materialOf(game.position.fen).balance;
|
|
128
|
+
const start = { atRisk: riskLine(before), mates: matesOf(before) };
|
|
129
|
+
// What was at risk on the line before: a move tells it only when it changed.
|
|
130
|
+
let risk = start.atRisk;
|
|
131
|
+
const moves = record.moves.map(({ san, comment }, ply) => {
|
|
132
|
+
const side = game.position.turn;
|
|
133
|
+
const played = game.playRecorded(san);
|
|
134
|
+
if (!played.played) {
|
|
135
|
+
throw new Error(`The game's move ${numbers[ply]}${san} cannot be played: ${played.message} Fix the game's PGN.`);
|
|
136
|
+
}
|
|
137
|
+
const outOfBook = inBook && !bookReaches(game.position.fen);
|
|
138
|
+
inBook &&= !outOfBook;
|
|
139
|
+
const facts = game.facts;
|
|
140
|
+
const over = game.outcome.over;
|
|
141
|
+
const now = materialOf(game.position.fen).balance;
|
|
142
|
+
const clock = clockOfComment(comment ?? "");
|
|
143
|
+
const previous = left[side];
|
|
144
|
+
const risks = riskLine(facts);
|
|
145
|
+
const move = {
|
|
146
|
+
move: `${numbers[ply]}${san}`,
|
|
147
|
+
took: before.pieces.find((piece) => piece.square === played.move.to)?.piece ?? (san.includes("x") ? "P" : undefined),
|
|
148
|
+
spent: clock === undefined || previous === undefined || increment === undefined ? undefined : Math.max(0, previous - clock + increment),
|
|
149
|
+
left: clock,
|
|
150
|
+
material: now === balance ? undefined : balanceText(now),
|
|
151
|
+
atRisk: over || risks === risk ? undefined : (risks ?? "none"),
|
|
152
|
+
mates: over ? undefined : matesOf(facts),
|
|
153
|
+
outOfBook: outOfBook ? true : undefined,
|
|
154
|
+
drawOffer: comment?.includes("(=)") === true ? true : undefined,
|
|
155
|
+
said: side === ai ? saidIn(comment) : undefined,
|
|
156
|
+
review: reviewOf(comment),
|
|
157
|
+
plan: side === ai ? planOf(comment) : undefined,
|
|
158
|
+
};
|
|
159
|
+
if (clock !== undefined) {
|
|
160
|
+
left[side] = clock;
|
|
161
|
+
}
|
|
162
|
+
before = facts;
|
|
163
|
+
balance = now;
|
|
164
|
+
risk = risks;
|
|
165
|
+
return move;
|
|
166
|
+
});
|
|
167
|
+
return { start, moves, outcome: game.outcome };
|
|
168
|
+
}
|
|
169
|
+
/** The head of the analysis: who played, when and how fast, how it ended, the opening, and the game's name for look and position. */
|
|
170
|
+
function headLines(analysis) {
|
|
171
|
+
const { ai, level, player } = analysis;
|
|
172
|
+
const named = (side) => `${side === "white" ? analysis.white : analysis.black}${side === player ? " (the player)" : ""}`;
|
|
173
|
+
const players = ai === undefined
|
|
174
|
+
? `White: ${named("white")}; Black: ${named("black")}.`
|
|
175
|
+
: `You played ${colourName(ai)} at the ${level} level, the player ${colourName(ai === "white" ? "black" : "white")}.`;
|
|
176
|
+
const when = [analysis.date, analysis.timeControl === undefined ? undefined : `time control ${analysis.timeControl}`].filter((part) => part !== undefined).join(", ");
|
|
177
|
+
const count = Math.ceil(analysis.moves.length / 2);
|
|
178
|
+
const result = analysis.result === undefined ? "unknown" : `${analysis.result}${analysis.termination === undefined ? "" : ` (${analysis.termination})`}`;
|
|
179
|
+
const book = analysis.bookTo === undefined ? "" : analysis.inBook ? ", in the book to the end" : `, in the book to ${analysis.bookTo}`;
|
|
180
|
+
const badged = analysis.bookTo === undefined || analysis.name === undefined ? "" : ": those moves have the book badge, unless you give another";
|
|
181
|
+
const opening = analysis.opening === undefined ? [] : [`Opening: ${analysis.opening}${book}${badged}`];
|
|
182
|
+
const name = analysis.name === undefined ? 'Look and position take a move alone, such as move "12..."' : `Look and position take it as game "${analysis.name}"`;
|
|
183
|
+
const start = analysis.start === undefined ? [] : [moveLine({ ...analysis.start, move: `Start, ${analysis.start.fen}` })];
|
|
184
|
+
const head = `${players}${when === "" ? "" : ` ${when[0].toUpperCase()}${when.slice(1)}.`} Result: ${result}, after ${count} ${count === 1 ? "move" : "moves"}.`;
|
|
185
|
+
const review = analysis.review === undefined ? [] : [`Your review: ${analysis.review}`];
|
|
186
|
+
return [head, ...opening, ...review, `${name}.`, ...start];
|
|
187
|
+
}
|
|
188
|
+
/** 12...Nxe5: took P; spent 0:12, 4:32 left; material White +1; at risk: undefended Black Nf6; mate in one: Qh7#; out of book; you said "…"; your plan: … */
|
|
189
|
+
function moveLine(move) {
|
|
190
|
+
const left = move.left === undefined ? undefined : `${clockText(move.left)} left`;
|
|
191
|
+
const parts = [
|
|
192
|
+
move.took === undefined ? undefined : `took ${move.took}`,
|
|
193
|
+
move.spent === undefined ? left : `spent ${clockText(move.spent)}, ${left}`,
|
|
194
|
+
move.material === undefined ? undefined : `material ${move.material}`,
|
|
195
|
+
move.atRisk === undefined ? undefined : `at risk: ${move.atRisk}`,
|
|
196
|
+
move.mates === undefined ? undefined : `mate in one: ${move.mates.join(" ")}`,
|
|
197
|
+
move.outOfBook ? "out of book" : undefined,
|
|
198
|
+
move.drawOffer ? "offers a draw" : undefined,
|
|
199
|
+
move.said === undefined ? undefined : `you said "${move.said}"`,
|
|
200
|
+
move.plan === undefined ? undefined : `your plan: ${move.plan}`,
|
|
201
|
+
// The book's badge alone is Chesspal's, not the AI's word: the line says the move left the book where it did.
|
|
202
|
+
move.review === undefined || (move.review.badge === "book" && move.review.note === undefined) ? undefined : reviewLine(move.review),
|
|
203
|
+
].filter((part) => part !== undefined);
|
|
204
|
+
return parts.length === 0 ? move.move : `${move.move}: ${parts.join("; ")}`;
|
|
205
|
+
}
|
|
206
|
+
/**
|
|
207
|
+
* The analyst's instructions, the first time in a chat where the MCP server keeps what it told: again in a chat cleared or summarized, since its start
|
|
208
|
+
* keys them (D-57); never on the command line, where no analyst reads.
|
|
209
|
+
*/
|
|
210
|
+
function analystInstructions(context) {
|
|
211
|
+
const key = `analyst ${lastChatStart(context.folder)?.at ?? ""}`;
|
|
212
|
+
if (context.told === undefined || context.told.has(key)) {
|
|
213
|
+
return undefined;
|
|
214
|
+
}
|
|
215
|
+
context.told.add(key);
|
|
216
|
+
instructions ??= readFileSync(new URL("../../instructions/analyst.md", import.meta.url), "utf8").trim();
|
|
217
|
+
return instructions;
|
|
218
|
+
}
|
|
219
|
+
/** The mates in one the side to move has; none when it has none. */
|
|
220
|
+
function matesOf(facts) {
|
|
221
|
+
const mates = facts.checks.filter((check) => check.endsWith("#"));
|
|
222
|
+
return mates.length === 0 ? undefined : mates;
|
|
223
|
+
}
|
|
224
|
+
/** The side a name plays, by the White and Black tags, whatever its case; none when neither is that name. */
|
|
225
|
+
function sideNamed({ tags }, name) {
|
|
226
|
+
const same = (tag) => tag?.trim().toLowerCase() === name.trim().toLowerCase();
|
|
227
|
+
return same(tags.White) ? "white" : same(tags.Black) ? "black" : undefined;
|
|
228
|
+
}
|
|
229
|
+
/** The words of a move's comment, without the commands PGN hides in it ([%clk], [%plan]) and the draw offer. */
|
|
230
|
+
function saidIn(comment) {
|
|
231
|
+
const words = (comment ?? "").replace(/\[%[^\]]*\]/g, "").replace("(=)", "").replace(/\s+/g, " ").trim();
|
|
232
|
+
return words === "" ? undefined : words;
|
|
233
|
+
}
|
|
234
|
+
/** What the AI kept on a move with annotate_game: you marked blunder: "note", or you noted "note". */
|
|
235
|
+
function reviewLine({ badge, note }) {
|
|
236
|
+
const quoted = note === undefined ? "" : `"${note}"`;
|
|
237
|
+
return badge === undefined ? `you noted ${quoted}` : `you marked ${badge}${quoted === "" ? "" : `: ${quoted}`}`;
|
|
238
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** What the review kept: the game's name, how many moves it marked, and whether it holds a word on the whole game. */
|
|
3
|
+
interface Annotated {
|
|
4
|
+
game: string;
|
|
5
|
+
marked: number;
|
|
6
|
+
summary: boolean;
|
|
7
|
+
closing: boolean;
|
|
8
|
+
/** The opening book's moves still without a note of the AI's, as analyse_game numbers them. */
|
|
9
|
+
bookUnnoted: string[];
|
|
10
|
+
}
|
|
11
|
+
/** The AI's badges and notes on a game over, and its word on the whole game, written into the game's PGN (D-69). */
|
|
12
|
+
export declare const annotateGame: import("./registry.ts").Command<z.ZodObject<{
|
|
13
|
+
game: z.ZodOptional<z.ZodString>;
|
|
14
|
+
notes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
15
|
+
move: z.ZodString;
|
|
16
|
+
badge: z.ZodOptional<z.ZodEnum<{
|
|
17
|
+
brilliant: "brilliant";
|
|
18
|
+
great: "great";
|
|
19
|
+
best: "best";
|
|
20
|
+
excellent: "excellent";
|
|
21
|
+
good: "good";
|
|
22
|
+
book: "book";
|
|
23
|
+
inaccuracy: "inaccuracy";
|
|
24
|
+
mistake: "mistake";
|
|
25
|
+
miss: "miss";
|
|
26
|
+
blunder: "blunder";
|
|
27
|
+
}>>;
|
|
28
|
+
note: z.ZodOptional<z.ZodString>;
|
|
29
|
+
arrows: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
30
|
+
squares: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
31
|
+
}, z.core.$strip>>>;
|
|
32
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
33
|
+
closing: z.ZodOptional<z.ZodString>;
|
|
34
|
+
}, z.core.$strip>, Annotated>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// The AI's review of a game over (D-69): a badge on each move that matters, as chess.com marks them, with a note when it needs one, and its word on the
|
|
2
|
+
// whole game, kept in the game's PGN for the board page. The judgment is the AI's alone: Chesspal gives no criteria (D-6, D-43).
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { annotatorOf, badges, reviewed, reviewOf, withAnnotator, withGameReview } from "../core/index.js";
|
|
5
|
+
import { gameInAnalysis, rewriteGame, startAnalysis } from "../games/index.js";
|
|
6
|
+
import { gameFolder } from "../profiles/index.js";
|
|
7
|
+
import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
|
|
8
|
+
import { defineCommand } from "./registry.js";
|
|
9
|
+
import { moveIndex, moveNumbers, namedGame, noGameGoingOn } from "./study.js";
|
|
10
|
+
const summaryLimit = 1500;
|
|
11
|
+
const closingLimit = 400;
|
|
12
|
+
/** The AI's badges and notes on a game over, and its word on the whole game, written into the game's PGN (D-69). */
|
|
13
|
+
export const annotateGame = defineCommand({
|
|
14
|
+
name: "annotate_game",
|
|
15
|
+
description: "Keeps your review of a game that is over for the board page: on each move that matters a badge, as chess.com has them, with a short note "
|
|
16
|
+
+ "when it needs one, or a note alone on a move worth a word without a badge; your word on the whole game; and a last short word on how it ended, "
|
|
17
|
+
+ "its win or loss. The judgment is yours: mark the moves you would point out, not every move. A note can draw arrows and colour squares on the board to show what it says. The opening book's moves have their book badge already: give each a short note too, what it is for, in the player's words; without one the page shows the opening's name from the book, in English. A move marked again takes its new badge and note. Notes in the player's language and in words, as in the chat; **two asterisks** put the few words that matter in bold, a square or a file too (**g7**, the **h** pawn). Without game, the game analyse_game gave last. Never "
|
|
18
|
+
+ 'while a game goes on. Example: annotate_game {notes: [{move: "12...", badge: "blunder", note: "leaves the queen to the knight"}], summary: "…", closing: "…"}.',
|
|
19
|
+
input: z.object({
|
|
20
|
+
game: z.string().optional().describe("the game's name, as analyse_game writes it; without it, the game analysed last"),
|
|
21
|
+
notes: z.array(z.object({
|
|
22
|
+
move: z.string().describe("the move, as analyse_game numbers it: 12. for White's, 12... for Black's"),
|
|
23
|
+
badge: z.enum(badges).optional(),
|
|
24
|
+
note: noteInput.optional().describe("why, in a few sentences, with the better move when there was one"),
|
|
25
|
+
arrows: arrowsInput,
|
|
26
|
+
squares: squaresInput,
|
|
27
|
+
})).optional().describe("the moves that matter, each with its badge, its note, or both"),
|
|
28
|
+
summary: z.string().max(summaryLimit).optional().describe("your word on the whole game: a short paragraph, with a few points as lines starting with - if it helps; the moves have their own notes"),
|
|
29
|
+
closing: z.string().max(closingLimit).optional().describe("your last word on how the game ended, its win or loss, in a sentence or two: the page shows it at the end"),
|
|
30
|
+
}),
|
|
31
|
+
run: async ({ game, notes = [], summary, closing }, context) => {
|
|
32
|
+
noGameGoingOn(context);
|
|
33
|
+
if (notes.length === 0 && summary === undefined && closing === undefined) {
|
|
34
|
+
throw new Error("Give notes on the moves that matter, your word on the whole game as summary, your last word on the ending as closing, or all of them. Nothing changed.");
|
|
35
|
+
}
|
|
36
|
+
const studied = game === undefined ? gameAnalysed(context) : namedGame(context, game);
|
|
37
|
+
// Every move is checked before the game is written: all or nothing.
|
|
38
|
+
const moves = [...studied.record.moves];
|
|
39
|
+
for (const { move, badge, note, arrows = [], squares = [] } of notes) {
|
|
40
|
+
if (badge === undefined && note === undefined) {
|
|
41
|
+
throw new Error(`Give ${move} a badge, a note, or both: arrows and squares go with them. Nothing changed.`);
|
|
42
|
+
}
|
|
43
|
+
const index = moveIndex(studied.record, move);
|
|
44
|
+
// A note alone on a move of the book keeps its book badge: a fact Chesspal put there, not the AI's judgment.
|
|
45
|
+
const kept = badge ?? (reviewOf(moves[index].comment)?.badge === "book" ? "book" : undefined);
|
|
46
|
+
moves[index] = reviewed(moves[index], { ...(kept === undefined ? {} : { badge: kept }), ...(note === undefined ? {} : { note }), ...drawingsOf(arrows, squares) });
|
|
47
|
+
}
|
|
48
|
+
// Who reviewed it, as far as the host says it for certain (D-81): on the command line, or by a host that names no model over one that did, as it was.
|
|
49
|
+
const caller = await context.caller?.();
|
|
50
|
+
const kept = caller === undefined || (caller.hostOnly === true && annotatorOf(studied.record) !== undefined);
|
|
51
|
+
const marked = { ...(kept ? studied.record : withAnnotator(studied.record, caller)), moves };
|
|
52
|
+
rewriteGame(gameFolder(context.home()), studied.name, summary === undefined && closing === undefined ? marked : withGameReview(marked, { ...(summary === undefined ? {} : { summary }), ...(closing === undefined ? {} : { closing }) }));
|
|
53
|
+
startAnalysis(gameFolder(context.home()), studied.name, context.now());
|
|
54
|
+
const numbers = moveNumbers(marked);
|
|
55
|
+
const bookUnnoted = moves.flatMap((move, index) => {
|
|
56
|
+
const review = reviewOf(move.comment);
|
|
57
|
+
return review?.badge === "book" && review.note === undefined ? [numbers[index]] : [];
|
|
58
|
+
});
|
|
59
|
+
return { game: studied.name, marked: notes.length, summary: summary !== undefined, closing: closing !== undefined, bookUnnoted };
|
|
60
|
+
},
|
|
61
|
+
format: ({ game, marked, summary, closing, bookUnnoted }) => {
|
|
62
|
+
const kept = [marked === 0 ? undefined : `${marked} ${marked === 1 ? "move" : "moves"} marked`, summary ? "your word on the game" : undefined, closing ? "your last word" : undefined].filter((part) => part !== undefined);
|
|
63
|
+
const book = bookUnnoted.length === 0
|
|
64
|
+
? []
|
|
65
|
+
: [
|
|
66
|
+
`${bookUnnoted.length === 1 ? "The book's move" : "The book's moves"} ${bookUnnoted.join(" ")} ${bookUnnoted.length === 1 ? "has" : "have"} no note of yours: `
|
|
67
|
+
+ "the page shows the book's English name there. A short note on each, in the player's words, takes its place.",
|
|
68
|
+
];
|
|
69
|
+
return [`Kept in ${game}: ${[kept.slice(0, -1).join(", "), kept.at(-1)].filter((part) => part !== "").join(" and ")}. The board page shows them.`, ...book].join("\n");
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
function gameAnalysed(context) {
|
|
73
|
+
const analysis = gameInAnalysis(gameFolder(context.home()));
|
|
74
|
+
if (analysis === undefined) {
|
|
75
|
+
throw new Error("No game is in analysis: call analyse_game first, or give the game's name as game. Nothing changed.");
|
|
76
|
+
}
|
|
77
|
+
return namedGame(context, analysis.game);
|
|
78
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { type Side } from "../core/index.ts";
|
|
2
|
+
import { type CurrentGame } from "../games/index.ts";
|
|
3
|
+
/** What the AI reads of the clock: the time control, and the time each side has left when the command ran, in milliseconds. */
|
|
4
|
+
export interface ClockSummary {
|
|
5
|
+
control: string;
|
|
6
|
+
left: Record<Side, number>;
|
|
7
|
+
/** The clocks wait for the player to start them. */
|
|
8
|
+
waiting?: true;
|
|
9
|
+
}
|
|
10
|
+
/** The current game of a folder `now`, or undefined: a game whose side to move has run out of time is over, saved and archived first. */
|
|
11
|
+
export declare function gameAt(folder: string, now: Date): CurrentGame | undefined;
|
|
12
|
+
/** The clock of a game `now`, as the AI reads it; none in a game without one. */
|
|
13
|
+
export declare function clockSummaryOf(current: CurrentGame, now: Date): ClockSummary | undefined;
|
|
14
|
+
/** The clock's line: Clock: you 4:32, player 5:01 (10+5), and whether the clocks still wait for the player. */
|
|
15
|
+
export declare function clockLine({ control, left, waiting }: ClockSummary, ai: Side): string;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// The clock in the commands (D-13): a flag that falls ends the game as soon as a command reads it, and the clock's line for the AI.
|
|
2
|
+
import { canMate, clockText, Game, timeControlText, timeLeft } from "../core/index.js";
|
|
3
|
+
import { archiveEnded, endOf, loadGame, saveGame } from "../games/index.js";
|
|
4
|
+
import { colourName } from "./view.js";
|
|
5
|
+
/** The current game of a folder `now`, or undefined: a game whose side to move has run out of time is over, saved and archived first. */
|
|
6
|
+
export function gameAt(folder, now) {
|
|
7
|
+
const current = loadGame(folder);
|
|
8
|
+
if (current?.clock === undefined || endOf(current.game) !== undefined) {
|
|
9
|
+
return current;
|
|
10
|
+
}
|
|
11
|
+
const { turn } = current.game.position;
|
|
12
|
+
if (timeLeft(current.clock, turn, now)[turn] > 0) {
|
|
13
|
+
return current;
|
|
14
|
+
}
|
|
15
|
+
const ended = outOfTime(current, turn, now);
|
|
16
|
+
saveGame(folder, ended);
|
|
17
|
+
// An archive that cannot be written leaves the game in game.json, where status shows it, until a new game archives it.
|
|
18
|
+
archiveEnded(folder, ended);
|
|
19
|
+
return ended;
|
|
20
|
+
}
|
|
21
|
+
/** The clock of a game `now`, as the AI reads it; none in a game without one. */
|
|
22
|
+
export function clockSummaryOf(current, now) {
|
|
23
|
+
const { clock, game } = current;
|
|
24
|
+
if (clock === undefined) {
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
const left = endOf(game) === undefined ? timeLeft(clock, game.position.turn, now) : { ...clock.left };
|
|
28
|
+
return { control: timeControlText(clock.control), left, ...(clock.waiting === true && endOf(game) === undefined ? { waiting: true } : {}) };
|
|
29
|
+
}
|
|
30
|
+
/** The clock's line: Clock: you 4:32, player 5:01 (10+5), and whether the clocks still wait for the player. */
|
|
31
|
+
export function clockLine({ control, left, waiting }, ai) {
|
|
32
|
+
const player = ai === "white" ? "black" : "white";
|
|
33
|
+
const line = `Clock: you ${clockText(left[ai])}, player ${clockText(left[player])} (${control}).`;
|
|
34
|
+
return waiting === true ? `${line} The clocks wait: they start with the player's first move, or with Start on the board page when your move is first.` : line;
|
|
35
|
+
}
|
|
36
|
+
/** The game lost on time by `loser`, or drawn when the other side could not mate with any series of legal moves (FIDE 6.9). */
|
|
37
|
+
function outOfTime(current, loser, now) {
|
|
38
|
+
const winner = loser === "white" ? "black" : "white";
|
|
39
|
+
const ending = canMate(current.game.position.fen, winner)
|
|
40
|
+
? { Result: loser === "white" ? "0-1" : "1-0", Termination: `${colourName(loser)} lost on time` }
|
|
41
|
+
: { Result: "1/2-1/2", Termination: `${colourName(loser)} ran out of time and ${colourName(winner)} cannot mate` };
|
|
42
|
+
const { record } = current.game;
|
|
43
|
+
const clock = { ...current.clock, left: { ...current.clock.left, [loser]: 0 }, since: now.toISOString() };
|
|
44
|
+
return { ...current, game: new Game({ ...record, tags: { ...record.tags, ...ending } }), clock };
|
|
45
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { Position } from "./play-move.ts";
|
|
2
|
+
/** What a command runs with, besides its input: given by the command line or the MCP server, and by the tests. */
|
|
3
|
+
export interface Context {
|
|
4
|
+
/** The folder Chesspal runs in: the player's, where the AI host started it, which keeps only the host's files (D-71). */
|
|
5
|
+
folder: string;
|
|
6
|
+
/** The player's data folder (D-71), with a profile in it: the profiles, their games and the games kept to study, the same for every chat. */
|
|
7
|
+
home: () => string;
|
|
8
|
+
/** A number from 0 to 1, as Math.random gives: the colour drawn for the AI. */
|
|
9
|
+
random: () => number;
|
|
10
|
+
/** The time now: when a game starts. */
|
|
11
|
+
now: () => Date;
|
|
12
|
+
/** Waits a while, in milliseconds: a tool that waits for the player, as ponder does; the tests move their clock instead. */
|
|
13
|
+
pause?: (milliseconds: number) => Promise<void>;
|
|
14
|
+
/** The operating system, as process.platform names it: how a host starts the server. */
|
|
15
|
+
platform: string;
|
|
16
|
+
/** The player's language, as the system names it (it-IT): the command line's words to the player, in English when none or another. */
|
|
17
|
+
language?: string;
|
|
18
|
+
/** The address of the board page, when the MCP server serves it (D-47). */
|
|
19
|
+
page?: string;
|
|
20
|
+
/** Whether a browser has the board page open now, when the MCP server serves it. */
|
|
21
|
+
pageOpen?: () => boolean;
|
|
22
|
+
/** Opens a folder in the system's file manager: the data folder, from the board page (T-119); whether it could; none in the tests. */
|
|
23
|
+
openFolder?: (folder: string) => Promise<boolean>;
|
|
24
|
+
/** Opens the New game window on the board pages open now, where the player chooses the game: whether one was open. */
|
|
25
|
+
askNewGame?: () => boolean;
|
|
26
|
+
/** The AI host, as it names itself to the MCP server: claude-code, which notes how each chat starts (D-57); none on the command line. */
|
|
27
|
+
host?: () => string | undefined;
|
|
28
|
+
/** The player's words on the board page given to the AI in this process, by line: each comes once (T-65). */
|
|
29
|
+
readonly given: Set<string>;
|
|
30
|
+
/** Told when the board page has news that no file holds: words the AI was given, which it shows read (D-53), and the AI's model (D-82). */
|
|
31
|
+
heard?: () => void;
|
|
32
|
+
/** Told when the chat calls a tool, before it runs: the board page hears the chat listens, and the AI's clock, held while nobody did, starts. */
|
|
33
|
+
calls?: () => void;
|
|
34
|
+
/** The game the chat saw when its last tool ended, its folder and its stamp: the doorbell wakes it only for a game changed since. */
|
|
35
|
+
seen?: string | undefined;
|
|
36
|
+
/**
|
|
37
|
+
* The position the last tool that showed the game showed the chat: its moves are refused on a game changed since, by another chat or on the page
|
|
38
|
+
* (D-71); null in a chat no tool has shown the game yet, none outside a chat.
|
|
39
|
+
*/
|
|
40
|
+
positionSeen?: Position | null | undefined;
|
|
41
|
+
/** The instructions the MCP server gave in this chat, by their name and the chat's start: each comes once a chat (D-68); none on the command line. */
|
|
42
|
+
told?: Set<string>;
|
|
43
|
+
/** Who makes this tool call, as far as the host ties it to the call (D-81); none on the command line. */
|
|
44
|
+
caller?: () => Promise<Caller | undefined>;
|
|
45
|
+
}
|
|
46
|
+
/** Who made a tool call: the model, else the host's name, never what the AI says of itself; the effort only when the host gives it (T-113). */
|
|
47
|
+
export interface Caller {
|
|
48
|
+
readonly annotator: string;
|
|
49
|
+
readonly effort?: string;
|
|
50
|
+
/** The annotator is the host's name: the model is not known. */
|
|
51
|
+
readonly hostOnly?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/** The context of a process started in the player's folder: the data folder is found at the first command that needs it, once. */
|
|
54
|
+
export declare function defaultContext(): Context;
|
|
55
|
+
/** A data folder with a profile in it, the first made when there is none, named after the system's account (D-71). */
|
|
56
|
+
export declare function withProfile(folder: string, account: string): string;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { userInfo } from "node:os";
|
|
2
|
+
import { dataFolder, openFolder } from "../home/index.js";
|
|
3
|
+
import { ensureProfile } from "../profiles/index.js";
|
|
4
|
+
/** The context of a process started in the player's folder: the data folder is found at the first command that needs it, once. */
|
|
5
|
+
export function defaultContext() {
|
|
6
|
+
let found;
|
|
7
|
+
const home = () => withProfile(found ??= dataFolder(), accountName());
|
|
8
|
+
return {
|
|
9
|
+
folder: process.cwd(),
|
|
10
|
+
home,
|
|
11
|
+
random: Math.random,
|
|
12
|
+
now: () => new Date(),
|
|
13
|
+
platform: process.platform,
|
|
14
|
+
language: systemLanguage(),
|
|
15
|
+
given: new Set(),
|
|
16
|
+
openFolder: (folder) => openFolder(folder, process.platform),
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** A data folder with a profile in it, the first made when there is none, named after the system's account (D-71). */
|
|
20
|
+
export function withProfile(folder, account) {
|
|
21
|
+
ensureProfile(folder, account);
|
|
22
|
+
return folder;
|
|
23
|
+
}
|
|
24
|
+
/** The name the system knows the user by, for the first profile: Player when it has none. */
|
|
25
|
+
function accountName() {
|
|
26
|
+
try {
|
|
27
|
+
return userInfo().username.trim() || "Player";
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "Player";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** The system's language, as Node reads it: the user's locale on Windows, LANG and LC_ALL elsewhere. */
|
|
34
|
+
function systemLanguage() {
|
|
35
|
+
return Intl.DateTimeFormat().resolvedOptions().locale;
|
|
36
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Context } from "./context.ts";
|
|
2
|
+
/** A draw offer or answer refused, and why, for the page to say it in the player's language: one stands already, none stands, or no side has moved yet. */
|
|
3
|
+
export declare class DrawRefused extends Error {
|
|
4
|
+
readonly reason: "offered" | "none" | "early";
|
|
5
|
+
constructor(message: string, reason: DrawRefused["reason"]);
|
|
6
|
+
}
|
|
7
|
+
/** The player offers a draw on their turn, once each side has moved: it stands until the AI accepts it or moves, and one stands at a time. */
|
|
8
|
+
export declare function offerDraw(context: Context): void;
|
|
9
|
+
/** The player's answer to the AI's draw offer: accepted, the game ends drawn by agreement; declined, it goes on. */
|
|
10
|
+
export declare function answerDraw(context: Context, accept: boolean): void;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// The draw offers of the board page (D-65): the player's, made on their turn as on Lichess, and their answer to the AI's, which ai_move makes with its move.
|
|
2
|
+
import { bothSidesMoved, drawOfferOf, saveGame } from "../games/index.js";
|
|
3
|
+
import { gameFolder } from "../profiles/index.js";
|
|
4
|
+
import { endGame } from "./end-game.js";
|
|
5
|
+
import { gameGoingOn } from "./game-summary.js";
|
|
6
|
+
/** A draw offer or answer refused, and why, for the page to say it in the player's language: one stands already, none stands, or no side has moved yet. */
|
|
7
|
+
export class DrawRefused extends Error {
|
|
8
|
+
reason;
|
|
9
|
+
constructor(message, reason) {
|
|
10
|
+
super(message);
|
|
11
|
+
this.reason = reason;
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
/** The player offers a draw on their turn, once each side has moved: it stands until the AI accepts it or moves, and one stands at a time. */
|
|
15
|
+
export function offerDraw(context) {
|
|
16
|
+
const folder = gameFolder(context.home());
|
|
17
|
+
const current = gameGoingOn(folder, context.now());
|
|
18
|
+
const offer = drawOfferOf(current);
|
|
19
|
+
if (current.game.position.turn === current.ai) {
|
|
20
|
+
throw new Error("Offer a draw on your turn: wait for the AI's move. Nothing changed.");
|
|
21
|
+
}
|
|
22
|
+
if (!bothSidesMoved(current.game)) {
|
|
23
|
+
throw new DrawRefused("A draw can be offered once each side has made a move. Nothing changed.", "early");
|
|
24
|
+
}
|
|
25
|
+
if (offer !== undefined) {
|
|
26
|
+
throw new DrawRefused(offer === "player" ? "You have offered a draw already: it stands until the AI's move. Nothing changed." : "The AI offers a draw: accept it or decline it. Nothing changed.", "offered");
|
|
27
|
+
}
|
|
28
|
+
saveGame(folder, { ...current, drawOffer: "player" });
|
|
29
|
+
}
|
|
30
|
+
/** The player's answer to the AI's draw offer: accepted, the game ends drawn by agreement; declined, it goes on. */
|
|
31
|
+
export function answerDraw(context, accept) {
|
|
32
|
+
const folder = gameFolder(context.home());
|
|
33
|
+
const current = gameGoingOn(folder, context.now());
|
|
34
|
+
if (drawOfferOf(current) !== "ai") {
|
|
35
|
+
throw new DrawRefused("The AI offers no draw to answer. Nothing changed.", "none");
|
|
36
|
+
}
|
|
37
|
+
if (accept) {
|
|
38
|
+
endGame.run({ result: "draw_agreed" }, context);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
saveGame(folder, { ...current, drawOffer: undefined });
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type DrawnArrow, type DrawnSquare } from "../core/index.ts";
|
|
3
|
+
/** The most a note of the AI's on a move may hold: a few sentences. */
|
|
4
|
+
export declare const noteLimit = 700;
|
|
5
|
+
/** A note of the AI's on a move, as its input reads it: on one line of commands, so without brackets or braces. */
|
|
6
|
+
export declare const noteInput: z.ZodString;
|
|
7
|
+
/** The arrows the AI draws, as their input reads them. */
|
|
8
|
+
export declare const arrowsInput: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
9
|
+
/** The squares the AI marks, as their input reads them. */
|
|
10
|
+
export declare const squaresInput: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
11
|
+
/** The arrows and squares drawn, those that read; none of either left out. */
|
|
12
|
+
export declare function drawingsOf(arrows?: readonly string[], squares?: readonly string[]): {
|
|
13
|
+
arrows?: DrawnArrow[];
|
|
14
|
+
squares?: DrawnSquare[];
|
|
15
|
+
};
|