chesspal 0.2.3 → 0.2.4

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 (75) hide show
  1. package/dist/commands/agents.js +2 -1
  2. package/dist/commands/ai-say.d.ts +2 -1
  3. package/dist/commands/analyse-game.d.ts +2 -0
  4. package/dist/commands/analyse-game.js +7 -4
  5. package/dist/commands/annotate-game.d.ts +2 -0
  6. package/dist/commands/annotate-game.js +26 -8
  7. package/dist/commands/answer-lines.d.ts +8 -4
  8. package/dist/commands/answer-lines.js +19 -3
  9. package/dist/commands/coach.js +2 -2
  10. package/dist/commands/context.d.ts +2 -0
  11. package/dist/commands/delete-review.js +3 -2
  12. package/dist/commands/game-summary.d.ts +7 -2
  13. package/dist/commands/game-summary.js +7 -0
  14. package/dist/commands/index.d.ts +1 -1
  15. package/dist/commands/index.js +1 -1
  16. package/dist/commands/look.d.ts +9 -5
  17. package/dist/commands/look.js +19 -9
  18. package/dist/commands/ponder.js +1 -1
  19. package/dist/commands/position.js +2 -2
  20. package/dist/commands/say.d.ts +11 -6
  21. package/dist/commands/say.js +21 -13
  22. package/dist/commands/status.d.ts +3 -1
  23. package/dist/commands/status.js +31 -6
  24. package/dist/commands/study.d.ts +11 -1
  25. package/dist/commands/study.js +31 -2
  26. package/dist/commands/view.d.ts +17 -5
  27. package/dist/commands/view.js +67 -10
  28. package/dist/core/facts.d.ts +14 -2
  29. package/dist/core/facts.js +32 -7
  30. package/dist/core/game.d.ts +5 -0
  31. package/dist/core/game.js +9 -0
  32. package/dist/core/index.d.ts +1 -0
  33. package/dist/core/placement.d.ts +63 -0
  34. package/dist/core/placement.js +259 -0
  35. package/dist/core/possibilities.d.ts +38 -0
  36. package/dist/core/possibilities.js +254 -0
  37. package/dist/core/review.d.ts +4 -1
  38. package/dist/core/review.js +17 -6
  39. package/dist/games/analysis.d.ts +8 -4
  40. package/dist/games/analysis.js +9 -5
  41. package/dist/games/current-game.d.ts +16 -1
  42. package/dist/games/current-game.js +10 -8
  43. package/dist/games/files.d.ts +2 -0
  44. package/dist/games/files.js +7 -1
  45. package/dist/games/index.d.ts +2 -2
  46. package/dist/games/index.js +1 -1
  47. package/dist/games/table-talk.d.ts +2 -0
  48. package/dist/games/table-talk.js +5 -1
  49. package/dist/games/talk.d.ts +4 -3
  50. package/dist/games/talk.js +3 -3
  51. package/dist/page/app/app.js +36 -12
  52. package/dist/page/app/keys.js +16 -2
  53. package/dist/page/app/served.js +2 -2
  54. package/dist/page/board/board.js +2 -1
  55. package/dist/page/board/marks.js +4 -24
  56. package/dist/page/board/signs.js +23 -0
  57. package/dist/page/game/column.js +2 -2
  58. package/dist/page/game/game.css +12 -0
  59. package/dist/page/game/move-list.js +2 -2
  60. package/dist/page/game/plies.js +8 -0
  61. package/dist/page/helps/helps.css +3 -1
  62. package/dist/page/helps/helps.js +15 -15
  63. package/dist/page/layout/layout.css +29 -5
  64. package/dist/page/legend/legend.css +203 -0
  65. package/dist/page/legend/legend.js +169 -0
  66. package/dist/page/legend/view.js +54 -0
  67. package/dist/page/style.css +2 -0
  68. package/dist/page/talk/chat.css +90 -0
  69. package/dist/page/talk/talk.js +27 -5
  70. package/dist/page/text.js +38 -2
  71. package/dist/server/mcp.js +8 -6
  72. package/dist/server/page/game.js +20 -7
  73. package/dist/server/page/page.js +5 -3
  74. package/instructions/opponent.md +1 -1
  75. package/package.json +1 -1
@@ -1,5 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { type GameSummary } from "./game-summary.ts";
3
+ import { type Written } from "./say.ts";
3
4
  import { type Speech } from "./speech.ts";
