chesspal 0.3.6 → 0.4.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/dist/cli/cli.js +7 -2
- package/dist/cli/command-line.js +5 -3
- package/dist/cli/prompt-hook.d.ts +17 -0
- package/dist/cli/prompt-hook.js +100 -0
- package/dist/commands/analysis/analyse.js +8 -14
- package/dist/commands/analysis/annotate.d.ts +8 -0
- package/dist/commands/analysis/annotate.js +19 -3
- package/dist/commands/answers/agents.d.ts +7 -5
- package/dist/commands/answers/agents.js +16 -9
- package/dist/commands/answers/clock.js +2 -1
- package/dist/commands/answers/drawings.js +6 -2
- package/dist/commands/answers/index.d.ts +4 -3
- package/dist/commands/answers/index.js +4 -3
- package/dist/commands/answers/lines.d.ts +5 -3
- package/dist/commands/answers/lines.js +14 -7
- package/dist/commands/answers/opening-book.d.ts +13 -7
- package/dist/commands/answers/opening-book.js +34 -38
- package/dist/commands/answers/opening-tags.d.ts +4 -0
- package/dist/commands/answers/opening-tags.js +11 -0
- package/dist/commands/answers/play-move.d.ts +3 -1
- package/dist/commands/answers/play-move.js +9 -6
- package/dist/commands/answers/say.js +2 -1
- package/dist/commands/answers/summary.d.ts +5 -2
- package/dist/commands/answers/summary.js +16 -5
- package/dist/commands/game/ai-move.js +4 -4
- package/dist/commands/game/coach.js +7 -6
- package/dist/commands/game/end.js +2 -2
- package/dist/commands/game/export.d.ts +1 -1
- package/dist/commands/game/index.d.ts +1 -1
- package/dist/commands/game/index.js +1 -1
- package/dist/commands/game/new.js +41 -11
- package/dist/commands/game/player-move.d.ts +16 -0
- package/dist/commands/game/player-move.js +69 -19
- package/dist/commands/game/ponder.js +4 -1
- package/dist/commands/game/status.d.ts +1 -1
- package/dist/commands/game/status.js +6 -6
- package/dist/commands/index.d.ts +3 -3
- package/dist/commands/index.js +3 -3
- package/dist/commands/profiles/past-games.d.ts +1 -1
- package/dist/commands/profiles/past-games.js +21 -10
- package/dist/commands/registry/context.d.ts +2 -0
- package/dist/commands/registry/context.js +5 -0
- package/dist/commands/registry/index.d.ts +1 -1
- package/dist/commands/registry/index.js +1 -1
- package/dist/commands/talk/ai-say.d.ts +1 -1
- package/dist/commands/talk/ai-say.js +6 -5
- package/dist/core/game.d.ts +13 -0
- package/dist/core/game.js +29 -3
- package/dist/core/index.d.ts +4 -4
- package/dist/core/index.js +3 -3
- package/dist/core/openings.d.ts +20 -6
- package/dist/core/openings.js +45 -22
- package/dist/core/pgn.d.ts +4 -0
- package/dist/core/pgn.js +35 -6
- package/dist/games/archive.d.ts +25 -5
- package/dist/games/archive.js +52 -19
- package/dist/games/chat-start.d.ts +2 -2
- package/dist/games/chat-start.js +6 -1
- package/dist/games/current-game.d.ts +2 -2
- package/dist/games/current-game.js +3 -2
- package/dist/games/index.d.ts +2 -1
- package/dist/games/index.js +2 -1
- package/dist/games/position-shown.d.ts +24 -0
- package/dist/games/position-shown.js +51 -0
- package/dist/home/index.d.ts +1 -1
- package/dist/home/index.js +1 -1
- package/dist/home/same-path.d.ts +6 -1
- package/dist/home/same-path.js +22 -3
- package/dist/host-files/host-files.d.ts +5 -0
- package/dist/host-files/host-files.js +13 -4
- package/dist/host-files/index.d.ts +1 -1
- package/dist/host-files/index.js +1 -1
- package/dist/page/app/app.js +30 -7
- package/dist/page/app/served.js +9 -4
- package/dist/page/avatars/torretta.css +4 -4
- package/dist/page/base.css +1 -1
- package/dist/page/board/board.css +4 -2
- package/dist/page/dialogs/dialogs.css +3 -1
- package/dist/page/game/column.js +10 -8
- package/dist/page/game/game.css +86 -6
- package/dist/page/game/plies.js +4 -0
- package/dist/page/game/review-progress.js +11 -0
- package/dist/page/guide/drawings/drawings.css +0 -1
- package/dist/page/guide/en.md +2 -2
- package/dist/page/guide/it.md +2 -2
- package/dist/page/layout/icon.ico +0 -0
- package/dist/page/layout/layout.css +32 -15
- package/dist/page/layout/window.js +41 -2
- package/dist/page/legend/legend.css +4 -16
- package/dist/page/legend/legend.js +28 -15
- package/dist/page/profile/settings.js +1 -0
- package/dist/page/table/action.js +42 -0
- package/dist/page/table/table.css +57 -39
- package/dist/page/table/window.js +67 -67
- package/dist/page/talk/talk.css +52 -9
- package/dist/page/talk/talk.js +65 -20
- package/dist/page/talk/voice.js +92 -21
- package/dist/page/text.js +24 -8
- package/dist/server/bell/bell.d.ts +24 -27
- package/dist/server/bell/bell.js +79 -143
- package/dist/server/bell/index.d.ts +1 -1
- package/dist/server/bell/index.js +2 -2
- package/dist/server/mcp/mcp.js +49 -7
- package/dist/server/page/analyses.js +15 -4
- package/dist/server/page/files.js +3 -1
- package/dist/server/page/game.d.ts +5 -0
- package/dist/server/page/game.js +78 -13
- package/dist/server/page/http.d.ts +1 -1
- package/dist/server/page/page.d.ts +14 -9
- package/dist/server/page/page.js +111 -26
- package/dist/server/page/table.d.ts +38 -4
- package/dist/server/page/table.js +61 -23
- package/dist/server/page/windows.d.ts +22 -0
- package/dist/server/page/windows.js +45 -0
- package/dist/server/ports/index.d.ts +1 -0
- package/dist/server/ports/index.js +2 -0
- package/dist/server/ports/ports.d.ts +78 -0
- package/dist/server/ports/ports.js +222 -0
- package/dist/server/table/browser.d.ts +22 -6
- package/dist/server/table/browser.js +98 -20
- package/dist/server/table/hosts/hooks.d.ts +6 -0
- package/dist/server/table/hosts/hooks.js +38 -0
- package/dist/server/table/hosts/hosts.d.ts +13 -5
- package/dist/server/table/hosts/hosts.js +16 -10
- package/dist/server/table/hosts/index.d.ts +1 -0
- package/dist/server/table/hosts/index.js +1 -0
- package/dist/server/table/hosts/play.d.ts +6 -4
- package/dist/server/table/hosts/play.js +58 -16
- package/dist/server/table/index.d.ts +1 -1
- package/dist/server/table/launch.d.ts +5 -4
- package/dist/server/table/launch.js +18 -5
- package/dist/server/table/link.js +1 -0
- package/dist/server/table/table.d.ts +7 -0
- package/dist/server/table/table.js +39 -11
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
|
@@ -1,45 +1,31 @@
|
|
|
1
|
-
// The book of the openings the build indexed (D-16), read once at the first lookup
|
|
1
|
+
// The book of the openings the build indexed (D-16), read once at the first lookup: where a game is in it (D-68) at every level, its moves for the master (D-60).
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
|
-
import { bookMoves,
|
|
3
|
+
import { bookMoves, bookPlaceOf, Game, outOfBookMove, positionKeysOf } from "../../core/index.js";
|
|
4
4
|
// The index of the named openings and the book of their moves, each read at its first lookup; null where the build made none.
|
|
5
5
|
let index;
|
|
6
6
|
let book;
|
|
7
7
|
// Each record is looked up once: the live game keeps its record until a move changes it.
|
|
8
|
-
const
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
8
|
+
const places = new WeakMap();
|
|
9
|
+
/**
|
|
10
|
+
* The game's opening as the AI reads it: ECO and name, and the move it left the book at (D-68); none outside the book, or where the build made no index.
|
|
11
|
+
* A live game gives the positions it went through; a record alone is replayed.
|
|
12
|
+
*/
|
|
13
|
+
export function openingLine(game) {
|
|
14
|
+
const place = bookPlace(game);
|
|
15
|
+
if (place?.opening === undefined) {
|
|
16
|
+
return undefined;
|
|
15
17
|
}
|
|
16
|
-
|
|
18
|
+
const left = outOfBookMove(recordOf(game).moves.length, place.plies);
|
|
19
|
+
return `${place.opening.eco} ${place.opening.name}${left === undefined ? "" : ` (out of book since move ${left})`}`;
|
|
17
20
|
}
|
|
18
21
|
/** The moves the book knows from a position (D-15), for the AI at the master level; none out of the book, or where the build made no book. */
|
|
19
22
|
export function knownMoves(fen) {
|
|
20
23
|
const moves = openingBook();
|
|
21
24
|
return moves === undefined ? [] : bookMoves(fen, moves);
|
|
22
25
|
}
|
|
23
|
-
/** How many plies from the start the book knows, to the last before the game left it (D-69); none from another position. */
|
|
24
|
-
export function bookPlies(
|
|
25
|
-
|
|
26
|
-
return 0;
|
|
27
|
-
}
|
|
28
|
-
const game = new Game({ tags: record.tags, moves: [] });
|
|
29
|
-
let plies = 0;
|
|
30
|
-
for (const { san } of record.moves) {
|
|
31
|
-
if (!game.playRecorded(san).played || !bookReaches(game.position.fen)) {
|
|
32
|
-
break;
|
|
33
|
-
}
|
|
34
|
-
plies++;
|
|
35
|
-
}
|
|
36
|
-
return plies;
|
|
37
|
-
}
|
|
38
|
-
/** Whether a line of Lichess's openings reaches a position, by any order of moves (D-68); never where the build made no book. */
|
|
39
|
-
export function bookReaches(fen) {
|
|
40
|
-
const named = openingIndex();
|
|
41
|
-
const moves = openingBook();
|
|
42
|
-
return named !== undefined && moves !== undefined && inOpenings(fen, named, moves);
|
|
26
|
+
/** How many plies from the start the book knows, to the last before the game left it (D-68, D-69); none from another position. */
|
|
27
|
+
export function bookPlies(game) {
|
|
28
|
+
return bookPlace(game)?.plies ?? 0;
|
|
43
29
|
}
|
|
44
30
|
/** The opening a game reached, ECO and name, without where it left the book: for the games in the archive. */
|
|
45
31
|
export function openingName(record) {
|
|
@@ -48,15 +34,25 @@ export function openingName(record) {
|
|
|
48
34
|
}
|
|
49
35
|
/** The last named opening a game reached, and at which ply; none outside the book, or where the build made no index. */
|
|
50
36
|
export function bookOpening(record) {
|
|
51
|
-
|
|
52
|
-
return named === undefined ? undefined : openingOf(record, named);
|
|
37
|
+
return bookPlace(record)?.opening;
|
|
53
38
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
39
|
+
/**
|
|
40
|
+
* Where a game stands in the book, its opening and its plies the book knows, once per record: from the positions a live game went through, or a record
|
|
41
|
+
* replayed; none from another position, or where the build made no index.
|
|
42
|
+
*/
|
|
43
|
+
export function bookPlace(game) {
|
|
44
|
+
const record = recordOf(game);
|
|
45
|
+
let place = places.get(record);
|
|
46
|
+
if (place === undefined) {
|
|
47
|
+
const named = openingIndex();
|
|
48
|
+
const keys = game instanceof Game ? game.positionKeys : positionKeysOf(record);
|
|
49
|
+
place = named === undefined ? null : bookPlaceOf(keys, named, openingBook()) ?? null;
|
|
50
|
+
places.set(record, place);
|
|
58
51
|
}
|
|
59
|
-
return
|
|
52
|
+
return place ?? undefined;
|
|
53
|
+
}
|
|
54
|
+
function recordOf(game) {
|
|
55
|
+
return game instanceof Game ? game.record : game;
|
|
60
56
|
}
|
|
61
57
|
function openingBook() {
|
|
62
58
|
if (book === undefined) {
|
|
@@ -64,7 +60,7 @@ function openingBook() {
|
|
|
64
60
|
book = JSON.parse(readFileSync(new URL("../../../dist/core/book.json", import.meta.url), "utf8"));
|
|
65
61
|
}
|
|
66
62
|
catch {
|
|
67
|
-
// Not built yet: the master goes without
|
|
63
|
+
// Not built yet: no game is in the book, and the master goes without its moves.
|
|
68
64
|
book = null;
|
|
69
65
|
}
|
|
70
66
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { GameRecord } from "../../core/index.ts";
|
|
2
|
+
import type { OpeningTags } from "../../games/index.ts";
|
|
3
|
+
/** The opening a game reached, in the ECO and Opening tags the archive writes: none from another position or outside the book. */
|
|
4
|
+
export declare function openingTags(record: GameRecord): OpeningTags;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { openingName } from "./opening-book.js";
|
|
2
|
+
/** The opening a game reached, in the ECO and Opening tags the archive writes: none from another position or outside the book. */
|
|
3
|
+
export function openingTags(record) {
|
|
4
|
+
const opening = openingName(record);
|
|
5
|
+
if (opening === undefined) {
|
|
6
|
+
return {};
|
|
7
|
+
}
|
|
8
|
+
// The ECO code, then the name: B50 Sicilian Defense.
|
|
9
|
+
const space = opening.indexOf(" ");
|
|
10
|
+
return { ECO: opening.slice(0, space), Opening: opening.slice(space + 1) };
|
|
11
|
+
}
|
|
@@ -14,12 +14,14 @@ export interface WithMove {
|
|
|
14
14
|
allowance?: number;
|
|
15
15
|
/** A coach judges the player's moves with the helps on (D-88): the AI reads it after the player's move. */
|
|
16
16
|
coached?: boolean;
|
|
17
|
+
/** The chat's host runs the doorbell: the AI refused a move hears the page will wake it, else that the player tells it. */
|
|
18
|
+
doorbell?: boolean;
|
|
17
19
|
}
|
|
18
20
|
/**
|
|
19
21
|
* Plays a move for one side `now` and saves the game, with the time it took and the clock after it in its comment ([%clk]). A refused move, or words refused
|
|
20
22
|
* with it, change nothing: it throws the message `refused` writes from the core's.
|
|
21
23
|
*/
|
|
22
|
-
export declare function playMove(folder: string, now: Date, mover: Mover, text: string, refused: (message: string, legal: readonly string[]) => string, { comment, said, offerDraw, allowance, coached }?: WithMove): MovePlayed;
|
|
24
|
+
export declare function playMove(folder: string, now: Date, mover: Mover, text: string, refused: (message: string, legal: readonly string[]) => string, { comment, said, offerDraw, allowance, coached, doorbell }?: WithMove): MovePlayed;
|
|
23
25
|
/** A chat's move, resignation or game replaced on a position it has not seen (D-71): it was decided on another. */
|
|
24
26
|
export declare class PositionChanged extends Error {
|
|
25
27
|
constructor();
|
|
@@ -1,21 +1,24 @@
|
|
|
1
1
|
// A move of the AI or of the player on the current game: played, timed by the clock, saved, and archived when it ends the game.
|
|
2
2
|
import { clockAfterMove, clockTag } from "../../core/index.js";
|
|
3
3
|
import { archiveEnded, endOf, loadGame, saveGame, talkLine } from "../../games/index.js";
|
|
4
|
+
import { toldMove } from "./lines.js";
|
|
5
|
+
import { openingTags } from "./opening-tags.js";
|
|
4
6
|
import { gameGoingOn, summaryOf, withThought } from "./summary.js";
|
|
5
7
|
import { colourName } from "./view.js";
|
|
6
8
|
/**
|
|
7
9
|
* Plays a move for one side `now` and saves the game, with the time it took and the clock after it in its comment ([%clk]). A refused move, or words refused
|
|
8
10
|
* with it, change nothing: it throws the message `refused` writes from the core's.
|
|
9
11
|
*/
|
|
10
|
-
export function playMove(folder, now, mover, text, refused, { comment, said, offerDraw = false, allowance = 0, coached = false } = {}) {
|
|
12
|
+
export function playMove(folder, now, mover, text, refused, { comment, said, offerDraw = false, allowance = 0, coached = false, doorbell = true } = {}) {
|
|
11
13
|
// A flag falls only past the allowance.
|
|
12
|
-
const before = gameToMove(folder, mover, new Date(now.getTime() - allowance));
|
|
14
|
+
const before = gameToMove(folder, mover, new Date(now.getTime() - allowance), doorbell);
|
|
13
15
|
const { game } = before;
|
|
14
16
|
const position = game.position;
|
|
15
17
|
// The words are checked before the move, which changes the live game; they count as said after it.
|
|
16
18
|
const line = said === undefined ? undefined : talkLine(before, mover, said.text, said.at, game.record.moves.length + 1);
|
|
17
19
|
if (mover === "ai" && before.clock?.waiting === true) {
|
|
18
|
-
|
|
20
|
+
const then = doorbell ? "the doorbell wakes you then" : "call status when they tell you they did";
|
|
21
|
+
throw new Error(`The clocks wait for the player to start the game with Start on the board page: don't move yet, ${then}. Nothing changed.`);
|
|
19
22
|
}
|
|
20
23
|
if (offerDraw && game.record.moves.length === 0) {
|
|
21
24
|
throw new Error("A draw can be offered once each side has made a move: play the move without offer_draw. Nothing changed.");
|
|
@@ -37,7 +40,7 @@ export function playMove(folder, now, mover, text, refused, { comment, said, off
|
|
|
37
40
|
current.talk.push(line);
|
|
38
41
|
}
|
|
39
42
|
saveGame(folder, current);
|
|
40
|
-
const { archive, warning } = endOf(game) === undefined ? {} : archiveEnded(folder, current);
|
|
43
|
+
const { archive, warning } = endOf(game) === undefined ? {} : archiveEnded(folder, current, openingTags(game.record));
|
|
41
44
|
const result = { move: `${position.moveNumber}${position.turn === "white" ? "." : "..."}${played.move.san}`, game: withThought(summaryOf(current, now, archive), folder, current, coached) };
|
|
42
45
|
if (played.note !== undefined) {
|
|
43
46
|
result.note = played.note;
|
|
@@ -80,7 +83,7 @@ export function refuseOtherGame(context, folder) {
|
|
|
80
83
|
export function samePosition(one, other) {
|
|
81
84
|
return one.started === other.started && one.plies === other.plies;
|
|
82
85
|
}
|
|
83
|
-
function gameToMove(folder, mover, now) {
|
|
86
|
+
function gameToMove(folder, mover, now, doorbell) {
|
|
84
87
|
const current = gameGoingOn(folder, now);
|
|
85
88
|
const { turn } = current.game.position;
|
|
86
89
|
const aiToMove = turn === current.ai;
|
|
@@ -88,7 +91,7 @@ function gameToMove(folder, mover, now) {
|
|
|
88
91
|
throw new Error(`It is your move (${colourName(turn)}): call ai_move. Nothing changed.`);
|
|
89
92
|
}
|
|
90
93
|
if (mover === "ai" && !aiToMove) {
|
|
91
|
-
throw new Error(`It is the player's move (${colourName(turn)}): wait for it, then call player_move. Nothing changed.`);
|
|
94
|
+
throw new Error(`It is the player's move (${colourName(turn)}): ${doorbell ? "wait for it, then call player_move." : toldMove} Nothing changed.`);
|
|
92
95
|
}
|
|
93
96
|
return current;
|
|
94
97
|
}
|
|
@@ -4,6 +4,7 @@ import { archiveEnded, endOf, gameInAnalysis, loadGame, loadTableTalk, saveGame,
|
|
|
4
4
|
import { gameFolder, helpsIn } from "../../profiles/index.js";
|
|
5
5
|
import { gameAt } from "./clock.js";
|
|
6
6
|
import { shownMove } from "./lines.js";
|
|
7
|
+
import { openingTags } from "./opening-tags.js";
|
|
7
8
|
/**
|
|
8
9
|
* 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).
|
|
9
10
|
* 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.
|
|
@@ -24,7 +25,7 @@ export function say(folder, who, text, at, mood, shown) {
|
|
|
24
25
|
}
|
|
25
26
|
saveGame(folder, current);
|
|
26
27
|
if (mood !== undefined) {
|
|
27
|
-
archiveEnded(folder, current);
|
|
28
|
+
archiveEnded(folder, current, openingTags(current.game.record));
|
|
28
29
|
}
|
|
29
30
|
return current;
|
|
30
31
|
}
|
|
@@ -77,10 +77,13 @@ export declare function withThought(summary: GameSummary, folder: string, curren
|
|
|
77
77
|
export declare function gameGoingOn(folder: string, now: Date): CurrentGame;
|
|
78
78
|
/** Whether a game has no move yet, or was closed before any: nothing to go over. */
|
|
79
79
|
export declare function unplayed(summary: GameSummary): boolean;
|
|
80
|
-
/**
|
|
80
|
+
/**
|
|
81
|
+
* How a game ended, in words: its result, and how it came when known, each colour named with who played it, the AI as `aiName`:
|
|
82
|
+
* "0-1, White (the player) resigns", "1-0, White (you) checkmates Black (the player)".
|
|
83
|
+
*/
|
|
81
84
|
export declare function endText({ result, termination }: {
|
|
82
85
|
result: string;
|
|
83
86
|
termination?: string;
|
|
84
|
-
}): string;
|
|
87
|
+
}, ai: Side, aiName?: string): string;
|
|
85
88
|
/** The last plies of a record, numbered as a player writes them, from the move number and side of its FEN. */
|
|
86
89
|
export declare function lastMoves(record: GameRecord, count: number): string;
|
|
@@ -14,7 +14,7 @@ export function summaryOf(current, now, archive) {
|
|
|
14
14
|
if (lastMove !== undefined) {
|
|
15
15
|
summary.lastMove = { move: lastMoves(record, 1), from: lastMove.from, to: lastMove.to };
|
|
16
16
|
}
|
|
17
|
-
const opening = openingLine(
|
|
17
|
+
const opening = openingLine(game);
|
|
18
18
|
const clock = clockSummaryOf(current, now);
|
|
19
19
|
const drawOffer = drawOfferOf(current);
|
|
20
20
|
if (drawOffer !== undefined) {
|
|
@@ -58,7 +58,7 @@ export function gameGoingOn(folder, now) {
|
|
|
58
58
|
}
|
|
59
59
|
const end = endOf(current.game);
|
|
60
60
|
if (end !== undefined) {
|
|
61
|
-
throw new Error(`The game is over (${endText(end)}): new_game starts another. Nothing changed.`);
|
|
61
|
+
throw new Error(`The game is over (${endText(end, current.ai)}): new_game starts another. Nothing changed.`);
|
|
62
62
|
}
|
|
63
63
|
return current;
|
|
64
64
|
}
|
|
@@ -66,9 +66,20 @@ export function gameGoingOn(folder, now) {
|
|
|
66
66
|
export function unplayed(summary) {
|
|
67
67
|
return summary.lastMoves === "";
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
69
|
+
/**
|
|
70
|
+
* How a game ended, in words: its result, and how it came when known, each colour named with who played it, the AI as `aiName`:
|
|
71
|
+
* "0-1, White (the player) resigns", "1-0, White (you) checkmates Black (the player)".
|
|
72
|
+
*/
|
|
73
|
+
export function endText({ result, termination }, ai, aiName = "you") {
|
|
74
|
+
if (termination === undefined) {
|
|
75
|
+
return result;
|
|
76
|
+
}
|
|
77
|
+
const named = (colour) => `${colour} (${colour.toLowerCase() === ai ? aiName : "the player"})`;
|
|
78
|
+
if (termination === "checkmate" && result !== "1/2-1/2") {
|
|
79
|
+
const winner = result === "1-0" ? "White" : "Black";
|
|
80
|
+
return `${result}, ${named(winner)} checkmates ${named(winner === "White" ? "Black" : "White")}`;
|
|
81
|
+
}
|
|
82
|
+
return `${result}, ${termination.replace(/\b(White|Black)\b/g, named)}`;
|
|
72
83
|
}
|
|
73
84
|
/** The last plies of a record, numbered as a player writes them, from the move number and side of its FEN. */
|
|
74
85
|
export function lastMoves(record, count) {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { boardText, drawingsText, moods, moodTag } from "../../core/index.js";
|
|
3
3
|
import { commentWithPlan, talkLimits } from "../../games/index.js";
|
|
4
|
-
import { coachAgent, ponderAgentFor } from "../../host-files/index.js";
|
|
4
|
+
import { coachAgent, hasDoorbell, ponderAgentFor } from "../../host-files/index.js";
|
|
5
5
|
import { gameFolder, helpsIn } from "../../profiles/index.js";
|
|
6
6
|
import { agentRuns, agentsLine, arrowsInput, closingLines, coachRuns, drawingsOf, give, moveLines, playMove, refuseUnseenPosition, shownMove, squaresInput, waitingWords, withAnalyst } from "../answers/index.js";
|
|
7
7
|
import { defineCommand, sentence } from "../registry/index.js";
|
|
@@ -36,12 +36,12 @@ export const aiMove = defineCommand({
|
|
|
36
36
|
// The player's words are read before the move, which would count as the answer to them; a move refused leaves them for the next call.
|
|
37
37
|
const waiting = waitingWords(context);
|
|
38
38
|
const words = [commentWithPlan(comment, plan), drawingsText(drawings), mood === undefined ? "" : moodTag(mood)].filter((part) => part !== "").join(" ");
|
|
39
|
-
const played = playMove(folder, context.now(), "ai", move, (message, legal) => `${message} Legal: ${legal.join(" ")}. Nothing changed.`, { comment: words, offerDraw });
|
|
39
|
+
const played = playMove(folder, context.now(), "ai", move, (message, legal) => `${message} Legal: ${legal.join(" ")}. Nothing changed.`, { comment: words, offerDraw, doorbell: hasDoorbell(context.host?.()) });
|
|
40
40
|
const agents = agentsAfter(context, folder, played);
|
|
41
41
|
const board = context.pageOpen?.() === true ? {} : { board: true };
|
|
42
42
|
return { ...played, game: withAnalyst(played.game, context), words: give(context, waiting), ...(agents.length === 0 ? {} : { agents }), ...board };
|
|
43
43
|
},
|
|
44
|
-
format: (result) => {
|
|
44
|
+
format: (result, context) => {
|
|
45
45
|
// The mood the AI has just given is not told back: player_move and status tell it while it lasts (Priya).
|
|
46
46
|
const game = { ...result.game };
|
|
47
47
|
delete game.mood;
|
|
@@ -51,7 +51,7 @@ export const aiMove = defineCommand({
|
|
|
51
51
|
...moveLines({ ...result, game }),
|
|
52
52
|
...(result.board === true ? ["No board page is open: copy this board into your message.", boardText(result.game.fen)] : []),
|
|
53
53
|
...(result.agents === undefined ? [] : [agentsLine(result.agents)]),
|
|
54
|
-
...closingLines(result.game, result.words),
|
|
54
|
+
...closingLines(result.game, result.words, false, hasDoorbell(context?.host?.())),
|
|
55
55
|
].join("\n");
|
|
56
56
|
},
|
|
57
57
|
});
|
|
@@ -3,7 +3,7 @@ import { badges, Game, leadText, levelOf, materialOf, withSanLetters } from "../
|
|
|
3
3
|
import { archiveCoached, coachWaitingSince, coachWords, endOf, keepHint, keepJudgment, keepLead, loadGame, startCoach } from "../../games/index.js";
|
|
4
4
|
import { agentHosts, coachAgent } from "../../host-files/index.js";
|
|
5
5
|
import { activeProfile, gameFolder, helpsIn } from "../../profiles/index.js";
|
|
6
|
-
import { aheadWords, arrowsInput, clockLine, clockSummaryOf, colourName, compactView, drawingsOf, knownMoves, lastMoves, leadFrom, leadInput, materialText, noteInput, openingLine, riskLine, speechLine, speechOf, squaresInput, } from "../answers/index.js";
|
|
6
|
+
import { aheadWords, arrowsInput, clockLine, clockSummaryOf, colourName, compactView, drawingsOf, endText, knownMoves, lastMoves, leadFrom, leadInput, materialText, noteInput, openingLine, openingTags, riskLine, speechLine, speechOf, squaresInput, } from "../answers/index.js";
|
|
7
7
|
import { ratingsText } from "../profiles/index.js";
|
|
8
8
|
import { defineCommand } from "../registry/index.js";
|
|
9
9
|
import { waitForPlayer, wordsOf } from "./ponder.js";
|
|
@@ -149,7 +149,7 @@ function startOf(current, folder, context) {
|
|
|
149
149
|
const head = over === undefined
|
|
150
150
|
? `Game "${place.game}", position ${plies}: the player (${colourName(player)}) to move, move ${game.position.moveNumber}; the opponent plays ${colourName(ai)} at the `
|
|
151
151
|
+ `${levelOf(record)} level.`
|
|
152
|
-
: `Game "${place.game}", position ${plies}: the game is over, ${over
|
|
152
|
+
: `Game "${place.game}", position ${plies}: the game is over, ${endText(over, ai, "the opponent")}, with ${ender} move.`;
|
|
153
153
|
const speech = speechOf(folder, context.language);
|
|
154
154
|
const clock = clockSummaryOf(current, context.now());
|
|
155
155
|
const keepLine = over === undefined
|
|
@@ -160,7 +160,7 @@ function startOf(current, folder, context) {
|
|
|
160
160
|
head,
|
|
161
161
|
speech === undefined ? "The player speaks English." : speechLine(speech),
|
|
162
162
|
...playerLines(context),
|
|
163
|
-
...movesLines(
|
|
163
|
+
...movesLines(game),
|
|
164
164
|
...(clock === undefined ? [] : [clockLine(clock, ai)]),
|
|
165
165
|
...(judged === undefined ? [] : judgedLines(current, judged, player)),
|
|
166
166
|
...(over === undefined ? nowLines(current, folder, player) : []),
|
|
@@ -168,8 +168,9 @@ function startOf(current, folder, context) {
|
|
|
168
168
|
];
|
|
169
169
|
}
|
|
170
170
|
/** The game's moves as the coach reads them, with the position a game set up began in, the last ones of a long game, and its opening by the book (Anatolij). */
|
|
171
|
-
function movesLines(
|
|
172
|
-
const
|
|
171
|
+
function movesLines(game) {
|
|
172
|
+
const { record } = game;
|
|
173
|
+
const opening = openingLine(game);
|
|
173
174
|
return [
|
|
174
175
|
...(record.tags.FEN === undefined ? [] : [`The game began from a set position: ${record.tags.FEN}`]),
|
|
175
176
|
`${record.moves.length > pliesShown ? `Moves (the last ${pliesShown} plies)` : "Moves"}: ${lastMoves(record, pliesShown)}`,
|
|
@@ -291,7 +292,7 @@ function keep(folder, current, place, { ahead, badge, note, hint, arrows, square
|
|
|
291
292
|
const hintLeft = hint !== undefined && !toMove;
|
|
292
293
|
if (over) {
|
|
293
294
|
// The game went to the archive with the move that ended it: the judgment goes there too.
|
|
294
|
-
archiveCoached(folder, current);
|
|
295
|
+
archiveCoached(folder, current, openingTags(current.game.record));
|
|
295
296
|
return { kept: place, then: "over", saved, hintLeft };
|
|
296
297
|
}
|
|
297
298
|
if (!toMove) {
|
|
@@ -2,7 +2,7 @@ import { z } from "zod";
|
|
|
2
2
|
import { canMate, endWithMood, Game, moods, timeLeft } from "../../core/index.js";
|
|
3
3
|
import { archiveEnded, bothSidesMoved, drawOfferOf, saveGame } from "../../games/index.js";
|
|
4
4
|
import { gameFolder } from "../../profiles/index.js";
|
|
5
|
-
import { colourName, gameGoingOn, nextStep, positionLines, refuseUnseenPosition, summaryOf, withAnalyst } from "../answers/index.js";
|
|
5
|
+
import { colourName, gameGoingOn, nextStep, openingTags, positionLines, refuseUnseenPosition, summaryOf, withAnalyst } from "../answers/index.js";
|
|
6
6
|
import { defineCommand } from "../registry/index.js";
|
|
7
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
8
|
export const endGame = defineCommand({
|
|
@@ -35,7 +35,7 @@ export const endGame = defineCommand({
|
|
|
35
35
|
? { ...current, game }
|
|
36
36
|
: { ...current, game, clock: { ...current.clock, left: timeLeft(current.clock, current.game.position.turn, now), since: now.toISOString() } };
|
|
37
37
|
saveGame(folder, ended);
|
|
38
|
-
const { archive, warning } = archiveEnded(folder, ended);
|
|
38
|
+
const { archive, warning } = archiveEnded(folder, ended, openingTags(ended.game.record));
|
|
39
39
|
const summary = withAnalyst(summaryOf(ended, now, archive), context);
|
|
40
40
|
return warning === undefined ? { game: summary } : { game: summary, warning };
|
|
41
41
|
},
|
|
@@ -6,9 +6,9 @@ interface Exported {
|
|
|
6
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. */
|
|
7
7
|
export declare const exportGame: import("../registry/registry.ts").Command<z.ZodObject<{
|
|
8
8
|
format: z.ZodEnum<{
|
|
9
|
-
fen: "fen";
|
|
10
9
|
moves: "moves";
|
|
11
10
|
pgn: "pgn";
|
|
11
|
+
fen: "fen";
|
|
12
12
|
}>;
|
|
13
13
|
}, z.core.$strip>, Exported>;
|
|
14
14
|
export {};
|
|
@@ -5,7 +5,7 @@ export { endGame } from "./end.ts";
|
|
|
5
5
|
export { exportGame } from "./export.ts";
|
|
6
6
|
export { look } from "./look.ts";
|
|
7
7
|
export { ChatNotClear, newGame, startNewGame } from "./new.ts";
|
|
8
|
-
export { playerMove } from "./player-move.ts";
|
|
8
|
+
export { type MoveSentAlone, playerMove, playMoveSentAlone } from "./player-move.ts";
|
|
9
9
|
export { ponder } from "./ponder.ts";
|
|
10
10
|
export { position } from "./position.ts";
|
|
11
11
|
export { status } from "./status.ts";
|
|
@@ -7,7 +7,7 @@ export { endGame } from "./end.js";
|
|
|
7
7
|
export { exportGame } from "./export.js";
|
|
8
8
|
export { look } from "./look.js";
|
|
9
9
|
export { ChatNotClear, newGame, startNewGame } from "./new.js";
|
|
10
|
-
export { playerMove } from "./player-move.js";
|
|
10
|
+
export { playerMove, playMoveSentAlone } from "./player-move.js";
|
|
11
11
|
export { ponder } from "./ponder.js";
|
|
12
12
|
export { position } from "./position.js";
|
|
13
13
|
export { status } from "./status.js";
|
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
import { existsSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { z } from "zod";
|
|
2
4
|
import { aiLevels, Game, levelTag, pgnDate, readPgn, startClock, timeControlOf, timeControlTag, withoutMoods } from "../../core/index.js";
|
|
3
|
-
import { archivedName, archiveGame, cleanSince, endAnalysis, endOf, endTableTalk, lastChatStart, loadGame, saveGame, startOfNewGame, withoutPlans } from "../../games/index.js";
|
|
5
|
+
import { archivedName, archiveGame, archiveName, cleanSince, endAnalysis, endOf, endTableTalk, lastChatStart, loadGame, saveGame, startOfNewGame, withoutPlans } from "../../games/index.js";
|
|
6
|
+
import { hasDoorbell } from "../../host-files/index.js";
|
|
4
7
|
import { gameFolder, gameFolders } from "../../profiles/index.js";
|
|
5
|
-
import { coachRuns, gameAt, nextStep, positionLines, refuseUnseenPosition, sidesLine, speechLine, speechOf, summaryOf, withThought } from "../answers/index.js";
|
|
8
|
+
import { coachRuns, gameAt, nextStep, openingTags, positionLines, refuseUnseenPosition, sidesLine, speechLine, speechOf, summaryOf, withThought } from "../answers/index.js";
|
|
6
9
|
import { pastGameLines, playerLine } from "../profiles/index.js";
|
|
7
10
|
import { defineCommand } from "../registry/index.js";
|
|
8
11
|
import { levelText, levelToPlay } from "./levels.js";
|
|
@@ -37,7 +40,7 @@ export const newGame = defineCommand({
|
|
|
37
40
|
}
|
|
38
41
|
return startNewGame(input, context, "chat");
|
|
39
42
|
},
|
|
40
|
-
format: (result) => "window" in result ? askedLine(result) : startedLines(result),
|
|
43
|
+
format: (result, context) => "window" in result ? askedLine(result, hasDoorbell(context?.host?.())) : startedLines(result, hasDoorbell(context?.host?.())),
|
|
41
44
|
});
|
|
42
45
|
/**
|
|
43
46
|
* Starts the new game with the player's choices: those the AI asked in the chat, or the page's window gave (D-76). Only the chat's is refused on a game
|
|
@@ -71,8 +74,7 @@ export function startNewGame({ ai_colour: colour, from, clock, level: chosen, re
|
|
|
71
74
|
const current = control === undefined
|
|
72
75
|
? { game, ai, started: started.toISOString(), talk: [] }
|
|
73
76
|
: { game, ai, started: started.toISOString(), talk: [], clock: startClock(control, now, context.pageOpen?.() === true) };
|
|
74
|
-
const archived =
|
|
75
|
-
saveGame(folder, current);
|
|
77
|
+
const archived = replaceGame(folder, current, before, going !== undefined);
|
|
76
78
|
endAnalysis(folder);
|
|
77
79
|
endTableTalk(folder);
|
|
78
80
|
const past = pastGameLines(folder, remembered);
|
|
@@ -92,7 +94,7 @@ export function startNewGame({ ai_colour: colour, from, clock, level: chosen, re
|
|
|
92
94
|
};
|
|
93
95
|
}
|
|
94
96
|
/** The new game's lines for the AI. */
|
|
95
|
-
function startedLines({ game, archived, past, player, speech, closedPage: closed }) {
|
|
97
|
+
function startedLines({ game, archived, past, player, speech, closedPage: closed }, doorbell) {
|
|
96
98
|
return [
|
|
97
99
|
`New game: ${sidesLine(game.ai)}, ${levelText(game.level)}.`,
|
|
98
100
|
...(player === undefined ? [] : [player]),
|
|
@@ -101,7 +103,7 @@ function startedLines({ game, archived, past, player, speech, closedPage: closed
|
|
|
101
103
|
...(past.length === 0 ? [] : ["Last games with this player (past_games gives more, and their moves):", ...past]),
|
|
102
104
|
...positionLines(game),
|
|
103
105
|
...(closed === undefined ? [] : [pageLine(closed, true)]),
|
|
104
|
-
nextStep(game),
|
|
106
|
+
nextStep(game, [], false, doorbell),
|
|
105
107
|
].join("\n");
|
|
106
108
|
}
|
|
107
109
|
/**
|
|
@@ -131,11 +133,16 @@ function justBegun(context) {
|
|
|
131
133
|
const current = gameAt(gameFolder(context.home()), context.now());
|
|
132
134
|
return current !== undefined && endOf(current.game) === undefined && current.game.record.moves.length === 0;
|
|
133
135
|
}
|
|
134
|
-
function askedLine({ window, page }) {
|
|
135
|
-
|
|
136
|
+
function askedLine({ window, page }, doorbell) {
|
|
137
|
+
// A host with no doorbell hears of the game from the player, in its window.
|
|
138
|
+
const told = "End your turn, and call status when they tell you the game started.";
|
|
139
|
+
if (window !== "opened") {
|
|
140
|
+
return `Nobody has the board page open: give the player its address, ${page}, and tell them to start the game there with its New game button. ${doorbell ? "End your turn." : told}`;
|
|
141
|
+
}
|
|
142
|
+
return doorbell
|
|
136
143
|
? "The New game window is open on the board page: the player chooses the game there. Don't ask in the chat; end your turn, the doorbell wakes you when "
|
|
137
144
|
+ "the game starts."
|
|
138
|
-
: `
|
|
145
|
+
: `The New game window is open on the board page: the player chooses the game there. Don't ask in the chat. ${told}`;
|
|
139
146
|
}
|
|
140
147
|
/** The refusal of a new game in a chat that still holds the game before (D-57): the board page says it in the player's language. */
|
|
141
148
|
export class ChatNotClear extends Error {
|
|
@@ -164,9 +171,32 @@ function chatToClear(context) {
|
|
|
164
171
|
}
|
|
165
172
|
// The hosts whose SessionStart hook, written by init, notes how each chat starts: Claude Code and Codex, by the names they give the server.
|
|
166
173
|
const hostsNotingChats = new Set(["claude-code", "codex-mcp-client"]);
|
|
174
|
+
/**
|
|
175
|
+
* Writes the new game over the one before, archived first: both under game.json's lock, on the game before as read, so a game another process changed
|
|
176
|
+
* since is neither archived stale nor lost. When game.json cannot be written, an archive this call made goes: nothing changed.
|
|
177
|
+
*/
|
|
178
|
+
function replaceGame(folder, current, before, going) {
|
|
179
|
+
let archived;
|
|
180
|
+
let made = false;
|
|
181
|
+
try {
|
|
182
|
+
saveGame(folder, current, () => {
|
|
183
|
+
if (before !== undefined && (going || archivedName(folder, before) === undefined)) {
|
|
184
|
+
made = !existsSync(join(folder, archiveName(before)));
|
|
185
|
+
archived = archiveBefore(folder, before);
|
|
186
|
+
}
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
catch (error) {
|
|
190
|
+
if (made && archived !== undefined) {
|
|
191
|
+
rmSync(join(folder, archived), { force: true });
|
|
192
|
+
}
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
return archived;
|
|
196
|
+
}
|
|
167
197
|
function archiveBefore(folder, before) {
|
|
168
198
|
try {
|
|
169
|
-
return archiveGame(folder, before);
|
|
199
|
+
return archiveGame(folder, before, openingTags(before.game.record));
|
|
170
200
|
}
|
|
171
201
|
catch (error) {
|
|
172
202
|
throw new Error(`${error.message} Nothing changed.`, { cause: error });
|
|
@@ -1,10 +1,26 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { type MovePlayed } from "../answers/index.ts";
|
|
3
|
+
import { type Context, type SeenPosition } from "../registry/index.ts";
|
|
3
4
|
/** The player's move, written in the chat and given by the AI: it answers with the position the AI decides on (D-34). */
|
|
4
5
|
export declare const playerMove: import("../registry/registry.ts").Command<z.ZodObject<{
|
|
5
6
|
move: z.ZodString;
|
|
6
7
|
offer_draw: z.ZodOptional<z.ZodBoolean>;
|
|
7
8
|
}, z.core.$strip>, PlayerMove>;
|
|
9
|
+
/**
|
|
10
|
+
* A move the player sent alone in the chat, played by Chesspal before the AI reads it: what the AI is told, the position it then sees, the stamp of
|
|
11
|
+
* game.json after it, and the keys of the player's words on the board page given with it, for the chat's server to count them as given.
|
|
12
|
+
*/
|
|
13
|
+
export interface MoveSentAlone {
|
|
14
|
+
answer: string;
|
|
15
|
+
position: SeenPosition;
|
|
16
|
+
stamp?: string;
|
|
17
|
+
given: string[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Plays the move the player sent alone in the chat, before the AI reads it (D-36): at the moment they sent it, so with no allowance, and answered as
|
|
21
|
+
* player_move answers, with the player's words on the board page the AI has not answered. Refused as player_move refuses it: the AI reads the prompt then.
|
|
22
|
+
*/
|
|
23
|
+
export declare function playMoveSentAlone(move: string, context: Context): MoveSentAlone;
|
|
8
24
|
/** The player's move, with the board page's address once a game while no browser has it open (D-85), and the coach to start when it ended the game (D-88). */
|
|
9
25
|
interface PlayerMove extends MovePlayed {
|
|
10
26
|
invite?: string;
|