chesspal 0.2.3 → 0.3.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 (214) hide show
  1. package/README.md +11 -12
  2. package/dist/cli/cli.js +83 -3
  3. package/dist/cli/command-line.js +26 -9
  4. package/dist/cli/icon.d.ts +25 -0
  5. package/dist/cli/icon.js +80 -0
  6. package/dist/commands/agents.d.ts +19 -2
  7. package/dist/commands/agents.js +41 -14
  8. package/dist/commands/ai-say.d.ts +2 -1
  9. package/dist/commands/ai-say.js +3 -4
  10. package/dist/commands/analyse-game.d.ts +6 -0
  11. package/dist/commands/analyse-game.js +51 -15
  12. package/dist/commands/annotate-game.d.ts +4 -0
  13. package/dist/commands/annotate-game.js +60 -20
  14. package/dist/commands/answer-lines.d.ts +10 -4
  15. package/dist/commands/answer-lines.js +30 -4
  16. package/dist/commands/coach.js +6 -4
  17. package/dist/commands/context.d.ts +11 -1
  18. package/dist/commands/context.js +5 -0
  19. package/dist/commands/delete-review.js +9 -4
  20. package/dist/commands/game-summary.d.ts +9 -2
  21. package/dist/commands/game-summary.js +11 -0
  22. package/dist/commands/index.d.ts +4 -3
  23. package/dist/commands/index.js +8 -5
  24. package/dist/commands/init.js +2 -4
  25. package/dist/commands/instructions.d.ts +9 -0
  26. package/dist/commands/instructions.js +30 -0
  27. package/dist/commands/lead-input.d.ts +1 -1
  28. package/dist/commands/lead-input.js +7 -4
  29. package/dist/commands/look.d.ts +9 -5
  30. package/dist/commands/look.js +19 -9
  31. package/dist/commands/page-link.js +6 -5
  32. package/dist/commands/past-games.d.ts +1 -1
  33. package/dist/commands/past-games.js +12 -5
  34. package/dist/commands/player-move.js +2 -3
  35. package/dist/commands/ponder.js +1 -1
  36. package/dist/commands/position.js +2 -2
  37. package/dist/commands/profiles.d.ts +23 -9
  38. package/dist/commands/profiles.js +76 -35
  39. package/dist/commands/ratings.d.ts +8 -0
  40. package/dist/commands/ratings.js +45 -0
  41. package/dist/commands/registry.d.ts +1 -1
  42. package/dist/commands/registry.js +3 -2
  43. package/dist/commands/say.d.ts +11 -6
  44. package/dist/commands/say.js +21 -13
  45. package/dist/commands/status.d.ts +6 -2
  46. package/dist/commands/status.js +70 -26
  47. package/dist/commands/study.d.ts +11 -1
  48. package/dist/commands/study.js +39 -7
  49. package/dist/commands/view.d.ts +17 -5
  50. package/dist/commands/view.js +67 -10
  51. package/dist/core/facts.d.ts +14 -2
  52. package/dist/core/facts.js +32 -7
  53. package/dist/core/game.d.ts +5 -0
  54. package/dist/core/game.js +9 -0
  55. package/dist/core/index.d.ts +2 -1
  56. package/dist/core/index.js +1 -1
  57. package/dist/core/placement.d.ts +63 -0
  58. package/dist/core/placement.js +259 -0
  59. package/dist/core/possibilities.d.ts +38 -0
  60. package/dist/core/possibilities.js +254 -0
  61. package/dist/core/review.d.ts +6 -1
  62. package/dist/core/review.js +21 -6
  63. package/dist/games/ai-model.d.ts +4 -2
  64. package/dist/games/ai-model.js +10 -1
  65. package/dist/games/analysis.d.ts +31 -4
  66. package/dist/games/analysis.js +44 -5
  67. package/dist/games/archive.d.ts +6 -3
  68. package/dist/games/archive.js +13 -5
  69. package/dist/games/current-game.d.ts +26 -2
  70. package/dist/games/current-game.js +57 -21
  71. package/dist/games/files.d.ts +7 -0
  72. package/dist/games/files.js +48 -1
  73. package/dist/games/index.d.ts +4 -4
  74. package/dist/games/index.js +4 -4
  75. package/dist/games/table-talk.d.ts +2 -0
  76. package/dist/games/table-talk.js +5 -1
  77. package/dist/games/talk.d.ts +4 -3
  78. package/dist/games/talk.js +3 -3
  79. package/dist/home/data-folder.d.ts +2 -0
  80. package/dist/home/data-folder.js +5 -1
  81. package/dist/home/index.d.ts +1 -1
  82. package/dist/home/index.js +1 -1
  83. package/dist/host-files/agents.d.ts +2 -2
  84. package/dist/host-files/agents.js +12 -6
  85. package/dist/host-files/host-files.d.ts +5 -3
  86. package/dist/host-files/host-files.js +15 -8
  87. package/dist/host-files/index.d.ts +2 -2
  88. package/dist/host-files/index.js +2 -2
  89. package/dist/page/app/app.js +59 -22
  90. package/dist/page/app/keys.js +16 -2
  91. package/dist/page/app/served.js +2 -2
  92. package/dist/page/app/title.js +17 -0
  93. package/dist/page/avatars/torretta.js +4 -4
  94. package/dist/page/board/board.js +2 -1
  95. package/dist/page/board/marks.js +4 -24
  96. package/dist/page/board/signs.js +23 -0
  97. package/dist/page/dialogs/analyses.js +4 -4
  98. package/dist/page/dialogs/dialog.js +4 -1
  99. package/dist/page/dialogs/dialogs.css +84 -10
  100. package/dist/page/dialogs/profiles.js +95 -42
  101. package/dist/page/game/column.js +6 -2
  102. package/dist/page/game/game.css +12 -0
  103. package/dist/page/game/move-list.js +2 -2
  104. package/dist/page/game/plies.js +11 -0
  105. package/dist/page/guide/blocks.js +67 -0
  106. package/dist/page/guide/en.md +66 -0
  107. package/dist/page/guide/foot.js +18 -0
  108. package/dist/page/guide/guide.css +200 -0
  109. package/dist/page/guide/guide.js +89 -0
  110. package/dist/page/guide/it.md +66 -0
  111. package/dist/page/guide/markdown.js +67 -0
  112. package/dist/page/helps/helps.css +10 -11
  113. package/dist/page/helps/helps.js +15 -15
  114. package/dist/page/layout/favicon.svg +13 -0
  115. package/dist/page/layout/header.js +3 -3
  116. package/dist/page/layout/icon.icns +0 -0
  117. package/dist/page/layout/icon.ico +0 -0
  118. package/dist/page/layout/icon.png +0 -0
  119. package/dist/page/layout/layout.css +140 -9
  120. package/dist/page/layout/window.js +45 -0
  121. package/dist/page/legend/legend.css +203 -0
  122. package/dist/page/legend/legend.js +169 -0
  123. package/dist/page/legend/view.js +54 -0
  124. package/dist/page/main.js +4 -1
  125. package/dist/page/players/card.js +1 -1
  126. package/dist/page/profile/profile.js +14 -9
  127. package/dist/page/profile/settings.js +9 -0
  128. package/dist/page/style.css +4 -0
  129. package/dist/page/table/opener.css +65 -0
  130. package/dist/page/table/opener.js +59 -0
  131. package/dist/page/talk/chat.css +97 -0
  132. package/dist/page/talk/talk.css +38 -26
  133. package/dist/page/talk/talk.js +31 -7
  134. package/dist/page/text.js +120 -18
  135. package/dist/page/theme.css +7 -0
  136. package/dist/profiles/index.d.ts +2 -1
  137. package/dist/profiles/index.js +2 -1
  138. package/dist/profiles/profiles.d.ts +10 -15
  139. package/dist/profiles/profiles.js +30 -15
  140. package/dist/profiles/ratings.d.ts +35 -0
  141. package/dist/profiles/ratings.js +61 -0
  142. package/dist/server/bell.d.ts +24 -3
  143. package/dist/server/bell.js +84 -10
  144. package/dist/server/caller.d.ts +2 -1
  145. package/dist/server/caller.js +3 -2
  146. package/dist/server/mcp.d.ts +7 -3
  147. package/dist/server/mcp.js +106 -56
  148. package/dist/server/page/analyses.d.ts +2 -0
  149. package/dist/server/page/analyses.js +20 -5
  150. package/dist/server/page/attach.d.ts +4 -0
  151. package/dist/server/page/attach.js +112 -0
  152. package/dist/server/page/chat.d.ts +60 -0
  153. package/dist/server/page/chat.js +79 -0
  154. package/dist/server/page/events.d.ts +7 -0
  155. package/dist/server/page/events.js +20 -0
  156. package/dist/server/page/files.js +10 -1
  157. package/dist/server/page/game.js +62 -29
  158. package/dist/server/page/page.d.ts +47 -7
  159. package/dist/server/page/page.js +74 -51
  160. package/dist/server/page/profiles.d.ts +5 -0
  161. package/dist/server/page/profiles.js +26 -10
  162. package/dist/server/page/table.d.ts +39 -0
  163. package/dist/server/page/table.js +102 -0
  164. package/dist/server/table/browser.d.ts +6 -0
  165. package/dist/server/table/browser.js +48 -0
  166. package/dist/server/table/hosts/chats.d.ts +7 -0
  167. package/dist/server/table/hosts/chats.js +31 -0
  168. package/dist/server/table/hosts/find.d.ts +2 -0
  169. package/dist/server/table/hosts/find.js +20 -0
  170. package/dist/server/table/hosts/hosts.d.ts +34 -0
  171. package/dist/server/table/hosts/hosts.js +49 -0
  172. package/dist/server/table/hosts/index.d.ts +3 -0
  173. package/dist/server/table/hosts/index.js +4 -0
  174. package/dist/server/table/hosts/play.d.ts +25 -0
  175. package/dist/server/table/hosts/play.js +152 -0
  176. package/dist/server/table/hosts/seat.d.ts +25 -0
  177. package/dist/server/table/hosts/seat.js +74 -0
  178. package/dist/server/table/hosts/terminal.d.ts +6 -0
  179. package/dist/server/table/hosts/terminal.js +63 -0
  180. package/dist/server/table/icon/index.d.ts +32 -0
  181. package/dist/server/table/icon/index.js +83 -0
  182. package/dist/server/table/icon/linux.d.ts +5 -0
  183. package/dist/server/table/icon/linux.js +47 -0
  184. package/dist/server/table/icon/macos.d.ts +3 -0
  185. package/dist/server/table/icon/macos.js +38 -0
  186. package/dist/server/table/icon/windows.d.ts +3 -0
  187. package/dist/server/table/icon/windows.js +48 -0
  188. package/dist/server/table/index.d.ts +6 -0
  189. package/dist/server/table/index.js +7 -0
  190. package/dist/server/table/launch.d.ts +19 -0
  191. package/dist/server/table/launch.js +78 -0
  192. package/dist/server/table/link.d.ts +10 -0
  193. package/dist/server/table/link.js +230 -0
  194. package/dist/server/table/table.d.ts +35 -0
  195. package/dist/server/table/table.js +94 -0
  196. package/dist/sites/chesscom.d.ts +4 -0
  197. package/dist/sites/chesscom.js +25 -0
  198. package/dist/sites/choice.d.ts +17 -0
  199. package/dist/sites/choice.js +13 -0
  200. package/dist/sites/index.d.ts +2 -0
  201. package/dist/sites/index.js +4 -0
  202. package/dist/sites/lichess.d.ts +4 -0
  203. package/dist/sites/lichess.js +32 -0
  204. package/dist/sites/request.d.ts +11 -0
  205. package/dist/sites/request.js +15 -0
  206. package/dist/sites/sites.d.ts +6 -0
  207. package/dist/sites/sites.js +33 -0
  208. package/instructions/analysis.md +1 -1
  209. package/instructions/analyst-agent.md +1 -1
  210. package/instructions/coach.md +2 -1
  211. package/instructions/opponent.md +6 -6
  212. package/instructions/ponder.md +1 -0
  213. package/instructions/start.md +1 -0
  214. package/package.json +1 -1
