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,58 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { moods } from "../core/index.js";
|
|
3
|
+
import { archivedName, talkLimits } from "../games/index.js";
|
|
4
|
+
import { coachAgent } from "../host-files/index.js";
|
|
5
|
+
import { gameFolder } from "../profiles/index.js";
|
|
6
|
+
import { agentsLine, analystLine, coachOnEnd, reviewedLine, withAnalyst } from "./agents.js";
|
|
7
|
+
import { newGameStep, nextStep, wordLines } from "./answer-lines.js";
|
|
8
|
+
import { summaryOf } from "./game-summary.js";
|
|
9
|
+
import { refuseOtherGame } from "./play-move.js";
|
|
10
|
+
import { defineCommand } from "./registry.js";
|
|
11
|
+
import { give, say, waitingWords } from "./say.js";
|
|
12
|
+
/** The AI's words on the board page without a move, read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
|
|
13
|
+
export const aiSay = defineCommand({
|
|
14
|
+
name: "ai_say",
|
|
15
|
+
description: "Says a word on the board page without a move, once while the player is to move: an answer to what they wrote or to their draw offer, or "
|
|
16
|
+
+ "something you have to say; when you are to move, speak in ai_move's comment. Between games, and before the first, it speaks freely: about the game just over, or to start another. "
|
|
17
|
+
+ 'Example: ai_say {text: "Sure, I\'ll play faster."}.',
|
|
18
|
+
input: z.object({
|
|
19
|
+
text: z.string().trim().min(1, "write what you say").max(talkLimits.aiHelps, `write ${talkLimits.aiHelps} characters at most`).describe(`your words for the player, one or two short sentences, ${talkLimits.ai} characters at most (${talkLimits.aiHelps} with the helps on): pieces and moves in `
|
|
20
|
+
+ `words ("your knight takes on f6"), a move you write with the pieces' figurines, as the tools show it (♞xf6+ for Nxf6+); your plans and threats only with the helps on; **two asterisks** put a word or two in bold on the page`),
|
|
21
|
+
mood: z.enum(moods).optional().describe("only after the game has ended: the face the board page keeps for its end"),
|
|
22
|
+
}),
|
|
23
|
+
run: ({ text, mood }, context) => {
|
|
24
|
+
// The player's words are read before the AI's line is saved: those written while it thought would fall before it, unread.
|
|
25
|
+
const folder = gameFolder(context.home());
|
|
26
|
+
refuseOtherGame(context, folder);
|
|
27
|
+
const waiting = waitingWords(context);
|
|
28
|
+
const current = say(folder, "ai", text, context.now(), mood);
|
|
29
|
+
const words = give(context, waiting);
|
|
30
|
+
if (!("game" in current)) {
|
|
31
|
+
return { words };
|
|
32
|
+
}
|
|
33
|
+
// The chat woken by the player's ending move on the page may have gone to its words without the coach (Priya).
|
|
34
|
+
return { game: withAnalyst(summaryOf(current, context.now(), archivedName(folder, current)), context, current), words, ...(coachOnEnd(context, folder, current) ? { coach: true } : {}) };
|
|
35
|
+
},
|
|
36
|
+
format: ({ game, words, coach }) => [
|
|
37
|
+
"Said on the board page, where the player reads it: don't write it again in the chat.",
|
|
38
|
+
...wordLines(words),
|
|
39
|
+
...(coach === true ? [agentsLine([coachAgent])] : []),
|
|
40
|
+
afterWords(game, words),
|
|
41
|
+
].join("\n"),
|
|
42
|
+
});
|
|
43
|
+
/** The next step once the AI has spoken: the end of a game over has been told already, and the player's draw offer answered in words. */
|
|
44
|
+
function afterWords(game, words) {
|
|
45
|
+
if (words.length === 0 && game?.end !== undefined) {
|
|
46
|
+
// The offer to go over the game, made in the AI's words, waits for the player's yes (D-89).
|
|
47
|
+
if (game.end.analyst === "done") {
|
|
48
|
+
return `Next: ${reviewedLine(game.end.archive)} When they want another game, ${newGameStep}`;
|
|
49
|
+
}
|
|
50
|
+
return game.end.analyst === "offer"
|
|
51
|
+
? `Next: when the player wants this game gone over, ${analystLine(game.end.archive)} When they want another game, ${newGameStep}`
|
|
52
|
+
: `Next: when the player wants another game, ${newGameStep}`;
|
|
53
|
+
}
|
|
54
|
+
if (words.length === 0 && game?.drawOffer === "player" && game.turn !== game.ai) {
|
|
55
|
+
return "Next: accept the draw with end_game draw_agreed, or wait for the player's move.";
|
|
56
|
+
}
|
|
57
|
+
return nextStep(game, words);
|
|
58
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
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
|
+
/** Who is ahead after it, as the AI estimated it (D-90). */
|
|
26
|
+
lead?: string;
|
|
27
|
+
}
|
|
28
|
+
/** A game as the analysis gives it. */
|
|
29
|
+
interface Analysis {
|
|
30
|
+
/** How to analyse, the first time in a chat (D-68). */
|
|
31
|
+
instructions?: string;
|
|
32
|
+
/** The player's language, for the analyst's agent, which talks with no one (D-89). */
|
|
33
|
+
speech?: string;
|
|
34
|
+
white: string;
|
|
35
|
+
black: string;
|
|
36
|
+
/** In a game played here: the side the AI played, and its level. */
|
|
37
|
+
ai?: Side;
|
|
38
|
+
level?: AiLevel;
|
|
39
|
+
/** In a game played elsewhere: the side whose name is the player's profile's. */
|
|
40
|
+
player?: Side;
|
|
41
|
+
date?: string;
|
|
42
|
+
timeControl?: string;
|
|
43
|
+
result?: string;
|
|
44
|
+
termination?: string;
|
|
45
|
+
opening?: string;
|
|
46
|
+
/** Whether the game stayed in the opening book to its last move. */
|
|
47
|
+
inBook?: true;
|
|
48
|
+
/** The last move the opening book knows, the end of the moves with the book's badge. */
|
|
49
|
+
bookTo?: string;
|
|
50
|
+
/** The game's name in the archive, a game brought as a PGN too. */
|
|
51
|
+
name?: string;
|
|
52
|
+
/** In a game from a position: that position, and what is at risk and the mates in one there. */
|
|
53
|
+
start?: Omit<MoveFacts, "move"> & {
|
|
54
|
+
fen: string;
|
|
55
|
+
};
|
|
56
|
+
moves: MoveFacts[];
|
|
57
|
+
/** The AI's word on the whole game, from annotate_game. */
|
|
58
|
+
review?: string;
|
|
59
|
+
}
|
|
60
|
+
/** 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. */
|
|
61
|
+
export declare const analyseGame: import("./registry.ts").Command<z.ZodObject<{
|
|
62
|
+
game: z.ZodOptional<z.ZodString>;
|
|
63
|
+
pgn: z.ZodOptional<z.ZodString>;
|
|
64
|
+
}, z.core.$strip>, Analysis>;
|
|
65
|
+
export {};
|
|
@@ -0,0 +1,265 @@
|
|
|
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 { z } from "zod";
|
|
3
|
+
import { clockOfComment, clockText, closingOf, Game, gameReviewOf, isReviewed, leadOf, leadText, levelOf, materialOf, readPgn, reviewed, reviewOf, timeControlOfTag, timeControlText, withGameReview, withLead, } from "../core/index.js";
|
|
4
|
+
import { isGameName, keepToStudy, lastChatStart, planOf, rewriteGame, startAnalysis } from "../games/index.js";
|
|
5
|
+
import { analystAgent, instructions } from "../host-files/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 { speechLine, speechOf } from "./speech.js";
|
|
10
|
+
import { gameToStudy, knownTag, moveNumbers, namedGame, noGameGoingOn } from "./study.js";
|
|
11
|
+
import { balanceText, colourName, riskLine } from "./view.js";
|
|
12
|
+
// The analyst's instructions, read at the first analysis.
|
|
13
|
+
let analystText;
|
|
14
|
+
/** 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. */
|
|
15
|
+
export const analyseGame = defineCommand({
|
|
16
|
+
name: "analyse_game",
|
|
17
|
+
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 "
|
|
18
|
+
+ "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 "
|
|
19
|
+
+ "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 "
|
|
20
|
+
+ "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 "
|
|
21
|
+
+ "it also says how to analyse. annotate_game then keeps your badges and notes for the board page. Example: analyse_game {}.",
|
|
22
|
+
input: z.object({
|
|
23
|
+
game: z.string().optional().describe("a game's name, as past_games, study_games or analyse_game writes it"),
|
|
24
|
+
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"),
|
|
25
|
+
}),
|
|
26
|
+
sharedWith: [analystAgent],
|
|
27
|
+
run: ({ game, pgn }, context) => {
|
|
28
|
+
if (game !== undefined && pgn !== undefined) {
|
|
29
|
+
throw new Error("Give game or pgn, not both: game names a game of the archive, pgn brings one played elsewhere.");
|
|
30
|
+
}
|
|
31
|
+
// A game's name given as pgn, as an AI often gives it, is that game (Priya).
|
|
32
|
+
const named = pgn !== undefined && isGameName(pgn) ? pgn.trim() : game;
|
|
33
|
+
const studied = pgn === undefined || named !== game ? gameToStudy(context, named) : importedGame(context, pgn);
|
|
34
|
+
// One analyst a game: the chat goes over the review there is (Stefano).
|
|
35
|
+
if (context.agent === analystAgent && isReviewed(studied.record)) {
|
|
36
|
+
throw new Error("Stop now: this game has its review already. End with one line for the AI that started you: go over that review with the player.");
|
|
37
|
+
}
|
|
38
|
+
const analysis = analysisOf(studied, activeProfile(context.home()).name);
|
|
39
|
+
if (studied.name !== undefined) {
|
|
40
|
+
markBook(context, { record: studied.record, name: studied.name }, analysis.moves);
|
|
41
|
+
startAnalysis(gameFolder(context.home()), studied.name, context.now());
|
|
42
|
+
}
|
|
43
|
+
// The analyst's agent has its instructions in its own file, and the player's language in its prompt or, when Chesspal knows it, here (D-89).
|
|
44
|
+
if (context.agent === analystAgent) {
|
|
45
|
+
const speech = speechOf(gameFolder(context.home()), context.language);
|
|
46
|
+
return speech === undefined ? analysis : { ...analysis, speech: speechLine(speech) };
|
|
47
|
+
}
|
|
48
|
+
const told = analystInstructions(context);
|
|
49
|
+
return told === undefined ? analysis : { instructions: told, ...analysis };
|
|
50
|
+
},
|
|
51
|
+
format: (analysis) => [
|
|
52
|
+
...(analysis.instructions === undefined ? [] : [analysis.instructions, ""]),
|
|
53
|
+
...(analysis.speech === undefined ? [] : [analysis.speech]),
|
|
54
|
+
...analysisHead(analysis),
|
|
55
|
+
...analysis.moves.map(moveLine),
|
|
56
|
+
].join("\n"),
|
|
57
|
+
});
|
|
58
|
+
/** 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). */
|
|
59
|
+
function importedGame(context, pgn) {
|
|
60
|
+
noGameGoingOn(context);
|
|
61
|
+
if (/\.pgn\s*$/i.test(pgn)) {
|
|
62
|
+
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 `
|
|
63
|
+
+ "its name, such as games/2026-11-02-153012.pgn.");
|
|
64
|
+
}
|
|
65
|
+
const records = readPgn(pgn);
|
|
66
|
+
if (records.length !== 1) {
|
|
67
|
+
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.`);
|
|
68
|
+
}
|
|
69
|
+
const [{ tags, moves }] = records;
|
|
70
|
+
// A game analysed here and exported comes back with its review: Chesspal's own marks stay, as the clock; any other comment goes.
|
|
71
|
+
const brought = {
|
|
72
|
+
tags,
|
|
73
|
+
moves: moves.map(({ san, comment }) => {
|
|
74
|
+
const clock = /\[%clk [^\]]*\]/.exec(comment ?? "")?.[0];
|
|
75
|
+
const move = clock === undefined ? { san } : { san, comment: clock };
|
|
76
|
+
const review = reviewOf(comment);
|
|
77
|
+
const lead = leadOf(comment);
|
|
78
|
+
const kept = review === undefined ? move : reviewed(move, review);
|
|
79
|
+
return lead === undefined ? kept : withLead(kept, lead);
|
|
80
|
+
}),
|
|
81
|
+
};
|
|
82
|
+
const summary = gameReviewOf(records[0]);
|
|
83
|
+
const closing = closingOf(records[0]);
|
|
84
|
+
const record = summary === undefined && closing === undefined
|
|
85
|
+
? brought
|
|
86
|
+
: withGameReview(brought, { ...(summary === undefined ? {} : { summary }), ...(closing === undefined ? {} : { closing }) });
|
|
87
|
+
// A game brought again is the one already kept, with the review written on it since.
|
|
88
|
+
return namedGame(context, keepToStudy(gameFolder(context.home()), record, context.now()));
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 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
|
|
92
|
+
* last one the book knows, where the AI gave none; a game from another position has none.
|
|
93
|
+
*/
|
|
94
|
+
function markBook(context, { record, name }, moves) {
|
|
95
|
+
const inBook = bookPliesOfFacts(record, moves);
|
|
96
|
+
const unmarked = record.moves.slice(0, inBook).some((move) => reviewOf(move.comment)?.badge === undefined);
|
|
97
|
+
if (!unmarked) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
const marked = record.moves.map((move, ply) => {
|
|
101
|
+
const review = reviewOf(move.comment);
|
|
102
|
+
return ply < inBook && review?.badge === undefined ? reviewed(move, { ...review, badge: "book" }) : move;
|
|
103
|
+
});
|
|
104
|
+
rewriteGame(gameFolder(context.home()), name, { ...record, moves: marked });
|
|
105
|
+
}
|
|
106
|
+
/** How many plies from the start the opening book knows, as opening-book's bookPlies counts them, read from the facts already found: none from another position. */
|
|
107
|
+
function bookPliesOfFacts(record, moves) {
|
|
108
|
+
const firstOut = moves.findIndex((move) => move.outOfBook);
|
|
109
|
+
return record.tags.FEN !== undefined ? 0 : firstOut >= 0 ? firstOut : moves.length;
|
|
110
|
+
}
|
|
111
|
+
function analysisOf({ record, name }, playerName) {
|
|
112
|
+
const { tags } = record;
|
|
113
|
+
const ai = sideNamed(record, "AI");
|
|
114
|
+
const control = timeControlOfTag(tags.TimeControl ?? "");
|
|
115
|
+
const { start, moves, outcome } = movesOf(record, ai);
|
|
116
|
+
// The board's end before the tags': a record may say what the board does not.
|
|
117
|
+
const ended = outcome.over ? { result: outcome.result, termination: outcome.ending } : tags.Result !== undefined && tags.Result !== "*" ? { result: tags.Result, termination: tags.Termination } : undefined;
|
|
118
|
+
const opening = bookOpening(record);
|
|
119
|
+
return {
|
|
120
|
+
white: tags.White ?? "?",
|
|
121
|
+
black: tags.Black ?? "?",
|
|
122
|
+
ai,
|
|
123
|
+
level: ai === undefined ? undefined : levelOf(record),
|
|
124
|
+
player: ai === undefined && playerName !== undefined ? sideNamed(record, playerName) : undefined,
|
|
125
|
+
date: knownTag(tags.Date),
|
|
126
|
+
timeControl: control === undefined ? knownTag(tags.TimeControl) : timeControlText(control),
|
|
127
|
+
result: ended?.result,
|
|
128
|
+
termination: ended?.termination,
|
|
129
|
+
opening: opening === undefined ? undefined : `${opening.eco} ${opening.name}`,
|
|
130
|
+
inBook: opening !== undefined && !moves.some((move) => move.outOfBook) ? true : undefined,
|
|
131
|
+
bookTo: opening === undefined ? undefined : moves[bookPliesOfFacts(record, moves) - 1]?.move,
|
|
132
|
+
review: [gameReviewOf(record), closingOf(record)].filter((word) => word !== undefined).join(" ") || undefined,
|
|
133
|
+
name,
|
|
134
|
+
start: tags.FEN === undefined ? undefined : { fen: tags.FEN, ...start },
|
|
135
|
+
moves,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/** The facts of every move, from a replay of the game: the facts after each move are those before the next. */
|
|
139
|
+
function movesOf(record, ai) {
|
|
140
|
+
const numbers = moveNumbers(record);
|
|
141
|
+
const game = new Game({ tags: record.tags, moves: [] });
|
|
142
|
+
const increment = timeControlOfTag(record.tags.TimeControl ?? "")?.increment;
|
|
143
|
+
// 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.
|
|
144
|
+
const left = {};
|
|
145
|
+
let inBook = record.tags.FEN === undefined && bookReaches(game.position.fen);
|
|
146
|
+
let before = game.facts;
|
|
147
|
+
let balance = materialOf(game.position.fen).balance;
|
|
148
|
+
const start = { atRisk: riskLine(before), mates: matesOf(before) };
|
|
149
|
+
// What was at risk on the line before: a move tells it only when it changed.
|
|
150
|
+
let risk = start.atRisk;
|
|
151
|
+
const moves = record.moves.map(({ san, comment }, ply) => {
|
|
152
|
+
const side = game.position.turn;
|
|
153
|
+
const played = game.playRecorded(san);
|
|
154
|
+
if (!played.played) {
|
|
155
|
+
throw new Error(`The game's move ${numbers[ply]}${san} cannot be played: ${played.message} Fix the game's PGN.`);
|
|
156
|
+
}
|
|
157
|
+
const outOfBook = inBook && !bookReaches(game.position.fen);
|
|
158
|
+
inBook &&= !outOfBook;
|
|
159
|
+
const facts = game.facts;
|
|
160
|
+
const over = game.outcome.over;
|
|
161
|
+
const now = materialOf(game.position.fen).balance;
|
|
162
|
+
const clock = clockOfComment(comment ?? "");
|
|
163
|
+
const previous = left[side];
|
|
164
|
+
const risks = riskLine(facts);
|
|
165
|
+
const move = {
|
|
166
|
+
move: `${numbers[ply]}${san}`,
|
|
167
|
+
took: before.pieces.find((piece) => piece.square === played.move.to)?.piece ?? (san.includes("x") ? "P" : undefined),
|
|
168
|
+
spent: clock === undefined || previous === undefined || increment === undefined ? undefined : Math.max(0, previous - clock + increment),
|
|
169
|
+
left: clock,
|
|
170
|
+
material: now === balance ? undefined : balanceText(now),
|
|
171
|
+
atRisk: over || risks === risk ? undefined : (risks ?? "none"),
|
|
172
|
+
mates: over ? undefined : matesOf(facts),
|
|
173
|
+
outOfBook: outOfBook ? true : undefined,
|
|
174
|
+
drawOffer: comment?.includes("(=)") === true ? true : undefined,
|
|
175
|
+
said: side === ai ? saidIn(comment) : undefined,
|
|
176
|
+
review: reviewOf(comment),
|
|
177
|
+
plan: side === ai ? planOf(comment) : undefined,
|
|
178
|
+
lead: estimateOf(comment),
|
|
179
|
+
};
|
|
180
|
+
if (clock !== undefined) {
|
|
181
|
+
left[side] = clock;
|
|
182
|
+
}
|
|
183
|
+
before = facts;
|
|
184
|
+
balance = now;
|
|
185
|
+
risk = risks;
|
|
186
|
+
return move;
|
|
187
|
+
});
|
|
188
|
+
return { start, moves, outcome: game.outcome };
|
|
189
|
+
}
|
|
190
|
+
/** 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. */
|
|
191
|
+
function analysisHead(analysis) {
|
|
192
|
+
const { ai, level, player } = analysis;
|
|
193
|
+
const named = (side) => `${side === "white" ? analysis.white : analysis.black}${side === player ? " (the player)" : ""}`;
|
|
194
|
+
const players = ai === undefined
|
|
195
|
+
? `White: ${named("white")}; Black: ${named("black")}.`
|
|
196
|
+
: `You played ${colourName(ai)} at the ${level} level, the player ${colourName(ai === "white" ? "black" : "white")}.`;
|
|
197
|
+
const when = [analysis.date, analysis.timeControl === undefined ? undefined : `time control ${analysis.timeControl}`].filter((part) => part !== undefined).join(", ");
|
|
198
|
+
const count = Math.ceil(analysis.moves.length / 2);
|
|
199
|
+
const result = analysis.result === undefined ? "unknown" : `${analysis.result}${analysis.termination === undefined ? "" : ` (${analysis.termination})`}`;
|
|
200
|
+
const book = analysis.bookTo === undefined ? "" : analysis.inBook ? ", in the book to the end" : `, in the book to ${analysis.bookTo}`;
|
|
201
|
+
const badged = analysis.bookTo === undefined || analysis.name === undefined ? "" : ": those moves have the book badge, unless you give another";
|
|
202
|
+
const opening = analysis.opening === undefined ? [] : [`Opening: ${analysis.opening}${book}${badged}`];
|
|
203
|
+
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}"`;
|
|
204
|
+
const start = analysis.start === undefined ? [] : [moveLine({ ...analysis.start, move: `Start, ${analysis.start.fen}` })];
|
|
205
|
+
const head = `${players}${when === "" ? "" : ` ${when[0].toUpperCase()}${when.slice(1)}.`} Result: ${result}, after ${count} ${count === 1 ? "move" : "moves"}.`;
|
|
206
|
+
const review = analysis.review === undefined ? [] : [`Your review: ${analysis.review}`];
|
|
207
|
+
return [head, ...opening, ...review, `${name}.`, ...start];
|
|
208
|
+
}
|
|
209
|
+
/** 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: …; ahead -1.5 */
|
|
210
|
+
function moveLine(move) {
|
|
211
|
+
const left = move.left === undefined ? undefined : `${clockText(move.left)} left`;
|
|
212
|
+
const parts = [
|
|
213
|
+
move.took === undefined ? undefined : `took ${move.took}`,
|
|
214
|
+
move.spent === undefined ? left : `spent ${clockText(move.spent)}, ${left}`,
|
|
215
|
+
move.material === undefined ? undefined : `material ${move.material}`,
|
|
216
|
+
move.atRisk === undefined ? undefined : `at risk: ${move.atRisk}`,
|
|
217
|
+
move.mates === undefined ? undefined : `mate in one: ${move.mates.join(" ")}`,
|
|
218
|
+
move.outOfBook ? "out of book" : undefined,
|
|
219
|
+
move.drawOffer ? "offers a draw" : undefined,
|
|
220
|
+
move.said === undefined ? undefined : `you said "${move.said}"`,
|
|
221
|
+
move.plan === undefined ? undefined : `your plan: ${move.plan}`,
|
|
222
|
+
// The book's badge alone is Chesspal's, not the AI's word: the line says the move left the book where it did.
|
|
223
|
+
move.review === undefined || (move.review.badge === "book" && move.review.note === undefined) ? undefined : reviewLine(move.review),
|
|
224
|
+
move.lead === undefined ? undefined : `ahead ${move.lead}`,
|
|
225
|
+
].filter((part) => part !== undefined);
|
|
226
|
+
return parts.length === 0 ? move.move : `${move.move}: ${parts.join("; ")}`;
|
|
227
|
+
}
|
|
228
|
+
/**
|
|
229
|
+
* 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
|
|
230
|
+
* keys them (D-57); never on the command line, where no analyst reads.
|
|
231
|
+
*/
|
|
232
|
+
function analystInstructions(context) {
|
|
233
|
+
const key = `analyst ${lastChatStart(context.folder)?.at ?? ""}`;
|
|
234
|
+
if (context.told === undefined || context.told.has(key)) {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
context.told.add(key);
|
|
238
|
+
analystText ??= `${instructions("analyst")}\n${instructions("analysis")}`;
|
|
239
|
+
return analystText;
|
|
240
|
+
}
|
|
241
|
+
/** Who is ahead after a move, as the AI estimated it with annotate_game or the coach during the game (D-90). */
|
|
242
|
+
function estimateOf(comment) {
|
|
243
|
+
const lead = leadOf(comment);
|
|
244
|
+
return lead === undefined ? undefined : leadText(lead);
|
|
245
|
+
}
|
|
246
|
+
/** The mates in one the side to move has; none when it has none. */
|
|
247
|
+
function matesOf(facts) {
|
|
248
|
+
const mates = facts.checks.filter((check) => check.endsWith("#"));
|
|
249
|
+
return mates.length === 0 ? undefined : mates;
|
|
250
|
+
}
|
|
251
|
+
/** The side a name plays, by the White and Black tags, whatever its case; none when neither is that name. */
|
|
252
|
+
function sideNamed({ tags }, name) {
|
|
253
|
+
const same = (tag) => tag?.trim().toLowerCase() === name.trim().toLowerCase();
|
|
254
|
+
return same(tags.White) ? "white" : same(tags.Black) ? "black" : undefined;
|
|
255
|
+
}
|
|
256
|
+
/** The words of a move's comment, without the commands PGN hides in it ([%clk], [%plan]) and the draw offer. */
|
|
257
|
+
function saidIn(comment) {
|
|
258
|
+
const words = (comment ?? "").replace(/\[%[^\]]*\]/g, "").replace("(=)", "").replace(/\s+/g, " ").trim();
|
|
259
|
+
return words === "" ? undefined : words;
|
|
260
|
+
}
|
|
261
|
+
/** What the AI kept on a move with annotate_game: you marked blunder: "note", or you noted "note". */
|
|
262
|
+
function reviewLine({ badge, note }) {
|
|
263
|
+
const quoted = note === undefined ? "" : `"${note}"`;
|
|
264
|
+
return badge === undefined ? `you noted ${quoted}` : `you marked ${badge}${quoted === "" ? "" : `: ${quoted}`}`;
|
|
265
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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
|
+
/** How many moves took who is ahead after them, and those still without it, as analyse_game numbers them (D-90). */
|
|
11
|
+
estimated: number;
|
|
12
|
+
unestimated: string[];
|
|
13
|
+
}
|
|
14
|
+
/** 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). */
|
|
15
|
+
export declare const annotateGame: import("./registry.ts").Command<z.ZodObject<{
|
|
16
|
+
game: z.ZodOptional<z.ZodString>;
|
|
17
|
+
notes: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
18
|
+
move: z.ZodString;
|
|
19
|
+
badge: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
brilliant: "brilliant";
|
|
21
|
+
great: "great";
|
|
22
|
+
best: "best";
|
|
23
|
+
excellent: "excellent";
|
|
24
|
+
good: "good";
|
|
25
|
+
book: "book";
|
|
26
|
+
inaccuracy: "inaccuracy";
|
|
27
|
+
mistake: "mistake";
|
|
28
|
+
miss: "miss";
|
|
29
|
+
blunder: "blunder";
|
|
30
|
+
}>>;
|
|
31
|
+
note: z.ZodOptional<z.ZodString>;
|
|
32
|
+
arrows: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
33
|
+
squares: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
34
|
+
}, z.core.$strip>>>;
|
|
35
|
+
ahead: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodUnion<readonly [z.ZodNumber, z.ZodString]>>>;
|
|
36
|
+
summary: z.ZodOptional<z.ZodString>;
|
|
37
|
+
closing: z.ZodOptional<z.ZodString>;
|
|
38
|
+
}, z.core.$strip>, Annotated>;
|
|
39
|
+
export {};
|
|
@@ -0,0 +1,131 @@
|
|
|
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, Game, leadOf, reviewed, reviewOf, withAnnotator, withGameReview, withLead } from "../core/index.js";
|
|
5
|
+
import { gameInAnalysis, rewriteGame, startAnalysis } from "../games/index.js";
|
|
6
|
+
import { analystAgent } from "../host-files/index.js";
|
|
7
|
+
import { gameFolder } from "../profiles/index.js";
|
|
8
|
+
import { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
|
|
9
|
+
import { leadFrom, leadInput } from "./lead-input.js";
|
|
10
|
+
import { defineCommand } from "./registry.js";
|
|
11
|
+
import { moveIndex, moveNumbers, namedGame, noGameGoingOn } from "./study.js";
|
|
12
|
+
const summaryLimit = 1500;
|
|
13
|
+
const closingLimit = 400;
|
|
14
|
+
// How many moves still without who is ahead the answer names; the rest it counts.
|
|
15
|
+
const unestimatedShown = 10;
|
|
16
|
+
/** 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). */
|
|
17
|
+
export const annotateGame = defineCommand({
|
|
18
|
+
name: "annotate_game",
|
|
19
|
+
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 "
|
|
20
|
+
+ "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, "
|
|
21
|
+
+ "its win or loss; and who is ahead after every move as ahead, for the page's bar and chart. 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 "
|
|
22
|
+
+ 'while a game goes on. Example: annotate_game {notes: [{move: "12...", badge: "blunder", note: "leaves the queen to the knight"}], ahead: {"1.": 0.3, "1...": 0.2, "12...": 3.5, "13.": "#4"}, '
|
|
23
|
+
+ 'summary: "…", closing: "…"}.',
|
|
24
|
+
input: z.object({
|
|
25
|
+
game: z.string().optional().describe("the game's name, as analyse_game writes it; without it, the game analysed last"),
|
|
26
|
+
notes: z.array(z.object({
|
|
27
|
+
move: z.string().describe("the move, as analyse_game numbers it: 12. for White's, 12... for Black's"),
|
|
28
|
+
badge: z.enum(badges).optional(),
|
|
29
|
+
note: noteInput.optional().describe("why, in a few sentences, with the better move when there was one"),
|
|
30
|
+
arrows: arrowsInput,
|
|
31
|
+
squares: squaresInput,
|
|
32
|
+
})).optional().describe("the moves that matter, each with its badge, its note, or both"),
|
|
33
|
+
ahead: z.record(z.string(), leadInput).optional().describe("who is ahead after each move, by the move as analyse_game numbers it: pawns from White's side as chess.com shows it, 1.5 for White, -3 for Black, 0 for "
|
|
34
|
+
+ 'even, or a mate you see in moves of the side that mates, "#3" for White\'s, "#-3" for Black\'s; every move, in one call or a few; a move given again '
|
|
35
|
+
+ "takes its new figure"),
|
|
36
|
+
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"),
|
|
37
|
+
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"),
|
|
38
|
+
}),
|
|
39
|
+
sharedWith: [analystAgent],
|
|
40
|
+
run: async ({ game, notes = [], ahead = {}, summary, closing }, context) => {
|
|
41
|
+
noGameGoingOn(context);
|
|
42
|
+
const estimates = Object.entries(ahead);
|
|
43
|
+
if (notes.length === 0 && estimates.length === 0 && summary === undefined && closing === undefined) {
|
|
44
|
+
throw new Error("Give notes on the moves that matter, who is ahead after each move as ahead, your word on the whole game as summary, your last word on the ending as "
|
|
45
|
+
+ "closing, or all of them. Nothing changed.");
|
|
46
|
+
}
|
|
47
|
+
// Who reviews it, before the game is read: no wait between reading and writing it, in which the coach's judgment or another review written meanwhile
|
|
48
|
+
// would be lost (Federico).
|
|
49
|
+
const caller = await context.caller?.();
|
|
50
|
+
const studied = game === undefined ? gameAnalysed(context) : namedGame(context, game);
|
|
51
|
+
// Every move is checked before the game is written: all or nothing.
|
|
52
|
+
const moves = [...studied.record.moves];
|
|
53
|
+
for (const { move, badge, note, arrows = [], squares = [] } of notes) {
|
|
54
|
+
if (badge === undefined && note === undefined) {
|
|
55
|
+
throw new Error(`Give ${move} a badge, a note, or both: arrows and squares go with them. Nothing changed.`);
|
|
56
|
+
}
|
|
57
|
+
const index = indexIn(studied.record, move, "notes");
|
|
58
|
+
// A note alone on a move of the book keeps its book badge: a fact Chesspal put there, not the AI's judgment.
|
|
59
|
+
const kept = badge ?? (reviewOf(moves[index].comment)?.badge === "book" ? "book" : undefined);
|
|
60
|
+
moves[index] = reviewed(moves[index], { ...(kept === undefined ? {} : { badge: kept }), ...(note === undefined ? {} : { note }), ...drawingsOf(arrows, squares) });
|
|
61
|
+
}
|
|
62
|
+
for (const [move, lead] of estimates) {
|
|
63
|
+
const index = indexIn(studied.record, move, "ahead");
|
|
64
|
+
moves[index] = withLead(moves[index], leadFrom(lead));
|
|
65
|
+
}
|
|
66
|
+
// 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.
|
|
67
|
+
const kept = caller === undefined || (caller.hostOnly === true && annotatorOf(studied.record) !== undefined);
|
|
68
|
+
const marked = { ...(kept ? studied.record : withAnnotator(studied.record, caller)), moves };
|
|
69
|
+
rewriteGame(gameFolder(context.home()), studied.name, summary === undefined && closing === undefined ? marked : withGameReview(marked, { ...(summary === undefined ? {} : { summary }), ...(closing === undefined ? {} : { closing }) }));
|
|
70
|
+
startAnalysis(gameFolder(context.home()), studied.name, context.now());
|
|
71
|
+
const numbers = moveNumbers(marked);
|
|
72
|
+
const bookUnnoted = moves.flatMap((move, index) => {
|
|
73
|
+
const review = reviewOf(move.comment);
|
|
74
|
+
return review?.badge === "book" && review.note === undefined ? [numbers[index]] : [];
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
game: studied.name,
|
|
78
|
+
marked: notes.length,
|
|
79
|
+
summary: summary !== undefined,
|
|
80
|
+
closing: closing !== undefined,
|
|
81
|
+
bookUnnoted,
|
|
82
|
+
estimated: estimates.length,
|
|
83
|
+
// Asked every time, of an older review too: an analyst whose instructions predate the estimates learns of them here.
|
|
84
|
+
unestimated: unestimatedMoves(marked, numbers),
|
|
85
|
+
};
|
|
86
|
+
},
|
|
87
|
+
format: ({ game, marked, summary, closing, bookUnnoted, estimated, unestimated }) => {
|
|
88
|
+
const kept = [
|
|
89
|
+
marked === 0 ? undefined : `${marked} ${marked === 1 ? "move" : "moves"} marked`,
|
|
90
|
+
estimated === 0 ? undefined : `who is ahead after ${estimated} ${estimated === 1 ? "move" : "moves"}`,
|
|
91
|
+
summary ? "your word on the game" : undefined,
|
|
92
|
+
closing ? "your last word" : undefined,
|
|
93
|
+
].filter((part) => part !== undefined);
|
|
94
|
+
const book = bookUnnoted.length === 0
|
|
95
|
+
? []
|
|
96
|
+
: [
|
|
97
|
+
`${bookUnnoted.length === 1 ? "The book's move" : "The book's moves"} ${bookUnnoted.join(" ")} ${bookUnnoted.length === 1 ? "has" : "have"} no note of yours: `
|
|
98
|
+
+ "the page shows the book's English name there. A short note on each, in the player's words, takes its place.",
|
|
99
|
+
];
|
|
100
|
+
const shown = unestimated.slice(0, unestimatedShown).join(" ");
|
|
101
|
+
const more = unestimated.length > unestimatedShown ? ` and ${unestimated.length - unestimatedShown} more` : "";
|
|
102
|
+
const ahead = unestimated.length === 0 ? [] : [`Who is ahead is still missing after ${shown}${more}: give it with ahead, for the page's bar and chart.`];
|
|
103
|
+
return [`Kept in ${game}: ${[kept.slice(0, -1).join(", "), kept.at(-1)].filter((part) => part !== "").join(" and ")}. The board page shows them.`, ...book, ...ahead].join("\n");
|
|
104
|
+
},
|
|
105
|
+
});
|
|
106
|
+
/** The moves without who is ahead after them, as analyse_game numbers them: all but the last when the board ended the game there, which says it. */
|
|
107
|
+
function unestimatedMoves(record, numbers) {
|
|
108
|
+
const ended = new Game(record).outcome.over;
|
|
109
|
+
return record.moves.flatMap((move, index) => leadOf(move.comment) !== undefined || (ended && index === record.moves.length - 1) ? [] : [numbers[index]]);
|
|
110
|
+
}
|
|
111
|
+
/** A move given in notes or in ahead, as analyse_game numbers it, refused with where it was given (Priya). */
|
|
112
|
+
function indexIn(record, move, field) {
|
|
113
|
+
try {
|
|
114
|
+
return moveIndex(record, move);
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
const message = error.message;
|
|
118
|
+
throw new Error(`In ${field}, ${message.charAt(0).toLowerCase()}${message.slice(1)} Nothing changed.`);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
function gameAnalysed(context) {
|
|
122
|
+
// The game in analysis follows the board page, which the player may turn to another game while an agent works (Federico).
|
|
123
|
+
if (context.agent !== undefined) {
|
|
124
|
+
throw new Error("Give the game's name as game, as analyse_game named it: the board page may have opened another game since. Nothing changed.");
|
|
125
|
+
}
|
|
126
|
+
const analysis = gameInAnalysis(gameFolder(context.home()));
|
|
127
|
+
if (analysis === undefined) {
|
|
128
|
+
throw new Error("No game is in analysis: call analyse_game first, or give the game's name as game. Nothing changed.");
|
|
129
|
+
}
|
|
130
|
+
return namedGame(context, analysis.game);
|
|
131
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { type Side } from "../core/index.ts";
|
|
2
|
+
import { type GameSummary, type LastMove, type MovePlayed } from "./game-summary.ts";
|
|
3
|
+
/** Where the game stands: the AI's position to decide on, with its last move unless the lines before named it; the player's turn; or how it ended. */
|
|
4
|
+
export declare function positionLines(game: GameSummary, withLastMove?: boolean): string[];
|
|
5
|
+
/** The head of a view: where the game stands, the last move unless the lines before named it, the opening, the material as the player sees it, and the FEN. */
|
|
6
|
+
export declare function headLines(game: GameSummary, withLastMove?: boolean): string[];
|
|
7
|
+
/** The last move, as the player is shown it, with its squares: 4.♞g5 (f3-g5). */
|
|
8
|
+
export declare function lastMoveText({ move, from, to }: LastMove): string;
|
|
9
|
+
/**
|
|
10
|
+
* A numbered move as the player is shown it, with the pieces' figurines (D-86): the form alone, without the SAN, which the AI copied into its message
|
|
11
|
+
* (Priya, test of D-86): 4.Ng5 is 4.♞g5.
|
|
12
|
+
*/
|
|
13
|
+
export declare function shownMove(move: string): string;
|
|
14
|
+
/** The lines after a move, which the line before named: its note, where the game stands, a warning. closingLines end them. */
|
|
15
|
+
export declare function moveLines({ note, game, warning }: MovePlayed): string[];
|
|
16
|
+
/** The last lines of a tool's answer: the player's words from the board page, then the next step, which stays the last line. */
|
|
17
|
+
export declare function closingLines(game: GameSummary | undefined, words?: readonly string[], pageClosed?: boolean): string[];
|
|
18
|
+
/** The player's words from the board page, one line each: lines they broke are joined, so no line of theirs reads as one of Chesspal's. */
|
|
19
|
+
export declare function wordLines(words: readonly string[]): string[];
|
|
20
|
+
/**
|
|
21
|
+
* What the AI does next: the one line every host reads, even where the server's instructions are not shown. Words of the player's ask for an answer. With the
|
|
22
|
+
* board page open in no browser, a new game starts there, never in the chat.
|
|
23
|
+
*/
|
|
24
|
+
export declare function nextStep(game: GameSummary | undefined, words?: readonly string[], pageClosed?: boolean): string;
|
|
25
|
+
/** How a new game starts: the player chooses it in the board page's window, which new_game without choices opens (D-85). */
|
|
26
|
+
export declare const newGameStep = "call new_game without choices: it opens the board page's New game window, where the player chooses the colour, the clock and the level.";
|
|
27
|
+
/** Who plays what, from the AI's side. */
|
|
28
|
+
export declare function sidesLine(ai: Side): string;
|