4
5
  interface Status {
5
6
  version: string;
@@ -16,12 +17,13 @@ interface Status {
16
17
  /** Why the game on disk cannot be read, when it cannot. */
17
18
  problem?: string;
18
19
  /** What the player wrote on the board page that the AI has not answered yet, all of it. */
19
- words?: string[];
20
+ words?: Written[];
20
21
  /** The game in analysis on the board page once none goes on (D-69), which the player may have brought there. */
21
22
  analysis?: {
22
23
  game: string;
23
24
  brought?: true;
24
25
  analyst?: true;
26
+ review?: string;
25
27
  };
26
28
  /** A game going on from before this chat, told once a chat: the player says whether to go on with it. */
27
29
  fromBefore?: true;
@@ -1,4 +1,5 @@
1
1
  import { z } from "zod";
2
+ import { gameReviewOf, leadOf, reviewOf } from "../core/index.js";
2
3
  import { archivedName, cleanSince, endOf, gameInAnalysis, lastChatStart } from "../games/index.js";
3
4
  import { coachAgent, outdatedHostFiles } from "../host-files/index.js";
4
5
  import { gameFolder } from "../profiles/index.js";
@@ -13,6 +14,7 @@ import { playerLine } from "./profiles.js";
13
14
  import { defineCommand } from "./registry.js";
14
15
  import { unansweredWords } from "./say.js";
15
16
  import { speechLine, speechOf } from "./speech.js";
17
+ import { namedGame } from "./study.js";
16
18
  /** What Chesspal sees now, the first call of a chat: a dashboard under 150 tokens while the player is to move, with the next step. */
17
19
  export const status = defineCommand({
18
20
  name: "status",
@@ -60,7 +62,13 @@ export const status = defineCommand({
60
62
  const analysis = current === undefined || status.game?.end !== undefined ? gameInAnalysis(folder) : undefined;
61
63
  if (analysis !== undefined) {
62
64
  // The analyst agent goes over a game the player brought, where it runs, and the chat over what it reviewed (D-89).
63
- status.analysis = { game: analysis.game, ...(analysis.brought === true ? { brought: true } : {}), ...(analystRuns(context) ? { analyst: true } : {}) };
65
+ const review = analysis.brought === true ? undefined : reviewProgress(context, analysis.game);
66
+ status.analysis = {
67
+ game: analysis.game,
68
+ ...(analysis.brought === true ? { brought: true } : {}),
69
+ ...(analystRuns(context) ? { analyst: true } : {}),
70
+ ...(review === undefined ? {} : { review }),
71
+ };
64
72
  }
65
73
  }
66
74
  catch (error) {
@@ -103,16 +111,33 @@ function fromBeforeChat(context, started) {
103
111
  const start = lastChatStart(context.folder);
104
112
  return start === undefined || cleanSince(start, started);
105
113
  }
106
- /** The game in analysis; the next step when the player has just brought it on the board page. */
107
- function analysisLine({ game, brought, analyst }) {
114
+ /** The game in analysis, with how far its review has got; the next step when the player has just brought it on the board page. */
115
+ function analysisLine({ game, brought, analyst, review }) {
108
116
  if (brought === true) {
109
117
  return analyst === true
110
118
  ? `The player brought ${game} on the board page to analyse it. Next: ${analystLine(game)}`
111
119
  : `The player brought ${game} on the board page to analyse it. Next: call analyse_game {"game": "${game}"}, then keep your review on that page with annotate_game, and go over it with them.`;
112
120
  }
113
- return analyst === true
114
- ? `In analysis on the board page: ${game}. ${reviewedLine(game)}`
115
- : `In analysis on the board page: ${game}; analyse_game {"game": "${game}"} goes over it with the player.`;
121
+ const shown = `In analysis on the board page: ${game}${review === undefined ? "" : ` (${review})`}`;
122
+ return analyst === true ? `${shown}. ${reviewedLine(game)}` : `${shown}; analyse_game {"game": "${game}"} goes over it with the player.`;
123
+ }
124
+ /** How far the review of a game has got, as the board page shows it (Priya): done once it holds the word on the whole game. */
125
+ function reviewProgress(context, game) {
126
+ try {
127
+ const { record } = namedGame(context, game);
128
+ if (gameReviewOf(record) !== undefined) {
129
+ return "its review is done";
130
+ }
131
+ const noted = record.moves.filter(({ comment }) => {
132
+ const review = reviewOf(comment);
133
+ return review !== undefined && (review.badge !== "book" || review.note !== undefined);
134
+ }).length;
135
+ const estimated = record.moves.filter(({ comment }) => leadOf(comment) !== undefined).length;
136
+ return noted === 0 && estimated === 0 ? "no review yet" : `its review so far: notes on ${noted} moves, who is ahead after ${estimated} of ${record.moves.length}`;
137
+ }
138
+ catch {
139
+ return undefined;
140
+ }
116
141
  }
117
142
  /**
118
143
  * 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
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { Game, type GameRecord } from "../core/index.ts";
3
- import { type CurrentGame } from "../games/index.ts";
3
+ import { type CurrentGame, type Shown } from "../games/index.ts";
4
4
  import type { Context } from "./context.ts";
5
5
  /** What look and position take to show any position of a game that is over. */
6
6
  export declare const studyInput: {
@@ -21,10 +21,20 @@ export interface StudiedPosition {
21
21
  export declare function noGameGoingOn(context: Context): CurrentGame | undefined;
22
22
  /** The game played or kept to study by that name; without one, the current game over, or else the latest of the archive; refused while a game goes on. */
23
23
  export declare function gameToStudy(context: Context, name?: string): Studied;
24
+ /**
25
+ * The game look and position show: without a name, for the chat, the one in analysis on the board page, which the player's words there name by their move
26
+ * alone (D-101, Priya); else as gameToStudy. An agent names its game, since the page may turn to another meanwhile.
27
+ */
28
+ export declare function gameShown(context: Context, name?: string): Studied;
24
29
  /** A game played or kept to study by its name as the lists write it, or without its folder or its .pgn; refused when it is not there. */
25
30
  export declare function namedGame(context: Context, name: string): Required<Studied>;
26
31
  /** The index of a move in the game's record, given as analyse_game numbers it: 12. for White's, 12... for Black's, 12...Kf8 too. */
27
32
  export declare function moveIndex(record: GameRecord, move: string): number;
33
+ /**
34
+ * 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.
36
+ */
37
+ export declare function shownPosition(context: Context, game: string, at: number): Shown | undefined;
28
38
  /** The number of each move of a game as the analysis writes it: 1. for White's first move, 1... for Black's. */
29
39
  export declare function moveNumbers(record: GameRecord): string[];
30
40
  /**
@@ -2,13 +2,13 @@
2
2
  // goes on, nothing is studied: the analysis would help the game being played (D-60, D-62).
3
3
  import { z } from "zod";
4
4
  import { Game } from "../core/index.js";
5
- import { archivedGame, archivedGames, archivedName, endOf, keptGame, listedName } from "../games/index.js";
5
+ import { archivedGame, archivedGames, archivedName, endOf, gameInAnalysis, keptGame, listedName } from "../games/index.js";
6
6
  import { gameFolder } from "../profiles/index.js";
7
7
  import { gameAt } from "./clock.js";
8
8
  import { colourName } from "./view.js";
9
9
  /** What look and position take to show any position of a game that is over. */
10
10
  export const studyInput = {
11
- game: z.string().optional().describe("a game's name, as past_games, study_games or analyse_game writes it: one of its positions"),
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
13
  };
14
14
  /** The current game when it is over, or none; refused while it goes on, for every game: games are analysed once the one being played is over. */
@@ -38,6 +38,18 @@ export function gameToStudy(context, name) {
38
38
  }
39
39
  return { record: archivedGame(folder, latest), name: latest };
40
40
  }
41
+ /**
42
+ * The game look and position show: without a name, for the chat, the one in analysis on the board page, which the player's words there name by their move
43
+ * alone (D-101, Priya); else as gameToStudy. An agent names its game, since the page may turn to another meanwhile.
44
+ */
45
+ export function gameShown(context, name) {
46
+ const analysis = name === undefined && context.agent === undefined ? gameInAnalysis(gameFolder(context.home())) : undefined;
47
+ if (analysis === undefined) {
48
+ return gameToStudy(context, name);
49
+ }
50
+ noGameGoingOn(context);
51
+ return namedGame(context, analysis.game);
52
+ }
41
53
  /** A game played or kept to study by its name as the lists write it, or without its folder or its .pgn; refused when it is not there. */
42
54
  export function namedGame(context, name) {
43
55
  const folder = gameFolder(context.home());
@@ -48,6 +60,23 @@ export function namedGame(context, name) {
48
60
  export function moveIndex(record, move) {
49
61
  return plyOf(record, moveNumbers(record), move);
50
62
  }
63
+ /**
64
+ * 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;
65
+ * none for a game or a position there is not, or while a game goes on.
66
+ */
67
+ export function shownPosition(context, game, at) {
68
+ try {
69
+ noGameGoingOn(context);
70
+ const { name, record } = namedGame(context, game);
71
+ if (!Number.isInteger(at) || at < 0 || at > record.moves.length) {
72
+ return undefined;
73
+ }
74
+ return at === 0 ? { game: name, at } : { game: name, at, move: `${moveNumbers(record)[at - 1]}${record.moves[at - 1].san}` };
75
+ }
76
+ catch {
77
+ return undefined;
78
+ }
79
+ }
51
80
  /** The number of each move of a game as the analysis writes it: 1. for White's first move, 1... for Black's. */
52
81
  export function moveNumbers(record) {
53
82
  const start = new Game({ tags: record.tags, moves: [] }).position;
@@ -1,10 +1,22 @@
1
- import type { Facts, Material, Side } from "../core/index.ts";
2
- /** The view after the player's move: the check, the pieces attacked, then what is at risk, the pins, the checks and the moves of each piece. */
3
- export declare function compactView(facts: Facts, turn: Side): string[];
1
+ import type { Chances, Facts, Material, Possibilities, Side } from "../core/index.ts";
2
+ /** What the moves would do (D-100), and the other side's threats, none in check. */
3
+ export interface Possible {
4
+ readonly possibilities: Possibilities;
5
+ readonly threats: Chances | undefined;
6
+ /** The level sees the threats' double attacks and discovered checks, and the stalemate traps: from the strong level up. */
7
+ readonly strong: boolean;
8
+ }
9
+ /**
10
+ * The view after the player's move, in the masters' order (D-100): the check, the checks, the captures, the threats, what the moves would do, the pins,
11
+ * skewers and loose pieces, the pieces attacked, the moves of each piece.
12
+ */
13
+ export declare function compactView(facts: Facts, turn: Side, possible?: Possible): string[];
4
14
  /** The view of a level without the tactical facts (D-60): the check and the legal moves by piece, which are the rules (D-17). */
5
15
  export declare function boardView(facts: Facts, turn: Side): string[];
6
- /** The whole view, for look: every piece with its attackers and defenders, then the same lines as the compact view. */
7
- export declare function fullView(facts: Facts, turn: Side): string[];
16
+ /** The whole view, for look: the same lines as the compact view, with every piece's attackers and defenders. */
17
+ export declare function fullView(facts: Facts, turn: Side, possible?: Possible): string[];
18
+ /** What the moves of the side to move would do, in one line for a move of the analysis: fork Nc7+ (Ke8 Ra8); stalemate Qf7; none when nothing. */
19
+ export declare function tacticsText(facts: Facts, possibilities: Possibilities, withTraps: boolean): string | undefined;
8
20
  /** What is at risk, in one line for a move of the analysis (D-68): undefended Black Nf6; by a cheaper piece White Qd1; outnumbered Black Pe5; none when nothing is. */
9
21
  export declare function riskLine(facts: Facts): string | undefined;
10
22
  export declare function colourName(side: Side): string;
@@ -1,23 +1,37 @@
1
- /** The view after the player's move: the check, the pieces attacked, then what is at risk, the pins, the checks and the moves of each piece. */
2
- export function compactView(facts, turn) {
1
+ /**
2
+ * The view after the player's move, in the masters' order (D-100): the check, the checks, the captures, the threats, what the moves would do, the pins,
3
+ * skewers and loose pieces, the pieces attacked, the moves of each piece.
4
+ */
5
+ export function compactView(facts, turn, possible) {
3
6
  const names = namesOf(facts);
4
7
  const attacked = officersOf(facts).filter((piece) => piece.attackers.length > 0);
5
8
  const pieces = attacked.length === 0 ? ["Attacked pieces: none"] : ["Attacked pieces (attackers / defenders):", ...attacked.map((piece) => pieceLine(piece, names, true))];
6
- return [...checkLines(facts, turn, names), ...pieces, ...tacticLines(facts, turn, names)];
9
+ return [...checkLines(facts, turn, names), ...tacticLines(facts, turn, names, possible, pieces)];
7
10
  }
8
11
  /** The view of a level without the tactical facts (D-60): the check and the legal moves by piece, which are the rules (D-17). */
9
12
  export function boardView(facts, turn) {
10
13
  const names = namesOf(facts);
11
14
  return [...checkLines(facts, turn, names), ...legalLines(facts, names)];
12
15
  }
13
- /** The whole view, for look: every piece with its attackers and defenders, then the same lines as the compact view. */
14
- export function fullView(facts, turn) {
16
+ /** The whole view, for look: the same lines as the compact view, with every piece's attackers and defenders. */
17
+ export function fullView(facts, turn, possible) {
15
18
  const names = namesOf(facts);
16
19
  const sides = sidesInOrder.flatMap((side) => [
17
20
  `${colourName(side)} pieces (attackers / defenders):`,
18
21
  ...officersOf(facts).filter((piece) => piece.side === side).map((piece) => pieceLine(piece, names, false)),
19
22
  ]);
20
- return [...checkLines(facts, turn, names), ...sides, ...tacticLines(facts, turn, names)];
23
+ return [...checkLines(facts, turn, names), ...tacticLines(facts, turn, names, possible, sides)];
24
+ }
25
+ /** What the moves of the side to move would do, in one line for a move of the analysis: fork Nc7+ (Ke8 Ra8); stalemate Qf7; none when nothing. */
26
+ export function tacticsText(facts, possibilities, withTraps) {
27
+ const names = namesOf(facts);
28
+ const traps = withTraps ? possibilities.stalemateTraps.map(({ san, replies }) => `${san} (if ${replies === "all" ? "any reply" : replies.join(" or ")})`) : [];
29
+ const parts = [
30
+ ...chanceParts(possibilities, names),
31
+ kindPart("stalemate", possibilities.stalemates),
32
+ kindPart("stalemate trap", traps),
33
+ ].filter((part) => part !== undefined);
34
+ return parts.length === 0 ? undefined : parts.join("; ");
21
35
  }
22
36
  /** What is at risk, in one line for a move of the analysis (D-68): undefended Black Nf6; by a cheaper piece White Qd1; outnumbered Black Pe5; none when nothing is. */
23
37
  export function riskLine(facts) {
@@ -54,22 +68,65 @@ const sidesInOrder = ["white", "black"];
54
68
  function checkLines(facts, turn, names) {
55
69
  return facts.checkedBy === undefined ? [] : [`${colourName(turn)} is in check from ${facts.checkedBy.map(names.piece).join(" and ")}.`];
56
70
  }
57
- /** What is at risk, loose and pinned, the checks of both sides, and the legal moves of the side to move by piece. */
58
- function tacticLines(facts, turn, names) {
71
+ /**
72
+ * In the masters' order (D-100): the side to move's checks and captures, the threats (what is at risk, the other side's checks and chances), what its
73
+ * moves would do, the pins, skewers and loose pieces, the pieces in detail, its legal moves by piece.
74
+ */
75
+ function tacticLines(facts, turn, names, possible, detail) {
59
76
  const list = (squares) => bySide(facts, squares, names) || "none";
60
77
  const atRisk = facts.undefended.length === 0 && facts.attackedByCheaper.length === 0 && facts.outnumbered.length === 0
61
78
  ? "At risk: none"
62
79
  : `Attacked and undefended: ${list(facts.undefended)}; defended, but attacked by a cheaper piece: ${list(facts.attackedByCheaper)}; `
63
80
  + `attacked more times than defended: ${list(facts.outnumbered)}`;
64
81
  const pins = facts.pins.map((pin) => `${names.sided(pin.square)} to ${names.piece(pin.to)} by ${names.piece(pin.by)}`);
82
+ const skewers = facts.skewers.map((skewer) => `${names.sided(skewer.square)} with ${names.piece(skewer.to)} behind, by ${names.piece(skewer.by)}`);
83
+ const [ownChecks, ...otherChecks] = checksLines(facts, turn);
84
+ // A game over has no moves to tell of.
85
+ const moves = possible !== undefined && facts.pieces.some((piece) => (piece.moves?.length ?? 0) > 0) ? possible : undefined;
65
86
  return [
87
+ ownChecks,
88
+ ...(moves === undefined ? [] : [captureLine(moves.possibilities)]),
66
89
  atRisk,
67
- `Loose: ${list(facts.loose)}`,
90
+ ...otherChecks,
91
+ ...(moves === undefined ? [] : threatLines(moves, turn, names)),
92
+ ...(moves === undefined ? [] : [`${colourName(turn)} (to move) tactics: ${tacticsText(facts, moves.possibilities, moves.strong) ?? "none"}`]),
68
93
  `Pinned: ${pins.join(", ") || "none"}`,
69
- ...checksLines(facts, turn),
94
+ ...(skewers.length === 0 ? [] : [`Skewered: ${skewers.join(", ")}`]),
95
+ `Loose: ${list(facts.loose)}`,
96
+ ...detail,
70
97
  ...legalLines(facts, names),
71
98
  ];
72
99
  }
100
+ /** The captures of the side to move, each with what the piece taken is to the piece taking it. */
101
+ function captureLine({ captures }) {
102
+ return `Captures: ${captures.map((capture) => (capture.fact === undefined ? capture.san : `${capture.san} (${capture.fact})`)).join(", ") || "none"}`;
103
+ }
104
+ /** What the other side's moves would do if it were to move: its promotions, and as the level sees them its double attacks and discovered checks. */
105
+ function threatLines({ threats, strong }, turn, names) {
106
+ if (threats === undefined) {
107
+ return [];
108
+ }
109
+ const parts = [
110
+ kindPart("promote", threats.promotions),
111
+ ...(strong ? chanceParts({ ...threats, promotions: [] }, names) : []),
112
+ ].filter((part) => part !== undefined);
113
+ return parts.length === 0 ? [] : [`${colourName(turn === "white" ? "black" : "white")} threats, if it were to move: ${parts.join("; ")}`];
114
+ }
115
+ /** A side's forks, double attacks, double and discovered checks and promotions, each kind with its moves. */
116
+ function chanceParts(chances, names) {
117
+ const attack = ({ san, targets }) => `${san} (${targets.map(names.piece).join(" ")})`;
118
+ return [
119
+ kindPart("fork", chances.doubleAttacks.filter((each) => each.fork).map(attack)),
120
+ kindPart("double attack", chances.doubleAttacks.filter((each) => !each.fork).map(attack)),
121
+ kindPart("double check", chances.doubleChecks),
122
+ kindPart("discovered check", chances.discoveredChecks),
123
+ kindPart("promote", chances.promotions),
124
+ ];
125
+ }
126
+ /** A kind with its moves, comma-separated; none without moves. */
127
+ function kindPart(kind, moves) {
128
+ return moves.length === 0 ? undefined : `${kind} ${moves.join(", ")}`;
129
+ }
73
130
  /** The checks the side to move can give, and those the other side could give if it were to move; one line when neither has any. */
74
131
  function checksLines(facts, turn) {
75
132
  if (facts.checks.length === 0 && facts.otherChecks?.length === 0) {
@@ -1,4 +1,4 @@
1
- import { Chess, type Color, type Square } from "chess.js";
1
+ import { Chess, type Color, type PieceSymbol, type Square } from "chess.js";
2
2
  import type { Move, Side } from "./game.ts";
3
3
  /** A piece's letter, as SAN writes it, with P for a pawn. */
4
4
  export type PieceLetter = "K" | "Q" | "R" | "B" | "N" | "P";
@@ -60,8 +60,20 @@ export interface Facts {
60
60
  export declare function factsOf(chess: Chess, legal: readonly Move[]): Facts;
61
61
  /** Which pieces attack a square: chess.js's direct attacks, a piece pinned to its king only along the pin's line, and the pawns that take en passant. */
62
62
  export type Reach = (square: Square, color: Color) => Square[];
63
+ /** What the attacks read of a board: its pieces, where a side's pieces of a type stand, and which pieces of a side attack a square, as chess.js counts them. */
64
+ export interface Board {
65
+ get(square: Square): {
66
+ type: PieceSymbol;
67
+ color: Color;
68
+ } | undefined;
69
+ findPiece(piece: {
70
+ type: PieceSymbol;
71
+ color: Color;
72
+ }): Square[];
73
+ attackers(square: Square, color: Color): Square[];
74
+ }
63
75
  /** Which pieces attack a square as the facts count them: a piece pinned to its king only along the pin's line, though it still gives check; no en passant. */
64
- export declare function attacksOf(chess: Chess): Reach;
76
+ export declare function attacksOf(board: Board): Reach;
65
77
  /** The squares strictly between two squares on one line. */
66
78
  export declare function squaresBetween(from: Square, to: Square): Square[];
67
79
  export declare function fileOf(square: Square): number;
@@ -1,5 +1,6 @@
1
1
  // What a position shows, computed without judging it (D-19, D-26): who attacks and defends each piece, what is at risk, the pins and skewers, the checks, the moves of each piece.
2
2
  import { Chess } from "chess.js";
3
+ import { mayCastle, Placement, turnPassed } from "./placement.js";
3
4
  /** The facts of a chess.js board, whose legal moves are given: computed once for each position, by its key, and frozen. */
4
5
  export function factsOf(chess, legal) {
5
6
  const key = chess.hash();
@@ -24,8 +25,9 @@ const order = { k: 0, q: 1, r: 2, b: 3, n: 4, p: 5 };
24
25
  const lines = [[0, 1, "r"], [0, -1, "r"], [1, 0, "r"], [-1, 0, "r"], [1, 1, "b"], [1, -1, "b"], [-1, 1, "b"], [-1, -1, "b"]];
25
26
  const files = "abcdefgh";
26
27
  /** Which pieces attack a square as the facts count them: a piece pinned to its king only along the pin's line, though it still gives check; no en passant. */
27
- export function attacksOf(chess) {
28
- return alongPins(chess, piecesOf(chess).filter((piece) => piece.type === "k").flatMap((king) => pinsTo(chess, king)));
28
+ export function attacksOf(board) {
29
+ const kings = ["w", "b"].flatMap((color) => board.findPiece({ type: "k", color }).map((square) => ({ square, type: "k", color })));
30
+ return alongPins(board, kings.flatMap((king) => pinsTo(board, king)));
29
31
  }
30
32
  /** The squares strictly between two squares on one line. */
31
33
  export function squaresBetween(from, to) {
@@ -164,17 +166,40 @@ function enPassantVictim(chess) {
164
166
  }
165
167
  /** The checks of the side not to move, on the same board with the turn passed: no en passant, which belonged to the side to move. */
166
168
  function otherChecks(chess) {
167
- const fields = chess.fen().split(" ");
168
- fields[1] = chess.turn() === "w" ? "b" : "w";
169
- fields[3] = "-";
170
- return new Chess(fields.join(" ")).moves().filter((san) => /[+#]$/.test(san));
169
+ const other = chess.turn() === "w" ? "b" : "w";
170
+ // Castling may check, and the board of pieces alone does not try it: where the other side may castle, chess.js lists all its moves.
171
+ if (mayCastle(chess, other)) {
172
+ return turnPassed(chess).moves().filter((san) => /[+#]$/.test(san));
173
+ }
174
+ // Else its moves are tried and written on the board of pieces alone, in chess.js's order (D-34); chess.js writes the rare one it cannot.
175
+ const board = new Placement(chess);
176
+ const moves = board.movesOf(other);
177
+ const checks = moves.filter((move) => {
178
+ board.play(move);
179
+ const check = board.inCheck(chess.turn());
180
+ board.undo();
181
+ return check;
182
+ }).map((move) => board.sanOf(move, moves));
183
+ return checks.includes(undefined) ? turnPassed(chess).moves().filter((san) => /[+#]$/.test(san)) : checks;
171
184
  }
172
185
  /** The pieces on the board, White's first, each side's from the king down (K Q R B N P), then by square. */
173
186
  function piecesOf(chess) {
174
187
  return chess.board().flat().filter((cell) => cell !== null).map(({ square, type, color }) => ({ square, type, color })).sort((left, right) => compare(right.color, left.color) || order[left.type] - order[right.type] || compare(left.square, right.square));
175
188
  }
176
- /** The squares from a square to the edge of the board, one step at a time. */
189
+ /** The squares from a square to the edge of the board, one step at a time; worked out once for each square and step. */
177
190
  function rayFrom(from, fileStep, rankStep) {
191
+ const step = (fileStep + 1) * 3 + rankStep + 1;
192
+ let known = rays.get(from);
193
+ if (known === undefined) {
194
+ known = [];
195
+ rays.set(from, known);
196
+ }
197
+ known[step] ??= Object.freeze(walked(from, fileStep, rankStep));
198
+ return known[step];
199
+ }
200
+ // The rays from each square already walked, by their step.
201
+ const rays = new Map();
202
+ function walked(from, fileStep, rankStep) {
178
203
  const ray = [];
179
204
  let file = fileOf(from) + fileStep;
180
205
  let rank = rankOf(from) + rankStep;
@@ -1,6 +1,7 @@
1
1
  import { type Square } from "chess.js";
2
2
  import { type Facts } from "./facts.ts";
3
3
  import { type Illegal } from "./illegal.ts";
4
+ import { type Chances, type Possibilities } from "./possibilities.ts";
4
5
  import { type Profile } from "./profile.ts";
5
6
  import { type Annotation, type GameRecord } from "./record.ts";
6
7
  export type { Square } from "chess.js";
@@ -73,6 +74,10 @@ export declare class Game {
73
74
  get lastMove(): Move | undefined;
74
75
  /** What the position shows (D-26): attackers and defenders, pieces at risk, pins, checks, the moves of each piece; computed once for each position. */
75
76
  get facts(): Facts;
77
+ /** What the moves would do (D-100): captures, double attacks, discovered checks, promotions, stalemates; once for each position. */
78
+ get possibilities(): Possibilities;
79
+ /** What the other side's moves would do if it were to move (D-100), none in check; once for each position, only where it is shown. */
80
+ get threats(): Chances | undefined;
76
81
  /** The positional profile (D-19): pawns, files, outposts, space, mobility, development, king safety, control, phase, draws; computed once for each position. */
77
82
  get profile(): Profile;
78
83
  /** The legal moves, or those of the piece on a square. */
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 { possibilitiesOf, threatsOf } from "./possibilities.js";
5
6
  import { profileOf } from "./profile.js";
6
7
  import { cleanComment, recordedMove } from "./record.js";
7
8
  /**
@@ -65,6 +66,14 @@ export class Game {
65
66
  get facts() {
66
67
  return factsOf(this.chess, legalMovesOf(this.chess));
67
68
  }
69
+ /** What the moves would do (D-100): captures, double attacks, discovered checks, promotions, stalemates; once for each position. */
70
+ get possibilities() {
71
+ return possibilitiesOf(this.chess, legalMovesOf(this.chess));
72
+ }
73
+ /** What the other side's moves would do if it were to move (D-100), none in check; once for each position, only where it is shown. */
74
+ get threats() {
75
+ return threatsOf(this.chess);
76
+ }
68
77
  /** The positional profile (D-19): pawns, files, outposts, space, mobility, development, king safety, control, phase, draws; computed once for each position. */
69
78
  get profile() {
70
79
  const key = this.chess.hash();
@@ -14,6 +14,7 @@ export { endWithMood, type Mood, moodAt, type MoodNow, moodOf, moods, moodTag, w
14
14
  export { bookMoves, indexBook, indexOpenings, inOpenings, openingOf, outOfBookMove } from "./openings.ts";
15
15
  export type { Opening, OpeningBook, OpeningIndex } from "./openings.ts";
16
16
  export { pgnDate, readPgn, writePgn } from "./pgn.ts";
17
+ export type { Capture, Chances, DoubleAttack, Possibilities, StalemateTrap } from "./possibilities.ts";
17
18
  export type { BishopFacts, CentreSquare, KingFacts, Outpost, PawnFacts, PawnSquare, Phase, Profile, SideProfile } from "./profile.ts";
18
19
  export { movesOnly } from "./record.ts";
19
20
  export type { Annotation, GameRecord, RecordedMove } from "./record.ts";
@@ -0,0 +1,63 @@
1
+ import { Chess, type Color, type PieceSymbol, type Square } from "chess.js";
2
+ /** A piece, by its type and side. */
3
+ export interface Piece {
4
+ readonly type: PieceSymbol;
5
+ readonly color: Color;
6
+ }
7
+ /** A piece on its square. */
8
+ export interface Placed extends Piece {
9
+ readonly square: Square;
10
+ }
11
+ /** A move to try: its squares, and the piece a pawn becomes. */
12
+ export interface Tried {
13
+ readonly from: Square;
14
+ readonly to: Square;
15
+ readonly promotion?: PieceSymbol;
16
+ }
17
+ /** The pieces of a position, moved without the rules' checks and taken back in the reverse order. */
18
+ export declare class Placement {
19
+ #private;
20
+ constructor(chess: Chess);
21
+ get(square: Square): Piece | undefined;
22
+ /** The squares of a side's pieces of one type. */
23
+ findPiece({ type, color }: Piece): Square[];
24
+ /** A side's pieces. */
25
+ piecesOf(color: Color): Placed[];
26
+ /** The pieces of a side that attack a square, pinned or not: pawns, knights and kings by their steps, the others along their lines. */
27
+ attackers(square: Square, color: Color): Square[];
28
+ /** The squares the piece on a square attacks: by its steps, or along its lines up to the first piece. */
29
+ attackedFrom(square: Square): Square[];
30
+ /** Whether a square lies between a rook, bishop or queen of a side and a piece of the other side besides a pawn, on their line with nothing else between. */
31
+ isBetween(square: Square, color: Color): boolean;
32
+ /** Whether a piece of a side stands between its king and a rook, bishop or queen of the other side on their line. */
33
+ isPinned(color: Color): boolean;
34
+ /** Plays a move: the piece taken goes, en passant too, and a pawn becomes the piece it promotes to. It returns the piece taken. */
35
+ play(move: Tried): Piece | undefined;
36
+ /** Takes back the last move played. */
37
+ undo(): void;
38
+ /**
39
+ * A side's moves that leave its king safe, in the order chess.js lists them: the pawns' pushes, captures and promotions, the pieces' steps and lines;
40
+ * never castling or en passant.
41
+ */
42
+ movesOf(color: Color): Tried[];
43
+ /** Whether a side has a move of those, stopping at the first. */
44
+ hasMove(color: Color): boolean;
45
+ /**
46
+ * A move of those in SAN, as chess.js writes it, with the side's moves for the disambiguation; undefined for a pawn pushed two squares that would
47
+ * mate, which an en passant capture may answer: chess.js tells.
48
+ */
49
+ sanOf(move: Tried, moves: readonly Tried[]): string | undefined;
50
+ /** Whether a side's king is attacked. */
51
+ inCheck(color: Color): boolean;
52
+ }
53
+ /** A move's SAN, and where chess.js lists it among the side's moves: the lists keep that order. */
54
+ export interface Named {
55
+ readonly san: string;
56
+ readonly order: number;
57
+ }
58
+ /** The SAN of moves of the side to move of a chess.js board, and their order as chess.js lists them; read square by square, as moves need them. */
59
+ export declare function namesOn(boardOf: () => Chess): (move: Tried) => Named | undefined;
60
+ /** Whether a side may castle on a chess.js board, as far as the board of pieces alone would miss it: a right it keeps, with the squares to its rook empty. */
61
+ export declare function mayCastle(chess: Chess, color: Color): boolean;
62
+ /** The same board with the other side to move, without en passant, which belonged to the side to move. */
63
+ export declare function turnPassed(chess: Chess): Chess;