@@ -0,0 +1,254 @@
1
+ // What the moves of a position would do (D-100): the captures and what they take, the forks and double attacks, the discovered and double checks, the
2
+ // promotions, the stalemates and the stalemate traps; for the side to move, and as threats for the other side. Facts on each move, never a judgment or a
3
+ // search beyond the move and, for a stalemate, the reply to it (D-6, D-19). The definitions are Anatolij's.
4
+ import { Chess } from "chess.js";
5
+ import { attacksOf, factsOf } from "./facts.js";
6
+ import { mayCastle, namesOn, Placement, turnPassed } from "./placement.js";
7
+ /** The possibilities of a chess.js board, whose legal moves are given: computed once for each position, by its key, and frozen. */
8
+ export function possibilitiesOf(chess, legal) {
9
+ return kept(possibilitiesByPosition, chess, () => computed(chess, legal));
10
+ }
11
+ /**
12
+ * What the other side's moves would do if it were to move, as threats: none when the side to move is in check, since the other side could not be to move.
13
+ * Computed apart from the possibilities, only where they are shown: once for each position, by its key, and frozen.
14
+ */
15
+ export function threatsOf(chess) {
16
+ return chess.inCheck() ? undefined : kept(threatsByPosition, chess, () => threatsComputed(chess));
17
+ }
18
+ // The possibilities and the threats of the positions seen last, by their key.
19
+ const possibilitiesByPosition = new Map();
20
+ const threatsByPosition = new Map();
21
+ const positionsKept = 256;
22
+ function kept(byPosition, chess, compute) {
23
+ const key = chess.hash();
24
+ let found = byPosition.get(key);
25
+ if (found === undefined) {
26
+ found = Object.freeze(compute());
27
+ if (byPosition.size >= positionsKept) {
28
+ byPosition.delete(byPosition.keys().next().value);
29
+ }
30
+ byPosition.set(key, found);
31
+ }
32
+ return found;
33
+ }
34
+ // The usual values, only to compare two pieces, never summed (D-19), as the facts have them.
35
+ const values = { p: 1, n: 3, b: 3, r: 5, q: 9, k: 100 };
36
+ // A stalemate is looked for only when the side it would stalemate has at most this many pieces besides its king and pawns (Anatolij).
37
+ const fewPieces = 1;
38
+ function computed(chess, legal) {
39
+ const side = chess.turn();
40
+ const stalemates = stalematesOf(chess, legal);
41
+ const strikes = strikesOf(chess, side, legal.filter((move) => !move.san.startsWith("O-O")));
42
+ return {
43
+ captures: capturesOf(chess, legal),
44
+ ...chancesOf(strikes, (move) => ({ san: move.san, order: 0 }), promotionsOf(legal, stalemates)),
45
+ stalemates,
46
+ stalemateTraps: stalemateTrapsOf(chess, legal),
47
+ };
48
+ }
49
+ /** Each capture with the fact of the piece taken: undefended, worth more than the taker, worth the same, or attacked more times than defended. */
50
+ function capturesOf(chess, legal) {
51
+ const pieces = new Map(factsOf(chess, legal).pieces.map((piece) => [piece.square, piece]));
52
+ return legal.filter((move) => move.san.includes("x")).map((move) => {
53
+ const taken = chess.get(move.to) === undefined ? `${move.to[0]}${move.from[1]}` : move.to;
54
+ const victim = pieces.get(taken);
55
+ const valueTaken = values[chess.get(taken).type];
56
+ const valueTaker = values[chess.get(move.from).type];
57
+ if (victim === undefined || victim.defenders.length === 0) {
58
+ return { san: move.san, fact: "undefended" };
59
+ }
60
+ if (valueTaken !== valueTaker) {
61
+ return valueTaken > valueTaker ? { san: move.san, fact: "worth more" } : victim.attackers.length > victim.defenders.length ? { san: move.san, fact: "outnumbered" } : { san: move.san };
62
+ }
63
+ return { san: move.san, fact: "same value" };
64
+ });
65
+ }
66
+ /** The other side's moves tried on the same board without SAN, given only to the moves that do something. */
67
+ function threatsComputed(chess) {
68
+ const other = chess.turn() === "w" ? "b" : "w";
69
+ const board = new Placement(chess);
70
+ const moves = board.movesOf(other);
71
+ // Written on the board of pieces alone, in chess.js's order; chess.js writes the rare move it cannot.
72
+ const fallback = namesWithTurnPassed(chess);
73
+ const nameOf = (move) => {
74
+ const san = board.sanOf(move, moves) ?? fallback(move)?.san;
75
+ return san === undefined ? undefined : { san, order: moves.indexOf(move) };
76
+ };
77
+ const promoting = moves.filter((move) => move.promotion !== undefined).flatMap((move) => {
78
+ const name = nameOf(move);
79
+ return name === undefined ? [] : [{ ...move, ...name }];
80
+ });
81
+ return chancesOf(strikesOf(chess, other, moves), nameOf, promotionsOf(promoting));
82
+ }
83
+ /** A side's double attacks, discovered and double checks, by the SAN of their moves; a move without one is left out. */
84
+ function chancesOf(strikes, nameOf, promotions) {
85
+ const named = strikes.flatMap((strike) => {
86
+ const name = nameOf(strike.move);
87
+ return name === undefined ? [] : [{ ...strike, ...name }];
88
+ }).sort((one, two) => one.order - two.order);
89
+ return {
90
+ doubleAttacks: named.flatMap(({ san, attack }) => (attack === undefined ? [] : [{ san, ...attack }])),
91
+ discoveredChecks: named.filter((strike) => !strike.double && strike.discovered).map((strike) => strike.san),
92
+ doubleChecks: named.filter((strike) => strike.double).map((strike) => strike.san),
93
+ promotions,
94
+ };
95
+ }
96
+ /** The moves of a side that check twice, check by uncovering a piece, or attack two targets, each tried on a board of pieces alone. */
97
+ function strikesOf(chess, side, moves) {
98
+ const board = new Placement(chess);
99
+ // On a board of its own, which no move changes: the reach reads the board at each call.
100
+ const before = remembered(attacksOf(new Placement(chess)));
101
+ // A piece pinned to its king may attack more once the pin is gone: every move is looked at whole.
102
+ const pinned = board.isPinned(side);
103
+ return moves.flatMap((move) => {
104
+ const passant = board.get(move.to) === undefined && move.from[0] !== move.to[0] && board.get(move.from).type === "p";
105
+ const taken = board.play(move);
106
+ const strike = pinned || passant || mayStrike(board, side, move) ? afterMove(board, side, move, taken, before) : undefined;
107
+ board.undo();
108
+ return strike !== undefined && (strike.double || strike.discovered || strike.attack !== undefined) ? [strike] : [];
109
+ });
110
+ }
111
+ /**
112
+ * A first look at a move just played, cheaper than the whole: the piece moved attacks the enemy king or two enemy pieces besides pawns, or it left a line
113
+ * between a rook, bishop or queen of its side and an enemy piece besides a pawn. A move that does neither checks, forks and uncovers nothing.
114
+ */
115
+ function mayStrike(board, side, move) {
116
+ const hit = board.attackedFrom(move.to).map((square) => board.get(square)).filter((piece) => piece !== undefined && piece.color !== side && piece.type !== "p");
117
+ return hit.length >= 2 || hit.some((piece) => piece.type === "k") || board.isBetween(move.from, side);
118
+ }
119
+ /** What a move just played on the board did: a double check, a discovered check, a fork or a double attack. */
120
+ function afterMove(board, side, move, taken, before) {
121
+ const reach = remembered(attacksOf(board));
122
+ const other = side === "w" ? "b" : "w";
123
+ const enemies = board.piecesOf(other);
124
+ const king = enemies.find((piece) => piece.type === "k");
125
+ const checkers = king === undefined ? [] : reach(king.square, side);
126
+ const double = checkers.length >= 2;
127
+ const discovered = checkers.some((square) => square !== move.to);
128
+ const moved = board.get(move.to);
129
+ // A piece of its side attacks an enemy piece it did not attack before the move: the piece moved, or one its move uncovered.
130
+ const targetsOf = (attacker) => enemies.filter((enemy) => enemy.type !== "p" && reach(enemy.square, side).includes(attacker) && isTarget(enemy, attacker));
131
+ const isTarget = (enemy, attacker) => enemy.type === "k" || values[enemy.type] > values[board.get(attacker).type]
132
+ || (reach(enemy.square, other).length === 0 && !reach(attacker, other).includes(enemy.square));
133
+ const own = targetsOf(move.to);
134
+ const fresh = own.filter((enemy) => !before(enemy.square, side).includes(move.from));
135
+ const uncovered = board.piecesOf(side).filter((piece) => piece.square !== move.to && "rbq".includes(piece.type)).flatMap((line) => targetsOf(line.square).filter((enemy) => enemy.type !== "k" && !before(enemy.square, side).includes(line.square)));
136
+ const attackers = reach(move.to, other);
137
+ const atRisk = attackers.length > 0 && (reach(move.to, side).length === 0 || Math.min(...attackers.map((square) => values[board.get(square).type])) < values[moved.type]);
138
+ const fork = own.length >= 2 && fresh.length >= 1 && (!atRisk || double);
139
+ const targets = [...new Set([...own, ...uncovered].map((enemy) => enemy.square))].sort();
140
+ // A piece uncovered counts when the piece moved does something too: it attacks another target, takes, checks or promotes.
141
+ const strikes = taken !== undefined || move.promotion !== undefined || checkers.length > 0;
142
+ const withUncovered = uncovered.length > 0 && (targets.length >= 2 || strikes);
143
+ return !fork && !withUncovered ? { move, double, discovered } : { move, double, discovered, attack: { targets, fork: uncovered.length === 0 } };
144
+ }
145
+ /** A reach that reads each square once for each side. */
146
+ function remembered(reach) {
147
+ const known = new Map();
148
+ return (square, color) => {
149
+ const key = `${square}${color}`;
150
+ let found = known.get(key);
151
+ if (found === undefined) {
152
+ found = reach(square, color);
153
+ known.set(key, found);
154
+ }
155
+ return found;
156
+ };
157
+ }
158
+ /** The promotions, a queen's for each pawn and square, and another piece's only when it checks or mates where the queen does not, or the queen stalemates and it does not. */
159
+ function promotionsOf(legal, stalemates = []) {
160
+ const queens = legal.filter((move) => move.promotion === "q");
161
+ const suffix = (san) => /[+#]$/.exec(san)?.[0] ?? "";
162
+ const others = legal.filter((move) => {
163
+ const queen = queens.find((each) => each.from === move.from && each.to === move.to)?.san ?? "";
164
+ const checksAlone = suffix(move.san) !== "" && suffix(move.san) !== suffix(queen);
165
+ return move.promotion !== undefined && move.promotion !== "q" && (checksAlone || (stalemates.includes(queen) && !stalemates.includes(move.san)));
166
+ });
167
+ return [...queens, ...others].map((move) => move.san);
168
+ }
169
+ /** The moves after which the other side, with few pieces, is not in check and has no legal move. */
170
+ function stalematesOf(chess, legal) {
171
+ const other = chess.turn() === "w" ? "b" : "w";
172
+ if (officersOf(chess, other) > fewPieces + 1) {
173
+ return [];
174
+ }
175
+ const board = new Chess(chess.fen());
176
+ const placement = new Placement(chess);
177
+ return legal.filter((move) => {
178
+ // Castling moves a rook the board of pieces alone does not.
179
+ if (move.san.startsWith("O-O")) {
180
+ return stalemateAfter(board, [move]);
181
+ }
182
+ // A side with a move on the board of pieces alone is not stalemated; one without may still castle or take en passant, which chess.js tells.
183
+ placement.play(move);
184
+ const maybe = placement.piecesOf(other).filter((piece) => piece.type !== "k" && piece.type !== "p").length <= fewPieces && !placement.inCheck(other) && !placement.hasMove(other);
185
+ placement.undo();
186
+ return maybe && stalemateAfter(board, [move]);
187
+ }).map((move) => move.san);
188
+ }
189
+ /** The moves of a side with few pieces after which a reply of the other side stalemates it, with those replies. */
190
+ function stalemateTrapsOf(chess, legal) {
191
+ const side = chess.turn();
192
+ if (officersOf(chess, side) > fewPieces) {
193
+ return [];
194
+ }
195
+ const board = new Chess(chess.fen());
196
+ const placement = new Placement(chess);
197
+ // The board of pieces alone lists every reply but castling and en passant: where the other side may castle, or take a pawn just pushed two squares, chess.js does.
198
+ const otherCastles = mayCastle(chess, side === "w" ? "b" : "w");
199
+ return legal.flatMap((move) => {
200
+ const pushedTwo = board.get(move.from)?.type === "p" && Math.abs(Number(move.to[1]) - Number(move.from[1])) === 2;
201
+ const trap = otherCastles || pushedTwo || move.san.startsWith("O-O") ? trapByChess(board, move) : trapOnPlacement(placement, board, side, move);
202
+ return trap === undefined ? [] : [trap];
203
+ });
204
+ }
205
+ /** The replies to a move that stalemate the side that moved, tried on chess.js. */
206
+ function trapByChess(board, move) {
207
+ board.move(chessMove(move));
208
+ const replies = board.moves({ verbose: true });
209
+ const stalemating = replies.filter((reply) => stalemateAfter(board, [reply]));
210
+ board.undo();
211
+ return stalemating.length === 0 ? undefined : { san: move.san, replies: stalemating.length === replies.length ? "all" : stalemating.map((reply) => reply.san) };
212
+ }
213
+ /** The same, the replies tried on the board of pieces alone; chess.js names those that stalemate there, and tells whether castling or en passant saves the side. */
214
+ function trapOnPlacement(placement, board, side, move) {
215
+ placement.play(move);
216
+ const replies = placement.movesOf(side === "w" ? "b" : "w");
217
+ const stalemating = replies.filter((reply) => {
218
+ placement.play(reply);
219
+ const maybe = !placement.inCheck(side) && !placement.hasMove(side);
220
+ placement.undo();
221
+ return maybe;
222
+ });
223
+ placement.undo();
224
+ if (stalemating.length === 0) {
225
+ return undefined;
226
+ }
227
+ board.move(chessMove(move));
228
+ const nameOf = namesOn(() => board);
229
+ const named = stalemating.flatMap((reply) => {
230
+ const name = nameOf(reply);
231
+ return name !== undefined && stalemateAfter(board, [reply]) ? [name] : [];
232
+ }).sort((one, two) => one.order - two.order);
233
+ board.undo();
234
+ return named.length === 0 ? undefined : { san: move.san, replies: named.length === replies.length ? "all" : named.map((name) => name.san) };
235
+ }
236
+ /** Whether moves played on a chess.js board stalemate the side to move then; the board is left as it was. */
237
+ function stalemateAfter(board, moves) {
238
+ moves.forEach((move) => board.move(chessMove(move)));
239
+ const stalemate = board.isStalemate();
240
+ moves.forEach(() => board.undo());
241
+ return stalemate;
242
+ }
243
+ function chessMove({ from, to, promotion }) {
244
+ return promotion === undefined ? { from, to } : { from, to, promotion };
245
+ }
246
+ /** A side's pieces besides its king and pawns. */
247
+ function officersOf(chess, color) {
248
+ return chess.board().flat().filter((cell) => cell !== null && cell.color === color && cell.type !== "k" && cell.type !== "p").length;
249
+ }
250
+ /** The SAN of the other side's moves and their order, read on the board with the turn passed, which is set up only when a move needs it. */
251
+ function namesWithTurnPassed(chess) {
252
+ let passed;
253
+ return namesOn(() => (passed ??= turnPassed(chess)));
254
+ }
@@ -51,6 +51,8 @@ export declare function reviewed(move: RecordedMove, review: MoveReview): Record
51
51
  * the game and who signed it. What the comments held besides stays, as do the NAGs of moves without a badge and the Annotator of a game it never reviewed.
52
52
  */
53
53
  export declare function unreviewed(record: GameRecord): GameRecord;
54
+ /** The tags without who signed a review, the Annotator and its effort: the same game whoever reviewed it. */
55
+ export declare function unsigned(tags: Readonly<Record<string, string>>): Record<string, string>;
54
56
  /** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
55
57
  export declare function drawingsText({ arrows, squares }: Pick<MoveReview, "arrows" | "squares">): string;
56
58
  /** The record with who reviewed it (D-81): the model, else the host, as PGN's Annotator, and the effort only when known; an effort no longer known goes. */
@@ -65,7 +67,10 @@ export interface AiModel {
65
67
  }
66
68
  /** The record with the model the AI played it with (D-82), and the effort only when known; with none known, as it is. */
67
69
  export declare function withAiModel(record: GameRecord, played: AiModel | undefined): GameRecord;
68
- /** Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator. */
70
+ /**
71
+ * Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator, or on a game the AI has not reviewed,
72
+ * whose Annotator is someone else's, such as Lichess's.
73
+ */
69
74
  export declare function annotatorOf(record: GameRecord): {
70
75
  annotator: string;
71
76
  effort?: string;
@@ -63,11 +63,14 @@ export function unreviewed(record) {
63
63
  return recordedMove(move.san, nags, withoutReview(move.comment).replace(/\s*\[%lead [^\]]*\]/g, "").trim());
64
64
  });
