chesspal 0.0.1 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +136 -2
- package/dist/cli/chat-hook.d.ts +2 -0
- package/dist/cli/chat-hook.js +33 -0
- package/dist/cli/cli.d.ts +2 -0
- package/dist/cli/cli.js +28 -0
- package/dist/cli/command-line.d.ts +8 -0
- package/dist/cli/command-line.js +178 -0
- package/dist/commands/ai-move.d.ts +37 -0
- package/dist/commands/ai-move.js +92 -0
- package/dist/commands/ai-say.d.ts +20 -0
- package/dist/commands/ai-say.js +40 -0
- package/dist/commands/analyse-game.d.ts +61 -0
- package/dist/commands/analyse-game.js +238 -0
- package/dist/commands/annotate-game.d.ts +35 -0
- package/dist/commands/annotate-game.js +78 -0
- package/dist/commands/clock.d.ts +15 -0
- package/dist/commands/clock.js +45 -0
- package/dist/commands/context.d.ts +56 -0
- package/dist/commands/context.js +36 -0
- package/dist/commands/draw-offer.d.ts +10 -0
- package/dist/commands/draw-offer.js +43 -0
- package/dist/commands/drawings.d.ts +15 -0
- package/dist/commands/drawings.js +23 -0
- package/dist/commands/end-game.d.ts +23 -0
- package/dist/commands/end-game.js +64 -0
- package/dist/commands/export-game.d.ts +14 -0
- package/dist/commands/export-game.js +26 -0
- package/dist/commands/game-summary.d.ts +95 -0
- package/dist/commands/game-summary.js +239 -0
- package/dist/commands/index.d.ts +3 -0
- package/dist/commands/index.js +19 -0
- package/dist/commands/init.d.ts +15 -0
- package/dist/commands/init.js +73 -0
- package/dist/commands/levels.d.ts +9 -0
- package/dist/commands/levels.js +25 -0
- package/dist/commands/look.d.ts +31 -0
- package/dist/commands/look.js +61 -0
- package/dist/commands/new-game.d.ts +69 -0
- package/dist/commands/new-game.js +215 -0
- package/dist/commands/opening-book.d.ts +13 -0
- package/dist/commands/opening-book.js +84 -0
- package/dist/commands/page-link.d.ts +11 -0
- package/dist/commands/page-link.js +28 -0
- package/dist/commands/past-games.d.ts +11 -0
- package/dist/commands/past-games.js +47 -0
- package/dist/commands/play-move.d.ts +44 -0
- package/dist/commands/play-move.js +102 -0
- package/dist/commands/player-move.d.ts +17 -0
- package/dist/commands/player-move.js +44 -0
- package/dist/commands/player-say.d.ts +5 -0
- package/dist/commands/player-say.js +16 -0
- package/dist/commands/ponder.d.ts +42 -0
- package/dist/commands/ponder.js +167 -0
- package/dist/commands/position.d.ts +14 -0
- package/dist/commands/position.js +128 -0
- package/dist/commands/profiles.d.ts +60 -0
- package/dist/commands/profiles.js +129 -0
- package/dist/commands/registry.d.ts +54 -0
- package/dist/commands/registry.js +61 -0
- package/dist/commands/say.d.ts +16 -0
- package/dist/commands/say.js +84 -0
- package/dist/commands/status.d.ts +29 -0
- package/dist/commands/status.js +103 -0
- package/dist/commands/study-games.d.ts +5 -0
- package/dist/commands/study-games.js +40 -0
- package/dist/commands/study.d.ts +38 -0
- package/dist/commands/study.js +122 -0
- package/dist/commands/view.d.ts +14 -0
- package/dist/commands/view.js +125 -0
- package/dist/core/board-text.d.ts +7 -0
- package/dist/core/board-text.js +18 -0
- package/dist/core/book.json +1 -0
- package/dist/core/clock.d.ts +45 -0
- package/dist/core/clock.js +109 -0
- package/dist/core/facts.d.ts +55 -0
- package/dist/core/facts.js +175 -0
- package/dist/core/game.d.ts +93 -0
- package/dist/core/game.js +348 -0
- package/dist/core/illegal.d.ts +19 -0
- package/dist/core/illegal.js +141 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +11 -0
- package/dist/core/levels.d.ts +20 -0
- package/dist/core/levels.js +33 -0
- package/dist/core/material.d.ts +9 -0
- package/dist/core/material.js +12 -0
- package/dist/core/moods.d.ts +23 -0
- package/dist/core/moods.js +45 -0
- package/dist/core/openings.d.ts +37 -0
- package/dist/core/openings.js +98 -0
- package/dist/core/openings.json +1 -0
- package/dist/core/pgn.d.ts +10 -0
- package/dist/core/pgn.js +213 -0
- package/dist/core/profile.d.ts +117 -0
- package/dist/core/profile.js +232 -0
- package/dist/core/record.d.ts +21 -0
- package/dist/core/record.js +25 -0
- package/dist/core/review.d.ts +74 -0
- package/dist/core/review.js +126 -0
- package/dist/games/ai-model.d.ts +5 -0
- package/dist/games/ai-model.js +15 -0
- package/dist/games/analysis.d.ts +12 -0
- package/dist/games/analysis.js +26 -0
- package/dist/games/archive.d.ts +42 -0
- package/dist/games/archive.js +168 -0
- package/dist/games/chat-start.d.ts +16 -0
- package/dist/games/chat-start.js +26 -0
- package/dist/games/current-game.d.ts +54 -0
- package/dist/games/current-game.js +128 -0
- package/dist/games/files.d.ts +9 -0
- package/dist/games/files.js +52 -0
- package/dist/games/index.d.ts +10 -0
- package/dist/games/index.js +11 -0
- package/dist/games/plans.d.ts +9 -0
- package/dist/games/plans.js +23 -0
- package/dist/games/table-talk.d.ts +12 -0
- package/dist/games/table-talk.js +25 -0
- package/dist/games/talk.d.ts +40 -0
- package/dist/games/talk.js +131 -0
- package/dist/games/thoughts.d.ts +15 -0
- package/dist/games/thoughts.js +60 -0
- package/dist/home/data-folder.d.ts +12 -0
- package/dist/home/data-folder.js +55 -0
- package/dist/home/index.d.ts +2 -0
- package/dist/home/index.js +3 -0
- package/dist/home/open-folder.d.ts +2 -0
- package/dist/home/open-folder.js +14 -0
- package/dist/host-files/host-files.d.ts +10 -0
- package/dist/host-files/host-files.js +167 -0
- package/dist/host-files/index.d.ts +2 -0
- package/dist/host-files/index.js +3 -0
- package/dist/host-files/ponder-agents.d.ts +12 -0
- package/dist/host-files/ponder-agents.js +40 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +2 -0
- package/dist/page/app.js +576 -0
- package/dist/page/avatars/beanie.webp +0 -0
- package/dist/page/avatars/beard.webp +0 -0
- package/dist/page/avatars/bun.webp +0 -0
- package/dist/page/avatars/cat.webp +0 -0
- package/dist/page/avatars/curls.webp +0 -0
- package/dist/page/avatars/glasses.webp +0 -0
- package/dist/page/avatars/moustache.webp +0 -0
- package/dist/page/avatars/owl.webp +0 -0
- package/dist/page/avatars/short-hair.webp +0 -0
- package/dist/page/avatars/silver-bob.webp +0 -0
- package/dist/page/avatars.js +16 -0
- package/dist/page/badges.js +50 -0
- package/dist/page/board.js +566 -0
- package/dist/page/browse.js +17 -0
- package/dist/page/clock-time.js +12 -0
- package/dist/page/dialog.js +26 -0
- package/dist/page/ending.js +73 -0
- package/dist/page/explain.js +58 -0
- package/dist/page/fold-tab.js +9 -0
- package/dist/page/folds.js +43 -0
- package/dist/page/game-column.js +44 -0
- package/dist/page/header.js +27 -0
- package/dist/page/helps-view.js +51 -0
- package/dist/page/helps.js +90 -0
- package/dist/page/main.js +8 -0
- package/dist/page/model-name.js +20 -0
- package/dist/page/move-list.js +68 -0
- package/dist/page/new-game-dialog.js +42 -0
- package/dist/page/opening-book.js +14 -0
- package/dist/page/pieces/CREDITS.md +36 -0
- package/dist/page/pieces/cburnett-small.svg +20 -0
- package/dist/page/pieces/cburnett.svg +20 -0
- package/dist/page/pieces/nitida-small.svg +19 -0
- package/dist/page/pieces/nitida.svg +20 -0
- package/dist/page/pieces/torretta-small.svg +19 -0
- package/dist/page/pieces/torretta.svg +19 -0
- package/dist/page/pieces.js +28 -0
- package/dist/page/player-card.js +60 -0
- package/dist/page/plies.js +62 -0
- package/dist/page/profile-dialogs.js +123 -0
- package/dist/page/profile.js +95 -0
- package/dist/page/settings-panel.js +32 -0
- package/dist/page/settings.js +76 -0
- package/dist/page/sounds/capture.ogg +0 -0
- package/dist/page/sounds/check.ogg +0 -0
- package/dist/page/sounds/draw.ogg +0 -0
- package/dist/page/sounds/flag.ogg +0 -0
- package/dist/page/sounds/mate.ogg +0 -0
- package/dist/page/sounds/move.ogg +0 -0
- package/dist/page/sounds/offer.ogg +0 -0
- package/dist/page/sounds/resign.ogg +0 -0
- package/dist/page/sounds.js +105 -0
- package/dist/page/studied.js +14 -0
- package/dist/page/study-dialog.js +58 -0
- package/dist/page/style.css +4398 -0
- package/dist/page/talk.js +172 -0
- package/dist/page/text.js +593 -0
- package/dist/page/torretta.js +38 -0
- package/dist/page/voice.js +38 -0
- package/dist/profiles/avatar.d.ts +23 -0
- package/dist/profiles/avatar.js +84 -0
- package/dist/profiles/front-matter.d.ts +9 -0
- package/dist/profiles/front-matter.js +33 -0
- package/dist/profiles/index.d.ts +3 -0
- package/dist/profiles/index.js +5 -0
- package/dist/profiles/profiles.d.ts +58 -0
- package/dist/profiles/profiles.js +237 -0
- package/dist/profiles/settings.d.ts +12 -0
- package/dist/profiles/settings.js +46 -0
- package/dist/server/analyses.d.ts +17 -0
- package/dist/server/analyses.js +46 -0
- package/dist/server/bell.d.ts +60 -0
- package/dist/server/bell.js +209 -0
- package/dist/server/caller.d.ts +10 -0
- package/dist/server/caller.js +99 -0
- package/dist/server/http.d.ts +16 -0
- package/dist/server/http.js +57 -0
- package/dist/server/mcp.d.ts +14 -0
- package/dist/server/mcp.js +179 -0
- package/dist/server/page.d.ts +46 -0
- package/dist/server/page.js +657 -0
- package/dist/server/profile-routes.d.ts +19 -0
- package/dist/server/profile-routes.js +186 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.js +3 -0
- package/instructions/analyst.md +8 -0
- package/instructions/opponent.md +15 -0
- package/instructions/ponder.md +9 -0
- package/package.json +66 -5
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
// What the AI draws on the board to show what it says, as the analyst (D-69) and the opponent with the helps on (D-78): arrows and squares, as PGN's
|
|
2
|
+
// %cal and %csl write them.
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { arrowOf, squareOf } from "../core/index.js";
|
|
5
|
+
/** The most a note of the AI's on a move may hold: a few sentences. */
|
|
6
|
+
export const noteLimit = 700;
|
|
7
|
+
// The most arrows, and the most squares, on one move.
|
|
8
|
+
const drawLimit = 6;
|
|
9
|
+
/** A note of the AI's on a move, as its input reads it: on one line of commands, so without brackets or braces. */
|
|
10
|
+
export const noteInput = z.string().trim().min(1).max(noteLimit).regex(/^[^[\]{}]*$/, "write it without [ ] { }");
|
|
11
|
+
/** The arrows the AI draws, as their input reads them. */
|
|
12
|
+
export const arrowsInput = z.array(z.string().regex(/^[GRYB][a-h][1-8][a-h][1-8]$/, "an arrow is a colour's letter and two squares: Gf6e4; a single square goes in squares")
|
|
13
|
+
.refine((arrow) => arrow.slice(1, 3) !== arrow.slice(3, 5), "an arrow goes from a square to another: a single square goes in squares")).max(drawLimit).optional()
|
|
14
|
+
.describe("arrows on the board, as PGN's %cal writes them: Rf2e1. G green: the better move or the good idea; R red: a threat or the mistake; Y yellow: another way; B blue: a plan.");
|
|
15
|
+
/** The squares the AI marks, as their input reads them. */
|
|
16
|
+
export const squaresInput = z.array(z.string().regex(/^[GRYB][a-h][1-8]$/, "a square is a colour's letter and the square: Rf2")).max(drawLimit).optional()
|
|
17
|
+
.describe("squares to mark, as PGN's %csl writes them: Rf2; colours as for arrows");
|
|
18
|
+
/** The arrows and squares drawn, those that read; none of either left out. */
|
|
19
|
+
export function drawingsOf(arrows = [], squares = []) {
|
|
20
|
+
const drawnArrows = arrows.map(arrowOf).filter((arrow) => arrow !== undefined);
|
|
21
|
+
const drawnSquares = squares.map(squareOf).filter((square) => square !== undefined);
|
|
22
|
+
return { ...(drawnArrows.length === 0 ? {} : { arrows: drawnArrows }), ...(drawnSquares.length === 0 ? {} : { squares: drawnSquares }) };
|
|
23
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { type GameSummary } from "./game-summary.ts";
|
|
3
|
+
interface Ended {
|
|
4
|
+
game: GameSummary;
|
|
5
|
+
warning?: string;
|
|
6
|
+
}
|
|
7
|
+
/** The endings the board cannot see: a resignation, a draw agreed on an offer (D-65). Checkmate, stalemate and the draws by rule end the game by themselves (D-37). */
|
|
8
|
+
export declare const endGame: import("./registry.ts").Command<z.ZodObject<{
|
|
9
|
+
result: z.ZodEnum<{
|
|
10
|
+
player_resigns: "player_resigns";
|
|
11
|
+
ai_resigns: "ai_resigns";
|
|
12
|
+
draw_agreed: "draw_agreed";
|
|
13
|
+
}>;
|
|
14
|
+
mood: z.ZodOptional<z.ZodEnum<{
|
|
15
|
+
neutral: "neutral";
|
|
16
|
+
happy: "happy";
|
|
17
|
+
surprised: "surprised";
|
|
18
|
+
worried: "worried";
|
|
19
|
+
thoughtful: "thoughtful";
|
|
20
|
+
sad: "sad";
|
|
21
|
+
}>>;
|
|
22
|
+
}, z.core.$strip>, Ended>;
|
|
23
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { canMate, endWithMood, Game, moods, timeLeft } from "../core/index.js";
|
|
3
|
+
import { archiveEnded, bothSidesMoved, drawOfferOf, saveGame } from "../games/index.js";
|
|
4
|
+
import { gameFolder } from "../profiles/index.js";
|
|
5
|
+
import { gameGoingOn, nextStep, positionLines, summaryOf } from "./game-summary.js";
|
|
6
|
+
import { refuseUnseenPosition } from "./play-move.js";
|
|
7
|
+
import { defineCommand } from "./registry.js";
|
|
8
|
+
import { colourName } from "./view.js";
|
|
9
|
+
/** The endings the board cannot see: a resignation, a draw agreed on an offer (D-65). Checkmate, stalemate and the draws by rule end the game by themselves (D-37). */
|
|
10
|
+
export const endGame = defineCommand({
|
|
11
|
+
name: "end_game",
|
|
12
|
+
description: "Ends the game by a resignation or a draw agreed; checkmate, stalemate and the draws by rule end it by themselves. Call it to resign, when "
|
|
13
|
+
+ 'the player resigns in the chat, or to agree a draw while an offer stands: theirs, or yours they accept in the chat. Example: end_game {result: "player_resigns"}.',
|
|
14
|
+
input: z.object({
|
|
15
|
+
result: z.enum(["player_resigns", "ai_resigns", "draw_agreed"]).describe("how the game ends"),
|
|
16
|
+
mood: z.enum(moods).optional().describe("how you feel as the game ends, which your face on the board page keeps"),
|
|
17
|
+
}),
|
|
18
|
+
idempotent: true,
|
|
19
|
+
showsGame: true,
|
|
20
|
+
positionals: ["result"],
|
|
21
|
+
run: ({ result, mood }, context) => {
|
|
22
|
+
const now = context.now();
|
|
23
|
+
const folder = gameFolder(context.home());
|
|
24
|
+
refuseUnseenPosition(context, folder);
|
|
25
|
+
const current = gameGoingOn(folder, now);
|
|
26
|
+
if (result === "draw_agreed") {
|
|
27
|
+
refuseDraw(current);
|
|
28
|
+
}
|
|
29
|
+
const ending = result === "draw_agreed" ? { Result: "1/2-1/2", Termination: "draw agreed" } : resignation(current, result === "player_resigns" ? player(current) : current.ai);
|
|
30
|
+
const { record } = current.game;
|
|
31
|
+
const game = new Game({ ...record, tags: { ...record.tags, ...ending } });
|
|
32
|
+
if (mood !== undefined) {
|
|
33
|
+
endWithMood(game, mood);
|
|
34
|
+
}
|
|
35
|
+
// The clock stops at the end, with the time the side to move had spent.
|
|
36
|
+
const ended = current.clock === undefined
|
|
37
|
+
? { ...current, game }
|
|
38
|
+
: { ...current, game, clock: { ...current.clock, left: timeLeft(current.clock, current.game.position.turn, now), since: now.toISOString() } };
|
|
39
|
+
saveGame(folder, ended);
|
|
40
|
+
const { archive, warning } = archiveEnded(folder, ended);
|
|
41
|
+
return warning === undefined ? { game: summaryOf(ended, now, archive) } : { game: summaryOf(ended, now, archive), warning };
|
|
42
|
+
},
|
|
43
|
+
format: ({ game, warning }) => [...positionLines(game), ...(warning === undefined ? [] : [warning]), nextStep(game)].join("\n"),
|
|
44
|
+
});
|
|
45
|
+
/** A draw is agreed only on an offer that stands, once each side has moved (FIDE 5.2.3). */
|
|
46
|
+
function refuseDraw(current) {
|
|
47
|
+
if (!bothSidesMoved(current.game)) {
|
|
48
|
+
throw new Error("A draw is agreed only once each side has made a move. Nothing changed.");
|
|
49
|
+
}
|
|
50
|
+
if (drawOfferOf(current) === undefined) {
|
|
51
|
+
throw new Error("No draw offer stands: the player offers one with their move (player_move with offer_draw), you with yours (ai_move with offer_draw). Nothing changed.");
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/** The side that resigns loses, unless the other could not mate by any series of legal moves: then the game is drawn (FIDE 5.1.2). */
|
|
55
|
+
function resignation(current, loser) {
|
|
56
|
+
const winner = loser === "white" ? "black" : "white";
|
|
57
|
+
if (canMate(current.game.position.fen, winner)) {
|
|
58
|
+
return { Result: loser === "white" ? "0-1" : "1-0", Termination: `${colourName(loser)} resigns` };
|
|
59
|
+
}
|
|
60
|
+
return { Result: "1/2-1/2", Termination: `${colourName(loser)} resigns and ${colourName(winner)} cannot mate` };
|
|
61
|
+
}
|
|
62
|
+
function player(current) {
|
|
63
|
+
return current.ai === "white" ? "black" : "white";
|
|
64
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
interface Exported {
|
|
3
|
+
format: "pgn" | "moves" | "fen";
|
|
4
|
+
text: string;
|
|
5
|
+
}
|
|
6
|
+
/** The game as PGN or its position as FEN, for the player to copy or keep: the plans stay hidden while it goes on (D-29); the moves alone for another site (T-59). */
|
|
7
|
+
export declare const exportGame: import("./registry.ts").Command<z.ZodObject<{
|
|
8
|
+
format: z.ZodEnum<{
|
|
9
|
+
fen: "fen";
|
|
10
|
+
moves: "moves";
|
|
11
|
+
pgn: "pgn";
|
|
12
|
+
}>;
|
|
13
|
+
}, z.core.$strip>, Exported>;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import { movesPgn, playerPgn } from "../games/index.js";
|
|
3
|
+
import { gameFolder } from "../profiles/index.js";
|
|
4
|
+
import { gameAt } from "./clock.js";
|
|
5
|
+
import { defineCommand } from "./registry.js";
|
|
6
|
+
/** The game as PGN or its position as FEN, for the player to copy or keep: the plans stay hidden while it goes on (D-29); the moves alone for another site (T-59). */
|
|
7
|
+
export const exportGame = defineCommand({
|
|
8
|
+
name: "export",
|
|
9
|
+
description: "Gives the game as PGN, its moves alone as PGN for Lichess or chess.com, or its position as FEN, for the player to copy or keep. While the game goes on, "
|
|
10
|
+
+ 'the PGN leaves out your plans. Example: export {format: "moves"}.',
|
|
11
|
+
input: z.object({
|
|
12
|
+
format: z.enum(["pgn", "moves", "fen"]).describe("pgn for the whole game with its comments, moves for the moves alone, to load on Lichess or chess.com, fen for the position"),
|
|
13
|
+
}),
|
|
14
|
+
// Read only for the host: a flag that has fallen is written, as the time already decided.
|
|
15
|
+
readOnly: true,
|
|
16
|
+
positionals: ["format"],
|
|
17
|
+
run: ({ format }, context) => {
|
|
18
|
+
const current = gameAt(gameFolder(context.home()), context.now());
|
|
19
|
+
if (current === undefined) {
|
|
20
|
+
throw new Error("There is no game to export: start one with new_game.");
|
|
21
|
+
}
|
|
22
|
+
const text = format === "fen" ? current.game.position.fen : format === "moves" ? movesPgn(current) : playerPgn(current);
|
|
23
|
+
return { format, text };
|
|
24
|
+
},
|
|
25
|
+
format: (result) => result.text,
|
|
26
|
+
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type AiLevel, type Facts, type MoodNow, type Side, type Square } from "../core/index.ts";
|
|
2
|
+
import { type CurrentGame, type Speaker, type Thought } from "../games/index.ts";
|
|
3
|
+
import { type ClockSummary } from "./clock.ts";
|
|
4
|
+
/** The current game, as the commands give it. */
|
|
5
|
+
export interface GameSummary {
|
|
6
|
+
ai: Side;
|
|
7
|
+
/** The AI's level: the layers of facts it gets (D-60). */
|
|
8
|
+
level: AiLevel;
|
|
9
|
+
turn: Side;
|
|
10
|
+
moveNumber: number;
|
|
11
|
+
fen: string;
|
|
12
|
+
/** The last moves, numbered: 3.Bc4 Nf6 4.Ng5 d5. */
|
|
13
|
+
lastMoves: string;
|
|
14
|
+
/** The last move, numbered, with its squares. */
|
|
15
|
+
lastMove?: LastMove;
|
|
16
|
+
/** The opening by Lichess's book (D-15): ECO and name, and the move the game left the book at. */
|
|
17
|
+
opening?: string;
|
|
18
|
+
/** When the AI is to move: the facts of the position (D-26); below the club level, only the check and the legal moves of each piece are shown. */
|
|
19
|
+
facts?: Facts;
|
|
20
|
+
/** When the AI is to move at the master level, in the book: the moves it knows from the position (D-15, D-60). */
|
|
21
|
+
book?: readonly string[];
|
|
22
|
+
/** When the AI is to move: the plan it wrote last. */
|
|
23
|
+
plan?: string;
|
|
24
|
+
/** When the AI is to move: what it thought of the player's move on their time (D-72). */
|
|
25
|
+
thought?: Thought;
|
|
26
|
+
/** When the AI is to move: the helps on, so it may teach (D-78), or turned off since it judged or drew on a move. */
|
|
27
|
+
helps?: "on" | "off now";
|
|
28
|
+
/** In a game with a time control: the time each side had left when the command ran (D-13). */
|
|
29
|
+
clock?: ClockSummary;
|
|
30
|
+
/** Who offered a draw, while the offer stands (D-65). */
|
|
31
|
+
drawOffer?: Speaker;
|
|
32
|
+
/** The AI's mood while it lasts, in a game going on (D-70). */
|
|
33
|
+
mood?: MoodNow;
|
|
34
|
+
/** When the game is over: how it ended, and its name in the archive once it is there. */
|
|
35
|
+
end?: {
|
|
36
|
+
result: string;
|
|
37
|
+
termination?: string;
|
|
38
|
+
archive?: string;
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
/** A move numbered as in the game (4.Ng5, 4...d5), with the squares it went from and to. */
|
|
42
|
+
export interface LastMove {
|
|
43
|
+
move: string;
|
|
44
|
+
from: Square;
|
|
45
|
+
to: Square;
|
|
46
|
+
}
|
|
47
|
+
/** A move played by the AI or the player, with where the game stands after it. */
|
|
48
|
+
export interface MovePlayed {
|
|
49
|
+
/** The move, numbered as in the game: 4.Ng5, 4...d5. */
|
|
50
|
+
move: string;
|
|
51
|
+
/** A + or # the move was written with and does not give. */
|
|
52
|
+
note?: string;
|
|
53
|
+
game: GameSummary;
|
|
54
|
+
/** What went wrong after the move was saved, such as the archive not written. */
|
|
55
|
+
warning?: string;
|
|
56
|
+
/** What the player wrote on the board page that the AI has not been given yet (T-65). */
|
|
57
|
+
words?: string[];
|
|
58
|
+
}
|
|
59
|
+
/** The game as the commands tell it; a game over with the name it has in the archive, when it is there. */
|
|
60
|
+
export declare function summaryOf(current: CurrentGame, now: Date, archive?: string): GameSummary;
|
|
61
|
+
/** The summary with what the AI reads when it is to move: what it thought of the player's last move on their time (D-72), and whether the helps are on (D-78). */
|
|
62
|
+
export declare function withThought(summary: GameSummary, folder: string, current: CurrentGame): GameSummary;
|
|
63
|
+
/** The current game `now`, refused when there is none or when it is over, on time too. */
|
|
64
|
+
export declare function gameGoingOn(folder: string, now: Date): CurrentGame;
|
|
65
|
+
/** 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. */
|
|
66
|
+
export declare function positionLines(game: GameSummary, withLastMove?: boolean): string[];
|
|
67
|
+
/** 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. */
|
|
68
|
+
export declare function headLines(game: GameSummary, withLastMove?: boolean): string[];
|
|
69
|
+
/** The last move, as the player is shown it, with its squares: 4.♞g5 (f3-g5). */
|
|
70
|
+
export declare function lastMoveText({ move, from, to }: LastMove): string;
|
|
71
|
+
/**
|
|
72
|
+
* 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
|
|
73
|
+
* (Priya, test of D-86): 4.Ng5 is 4.♞g5.
|
|
74
|
+
*/
|
|
75
|
+
export declare function shownMove(move: string): string;
|
|
76
|
+
/** The player's language by its English name when it is not English. */
|
|
77
|
+
export interface Speech {
|
|
78
|
+
language: string;
|
|
79
|
+
}
|
|
80
|
+
/** The player's speech, from the page's settings or the system's language; undefined in English, which the AI speaks unless told. */
|
|
81
|
+
export declare function speechOf(folder: string, systemLanguage: string | undefined): Speech | undefined;
|
|
82
|
+
/** What the AI is told of the player's speech once a chat (Priya, retest of T-54); the moves are SAN in every language (D-87). */
|
|
83
|
+
export declare function speechLine({ language }: Speech): string;
|
|
84
|
+
/** The lines after a move, which the line before named: its note, where the game stands, a warning. closingLines end them. */
|
|
85
|
+
export declare function moveLines({ note, game, warning }: MovePlayed): string[];
|
|
86
|
+
/** 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. */
|
|
87
|
+
export declare function closingLines(game: GameSummary | undefined, words?: readonly string[]): string[];
|
|
88
|
+
/** 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. */
|
|
89
|
+
export declare function wordLines(words: readonly string[]): string[];
|
|
90
|
+
/** 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. */
|
|
91
|
+
export declare function nextStep(game: GameSummary | undefined, words?: readonly string[]): string;
|
|
92
|
+
/** How a new game starts: the player chooses it in the board page's window, which new_game without choices opens; in the chat only where there is no page (D-85). */
|
|
93
|
+
export declare const newGameStep: string;
|
|
94
|
+
/** Who plays what, from the AI's side. */
|
|
95
|
+
export declare function sidesLine(ai: Side): string;
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
// What the chess commands tell about the current game: plain data for the command line's JSON, and the lines an AI reads.
|
|
2
|
+
import { figurinesOf, levelOf, materialOf, moodAt, sees } from "../core/index.js";
|
|
3
|
+
import { currentPlan, drawOfferOf, endOf, talkLimits, thoughtForLastMove } from "../games/index.js";
|
|
4
|
+
import { onQuickModel } from "../host-files/index.js";
|
|
5
|
+
import { helpsIn, languageIn } from "../profiles/index.js";
|
|
6
|
+
import { clockLine, clockSummaryOf, gameAt } from "./clock.js";
|
|
7
|
+
import { knownMoves, openingLine } from "./opening-book.js";
|
|
8
|
+
import { boardView, colourName, compactView, materialText } from "./view.js";
|
|
9
|
+
/** The game as the commands tell it; a game over with the name it has in the archive, when it is there. */
|
|
10
|
+
export function summaryOf(current, now, archive) {
|
|
11
|
+
const { game, ai } = current;
|
|
12
|
+
const { position, record, lastMove } = game;
|
|
13
|
+
const end = endOf(game);
|
|
14
|
+
const level = levelOf(record);
|
|
15
|
+
const summary = { ai, level, turn: position.turn, moveNumber: position.moveNumber, fen: position.fen, lastMoves: lastMoves(record, 6) };
|
|
16
|
+
if (lastMove !== undefined) {
|
|
17
|
+
summary.lastMove = { move: lastMoves(record, 1), from: lastMove.from, to: lastMove.to };
|
|
18
|
+
}
|
|
19
|
+
const opening = openingLine(record);
|
|
20
|
+
const clock = clockSummaryOf(current, now);
|
|
21
|
+
const drawOffer = drawOfferOf(current);
|
|
22
|
+
if (drawOffer !== undefined) {
|
|
23
|
+
summary.drawOffer = drawOffer;
|
|
24
|
+
}
|
|
25
|
+
const mood = moodAt(record, ai);
|
|
26
|
+
if (end === undefined && mood.mood !== "neutral") {
|
|
27
|
+
summary.mood = mood;
|
|
28
|
+
}
|
|
29
|
+
if (clock !== undefined) {
|
|
30
|
+
summary.clock = clock;
|
|
31
|
+
}
|
|
32
|
+
if (opening !== undefined) {
|
|
33
|
+
summary.opening = opening;
|
|
34
|
+
}
|
|
35
|
+
if (end !== undefined) {
|
|
36
|
+
summary.end = archive === undefined ? end : { ...end, archive };
|
|
37
|
+
}
|
|
38
|
+
else if (position.turn === ai) {
|
|
39
|
+
addAiFacts(summary, current);
|
|
40
|
+
}
|
|
41
|
+
return summary;
|
|
42
|
+
}
|
|
43
|
+
/** The summary with what the AI reads when it is to move: what it thought of the player's last move on their time (D-72), and whether the helps are on (D-78). */
|
|
44
|
+
export function withThought(summary, folder, current) {
|
|
45
|
+
if (summary.turn !== summary.ai || summary.end !== undefined) {
|
|
46
|
+
return summary;
|
|
47
|
+
}
|
|
48
|
+
const thought = thoughtForLastMove(folder, current);
|
|
49
|
+
const helps = helpsIn(folder) ? "on" : judgedBefore(current) ? "off now" : undefined;
|
|
50
|
+
return { ...summary, ...(thought === undefined ? {} : { thought }), ...(helps === undefined ? {} : { helps }) };
|
|
51
|
+
}
|
|
52
|
+
/** The current game `now`, refused when there is none or when it is over, on time too. */
|
|
53
|
+
export function gameGoingOn(folder, now) {
|
|
54
|
+
const current = gameAt(folder, now);
|
|
55
|
+
if (current === undefined) {
|
|
56
|
+
throw new Error("There is no game going on: start one with new_game. Nothing changed.");
|
|
57
|
+
}
|
|
58
|
+
const end = endOf(current.game);
|
|
59
|
+
if (end !== undefined) {
|
|
60
|
+
throw new Error(`The game is over (${endText(end)}): new_game starts another. Nothing changed.`);
|
|
61
|
+
}
|
|
62
|
+
return current;
|
|
63
|
+
}
|
|
64
|
+
/** 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. */
|
|
65
|
+
export function positionLines(game, withLastMove = true) {
|
|
66
|
+
if (game.end !== undefined || game.turn !== game.ai) {
|
|
67
|
+
return [whereLine(game), ...offerLines(game), ...clockLines(game), ...moodLines(game)];
|
|
68
|
+
}
|
|
69
|
+
return [
|
|
70
|
+
...headLines(game, withLastMove),
|
|
71
|
+
...(game.facts === undefined ? [] : (sees(game.level, "tactics") ? compactView : boardView)(game.facts, game.turn)),
|
|
72
|
+
...(game.book === undefined ? [] : [`Book moves, most named lines first: ${game.book.join(", ")}`]),
|
|
73
|
+
...(game.plan === undefined ? [] : [`Plan: ${game.plan}`]),
|
|
74
|
+
...(game.thought === undefined ? [] : [thoughtLine(game.thought, game.level)]),
|
|
75
|
+
...moodLines(game),
|
|
76
|
+
...(game.helps === "on" ? [helpsLine] : game.helps === "off now" ? [helpsOffLine] : []),
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
/** The helps turned on: the AI may teach as the analyst does (D-62, D-78), never playing for the player. */
|
|
80
|
+
const helpsLine = "Helps on: the player wants to learn. Tell your intentions, never their best move; after their move judge it when it stands out (ai_move's badge "
|
|
81
|
+
+ `and note), draw arrows and squares to show what you say, and write up to ${talkLimits.aiHelps} characters.`;
|
|
82
|
+
/** The helps turned off during a game: what the AI said and drew before no longer holds (Priya, round of D-78). */
|
|
83
|
+
const helpsOffLine = `Helps off now: play as at a tournament again: no intentions, badges or arrows, comments of ${talkLimits.ai} characters at most.`;
|
|
84
|
+
/** Whether the AI judged or drew on a move of this game, which only the helps allow: a sign they were on. */
|
|
85
|
+
function judgedBefore(current) {
|
|
86
|
+
return current.game.record.moves.some((move) => /\[%(badge|review|cal|csl) /.test(move.comment ?? ""));
|
|
87
|
+
}
|
|
88
|
+
/** 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. */
|
|
89
|
+
export function headLines(game, withLastMove = true) {
|
|
90
|
+
return [
|
|
91
|
+
whereLine(game),
|
|
92
|
+
...offerLines(game),
|
|
93
|
+
...(withLastMove && game.lastMove !== undefined ? [`Last move: ${lastMoveText(game.lastMove)}.`] : []),
|
|
94
|
+
...(game.opening === undefined ? [] : [`Opening: ${game.opening}`]),
|
|
95
|
+
`Material: ${materialText(materialOf(game.fen))}`,
|
|
96
|
+
...clockLines(game),
|
|
97
|
+
`FEN: ${game.fen}`,
|
|
98
|
+
];
|
|
99
|
+
}
|
|
100
|
+
/** The last move, as the player is shown it, with its squares: 4.♞g5 (f3-g5). */
|
|
101
|
+
export function lastMoveText({ move, from, to }) {
|
|
102
|
+
return `${shownMove(move)} (${from}-${to})`;
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* 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
|
|
106
|
+
* (Priya, test of D-86): 4.Ng5 is 4.♞g5.
|
|
107
|
+
*/
|
|
108
|
+
export function shownMove(move) {
|
|
109
|
+
const [, number = "", san = move] = /^(\d+\.+)(.+)$/.exec(move) ?? [];
|
|
110
|
+
return `${number}${figurinesOf(san)}`;
|
|
111
|
+
}
|
|
112
|
+
/** The player's speech, from the page's settings or the system's language; undefined in English, which the AI speaks unless told. */
|
|
113
|
+
export function speechOf(folder, systemLanguage) {
|
|
114
|
+
const language = languageName(languageIn(folder, systemLanguage));
|
|
115
|
+
return language === undefined ? undefined : { language };
|
|
116
|
+
}
|
|
117
|
+
/** What the AI is told of the player's speech once a chat (Priya, retest of T-54); the moves are SAN in every language (D-87). */
|
|
118
|
+
export function speechLine({ language }) {
|
|
119
|
+
return `The player speaks ${language}.`;
|
|
120
|
+
}
|
|
121
|
+
/** The lines after a move, which the line before named: its note, where the game stands, a warning. closingLines end them. */
|
|
122
|
+
export function moveLines({ note, game, warning }) {
|
|
123
|
+
return [...(note === undefined ? [] : [`Note: ${note}`]), ...positionLines(game, false), ...(warning === undefined ? [] : [warning])];
|
|
124
|
+
}
|
|
125
|
+
/** 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. */
|
|
126
|
+
export function closingLines(game, words = []) {
|
|
127
|
+
return [...wordLines(words), nextStep(game, words)];
|
|
128
|
+
}
|
|
129
|
+
/** 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. */
|
|
130
|
+
export function wordLines(words) {
|
|
131
|
+
return words.map((text) => `Player wrote on the board page: «${text.replace(/\s+/g, " ")}»`);
|
|
132
|
+
}
|
|
133
|
+
/** 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. */
|
|
134
|
+
export function nextStep(game, words = []) {
|
|
135
|
+
if (game === undefined) {
|
|
136
|
+
return words.length === 0 ? `Next: when the player wants a game, ${newGameStep}` : `Next: answer the player with ai_say. When they want a game, ${newGameStep}`;
|
|
137
|
+
}
|
|
138
|
+
if (game.end !== undefined) {
|
|
139
|
+
return words.length === 0
|
|
140
|
+
? "Next: tell the player how the game ended, with ai_say and how you feel as its mood if they play on the board page; then offer them, in words, another game or "
|
|
141
|
+
+ "to go over this one "
|
|
142
|
+
+ `(analyse_game). For another, ${newGameStep}`
|
|
143
|
+
: `Next: answer the player with ai_say. When they want another game, ${newGameStep}`;
|
|
144
|
+
}
|
|
145
|
+
if (game.turn === game.ai && game.clock?.waiting === true) {
|
|
146
|
+
return "Next: don't move yet: the player starts the game with Start on the board page, and the doorbell wakes you then. End your turn.";
|
|
147
|
+
}
|
|
148
|
+
if (game.turn === game.ai) {
|
|
149
|
+
return words.length === 0
|
|
150
|
+
? "Next: choose your move, then call ai_move with it, your plan and any comment."
|
|
151
|
+
: "Next: choose your move, then call ai_move with it, your plan and a comment that answers the player.";
|
|
152
|
+
}
|
|
153
|
+
if (game.drawOffer === "player") {
|
|
154
|
+
return "Next: accept the draw with end_game draw_agreed, or answer with ai_say and wait for the player's move.";
|
|
155
|
+
}
|
|
156
|
+
return words.length === 0 ? "Next: wait for the player's move, then call player_move with it." : "Next: answer the player with ai_say, then wait for their move.";
|
|
157
|
+
}
|
|
158
|
+
/** How a new game starts: the player chooses it in the board page's window, which new_game without choices opens; in the chat only where there is no page (D-85). */
|
|
159
|
+
export 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 "
|
|
160
|
+
+ "level; where there is no board page, it tells you to ask them in the chat.";
|
|
161
|
+
/** Who plays what, from the AI's side. */
|
|
162
|
+
export function sidesLine(ai) {
|
|
163
|
+
return `you play ${colourName(ai)}, the player ${colourName(ai === "white" ? "black" : "white")}`;
|
|
164
|
+
}
|
|
165
|
+
/** What the AI gets when it is to move: the facts its level sees (D-60), and its plan. */
|
|
166
|
+
function addAiFacts(summary, { game }) {
|
|
167
|
+
const plan = currentPlan(game.record);
|
|
168
|
+
summary.facts = game.facts;
|
|
169
|
+
if (sees(summary.level, "theory")) {
|
|
170
|
+
const book = knownMoves(summary.fen);
|
|
171
|
+
if (book.length > 0) {
|
|
172
|
+
summary.book = book;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
if (plan !== undefined) {
|
|
176
|
+
summary.plan = plan;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
/** The draw offer that stands, and how to answer it or what becomes of it (D-65). */
|
|
180
|
+
function offerLines({ drawOffer, turn, ai }) {
|
|
181
|
+
if (drawOffer === "ai") {
|
|
182
|
+
return ["Your draw offer stands until the player moves; if they accept it in the chat, end_game draw_agreed ends the game."];
|
|
183
|
+
}
|
|
184
|
+
if (drawOffer === "player") {
|
|
185
|
+
return [turn === ai ? "The player offers a draw: end_game draw_agreed accepts it; your move declines it." : "The player offers a draw before their move; your next move declines it."];
|
|
186
|
+
}
|
|
187
|
+
return [];
|
|
188
|
+
}
|
|
189
|
+
/** What the AI thought on the player's time, saying by which model and that Chesspal checked only that it is legal: the facts do not cover every blunder (Anatolij, round of T-109). */
|
|
190
|
+
function thoughtLine({ move, why }, level) {
|
|
191
|
+
return `Thought on their time ${onQuickModel(level) ? "by a quicker model" : "by your model"} (legal, not checked): ${move}, ${why}`;
|
|
192
|
+
}
|
|
193
|
+
/** The AI's mood while it lasts, and how long its face shows it (D-70). */
|
|
194
|
+
function moodLines({ mood }) {
|
|
195
|
+
if (mood === undefined) {
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
const shown = mood.left === 0 ? "until your next move" : `for ${mood.left} more of your moves`;
|
|
199
|
+
return [`Your mood: ${mood.mood}, on your face ${shown}, then neutral by itself.`];
|
|
200
|
+
}
|
|
201
|
+
/** Who is to move, or how the game ended. */
|
|
202
|
+
function clockLines(game) {
|
|
203
|
+
return game.clock === undefined ? [] : [clockLine(game.clock, game.ai)];
|
|
204
|
+
}
|
|
205
|
+
function whereLine(game) {
|
|
206
|
+
if (game.end !== undefined) {
|
|
207
|
+
const archived = game.end.archive === undefined ? "" : ` Archived in ${game.end.archive}.`;
|
|
208
|
+
return `Game over: ${endText(game.end)}.${archived}`;
|
|
209
|
+
}
|
|
210
|
+
return `${game.turn === game.ai ? "You" : "Player"} (${colourName(game.turn)}) to move, move ${game.moveNumber}.`;
|
|
211
|
+
}
|
|
212
|
+
function endText({ result, termination }) {
|
|
213
|
+
return termination === undefined ? result : `${result}, ${termination}`;
|
|
214
|
+
}
|
|
215
|
+
/** The last plies of a record, numbered as a player writes them, from the move number and side of its FEN. */
|
|
216
|
+
function lastMoves(record, count) {
|
|
217
|
+
const [, turn, , , , number] = (record.tags.FEN ?? "").split(" ");
|
|
218
|
+
const startNumber = Number(number) || 1;
|
|
219
|
+
// The ply count from white's move of the first move number: 1 when black moves first.
|
|
220
|
+
const offset = turn === "b" ? 1 : 0;
|
|
221
|
+
const first = Math.max(0, record.moves.length - count);
|
|
222
|
+
return record.moves.slice(first).map((move, index) => {
|
|
223
|
+
const ply = first + index + offset;
|
|
224
|
+
const moveNumber = startNumber + Math.floor(ply / 2);
|
|
225
|
+
if (ply % 2 === 0) {
|
|
226
|
+
return `${moveNumber}.${move.san}`;
|
|
227
|
+
}
|
|
228
|
+
return index === 0 ? `${moveNumber}...${move.san}` : move.san;
|
|
229
|
+
}).join(" ");
|
|
230
|
+
}
|
|
231
|
+
/** 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). */
|
|
232
|
+
function languageName(language) {
|
|
233
|
+
const code = language?.split(/[-_]/)[0]?.toLowerCase() ?? "";
|
|
234
|
+
if (!/^[a-z]{2,3}$/.test(code) || code === "en" || code === "und") {
|
|
235
|
+
return undefined;
|
|
236
|
+
}
|
|
237
|
+
const name = new Intl.DisplayNames(["en"], { type: "language" }).of(code);
|
|
238
|
+
return name === undefined || name === code ? undefined : name;
|
|
239
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { aiMove } from "./ai-move.js";
|
|
2
|
+
import { aiSay } from "./ai-say.js";
|
|
3
|
+
import { analyseGame } from "./analyse-game.js";
|
|
4
|
+
import { annotateGame } from "./annotate-game.js";
|
|
5
|
+
import { endGame } from "./end-game.js";
|
|
6
|
+
import { exportGame } from "./export-game.js";
|
|
7
|
+
import { init } from "./init.js";
|
|
8
|
+
import { look } from "./look.js";
|
|
9
|
+
import { newGame } from "./new-game.js";
|
|
10
|
+
import { pastGames } from "./past-games.js";
|
|
11
|
+
import { playerMove } from "./player-move.js";
|
|
12
|
+
import { playerSay } from "./player-say.js";
|
|
13
|
+
import { ponder } from "./ponder.js";
|
|
14
|
+
import { position } from "./position.js";
|
|
15
|
+
import { newProfile, profiles, setAvatar, useProfile } from "./profiles.js";
|
|
16
|
+
import { status } from "./status.js";
|
|
17
|
+
import { studyGames } from "./study-games.js";
|
|
18
|
+
/** Every command of Chesspal, in the order the command line lists them and the MCP server offers them. */
|
|
19
|
+
export const commands = [init, status, newGame, playerMove, playerSay, aiMove, aiSay, ponder, look, position, endGame, exportGame, pastGames, analyseGame, annotateGame, studyGames, profiles, newProfile, useProfile, setAvatar];
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
/** The player's folder set up for the AI hosts (T-7): the folder, and the files written there. */
|
|
3
|
+
interface FolderSetUp {
|
|
4
|
+
folder: string;
|
|
5
|
+
written: string[];
|
|
6
|
+
/** The folder as the player goes into it from where init ran, when it is not that one (T-127): chess. */
|
|
7
|
+
enter?: string;
|
|
8
|
+
/** Codex's hooks changed in a folder that had them: Codex runs them only once allowed again in /hooks (T-125). */
|
|
9
|
+
hooksChanged?: true;
|
|
10
|
+
}
|
|
11
|
+
/** Makes a folder the player's chess table: the files through which Claude Code and Codex find Chesspal and the opponent's instructions (D-31). */
|
|
12
|
+
export declare const init: import("./registry.ts").Command<z.ZodObject<{
|
|
13
|
+
folder: z.ZodOptional<z.ZodString>;
|
|
14
|
+
}, z.core.$strip>, FolderSetUp>;
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { relative, resolve } from "node:path";
|
|
3
|
+
// Colours only on a terminal that shows them: none in a file, a pipe or with NO_COLOR.
|
|
4
|
+
import { styleText } from "node:util";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { codexHooksPath, setUpHosts } from "../host-files/index.js";
|
|
7
|
+
import { defineCommand } from "./registry.js";
|
|
8
|
+
/** Makes a folder the player's chess table: the files through which Claude Code and Codex find Chesspal and the opponent's instructions (D-31). */
|
|
9
|
+
export const init = defineCommand({
|
|
10
|
+
name: "init",
|
|
11
|
+
description: "Sets up a folder to play chess with an AI: the .mcp.json, AGENTS.md, CLAUDE.md and the settings through which Claude Code and Codex find "
|
|
12
|
+
+ "Chesspal. Run it again to update Chesspal's lines; what you wrote there stays. Example: chesspal init chess.",
|
|
13
|
+
input: z.object({ folder: z.string().optional().describe("the folder to set up; the current folder if left out") }),
|
|
14
|
+
cliOnly: true,
|
|
15
|
+
idempotent: true,
|
|
16
|
+
positionals: ["folder"],
|
|
17
|
+
run: ({ folder }, context) => {
|
|
18
|
+
const root = resolve(context.folder, folder ?? ".");
|
|
19
|
+
const hooks = codexHooksPath(root);
|
|
20
|
+
const hadHooks = existsSync(hooks);
|
|
21
|
+
const written = setUpHosts(root, context.platform);
|
|
22
|
+
const enter = relative(context.folder, root);
|
|
23
|
+
return {
|
|
24
|
+
folder: root,
|
|
25
|
+
written: written.map((path) => relative(root, path)),
|
|
26
|
+
...(enter === "" ? {} : { enter }),
|
|
27
|
+
...(hadHooks && written.includes(hooks) ? { hooksChanged: true } : {}),
|
|
28
|
+
};
|
|
29
|
+
},
|
|
30
|
+
format: ({ folder, written, enter, hooksChanged }, context) => {
|
|
31
|
+
const words = wordsFor(context?.language);
|
|
32
|
+
const steps = [words.open(enter === undefined ? undefined : quoted(enter)), ...words.steps];
|
|
33
|
+
return [
|
|
34
|
+
styleText(["bold", "green"], `♜ ${written.length === 0 ? words.ready(folder) : words.set(folder)}`),
|
|
35
|
+
"",
|
|
36
|
+
` ${words.toPlay}`,
|
|
37
|
+
...steps.map((step, index) => ` ${styleText("cyan", `${index + 1}.`)} ${step.join("\n ")}`),
|
|
38
|
+
...(hooksChanged === true ? ["", ` ${styleText("yellow", words.hooksChanged)}`] : []),
|
|
39
|
+
...(written.length === 0 ? [] : ["", styleText("gray", ` ${words.updated(written.length)}`)]),
|
|
40
|
+
].join("\n");
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
const english = {
|
|
44
|
+
set: (folder) => `Chesspal is ready in ${folder}`,
|
|
45
|
+
ready: (folder) => `Chesspal is ready in ${folder}: nothing to change`,
|
|
46
|
+
toPlay: "To play:",
|
|
47
|
+
open: (enter) => enter === undefined ? ["Open Claude Code or Codex in this folder."] : [`Go into the folder: cd ${enter}`, "then open Claude Code (claude) or Codex (codex)."],
|
|
48
|
+
steps: [
|
|
49
|
+
["The first time, it asks whether to trust the folder: say yes.", "In Codex, also type /hooks and allow Chesspal's two hooks:", "small helpers that let it notice your moves on the board."],
|
|
50
|
+
["Say hello: the AI gives you the link to the board."],
|
|
51
|
+
],
|
|
52
|
+
hooksChanged: "Chesspal's hooks for Codex have changed: in Codex, type /hooks and allow them again.",
|
|
53
|
+
updated: (count) => `Updated ${count} ${count === 1 ? "file" : "files"}.`,
|
|
54
|
+
};
|
|
55
|
+
const italian = {
|
|
56
|
+
set: (folder) => `Chesspal è pronto in ${folder}`,
|
|
57
|
+
ready: (folder) => `Chesspal è già pronto in ${folder}: niente da cambiare`,
|
|
58
|
+
toPlay: "Per giocare:",
|
|
59
|
+
open: (enter) => enter === undefined ? ["Apri Claude Code o Codex in questa cartella."] : [`Entra nella cartella: cd ${enter}`, "poi apri Claude Code (claude) o Codex (codex)."],
|
|
60
|
+
steps: [
|
|
61
|
+
["La prima volta ti chiede se fidarti della cartella: rispondi sì.", "In Codex scrivi anche /hooks e consenti i due «hook» di Chesspal:", "piccoli aiuti che gli fanno vedere le tue mosse sulla scacchiera."],
|
|
62
|
+
["Saluta: l'AI ti dà il link della scacchiera."],
|
|
63
|
+
],
|
|
64
|
+
hooksChanged: "Gli «hook» di Chesspal per Codex sono cambiati: in Codex scrivi /hooks e consentili di nuovo.",
|
|
65
|
+
updated: (count) => count === 1 ? "Aggiornato 1 file." : `Aggiornati ${count} file.`,
|
|
66
|
+
};
|
|
67
|
+
function wordsFor(language) {
|
|
68
|
+
return language?.slice(0, 2).toLowerCase() === "it" ? italian : english;
|
|
69
|
+
}
|
|
70
|
+
/** A path as a shell reads it as one word: in quotes when it has spaces. */
|
|
71
|
+
function quoted(path) {
|
|
72
|
+
return /\s/.test(path) ? `"${path}"` : path;
|
|
73
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type AiLevel, type Layer } from "../core/index.ts";
|
|
2
|
+
import { type CurrentGame } from "../games/index.ts";
|
|
3
|
+
import type { Context } from "./context.ts";
|
|
4
|
+
/** The level of a new game when the call names none: the game before's, else the one that suits the player in use, else the level before levels came. */
|
|
5
|
+
export declare function levelToPlay(context: Context, before: CurrentGame | undefined): AiLevel;
|
|
6
|
+
/** The level and what the AI sees at it, in few words: at the club level (board and tactical facts: no position). */
|
|
7
|
+
export declare function levelText(level: AiLevel): string;
|
|
8
|
+
/** Throws when the game's level does not give a layer, saying what the level gives instead; a game over gives every layer, to its analysis (D-81). */
|
|
9
|
+
export declare function refuseUnseen(current: CurrentGame, layer: Layer, tool: string): void;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// The level of a game as the AI reads it (D-60, D-61): what it sees, and the refusal of a tool its level does not give.
|
|
2
|
+
import { levelOf, sees, suggestedLevel } from "../core/index.js";
|
|
3
|
+
import { endOf } from "../games/index.js";
|
|
4
|
+
import { activeProfile } from "../profiles/index.js";
|
|
5
|
+
/** The level of a new game when the call names none: the game before's, else the one that suits the player in use, else the level before levels came. */
|
|
6
|
+
export function levelToPlay(context, before) {
|
|
7
|
+
return suggestedLevel(before?.game.record, before === undefined ? activeProfile(context.home()).level : undefined);
|
|
8
|
+
}
|
|
9
|
+
/** The level and what the AI sees at it, in few words: at the club level (board and tactical facts: no position). */
|
|
10
|
+
export function levelText(level) {
|
|
11
|
+
return `at the ${level} level (${seenText[level]})`;
|
|
12
|
+
}
|
|
13
|
+
/** Throws when the game's level does not give a layer, saying what the level gives instead; a game over gives every layer, to its analysis (D-81). */
|
|
14
|
+
export function refuseUnseen(current, layer, tool) {
|
|
15
|
+
const level = levelOf(current.game.record);
|
|
16
|
+
if (!sees(level, layer) && endOf(current.game) === undefined) {
|
|
17
|
+
throw new Error(`${tool} is not yours in this game: you play ${levelText(level)}. Decide with the facts you have.`);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const seenText = {
|
|
21
|
+
beginner: "board and legal moves: no tactical facts, no position",
|
|
22
|
+
club: "board and tactical facts: no position",
|
|
23
|
+
strong: "board, tactical and positional facts",
|
|
24
|
+
master: "board, tactical and positional facts, opening book",
|
|
25
|
+
};
|