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.
Files changed (136) hide show
  1. package/dist/cli/cli.js +7 -2
  2. package/dist/cli/command-line.js +5 -3
  3. package/dist/cli/prompt-hook.d.ts +17 -0
  4. package/dist/cli/prompt-hook.js +100 -0
  5. package/dist/commands/analysis/analyse.js +8 -14
  6. package/dist/commands/analysis/annotate.d.ts +8 -0
  7. package/dist/commands/analysis/annotate.js +19 -3
  8. package/dist/commands/answers/agents.d.ts +7 -5
  9. package/dist/commands/answers/agents.js +16 -9
  10. package/dist/commands/answers/clock.js +2 -1
  11. package/dist/commands/answers/drawings.js +6 -2
  12. package/dist/commands/answers/index.d.ts +4 -3
  13. package/dist/commands/answers/index.js +4 -3
  14. package/dist/commands/answers/lines.d.ts +5 -3
  15. package/dist/commands/answers/lines.js +14 -7
  16. package/dist/commands/answers/opening-book.d.ts +13 -7
  17. package/dist/commands/answers/opening-book.js +34 -38
  18. package/dist/commands/answers/opening-tags.d.ts +4 -0
  19. package/dist/commands/answers/opening-tags.js +11 -0
  20. package/dist/commands/answers/play-move.d.ts +3 -1
  21. package/dist/commands/answers/play-move.js +9 -6
  22. package/dist/commands/answers/say.js +2 -1
  23. package/dist/commands/answers/summary.d.ts +5 -2
  24. package/dist/commands/answers/summary.js +16 -5
  25. package/dist/commands/game/ai-move.js +4 -4
  26. package/dist/commands/game/coach.js +7 -6
  27. package/dist/commands/game/end.js +2 -2
  28. package/dist/commands/game/export.d.ts +1 -1
  29. package/dist/commands/game/index.d.ts +1 -1
  30. package/dist/commands/game/index.js +1 -1
  31. package/dist/commands/game/new.js +41 -11
  32. package/dist/commands/game/player-move.d.ts +16 -0
  33. package/dist/commands/game/player-move.js +69 -19
  34. package/dist/commands/game/ponder.js +4 -1
  35. package/dist/commands/game/status.d.ts +1 -1
  36. package/dist/commands/game/status.js +6 -6
  37. package/dist/commands/index.d.ts +3 -3
  38. package/dist/commands/index.js +3 -3
  39. package/dist/commands/profiles/past-games.d.ts +1 -1
  40. package/dist/commands/profiles/past-games.js +21 -10
  41. package/dist/commands/registry/context.d.ts +2 -0
  42. package/dist/commands/registry/context.js +5 -0
  43. package/dist/commands/registry/index.d.ts +1 -1
  44. package/dist/commands/registry/index.js +1 -1
  45. package/dist/commands/talk/ai-say.d.ts +1 -1
  46. package/dist/commands/talk/ai-say.js +6 -5
  47. package/dist/core/game.d.ts +13 -0
  48. package/dist/core/game.js +29 -3
  49. package/dist/core/index.d.ts +4 -4
  50. package/dist/core/index.js +3 -3
  51. package/dist/core/openings.d.ts +20 -6
  52. package/dist/core/openings.js +45 -22
  53. package/dist/core/pgn.d.ts +4 -0
  54. package/dist/core/pgn.js +35 -6
  55. package/dist/games/archive.d.ts +25 -5
  56. package/dist/games/archive.js +52 -19
  57. package/dist/games/chat-start.d.ts +2 -2
  58. package/dist/games/chat-start.js +6 -1
  59. package/dist/games/current-game.d.ts +2 -2
  60. package/dist/games/current-game.js +3 -2
  61. package/dist/games/index.d.ts +2 -1
  62. package/dist/games/index.js +2 -1
  63. package/dist/games/position-shown.d.ts +24 -0
  64. package/dist/games/position-shown.js +51 -0
  65. package/dist/home/index.d.ts +1 -1
  66. package/dist/home/index.js +1 -1
  67. package/dist/home/same-path.d.ts +6 -1
  68. package/dist/home/same-path.js +22 -3
  69. package/dist/host-files/host-files.d.ts +5 -0
  70. package/dist/host-files/host-files.js +13 -4
  71. package/dist/host-files/index.d.ts +1 -1
  72. package/dist/host-files/index.js +1 -1
  73. package/dist/page/app/app.js +30 -7
  74. package/dist/page/app/served.js +9 -4
  75. package/dist/page/avatars/torretta.css +4 -4
  76. package/dist/page/base.css +1 -1
  77. package/dist/page/board/board.css +4 -2
  78. package/dist/page/dialogs/dialogs.css +3 -1
  79. package/dist/page/game/column.js +10 -8
  80. package/dist/page/game/game.css +86 -6
  81. package/dist/page/game/plies.js +4 -0
  82. package/dist/page/game/review-progress.js +11 -0
  83. package/dist/page/guide/drawings/drawings.css +0 -1
  84. package/dist/page/guide/en.md +2 -2
  85. package/dist/page/guide/it.md +2 -2
  86. package/dist/page/layout/icon.ico +0 -0
  87. package/dist/page/layout/layout.css +32 -15
  88. package/dist/page/layout/window.js +41 -2
  89. package/dist/page/legend/legend.css +4 -16
  90. package/dist/page/legend/legend.js +28 -15
  91. package/dist/page/profile/settings.js +1 -0
  92. package/dist/page/table/action.js +42 -0
  93. package/dist/page/table/table.css +57 -39
  94. package/dist/page/table/window.js +67 -67
  95. package/dist/page/talk/talk.css +52 -9
  96. package/dist/page/talk/talk.js +65 -20
  97. package/dist/page/talk/voice.js +92 -21
  98. package/dist/page/text.js +24 -8
  99. package/dist/server/bell/bell.d.ts +24 -27
  100. package/dist/server/bell/bell.js +79 -143
  101. package/dist/server/bell/index.d.ts +1 -1
  102. package/dist/server/bell/index.js +2 -2
  103. package/dist/server/mcp/mcp.js +49 -7
  104. package/dist/server/page/analyses.js +15 -4
  105. package/dist/server/page/files.js +3 -1
  106. package/dist/server/page/game.d.ts +5 -0
  107. package/dist/server/page/game.js +78 -13
  108. package/dist/server/page/http.d.ts +1 -1
  109. package/dist/server/page/page.d.ts +14 -9
  110. package/dist/server/page/page.js +111 -26
  111. package/dist/server/page/table.d.ts +38 -4
  112. package/dist/server/page/table.js +61 -23
  113. package/dist/server/page/windows.d.ts +22 -0
  114. package/dist/server/page/windows.js +45 -0
  115. package/dist/server/ports/index.d.ts +1 -0
  116. package/dist/server/ports/index.js +2 -0
  117. package/dist/server/ports/ports.d.ts +78 -0
  118. package/dist/server/ports/ports.js +222 -0
  119. package/dist/server/table/browser.d.ts +22 -6
  120. package/dist/server/table/browser.js +98 -20
  121. package/dist/server/table/hosts/hooks.d.ts +6 -0
  122. package/dist/server/table/hosts/hooks.js +38 -0
  123. package/dist/server/table/hosts/hosts.d.ts +13 -5
  124. package/dist/server/table/hosts/hosts.js +16 -10
  125. package/dist/server/table/hosts/index.d.ts +1 -0
  126. package/dist/server/table/hosts/index.js +1 -0
  127. package/dist/server/table/hosts/play.d.ts +6 -4
  128. package/dist/server/table/hosts/play.js +58 -16
  129. package/dist/server/table/index.d.ts +1 -1
  130. package/dist/server/table/launch.d.ts +5 -4
  131. package/dist/server/table/launch.js +18 -5
  132. package/dist/server/table/link.js +1 -0
  133. package/dist/server/table/table.d.ts +7 -0
  134. package/dist/server/table/table.js +39 -11
  135. package/instructions/opponent.md +1 -1
  136. package/package.json +1 -1
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
- import { loadGame } from "../../games/index.js";
2
+ import { withSanPieces } from "../../core/index.js";
3
+ import { endOf, gameStamp, loadGame, rerunIfChanged } from "../../games/index.js";
4
+ import { hasDoorbell } from "../../host-files/index.js";
3
5
  import { gameFolder } from "../../profiles/index.js";