65
65
  const comment = withoutReview(record.comment);
66
- // The signature is the AI's only on a game it reviewed: a game from elsewhere keeps its own Annotator.
67
- const signedByAi = isReviewed(record) || record.moves.some((move) => reviewMark.test(move.comment ?? ""));
68
- const tags = signedByAi ? Object.fromEntries(Object.entries(record.tags).filter(([name]) => name !== "Annotator" && name !== effortTag)) : record.tags;
66
+ // A game from elsewhere keeps its own Annotator.
67
+ const tags = carriesReview(record) ? unsigned(record.tags) : record.tags;
69
68
  return comment === "" ? { tags, moves } : { tags, comment, moves };
70
69
  }
70
+ /** The tags without who signed a review, the Annotator and its effort: the same game whoever reviewed it. */
71
+ export function unsigned(tags) {
72
+ return Object.fromEntries(Object.entries(tags).filter(([name]) => name !== "Annotator" && name !== effortTag));
73
+ }
71
74
  /** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
72
75
  export function drawingsText({ arrows = [], squares = [] }) {
73
76
  const arrowList = arrows.map(({ colour, from, to }) => `${letterOf(colour)}${from}${to}`).join(",");
@@ -82,9 +85,12 @@ export function withAnnotator(record, { annotator, effort }) {
82
85
  export function withAiModel(record, played) {
83
86
  return played === undefined ? record : { ...record, tags: signed(record.tags, ["AIModel", "AIEffort"], played.model, played.effort) };
84
87
  }
85
- /** Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator. */
88
+ /**
89
+ * Who reviewed the game, as its tags say: the Annotator, and the effort when known; undefined without an Annotator, or on a game the AI has not reviewed,
90
+ * whose Annotator is someone else's, such as Lichess's.
91
+ */
86
92
  export function annotatorOf(record) {
87
- const found = signerOf(record.tags, ["Annotator", effortTag]);
93
+ const found = carriesReview(record) ? signerOf(record.tags, ["Annotator", effortTag]) : undefined;
88
94
  return found === undefined ? undefined : { annotator: found.model, ...(found.effort === undefined ? {} : { effort: found.effort }) };
89
95
  }
