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,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type Profile } from "../core/index.ts";
|
|
3
|
+
/** The positional profile of a position: of a game studied, with where it stands. */
|
|
4
|
+
type Described = Profile & {
|
|
5
|
+
where?: string;
|
|
6
|
+
};
|
|
7
|
+
/** The positional profile of the game in progress (D-19), or of any position of a game over (D-68): separate facts, never a score. */
|
|
8
|
+
export declare const position: import("./registry.ts").Command<z.ZodObject<{
|
|
9
|
+
game: z.ZodOptional<z.ZodString>;
|
|
10
|
+
move: z.ZodOptional<z.ZodString>;
|
|
11
|
+
}, z.core.$strip>, Described>;
|
|
12
|
+
/** The profile as lines for the AI, White's facts before Black's; a line with nothing to say is left out. */
|
|
13
|
+
export declare function profileLines(profile: Profile): string[];
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import {} from "../core/index.js";
|
|
3
|
+
import { analystAgent } from "../host-files/index.js";
|
|
4
|
+
import { gameFolder } from "../profiles/index.js";
|
|
5
|
+
import { gameAt } from "./clock.js";
|
|
6
|
+
import { refuseUnseen } from "./levels.js";
|
|
7
|
+
import { defineCommand } from "./registry.js";
|
|
8
|
+
import { gameToStudy, positionAfter, positionLine, studyInput } from "./study.js";
|
|
9
|
+
import { colourName, materialText } from "./view.js";
|
|
10
|
+
/** The positional profile of the game in progress (D-19), or of any position of a game over (D-68): separate facts, never a score. */
|
|
11
|
+
export const position = defineCommand({
|
|
12
|
+
name: "position",
|
|
13
|
+
description: "Shows the positional facts of the position, from the strong level up and at every level once the game is over: material, pawns (isolated, doubled, backward, passed, islands), open files, rooks and queens on the seventh, bishops, "
|
|
14
|
+
+ "outposts, centre, space, control, mobility, development and castling, king safety, the phase, the draw rules and, with only kings and pawns, "
|
|
15
|
+
+ "the rule of the square and the opposition. Facts only, never a score: weigh them yourself. Call it when you set or change your plan: out of "
|
|
16
|
+
+ "the opening, after a big exchange, in the endgame; not at every move. With move, and game for a game of the archive, the facts of any position of a "
|
|
17
|
+
+ 'game over, to analyse it. Example: position {}, position {move: "24."}.',
|
|
18
|
+
input: z.object(studyInput),
|
|
19
|
+
readOnly: true,
|
|
20
|
+
idempotent: true,
|
|
21
|
+
sharedWith: [analystAgent],
|
|
22
|
+
run: ({ game, move }, context) => {
|
|
23
|
+
if (game !== undefined || move !== undefined) {
|
|
24
|
+
const studied = gameToStudy(context, game);
|
|
25
|
+
const at = positionAfter(studied.record, move);
|
|
26
|
+
return { ...at.game.profile, where: positionLine(at, studied.name) };
|
|
27
|
+
}
|
|
28
|
+
const current = gameAt(gameFolder(context.home()), context.now());
|
|
29
|
+
if (current === undefined) {
|
|
30
|
+
throw new Error("There is no game to describe: start one with new_game.");
|
|
31
|
+
}
|
|
32
|
+
refuseUnseen(current, "strategy", "position");
|
|
33
|
+
return current.game.profile;
|
|
34
|
+
},
|
|
35
|
+
format: (described) => [...(described.where === undefined ? [] : [described.where]), ...profileLines(described)].join("\n"),
|
|
36
|
+
});
|
|
37
|
+
/** The profile as lines for the AI, White's facts before Black's; a line with nothing to say is left out. */
|
|
38
|
+
export function profileLines(profile) {
|
|
39
|
+
const sides = [["white", profile.white], ["black", profile.black]];
|
|
40
|
+
const perSide = (line) => sides.map(([side, facts]) => {
|
|
41
|
+
const text = line(facts);
|
|
42
|
+
return text === undefined ? undefined : `${colourName(side)} ${text}`;
|
|
43
|
+
}).filter((text) => text !== undefined).join("; ");
|
|
44
|
+
const lines = [
|
|
45
|
+
`Phase: ${profile.phase}`,
|
|
46
|
+
`Material: ${materialText(profile.material)}`,
|
|
47
|
+
...sides.map(([side, facts]) => pawnLine(side, facts)),
|
|
48
|
+
filesLine(profile),
|
|
49
|
+
seventhLine(perSide((side) => joined(side.onSeventh))),
|
|
50
|
+
bishopsLine(profile),
|
|
51
|
+
`Outposts: ${perSide((side) => joined(side.outposts.map((outpost) => (outpost.piece === undefined ? outpost.square : `${outpost.piece}${outpost.square}`)))) || "none"}`,
|
|
52
|
+
`Centre (White/Black pieces reaching it): ${profile.centre.map(centreText).join(", ")}`,
|
|
53
|
+
`Space (squares of the other half attacked): White ${profile.white.space}, Black ${profile.black.space}`,
|
|
54
|
+
`Control: only White ${profile.controlled.white}, only Black ${profile.controlled.black}, both ${profile.controlled.contested}`,
|
|
55
|
+
`Mobility: ${perSide((side) => (side.mobility === undefined ? undefined : mobilityText(side.mobility)))}`,
|
|
56
|
+
`Development: ${perSide(developmentText)}`,
|
|
57
|
+
...sides.flatMap(([side, facts]) => (facts.king === undefined ? [] : [kingLine(side, facts.king)])),
|
|
58
|
+
drawLine(profile),
|
|
59
|
+
...endingLines(profile),
|
|
60
|
+
];
|
|
61
|
+
return lines.filter((line) => line !== "");
|
|
62
|
+
}
|
|
63
|
+
const pieceOrder = ["K", "Q", "R", "B", "N", "P"];
|
|
64
|
+
function pawnLine(side, { pawns }) {
|
|
65
|
+
const kinds = [["isolated", pawns.isolated], ["doubled", pawns.doubled], ["backward", pawns.backward], ["passed", pawns.passed]];
|
|
66
|
+
const named = kinds.filter(([, squares]) => squares.length > 0).map(([kind, squares]) => `${kind} ${squares.join(" ")}`);
|
|
67
|
+
return `${colourName(side)} pawns: ${[...named, `islands ${pawns.islands.join(", ") || "none"}`].join("; ")}`;
|
|
68
|
+
}
|
|
69
|
+
function filesLine(profile) {
|
|
70
|
+
const parts = [
|
|
71
|
+
profile.openFiles.length === 0 ? "" : `open ${profile.openFiles.join(" ")}`,
|
|
72
|
+
...["white", "black"].map((side) => {
|
|
73
|
+
const files = profile[side].halfOpenFiles;
|
|
74
|
+
return files.length === 0 ? "" : `half-open for ${colourName(side)} ${files.join(" ")}`;
|
|
75
|
+
}),
|
|
76
|
+
...["white", "black"].map((side) => {
|
|
77
|
+
const heavy = profile[side].heavyOnFiles;
|
|
78
|
+
return heavy.length === 0 ? "" : `${colourName(side)} heavy pieces on them ${heavy.join(" ")}`;
|
|
79
|
+
}),
|
|
80
|
+
].filter((part) => part !== "");
|
|
81
|
+
return `Files: ${parts.join("; ") || "no open or half-open file"}`;
|
|
82
|
+
}
|
|
83
|
+
function seventhLine(pieces) {
|
|
84
|
+
return pieces === "" ? "" : `Rooks and queens on the seventh rank (the second for Black): ${pieces}`;
|
|
85
|
+
}
|
|
86
|
+
function bishopsLine(profile) {
|
|
87
|
+
const parts = ["white", "black"].flatMap((side) => {
|
|
88
|
+
const facts = profile[side];
|
|
89
|
+
const each = facts.bishops.map((bishop) => `B${bishop.square} ${bishop.pawnsOnColour}/${bishop.pawns}`);
|
|
90
|
+
return each.length === 0 ? [] : [`${colourName(side)}${facts.bishopPair ? " (pair)" : ""} ${each.join(", ")}`];
|
|
91
|
+
});
|
|
92
|
+
return parts.length === 0 ? "" : `Bishops (own pawns on its colour/all): ${parts.join("; ")}${profile.oppositeBishops ? "; opposite-coloured" : ""}`;
|
|
93
|
+
}
|
|
94
|
+
function centreText({ square, piece, white, black }) {
|
|
95
|
+
const on = piece === undefined ? "" : ` ${colourName(piece.side)} ${piece.letter}`;
|
|
96
|
+
return `${square}${on} ${white}/${black}`;
|
|
97
|
+
}
|
|
98
|
+
function mobilityText(mobility) {
|
|
99
|
+
const total = Object.values(mobility).reduce((sum, count) => sum + count, 0);
|
|
100
|
+
return total === 0 ? "0" : `${total} (${pieceOrder.filter((letter) => mobility[letter] !== undefined).map((letter) => `${letter}${mobility[letter]}`).join(" ")})`;
|
|
101
|
+
}
|
|
102
|
+
function developmentText(side) {
|
|
103
|
+
const castled = side.castled === undefined ? "" : side.castled === "no" ? "not castled" : `castled ${side.castled}`;
|
|
104
|
+
const rights = side.castlingRights.length === 0 ? "" : `may castle ${side.castlingRights.join(" ")}`;
|
|
105
|
+
const home = side.undeveloped.length === 0 ? "" : `at home ${side.undeveloped.join(" ")}`;
|
|
106
|
+
return [castled, rights, home].filter((part) => part !== "").join(", ") || "nothing to say";
|
|
107
|
+
}
|
|
108
|
+
function kingLine(side, king) {
|
|
109
|
+
const files = king.openFiles.length === 0 ? "" : `, no own pawn on ${king.openFiles.join(" ")}`;
|
|
110
|
+
return `${colourName(side)} K${king.square}: shield ${king.shield.join(" ") || "none"}${files}, enemy attacks around it ${king.attacksNear}`;
|
|
111
|
+
}
|
|
112
|
+
function drawLine({ draw }) {
|
|
113
|
+
const parts = [
|
|
114
|
+
draw.repetitions > 1 ? `position seen ${draw.repetitions} times` : "",
|
|
115
|
+
draw.fiftyMovesLeft <= 20 ? `${draw.fiftyMovesLeft} half-moves left before the fifty-move draw` : "",
|
|
116
|
+
draw.insufficient ? "material insufficient to mate" : "",
|
|
117
|
+
].filter((part) => part !== "");
|
|
118
|
+
return parts.length === 0 ? "" : `Draw rules: ${parts.join("; ")}`;
|
|
119
|
+
}
|
|
120
|
+
function endingLines({ pawnEnding }) {
|
|
121
|
+
if (pawnEnding === undefined) {
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
const squares = pawnEnding.squares.map((square) => `${square.pawn} ${square.caught ? "inside" : "outside"} the enemy king's square`);
|
|
125
|
+
const opposition = pawnEnding.opposition === undefined ? "nobody" : colourName(pawnEnding.opposition);
|
|
126
|
+
return [`Pawn ending: ${[...squares, `opposition: ${opposition}`].join("; ")}`];
|
|
127
|
+
}
|
|
128
|
+
function joined(items) {
|
|
129
|
+
return items.length === 0 ? undefined : items.join(" ");
|
|
130
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type DrawnAvatar, type PlayerProfile } from "../profiles/index.ts";
|
|
3
|
+
import type { Context } from "./context.ts";
|
|
4
|
+
/** A change of profile refused while a game goes on: the page says it in the player's language. */
|
|
5
|
+
export declare class GameGoingOn extends Error {
|
|
6
|
+
constructor();
|
|
7
|
+
}
|
|
8
|
+
/** The profiles, the one in use first. */
|
|
9
|
+
export declare const profiles: import("./registry.ts").Command<z.ZodObject<{}, z.core.$strip>, {
|
|
10
|
+
profiles: PlayerProfile[];
|
|
11
|
+
active: string;
|
|
12
|
+
}>;
|
|
13
|
+
/**
|
|
14
|
+
* A new profile, put in use unless asked not to; while a game goes on it is made all the same and waits, the player in use staying. The one Chesspal made at
|
|
15
|
+
* the first start, while new, becomes it, game and all (D-71).
|
|
16
|
+
*/
|
|
17
|
+
export declare const newProfile: import("./registry.ts").Command<z.ZodObject<{
|
|
18
|
+
name: z.ZodString;
|
|
19
|
+
level: z.ZodOptional<z.ZodEnum<{
|
|
20
|
+
learning: "learning";
|
|
21
|
+
casual: "casual";
|
|
22
|
+
regular: "regular";
|
|
23
|
+
tournament: "tournament";
|
|
24
|
+
}>>;
|
|
25
|
+
rating: z.ZodOptional<z.ZodInt>;
|
|
26
|
+
site: z.ZodOptional<z.ZodEnum<{
|
|
27
|
+
lichess: "lichess";
|
|
28
|
+
"chess.com": "chess.com";
|
|
29
|
+
fide: "fide";
|
|
30
|
+
}>>;
|
|
31
|
+
avatar: z.ZodOptional<z.ZodEnum<{
|
|
32
|
+
"short-hair": "short-hair";
|
|
33
|
+
bun: "bun";
|
|
34
|
+
glasses: "glasses";
|
|
35
|
+
beanie: "beanie";
|
|
36
|
+
beard: "beard";
|
|
37
|
+
curls: "curls";
|
|
38
|
+
moustache: "moustache";
|
|
39
|
+
"silver-bob": "silver-bob";
|
|
40
|
+
cat: "cat";
|
|
41
|
+
owl: "owl";
|
|
42
|
+
}>>;
|
|
43
|
+
use: z.ZodOptional<z.ZodBoolean>;
|
|
44
|
+
}, z.core.$strip>, PlayerProfile & {
|
|
45
|
+
readonly inUse: boolean;
|
|
46
|
+
}>;
|
|
47
|
+
/** Another profile put in use, once the game going on has ended. */
|
|
48
|
+
export declare const useProfile: import("./registry.ts").Command<z.ZodObject<{
|
|
49
|
+
profile: z.ZodString;
|
|
50
|
+
}, z.core.$strip>, PlayerProfile>;
|
|
51
|
+
/** The picture of the profile in use: a drawn avatar, or an image of the player's, which replaces any before. */
|
|
52
|
+
export declare const setAvatar: import("./registry.ts").Command<z.ZodObject<{
|
|
53
|
+
avatar: z.ZodString;
|
|
54
|
+
}, z.core.$strip>, {
|
|
55
|
+
profile: string;
|
|
56
|
+
avatar: string;
|
|
57
|
+
}>;
|
|
58
|
+
/** The player's names on Lichess and chess.com, by which the games they load are theirs (D-93): each one given is written, an empty one goes. */
|
|
59
|
+
export declare const setAccounts: import("./registry.ts").Command<z.ZodObject<{
|
|
60
|
+
lichess: z.ZodOptional<z.ZodString>;
|
|
61
|
+
chesscom: z.ZodOptional<z.ZodString>;
|
|
62
|
+
}, z.core.$strip>, PlayerProfile>;
|
|
63
|
+
/** A drawn avatar chosen for a profile: the player's own image goes, or it would still show. */
|
|
64
|
+
export declare function chooseDrawnAvatar(context: Context, id: string, avatar: DrawnAvatar): void;
|
|
65
|
+
/**
|
|
66
|
+
* Who plays, as the AI reads it in status and new_game: their name, how they play and their rating; nothing while the profile is new, named after the
|
|
67
|
+
* system's account until the player completes it (D-71).
|
|
68
|
+
*/
|
|
69
|
+
export declare function playerLine(context: Context): string | undefined;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// The players' profiles (D-23), on the command line and the board page: listed, made, put in use, with their picture (D-45). The AI reads only who plays.
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { endOf } from "../games/index.js";
|
|
6
|
+
import { activateProfile, activeProfile, createProfile, drawnAvatars, gameFolder, levels, listProfiles, nameLimit, ratingLimits, ratingSites, removeOwnImage, saveOwnImage, updateProfile, } from "../profiles/index.js";
|
|
7
|
+
import { gameAt } from "./clock.js";
|
|
8
|
+
import { defineCommand } from "./registry.js";
|
|
9
|
+
/** A change of profile refused while a game goes on: the page says it in the player's language. */
|
|
10
|
+
export class GameGoingOn extends Error {
|
|
11
|
+
constructor() {
|
|
12
|
+
super("A game is going on in this profile: finish it, or resign, before changing profile. Nothing changed.");
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
/** The profiles, the one in use first. */
|
|
16
|
+
export const profiles = defineCommand({
|
|
17
|
+
name: "profiles",
|
|
18
|
+
description: "Lists the players' profiles, the one in use marked with *. Example: chesspal profiles.",
|
|
19
|
+
input: z.object({}),
|
|
20
|
+
cliOnly: true,
|
|
21
|
+
readOnly: true,
|
|
22
|
+
idempotent: true,
|
|
23
|
+
run: (input, context) => ({ profiles: listProfiles(context.home()), active: activeProfile(context.home()).id }),
|
|
24
|
+
format: ({ profiles, active }) => profiles.map((profile) => `${profile.id === active ? "*" : " "} ${profile.id}: ${playerText(profile)}`).join("\n"),
|
|
25
|
+
});
|
|
26
|
+
/**
|
|
27
|
+
* A new profile, put in use unless asked not to; while a game goes on it is made all the same and waits, the player in use staying. The one Chesspal made at
|
|
28
|
+
* the first start, while new, becomes it, game and all (D-71).
|
|
29
|
+
*/
|
|
30
|
+
export const newProfile = defineCommand({
|
|
31
|
+
name: "new_profile",
|
|
32
|
+
description: "Makes a player's profile and puts it in use, unless --use false or a game is going on: their games, picture and page settings go in "
|
|
33
|
+
+ "profiles/<id>/ of Chesspal's data folder; the profile Chesspal made at the first start becomes this one, its folder renamed, while nobody has completed "
|
|
34
|
+
+ "it. Example: chesspal new_profile Anna --level regular --rating 1450 --site lichess.",
|
|
35
|
+
input: z.object({
|
|
36
|
+
name: z.string().trim().min(1).max(nameLimit).regex(/^[^\p{Cc}]+$/u, "one line of text").describe("the player's name"),
|
|
37
|
+
level: z.enum(levels).optional().describe("how they play: learning, casual, regular or tournament"),
|
|
38
|
+
rating: z.int().min(ratingLimits.lowest).max(ratingLimits.highest).optional().describe("their rating, with site"),
|
|
39
|
+
site: z.enum(ratingSites).optional().describe("where the rating comes from: lichess, chess.com or fide"),
|
|
40
|
+
avatar: z.enum(drawnAvatars).optional().describe(`a drawn avatar: ${drawnAvatars.join(", ")}`),
|
|
41
|
+
use: z.boolean().optional().describe("false to make it without putting it in use"),
|
|
42
|
+
}),
|
|
43
|
+
cliOnly: true,
|
|
44
|
+
positionals: ["name"],
|
|
45
|
+
run: ({ name, level, rating, site, avatar, use }, context) => {
|
|
46
|
+
if ((rating === undefined) !== (site === undefined)) {
|
|
47
|
+
throw new Error("Give the rating and its site together, such as --rating 1450 --site lichess, or neither. Nothing changed.");
|
|
48
|
+
}
|
|
49
|
+
const first = activeProfile(context.home()).new === true;
|
|
50
|
+
const inUse = first || (use !== false && !isPlaying(context));
|
|
51
|
+
const profile = createProfile(context.home(), {
|
|
52
|
+
name,
|
|
53
|
+
...(level === undefined ? {} : { level }),
|
|
54
|
+
...(rating === undefined || site === undefined ? {} : { rating: { value: rating, site } }),
|
|
55
|
+
...(avatar === undefined ? {} : { avatar }),
|
|
56
|
+
}, inUse);
|
|
57
|
+
return { ...profile, inUse };
|
|
58
|
+
},
|
|
59
|
+
format: (profile) => `Profile ${profile.id} made${profile.inUse ? ", and in use" : ", not in use"}: ${playerText(profile)}.`,
|
|
60
|
+
});
|
|
61
|
+
/** Another profile put in use, once the game going on has ended. */
|
|
62
|
+
export const useProfile = defineCommand({
|
|
63
|
+
name: "use_profile",
|
|
64
|
+
description: "Puts another player's profile in use, with its games; refused while a game is going on. Example: chesspal use_profile anna.",
|
|
65
|
+
input: z.object({ profile: z.string().min(1).describe("the profile, as chesspal profiles lists it") }),
|
|
66
|
+
cliOnly: true,
|
|
67
|
+
idempotent: true,
|
|
68
|
+
positionals: ["profile"],
|
|
69
|
+
run: ({ profile }, context) => {
|
|
70
|
+
const active = activeProfile(context.home());
|
|
71
|
+
if (active.id === profile) {
|
|
72
|
+
return active;
|
|
73
|
+
}
|
|
74
|
+
refuseWhilePlaying(context);
|
|
75
|
+
return activateProfile(context.home(), profile);
|
|
76
|
+
},
|
|
77
|
+
format: (profile) => `Profile ${profile.id} in use: ${playerText(profile)}.`,
|
|
78
|
+
});
|
|
79
|
+
/** The picture of the profile in use: a drawn avatar, or an image of the player's, which replaces any before. */
|
|
80
|
+
export const setAvatar = defineCommand({
|
|
81
|
+
name: "set_avatar",
|
|
82
|
+
description: `Sets the picture of the profile in use: a drawn avatar (${drawnAvatars.join(", ")}) or the path of a PNG, JPEG or WebP image of `
|
|
83
|
+
+ "1 MB at most. Example: chesspal set_avatar owl.",
|
|
84
|
+
input: z.object({ avatar: z.string().min(1).describe("a drawn avatar, or the path of an image") }),
|
|
85
|
+
cliOnly: true,
|
|
86
|
+
idempotent: true,
|
|
87
|
+
positionals: ["avatar"],
|
|
88
|
+
run: ({ avatar }, context) => {
|
|
89
|
+
const profile = activeProfile(context.home());
|
|
90
|
+
const drawn = drawnAvatars.find((one) => one === avatar);
|
|
91
|
+
if (drawn === undefined) {
|
|
92
|
+
saveOwnImage(context.home(), profile.id, imageFile(resolve(context.folder, avatar)));
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
chooseDrawnAvatar(context, profile.id, drawn);
|
|
96
|
+
}
|
|
97
|
+
return { profile: profile.id, avatar };
|
|
98
|
+
},
|
|
99
|
+
format: ({ profile, avatar }) => `The picture of ${profile} is ${avatar}.`,
|
|
100
|
+
});
|
|
101
|
+
/** The player's names on Lichess and chess.com, by which the games they load are theirs (D-93): each one given is written, an empty one goes. */
|
|
102
|
+
export const setAccounts = defineCommand({
|
|
103
|
+
name: "set_accounts",
|
|
104
|
+
description: "Sets the player's names on Lichess and chess.com in the profile in use, by which a game loaded from there is theirs; an empty name removes it. "
|
|
105
|
+
+ "Example: chesspal set_accounts --lichess anna_k --chesscom AnnaK.",
|
|
106
|
+
input: z.object({
|
|
107
|
+
lichess: accountName().optional().describe("the player's name on Lichess"),
|
|
108
|
+
chesscom: accountName().optional().describe("the player's name on chess.com"),
|
|
109
|
+
}),
|
|
110
|
+
cliOnly: true,
|
|
111
|
+
idempotent: true,
|
|
112
|
+
run: (names, context) => {
|
|
113
|
+
const profile = activeProfile(context.home());
|
|
114
|
+
const lichess = names.lichess ?? profile.accounts?.lichess ?? "";
|
|
115
|
+
const chesscom = names.chesscom ?? profile.accounts?.chesscom ?? "";
|
|
116
|
+
return updateProfile(context.home(), profile.id, { accounts: { ...(lichess === "" ? {} : { lichess }), ...(chesscom === "" ? {} : { chesscom }) } });
|
|
117
|
+
},
|
|
118
|
+
format: (profile) => `${profile.id} is ${accountsText(profile.accounts)}.`,
|
|
119
|
+
});
|
|
120
|
+
/** A drawn avatar chosen for a profile: the player's own image goes, or it would still show. */
|
|
121
|
+
export function chooseDrawnAvatar(context, id, avatar) {
|
|
122
|
+
updateProfile(context.home(), id, { avatar });
|
|
123
|
+
removeOwnImage(context.home(), id);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Who plays, as the AI reads it in status and new_game: their name, how they play and their rating; nothing while the profile is new, named after the
|
|
127
|
+
* system's account until the player completes it (D-71).
|
|
128
|
+
*/
|
|
129
|
+
export function playerLine(context) {
|
|
130
|
+
const profile = activeProfile(context.home());
|
|
131
|
+
return profile.new === true ? undefined : `Player: ${playerText(profile)}.`;
|
|
132
|
+
}
|
|
133
|
+
/** Throws while a game goes on in the profile in use. */
|
|
134
|
+
function refuseWhilePlaying(context) {
|
|
135
|
+
if (isPlaying(context)) {
|
|
136
|
+
throw new GameGoingOn();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
function isPlaying(context) {
|
|
140
|
+
const current = gameAt(gameFolder(context.home()), context.now());
|
|
141
|
+
return current !== undefined && endOf(current.game) === undefined;
|
|
142
|
+
}
|
|
143
|
+
const levelText = { learning: "has just learned", casual: "plays now and then", regular: "plays often", tournament: "plays tournaments" };
|
|
144
|
+
const siteText = { "lichess": "Lichess", "chess.com": "Chess.com", "fide": "FIDE" };
|
|
145
|
+
function playerText({ name, level, rating }) {
|
|
146
|
+
return [name, ...(level === undefined ? [] : [levelText[level]]), ...(rating === undefined ? [] : [`${siteText[rating.site]} ${rating.value}`])].join(", ");
|
|
147
|
+
}
|
|
148
|
+
/** A name on Lichess or chess.com: their letters, digits, underscores and hyphens; empty to remove it. */
|
|
149
|
+
function accountName() {
|
|
150
|
+
return z.string().trim().max(nameLimit).regex(/^[\w-]*$/, "A name on Lichess or chess.com has only letters, digits, underscores and hyphens.");
|
|
151
|
+
}
|
|
152
|
+
function accountsText(accounts) {
|
|
153
|
+
const named = [...(accounts?.lichess === undefined ? [] : [`${accounts.lichess} on Lichess`]), ...(accounts?.chesscom === undefined ? [] : [`${accounts.chesscom} on chess.com`])];
|
|
154
|
+
return named.length === 0 ? "on neither Lichess nor chess.com" : named.join(" and ");
|
|
155
|
+
}
|
|
156
|
+
function imageFile(path) {
|
|
157
|
+
try {
|
|
158
|
+
return readFileSync(path);
|
|
159
|
+
}
|
|
160
|
+
catch (error) {
|
|
161
|
+
throw new Error(`${path} cannot be read (${error.message}): give a drawn avatar's name, or the path of an image. Nothing changed.`, { cause: error });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { Context } from "./context.ts";
|
|
3
|
+
/** A command is defined once: the command line and the MCP server are generated from it (D-5); the page's routes call the same commands. */
|
|
4
|
+
export interface Command<Schema extends z.ZodObject = z.ZodObject, Result = unknown> {
|
|
5
|
+
name: string;
|
|
6
|
+
/** Written for an AI: what the command does (the first sentence, shown in the list of commands), when to use it, an example. */
|
|
7
|
+
description: string;
|
|
8
|
+
input: Schema;
|
|
9
|
+
/** The command changes nothing: hosts may run it without asking. */
|
|
10
|
+
readOnly?: boolean;
|
|
11
|
+
/** The command deletes or overwrites what the player keeps, such as a game: hosts may ask first. */
|
|
12
|
+
destructive?: boolean;
|
|
13
|
+
/** Calling the command again with the same input changes nothing more. */
|
|
14
|
+
idempotent?: boolean;
|
|
15
|
+
/** Hosts load the tool at once instead of on demand: for the ones the AI calls at the start of every chat. */
|
|
16
|
+
alwaysLoad?: boolean;
|
|
17
|
+
/** Its answer shows the chat the game as it is: the chat may move on it, and on no other (D-71). */
|
|
18
|
+
showsGame?: boolean;
|
|
19
|
+
/**
|
|
20
|
+
* The agents that call it apart from the chat, as ponder (D-72), by their names: the doorbell's record of what the chat has read stays as it was, and the
|
|
21
|
+
* server refuses it to the chat and to other agents (Priya).
|
|
22
|
+
*/
|
|
23
|
+
byAgents?: readonly string[];
|
|
24
|
+
/** The agents that call it besides the chat, as the analyst analyse_game (D-89): their calls leave the doorbell's record of what the chat has read as it was. */
|
|
25
|
+
sharedWith?: readonly string[];
|
|
26
|
+
/** The only host the MCP server offers it to, by the client's name, as ponder to Claude Code: elsewhere its description would cost tokens for nothing. */
|
|
27
|
+
host?: string;
|
|
28
|
+
/** Only the command line has it, as init: the MCP server leaves it out, and its description costs the AI no tokens. */
|
|
29
|
+
cliOnly?: boolean;
|
|
30
|
+
/** Fields the command line takes as positional arguments, in order. */
|
|
31
|
+
positionals?: Extract<keyof Schema["shape"], string>[];
|
|
32
|
+
/** Returns a value, never prints: the MCP server speaks on stdout, and only the command line writes there. */
|
|
33
|
+
run(input: z.infer<Schema>, context: Context): Result | Promise<Result>;
|
|
34
|
+
/** Plain-text rendering of the result, for the AI and the terminal alike; the context, for a command that speaks to the player, as init. */
|
|
35
|
+
format(result: Result, context?: Context): string;
|
|
36
|
+
}
|
|
37
|
+
export type AnyCommand = Command<any, any>;
|
|
38
|
+
/** Checks a command's types where it is written; the command is used as it is. */
|
|
39
|
+
export declare function defineCommand<Schema extends z.ZodObject, Result>(command: Command<Schema, Result>): Command<Schema, Result>;
|
|
40
|
+
/** One sentence of the AI's, kept in a PGN comment: no brackets or braces, which would break it. */
|
|
41
|
+
export declare function sentence(most: number): z.ZodString;
|
|
42
|
+
/**
|
|
43
|
+
* Why an input is refused, in words an AI can act on, the same on the command line and in MCP: one sentence for each problem, each parameter written as
|
|
44
|
+
* the caller writes it (colour, --colour).
|
|
45
|
+
*/
|
|
46
|
+
export declare function inputProblems(command: string, issues: readonly z.core.$ZodIssue[], given: Record<string, unknown>, written: (name: string) => string): string;
|
|
47
|
+
/**
|
|
48
|
+
* What keeps a field of a command's input from being given on the command line: one named no-x, read as turning x off, or json, read as answering in JSON;
|
|
49
|
+
* undefined for any other. A positional is never an option, so json is its name freely.
|
|
50
|
+
*/
|
|
51
|
+
export declare function commandLineFieldProblem(command: string, name: string, positional: boolean): string | undefined;
|
|
52
|
+
/** Whether a field of a command's input is a flag, true or false, as --hidden. */
|
|
53
|
+
export declare function isBoolean(field: z.ZodType): boolean;
|
|
54
|
+
/** A list option can be given more than once, as in --tag a --tag b. */
|
|
55
|
+
export declare function isList(field: z.ZodType): boolean;
|
|
56
|
+
/** The type of a field of a command's input, past optional, default and nullable: string, boolean, array… */
|
|
57
|
+
export declare function baseType(field: z.ZodType): string;
|
|
58
|
+
/** Whether a field takes numbers, alone or in a list: the command line reads them as text and turns them into numbers. */
|
|
59
|
+
export declare function takesNumbers(field: z.ZodType): boolean;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
// What a command is, and the reading of its input schema that the command line and the MCP server share.
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
/** Checks a command's types where it is written; the command is used as it is. */
|
|
4
|
+
export function defineCommand(command) {
|
|
5
|
+
return command;
|
|
6
|
+
}
|
|
7
|
+
/** One sentence of the AI's, kept in a PGN comment: no brackets or braces, which would break it. */
|
|
8
|
+
export function sentence(most) {
|
|
9
|
+
return z.string().trim().min(1, "write one sentence").max(most, `write one sentence of ${most} characters at most`).regex(/^[^[\]{}]*$/, "write it without [ ] { }");
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Why an input is refused, in words an AI can act on, the same on the command line and in MCP: one sentence for each problem, each parameter written as
|
|
13
|
+
* the caller writes it (colour, --colour).
|
|
14
|
+
*/
|
|
15
|
+
export function inputProblems(command, issues, given, written) {
|
|
16
|
+
return issues.map((issue) => inputIssue(command, issue, given, written)).join(" ");
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* What keeps a field of a command's input from being given on the command line: one named no-x, read as turning x off, or json, read as answering in JSON;
|
|
20
|
+
* undefined for any other. A positional is never an option, so json is its name freely.
|
|
21
|
+
*/
|
|
22
|
+
export function commandLineFieldProblem(command, name, positional) {
|
|
23
|
+
if (name.startsWith("no-")) {
|
|
24
|
+
return `${command} has a field named ${name}, and none may start with no-, which the command line reads as turning ${name.slice(3)} off: rename it.`;
|
|
25
|
+
}
|
|
26
|
+
return name === "json" && !positional ? `${command} has a field named json, which the command line reads as answering in JSON: rename it.` : undefined;
|
|
27
|
+
}
|
|
28
|
+
/** Whether a field of a command's input is a flag, true or false, as --hidden. */
|
|
29
|
+
export function isBoolean(field) {
|
|
30
|
+
return baseDefinition(field).type === "boolean";
|
|
31
|
+
}
|
|
32
|
+
/** A list option can be given more than once, as in --tag a --tag b. */
|
|
33
|
+
export function isList(field) {
|
|
34
|
+
return baseDefinition(field).type === "array";
|
|
35
|
+
}
|
|
36
|
+
/** The type of a field of a command's input, past optional, default and nullable: string, boolean, array… */
|
|
37
|
+
export function baseType(field) {
|
|
38
|
+
return baseDefinition(field).type;
|
|
39
|
+
}
|
|
40
|
+
/** Whether a field takes numbers, alone or in a list: the command line reads them as text and turns them into numbers. */
|
|
41
|
+
export function takesNumbers(field) {
|
|
42
|
+
const definition = baseDefinition(field);
|
|
43
|
+
return definition.type === "number" || (definition.type === "array" && definition.element !== undefined && baseType(definition.element) === "number");
|
|
44
|
+
}
|
|
45
|
+
function inputIssue(command, issue, given, written) {
|
|
46
|
+
const path = issue.path.map(String).join(".");
|
|
47
|
+
if (issue.code === "unrecognized_keys") {
|
|
48
|
+
return path === "" ? `${command} has no parameter ${issue.keys.map(written).join(", ")}.` : `${written(path)} has no ${issue.keys.join(", ")}.`;
|
|
49
|
+
}
|
|
50
|
+
// A parameter left out, whatever zod calls it.
|
|
51
|
+
if (issue.path.length === 1 && given[path] === undefined) {
|
|
52
|
+
return `${command} needs ${written(path)}.`;
|
|
53
|
+
}
|
|
54
|
+
return path === "" ? `${issue.message}.` : `${written(path)}: ${issue.message}.`;
|
|
55
|
+
}
|
|
56
|
+
function baseDefinition(field) {
|
|
57
|
+
let definition = field.def;
|
|
58
|
+
while (definition.innerType && ["optional", "default", "nullable"].includes(definition.type)) {
|
|
59
|
+
definition = definition.innerType.def;
|
|
60
|
+
}
|
|
61
|
+
return definition;
|
|
62
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type Mood } from "../core/index.ts";
|
|
2
|
+
import { type Sayer, type Talk, type Words } from "../games/index.ts";
|
|
3
|
+
import type { Context } from "./context.ts";
|
|
4
|
+
/**
|
|
5
|
+
* Adds a line to the talk of the current game, going on or just over, and saves it; the AI's mood once the game is over, which the archive keeps too (D-70).
|
|
6
|
+
* With no game the line goes in the talk before it (D-77). A line refused changes nothing: it throws, in words for whoever spoke.
|
|
7
|
+
*/
|
|
8
|
+
export declare function say(folder: string, who: Sayer, text: string, at: Date, mood?: Mood): Talk;
|
|
9
|
+
/** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once; never to an agent (D-89). */
|
|
10
|
+
export declare function newWords(context: Context): string[];
|
|
11
|
+
/** Every word of the player's the AI has not answered, given again: status gives them all, for a chat that lost them in a summary. */
|
|
12
|
+
export declare function unansweredWords(context: Context): string[];
|
|
13
|
+
/** What the player wrote on the board page that the AI has not been given in this chat, still to give: the AI's move would count as its answer. */
|
|
14
|
+
export declare function waitingWords(context: Context, given?: ReadonlySet<string>): Words[];
|
|
15
|
+
/** Gives the words to the AI: from now on they count as given in this chat. Their text. */
|
|
16
|
+
export declare function give(context: Context, words: readonly Words[]): string[];
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// The talk on the board page (D-51): a line of the player's or the AI's, saved with the game, and the player's words the AI has not been given yet.
|
|
2
|
+
import { endWithMood } from "../core/index.js";
|
|
3
|
+
import { archiveEnded, endOf, loadGame, loadTableTalk, saveGame, saveTableTalk, talkLimits, talkLine, unheardWords } from "../games/index.js";
|
|
4
|
+
import { gameFolder, helpsIn } from "../profiles/index.js";
|
|
5
|
+
import { gameAt } from "./clock.js";
|
|
6
|
+
/**
|
|
7
|
+
* Adds a line to the talk of the current game, going on or just over, and saves it; the AI's mood once the game is over, which the archive keeps too (D-70).
|
|
8
|
+
* With no game the line goes in the talk before it (D-77). A line refused changes nothing: it throws, in words for whoever spoke.
|
|
9
|
+
*/
|
|
10
|
+
export function say(folder, who, text, at, mood) {
|
|
11
|
+
const current = gameAt(folder, at);
|
|
12
|
+
if (current === undefined) {
|
|
13
|
+
return sayAtTable(folder, who, text, at, mood);
|
|
14
|
+
}
|
|
15
|
+
if (mood !== undefined && endOf(current.game) === undefined) {
|
|
16
|
+
throw new Error("While the game goes on your mood comes with ai_move: say it without mood. Nothing changed.");
|
|
17
|
+
}
|
|
18
|
+
// With the helps on the AI writes as long as the analyst (D-78).
|
|
19
|
+
current.talk.push(talkLine(current, who, text, at, undefined, aiMost(folder, who, text)));
|
|
20
|
+
if (mood !== undefined) {
|
|
21
|
+
endWithMood(current.game, mood);
|
|
22
|
+
}
|
|
23
|
+
saveGame(folder, current);
|
|
24
|
+
if (mood !== undefined) {
|
|
25
|
+
archiveEnded(folder, current);
|
|
26
|
+
}
|
|
27
|
+
return current;
|
|
28
|
+
}
|
|
29
|
+
/** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once; never to an agent (D-89). */
|
|
30
|
+
export function newWords(context) {
|
|
31
|
+
return context.agent === undefined ? give(context, waitingWords(context)) : [];
|
|
32
|
+
}
|
|
33
|
+
/** Every word of the player's the AI has not answered, given again: status gives them all, for a chat that lost them in a summary. */
|
|
34
|
+
export function unansweredWords(context) {
|
|
35
|
+
return give(context, waitingWords(context, new Set()));
|
|
36
|
+
}
|
|
37
|
+
/** What the player wrote on the board page that the AI has not been given in this chat, still to give: the AI's move would count as its answer. */
|
|
38
|
+
export function waitingWords(context, given = context.given) {
|
|
39
|
+
try {
|
|
40
|
+
const folder = gameFolder(context.home());
|
|
41
|
+
return unheardWords(loadGame(folder) ?? loadTableTalk(folder), given);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
// A game.json that cannot be read: the tool says so in its own words.
|
|
45
|
+
return [];
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Gives the words to the AI: from now on they count as given in this chat. Their text. */
|
|
49
|
+
export function give(context, words) {
|
|
50
|
+
const before = context.given.size;
|
|
51
|
+
for (const { key } of words) {
|
|
52
|
+
context.given.add(key);
|
|
53
|
+
}
|
|
54
|
+
if (context.given.size > before) {
|
|
55
|
+
context.heard?.();
|
|
56
|
+
}
|
|
57
|
+
return words.map(({ text }) => text);
|
|
58
|
+
}
|
|
59
|
+
/** The most a line of the AI's may hold in the games of `folder`: an analyst's note with the helps on (D-78); a longer one with them off is refused. */
|
|
60
|
+
function aiMost(folder, who, text) {
|
|
61
|
+
if (who !== "ai") {
|
|
62
|
+
return talkLimits.ai;
|
|
63
|
+
}
|
|
64
|
+
if (helpsIn(folder)) {
|
|
65
|
+
return talkLimits.aiHelps;
|
|
66
|
+
}
|
|
67
|
+
if (text.trim().length > talkLimits.ai) {
|
|
68
|
+
throw new Error(`The player has the helps off: write at most ${talkLimits.ai} characters. Nothing changed.`);
|
|
69
|
+
}
|
|
70
|
+
return talkLimits.ai;
|
|
71
|
+
}
|
|
72
|
+
/** A line of the talk with no game, which starts it the first time; no mood, which only the end of a game takes. */
|
|
73
|
+
function sayAtTable(folder, who, text, at, mood) {
|
|
74
|
+
if (who === "agent") {
|
|
75
|
+
throw new Error("There is no game to think on. Nothing changed.");
|
|
76
|
+
}
|
|
77
|
+
if (mood !== undefined) {
|
|
78
|
+
throw new Error("There is no game: a mood goes with a game's end. Say it without mood. Nothing changed.");
|
|
79
|
+
}
|
|
80
|
+
const before = loadTableTalk(folder);
|
|
81
|
+
const table = { started: before.started === "" ? at.toISOString() : before.started, talk: [...before.talk, talkLine(before, who, text, at, undefined, aiMost(folder, who, text))] };
|
|
82
|
+
saveTableTalk(folder, table);
|
|
83
|
+
return table;
|
|
84
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The player's language by its English name when it is not English. */
|
|
2
|
+
export interface Speech {
|
|
3
|
+
language: string;
|
|
4
|
+
}
|
|
5
|
+
/** The player's speech, from the page's settings or the system's language; undefined in English, which the AI speaks unless told. */
|
|
6
|
+
export declare function speechOf(folder: string, systemLanguage: string | undefined): Speech | undefined;
|
|
7
|
+
/** What the AI is told of the player's speech once a chat (Priya); the moves are SAN in every language (D-87). */
|
|
8
|
+
export declare function speechLine({ language }: Speech): string;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The player's language, which the AI is told once a chat when it is not English: the moves stay in SAN (D-87).
|
|
2
|
+
import { languageIn } from "../profiles/index.js";
|
|
3
|
+
/** The player's speech, from the page's settings or the system's language; undefined in English, which the AI speaks unless told. */
|
|
4
|
+
export function speechOf(folder, systemLanguage) {
|
|
5
|
+
const language = languageName(languageIn(folder, systemLanguage));
|
|
6
|
+
return language === undefined ? undefined : { language };
|
|
7
|
+
}
|
|
8
|
+
/** What the AI is told of the player's speech once a chat (Priya); the moves are SAN in every language (D-87). */
|
|
9
|
+
export function speechLine({ language }) {
|
|
10
|
+
return `The player speaks ${language}.`;
|
|
11
|
+
}
|
|
12
|
+
/** A language by its English name when it is not English (Italian), from the way the system or the page names it (it-IT, it). */
|
|
13
|
+
function languageName(language) {
|
|
14
|
+
const code = language?.split(/[-_]/)[0]?.toLowerCase() ?? "";
|
|
15
|
+
if (!/^[a-z]{2,3}$/.test(code) || code === "en" || code === "und") {
|
|
16
|
+
return undefined;
|
|
17
|
+
}
|
|
18
|
+
const name = new Intl.DisplayNames(["en"], { type: "language" }).of(code);
|
|
19
|
+
return name === undefined || name === code ? undefined : name;
|
|
20
|
+
}
|