chesspal 0.5.0 → 0.7.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 (57) hide show
  1. package/README.md +3 -3
  2. package/dist/commands/analysis/analyse.js +7 -2
  3. package/dist/commands/analysis/annotate.js +6 -2
  4. package/dist/commands/answers/agents.d.ts +2 -0
  5. package/dist/commands/answers/agents.js +5 -0
  6. package/dist/commands/answers/index.d.ts +2 -2
  7. package/dist/commands/answers/index.js +2 -2
  8. package/dist/commands/answers/lines.js +3 -1
  9. package/dist/commands/answers/say.d.ts +7 -1
  10. package/dist/commands/answers/say.js +25 -7
  11. package/dist/commands/answers/study.d.ts +27 -5
  12. package/dist/commands/answers/study.js +121 -10
  13. package/dist/commands/game/look.d.ts +1 -0
  14. package/dist/commands/game/look.js +8 -8
  15. package/dist/commands/game/position.d.ts +1 -0
  16. package/dist/commands/game/position.js +5 -5
  17. package/dist/commands/index.d.ts +1 -1
  18. package/dist/core/index.d.ts +1 -1
  19. package/dist/core/index.js +1 -1
  20. package/dist/core/moods.d.ts +2 -0
  21. package/dist/core/moods.js +11 -2
  22. package/dist/games/analysis.d.ts +11 -0
  23. package/dist/games/analysis.js +23 -0
  24. package/dist/games/archive.d.ts +9 -1
  25. package/dist/games/archive.js +18 -1
  26. package/dist/games/current-game.d.ts +23 -1
  27. package/dist/games/current-game.js +6 -1
  28. package/dist/games/index.d.ts +3 -3
  29. package/dist/games/index.js +2 -2
  30. package/dist/page/app/app.js +148 -29
  31. package/dist/page/app/keys.js +16 -1
  32. package/dist/page/app/title.js +6 -3
  33. package/dist/page/board/board.css +6 -0
  34. package/dist/page/game/column.js +5 -4
  35. package/dist/page/game/game.css +150 -1
  36. package/dist/page/game/move-list.js +48 -10
  37. package/dist/page/game/variation.js +65 -0
  38. package/dist/page/guide/en.md +1 -0
  39. package/dist/page/guide/it.md +1 -0
  40. package/dist/page/table/window.js +8 -5
  41. package/dist/page/talk/chat.css +8 -0
  42. package/dist/page/talk/talk.js +20 -2
  43. package/dist/page/text.js +20 -0
  44. package/dist/server/bell/bell.d.ts +5 -2
  45. package/dist/server/bell/bell.js +36 -12
  46. package/dist/server/page/game.js +38 -7
  47. package/dist/server/page/page.js +1 -1
  48. package/dist/server/page/table.d.ts +12 -2
  49. package/dist/server/page/table.js +18 -6
  50. package/dist/server/table/hosts/chats.js +1 -1
  51. package/dist/server/table/hosts/play.d.ts +3 -1
  52. package/dist/server/table/hosts/play.js +6 -5
  53. package/dist/server/table/hosts/seat.d.ts +5 -1
  54. package/dist/server/table/hosts/seat.js +1 -1
  55. package/instructions/analysis.md +2 -2
  56. package/instructions/opponent.md +1 -1
  57. package/package.json +1 -1