90
96
  /** The model the AI played the game with, as its tags say (D-82), and the effort when known; undefined without one. */
@@ -123,6 +129,16 @@ export function withGameReview(record, { summary, closing }) {
123
129
  }
124
130
  // The effort has no tag of its own in PGN: Chesspal's sits beside the Annotator.
125
131
  const effortTag = "AnnotatorEffort";
132
+ /**
133
+ * The game holds the AI's review: its word on the game, or a note or badge on a move, the book's badge apart, which Chesspal puts there when the analysis
134
+ * starts. Only then is the Annotator the AI's.
135
+ */
136
+ function carriesReview(record) {
137
+ return isReviewed(record) || record.moves.some((move) => {
138
+ const review = reviewOf(move.comment);
139
+ return review?.note !== undefined || (review?.badge !== undefined && review.badge !== "book");
140
+ });
141
+ }
126
142
  /** A model and its effort as the tags say, each only when not blank; undefined without the model. */
127
143
  function signerOf(tags, [modelTag, modelEffortTag]) {
128
144
  const model = tags[modelTag]?.trim();
@@ -146,7 +162,6 @@ const judgmentNags = new Set([1, 2, 3, 4, 5, 6]);
146
162
  // The badges PGN has a NAG for: !! ! ?! ? ??.
147
163
  const nagOfBadge = { brilliant: 3, great: 1, inaccuracy: 6, mistake: 2, blunder: 4 };
148
164
  const badgePattern = /\[%badge (\w+)\]/;
149
- const reviewMark = /\[%(badge|review) /;
150
165
  const notePattern = /\[%review ([^\]]*)\]/;
151
166
  const closingPattern = /\[%closing ([^\]]*)\]/;
