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,122 @@
|
|
|
1
|
+
// A game to study (D-68): one that is over, played here or kept to study, and any position of it, by its moves as the analysis numbers them. While a game
|
|
2
|
+
// goes on, nothing is studied: the analysis would help the game being played (D-60, D-62).
|
|
3
|
+
import { z } from "zod";
|
|
4
|
+
import { Game } from "../core/index.js";
|
|
5
|
+
import { archivedGame, archivedGames, archivedName, endOf, keptGame, keptGames } from "../games/index.js";
|
|
6
|
+
import { gameFolder } from "../profiles/index.js";
|
|
7
|
+
import { gameAt } from "./clock.js";
|
|
8
|
+
import { colourName } from "./view.js";
|
|
9
|
+
/** What look and position take to show any position of a game that is over. */
|
|
10
|
+
export const studyInput = {
|
|
11
|
+
game: z.string().optional().describe("a game's name, as past_games, study_games or analyse_game writes it: one of its positions"),
|
|
12
|
+
move: z.string().optional().describe("the position after this move of a game over, as analyse_game numbers it: 12. for White's, 12... for Black's"),
|
|
13
|
+
};
|
|
14
|
+
/** The current game when it is over, or none; refused while it goes on, for every game: games are analysed once the one being played is over. */
|
|
15
|
+
export function noGameGoingOn(context) {
|
|
16
|
+
const current = gameAt(gameFolder(context.home()), context.now());
|
|
17
|
+
if (current !== undefined && endOf(current.game) === undefined) {
|
|
18
|
+
throw new Error("A game is going on: games are analysed once it is over, this one and the others, and not by you from a PGN or a file either. Tell the player, and go "
|
|
19
|
+
+ "over it when it ends. Nothing changed.");
|
|
20
|
+
}
|
|
21
|
+
return current;
|
|
22
|
+
}
|
|
23
|
+
/** The game played or kept to study by that name; without one, the current game over, or else the latest of the archive; refused while a game goes on. */
|
|
24
|
+
export function gameToStudy(context, name) {
|
|
25
|
+
const current = noGameGoingOn(context);
|
|
26
|
+
const folder = gameFolder(context.home());
|
|
27
|
+
if (name !== undefined) {
|
|
28
|
+
return namedGame(context, name);
|
|
29
|
+
}
|
|
30
|
+
if (current !== undefined) {
|
|
31
|
+
// The archive's record, which keeps the AI's review (D-69).
|
|
32
|
+
const archived = archivedName(folder, current);
|
|
33
|
+
return archived === undefined ? { record: current.game.record } : { record: archivedGame(folder, archived), name: archived };
|
|
34
|
+
}
|
|
35
|
+
const [latest] = archivedGames(folder);
|
|
36
|
+
if (latest === undefined) {
|
|
37
|
+
throw new Error("There is no game to study: play one with new_game, or give analyse_game the PGN of a game played elsewhere.");
|
|
38
|
+
}
|
|
39
|
+
return { record: archivedGame(folder, latest), name: latest };
|
|
40
|
+
}
|
|
41
|
+
/** A game played or kept to study by its name as the lists write it, or without its folder or its .pgn; refused when it is not there. */
|
|
42
|
+
export function namedGame(context, name) {
|
|
43
|
+
const folder = gameFolder(context.home());
|
|
44
|
+
const full = fullName(folder, name);
|
|
45
|
+
return { record: full.startsWith("study/") ? keptGame(folder, full) : archivedGame(folder, full), name: full };
|
|
46
|
+
}
|
|
47
|
+
/** The index of a move in the game's record, given as analyse_game numbers it: 12. for White's, 12... for Black's, 12...Kf8 too. */
|
|
48
|
+
export function moveIndex(record, move) {
|
|
49
|
+
return plyOf(record, moveNumbers(record), move);
|
|
50
|
+
}
|
|
51
|
+
/** The number of each move of a game as the analysis writes it: 1. for White's first move, 1... for Black's. */
|
|
52
|
+
export function moveNumbers(record) {
|
|
53
|
+
const start = new Game({ tags: record.tags, moves: [] }).position;
|
|
54
|
+
const blackFirst = start.turn === "black" ? 1 : 0;
|
|
55
|
+
return record.moves.map((_move, index) => {
|
|
56
|
+
const ply = index + blackFirst;
|
|
57
|
+
return `${start.moveNumber + Math.floor(ply / 2)}${ply % 2 === 0 ? "." : "..."}`;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The game as it stood after a move, given by its number (12. for White's, 12... for Black's) and, if wanted, the move itself (12...Kf8), which decides
|
|
62
|
+
* the side when the dots do not; after the last move without one.
|
|
63
|
+
*/
|
|
64
|
+
export function positionAfter(record, move) {
|
|
65
|
+
const numbers = moveNumbers(record);
|
|
66
|
+
const ply = move === undefined ? numbers.length - 1 : plyOf(record, numbers, move);
|
|
67
|
+
if (ply < 0) {
|
|
68
|
+
return { game: new Game(record) };
|
|
69
|
+
}
|
|
70
|
+
return { game: new Game({ ...record, moves: record.moves.slice(0, ply + 1) }), move: `${numbers[ply]}${record.moves[ply].san}` };
|
|
71
|
+
}
|
|
72
|
+
/** A tag PGN leaves unknown with ? or -, as nothing. */
|
|
73
|
+
export function knownTag(tag) {
|
|
74
|
+
return tag === undefined || /^[?.\-\s]*$/.test(tag) ? undefined : tag;
|
|
75
|
+
}
|
|
76
|
+
/** Where a position of a game studied stands: After 12...Kf8 in games/2026-11-02-153012.pgn: White to move. */
|
|
77
|
+
export function positionLine({ game, move }, name) {
|
|
78
|
+
const where = `${move === undefined ? "At the start" : `After ${move}`}${name === undefined ? "" : ` in ${name}`}`;
|
|
79
|
+
const { outcome, position } = game;
|
|
80
|
+
return `${where}: ${outcome.over ? `the board ends the game by ${outcome.ending}` : `${colourName(position.turn)} to move`}.`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* A game's name as the lists write it, from a name without its folder or its .pgn too: 2026-11-04-173054 is games/2026-11-04-173054.pgn, or the game kept
|
|
84
|
+
* to study by that name when no game played has it; any other name as it is, for the archive to refuse.
|
|
85
|
+
*/
|
|
86
|
+
function fullName(folder, name) {
|
|
87
|
+
const [, shelf, stem] = /^\s*(?:(games|study)\/)?(\d{4}-\d{2}-\d{2}-\d{6})(?:\.pgn)?\s*$/.exec(name) ?? [];
|
|
88
|
+
if (stem === undefined) {
|
|
89
|
+
return name;
|
|
90
|
+
}
|
|
91
|
+
if (shelf !== undefined) {
|
|
92
|
+
return `${shelf}/${stem}.pgn`;
|
|
93
|
+
}
|
|
94
|
+
const played = `games/${stem}.pgn`;
|
|
95
|
+
const kept = `study/${stem}.pgn`;
|
|
96
|
+
return !archivedGames(folder).includes(played) && keptGames(folder).includes(kept) ? kept : played;
|
|
97
|
+
}
|
|
98
|
+
function plyOf(record, numbers, move) {
|
|
99
|
+
const [, number, dots, san] = movePattern.exec(move) ?? [];
|
|
100
|
+
if (number === undefined) {
|
|
101
|
+
throw new Error(`"${move}" is not a move of the game: give its number as analyse_game writes it, 12. for White's move and 12... for Black's, such as 12...Kf8.`);
|
|
102
|
+
}
|
|
103
|
+
if (numbers.length === 0) {
|
|
104
|
+
throw new Error("This game has no moves: look and position without move show its position.");
|
|
105
|
+
}
|
|
106
|
+
const white = numbers.indexOf(`${Number(number)}.`);
|
|
107
|
+
const black = numbers.indexOf(`${Number(number)}...`);
|
|
108
|
+
const plies = (dots === "..." || dots === "…" ? [black] : [white, black]).filter((ply) => ply >= 0);
|
|
109
|
+
const found = san === undefined ? plies[0] : plies.find((ply) => isMove(record, ply, san));
|
|
110
|
+
if (found === undefined) {
|
|
111
|
+
const played = [white, black].filter((ply) => ply >= 0).map((ply) => `${numbers[ply]}${record.moves[ply].san}`);
|
|
112
|
+
const there = played.length === 0 ? `its moves go from ${numbers[0]}${record.moves[0].san} to ${numbers.at(-1)}${record.moves.at(-1).san}` : `move ${number} is ${played.join(" ")}`;
|
|
113
|
+
throw new Error(`There is no ${move.trim()} in this game: ${there}.`);
|
|
114
|
+
}
|
|
115
|
+
return found;
|
|
116
|
+
}
|
|
117
|
+
/** Whether the move written is the one the game played at that ply, however it is written: Nf3, g1f3, Nf3+. */
|
|
118
|
+
function isMove(record, ply, written) {
|
|
119
|
+
const played = new Game({ ...record, moves: record.moves.slice(0, ply) }).playRecorded(written);
|
|
120
|
+
return played.played && played.move.san === record.moves[ply].san;
|
|
121
|
+
}
|
|
122
|
+
const movePattern = /^\s*(\d+)\s*(\.\.\.|\.|…)?\s*(\S+)?\s*$/;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Facts, Material, Side } from "../core/index.ts";
|
|
2
|
+
/** The view after the player's move: the check, the pieces attacked, then what is at risk, the pins, the checks and the moves of each piece. */
|
|
3
|
+
export declare function compactView(facts: Facts, turn: Side): string[];
|
|
4
|
+
/** The view of a level without the tactical facts (D-60): the check and the legal moves by piece, which are the rules (D-17). */
|
|
5
|
+
export declare function boardView(facts: Facts, turn: Side): string[];
|
|
6
|
+
/** The whole view, for look: every piece with its attackers and defenders, then the same lines as the compact view. */
|
|
7
|
+
export declare function fullView(facts: Facts, turn: Side): string[];
|
|
8
|
+
/** What is at risk, in one line for a move of the analysis (D-68): undefended Black Nf6; by a cheaper piece White Qd1; outnumbered Black Pe5; none when nothing is. */
|
|
9
|
+
export declare function riskLine(facts: Facts): string | undefined;
|
|
10
|
+
export declare function colourName(side: Side): string;
|
|
11
|
+
/** The material as the player sees it beside the board (D-59): who is ahead and by how much, and the pieces each side took. */
|
|
12
|
+
export declare function materialText(material: Material): string;
|
|
13
|
+
/** Who is ahead in material, and by how many pawns: White +2, or even. */
|
|
14
|
+
export declare function balanceText(balance: number): string;
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/** The view after the player's move: the check, the pieces attacked, then what is at risk, the pins, the checks and the moves of each piece. */
|
|
2
|
+
export function compactView(facts, turn) {
|
|
3
|
+
const names = namesOf(facts);
|
|
4
|
+
const attacked = officersOf(facts).filter((piece) => piece.attackers.length > 0);
|
|
5
|
+
const pieces = attacked.length === 0 ? ["Attacked pieces: none"] : ["Attacked pieces (attackers / defenders):", ...attacked.map((piece) => pieceLine(piece, names, true))];
|
|
6
|
+
return [...checkLines(facts, turn, names), ...pieces, ...tacticLines(facts, turn, names)];
|
|
7
|
+
}
|
|
8
|
+
/** The view of a level without the tactical facts (D-60): the check and the legal moves by piece, which are the rules (D-17). */
|
|
9
|
+
export function boardView(facts, turn) {
|
|
10
|
+
const names = namesOf(facts);
|
|
11
|
+
return [...checkLines(facts, turn, names), ...legalLines(facts, names)];
|
|
12
|
+
}
|
|
13
|
+
/** The whole view, for look: every piece with its attackers and defenders, then the same lines as the compact view. */
|
|
14
|
+
export function fullView(facts, turn) {
|
|
15
|
+
const names = namesOf(facts);
|
|
16
|
+
const sides = sidesInOrder.flatMap((side) => [
|
|
17
|
+
`${colourName(side)} pieces (attackers / defenders):`,
|
|
18
|
+
...officersOf(facts).filter((piece) => piece.side === side).map((piece) => pieceLine(piece, names, false)),
|
|
19
|
+
]);
|
|
20
|
+
return [...checkLines(facts, turn, names), ...sides, ...tacticLines(facts, turn, names)];
|
|
21
|
+
}
|
|
22
|
+
/** What is at risk, in one line for a move of the analysis (D-68): undefended Black Nf6; by a cheaper piece White Qd1; outnumbered Black Pe5; none when nothing is. */
|
|
23
|
+
export function riskLine(facts) {
|
|
24
|
+
const names = namesOf(facts);
|
|
25
|
+
const kinds = [["undefended", facts.undefended], ["by a cheaper piece", facts.attackedByCheaper], ["outnumbered", facts.outnumbered]];
|
|
26
|
+
const text = kinds.filter(([, squares]) => squares.length > 0).map(([kind, squares]) => `${kind} ${bySide(facts, squares, names)}`).join("; ");
|
|
27
|
+
return text === "" ? undefined : text;
|
|
28
|
+
}
|
|
29
|
+
export function colourName(side) {
|
|
30
|
+
return side === "white" ? "White" : "Black";
|
|
31
|
+
}
|
|
32
|
+
/** The material as the player sees it beside the board (D-59): who is ahead and by how much, and the pieces each side took. */
|
|
33
|
+
export function materialText(material) {
|
|
34
|
+
const ahead = balanceText(material.balance);
|
|
35
|
+
if (material.taken.white.length === 0 && material.taken.black.length === 0) {
|
|
36
|
+
return `${ahead}, nothing taken`;
|
|
37
|
+
}
|
|
38
|
+
return `${ahead} (White took ${takenText(material.taken.white)}, Black took ${takenText(material.taken.black)})`;
|
|
39
|
+
}
|
|
40
|
+
/** Who is ahead in material, and by how many pawns: White +2, or even. */
|
|
41
|
+
export function balanceText(balance) {
|
|
42
|
+
return balance === 0 ? "even" : `${balance > 0 ? "White" : "Black"} +${Math.abs(balance)}`;
|
|
43
|
+
}
|
|
44
|
+
/** The pieces taken, counted by kind: Q 2R 7P. */
|
|
45
|
+
function takenText(taken) {
|
|
46
|
+
const kinds = [...new Set(taken)];
|
|
47
|
+
return kinds.map((piece) => {
|
|
48
|
+
const count = taken.filter((other) => other === piece).length;
|
|
49
|
+
return count === 1 ? piece : `${count}${piece}`;
|
|
50
|
+
}).join(" ") || "nothing";
|
|
51
|
+
}
|
|
52
|
+
const sidesInOrder = ["white", "black"];
|
|
53
|
+
/** The check the side to move is in, from which pieces; none when it is not. */
|
|
54
|
+
function checkLines(facts, turn, names) {
|
|
55
|
+
return facts.checkedBy === undefined ? [] : [`${colourName(turn)} is in check from ${facts.checkedBy.map(names.piece).join(" and ")}.`];
|
|
56
|
+
}
|
|
57
|
+
/** What is at risk, loose and pinned, the checks of both sides, and the legal moves of the side to move by piece. */
|
|
58
|
+
function tacticLines(facts, turn, names) {
|
|
59
|
+
const list = (squares) => bySide(facts, squares, names) || "none";
|
|
60
|
+
const atRisk = facts.undefended.length === 0 && facts.attackedByCheaper.length === 0 && facts.outnumbered.length === 0
|
|
61
|
+
? "At risk: none"
|
|
62
|
+
: `Attacked and undefended: ${list(facts.undefended)}; defended, but attacked by a cheaper piece: ${list(facts.attackedByCheaper)}; `
|
|
63
|
+
+ `attacked more times than defended: ${list(facts.outnumbered)}`;
|
|
64
|
+
const pins = facts.pins.map((pin) => `${names.sided(pin.square)} to ${names.piece(pin.to)} by ${names.piece(pin.by)}`);
|
|
65
|
+
return [
|
|
66
|
+
atRisk,
|
|
67
|
+
`Loose: ${list(facts.loose)}`,
|
|
68
|
+
`Pinned: ${pins.join(", ") || "none"}`,
|
|
69
|
+
...checksLines(facts, turn),
|
|
70
|
+
...legalLines(facts, names),
|
|
71
|
+
];
|
|
72
|
+
}
|
|
73
|
+
/** The checks the side to move can give, and those the other side could give if it were to move; one line when neither has any. */
|
|
74
|
+
function checksLines(facts, turn) {
|
|
75
|
+
if (facts.checks.length === 0 && facts.otherChecks?.length === 0) {
|
|
76
|
+
return ["Checks: none for either side"];
|
|
77
|
+
}
|
|
78
|
+
const other = colourName(turn === "white" ? "black" : "white");
|
|
79
|
+
const theirs = facts.otherChecks === undefined ? [] : [`Checks ${other} could give if it were to move: ${facts.otherChecks.join(" ") || "none"}`];
|
|
80
|
+
return [`Checks ${colourName(turn)} (to move) can give: ${facts.checks.join(" ") || "none"}`, ...theirs];
|
|
81
|
+
}
|
|
82
|
+
/** The legal moves of the side to move by piece, with how many, and its pieces that have none; nothing when it has no move, the game over. */
|
|
83
|
+
function legalLines(facts, names) {
|
|
84
|
+
const mine = facts.pieces.filter((piece) => piece.moves !== undefined);
|
|
85
|
+
const moving = mine.filter((piece) => piece.moves.length > 0);
|
|
86
|
+
const stuck = mine.filter((piece) => piece.moves.length === 0).map((piece) => names.piece(piece.square));
|
|
87
|
+
const count = moving.reduce((total, piece) => total + piece.moves.length, 0);
|
|
88
|
+
if (count === 0) {
|
|
89
|
+
return [];
|
|
90
|
+
}
|
|
91
|
+
const byPiece = moving.map((piece) => `${names.piece(piece.square)} (${piece.moves.length}) ${piece.moves.join(" ")}`);
|
|
92
|
+
return [`Legal by piece (${count}): ${[...byPiece, ...(stuck.length === 0 ? [] : [`no moves: ${stuck.join(" ")}`])].join("; ")}`];
|
|
93
|
+
}
|
|
94
|
+
/** A piece with the pieces that attack it and those that defend it, the pieces lined up behind them in brackets: Pf7: Bc4 Ng5 / Ke8; Qd2(+Rd1). */
|
|
95
|
+
function pieceLine(piece, names, withSide) {
|
|
96
|
+
const list = (squares) => squares.map((square) => `${names.piece(square)}${behindOf(piece, square, names)}`).join(" ") || "-";
|
|
97
|
+
const name = withSide ? names.sided(piece.square) : names.piece(piece.square);
|
|
98
|
+
return `${name}: ${list(piece.attackers)} / ${list(piece.defenders)}`;
|
|
99
|
+
}
|
|
100
|
+
function behindOf(piece, square, names) {
|
|
101
|
+
const behind = piece.behind?.[square];
|
|
102
|
+
return behind === undefined ? "" : `(+${behind.map(names.piece).join("+")})`;
|
|
103
|
+
}
|
|
104
|
+
/** Pieces by side, White's first: White Ra1 Pb2, Black Ra8. */
|
|
105
|
+
function bySide(facts, squares, names) {
|
|
106
|
+
const sideAt = new Map(facts.pieces.map((piece) => [piece.square, piece.side]));
|
|
107
|
+
const groups = sidesInOrder.map((side) => [side, squares.filter((square) => sideAt.get(square) === side)]).filter(([, group]) => group.length > 0);
|
|
108
|
+
return groups.map(([side, group]) => `${colourName(side)} ${group.map(names.piece).join(" ")}`).join(", ");
|
|
109
|
+
}
|
|
110
|
+
/** The pieces but the kings: a king attacked is a check, said in its own line, and a king is never defended. */
|
|
111
|
+
function officersOf(facts) {
|
|
112
|
+
return facts.pieces.filter((piece) => piece.piece !== "K");
|
|
113
|
+
}
|
|
114
|
+
function namesOf(facts) {
|
|
115
|
+
const pieces = new Map(facts.pieces.map((piece) => [piece.square, piece]));
|
|
116
|
+
const piece = (square) => {
|
|
117
|
+
const found = pieces.get(square);
|
|
118
|
+
return found === undefined ? square : `${found.piece}${square}`;
|
|
119
|
+
};
|
|
120
|
+
const sided = (square) => {
|
|
121
|
+
const found = pieces.get(square);
|
|
122
|
+
return found === undefined ? square : `${colourName(found.side)} ${found.piece}${square}`;
|
|
123
|
+
};
|
|
124
|
+
return { piece, sided };
|
|
125
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The board of a FEN as text: White at the bottom, the pieces as Unicode figurines, the empty squares and the files as signs a browser draws as wide as the
|
|
3
|
+
* figurines and a terminal in one cell each, so that the columns stay in line in Claude's desktop app and in a terminal alike.
|
|
4
|
+
*/
|
|
5
|
+
export declare function boardText(fen: string): string;
|
|
6
|
+
/** A move in SAN as the player is shown it, with the pieces' solid figurines for either side (D-86): Nxe4 is ♞xe4 and e8=Q is e8=♛. */
|
|
7
|
+
export declare function figurinesOf(san: string): string;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// The board and the moves as text for the player in the chat (D-36), with the pieces' figurines (D-86): the moves for the AI to copy, never its view of the board (D-26).
|
|
2
|
+
/**
|
|
3
|
+
* The board of a FEN as text: White at the bottom, the pieces as Unicode figurines, the empty squares and the files as signs a browser draws as wide as the
|
|
4
|
+
* figurines and a terminal in one cell each, so that the columns stay in line in Claude's desktop app and in a terminal alike.
|
|
5
|
+
*/
|
|
6
|
+
export function boardText(fen) {
|
|
7
|
+
const ranks = (fen.split(" ")[0] ?? "").split("/").map((rank) => rank.replace(/\d/g, (empty) => ".".repeat(Number(empty))));
|
|
8
|
+
const files = ` ${fileSigns.join(" ")}`;
|
|
9
|
+
return [files, ...ranks.map((rank, index) => `${8 - index} ${[...rank].map((square) => figurines[square] ?? emptySquare).join(" ")} ${8 - index}`), files].join("\n");
|
|
10
|
+
}
|
|
11
|
+
/** A move in SAN as the player is shown it, with the pieces' solid figurines for either side (D-86): Nxe4 is ♞xe4 and e8=Q is e8=♛. */
|
|
12
|
+
export function figurinesOf(san) {
|
|
13
|
+
return san.replace(/^[KQRBN]|(?<==)[QRBN]/g, (letter) => figurines[letter.toLowerCase()] ?? letter);
|
|
14
|
+
}
|
|
15
|
+
// The pieces' figurines by their FEN letters; the black pawn's asks for its text form, since alone it shows as an emoji.
|
|
16
|
+
const figurines = { K: "♔", Q: "♕", R: "♖", B: "♗", N: "♘", P: "♙", k: "♚", q: "♛", r: "♜", b: "♝", n: "♞", p: "♟︎" };
|
|
17
|
+
const emptySquare = "⊙";
|
|
18
|
+
const fileSigns = [..."ⓐⓑⓒⓓⓔⓕⓖⓗ"];
|