package/README.md CHANGED
@@ -49,7 +49,7 @@ In the steps below, copy each command, paste it in the terminal (Ctrl+V or right
49
49
  ```
50
50
 
51
51
  Choose **Sign in with ChatGPT** and sign in in your browser.
52
- - **Antigravity**, Google's, with a Google account (the free plan is enough: Chesspal plays with Gemini Flash). On Windows, in Command Prompt:
52
+ - **Antigravity**, Google's, with a Google account (the free plan is enough: Chesspal plays with Gemini Flash). Still experimental: now and then it may miss your move on the board, and then a message in its window wakes it. On Windows, in Command Prompt:
53
53
 
54
54
  ```bash
55
55
  curl -fsSL https://antigravity.google/cli/install.cmd -o install.cmd && install.cmd && del install.cmd
@@ -105,10 +105,10 @@ Coming later: an AI coach that helps you grow, exercises chosen on your weak poi
105
105
  ## What you need
106
106
 
107
107
  - **Node.js 22.13** or newer, with **npm 10.9.2** or newer, which comes with it.
108
- - **An AI agent** with your own subscription: [Claude Code](https://code.claude.com/docs/en/setup), with a paid Claude plan, [Codex](https://developers.openai.com/codex), with a ChatGPT account, or [Antigravity](https://antigravity.google/docs/getting-started), with a Google account.
108
+ - **An AI agent** with your own subscription: [Claude Code](https://code.claude.com/docs/en/setup), with a paid Claude plan, [Codex](https://developers.openai.com/codex), with a ChatGPT account, or [Antigravity](https://antigravity.google/docs/getting-started), with a Google account, still experimental.
109
109
  - **A web browser**, for the board page.
110
110
 
111
- Chesspal works with any agent that can use Chesspal's tools (MCP) and the files on your computer: we develop it with Claude Code, and Codex and Antigravity are connected too.
111
+ Chesspal works with any agent that can use Chesspal's tools (MCP) and the files on your computer: we develop it with Claude Code, Codex is connected too, and Antigravity is supported but still experimental.
112
112
 
113
113
  ## Good to know
114
114
 
@@ -1,11 +1,11 @@
1
1
  // The analyst's tool (D-21, D-68): a game over, move by move in one call, facts only and never a judgment, with the analyst's instructions once in a chat.
2
2
  import { z } from "zod";
3
3
  import { accountSide, clockOfComment, clockText, closingOf, Game, gameReviewOf, isReviewed, leadOf, leadText, levelOf, materialOf, readPgn, reviewed, reviewOf, timeControlOfTag, timeControlText, withGameReview, withLead, } from "../../core/index.js";
4
- import { gameInAnalysis, isGameName, keepToStudy, lastChatStart, planOf, rewriteGame, startAnalysis } from "../../games/index.js";
4
+ import { gameInAnalysis, isGameName, keepToStudy, lastChatStart, planOf, rewriteGame, startAnalysis, stoppedAnalysis } from "../../games/index.js";
5
5
  import { analystAgent, instructionsFile } from "../../host-files/index.js";
6
6
  import { activeProfile, gameFolder } from "../../profiles/index.js";
7
7
  import { pointsAtSite, siteGameName, siteOfGameName } from "../../sites/index.js";
8
- import { analystOf, anotherAnalyst, balanceText, bookOpening, bookPlace, bookPlies, colourName, gameToStudy, knownTag, moveNumbers, namedGame, noGameGoingOn, riskLine, shownMove, speechLine, speechOf, tacticsText, } from "../answers/index.js";
8
+ import { analysisStopped, analystOf, anotherAnalyst, balanceText, bookOpening, bookPlace, bookPlies, colourName, gameToStudy, knownTag, moveNumbers, namedGame, noGameGoingOn, riskLine, shownMove, speechLine, speechOf, tacticsText, } from "../answers/index.js";
9
9
  import { playerLine, siteText } from "../profiles/index.js";
10
10
  import { defineCommand } from "../registry/index.js";
11
11
  import { profileSiteGames } from "./site-games.js";
@@ -78,6 +78,11 @@ export const analyseGame = defineCommand({
78
78
  function oneAnalyst(context, studied) {
79
79
  const { name } = studied;
80
80
  const holder = name === undefined ? undefined : analystOf(context, name);
81
+ // The analyst that held the game when the player stopped its analysis is not let back in; one started later is, the player asking again.
82
+ const heldWhenStopped = name === undefined ? undefined : stoppedAnalysis(gameFolder(context.home()))?.analyst;
83
+ if (name !== undefined && heldWhenStopped?.game === name && heldWhenStopped.run === context.agentRun) {
84
+ throw new Error(analysisStopped(name));
85
+ }
81
86
  if (holder !== undefined && holder === context.agentRun) {
82
87
  return;
83
88
  }
@@ -2,10 +2,10 @@
2
2
  // whole game, kept in the game's PGN for the board page. The judgment is the AI's alone: Chesspal gives no criteria (D-6, D-43).
3
3
  import { z } from "zod";
4
4
  import { annotatorOf, badges, figurinesOf, foreignMovesIn, Game, gameReviewOf, leadOf, reviewed, reviewOf, withAnnotator, withGameReview, withLead } from "../../core/index.js";
5
- import { gameInAnalysis, rewriteGame, startAnalysis } from "../../games/index.js";
5
+ import { gameInAnalysis, rewriteGame, startAnalysis, stoppedAnalysis } from "../../games/index.js";
6
6
  import { analystAgent } from "../../host-files/index.js";
7
7
  import { gameFolder } from "../../profiles/index.js";
8
- import { analystOf, analystRuns, anotherAnalyst, arrowsInput, drawingsOf, leadFrom, moveIndex, moveNumbers, namedGame, noGameGoingOn, noteInput, squaresInput } from "../answers/index.js";
8
+ import { analysisStopped, analystOf, analystRuns, anotherAnalyst, arrowsInput, drawingsOf, leadFrom, moveIndex, moveNumbers, namedGame, noGameGoingOn, noteInput, squaresInput } from "../answers/index.js";
9
9
  import { defineCommand } from "../registry/index.js";
10
10
  const summaryLimit = 1500;
11
11
  const closingLimit = 400;
@@ -53,6 +53,10 @@ export const annotateGame = defineCommand({
53
53
  throw new Error(`${anotherAnalyst} Nothing changed.`);
54
54
  }
55
55
  const folder = gameFolder(context.home());
56
+ // A review whose analysis the player stopped on the page is not written on, nor brought back to the page, by whoever was at it (Stefano).
57
+ if (stoppedAnalysis(folder)?.game === studied.name) {
58
+ throw new Error(analysisStopped(studied.name));
59
+ }
56
60
  const numbers = moveNumbers(studied.record);
57
61
  // Where the analyst reviews the games, the chat's notes are corrections it made with the player: the analyst, at work then or later, leaves those notes
58
62
  // and is told of them at each call, and the chat is not asked for the analyst's work (D-101, Priya). Only a call the host ties to the chat is its own.
@@ -53,3 +53,5 @@ export declare function analystOf(context: Context, game: string): string | unde
53
53
  export declare function analystQuietAt(context: Context): Date | undefined;
54
54
  /** What stops an analyst agent on a game another analyst has taken: the chat hears of the review from that one (Priya). */
55
55
  export declare const anotherAnalyst: string;
56
+ /** What refuses a review's writes once the player has stopped its analysis on the board page: the analyst that held it ends, the chat waits for the player. */
57
+ export declare function analysisStopped(game: string): string;
@@ -136,3 +136,8 @@ const analystQuiet = 30 * 60 * 1000;
136
136
  export const anotherAnalyst = "Stop now: another analyst is reviewing this game. End with one line for the AI that started you: its review is on its way to the "
137
137
  + "board page, and its end comes to you by itself; meanwhile talk the game over with the player whenever they ask. Should it never come, the player has "
138
138
  + "the game analysed again from the board page's list of analyses.";
139
+ /** What refuses a review's writes once the player has stopped its analysis on the board page: the analyst that held it ends, the chat waits for the player. */
140
+ export function analysisStopped(game) {
141
+ return `Stop now: the player stopped the analysis of ${game} on the board page; what was written of its review stays. Nothing changed. An analyst agent ends `
142
+ + "with one line for the AI that started it: the player stopped the analysis. Go over the game again only when the player asks, with analyse_game first.";
143
+ }
@@ -1,4 +1,4 @@
1
- export { agentRuns, agentsLine, analystLine, analystOf, analystOnView, analystQuietAt, analystRuns, anotherAnalyst, coachOnEnd, coachOnEndLine, coachRuns, goneOver, reviewedLine, withAnalyst } from "./agents.ts";
1
+ export { agentRuns, agentsLine, analysisStopped, analystLine, analystOf, analystOnView, analystQuietAt, analystRuns, anotherAnalyst, coachOnEnd, coachOnEndLine, coachRuns, goneOver, reviewedLine, withAnalyst } from "./agents.ts";
2
2
  export { clockLine, clockSummaryOf, gameAt } from "./clock.ts";
3
3
  export { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.ts";
4
4
  export { aheadWords, leadFrom, leadInput } from "./lead-input.ts";
@@ -8,6 +8,6 @@ export { openingTags } from "./opening-tags.ts";
8
8
  export { playMove, PositionChanged, positionIn, refuseOtherGame, refuseUnseenPosition, samePosition } from "./play-move.ts";
9
9
  export { give, newWords, say, unansweredWords, waitingWords, type Written } from "./say.ts";
10
10
  export { type Speech, speechLine, speechOf } from "./speech.ts";
11
- export { gameShown, gameToStudy, knownTag, moveIndex, moveNumbers, namedGame, noGameGoingOn, positionAfter, positionLine, shownPosition, type Studied, studyInput } from "./study.ts";
11
+ export { gameShown, gameToStudy, knownTag, moveIndex, moveNumbers, namedGame, noGameGoingOn, positionAfter, positionAfterLine, positionLine, shownPosition, type Studied, studyInput, type VariationInput } from "./study.ts";
12
12
  export { endText, gameGoingOn, type GameSummary, lastMoves, type MovePlayed, summaryOf, withThought } from "./summary.ts";
13
13
  export { balanceText, boardView, colourName, compactView, fullView, materialText, riskLine, tacticsText } from "./view.ts";
@@ -1,6 +1,6 @@
1
1
  // What the commands share to answer the AI: the lines it reads, the game's summary and view, the clock, the talk, the moves played, the games to study.
2
2
  // Above the registry only, below every other command folder.
3
- export { agentRuns, agentsLine, analystLine, analystOf, analystOnView, analystQuietAt, analystRuns, anotherAnalyst, coachOnEnd, coachOnEndLine, coachRuns, goneOver, reviewedLine, withAnalyst } from "./agents.js";
3
+ export { agentRuns, agentsLine, analysisStopped, analystLine, analystOf, analystOnView, analystQuietAt, analystRuns, anotherAnalyst, coachOnEnd, coachOnEndLine, coachRuns, goneOver, reviewedLine, withAnalyst } from "./agents.js";
4
4
  export { clockLine, clockSummaryOf, gameAt } from "./clock.js";
5
5
  export { arrowsInput, drawingsOf, noteInput, squaresInput } from "./drawings.js";
6
6
  export { aheadWords, leadFrom, leadInput } from "./lead-input.js";
@@ -10,6 +10,6 @@ export { openingTags } from "./opening-tags.js";
10
10
  export { playMove, PositionChanged, positionIn, refuseOtherGame, refuseUnseenPosition, samePosition } from "./play-move.js";
11
11
  export { give, newWords, say, unansweredWords, waitingWords } from "./say.js";
12
12
  export { speechLine, speechOf } from "./speech.js";
13
- export { gameShown, gameToStudy, knownTag, moveIndex, moveNumbers, namedGame, noGameGoingOn, positionAfter, positionLine, shownPosition, studyInput } from "./study.js";
13
+ export { gameShown, gameToStudy, knownTag, moveIndex, moveNumbers, namedGame, noGameGoingOn, positionAfter, positionAfterLine, positionLine, shownPosition, studyInput } from "./study.js";
14
14
  export { endText, gameGoingOn, lastMoves, summaryOf, withThought } from "./summary.js";
15
15
  export { balanceText, boardView, colourName, compactView, fullView, materialText, riskLine, tacticsText } from "./view.js";
@@ -90,8 +90,10 @@ export function nextStep(game, words = [], pageClosed = false, doorbell = true)
90
90
  const step = pageClosed ? closedPageGameStep : newGameStep;
91
91
  // Words written on a game in analysis: the chat answers them there, and corrects the review where they show it wrong (D-101).
92
92
  if (words.some((word) => word.place !== undefined) && (game === undefined || game.end !== undefined)) {
93
+ const written = words.findLast((word) => word.variation !== undefined);
94
+ const variation = written?.variation === undefined ? undefined : `line: "${written.variation}"${written.variationGame === undefined ? "" : `, game: "${written.variationGame}"`}`;
93
95
  return "Next: answer the player with ai_say, about the move they were looking at. When they show you something the review missed, first correct that move's "
94
- + "note with annotate_game, that move only.";
96
+ + `note with annotate_game, that move only.${variation === undefined ? "" : ` The facts after their variation: look {${variation}}.`}`;
95
97
  }
96
98
  if (game === undefined) {
97
99
  return words.length === 0 ? `Next: when the player wants a game, ${step}` : `Next: answer the player with ai_say. When they want a game, ${step}`;
@@ -6,10 +6,16 @@ import type { Context } from "../registry/index.ts";
6
6
  * With no game the line goes in the talk before it (D-77). A line refused changes nothing: it throws, in words for whoever spoke.
7
7
  */
8
8
  export declare function say(folder: string, who: Sayer, text: string, at: Date, mood?: Mood, shown?: Shown): Talk;
9
- /** Words of the player's as the AI is given them: their text, and in analysis the move the board page showed as they wrote, 52...♚f6 (D-101). */
9
+ /**
10
+ * Words of the player's as the AI is given them: their text, and in analysis the move the board page showed as they wrote, 52...♚f6 (D-101), with the
11
+ * variation they played there as PGN writes it (D-116).
12
+ */
10
13
  export interface Written {
11
14
  readonly text: string;
12
15
  readonly place?: string;
16
+ readonly variation?: string;
17
+ /** The game of the variation, when it is not the one in analysis. */
18
+ readonly variationGame?: string;
13
19
  }
14
20
  /** What the player wrote on the board page that the AI has not been given in this chat, given now: each game tool gives it once; never to an agent (D-89). */
15
21
  export declare function newWords(context: Context): Written[];
@@ -1,9 +1,9 @@
1
1
  // The talk on the board page (D-51): a line of the player's or the AI's, saved with the game, and the player's words the AI has not been given yet.
2
2
  import { endWithMood } from "../../core/index.js";
3
- import { archiveEnded, endOf, gameInAnalysis, loadGame, loadTableTalk, saveGame, saveTableTalk, talkLimits, talkLine, unheardWords } from "../../games/index.js";
3
+ import { archiveMood, endOf, gameInAnalysis, loadGame, loadTableTalk, saveGame, saveTableTalk, talkLimits, talkLine, unheardWords } from "../../games/index.js";
4
4
  import { gameFolder, helpsIn } from "../../profiles/index.js";
5
5
  import { gameAt } from "./clock.js";
6
- import { shownMove } from "./lines.js";
6
+ import { shownMove, shownMoves } from "./lines.js";
7
7
  import { openingTags } from "./opening-tags.js";
8
8
  /**
9
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).
@@ -25,7 +25,7 @@ export function say(folder, who, text, at, mood, shown) {
25
25
  }
26
26
  saveGame(folder, current);
27
27
  if (mood !== undefined) {
28
- archiveEnded(folder, current, openingTags(current.game.record));
28
+ archiveMood(folder, current, mood, openingTags(current.game.record));
29
29
  }
30
30
  return current;
31
31
  }
@@ -58,11 +58,29 @@ export function give(context, words) {
58
58
  context.heard?.();
59
59
  }
60
60
  const analysed = words.some((word) => word.shown !== undefined) ? gameInAnalysis(gameFolder(context.home()))?.game : undefined;
61
- return words.map(({ text, shown }) => shown === undefined ? { text } : { text, place: placeOf(shown, analysed) });
61
+ return words.map(({ text, shown }) => {
62
+ if (shown === undefined) {
63
+ return { text };
64
+ }
65
+ const place = placeOf(shown, analysed);
66
+ if (shown.variation === undefined) {
67
+ return { text, place };
68
+ }
69
+ return shown.game === analysed ? { text, place, variation: shown.variation.text } : { text, place, variation: shown.variation.text, variationGame: shown.game };
70
+ });
62
71
  }
63
- /** Where the page stood: the move shown, as the player saw it, or the start; with the game's name when the page shows another now. */
64
- function placeOf({ game, move }, analysed) {
65
- return `${move === undefined ? "the start" : shownMove(move)}${game === analysed ? "" : ` of ${game}`}`;
72
+ /**
73
+ * Where the page stood: the move shown, as the player saw it, or the start; on a variation, its move shown and the whole variation, or the game's move with
74
+ * the variation set aside (D-116); with the game's name when the page shows another now.
75
+ */
76
+ function placeOf({ game, move, variation }, analysed) {
77
+ const there = move === undefined ? "the start" : shownMove(move);
78
+ const other = game === analysed ? "" : variation === undefined ? ` of ${game}` : ` in ${game}`;
79
+ if (variation === undefined) {
80
+ return `${there}${other}`;
81
+ }
82
+ const line = `the variation ${shownMoves(variation.text)}`;
83
+ return `${variation.move === undefined ? `${there}, ${line} set aside` : `${shownMove(variation.move)} of ${line}`}${other}`;
66
84
  }
67
85
  /** The most a line of the AI's may hold in the games of `folder`: an analyst's note with the helps on (D-78), or with no game going on (D-101); a longer one is refused. */
68
86
  function aiMost(folder, who, text, going) {
@@ -6,16 +6,24 @@ import type { Context } from "../registry/index.ts";
6
6
  export declare const studyInput: {
7
7
  game: z.ZodOptional<z.ZodString>;
8
8
  move: z.ZodOptional<z.ZodString>;
9
+ line: z.ZodOptional<z.ZodString>;
9
10
  };
10
11
  /** A game to study: its record, and its name in the archive when it is there. */
11
12
  export interface Studied {
12
13
  readonly record: GameRecord;
13
14
  readonly name?: string;
14
15
  }
15
- /** A position of a game: the game as it stood after a move, and that move as the analysis writes it (12...Kf8); none at the start. */
16
+ /**
17
+ * A position of a game: the game as it stood after a move, and that move as the analysis writes it (12...Kf8); none at the start. After a variation, the
18
+ * variation as PGN writes it and the game's own move it takes the place of, none past the game's end.
19
+ */
16
20
  export interface StudiedPosition {
17
21
  readonly game: Game;
18
22
  readonly move?: string;
23
+ readonly variation?: {
24
+ readonly text: string;
25
+ readonly instead?: string;
26
+ };
19
27
  }
20
28
  /** 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. */
21
29
  export declare function noGameGoingOn(context: Context): CurrentGame | undefined;
@@ -32,9 +40,20 @@ export declare function namedGame(context: Context, name: string): Required<Stud
32
40
  export declare function moveIndex(record: GameRecord, move: string): number;
33
41
  /**
34
42
  * A position of a game over as the board page shows it in analysis (D-101): the game's name, how many of its moves it shows, and the last of them numbered;
35
- * none for a game or a position there is not, or while a game goes on.
43
+ * none for a game or a position there is not, or while a game goes on. With the variation the player played there (D-116), refused when it cannot be played.
36
44
  */
37
- export declare function shownPosition(context: Context, game: string, at: number): Shown | undefined;
45
+ export declare function shownPosition(context: Context, game: string, at: number, variation?: VariationInput): Shown | undefined;
46
+ /** A variation as the board page sends it (D-116): how many of the game's moves come before it, its moves, and how many of them the board shows. */
47
+ export interface VariationInput {
48
+ readonly at: number;
49
+ readonly moves: readonly string[];
50
+ readonly cursor: number;
51
+ }
52
+ /**
53
+ * The position after a variation written as the AI writes it, 12...Kf8 13.Nxe5 Bxe2: from the position `move` names, or else from the number of its first
54
+ * move; refused when neither says where it starts, or they disagree.
55
+ */
56
+ export declare function positionAfterLine(record: GameRecord, move: string | undefined, line: string): StudiedPosition;
38
57
  /** The number of each move of a game as the analysis writes it: 1. for White's first move, 1... for Black's. */
39
58
  export declare function moveNumbers(record: GameRecord): string[];
40
59
  /**
@@ -44,5 +63,8 @@ export declare function moveNumbers(record: GameRecord): string[];
44
63
  export declare function positionAfter(record: GameRecord, move?: string): StudiedPosition;
45
64
  /** A tag PGN leaves unknown with ? or -, as nothing. */
46
65
  export declare function knownTag(tag: string | undefined): string | undefined;
47
- /** Where a position of a game studied stands: After 12...Kf8 in games/2026-11-02-153012.pgn: White to move. */
48
- export declare function positionLine({ game, move }: StudiedPosition, name?: string): string;
66
+ /**
67
+ * Where a position of a game studied stands: After 12...Kf8 in games/2026-11-02-153012.pgn: White to move. After a variation, with the game's move it takes
68
+ * the place of: After the variation 12...Kf8 13.Nxe5 Bxe2 (the game: 12...Qd7) in games/2026-11-02-153012.pgn: White to move.
69
+ */
70
+ export declare function positionLine({ game, move, variation }: StudiedPosition, name?: string): string;
@@ -10,6 +10,8 @@ import { colourName } from "./view.js";
10
10
  export const studyInput = {
11
11
  game: z.string().optional().describe("a game's name, as past_games, study_games or analyse_game writes it: one of its positions; without it, the game on the board page"),
12
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
+ line: z.string().max(800).optional().describe("a variation played on a scratch board from the position move names, or from the number of its first move: 12...Kf8 13.Nxe5 Bxe2; the position after it. "
14
+ + "The game stays as it is"),
13
15
  };
14
16
  /** 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
17
  export function noGameGoingOn(context) {
@@ -65,20 +67,46 @@ export function moveIndex(record, move) {
65
67
  }
66
68
  /**
67
69
  * A position of a game over as the board page shows it in analysis (D-101): the game's name, how many of its moves it shows, and the last of them numbered;
68
- * none for a game or a position there is not, or while a game goes on.
70
+ * none for a game or a position there is not, or while a game goes on. With the variation the player played there (D-116), refused when it cannot be played.
69
71
  */
70
- export function shownPosition(context, game, at) {
72
+ export function shownPosition(context, game, at, variation) {
73
+ let studied;
71
74
  try {
72
75
  noGameGoingOn(context);
73
- const { name, record } = namedGame(context, game);
74
- if (!Number.isInteger(at) || at < 0 || at > record.moves.length) {
75
- return undefined;
76
- }
77
- return at === 0 ? { game: name, at } : { game: name, at, move: `${moveNumbers(record)[at - 1]}${record.moves[at - 1].san}` };
76
+ studied = namedGame(context, game);
78
77
  }
79
78
  catch {
80
79
  return undefined;
81
80
  }
81
+ const { name, record } = studied;
82
+ if (!Number.isInteger(at) || at < 0 || at > record.moves.length) {
83
+ return undefined;
84
+ }
85
+ const shown = at === 0 ? { game: name, at } : { game: name, at, move: `${moveNumbers(record)[at - 1]}${record.moves[at - 1].san}` };
86
+ return variation === undefined ? shown : { ...shown, variation: shownVariation(record, variation) };
87
+ }
88
+ /**
89
+ * The position after a variation written as the AI writes it, 12...Kf8 13.Nxe5 Bxe2: from the position `move` names, or else from the number of its first
90
+ * move; refused when neither says where it starts, or they disagree.
91
+ */
92
+ export function positionAfterLine(record, move, line) {
93
+ const moves = lineMoves(line);
94
+ if (moves.length === 0) {
95
+ throw new Error("The line has no moves: write them as PGN does, 12...Kf8 13.Nxe5 Bxe2.");
96
+ }
97
+ if (moves.length > mostLinePlies) {
98
+ throw new Error(`The line has ${moves.length} moves: give at most ${mostLinePlies}.`);
99
+ }
100
+ const first = firstNumber.exec(moves[0]);
101
+ const from = move === undefined ? lineStart(record, first) : positionAfter(record, move).game.record.moves.length;
102
+ const label = nextLabel(new Game({ ...record, moves: record.moves.slice(0, from) }));
103
+ if (first !== null && label !== `${Number(first[1])}${first[2] === "." ? "." : "..."}`) {
104
+ const where = move === undefined ? "" : ` after ${move.trim()}`;
105
+ throw new Error(`The line starts with ${first[0]}, but the next move${where} is ${label}: number its first move so, or give move, the game's move it leaves from.`);
106
+ }
107
+ const played = playLine(record, from, moves);
108
+ const instead = record.moves[from] === undefined ? undefined : `${moveNumbers(record)[from]}${record.moves[from].san}`;
109
+ return { game: played.game, variation: instead === undefined ? { text: played.text } : { text: played.text, instead } };
82
110
  }
83
111
  /** The number of each move of a game as the analysis writes it: 1. for White's first move, 1... for Black's. */
84
112
  export function moveNumbers(record) {
@@ -105,12 +133,52 @@ export function positionAfter(record, move) {
105
133
  export function knownTag(tag) {
106
134
  return tag === undefined || /^[?.\-\s]*$/.test(tag) ? undefined : tag;
107
135
  }
108
- /** Where a position of a game studied stands: After 12...Kf8 in games/2026-11-02-153012.pgn: White to move. */
109
- export function positionLine({ game, move }, name) {
110
- const where = `${move === undefined ? "At the start" : `After ${move}`}${name === undefined ? "" : ` in ${name}`}`;
136
+ /**
137
+ * Where a position of a game studied stands: After 12...Kf8 in games/2026-11-02-153012.pgn: White to move. After a variation, with the game's move it takes
138
+ * the place of: After the variation 12...Kf8 13.Nxe5 Bxe2 (the game: 12...Qd7) in games/2026-11-02-153012.pgn: White to move.
139
+ */
140
+ export function positionLine({ game, move, variation }, name) {
141
+ const after = variation === undefined
142
+ ? move === undefined ? "At the start" : `After ${move}`
143
+ : `After the variation ${variation.text} (${variation.instead === undefined ? "past the game's end" : `the game: ${variation.instead}`})`;
144
+ const where = `${after}${name === undefined ? "" : ` in ${name}`}`;
111
145
  const { outcome, position } = game;
112
146
  return `${where}: ${outcome.over ? `the board ends the game by ${outcome.ending}` : `${colourName(position.turn)} to move`}.`;
113
147
  }
148
+ /**
149
+ * The variation the page showed with the player's words (D-116), its moves played from the game's first `at` on a scratch board: refused at the first that
150
+ * cannot be played, or when the game has no such position. The moves are kept in SAN as chess.js writes them, whatever the page sent.
151
+ */
152
+ function shownVariation(record, variation) {
153
+ const { at, moves, cursor } = variation;
154
+ if (!Number.isInteger(at) || at < 0 || at > record.moves.length || moves.length === 0 || moves.length > mostLinePlies) {
155
+ throw new Error(`A variation leaves from one of the game's ${record.moves.length + 1} positions and has 1 to ${mostLinePlies} moves.`);
156
+ }
157
+ if (!Number.isInteger(cursor) || cursor < 0 || cursor > moves.length) {
158
+ throw new Error(`The variation shows one of its ${moves.length} moves, or none: its cursor goes from 0 to ${moves.length}.`);
159
+ }
160
+ const played = playLine(record, at, moves);
161
+ const sans = played.game.record.moves.slice(at).map(({ san }) => san);
162
+ return { at, moves: sans, cursor, text: played.text, ...(cursor === 0 ? {} : { move: played.numbered[cursor - 1] }) };
163
+ }
164
+ /**
165
+ * Plays moves on a scratch board from the game's first `from` moves (D-28), each in SAN, with figurines or as squares, with or without its number: refused
166
+ * at the first that cannot be played, with why. The game itself never changes.
167
+ */
168
+ function playLine(record, from, moves) {
169
+ const game = new Game({ ...record, moves: record.moves.slice(0, from) });
170
+ const numbered = [];
171
+ for (const written of moves) {
172
+ const label = nextLabel(game);
173
+ const played = game.play(written);
174
+ if (!played.played) {
175
+ const before = numbered.length === 0 ? "" : ` after ${pgnText(numbered)}`;
176
+ throw new Error(`${label}${written.replace(numberBefore, "")} cannot be played${before}: ${played.message} The game stays as it is.`);
177
+ }
178
+ numbered.push(`${label}${played.move.san}`);
179
+ }
180
+ return { game, numbered, text: pgnText(numbered) };
181
+ }
114
182
  function plyOf(record, numbers, move) {
115
183
  const [, number, dots, san] = movePattern.exec(move) ?? [];
116
184
  if (number === undefined) {
@@ -136,3 +204,46 @@ function isMove(record, ply, written) {
136
204
  return played.played && played.move.san === record.moves[ply].san;
137
205
  }
138
206
  const movePattern = /^\s*(\d+)\s*(\.\.\.|\.|…)?\s*(\S+)?\s*$/;
207
+ /** The most moves a variation holds, on the page and in look and position. */
208
+ const mostLinePlies = 100;
209
+ /** The number written before a move, 12. or 12... or 12…, and what it says. */
210
+ const firstNumber = /^(\d+)\s*(\.\.\.|…|\.)/;
211
+ const numberBefore = /^\d+\s*(?:\.+|…)\s*/;
212
+ /** The number the next move of a game takes as PGN writes it: 13. for White's, 13... for Black's. */
213
+ function nextLabel(game) {
214
+ const { moveNumber, turn } = game.position;
215
+ return `${moveNumber}${turn === "white" ? "." : "..."}`;
216
+ }
217
+ /** Numbered moves as PGN writes them: the number before White's moves, and before the first when it is Black's. */
218
+ function pgnText(numbered) {
219
+ return numbered.map((move, index) => index > 0 && move.includes("...") ? move.replace(/^\d+\.\.\./, "") : move).join(" ");
220
+ }
221
+ /** A line's moves, each with the number written before it when there is one: "12... Kf8 13. Nxe5" is 12...Kf8 and 13.Nxe5. */
222
+ function lineMoves(line) {
223
+ const moves = [];
224
+ let number = "";
225
+ for (const word of line.trim().split(/\s+/).filter((word) => word !== "")) {
226
+ if (/^\d+\s*(?:\.+|…)$/.test(word)) {
227
+ number = word;
228
+ }
229
+ else {
230
+ moves.push(`${number}${word}`);
231
+ number = "";
232
+ }
233
+ }
234
+ return moves;
235
+ }
236
+ /** How many of the game's moves come before a line by the number of its first move, 12... after White's 12th; refused without one or out of the game. */
237
+ function lineStart(record, first) {
238
+ if (first === null) {
239
+ throw new Error("Say where the line starts: number its first move, 12...Kf8, or give move, the game's move it leaves from.");
240
+ }
241
+ const start = new Game({ tags: record.tags, moves: [] }).position;
242
+ const ply = (Number(first[1]) - start.moveNumber) * 2 + (first[2] === "." ? 0 : 1) - (start.turn === "black" ? 1 : 0);
243
+ if (ply < 0 || ply > record.moves.length) {
244
+ const numbers = moveNumbers(record);
245
+ const span = numbers.length === 0 ? "it has no moves" : `its moves go from ${numbers[0]}${record.moves[0].san} to ${numbers.at(-1)}${record.moves.at(-1).san}`;
246
+ throw new Error(`The game has no position before ${first[0]}: ${span}.`);
247
+ }
248
+ return ply;
249
+ }
@@ -19,6 +19,7 @@ interface StudyView {
19
19
  export declare const look: import("../registry/registry.ts").Command<z.ZodObject<{
20
20
  game: z.ZodOptional<z.ZodString>;
21
21
  move: z.ZodOptional<z.ZodString>;
22
+ line: z.ZodOptional<z.ZodString>;
22
23
  only: z.ZodOptional<z.ZodEnum<{
23
24
  board: "board";
24
25
  }>>;
@@ -3,7 +3,7 @@ import { boardText, materialOf, sees } from "../../core/index.js";
3
3
  import { archivedName, endOf } from "../../games/index.js";
4
4
  import { analystAgent } from "../../host-files/index.js";
5
5
  import { gameFolder } from "../../profiles/index.js";
6
- import { analystOnView, fullView, gameAt, gameShown, headLines, materialText, newWords, positionAfter, positionLine, studyInput, summaryOf, wordLines } from "../answers/index.js";
6
+ import { analystOnView, fullView, gameAt, gameShown, headLines, materialText, newWords, positionAfter, positionAfterLine, positionLine, studyInput, summaryOf, wordLines } from "../answers/index.js";
7
7
  import { defineCommand } from "../registry/index.js";
8
8
  import { refuseUnseen } from "./levels.js";
9
9
  /** The whole view (D-26), when the compact one after the player's move is not enough; or the board for the player, where the host does not show it (D-36). */
@@ -13,15 +13,15 @@ export const look = defineCommand({
13
13
  + "moves of each piece. The views of player_move, status and new_game give attackers and defenders only for the pieces attacked: call look "
14
14
  + 'before you attack or capture a piece that is not attacked yet, or plan an exchange. With only: "board", the board as text for the player, to '
15
15
  + "copy into your message where they do not see it. With move, and game for a game of the archive, the same of any position of a game over, to "
16
- + 'analyse it. Example: look {}, look {move: "12..."}.',
16
+ + 'analyse it; with line, of the position after a variation played from there on a scratch board. Example: look {}, look {move: "12..."}, look {line: "12...Kf8 13.Nxe5"}.',
17
17
  input: z.object({ only: z.enum(["board"]).optional().describe("board: only the board as text, for the player"), ...studyInput }),
18
18
  // Read only for the host: a flag that has fallen is written, as the time already decided.
19
19
  readOnly: true,
20
20
  idempotent: true,
21
21
  sharedWith: [analystAgent],
22
- run: ({ only, game, move }, context) => {
23
- if (game !== undefined || move !== undefined) {
24
- return studyLook(context, only, game, move);
22
+ run: ({ only, game, move, line }, context) => {
23
+ if (game !== undefined || move !== undefined || line !== undefined) {
24
+ return studyLook(context, only, game, move, line);
25
25
  }
26
26
  const current = gameAt(gameFolder(context.home()), context.now());
27
27
  if (current === undefined) {
@@ -45,10 +45,10 @@ export const look = defineCommand({
45
45
  return [...("game" in look ? gameLines(look.game) : studyLines(look.study)), ...wordLines(look.words), ...(look.analyst === undefined ? [] : [look.analyst])].join("\n");
46
46
  },
47
47
  });
48
- /** A position of a game over, as the view of the game going on gives it. */
49
- function studyLook(context, only, name, move) {
48
+ /** A position of a game over, or after a variation from it (D-116), as the view of the game going on gives it. */
49
+ function studyLook(context, only, name, move, line) {
50
50
  const studied = gameShown(context, name);
51
- const at = positionAfter(studied.record, move);
51
+ const at = line === undefined ? positionAfter(studied.record, move) : positionAfterLine(studied.record, move, line);
52
52
  const { position } = at.game;
53
53
  if (only === "board") {
54
54
  return { board: boardText(position.fen) };
@@ -9,6 +9,7 @@ type Described = Profile & {
9
9
  export declare const position: import("../registry/registry.ts").Command<z.ZodObject<{
10
10
  game: z.ZodOptional<z.ZodString>;
11
11
  move: z.ZodOptional<z.ZodString>;
12
+ line: z.ZodOptional<z.ZodString>;
12
13
  }, z.core.$strip>, Described>;
13
14
  /** The profile as lines for the AI, White's facts before Black's; a line with nothing to say is left out. */
14
15
  export declare function profileLines(profile: Profile): string[];
@@ -3,7 +3,7 @@ import {} from "../../core/index.js";
3
3
  import { archivedName, endOf } from "../../games/index.js";
4
4
  import { analystAgent } from "../../host-files/index.js";
5
5
  import { gameFolder } from "../../profiles/index.js";
6
- import { analystOnView, colourName, gameAt, gameShown, materialText, positionAfter, positionLine, studyInput } from "../answers/index.js";
6
+ import { analystOnView, colourName, gameAt, gameShown, materialText, positionAfter, positionAfterLine, positionLine, studyInput } from "../answers/index.js";
7
7
  import { defineCommand } from "../registry/index.js";
8
8
  import { refuseUnseen } from "./levels.js";
9
9
  /** The positional profile of the game in progress (D-19), or of any position of a game over (D-68): separate facts, never a score. */
@@ -13,15 +13,15 @@ export const position = defineCommand({
13
13
  + "outposts, centre, space, control, mobility, development and castling, king safety, the phase, the draw rules and, with only kings and pawns, "
14
14
  + "the rule of the square and the opposition. Facts only, never a score: weigh them yourself. Call it when you set or change your plan: out of "
15
15
  + "the opening, after a big exchange, in the endgame; not at every move. With move, and game for a game of the archive, the facts of any position of a "
16
- + 'game over, to analyse it. Example: position {}, position {move: "24."}.',
16
+ + 'game over, to analyse it; with line, after a variation played from there on a scratch board. Example: position {}, position {move: "24."}.',
17
17
  input: z.object(studyInput),
18
18
  readOnly: true,
19
19
  idempotent: true,
20
20
  sharedWith: [analystAgent],
21
- run: ({ game, move }, context) => {
22
- if (game !== undefined || move !== undefined) {
21
+ run: ({ game, move, line }, context) => {
22
+ if (game !== undefined || move !== undefined || line !== undefined) {
23
23
  const studied = gameShown(context, game);
24
- const at = positionAfter(studied.record, move);
24
+ const at = line === undefined ? positionAfter(studied.record, move) : positionAfterLine(studied.record, move, line);
25
25
  return withAnalystLine({ ...at.game.profile, where: positionLine(at, studied.name) }, analystOnView(context, studied.name));
26
26
  }
27
27
  const current = gameAt(gameFolder(context.home()), context.now());
@@ -1,6 +1,6 @@
1
1
  import type { AnyCommand } from "./registry/index.ts";
2
2
  export { analyseGame, deleteReview, siteGames, SiteGamesRefused } from "./analysis/index.ts";
3
- export { analystOf, analystQuietAt, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition } from "./answers/index.ts";
3
+ export { analystOf, analystQuietAt, bookOpening, bookPlies, gameAt, gameToStudy, namedGame, playMove, PositionChanged, positionIn, samePosition, say, shownPosition, type VariationInput } from "./answers/index.ts";
4
4
  export { aiMove, answerDraw, ChatNotClear, DrawRefused, endGame, type MoveSentAlone, newGame, offerDraw, playMoveSentAlone, pliesBeforeClearing, startNewGame } from "./game/index.ts";
5
5
  export { chooseDrawnAvatar, deleteGame, editProfile, GameGoingOn, newProfile, ratingsAge, refreshRatings, useProfile } from "./profiles/index.ts";
6
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";
@@ -11,7 +11,7 @@ export { aiLevels, layersOf, levelForPlayer, levelOf, levelTag, sees, suggestedL
11
11
  export type { AiLevel, Layer } from "./levels.ts";
12
12
  export { materialOf } from "./material.ts";
13
13
  export type { Material } from "./material.ts";
14
- export { endWithMood, type Mood, moodAt, type MoodNow, moodOf, moods, moodTag, withoutMoods } from "./moods.ts";
14
+ export { endWithMood, type Mood, moodAt, type MoodNow, moodOf, moods, moodTag, recordWithMood, withoutMoods } from "./moods.ts";
15
15
  export { bookMoves, bookPlaceOf, indexBook, indexOpenings, openingOf, outOfBookMove, positionKeysOf } from "./openings.ts";
16
16
  export type { BookPlace, Opening, OpeningBook, OpeningIndex } from "./openings.ts";
17
17
  export { pgnDate, readPgn, readPgnTags, readPgnUnplayed, writePgn } from "./pgn.ts";
@@ -6,7 +6,7 @@ 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
- export { endWithMood, moodAt, moodOf, moods, moodTag, withoutMoods } from "./moods.js";
9
+ export { endWithMood, moodAt, moodOf, moods, moodTag, recordWithMood, withoutMoods } from "./moods.js";
10
10
  export { bookMoves, bookPlaceOf, indexBook, indexOpenings, openingOf, outOfBookMove, positionKeysOf } from "./openings.js";
11
11
  export { pgnDate, readPgn, readPgnTags, readPgnUnplayed, writePgn } from "./pgn.js";
12
12
  export { movesOnly } from "./record.js";
@@ -15,6 +15,8 @@ export declare function moodTag(mood: Mood): string;
15
15
  export declare function moodOf(comment: string | undefined): Mood | undefined;
16
16
  /** Gives the AI's mood on the last move of a game that has ended, in place of the one it had: the face the game ends with. A game without moves keeps none. */
17
17
  export declare function endWithMood(game: Game, mood: Mood): void;
18
+ /** A game's record with the mood it ends with on its last move, in place of the one it had, as endWithMood gives it: the rest of the record as it is. */
19
+ export declare function recordWithMood(record: GameRecord, mood: Mood): GameRecord;
18
20
  /** The AI's mood after the first `plies` moves of a record: the last it gave, while it lasts, else neutral. A mood on the player's move is the one the game ended with. */
19
21
  export declare function moodAt(record: GameRecord, ai: Side, plies?: number): MoodNow;
20
22
  /** The record without moods: another game's are not the AI's. */
@@ -14,11 +14,15 @@ export function moodOf(comment) {
14
14
  /** Gives the AI's mood on the last move of a game that has ended, in place of the one it had: the face the game ends with. A game without moves keeps none. */
15
15
  export function endWithMood(game, mood) {
16
16
  const plies = game.record.moves.length;
17
- const comment = game.record.moves[plies - 1]?.comment?.replace(moodPattern, "");
18
17
  if (plies > 0) {
19
- game.annotate(plies, { comment: comment === undefined || comment === "" ? moodTag(mood) : `${comment} ${moodTag(mood)}` });
18
+ game.annotate(plies, { comment: withMood(game.record.moves[plies - 1]?.comment, mood) });
20
19
  }
21
20
  }
21
+ /** A game's record with the mood it ends with on its last move, in place of the one it had, as endWithMood gives it: the rest of the record as it is. */
22
+ export function recordWithMood(record, mood) {
23
+ const last = record.moves.length - 1;
24
+ return { ...record, moves: record.moves.map((move, index) => index === last ? { ...move, comment: withMood(move.comment, mood) } : move) };
25
+ }
22
26
  /** The AI's mood after the first `plies` moves of a record: the last it gave, while it lasts, else neutral. A mood on the player's move is the one the game ended with. */
23
27
  export function moodAt(record, ai, plies = record.moves.length) {
24
28
  // The ply count from white's move of the first move number: 1 when black moves first.
@@ -43,3 +47,8 @@ export function withoutMoods(record) {
43
47
  }
44
48
  const neutral = { mood: "neutral", left: 0 };
45
49
  const moodPattern = /\s*\[%mood ([a-z]+)\]/;
50
+ // A move's comment with the mood in place of the one it had.
51
+ function withMood(comment, mood) {
52
+ const kept = comment?.replace(moodPattern, "");
53
+ return kept === undefined || kept === "" ? moodTag(mood) : `${kept} ${moodTag(mood)}`;
54
+ }