152
167
  const arrowPattern = /\[%cal ([^\]]*)\]/;
@@ -3,7 +3,9 @@ import type { AiModel } from "../core/index.ts";
3
3
  export declare function noteAiModel(folder: string, noted: AiModel): boolean;
4
4
  /** The AI's model last noted for the games of a folder in this process; none on the command line, or before the AI's first move. */
5
5
  export declare function aiModelOf(folder: string): AiModel | undefined;
6
- /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. */
7
- export declare function noteAiHost(folder: string, host: string): void;
6
+ /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. Whether it changed. */
7
+ export declare function noteAiHost(folder: string, host: string): boolean;
8
+ /** Forgets who played as the AI for the games of a folder: the chat that did has left the table (D-104), and the next may be another. */
9
+ export declare function forgetTheAi(folder: string): void;
8
10
  /** The host the AI played its last move in, for the games of a folder in this process; none on the command line, or before the AI's first move. */
9
11
  export declare function aiHostOf(folder: string): string | undefined;
@@ -11,9 +11,18 @@ export function noteAiModel(folder, noted) {
11
11
  export function aiModelOf(folder) {
12
12
  return models.get(folder);
13
13
  }
14
- /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. */
14
+ /** Notes the host the AI plays in for the games of a folder, after its move (D-97), as the model. Whether it changed. */
15
15
  export function noteAiHost(folder, host) {
16
+ if (hosts.get(folder) === host) {
17
+ return false;
18
+ }
16
19
  hosts.set(folder, host);
20
+ return true;
21
+ }
22
+ /** Forgets who played as the AI for the games of a folder: the chat that did has left the table (D-104), and the next may be another. */
23
+ export function forgetTheAi(folder) {
24
+ models.delete(folder);
25
+ hosts.delete(folder);
17
26
  }
18
27
  /** The host the AI played its last move in, for the games of a folder in this process; none on the command line, or before the AI's first move. */
19
28
  export function aiHostOf(folder) {
@@ -1,12 +1,39 @@
1
- /** The game in analysis: its name as the lists write it, when the analysis last changed it, and whether the player brought it on the page and the AI has not gone over it yet. */
1
+ /**
2
+ * The game in analysis: its name as the lists write it, when the analysis last changed it, whether the player brought it on the page and the AI has not
3
+ * gone over it yet, the moves whose note the chat corrected with the player, which the analyst leaves (D-101), and the run of the analyst that took it.
4
+ */
2
5
  export interface Analysis {
3
6
  readonly game: string;
4
7
  readonly at: string;
5
8
  readonly brought?: true;
9
+ readonly corrected?: readonly string[];
10
+ readonly analyst?: AnalystRun;
6
11
  }
7
- /** Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. */
8
- export declare function startAnalysis(folder: string, game: string, now: Date, brought?: boolean): void;
12
+ /** The run of the analyst agent that took a game, as the host names it, the game it took and when. */
13
+ export interface AnalystRun {
14
+ readonly game: string;
15
+ readonly run: string;
16
+ readonly since: string;
17
+ }
18
+ /**
19
+ * How a game is named in analysis: brought to the page by the player, with the moves the chat just corrected, taken by a run of the analyst, or afresh,
20
+ * without the corrections and the analyst it had.
21
+ */
22
+ export interface AnalysisStart {
23
+ readonly brought?: boolean;
24
+ readonly corrected?: readonly string[];
25
+ readonly analyst?: AnalystRun;
26
+ readonly afresh?: boolean;
27
+ }
28
+ /**
29
+ * Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. The same game keeps its corrections
30
+ * unless it starts afresh, in one write, so that the page never sees it leave the analysis; the analyst stays with the game it took, whichever the page
31
+ * opens meanwhile (Federico), and goes when that game starts afresh.
32
+ */
33
+ export declare function startAnalysis(folder: string, game: string, now: Date, { brought, corrected, analyst, afresh }?: AnalysisStart): void;
34
+ /** Lets the analyst go from the game it took, when that game's review is deleted while the page shows another. */
35
+ export declare function forgetAnalyst(folder: string): void;
9
36
  /** The game in analysis, or none when there is none, or analysis.json cannot be read. */
10
37
  export declare function gameInAnalysis(folder: string): Analysis | undefined;
11
- /** Ends the analysis, when a new game starts. */
38
+ /** Ends the analysis, when a new game starts or its review is deleted. */
12
39
  export declare function endAnalysis(folder: string): void;
@@ -3,24 +3,63 @@
3
3
  import { readFileSync, rmSync } from "node:fs";
4
4
  import { join } from "node:path";
5
5
  import { writeAtomically } from "./files.js";
6
- /** Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. */
7
- export function startAnalysis(folder, game, now, brought = false) {
8
- writeAtomically(join(folder, fileName), `${JSON.stringify({ game, at: now.toISOString(), ...(brought ? { brought } : {}) }, null, 2)}\n`);
6
+ /**
7
+ * Names the game in analysis, over the one before; written again each time its review changes, so the page hears of it. The same game keeps its corrections
8
+ * unless it starts afresh, in one write, so that the page never sees it leave the analysis; the analyst stays with the game it took, whichever the page
9
+ * opens meanwhile (Federico), and goes when that game starts afresh.
10
+ */
11
+ export function startAnalysis(folder, game, now, { brought = false, corrected = [], analyst, afresh = false } = {}) {
12
+ const before = gameInAnalysis(folder);
13
+ const same = !afresh && before?.game === game;
14
+ const kept = [...new Set([...(same ? before.corrected ?? [] : []), ...corrected])];
15
+ const held = afresh && before?.analyst?.game === game ? undefined : before?.analyst;
16
+ writeAnalysis(folder, {
17
+ game,
18
+ at: now.toISOString(),
19
+ ...(brought ? { brought: true } : {}),
20
+ ...(kept.length === 0 ? {} : { corrected: kept }),
21
+ ...withAnalyst(analyst ?? held),
22
+ });
23
+ }
24
+ /** Lets the analyst go from the game it took, when that game's review is deleted while the page shows another. */
25
+ export function forgetAnalyst(folder) {
26
+ const analysis = gameInAnalysis(folder);
27
+ if (analysis?.analyst !== undefined) {
28
+ const { game, at, brought, corrected } = analysis;
29
+ writeAnalysis(folder, { game, at, ...(brought === undefined ? {} : { brought }), ...(corrected === undefined ? {} : { corrected }) });
30
+ }
9
31
  }
10
32
  /** The game in analysis, or none when there is none, or analysis.json cannot be read. */
11
33
  export function gameInAnalysis(folder) {
12
34
  try {
13
35
  const analysis = JSON.parse(readFileSync(join(folder, fileName), "utf8"));
36
+ const corrected = Array.isArray(analysis.corrected) ? analysis.corrected.filter((move) => typeof move === "string") : [];
14
37
  return typeof analysis.game === "string" && typeof analysis.at === "string"
15
- ? { game: analysis.game, at: analysis.at, ...(analysis.brought === true ? { brought: true } : {}) }
38
+ ? {
39
+ game: analysis.game,
40
+ at: analysis.at,
41
+ ...(analysis.brought === true ? { brought: true } : {}),
42
+ ...(corrected.length === 0 ? {} : { corrected }),
43
+ ...withAnalyst(isAnalystRun(analysis.analyst) ? analysis.analyst : undefined),
44
+ }
16
45
  : undefined;
17
46
  }
18
47
  catch {
19
48
  return undefined;
20
49
  }
21
50
  }
22
- /** Ends the analysis, when a new game starts. */
51
+ /** Ends the analysis, when a new game starts or its review is deleted. */
23
52
  export function endAnalysis(folder) {
24
53
  rmSync(join(folder, fileName), { force: true });
25
54
  }
26
55
  const fileName = "analysis.json";
56
+ function writeAnalysis(folder, analysis) {
57
+ writeAtomically(join(folder, fileName), `${JSON.stringify(analysis, null, 2)}\n`);
58
+ }
59
+ function withAnalyst(analyst) {
60
+ return analyst === undefined ? {} : { analyst: { game: analyst.game, run: analyst.run, since: analyst.since } };
61
+ }
62
+ function isAnalystRun(value) {
63
+ const run = value;
64
+ return typeof run?.game === "string" && typeof run.run === "string" && typeof run.since === "string";
65
+ }
@@ -11,10 +11,13 @@ export declare function isOverOnTable(folder: string, name: string): boolean;
11
11
  export declare function startOfNewGame(folder: string, now: Date, before?: CurrentGame): Date;
12
12
  /**
13
13
  * Writes the game to the archive, with the plans, how it ended and the AI's model and host last noted (D-82, D-97), and gives its name; the same game again rewrites the
14
- * same file.
14
+ * same file. A game closed before any move is not kept, as Lichess aborts it: none.
15
+ */
16
+ export declare function archiveGame(folder: string, current: CurrentGame): string | undefined;
17
+ /**
18
+ * Archives a game that has ended: its name in the archive, none for a game closed before any move, or, when that fails, a warning, since the game is saved in
19
+ * game.json all the same.
15
20
  */
16
- export declare function archiveGame(folder: string, current: CurrentGame): string;
17
- /** Archives a game that has ended: its name in the archive or, when that fails, a warning, since the game is saved in game.json all the same. */
18
21
  export declare function archiveEnded(folder: string, current: CurrentGame): {
19
22
  archive?: string;
20
23
  warning?: string;
@@ -1,7 +1,7 @@
1
1
  // The finished games, as PGN in games/ (D-9, D-41), with the AI's plans for the analyst (D-21); and the games played elsewhere, kept to study in study/ (D-68).
2
2
  import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { movesOnly, pgnDate, readPgn, withAiHost, withAiModel, writePgn } from "../core/index.js";
4
+ import { movesOnly, pgnDate, readPgn, unsigned, withAiHost, withAiModel, writePgn } from "../core/index.js";
5
5
  import { aiHostOf, aiModelOf } from "./ai-model.js";
6
6
  import { coachWords, withCoach } from "./coach.js";
7
7
  import { endOf, loadGame } from "./current-game.js";
@@ -35,9 +35,12 @@ export function startOfNewGame(folder, now, before) {
35
35
  }
36
36
  /**
37
37
  * Writes the game to the archive, with the plans, how it ended and the AI's model and host last noted (D-82, D-97), and gives its name; the same game again rewrites the
38
- * same file.
38
+ * same file. A game closed before any move is not kept, as Lichess aborts it: none.
39
39
  */
40
40
  export function archiveGame(folder, current) {
41
+ if (current.game.record.moves.length === 0) {
42
+ return undefined;
43
+ }
41
44
  const name = archiveName(current);
42
45
  const path = join(folder, name);
43
46
  try {
@@ -49,10 +52,14 @@ export function archiveGame(folder, current) {
49
52
  }
50
53
  return name;
51
54
  }
52
- /** Archives a game that has ended: its name in the archive or, when that fails, a warning, since the game is saved in game.json all the same. */
55
+ /**
56
+ * Archives a game that has ended: its name in the archive, none for a game closed before any move, or, when that fails, a warning, since the game is saved in
57
+ * game.json all the same.
58
+ */
53
59
  export function archiveEnded(folder, current) {
54
60
  try {
55
- return { archive: archiveGame(folder, current) };
61
+ const archive = archiveGame(folder, current);
62
+ return archive === undefined ? {} : { archive };
56
63
  }
57
64
  catch (error) {
58
65
  return { warning: `${error.message} The game is saved in game.json: it goes to the archive when a new game starts.` };
@@ -225,7 +232,8 @@ function gameNamed(folder, shelf, name) {
225
232
  }
226
233
  /** Whether two records are the same game: the same tags and the same moves, whatever their comments say. */
227
234
  function sameGame(one, other) {
228
- const shape = (record) => JSON.stringify([record?.tags, record?.moves.map((move) => move.san)]);
235
+ // Who signed a review is not the game: one the AI reviewed, or brought again with Lichess's Annotator, is the one kept.
236
+ const shape = (record) => JSON.stringify([record === undefined ? undefined : unsigned(record.tags), record?.moves.map((move) => move.san)]);
229
237
  return one !== undefined && shape(one) === shape(other);
230
238
  }
231
239
  function nameAt(date, shelf = "games") {
@@ -16,13 +16,23 @@ export interface CurrentGame {
16
16
  }
17
17
  /** Who speaks on the board page. */
18
18
  export type Speaker = "player" | "ai";
19
- /** A line said on the board page: who said it, its words, how many moves the game had then, and when, as an ISO date; the AI's agent on the player's time (D-72). */
19
+ /**
20
+ * A line said on the board page: who said it, its words, how many moves the game had then, and when, as an ISO date; the AI's agent on the player's time
21
+ * (D-72); in analysis, the position the page showed as the player wrote (D-101).
22
+ */
20
23
  export interface TalkLine {
21
24
  readonly who: Speaker;
22
25
  readonly text: string;
23
26
  readonly ply: number;
24
27
  readonly at: string;
25
28
  readonly agent?: true;
29
+ readonly shown?: Shown;
30
+ }
31
+ /** A position of a game in analysis: the game's name, how many of its moves the page showed, and the last of them numbered, 52...Kf6; none at the start. */
32
+ export interface Shown {
33
+ readonly game: string;
34
+ readonly at: number;
35
+ readonly move?: string;
26
36
  }
27
37
  /** How a game ended: its result, and why when the record says it or the board decided it. */
28
38
  export interface GameEnd {
@@ -31,8 +41,17 @@ export interface GameEnd {
31
41
  }
32
42
  /** The current game of a folder, as it is on disk, or undefined when there is none. A game.json Chesspal cannot read throws: nothing overwrites it. */
33
43
  export declare function loadGame(folder: string): CurrentGame | undefined;
34
- /** Writes the current game to game.json, whole or not at all; from then on it is the one kept in memory. */
44
+ /**
45
+ * Writes the current game to game.json, whole or not at all; from then on it is the one kept in memory. Refused with GameChanged when another process wrote
46
+ * the file since this one read it: the change made on the game read before would lose theirs.
47
+ */
35
48
  export declare function saveGame(folder: string, current: CurrentGame): void;
49
+ /** The refusal of a save on a game another process changed since this one read it: what wrote it reads it again and starts over, once (rerunIfChanged). */
50
+ export declare class GameChanged extends Error {
51
+ constructor();
52
+ }
53
+ /** Runs a change of the game, and once more from the start when another process changed the game under it: a second refusal stands. */
54
+ export declare function rerunIfChanged<T>(run: () => T): T;
36
55
  /** How the game ended, or undefined while it goes on: by the board, or by what the record says, such as a resignation. */
37
56
  export declare function endOf(game: Game): GameEnd | undefined;
38
57
  /** The draw offer that stands: none once the game is over (D-65). */
@@ -53,4 +72,9 @@ export declare const savedLine: z.ZodObject<{
53
72
  ply: z.ZodInt;
54
73
  at: z.ZodISODateTime;
55
74
  agent: z.ZodOptional<z.ZodLiteral<true>>;
75
+ shown: z.ZodOptional<z.ZodObject<{
76
+ game: z.ZodString;
77
+ at: z.ZodInt;
78
+ move: z.ZodOptional<z.ZodString>;
79
+ }, z.core.$strip>>;
56
80
  }, z.core.$strip>;