4
6
  import { closingLines, coachOnEnd, coachOnEndLine, coachRuns, lastMoveText, moveLines, newWords, playMove, positionIn, refuseUnseenPosition, shownMove, withAnalyst } from "../answers/index.js";
5
7
  import { defineCommand } from "../registry/index.js";
@@ -17,30 +19,78 @@ export const playerMove = defineCommand({
17
19
  alwaysLoad: true,
18
20
  showsGame: true,
19
21
  positionals: ["move"],
20
- run: ({ move, offer_draw: offerDraw = false }, context) => {
21
- const folder = gameFolder(context.home());
22
- // A chat that has seen no game yet, new or resumed, plays the player's own move: its answer shows the game the AI decides on (D-71).
23
- if (context.positionSeen !== null) {
24
- refuseUnseenPosition(context, folder);
25
- }
26
- const started = context.positionSeen === null ? positionIn(folder).started : context.positionSeen?.started;
27
- const played = playMove(folder, context.now(), "player", move, (message) => `${message} Ask the player again. Nothing changed.`, { allowance: chatAllowance, offerDraw, coached: coachRuns(context) });
28
- // A game over by this move has no more moves to play there: the next game's window gives the address (Federico, round of D-85).
29
- const invite = played.game.end === undefined ? pageInvite(context, started) : undefined;
30
- const ended = played.game.end === undefined ? undefined : loadGame(folder);
31
- const coach = ended !== undefined && coachOnEnd(context, folder, ended);
32
- return { ...played, game: withAnalyst(played.game, context), words: newWords(context), ...(invite === undefined ? {} : { invite }), ...(coach ? { coach } : {}) };
33
- },
34
- format: (result) => [
22
+ run: (input, context) => playersMove(input, context, chatAllowance),
23
+ format: (result, context) => [
35
24
  `Player: ${result.game.lastMove === undefined ? shownMove(result.move) : lastMoveText(result.game.lastMove)}.`,
36
25
  ...moveLines(result),
37
26
  ...(result.invite === undefined ? [] : [inviteLine(result.invite)]),
38
- ...(result.coach === true ? [coachOnEndLine] : []),
39
- ...closingLines(result.game, result.words),
27
+ ...(result.coach === true ? [coachOnEndLine(result.game)] : []),
28
+ ...closingLines(result.game, result.words, false, hasDoorbell(context?.host?.())),
40
29
  ].join("\n"),
41
30
  });
31
+ /**
32
+ * 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
33
+ * 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.
34
+ */
35
+ export function playMoveSentAlone(move, context) {
36
+ const folder = gameFolder(context.home());
37
+ const before = new Set(context.given);
38
+ const played = rerunIfChanged(() => playersMove({ move }, context, 0));
39
+ const stamp = gameStamp(folder);
40
+ return {
41
+ answer: `Chesspal has already played the move the player wrote alone in the chat: don't call player_move for it.\n${playerMove.format(played, context)}`,
42
+ position: positionIn(folder),
43
+ ...(stamp === undefined ? {} : { stamp }),
44
+ given: [...context.given].filter((key) => !before.has(key)),
45
+ };
46
+ }
42
47
  /**
43
48
  * The time a move in the chat leaves off the player's clock (D-36): the AI writing after its move and reading the player's, about 10 s as Priya measured.
44
- * Claude Code's hooks will time it exactly; the board page's moves need none.
49
+ * The board page's moves need none, nor those Chesspal plays as the player sends them.
45
50
  */
46
51
  const chatAllowance = 10_000;
52
+ function playersMove({ move, offer_draw: offerDraw = false }, context, allowance) {
53
+ const folder = gameFolder(context.home());
54
+ // A chat that has seen no game yet, new or resumed, plays the player's own move: its answer shows the game the AI decides on (D-71).
55
+ if (context.positionSeen !== null) {
56
+ refuseUnseenPosition(context, folder);
57
+ }
58
+ refuseKingAsRook(move, folder);
59
+ // A piece's letter of another language is read as SAN's (D-113): Cf3 is the knight's move, Tf1 the rook's.
60
+ const read = withSanPieces(move);
61
+ const readAs = read === move ? "" : `${move.trim()} is read as ${read.trim()}. `;
62
+ const started = context.positionSeen === null ? positionIn(folder).started : context.positionSeen?.started;
63
+ const played = playMove(folder, context.now(), "player", read, (message) => `${readAs}${message} Ask the player again. Nothing changed.`, { allowance, offerDraw, coached: coachRuns(context) });
64
+ // A game over by this move has no more moves to play there: the next game's window gives the address (Federico, round of D-85).
65
+ const invite = played.game.end === undefined ? pageInvite(context, started) : undefined;
66
+ const ended = played.game.end === undefined ? undefined : loadGame(folder);
67
+ const coach = ended !== undefined && coachOnEnd(context, folder, ended);
68
+ return { ...played, game: withAnalyst(played.game, context), words: newWords(context), ...(invite === undefined ? {} : { invite }), ...(coach ? { coach } : {}) };
69
+ }
70
+ /**
71
+ * Refuses a move written R and a square when the player's king could go there: R is the king in Italian, French and Spanish, and only the player's words
72
+ * tell which. A rook's move that names its file or rank, or given as squares, is never the king's.
73
+ */
74
+ function refuseKingAsRook(move, folder) {
75
+ const written = move.trim().replace(/^\d+\s*(?:\.[\s.]*|…\s*)/, "").replace(/\s+/g, "");
76
+ const target = /^[Rr][x:]?([a-h][1-8])[+#]*[!?]*$/.exec(written)?.[1];
77
+ const current = target === undefined ? undefined : loadGame(folder);
78
+ // Another side to move, or a game over, is refused by the move itself.
79
+ if (current === undefined || current.game.position.turn === current.ai || endOf(current.game) !== undefined) {
80
+ return;
81
+ }
82
+ const there = current.game.legalMoves().filter((legal) => legal.to === target);
83
+ const rooks = there.filter((legal) => legal.san.startsWith("R")).map(squaresOf);
84
+ const king = there.find((legal) => legal.san.startsWith("K"));
85
+ if (rooks.length > 0 && king !== undefined) {
86
+ throw new Error(`${written} may be the rook's move, ${rooks.join(" or ")}, or the king's, ${squaresOf(king)}: R is the king in Italian, French and Spanish. `
87
+ + "Unless the player's words name the piece, ask them which; then give it as squares. Nothing changed.");
88
+ }
89
+ if (rooks.length === 0 && king !== undefined) {
90
+ throw new Error(`${written} is not legal: R is the rook in SAN, and no rook can go to ${target}. The king can, ${king.san}, and R is the king in Italian, French and Spanish: `
91
+ + `ask the player whether they meant the king, and give ${king.san} only once they say so. Nothing changed.`);
92
+ }
93
+ }
94
+ function squaresOf(move) {
95
+ return `${move.from}${move.to}`;
96
+ }
@@ -26,7 +26,10 @@ export const ponder = defineCommand({
26
26
  }),
27
27
  byAgents: ponderAgents,
28
28
  hosts: agentHosts,
29
- run: async ({ position, reply, move, why, say, done = false }, context) => {
29
+ run: async (input, context) => {
30
+ const { reply, move, why, say, done = false } = input;
31
+ // A quick model writes ponder {} as ponder {position: 0}, and was told to stop: alone, it asks for the position too (Priya).
32
+ const position = input.position === 0 && Object.keys(input).length === 1 ? undefined : input.position;
30
33
  const folder = gameFolder(context.home());
31
34
  const current = toPonder(folder, context.now(), position);
32
35
  if (current === undefined) {
@@ -27,7 +27,7 @@ interface Status {
27
27
  };
28
28
  /** A game going on from before this chat, told once a chat: the player says whether to go on with it. */
29
29
  fromBefore?: true;
30
- /** The coach to start, once, for the player's move that ended the game (D-88). */
30
+ /** The coach to start, once a move ended the game, for the player's last move (D-88). */
31
31
  coach?: true;
32
32
  /** The files an older init wrote in the player's folder, which this version would write otherwise. */
33
33
  outdated?: string[];
@@ -1,7 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { Game, gameReviewOf, leadOf, reviewOf } from "../../core/index.js";
3
3
  import { archivedName, cleanSince, endOf, gameInAnalysis, lastChatStart } from "../../games/index.js";
4
- import { isCodex, outdatedHostFiles } from "../../host-files/index.js";
4
+ import { hasDoorbell, isCodex, outdatedHostFiles } from "../../host-files/index.js";
5
5
  import { gameFolder } from "../../profiles/index.js";
6
6
  import { version } from "../../version.js";
7
7
  import { analystLine, analystRuns, closingLines, coachOnEnd, coachOnEndLine, coachRuns, gameAt, goneOver, namedGame, newGameStep, positionLines, reviewedLine, shownMoves, sidesLine, speechLine, speechOf, summaryOf, unansweredWords, withAnalyst, withThought, wordLines, } from "../answers/index.js";
@@ -56,7 +56,7 @@ export const status = defineCommand({
56
56
  if (first && endOf(current.game) === undefined && current.game.record.moves.length > 0 && (chatStart === undefined || cleanSince(chatStart, current.started))) {
57
57
  status.fromBefore = true;
58
58
  }
59
- // The player's move on the board page that ended the game: the coach judges it too (D-88).
59
+ // A move that ended the game: the coach judges the player's last move, told until it starts (D-88).
60
60
  if (coachOnEnd(context, folder, current)) {
61
61
  status.coach = true;
62
62
  }
@@ -80,7 +80,7 @@ export const status = defineCommand({
80
80
  }
81
81
  return status;
82
82
  },
83
- format: ({ version, folder, page, pageClosed, player, speech, game, problem, words = [], analysis, fromBefore, coach, outdated, unread }) => [
83
+ format: ({ version, folder, page, pageClosed, player, speech, game, problem, words = [], analysis, fromBefore, coach, outdated, unread }, context) => [
84
84
  `Chesspal ${version}. Folder: ${folder}`,
85
85
  ...(unread === true ? [unreadLine] : []),
86
86
  ...(outdated === undefined ? [] : [outdatedLine(outdated)]),
@@ -88,7 +88,7 @@ export const status = defineCommand({
88
88
  ...(player === undefined ? [] : [player]),
89
89
  ...(speech === undefined ? [] : [speechLine(speech)]),
90
90
  ...(fromBefore === true ? [fromBeforeLine] : []),
91
- ...(problem !== undefined ? [problem] : gameLines(game, words, analysis?.brought === true, pageClosed === true, coach === true)),
91
+ ...(problem !== undefined ? [problem] : gameLines(game, words, analysis?.brought === true, pageClosed === true, coach === true, hasDoorbell(context?.host?.()))),
92
92
  ...(analysis === undefined ? [] : [analysisLine(analysis)]),
93
93
  ].join("\n"),
94
94
  });
@@ -166,8 +166,8 @@ function reviewProgress(context, game) {
166
166
  * The game's lines; a game the player brought to analyse takes the place of the next step. The coach to start goes just before the next step, where
167
167
  * the AI acts on it (Priya).
168
168
  */
169
- function gameLines(game, words, brought, pageClosed, coach) {
170
- const closing = [...(coach ? [coachOnEndLine] : []), ...(brought ? wordLines(words) : closingLines(game, words, pageClosed))];
169
+ function gameLines(game, words, brought, pageClosed, coach, doorbell) {
170
+ const closing = [...(coach && game !== undefined ? [coachOnEndLine(game)] : []), ...(brought ? wordLines(words) : closingLines(game, words, pageClosed, doorbell))];
171
171
  if (game === undefined) {
172
172
  return ["No game.", ...closing];
173
173
  }
@@ -1,8 +1,8 @@
1
1
  import type { AnyCommand } from "./registry/index.ts";
2
2
  export { analyseGame, deleteReview, siteGames, SiteGamesRefused } from "./analysis/index.ts";
3
- export { analystOf, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition } from "./answers/index.ts";
4
- export { aiMove, answerDraw, ChatNotClear, DrawRefused, endGame, newGame, offerDraw, startNewGame } from "./game/index.ts";
3
+ export { analystOf, analystQuietAt, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition } from "./answers/index.ts";
4
+ export { aiMove, answerDraw, ChatNotClear, DrawRefused, endGame, type MoveSentAlone, newGame, offerDraw, playMoveSentAlone, startNewGame } from "./game/index.ts";
5
5
  export { chooseDrawnAvatar, deleteGame, editProfile, GameGoingOn, newProfile, ratingsAge, refreshRatings, useProfile } from "./profiles/index.ts";
6
- export { type AnyCommand, baseType, type Caller, type Command, commandLineFieldProblem, type Context, defaultContext, inputProblems, isBoolean, isList, type SeenPosition, speaksItalian, takesNumbers } from "./registry/index.ts";
6
+ export { type AnyCommand, baseType, type Caller, type Command, commandLineFieldProblem, type Context, defaultContext, homeForOneCall, inputProblems, isBoolean, isList, type SeenPosition, speaksItalian, takesNumbers, } from "./registry/index.ts";
7
7
  /** Every command of Chesspal, in the order the command line lists them and the MCP server offers them. */
8
8
  export declare const commands: AnyCommand[];
@@ -6,10 +6,10 @@ import { init, instructions } from "./setup/index.js";
6
6
  import { aiSay, playerSay } from "./talk/index.js";
7
7
  // What the server and the command line use besides the list: the context, the reading of an input, and the operations the page shares with the tools.
8
8
  export { analyseGame, deleteReview, siteGames, SiteGamesRefused } from "./analysis/index.js";
9
- export { analystOf, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition } from "./answers/index.js";
10
- export { aiMove, answerDraw, ChatNotClear, DrawRefused, endGame, newGame, offerDraw, startNewGame } from "./game/index.js";
9
+ export { analystOf, analystQuietAt, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition } from "./answers/index.js";
10
+ export { aiMove, answerDraw, ChatNotClear, DrawRefused, endGame, newGame, offerDraw, playMoveSentAlone, startNewGame } from "./game/index.js";
11
11
  export { chooseDrawnAvatar, deleteGame, editProfile, GameGoingOn, newProfile, ratingsAge, refreshRatings, useProfile } from "./profiles/index.js";
12
- export { baseType, commandLineFieldProblem, defaultContext, inputProblems, isBoolean, isList, speaksItalian, takesNumbers } from "./registry/index.js";
12
+ export { baseType, commandLineFieldProblem, defaultContext, homeForOneCall, inputProblems, isBoolean, isList, speaksItalian, takesNumbers, } from "./registry/index.js";
13
13
  /** Every command of Chesspal, in the order the command line lists them and the MCP server offers them. */
14
14
  export const commands = [
15
15
  init,
@@ -11,6 +11,6 @@ export declare const pastGames: import("../registry/registry.ts").Command<z.ZodO
11
11
  }>;
12
12
  /**
13
13
  * The latest games of the archive, one line each as the AI reads them, those holding every word searched: none when there is no archive. A game closed
14
- * before any move is no game played.
14
+ * before any move is no game played. A game is read from its tags alone when the archive wrote the opening's and the plies', or else whole.
15
15
  */
16
16
  export declare function pastGameLines(folder: string, count: number, search?: string): string[];
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { writePgn } from "../../core/index.js";
3
- import { archivedGame, archivedGames, archivedWhen } from "../../games/index.js";
3
+ import { archivedGame, archivedGames, archivedTags, archivedWhen, hasListTags } from "../../games/index.js";
4
4
  import { gameFolder } from "../../profiles/index.js";
5
5
  import { openingName } from "../answers/index.js";
6
6
  import { defineCommand } from "../registry/index.js";
@@ -30,7 +30,7 @@ export const pastGames = defineCommand({
30
30
  });
31
31
  /**
32
32
  * The latest games of the archive, one line each as the AI reads them, those holding every word searched: none when there is no archive. A game closed
33
- * before any move is no game played.
33
+ * before any move is no game played. A game is read from its tags alone when the archive wrote the opening's and the plies', or else whole.
34
34
  */
35
35
  export function pastGameLines(folder, count, search = "") {
36
36
  const words = search.toLowerCase().split(/\s+/).filter((word) => word !== "");
@@ -40,8 +40,7 @@ export function pastGameLines(folder, count, search = "") {
40
40
  break;
41
41
  }
42
42
  try {
43
- const record = archivedGame(folder, name);
44
- const line = record.moves.length > 0 ? pastGameLine(name, record) : undefined;
43
+ const line = pastGameLine(folder, name);
45
44
  // The ending names the player's colour too, as "(Black resigns)": a colour searched is the AI's alone.
46
45
  const searched = line?.replace(/ \((?:White|Black) [^)]*\)/, "").toLowerCase();
47
46
  if (line !== undefined && words.every((word) => searched.includes(word))) {
@@ -55,14 +54,26 @@ export function pastGameLines(folder, count, search = "") {
55
54
  return lines;
56
55
  }
57
56
  const listed = 20;
57
+ /** A game's line, from its tags when they say all of it, or else from the whole game: none for a game closed before any move. */
58
+ function pastGameLine(folder, name) {
59
+ const tags = archivedTags(folder, name);
60
+ const { ECO: eco, Opening: opening, PlyCount: plies } = tags;
61
+ if (hasListTags(tags)) {
62
+ return lineOf(name, tags, Number(plies), eco === undefined || opening === undefined ? undefined : `${eco} ${opening}`);
63
+ }
64
+ const record = archivedGame(folder, name);
65
+ return lineOf(name, record.tags, record.moves.length, openingName(record));
66
+ }
58
67
  /** 2026-11-03 12:04, you (AI) played Black and won 0-1 (White resigns), B50 Sicilian Defense, 15 moves. games/2026-11-03-120423.pgn */
59
- function pastGameLine(name, record) {
60
- const ai = record.tags.Black === "AI" ? "black" : "white";
61
- const result = record.tags.Result ?? "*";
68
+ function lineOf(name, tags, plies, opening) {
69
+ if (plies === 0) {
70
+ return undefined;
71
+ }
72
+ const ai = tags.Black === "AI" ? "black" : "white";
73
+ const result = tags.Result ?? "*";
62
74
  const outcome = result === "*" ? ", unfinished" : ` and ${resultFor(result, ai)} ${result}`;
63
- const termination = record.tags.Termination === undefined || result === "*" ? "" : ` (${record.tags.Termination})`;
64
- const opening = openingName(record);
65
- const moves = Math.ceil(record.moves.length / 2);
75
+ const termination = tags.Termination === undefined || result === "*" ? "" : ` (${tags.Termination})`;
76
+ const moves = Math.ceil(plies / 2);
66
77
  return `${archivedWhen(name)}, you (AI) played ${ai === "white" ? "White" : "Black"}${outcome}${termination}${opening === undefined ? "" : `, ${opening}`}, ${moves} moves. ${name}`;
67
78
  }
68
79
  function resultFor(result, ai) {
@@ -74,6 +74,8 @@ export interface SeenPosition {
74
74
  }
75
75
  /** The context of a process started in the player's folder: the data folder is found at the first command that needs it, once. */
76
76
  export declare function defaultContext(): Context;
77
+ /** The data folder for one tool call: looked for, its profile made when missing, at its first use in the call alone; the next call looks again. */
78
+ export declare function homeForOneCall(context: Context): () => string;
77
79
  /** Whether the player's language is Italian: the command line speaks Italian to them then, English otherwise (D-32). */
78
80
  export declare function speaksItalian(language: string | undefined): boolean;
79
81
  /** A data folder with a profile in it, the first made when there is none, named after the system's account (D-71). */
@@ -18,6 +18,11 @@ export function defaultContext() {
18
18
  fetch: globalThis.fetch,
19
19
  };
20
20
  }
21
+ /** The data folder for one tool call: looked for, its profile made when missing, at its first use in the call alone; the next call looks again. */
22
+ export function homeForOneCall(context) {
23
+ let found;
24
+ return () => (found ??= context.home());
25
+ }
21
26
  /** Whether the player's language is Italian: the command line speaks Italian to them then, English otherwise (D-32). */
22
27
  export function speaksItalian(language) {
23
28
  return language?.slice(0, 2).toLowerCase() === "it";
@@ -1,2 +1,2 @@
1
- export { type Caller, type Context, defaultContext, type SeenPosition, speaksItalian } from "./context.ts";
1
+ export { type Caller, type Context, defaultContext, homeForOneCall, type SeenPosition, speaksItalian } from "./context.ts";
2
2
  export { type AnyCommand, baseType, type Command, commandLineFieldProblem, defineCommand, inputProblems, isBoolean, isList, sentence, takesNumbers } from "./registry.ts";
@@ -1,3 +1,3 @@
1
1
  // What a command is and what it runs with: the definition and the reading of its input, and the context of folders, time and randomness.
2
- export { defaultContext, speaksItalian } from "./context.js";
2
+ export { defaultContext, homeForOneCall, speaksItalian } from "./context.js";
3
3
  export { baseType, commandLineFieldProblem, defineCommand, inputProblems, isBoolean, isList, sentence, takesNumbers } from "./registry.js";
@@ -4,7 +4,7 @@ interface Said {
4
4
  /** The game, none when the AI spoke before the first (D-77). */
5
5
  game?: GameSummary;
6
6
  words: Written[];
7
- /** The coach yet to start on the player's move that ended the game (D-88). */
7
+ /** The coach yet to start once a move ended the game (D-88). */
8
8
  coach?: true;
9
9
  }
10
10
  /** The AI's words on the board page without a move, read only there, on the player's time: an answer, or a word it has to say (D-54, D-72). */
@@ -1,6 +1,7 @@
1
1
  import { z } from "zod";
2
2
  import { moods } from "../../core/index.js";
3
3
  import { archivedName, talkLimits } from "../../games/index.js";
4
+ import { hasDoorbell } from "../../host-files/index.js";
4
5
  import { gameFolder } from "../../profiles/index.js";
5
6
  import { analystLine, coachOnEnd, coachOnEndLine, give, newGameStep, nextStep, refuseOtherGame, reviewedLine, say, summaryOf, waitingWords, withAnalyst, wordLines } from "../answers/index.js";
6
7
  import { defineCommand } from "../registry/index.js";
@@ -28,15 +29,15 @@ export const aiSay = defineCommand({
28
29
  // The chat woken by the player's ending move on the page may have gone to its words without the coach (Priya).
29
30
  return { game: withAnalyst(summaryOf(current, context.now(), archivedName(folder, current)), context, current), words, ...(coachOnEnd(context, folder, current) ? { coach: true } : {}) };
30
31
  },
31
- format: ({ game, words, coach }) => [
32
+ format: ({ game, words, coach }, context) => [
32
33
  "Said on the board page, where the player reads it: don't write it again in the chat.",
33
34
  ...wordLines(words),
34
- ...(coach === true ? [coachOnEndLine] : []),
35
- afterWords(game, words),
35
+ ...(coach === true && game !== undefined ? [coachOnEndLine(game)] : []),
36
+ afterWords(game, words, hasDoorbell(context?.host?.())),
36
37
  ].join("\n"),
37
38
  });
38
39
  /** The next step once the AI has spoken: the end of a game over has been told already, and the player's draw offer answered in words. */
39
- function afterWords(game, words) {
40
+ function afterWords(game, words, doorbell) {
40
41
  if (words.length === 0 && game?.end !== undefined) {
41
42
  // The offer to go over the game, made in the AI's words, waits for the player's yes (D-89).
42
43
  if (game.end.analyst === "done") {
@@ -49,5 +50,5 @@ function afterWords(game, words) {
49
50
  if (words.length === 0 && game?.drawOffer === "player" && game.turn !== game.ai) {
50
51
  return "Next: accept the draw with end_game draw_agreed, or wait for the player's move.";
51
52
  }
52
- return nextStep(game, words);
53
+ return nextStep(game, words, false, doorbell);
53
54
  }
@@ -58,6 +58,7 @@ export declare class Game {
58
58
  private readonly moves;
59
59
  private last;
60
60
  private readonly keys;
61
+ private readonly positions;
61
62
  private readonly castled;
62
63
  private keptPosition;
63
64
  private keptOutcome;
@@ -70,6 +71,8 @@ export declare class Game {
70
71
  get outcome(): Outcome;
71
72
  /** The game as plain data, frozen; when the board ends the game, its Result tag is the board's, unless the record gave one. */
72
73
  get record(): GameRecord;
74
+ /** The key of every position the game has stood in, its start first, as the openings' book keys them (D-68): no replay to look the book up. */
75
+ get positionKeys(): readonly string[];
73
76
  /** The last move played, with its squares; none before the first. */
74
77
  get lastMove(): Move | undefined;
75
78
  /** What the position shows (D-26): attackers and defenders, pieces at risk, pins, checks, the moves of each piece; computed once for each position. */
@@ -98,3 +101,13 @@ export declare class Game {
98
101
  }
99
102
  /** A text with SAN's letters and signs for the figurines and signs others write: ♘f3 as Nf3, × as x; a pawn's figurine says nothing. */
100
103
  export declare function withSanLetters(text: string): string;
104
+ /** The moves a text writes with a piece's letter of another language, each with its SAN: Cxe5 as Nxe5. */
105
+ export declare function foreignMovesIn(text: string): {
106
+ written: string;
107
+ san: string;
108
+ }[];
109
+ /**
110
+ * A player's move written with a piece's letter of another language that names one piece only, in SAN's (D-113): Cf3 as Nf3, Tf1 as Rf1, e8=D as e8=Q;
111
+ * a small t too, which is no file. Any other text as it is: B stays the bishop, K the king, R the rook, a small letter a file.
112
+ */
113
+ export declare function withSanPieces(move: string): string;
package/dist/core/game.js CHANGED
@@ -2,6 +2,7 @@
2
2
  import { Chess, validateFen } from "chess.js";
3
3
  import { factsOf } from "./facts.js";
4
4
  import { whyIllegal } from "./illegal.js";
5
+ import { positionKey } from "./openings.js";
5
6
  import { possibilitiesOf, threatsOf } from "./possibilities.js";
6
7
  import { profileOf } from "./profile.js";
7
8
  import { cleanComment, recordedMove } from "./record.js";
@@ -17,6 +18,8 @@ export class Game {
17
18
  last;
18
19
  // The key of every position the game has stood in, the first included, and how each side castled: for the profile's repetitions and castling.
19
20
  keys;
21
+ // The same positions keyed as the openings' book keys them, its FEN without the move counters.
22
+ positions;
20
23
  castled = {};
21
24
  keptPosition;
22
25
  keptOutcome;
@@ -26,6 +29,7 @@ export class Game {
26
29
  this.chess = startingBoard(record.tags.FEN);
27
30
  // The first key without an en passant square no pawn can use, which the FEN may give: the position then repeats as the rules count it.
28
31
  this.keys = [new Chess(this.chess.fen()).hash()];
32
+ this.positions = [positionKey(this.chess.fen())];
29
33
  this.tags = { ...record.tags };
30
34
  this.comment = cleanComment(record.comment ?? "") || undefined;
31
35
  for (const { san, nags, comment } of record.moves) {
@@ -58,6 +62,10 @@ export class Game {
58
62
  this.keptRecord ??= this.snapshot();
59
63
  return this.keptRecord;
60
64
  }
65
+ /** The key of every position the game has stood in, its start first, as the openings' book keys them (D-68): no replay to look the book up. */
66
+ get positionKeys() {
67
+ return this.positions;
68
+ }
61
69
  /** The last move played, with its squares; none before the first. */
62
70
  get lastMove() {
63
71
  return this.last;
@@ -129,11 +137,10 @@ export class Game {
129
137
  if ("refusal" in move) {
130
138
  return move;
131
139
  }
132
- if (exact === undefined) {
133
- this.chess.move({ from: move.from, to: move.to, promotion: move.promotion });
134
- }
140
+ const after = exact === undefined ? this.chess.move({ from: move.from, to: move.to, promotion: move.promotion }).after : exact.after;
135
141
  this.last = move;
136
142
  this.keys.push(this.chess.hash());
143
+ this.positions.push(positionKey(after));
137
144
  if (move.san.startsWith("O-O")) {
138
145
  this.castled[mover] = move.san.startsWith("O-O-O") ? "O-O-O" : "O-O";
139
146
  }
@@ -223,6 +230,20 @@ const asciiOf = {
223
230
  export function withSanLetters(text) {
224
231
  return text.replace(/[︎️]/g, "").replace(/[–—−×♔-♟]/g, (sign) => asciiOf[sign] ?? sign);
225
232
  }
233
+ /** The moves a text writes with a piece's letter of another language, each with its SAN: Cxe5 as Nxe5. */
234
+ export function foreignMovesIn(text) {
235
+ return [...text.matchAll(foreignMove)]
236
+ .map(([written]) => ({ written, san: `${foreignPieces[written[0]][1]}${written.slice(1)}` }));
237
+ }
238
+ /**
239
+ * A player's move written with a piece's letter of another language that names one piece only, in SAN's (D-113): Cf3 as Nf3, Tf1 as Rf1, e8=D as e8=Q;
240
+ * a small t too, which is no file. Any other text as it is: B stays the bishop, K the king, R the rook, a small letter a file.
241
+ */
242
+ export function withSanPieces(move) {
243
+ return move
244
+ .replace(foreignPieceMove, (_, before, letter) => `${before}${foreignPieces[letter.toUpperCase()][1]}`)
245
+ .replace(foreignPromotion, (_, before, letter) => `${before}${foreignPieces[letter][1]}`);
246
+ }
226
247
  // The pieces' letters of other languages that name one piece only: the Italian, French, Spanish and German ones; R is the rook in SAN.
227
248
  const foreignPieces = {
228
249
  C: ["knight", "N"],
@@ -233,6 +254,11 @@ const foreignPieces = {
233
254
  T: ["rook", "R"],
234
255
  D: ["queen", "Q"],
235
256
  };
257
+ // A move written with one of those letters, standing alone in a text.
258
+ const foreignMove = new RegExp(`(?<![\\p{L}\\d])[${Object.keys(foreignPieces).join("")}][a-h]?[1-8]?x?[a-h][1-8][+#]?(?![\\p{L}\\d])`, "gu");
259
+ // A whole move with such a letter, after its number if any: Tf1, 4. Cxe5+, D d1; and a pawn's move to the last rank that becomes such a piece: e8=D, exd1T.
260
+ const foreignPieceMove = /^(\s*(?:\d+\s*(?:\.[\s.]*|…\s*))?)([CSAFLTDt])(?=\s*[a-h]?[1-8]?[-x:×]?[a-h][1-8][+#]*[!?]*\s*$)/;
261
+ const foreignPromotion = /^(\s*(?:\d+\s*(?:\.[\s.]*|…\s*))?[a-h]?[1-8]?[-x:×]?[a-h][18]=?)([CSAFLTD])(?=[+#]*[!?]*\s*$)/;
236
262
  // The legal moves of the positions seen last, by their key: about 4 KB each.
237
263
  const legalByPosition = new Map();
238
264
  const positionsKept = 1_000;
@@ -3,7 +3,7 @@ export { boardText, figurinesOf } from "./board-text.ts";
3
3
  export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.ts";
4
4
  export type { Clock, TimeControl } from "./clock.ts";
5
5
  export type { Facts, PieceFacts, PieceLetter, Pin, Skewer } from "./facts.ts";
6
- export { Game, withSanLetters } from "./game.ts";
6
+ export { foreignMovesIn, Game, withSanLetters, withSanPieces } from "./game.ts";
7
7
  export type { Ending, Move, Outcome, Played, Position, Promotion, Refusal, Side, Square } from "./game.ts";
8
8
  export type { Illegal, IllegalReason } from "./illegal.ts";
9
9
  export { type Lead, leadOf, leadText, readLead, withLead } from "./lead.ts";
@@ -12,9 +12,9 @@ export type { AiLevel, Layer } from "./levels.ts";
12
12
  export { materialOf } from "./material.ts";
13
13
  export type { Material } from "./material.ts";
14
14
  export { endWithMood, type Mood, moodAt, type MoodNow, moodOf, moods, moodTag, withoutMoods } from "./moods.ts";
15
- export { bookMoves, indexBook, indexOpenings, inOpenings, openingOf, outOfBookMove } from "./openings.ts";
16
- export type { Opening, OpeningBook, OpeningIndex } from "./openings.ts";
17
- export { pgnDate, readPgn, writePgn } from "./pgn.ts";
15
+ export { bookMoves, bookPlaceOf, indexBook, indexOpenings, openingOf, outOfBookMove, positionKeysOf } from "./openings.ts";
16
+ export type { BookPlace, Opening, OpeningBook, OpeningIndex } from "./openings.ts";
17
+ export { pgnDate, readPgn, readPgnTags, readPgnUnplayed, writePgn } from "./pgn.ts";
18
18
  export type { Capture, Chances, DoubleAttack, Possibilities, StalemateTrap } from "./possibilities.ts";
19
19
  export type { BishopFacts, CentreSquare, KingFacts, Outpost, PawnFacts, PawnSquare, Phase, Profile, SideProfile } from "./profile.ts";
20
20
  export { movesOnly } from "./record.ts";
@@ -2,12 +2,12 @@
2
2
  export { accountSide } from "./accounts.js";
3
3
  export { boardText, figurinesOf } from "./board-text.js";
4
4
  export { canMate, clockAfterMove, clockOfComment, clockStarted, clockTag, clockText, startClock, timeControlOf, timeControlOfTag, timeControlTag, timeControlText, timeLeft } from "./clock.js";
5
- export { Game, withSanLetters } from "./game.js";
5
+ export { foreignMovesIn, Game, withSanLetters, withSanPieces } from "./game.js";
6
6
  export { leadOf, leadText, readLead, withLead } from "./lead.js";
7
7
  export { aiLevels, layersOf, levelForPlayer, levelOf, levelTag, sees, suggestedLevel } from "./levels.js";
8
8
  export { materialOf } from "./material.js";
9
9
  export { endWithMood, moodAt, moodOf, moods, moodTag, withoutMoods } from "./moods.js";
10
- export { bookMoves, indexBook, indexOpenings, inOpenings, openingOf, outOfBookMove } from "./openings.js";
11
- export { pgnDate, readPgn, writePgn } from "./pgn.js";
10
+ export { bookMoves, bookPlaceOf, indexBook, indexOpenings, openingOf, outOfBookMove, positionKeysOf } from "./openings.js";
11
+ export { pgnDate, readPgn, readPgnTags, readPgnUnplayed, writePgn } from "./pgn.js";
12
12
  export { movesOnly } from "./record.js";
13
13
  export { aiHostIn, aiModelIn, aiNameOf, annotatorOf, arrowOf, badges, closingOf, drawingsIn, drawingsText, gameReviewOf, isReviewed, reviewed, reviewOf, squareOf, unreviewed, unsigned, withAiHost, withAiModel, withAnnotator, withGameReview, } from "./review.js";
@@ -12,8 +12,11 @@ export interface Opening {
12
12
  readonly variation?: string;
13
13
  /** How many plies the game had when it reached the opening's position. */
14
14
  readonly ply: number;
15
- /** Whether the game's position now is a named one; when not, it left the book at the ply after `ply`. */
16
- readonly inBook: boolean;
15
+ }
16
+ /** Where a game from the standard position stands in the book: the last named opening it reached, and how many of its first plies a line of the book reaches (D-68). */
17
+ export interface BookPlace {
18
+ readonly opening?: Opening;
19
+ readonly plies: number;
17
20
  }
18
21
  /** The book's moves: each position on a line of Lichess's files to the moves its lines play from it, the theory the AI knows at the master level (D-15, D-60). */
19
22
  export type OpeningBook = Readonly<Record<string, readonly string[]>>;
@@ -27,9 +30,20 @@ export declare function indexOpenings(tsv: string): Record<string, [string, stri
27
30
  export declare function indexBook(tsv: string): Record<string, string[]>;
28
31
  /** The moves the book knows from a position, in SAN; none out of the book. */
29
32
  export declare function bookMoves(fen: string, book: OpeningBook): readonly string[];
30
- /** Whether a line of the book reaches a position, by any order of moves: one its lines play on from, or one an opening is named after. */
31
- export declare function inOpenings(fen: string, index: OpeningIndex, book: OpeningBook): boolean;
32
33
  /** The opening of a game from the standard position, the last named one its moves reached; none before the first, or from another position. */
33
34
  export declare function openingOf(record: GameRecord, index: OpeningIndex): Opening | undefined;
34
- /** The move number at which a game that reached an opening at `ply` left the book. */
35
- export declare function outOfBookMove(opening: Opening): number;
35
+ /**
36
+ * Where a game stands in the book, from the keys of the positions it stood in, its start first: the last named position its moves reached, and its first
37
+ * plies whose position a line of the book reaches, by any order of moves, one its lines play on from or one an opening is named after; without the book,
38
+ * no ply is counted. None from another position than the standard one, whatever its FEN tag says.
39
+ */
40
+ export declare function bookPlaceOf(keys: readonly string[], index: OpeningIndex, book?: OpeningBook): BookPlace | undefined;
41
+ /** The keys of a record's positions, its start first, replayed to the book's depth for a game from the standard position: for a record no live game holds. */
42
+ export declare function positionKeysOf(record: GameRecord): readonly string[];
43
+ /** A position without its move counters, the key of the index and the book: the same position reached in another order is the same key. */
44
+ export declare function positionKey(fen: string): string;
45
+ /**
46
+ * The move number at which a game from the standard position left the book (D-68), the move of its first ply past the `inBook` plies a line of the book
47
+ * reaches; none while all its `plies` are the book's.
48
+ */
49
+ export declare function outOfBookMove(plies: number, inBook: number): number | undefined;