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.
- package/dist/commands/agents.js +2 -1
- package/dist/commands/ai-say.d.ts +2 -1
- package/dist/commands/analyse-game.d.ts +2 -0
- package/dist/commands/analyse-game.js +7 -4
- package/dist/commands/annotate-game.d.ts +2 -0
- package/dist/commands/annotate-game.js +26 -8
- package/dist/commands/answer-lines.d.ts +8 -4
- package/dist/commands/answer-lines.js +19 -3
- package/dist/commands/coach.js +2 -2
- package/dist/commands/context.d.ts +2 -0
- package/dist/commands/delete-review.js +3 -2
- package/dist/commands/game-summary.d.ts +7 -2
- package/dist/commands/game-summary.js +7 -0
- package/dist/commands/index.d.ts +1 -1
- package/dist/commands/index.js +1 -1
- package/dist/commands/look.d.ts +9 -5
- package/dist/commands/look.js +19 -9
- package/dist/commands/ponder.js +1 -1
- package/dist/commands/position.js +2 -2
- package/dist/commands/say.d.ts +11 -6
- package/dist/commands/say.js +21 -13
- package/dist/commands/status.d.ts +3 -1
- package/dist/commands/status.js +31 -6
- package/dist/commands/study.d.ts +11 -1
- package/dist/commands/study.js +31 -2
- package/dist/commands/view.d.ts +17 -5
- package/dist/commands/view.js +67 -10
- package/dist/core/facts.d.ts +14 -2
- package/dist/core/facts.js +32 -7
- package/dist/core/game.d.ts +5 -0
- package/dist/core/game.js +9 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/placement.d.ts +63 -0
- package/dist/core/placement.js +259 -0
- package/dist/core/possibilities.d.ts +38 -0
- package/dist/core/possibilities.js +254 -0
- package/dist/core/review.d.ts +4 -1
- package/dist/core/review.js +17 -6
- package/dist/games/analysis.d.ts +8 -4
- package/dist/games/analysis.js +9 -5
- package/dist/games/current-game.d.ts +16 -1
- package/dist/games/current-game.js +10 -8
- package/dist/games/files.d.ts +2 -0
- package/dist/games/files.js +7 -1
- package/dist/games/index.d.ts +2 -2
- package/dist/games/index.js +1 -1
- package/dist/games/table-talk.d.ts +2 -0
- package/dist/games/table-talk.js +5 -1
- package/dist/games/talk.d.ts +4 -3
- package/dist/games/talk.js +3 -3
- package/dist/page/app/app.js +36 -12
- package/dist/page/app/keys.js +16 -2
- package/dist/page/app/served.js +2 -2
- package/dist/page/board/board.js +2 -1
- package/dist/page/board/marks.js +4 -24
- package/dist/page/board/signs.js +23 -0
- package/dist/page/game/column.js +2 -2
- package/dist/page/game/game.css +12 -0
- package/dist/page/game/move-list.js +2 -2
- package/dist/page/game/plies.js +8 -0
- package/dist/page/helps/helps.css +3 -1
- package/dist/page/helps/helps.js +15 -15
- package/dist/page/layout/layout.css +29 -5
- package/dist/page/legend/legend.css +203 -0
- package/dist/page/legend/legend.js +169 -0
- package/dist/page/legend/view.js +54 -0
- package/dist/page/style.css +2 -0
- package/dist/page/talk/chat.css +90 -0
- package/dist/page/talk/talk.js +27 -5
- package/dist/page/text.js +38 -2
- package/dist/server/mcp.js +8 -6
- package/dist/server/page/game.js +20 -7
- package/dist/server/page/page.js +5 -3
- package/instructions/opponent.md +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// A board of pieces alone, to try many moves in a row fast (D-34): no turn, castling rights or history, which the possibilities do not read. Which pieces
|
|
2
|
+
// attack a square is read along the board's lines and steps, as chess.js counts it.
|
|
3
|
+
import { Chess } from "chess.js";
|
|
4
|
+
/** The pieces of a position, moved without the rules' checks and taken back in the reverse order. */
|
|
5
|
+
export class Placement {
|
|
6
|
+
#pieces = new Map();
|
|
7
|
+
#played = [];
|
|
8
|
+
constructor(chess) {
|
|
9
|
+
for (const cell of chess.board().flat()) {
|
|
10
|
+
if (cell !== null) {
|
|
11
|
+
this.#pieces.set(cell.square, { type: cell.type, color: cell.color });
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
get(square) {
|
|
16
|
+
return this.#pieces.get(square);
|
|
17
|
+
}
|
|
18
|
+
/** The squares of a side's pieces of one type. */
|
|
19
|
+
findPiece({ type, color }) {
|
|
20
|
+
const found = [];
|
|
21
|
+
for (const [square, piece] of this.#pieces) {
|
|
22
|
+
if (piece.type === type && piece.color === color) {
|
|
23
|
+
found.push(square);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return found;
|
|
27
|
+
}
|
|
28
|
+
/** A side's pieces. */
|
|
29
|
+
piecesOf(color) {
|
|
30
|
+
const found = [];
|
|
31
|
+
for (const [square, piece] of this.#pieces) {
|
|
32
|
+
if (piece.color === color) {
|
|
33
|
+
found.push({ square, type: piece.type, color });
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return found;
|
|
37
|
+
}
|
|
38
|
+
/** The pieces of a side that attack a square, pinned or not: pawns, knights and kings by their steps, the others along their lines. */
|
|
39
|
+
attackers(square, color) {
|
|
40
|
+
const of = (from, types) => {
|
|
41
|
+
const piece = this.#pieces.get(from);
|
|
42
|
+
return piece !== undefined && piece.color === color && types.includes(piece.type);
|
|
43
|
+
};
|
|
44
|
+
const found = [
|
|
45
|
+
...pawnsTaking[color].get(square).filter((from) => of(from, "p")),
|
|
46
|
+
...knightSteps.get(square).filter((from) => of(from, "n")),
|
|
47
|
+
...kingSteps.get(square).filter((from) => of(from, "k")),
|
|
48
|
+
];
|
|
49
|
+
for (const { squares, sliders } of linesFrom.get(square)) {
|
|
50
|
+
const first = squares.find((each) => this.#pieces.has(each));
|
|
51
|
+
if (first !== undefined && of(first, sliders)) {
|
|
52
|
+
found.push(first);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return found;
|
|
56
|
+
}
|
|
57
|
+
/** The squares the piece on a square attacks: by its steps, or along its lines up to the first piece. */
|
|
58
|
+
attackedFrom(square) {
|
|
59
|
+
const { type, color } = this.#pieces.get(square);
|
|
60
|
+
if (type === "p" || type === "n" || type === "k") {
|
|
61
|
+
return (type === "p" ? pawnCaptures[color] : type === "n" ? knightSteps : kingSteps).get(square);
|
|
62
|
+
}
|
|
63
|
+
return linesFrom.get(square).filter((line) => line.sliders.includes(type)).flatMap(({ squares }) => {
|
|
64
|
+
const stop = squares.findIndex((each) => this.#pieces.has(each));
|
|
65
|
+
return stop === -1 ? squares : squares.slice(0, stop + 1);
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
/** 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. */
|
|
69
|
+
isBetween(square, color) {
|
|
70
|
+
const lines = linesFrom.get(square);
|
|
71
|
+
const first = lines.map(({ squares }) => squares.find((each) => this.#pieces.has(each)));
|
|
72
|
+
const slider = (index) => {
|
|
73
|
+
const piece = first[index] === undefined ? undefined : this.#pieces.get(first[index]);
|
|
74
|
+
return piece !== undefined && piece.color === color && lines[index].sliders.includes(piece.type);
|
|
75
|
+
};
|
|
76
|
+
const target = (index) => {
|
|
77
|
+
const piece = first[index] === undefined ? undefined : this.#pieces.get(first[index]);
|
|
78
|
+
return piece !== undefined && piece.color !== color && piece.type !== "p";
|
|
79
|
+
};
|
|
80
|
+
return opposite.some(([one, two]) => (slider(one) && target(two)) || (slider(two) && target(one)));
|
|
81
|
+
}
|
|
82
|
+
/** Whether a piece of a side stands between its king and a rook, bishop or queen of the other side on their line. */
|
|
83
|
+
isPinned(color) {
|
|
84
|
+
const king = this.findPiece({ type: "k", color })[0];
|
|
85
|
+
return king !== undefined && linesFrom.get(king).some(({ squares, sliders }) => {
|
|
86
|
+
const [first, second] = squares.filter((each) => this.#pieces.has(each)).map((each) => this.#pieces.get(each));
|
|
87
|
+
return first?.color === color && second !== undefined && second.color !== color && sliders.includes(second.type);
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/** Plays a move: the piece taken goes, en passant too, and a pawn becomes the piece it promotes to. It returns the piece taken. */
|
|
91
|
+
play(move) {
|
|
92
|
+
const piece = this.#pieces.get(move.from);
|
|
93
|
+
const passant = piece.type === "p" && move.from[0] !== move.to[0] && !this.#pieces.has(move.to);
|
|
94
|
+
const takenOn = passant ? `${move.to[0]}${move.from[1]}` : move.to;
|
|
95
|
+
const taken = this.#pieces.get(takenOn);
|
|
96
|
+
this.#played.push({ move, piece, taken, takenOn });
|
|
97
|
+
this.#pieces.delete(move.from);
|
|
98
|
+
this.#pieces.delete(takenOn);
|
|
99
|
+
this.#pieces.set(move.to, move.promotion === undefined ? piece : { type: move.promotion, color: piece.color });
|
|
100
|
+
return taken;
|
|
101
|
+
}
|
|
102
|
+
/** Takes back the last move played. */
|
|
103
|
+
undo() {
|
|
104
|
+
const { move, piece, taken, takenOn } = this.#played.pop();
|
|
105
|
+
this.#pieces.delete(move.to);
|
|
106
|
+
this.#pieces.set(move.from, piece);
|
|
107
|
+
if (taken !== undefined) {
|
|
108
|
+
this.#pieces.set(takenOn, taken);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* 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;
|
|
113
|
+
* never castling or en passant.
|
|
114
|
+
*/
|
|
115
|
+
movesOf(color) {
|
|
116
|
+
return this.#placedOf(color).flatMap((piece) => this.#reachable(piece)).filter((move) => this.#leavesKingSafe(move, color));
|
|
117
|
+
}
|
|
118
|
+
/** Whether a side has a move of those, stopping at the first. */
|
|
119
|
+
hasMove(color) {
|
|
120
|
+
return this.#placedOf(color).some((piece) => this.#reachable(piece).some((move) => this.#leavesKingSafe(move, color)));
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 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
|
|
124
|
+
* mate, which an en passant capture may answer: chess.js tells.
|
|
125
|
+
*/
|
|
126
|
+
sanOf(move, moves) {
|
|
127
|
+
const { type, color } = this.#pieces.get(move.from);
|
|
128
|
+
const other = color === "w" ? "b" : "w";
|
|
129
|
+
const takes = this.#pieces.has(move.to) || (type === "p" && move.from[0] !== move.to[0]);
|
|
130
|
+
const head = type === "p" ? (takes ? `${move.from[0]}x` : "") : `${type.toUpperCase()}${this.#disambiguation(move, type, moves)}${takes ? "x" : ""}`;
|
|
131
|
+
this.play(move);
|
|
132
|
+
const check = this.inCheck(other);
|
|
133
|
+
const mate = check && !this.hasMove(other);
|
|
134
|
+
this.undo();
|
|
135
|
+
if (mate && type === "p" && Math.abs(Number(move.to[1]) - Number(move.from[1])) === 2) {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
return `${head}${move.to}${move.promotion === undefined ? "" : `=${move.promotion.toUpperCase()}`}${mate ? "#" : check ? "+" : ""}`;
|
|
139
|
+
}
|
|
140
|
+
/** Whether a side's king is attacked. */
|
|
141
|
+
inCheck(color) {
|
|
142
|
+
const king = this.findPiece({ type: "k", color })[0];
|
|
143
|
+
return king !== undefined && this.attackers(king, color === "w" ? "b" : "w").length > 0;
|
|
144
|
+
}
|
|
145
|
+
#leavesKingSafe(move, color) {
|
|
146
|
+
this.play(move);
|
|
147
|
+
const safe = !this.inCheck(color);
|
|
148
|
+
this.undo();
|
|
149
|
+
return safe;
|
|
150
|
+
}
|
|
151
|
+
// A side's pieces square by square from a8 to h1, as chess.js goes over them.
|
|
152
|
+
#placedOf(color) {
|
|
153
|
+
return boardOrder.flatMap((square) => {
|
|
154
|
+
const piece = this.#pieces.get(square);
|
|
155
|
+
return piece?.color === color ? [{ square, type: piece.type, color }] : [];
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
// The squares a move's piece starts from, when another of its type reaches the same square: its file, else its rank, else both (chess.js).
|
|
159
|
+
#disambiguation(move, type, moves) {
|
|
160
|
+
const rivals = moves.filter((each) => each.to === move.to && each.from !== move.from && this.#pieces.get(each.from)?.type === type);
|
|
161
|
+
if (rivals.length === 0) {
|
|
162
|
+
return "";
|
|
163
|
+
}
|
|
164
|
+
const sameRank = rivals.some((each) => each.from[1] === move.from[1]);
|
|
165
|
+
const sameFile = rivals.some((each) => each.from[0] === move.from[0]);
|
|
166
|
+
return sameRank && sameFile ? move.from : sameFile ? move.from[1] : move.from[0];
|
|
167
|
+
}
|
|
168
|
+
// A piece's moves before its king's safety is looked at, in chess.js's order.
|
|
169
|
+
#reachable({ square, type, color }) {
|
|
170
|
+
const free = (to) => !this.#pieces.has(to);
|
|
171
|
+
const takes = (to) => this.#pieces.get(to)?.color === (color === "w" ? "b" : "w");
|
|
172
|
+
const open = (to) => free(to) || takes(to);
|
|
173
|
+
if (type === "p") {
|
|
174
|
+
const forward = pawnPushes[color].get(square);
|
|
175
|
+
const pushes = free(forward[0]) ? (forward.length === 2 && free(forward[1]) ? forward : forward.slice(0, 1)) : [];
|
|
176
|
+
const captures = pawnCaptures[color].get(square).filter(takes);
|
|
177
|
+
return [...pushes, ...captures].flatMap((to) => (to[1] === "8" || to[1] === "1" ? promotions.map((promotion) => ({ from: square, to, promotion })) : [{ from: square, to }]));
|
|
178
|
+
}
|
|
179
|
+
if (type === "n" || type === "k") {
|
|
180
|
+
return (type === "n" ? knightSteps : kingSteps).get(square).filter(open).map((to) => ({ from: square, to }));
|
|
181
|
+
}
|
|
182
|
+
return sliderRays[type].get(square).flatMap((ray) => {
|
|
183
|
+
const stop = ray.findIndex((each) => !free(each));
|
|
184
|
+
const reached = stop === -1 ? ray : ray.slice(0, takes(ray[stop]) ? stop + 1 : stop);
|
|
185
|
+
return reached.map((to) => ({ from: square, to }));
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
/** 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. */
|
|
190
|
+
export function namesOn(boardOf) {
|
|
191
|
+
const bySquare = new Map();
|
|
192
|
+
return (move) => {
|
|
193
|
+
let moves = bySquare.get(move.from);
|
|
194
|
+
if (moves === undefined) {
|
|
195
|
+
moves = boardOf().moves({ square: move.from, verbose: true });
|
|
196
|
+
bySquare.set(move.from, moves);
|
|
197
|
+
}
|
|
198
|
+
const index = moves.findIndex((each) => each.to === move.to && each.promotion === move.promotion);
|
|
199
|
+
// chess.js lists the moves square by square from a8 to h1.
|
|
200
|
+
const square = (8 - Number(move.from[1])) * 8 + files.indexOf(move.from[0]);
|
|
201
|
+
return index === -1 ? undefined : { san: moves[index].san, order: square * 256 + index };
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
/** 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. */
|
|
205
|
+
export function mayCastle(chess, color) {
|
|
206
|
+
const rank = color === "w" ? "1" : "8";
|
|
207
|
+
const rights = chess.getCastlingRights(color);
|
|
208
|
+
const empty = (between) => [...between].every((file) => chess.get(`${file}${rank}`) === undefined);
|
|
209
|
+
return (rights.k && empty("fg")) || (rights.q && empty("bcd"));
|
|
210
|
+
}
|
|
211
|
+
/** The same board with the other side to move, without en passant, which belonged to the side to move. */
|
|
212
|
+
export function turnPassed(chess) {
|
|
213
|
+
const fields = chess.fen().split(" ");
|
|
214
|
+
fields[1] = chess.turn() === "w" ? "b" : "w";
|
|
215
|
+
fields[3] = "-";
|
|
216
|
+
return new Chess(fields.join(" "));
|
|
217
|
+
}
|
|
218
|
+
const files = "abcdefgh";
|
|
219
|
+
const promotions = ["n", "b", "r", "q"];
|
|
220
|
+
const squares = [...files].flatMap((file) => [1, 2, 3, 4, 5, 6, 7, 8].map((rank) => `${file}${rank}`));
|
|
221
|
+
const boardOrder = [8, 7, 6, 5, 4, 3, 2, 1].flatMap((rank) => [...files].map((file) => `${file}${rank}`));
|
|
222
|
+
// The steps and lines from each square, worked out once, in the order chess.js tries them.
|
|
223
|
+
const knightSteps = stepsFrom([[-2, 1], [-1, 2], [1, 2], [2, 1], [2, -1], [1, -2], [-1, -2], [-2, -1]]);
|
|
224
|
+
const kingSteps = stepsFrom([[-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0]]);
|
|
225
|
+
// The squares a pawn of a side takes a square from, those it takes on, and those it pushes to: one, or two from its first rank.
|
|
226
|
+
const pawnsTaking = { w: stepsFrom([[-1, -1], [1, -1]]), b: stepsFrom([[-1, 1], [1, 1]]) };
|
|
227
|
+
const pawnCaptures = { w: stepsFrom([[-1, 1], [1, 1]]), b: stepsFrom([[1, -1], [-1, -1]]) };
|
|
228
|
+
const pawnPushes = { w: pushesFrom(1, "2"), b: pushesFrom(-1, "7") };
|
|
229
|
+
const sliderRays = {
|
|
230
|
+
b: raysFrom([[-1, 1], [1, 1], [1, -1], [-1, -1]]),
|
|
231
|
+
r: raysFrom([[0, 1], [1, 0], [0, -1], [-1, 0]]),
|
|
232
|
+
q: raysFrom([[-1, 1], [0, 1], [1, 1], [1, 0], [1, -1], [0, -1], [-1, -1], [-1, 0]]),
|
|
233
|
+
};
|
|
234
|
+
// Each line from each square, with the pieces that move along it.
|
|
235
|
+
const lineSteps = [[0, 1, "rq"], [0, -1, "rq"], [1, 0, "rq"], [-1, 0, "rq"], [1, 1, "bq"], [1, -1, "bq"], [-1, 1, "bq"], [-1, -1, "bq"]];
|
|
236
|
+
const linesFrom = new Map(squares.map((square) => [square, lineSteps.map(([file, rank, sliders]) => ({ squares: rayFrom(square, file, rank), sliders }))]));
|
|
237
|
+
// The lines of lineSteps that run opposite ways from a square.
|
|
238
|
+
const opposite = [[0, 1], [2, 3], [4, 7], [5, 6]];
|
|
239
|
+
function stepsFrom(steps) {
|
|
240
|
+
return new Map(squares.map((square) => [square, steps.map(([file, rank]) => squareAt(square, file, rank)).filter((each) => each !== undefined)]));
|
|
241
|
+
}
|
|
242
|
+
function raysFrom(steps) {
|
|
243
|
+
return new Map(squares.map((square) => [square, steps.map(([file, rank]) => rayFrom(square, file, rank))]));
|
|
244
|
+
}
|
|
245
|
+
function pushesFrom(step, firstRank) {
|
|
246
|
+
return new Map(squares.map((square) => [square, (square[1] === firstRank ? [step, 2 * step] : [step]).map((rank) => squareAt(square, 0, rank)).filter((each) => each !== undefined)]));
|
|
247
|
+
}
|
|
248
|
+
function rayFrom(from, fileStep, rankStep) {
|
|
249
|
+
const ray = [];
|
|
250
|
+
for (let next = squareAt(from, fileStep, rankStep); next !== undefined; next = squareAt(next, fileStep, rankStep)) {
|
|
251
|
+
ray.push(next);
|
|
252
|
+
}
|
|
253
|
+
return ray;
|
|
254
|
+
}
|
|
255
|
+
function squareAt(from, fileStep, rankStep) {
|
|
256
|
+
const file = files.indexOf(from[0]) + fileStep;
|
|
257
|
+
const rank = Number(from[1]) + rankStep;
|
|
258
|
+
return file >= 0 && file < 8 && rank >= 1 && rank <= 8 ? `${files[file]}${rank}` : undefined;
|
|
259
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { Chess, type Square } from "chess.js";
|
|
2
|
+
import type { Move } from "./game.ts";
|
|
3
|
+
/** A capture of the side to move, with what the piece taken is to the piece taking it: none when it is defended and worth less. */
|
|
4
|
+
export interface Capture {
|
|
5
|
+
readonly san: string;
|
|
6
|
+
readonly fact?: "undefended" | "worth more" | "same value" | "outnumbered";
|
|
7
|
+
}
|
|
8
|
+
/** A move that attacks two targets or more: a fork when the piece moved attacks them all, else a double attack with a piece it uncovers. */
|
|
9
|
+
export interface DoubleAttack {
|
|
10
|
+
readonly san: string;
|
|
11
|
+
readonly targets: readonly Square[];
|
|
12
|
+
readonly fork: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** A move after which a reply of the other side stalemates the side that moved: those replies, or all when every one does. */
|
|
15
|
+
export interface StalemateTrap {
|
|
16
|
+
readonly san: string;
|
|
17
|
+
readonly replies: readonly string[] | "all";
|
|
18
|
+
}
|
|
19
|
+
/** What a side's moves would do to the other side. */
|
|
20
|
+
export interface Chances {
|
|
21
|
+
readonly doubleAttacks: readonly DoubleAttack[];
|
|
22
|
+
readonly discoveredChecks: readonly string[];
|
|
23
|
+
readonly doubleChecks: readonly string[];
|
|
24
|
+
readonly promotions: readonly string[];
|
|
25
|
+
}
|
|
26
|
+
/** What the moves of the side to move would do. */
|
|
27
|
+
export interface Possibilities extends Chances {
|
|
28
|
+
readonly captures: readonly Capture[];
|
|
29
|
+
readonly stalemates: readonly string[];
|
|
30
|
+
readonly stalemateTraps: readonly StalemateTrap[];
|
|
31
|
+
}
|
|
32
|
+
/** The possibilities of a chess.js board, whose legal moves are given: computed once for each position, by its key, and frozen. */
|
|
33
|
+
export declare function possibilitiesOf(chess: Chess, legal: readonly Move[]): Possibilities;
|
|
34
|
+
/**
|
|
35
|
+
* 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.
|
|
36
|
+
* Computed apart from the possibilities, only where they are shown: once for each position, by its key, and frozen.
|
|
37
|
+
*/
|
|
38
|
+
export declare function threatsOf(chess: Chess): Chances | undefined;
|
|
@@ -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
|
+
}
|
package/dist/core/review.d.ts
CHANGED
|
@@ -65,7 +65,10 @@ export interface AiModel {
|
|
|
65
65
|
}
|
|
66
66
|
/** 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
67
|
export declare function withAiModel(record: GameRecord, played: AiModel | undefined): GameRecord;
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* 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,
|
|
70
|
+
* whose Annotator is someone else's, such as Lichess's.
|
|
71
|
+
*/
|
|
69
72
|
export declare function annotatorOf(record: GameRecord): {
|
|
70
73
|
annotator: string;
|
|
71
74
|
effort?: string;
|
package/dist/core/review.js
CHANGED
|
@@ -63,9 +63,8 @@ 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
|
-
//
|
|
67
|
-
const
|
|
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) ? Object.fromEntries(Object.entries(record.tags).filter(([name]) => name !== "Annotator" && name !== effortTag)) : record.tags;
|
|
69
68
|
return comment === "" ? { tags, moves } : { tags, comment, moves };
|
|
70
69
|
}
|
|
71
70
|
/** The arrows and squares as PGN's [%cal] and [%csl] write them, for a comment; empty without any. */
|
|
@@ -82,9 +81,12 @@ export function withAnnotator(record, { annotator, effort }) {
|
|
|
82
81
|
export function withAiModel(record, played) {
|
|
83
82
|
return played === undefined ? record : { ...record, tags: signed(record.tags, ["AIModel", "AIEffort"], played.model, played.effort) };
|
|
84
83
|
}
|
|
85
|
-
/**
|
|
84
|
+
/**
|
|
85
|
+
* 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,
|
|
86
|
+
* whose Annotator is someone else's, such as Lichess's.
|
|
87
|
+
*/
|
|
86
88
|
export function annotatorOf(record) {
|
|
87
|
-
const found = signerOf(record.tags, ["Annotator", effortTag]);
|
|
89
|
+
const found = carriesReview(record) ? signerOf(record.tags, ["Annotator", effortTag]) : undefined;
|
|
88
90
|
return found === undefined ? undefined : { annotator: found.model, ...(found.effort === undefined ? {} : { effort: found.effort }) };
|
|
89
91
|
}
|
|
90
92
|
/** The model the AI played the game with, as its tags say (D-82), and the effort when known; undefined without one. */
|
|
@@ -123,6 +125,16 @@ export function withGameReview(record, { summary, closing }) {
|
|
|
123
125
|
}
|
|
124
126
|
// The effort has no tag of its own in PGN: Chesspal's sits beside the Annotator.
|
|
125
127
|
const effortTag = "AnnotatorEffort";
|
|
128
|
+
/**
|
|
129
|
+
* 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
|
|
130
|
+
* starts. Only then is the Annotator the AI's.
|
|
131
|
+
*/
|
|
132
|
+
function carriesReview(record) {
|
|
133
|
+
return isReviewed(record) || record.moves.some((move) => {
|
|
134
|
+
const review = reviewOf(move.comment);
|
|
135
|
+
return review?.note !== undefined || (review?.badge !== undefined && review.badge !== "book");
|
|
136
|
+
});
|
|
137
|
+
}
|
|
126
138
|
/** A model and its effort as the tags say, each only when not blank; undefined without the model. */
|
|
127
139
|
function signerOf(tags, [modelTag, modelEffortTag]) {
|
|
128
140
|
const model = tags[modelTag]?.trim();
|
|
@@ -146,7 +158,6 @@ const judgmentNags = new Set([1, 2, 3, 4, 5, 6]);
|
|
|
146
158
|
// The badges PGN has a NAG for: !! ! ?! ? ??.
|
|
147
159
|
const nagOfBadge = { brilliant: 3, great: 1, inaccuracy: 6, mistake: 2, blunder: 4 };
|
|
148
160
|
const badgePattern = /\[%badge (\w+)\]/;
|
|
149
|
-
const reviewMark = /\[%(badge|review) /;
|
|
150
161
|
const notePattern = /\[%review ([^\]]*)\]/;
|
|
151
162
|
const closingPattern = /\[%closing ([^\]]*)\]/;
|
|
152
163
|
const arrowPattern = /\[%cal ([^\]]*)\]/;
|
package/dist/games/analysis.d.ts
CHANGED
|
@@ -1,12 +1,16 @@
|
|
|
1
|
-
/**
|
|
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, and the moves whose note the chat corrected with the player, which the analyst leaves (D-101).
|
|
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[];
|
|
6
10
|
}
|
|
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;
|
|
11
|
+
/** 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. */
|
|
12
|
+
export declare function startAnalysis(folder: string, game: string, now: Date, brought?: boolean, corrected?: readonly string[]): void;
|
|
9
13
|
/** The game in analysis, or none when there is none, or analysis.json cannot be read. */
|
|
10
14
|
export declare function gameInAnalysis(folder: string): Analysis | undefined;
|
|
11
|
-
/** Ends the analysis, when a new game starts. */
|
|
15
|
+
/** Ends the analysis, when a new game starts or its review is deleted. */
|
|
12
16
|
export declare function endAnalysis(folder: string): void;
|