@connectorvol/chessops 0.15.1
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/LICENSE.txt +674 -0
- package/README.md +61 -0
- package/dist/cjs/attacks.js +152 -0
- package/dist/cjs/attacks.js.map +1 -0
- package/dist/cjs/board.js +143 -0
- package/dist/cjs/board.js.map +1 -0
- package/dist/cjs/chess.js +638 -0
- package/dist/cjs/chess.js.map +1 -0
- package/dist/cjs/compat.js +89 -0
- package/dist/cjs/compat.js.map +1 -0
- package/dist/cjs/debug.js +103 -0
- package/dist/cjs/debug.js.map +1 -0
- package/dist/cjs/fen.js +325 -0
- package/dist/cjs/fen.js.map +1 -0
- package/dist/cjs/index.js +94 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/pgn.js +796 -0
- package/dist/cjs/pgn.js.map +1 -0
- package/dist/cjs/san.js +174 -0
- package/dist/cjs/san.js.map +1 -0
- package/dist/cjs/setup.js +167 -0
- package/dist/cjs/setup.js.map +1 -0
- package/dist/cjs/squareSet.js +206 -0
- package/dist/cjs/squareSet.js.map +1 -0
- package/dist/cjs/transform.js +57 -0
- package/dist/cjs/transform.js.map +1 -0
- package/dist/cjs/types.js +24 -0
- package/dist/cjs/types.js.map +1 -0
- package/dist/cjs/util.js +104 -0
- package/dist/cjs/util.js.map +1 -0
- package/dist/cjs/variant.js +833 -0
- package/dist/cjs/variant.js.map +1 -0
- package/dist/esm/attacks.js +140 -0
- package/dist/esm/attacks.js.map +1 -0
- package/dist/esm/board.js +138 -0
- package/dist/esm/board.js.map +1 -0
- package/dist/esm/chess.js +624 -0
- package/dist/esm/chess.js.map +1 -0
- package/dist/esm/compat.js +81 -0
- package/dist/esm/compat.js.map +1 -0
- package/dist/esm/debug.js +94 -0
- package/dist/esm/debug.js.map +1 -0
- package/dist/esm/fen.js +308 -0
- package/dist/esm/fen.js.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/pgn.js +769 -0
- package/dist/esm/pgn.js.map +1 -0
- package/dist/esm/san.js +167 -0
- package/dist/esm/san.js.map +1 -0
- package/dist/esm/setup.js +157 -0
- package/dist/esm/setup.js.map +1 -0
- package/dist/esm/squareSet.js +202 -0
- package/dist/esm/squareSet.js.map +1 -0
- package/dist/esm/transform.js +48 -0
- package/dist/esm/transform.js.map +1 -0
- package/dist/esm/types.js +19 -0
- package/dist/esm/types.js.map +1 -0
- package/dist/esm/util.js +87 -0
- package/dist/esm/util.js.map +1 -0
- package/dist/esm/variant.js +812 -0
- package/dist/esm/variant.js.map +1 -0
- package/dist/types/attacks.d.ts +58 -0
- package/dist/types/board.d.ts +62 -0
- package/dist/types/chess.d.ts +82 -0
- package/dist/types/compat.d.ts +26 -0
- package/dist/types/debug.d.ts +10 -0
- package/dist/types/fen.d.ts +40 -0
- package/dist/types/index.d.ts +14 -0
- package/dist/types/pgn.d.ts +203 -0
- package/dist/types/san.d.ts +6 -0
- package/dist/types/setup.d.ts +65 -0
- package/dist/types/squareSet.d.ts +50 -0
- package/dist/types/transform.d.ts +9 -0
- package/dist/types/types.d.ts +58 -0
- package/dist/types/util.d.ts +21 -0
- package/dist/types/variant.d.ts +92 -0
- package/package.json +86 -0
- package/src/attacks.ts +160 -0
- package/src/board.ts +168 -0
- package/src/chess.ts +687 -0
- package/src/compat.ts +120 -0
- package/src/debug.ts +100 -0
- package/src/fen.ts +328 -0
- package/src/index.ts +85 -0
- package/src/pgn.ts +876 -0
- package/src/san.ts +190 -0
- package/src/setup.ts +203 -0
- package/src/squareSet.ts +243 -0
- package/src/transform.ts +49 -0
- package/src/types.ts +93 -0
- package/src/util.ts +116 -0
- package/src/variant.ts +939 -0
package/src/pgn.ts
ADDED
|
@@ -0,0 +1,876 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse, transform and write PGN.
|
|
3
|
+
*
|
|
4
|
+
* ## Parser
|
|
5
|
+
*
|
|
6
|
+
* The parser will interpret any input as a PGN, creating a tree of
|
|
7
|
+
* syntactically valid (but not necessarily legal) moves, skipping any invalid
|
|
8
|
+
* tokens.
|
|
9
|
+
*
|
|
10
|
+
* ```ts
|
|
11
|
+
* import { parsePgn, startingPosition } from '@connectorvol/chessops/pgn';
|
|
12
|
+
* import { parseSan } from '@connectorvol/chessops/san';
|
|
13
|
+
*
|
|
14
|
+
* const pgn = '1. d4 d5 *';
|
|
15
|
+
* const games = parsePgn(pgn);
|
|
16
|
+
* for (const game of games) {
|
|
17
|
+
* const pos = startingPosition(game.headers).unwrap();
|
|
18
|
+
* for (const node of game.moves.mainline()) {
|
|
19
|
+
* const move = parseSan(pos, node.san);
|
|
20
|
+
* if (!move) break; // Illegal move
|
|
21
|
+
* pos.play(move);
|
|
22
|
+
* }
|
|
23
|
+
* }
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ## Streaming parser
|
|
27
|
+
*
|
|
28
|
+
* The module also provides a denial-of-service resistant streaming parser.
|
|
29
|
+
* It can be configured with a budget for reasonable complexity of a single
|
|
30
|
+
* game, fed with chunks of text, and will yield parsed games as they are
|
|
31
|
+
* completed.
|
|
32
|
+
*
|
|
33
|
+
* ```ts
|
|
34
|
+
*
|
|
35
|
+
* import { createReadStream } from 'fs';
|
|
36
|
+
* import { PgnParser } from '@connectorvol/chessops/pgn';
|
|
37
|
+
*
|
|
38
|
+
* const stream = createReadStream('games.pgn', { encoding: 'utf-8' });
|
|
39
|
+
*
|
|
40
|
+
* const parser = new PgnParser((game, err) => {
|
|
41
|
+
* if (err) {
|
|
42
|
+
* // Budget exceeded.
|
|
43
|
+
* stream.destroy(err);
|
|
44
|
+
* }
|
|
45
|
+
*
|
|
46
|
+
* // Use game ...
|
|
47
|
+
* });
|
|
48
|
+
*
|
|
49
|
+
* await new Promise<void>(resolve =>
|
|
50
|
+
* stream
|
|
51
|
+
* .on('data', (chunk: string) => parser.parse(chunk, { stream: true }))
|
|
52
|
+
* .on('close', () => {
|
|
53
|
+
* parser.parse('');
|
|
54
|
+
* resolve();
|
|
55
|
+
* })
|
|
56
|
+
* );
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* ## Augmenting the game tree
|
|
60
|
+
*
|
|
61
|
+
* You can use `walk` to visit all nodes in the game tree, or `transform`
|
|
62
|
+
* to augment it with user data.
|
|
63
|
+
*
|
|
64
|
+
* Both allow you to provide context. You update the context inside the
|
|
65
|
+
* callback, and it is automatically `clone()`-ed at each fork.
|
|
66
|
+
* In the example below, the current position `pos` is provided as context.
|
|
67
|
+
*
|
|
68
|
+
* ```ts
|
|
69
|
+
* import { transform } from '@connectorvol/chessops/pgn';
|
|
70
|
+
* import { makeFen } from '@connectorvol/chessops/fen';
|
|
71
|
+
* import { parseSan, makeSanAndPlay } from '@connectorvol/chessops/san';
|
|
72
|
+
*
|
|
73
|
+
* const pos = startingPosition(game.headers).unwrap();
|
|
74
|
+
* game.moves = transform(game.moves, pos, (pos, node) => {
|
|
75
|
+
* const move = parseSan(pos, node.san);
|
|
76
|
+
* if (!move) {
|
|
77
|
+
* // Illegal move. Returning undefined cuts off the tree here.
|
|
78
|
+
* return;
|
|
79
|
+
* }
|
|
80
|
+
*
|
|
81
|
+
* const san = makeSanAndPlay(pos, move); // Mutating pos!
|
|
82
|
+
*
|
|
83
|
+
* return {
|
|
84
|
+
* ...node, // Keep comments and annotation glyphs
|
|
85
|
+
* san, // Normalized SAN
|
|
86
|
+
* fen: makeFen(pos.toSetup()), // Add arbitrary user data to node
|
|
87
|
+
* };
|
|
88
|
+
* });
|
|
89
|
+
* ```
|
|
90
|
+
*
|
|
91
|
+
* ## Writing
|
|
92
|
+
*
|
|
93
|
+
* Requires each node to at least have a `san` property.
|
|
94
|
+
*
|
|
95
|
+
* ```
|
|
96
|
+
* import { makePgn } from '@connectorvol/chessops/pgn';
|
|
97
|
+
*
|
|
98
|
+
* const rewrittenPgn = makePgn(game);
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @packageDocumentation
|
|
102
|
+
*/
|
|
103
|
+
import { Result } from "@badrap/result";
|
|
104
|
+
import { IllegalSetup, Position, PositionError } from "./chess.js";
|
|
105
|
+
import { FenError, makeFen, parseFen } from "./fen.js";
|
|
106
|
+
import { Outcome, Rules, Square } from "./types.js";
|
|
107
|
+
import { defined, makeSquare, parseSquare } from "./util.js";
|
|
108
|
+
import { defaultPosition, setupPosition } from "./variant.js";
|
|
109
|
+
|
|
110
|
+
export interface Game<T> {
|
|
111
|
+
headers: Map<string, string>;
|
|
112
|
+
comments?: string[];
|
|
113
|
+
moves: Node<T>;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export const defaultGame = <T>(
|
|
117
|
+
initHeaders: () => Map<string, string> = defaultHeaders,
|
|
118
|
+
): Game<T> => ({
|
|
119
|
+
headers: initHeaders(),
|
|
120
|
+
moves: new Node(),
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
export class Node<T> {
|
|
124
|
+
children: ChildNode<T>[] = [];
|
|
125
|
+
|
|
126
|
+
*mainlineNodes(): Iterable<ChildNode<T>> {
|
|
127
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
128
|
+
let node: Node<T> = this;
|
|
129
|
+
while (node.children.length) {
|
|
130
|
+
const child = node.children[0];
|
|
131
|
+
yield child;
|
|
132
|
+
node = child;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
*mainline(): Iterable<T> {
|
|
137
|
+
for (const child of this.mainlineNodes()) yield child.data;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
end(): Node<T> {
|
|
141
|
+
// eslint-disable-next-line @typescript-eslint/no-this-alias
|
|
142
|
+
let node: Node<T> = this;
|
|
143
|
+
while (node.children.length) node = node.children[0];
|
|
144
|
+
return node;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export class ChildNode<T> extends Node<T> {
|
|
149
|
+
constructor(public data: T) {
|
|
150
|
+
super();
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export const isChildNode = <T>(node: Node<T>): node is ChildNode<T> => node instanceof ChildNode;
|
|
155
|
+
|
|
156
|
+
export const extend = <T>(node: Node<T>, data: T[]): Node<T> => {
|
|
157
|
+
for (const d of data) {
|
|
158
|
+
const child = new ChildNode(d);
|
|
159
|
+
node.children.push(child);
|
|
160
|
+
node = child;
|
|
161
|
+
}
|
|
162
|
+
return node;
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export class Box<T> {
|
|
166
|
+
constructor(public value: T) {}
|
|
167
|
+
|
|
168
|
+
clone(): Box<T> {
|
|
169
|
+
return new Box(this.value);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export const transform = <T, U, C extends { clone(): C }>(
|
|
174
|
+
node: Node<T>,
|
|
175
|
+
ctx: C,
|
|
176
|
+
f: (ctx: C, data: T, childIndex: number) => U | undefined,
|
|
177
|
+
): Node<U> => {
|
|
178
|
+
const root = new Node<U>();
|
|
179
|
+
const stack = [
|
|
180
|
+
{
|
|
181
|
+
before: node,
|
|
182
|
+
after: root,
|
|
183
|
+
ctx,
|
|
184
|
+
},
|
|
185
|
+
];
|
|
186
|
+
let frame;
|
|
187
|
+
while ((frame = stack.pop())) {
|
|
188
|
+
for (let childIndex = 0; childIndex < frame.before.children.length; childIndex++) {
|
|
189
|
+
const ctx = childIndex < frame.before.children.length - 1 ? frame.ctx.clone() : frame.ctx;
|
|
190
|
+
const childBefore = frame.before.children[childIndex];
|
|
191
|
+
const data = f(ctx, childBefore.data, childIndex);
|
|
192
|
+
if (defined(data)) {
|
|
193
|
+
const childAfter = new ChildNode(data);
|
|
194
|
+
frame.after.children.push(childAfter);
|
|
195
|
+
stack.push({
|
|
196
|
+
before: childBefore,
|
|
197
|
+
after: childAfter,
|
|
198
|
+
ctx,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
return root;
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
export const walk = <T, C extends { clone(): C }>(
|
|
207
|
+
node: Node<T>,
|
|
208
|
+
ctx: C,
|
|
209
|
+
f: (ctx: C, data: T, childIndex: number) => boolean | void,
|
|
210
|
+
) => {
|
|
211
|
+
const stack = [{ node, ctx }];
|
|
212
|
+
let frame;
|
|
213
|
+
while ((frame = stack.pop())) {
|
|
214
|
+
for (let childIndex = 0; childIndex < frame.node.children.length; childIndex++) {
|
|
215
|
+
const ctx = childIndex < frame.node.children.length - 1 ? frame.ctx.clone() : frame.ctx;
|
|
216
|
+
const child = frame.node.children[childIndex];
|
|
217
|
+
if (f(ctx, child.data, childIndex) !== false) stack.push({ node: child, ctx });
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
export interface PgnNodeData {
|
|
223
|
+
san: string;
|
|
224
|
+
startingComments?: string[];
|
|
225
|
+
comments?: string[];
|
|
226
|
+
nags?: number[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
export const makeOutcome = (outcome: Outcome | undefined): string => {
|
|
230
|
+
if (!outcome) return "*";
|
|
231
|
+
else if (outcome.winner === "white") return "1-0";
|
|
232
|
+
else if (outcome.winner === "black") return "0-1";
|
|
233
|
+
else return "1/2-1/2";
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
export const parseOutcome = (s: string | undefined): Outcome | undefined => {
|
|
237
|
+
if (s === "1-0" || s === "1–0" || s === "1—0") return { winner: "white" };
|
|
238
|
+
else if (s === "0-1" || s === "0–1" || s === "0—1") return { winner: "black" };
|
|
239
|
+
else if (s === "1/2-1/2" || s === "1/2–1/2" || s === "1/2—1/2") return { winner: undefined };
|
|
240
|
+
else return;
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const escapeHeader = (value: string): string => value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
244
|
+
|
|
245
|
+
const safeComment = (comment: string): string => comment.replace(/\}/g, "");
|
|
246
|
+
|
|
247
|
+
const enum MakePgnState {
|
|
248
|
+
Pre = 0,
|
|
249
|
+
Sidelines = 1,
|
|
250
|
+
End = 2,
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
interface MakePgnFrame {
|
|
254
|
+
state: MakePgnState;
|
|
255
|
+
ply: number;
|
|
256
|
+
node: ChildNode<PgnNodeData>;
|
|
257
|
+
sidelines: Iterator<ChildNode<PgnNodeData>>;
|
|
258
|
+
startsVariation: boolean;
|
|
259
|
+
inVariation: boolean;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export const makePgn = (game: Game<PgnNodeData>): string => {
|
|
263
|
+
const builder = [],
|
|
264
|
+
tokens = [];
|
|
265
|
+
|
|
266
|
+
if (game.headers.size) {
|
|
267
|
+
for (const [key, value] of game.headers.entries()) {
|
|
268
|
+
builder.push("[", key, ' "', escapeHeader(value), '"]\n');
|
|
269
|
+
}
|
|
270
|
+
builder.push("\n");
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
for (const comment of game.comments || []) tokens.push("{", safeComment(comment), "}");
|
|
274
|
+
|
|
275
|
+
const fen = game.headers.get("FEN");
|
|
276
|
+
const initialPly = fen
|
|
277
|
+
? parseFen(fen).unwrap(
|
|
278
|
+
(setup) => (setup.fullmoves - 1) * 2 + (setup.turn === "white" ? 0 : 1),
|
|
279
|
+
(_) => 0,
|
|
280
|
+
)
|
|
281
|
+
: 0;
|
|
282
|
+
|
|
283
|
+
const stack: MakePgnFrame[] = [];
|
|
284
|
+
|
|
285
|
+
const variations = game.moves.children[Symbol.iterator]();
|
|
286
|
+
const firstVariation = variations.next();
|
|
287
|
+
if (!firstVariation.done) {
|
|
288
|
+
stack.push({
|
|
289
|
+
state: MakePgnState.Pre,
|
|
290
|
+
ply: initialPly,
|
|
291
|
+
node: firstVariation.value,
|
|
292
|
+
sidelines: variations,
|
|
293
|
+
startsVariation: false,
|
|
294
|
+
inVariation: false,
|
|
295
|
+
});
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let forceMoveNumber = true;
|
|
299
|
+
while (stack.length) {
|
|
300
|
+
const frame = stack[stack.length - 1];
|
|
301
|
+
|
|
302
|
+
if (frame.inVariation) {
|
|
303
|
+
tokens.push(")");
|
|
304
|
+
frame.inVariation = false;
|
|
305
|
+
forceMoveNumber = true;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
switch (frame.state) {
|
|
309
|
+
case MakePgnState.Pre:
|
|
310
|
+
for (const comment of frame.node.data.startingComments || []) {
|
|
311
|
+
tokens.push("{", safeComment(comment), "}");
|
|
312
|
+
forceMoveNumber = true;
|
|
313
|
+
}
|
|
314
|
+
if (forceMoveNumber || frame.ply % 2 === 0) {
|
|
315
|
+
tokens.push(Math.floor(frame.ply / 2) + 1 + (frame.ply % 2 ? "..." : "."));
|
|
316
|
+
forceMoveNumber = false;
|
|
317
|
+
}
|
|
318
|
+
tokens.push(frame.node.data.san);
|
|
319
|
+
for (const nag of frame.node.data.nags || []) {
|
|
320
|
+
tokens.push("$" + nag);
|
|
321
|
+
forceMoveNumber = true;
|
|
322
|
+
}
|
|
323
|
+
for (const comment of frame.node.data.comments || []) {
|
|
324
|
+
tokens.push("{", safeComment(comment), "}");
|
|
325
|
+
}
|
|
326
|
+
frame.state = MakePgnState.Sidelines; // fall through
|
|
327
|
+
case MakePgnState.Sidelines: {
|
|
328
|
+
const child = frame.sidelines.next();
|
|
329
|
+
if (child.done) {
|
|
330
|
+
const variations = frame.node.children[Symbol.iterator]();
|
|
331
|
+
const firstVariation = variations.next();
|
|
332
|
+
if (!firstVariation.done) {
|
|
333
|
+
stack.push({
|
|
334
|
+
state: MakePgnState.Pre,
|
|
335
|
+
ply: frame.ply + 1,
|
|
336
|
+
node: firstVariation.value,
|
|
337
|
+
sidelines: variations,
|
|
338
|
+
startsVariation: false,
|
|
339
|
+
inVariation: false,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
frame.state = MakePgnState.End;
|
|
343
|
+
} else {
|
|
344
|
+
tokens.push("(");
|
|
345
|
+
forceMoveNumber = true;
|
|
346
|
+
stack.push({
|
|
347
|
+
state: MakePgnState.Pre,
|
|
348
|
+
ply: frame.ply,
|
|
349
|
+
node: child.value,
|
|
350
|
+
sidelines: [][Symbol.iterator](),
|
|
351
|
+
startsVariation: true,
|
|
352
|
+
inVariation: false,
|
|
353
|
+
});
|
|
354
|
+
frame.inVariation = true;
|
|
355
|
+
}
|
|
356
|
+
break;
|
|
357
|
+
}
|
|
358
|
+
case MakePgnState.End:
|
|
359
|
+
stack.pop();
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
tokens.push(makeOutcome(parseOutcome(game.headers.get("Result"))));
|
|
364
|
+
|
|
365
|
+
builder.push(tokens.join(" "), "\n");
|
|
366
|
+
return builder.join("");
|
|
367
|
+
};
|
|
368
|
+
|
|
369
|
+
export const defaultHeaders = (): Map<string, string> =>
|
|
370
|
+
new Map([
|
|
371
|
+
["Event", "?"],
|
|
372
|
+
["Site", "?"],
|
|
373
|
+
["Date", "????.??.??"],
|
|
374
|
+
["Round", "?"],
|
|
375
|
+
["White", "?"],
|
|
376
|
+
["Black", "?"],
|
|
377
|
+
["Result", "*"],
|
|
378
|
+
]);
|
|
379
|
+
|
|
380
|
+
export const emptyHeaders = (): Map<string, string> => new Map();
|
|
381
|
+
|
|
382
|
+
const BOM = "\ufeff";
|
|
383
|
+
|
|
384
|
+
const isWhitespace = (line: string): boolean => /^\s*$/.test(line);
|
|
385
|
+
|
|
386
|
+
const isCommentLine = (line: string): boolean => line.startsWith("%");
|
|
387
|
+
|
|
388
|
+
export interface ParseOptions {
|
|
389
|
+
stream: boolean;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
interface ParserFrame {
|
|
393
|
+
parent: Node<PgnNodeData>;
|
|
394
|
+
root: boolean;
|
|
395
|
+
node?: ChildNode<PgnNodeData>;
|
|
396
|
+
startingComments?: string[];
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
const enum ParserState {
|
|
400
|
+
Bom = 0,
|
|
401
|
+
Pre = 1,
|
|
402
|
+
Headers = 2,
|
|
403
|
+
Moves = 3,
|
|
404
|
+
Comment = 4,
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export class PgnError extends Error {}
|
|
408
|
+
|
|
409
|
+
export class PgnParser {
|
|
410
|
+
private lineBuf: string[] = [];
|
|
411
|
+
|
|
412
|
+
private budget: number;
|
|
413
|
+
private found: boolean;
|
|
414
|
+
private state: ParserState;
|
|
415
|
+
private game: Game<PgnNodeData>;
|
|
416
|
+
private stack: ParserFrame[];
|
|
417
|
+
private commentBuf: string[];
|
|
418
|
+
|
|
419
|
+
constructor(
|
|
420
|
+
private emitGame: (game: Game<PgnNodeData>, err: PgnError | undefined) => void,
|
|
421
|
+
private initHeaders: () => Map<string, string> = defaultHeaders,
|
|
422
|
+
private maxBudget = 1_000_000,
|
|
423
|
+
) {
|
|
424
|
+
this.resetGame();
|
|
425
|
+
this.state = ParserState.Bom;
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
private resetGame() {
|
|
429
|
+
this.budget = this.maxBudget;
|
|
430
|
+
this.found = false;
|
|
431
|
+
this.state = ParserState.Pre;
|
|
432
|
+
this.game = defaultGame(this.initHeaders);
|
|
433
|
+
this.stack = [{ parent: this.game.moves, root: true }];
|
|
434
|
+
this.commentBuf = [];
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
private consumeBudget(cost: number) {
|
|
438
|
+
this.budget -= cost;
|
|
439
|
+
if (this.budget < 0) throw new PgnError("ERR_PGN_BUDGET");
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
parse(data: string, options?: ParseOptions): void {
|
|
443
|
+
if (this.budget < 0) return;
|
|
444
|
+
try {
|
|
445
|
+
let idx = 0;
|
|
446
|
+
for (;;) {
|
|
447
|
+
const nlIdx = data.indexOf("\n", idx);
|
|
448
|
+
if (nlIdx === -1) {
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
const crIdx = nlIdx > idx && data[nlIdx - 1] === "\r" ? nlIdx - 1 : nlIdx;
|
|
452
|
+
this.consumeBudget(nlIdx - idx);
|
|
453
|
+
this.lineBuf.push(data.slice(idx, crIdx));
|
|
454
|
+
idx = nlIdx + 1;
|
|
455
|
+
this.handleLine();
|
|
456
|
+
}
|
|
457
|
+
this.consumeBudget(data.length - idx);
|
|
458
|
+
this.lineBuf.push(data.slice(idx));
|
|
459
|
+
|
|
460
|
+
if (!options?.stream) {
|
|
461
|
+
this.handleLine();
|
|
462
|
+
this.emit(undefined);
|
|
463
|
+
}
|
|
464
|
+
} catch (err: unknown) {
|
|
465
|
+
this.emit(err as PgnError);
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
private handleLine() {
|
|
470
|
+
let freshLine = true;
|
|
471
|
+
let line = this.lineBuf.join("");
|
|
472
|
+
this.lineBuf = [];
|
|
473
|
+
|
|
474
|
+
continuedLine: for (;;) {
|
|
475
|
+
switch (this.state) {
|
|
476
|
+
case ParserState.Bom:
|
|
477
|
+
if (line.startsWith(BOM)) line = line.slice(BOM.length);
|
|
478
|
+
this.state = ParserState.Pre; // fall through
|
|
479
|
+
case ParserState.Pre:
|
|
480
|
+
if (isWhitespace(line) || isCommentLine(line)) return;
|
|
481
|
+
this.found = true;
|
|
482
|
+
this.state = ParserState.Headers; // fall through
|
|
483
|
+
case ParserState.Headers: {
|
|
484
|
+
if (isCommentLine(line)) return;
|
|
485
|
+
let moreHeaders = true;
|
|
486
|
+
while (moreHeaders) {
|
|
487
|
+
moreHeaders = false;
|
|
488
|
+
line = line.replace(
|
|
489
|
+
/^\s*\[([A-Za-z0-9][A-Za-z0-9_+#=:-]*)\s+"((?:[^"\\]|\\"|\\\\)*)"\]/,
|
|
490
|
+
(_match, headerName, headerValue) => {
|
|
491
|
+
this.consumeBudget(200);
|
|
492
|
+
this.handleHeader(
|
|
493
|
+
headerName,
|
|
494
|
+
headerValue.replace(/\\"/g, '"').replace(/\\\\/g, "\\"),
|
|
495
|
+
);
|
|
496
|
+
moreHeaders = true;
|
|
497
|
+
freshLine = false;
|
|
498
|
+
return "";
|
|
499
|
+
},
|
|
500
|
+
);
|
|
501
|
+
}
|
|
502
|
+
if (isWhitespace(line)) return;
|
|
503
|
+
this.state = ParserState.Moves; // fall through
|
|
504
|
+
}
|
|
505
|
+
case ParserState.Moves: {
|
|
506
|
+
if (freshLine) {
|
|
507
|
+
if (isCommentLine(line)) return;
|
|
508
|
+
if (isWhitespace(line)) return this.emit(undefined);
|
|
509
|
+
}
|
|
510
|
+
const tokenRegex =
|
|
511
|
+
/(?:[NBKRQ]?[a-h]?[1-8]?[-x]?[a-h][1-8](?:=?[nbrqkNBRQK])?|[pnbrqkPNBRQK]?@[a-h][1-8]|[O0o][-–—][O0o](?:[-–—][O0o])?)[+#]?|--|Z0|0000|@@@@|{|;|\$\d{1,4}|[?!]{1,2}|\(|\)|\*|1[-–—]0|0[-–—]1|1\/2[-–—]1\/2/g;
|
|
512
|
+
let match;
|
|
513
|
+
while ((match = tokenRegex.exec(line))) {
|
|
514
|
+
const frame = this.stack[this.stack.length - 1];
|
|
515
|
+
let token = match[0];
|
|
516
|
+
if (token === ";") return;
|
|
517
|
+
else if (token.startsWith("$")) this.handleNag(parseInt(token.slice(1), 10));
|
|
518
|
+
else if (token === "!") this.handleNag(1);
|
|
519
|
+
else if (token === "?") this.handleNag(2);
|
|
520
|
+
else if (token === "!!") this.handleNag(3);
|
|
521
|
+
else if (token === "??") this.handleNag(4);
|
|
522
|
+
else if (token === "!?") this.handleNag(5);
|
|
523
|
+
else if (token === "?!") this.handleNag(6);
|
|
524
|
+
else if (
|
|
525
|
+
token === "1-0" ||
|
|
526
|
+
token === "1–0" ||
|
|
527
|
+
token === "1—0" ||
|
|
528
|
+
token === "0-1" ||
|
|
529
|
+
token === "0–1" ||
|
|
530
|
+
token === "0—1" ||
|
|
531
|
+
token === "1/2-1/2" ||
|
|
532
|
+
token === "1/2–1/2" ||
|
|
533
|
+
token === "1/2—1/2" ||
|
|
534
|
+
token === "*"
|
|
535
|
+
) {
|
|
536
|
+
if (this.stack.length === 1 && token !== "*") this.handleHeader("Result", token);
|
|
537
|
+
} else if (token === "(") {
|
|
538
|
+
this.consumeBudget(100);
|
|
539
|
+
this.stack.push({ parent: frame.parent, root: false });
|
|
540
|
+
} else if (token === ")") {
|
|
541
|
+
if (this.stack.length > 1) this.stack.pop();
|
|
542
|
+
} else if (token === "{") {
|
|
543
|
+
const openIndex = tokenRegex.lastIndex;
|
|
544
|
+
const beginIndex = line[openIndex] === " " ? openIndex + 1 : openIndex;
|
|
545
|
+
line = line.slice(beginIndex);
|
|
546
|
+
this.state = ParserState.Comment;
|
|
547
|
+
continue continuedLine;
|
|
548
|
+
} else {
|
|
549
|
+
this.consumeBudget(100);
|
|
550
|
+
if (token.startsWith("O") || token.startsWith("0") || token.startsWith("o")) {
|
|
551
|
+
token = token.replace(/[0o]/g, "O").replace(/[–—]/g, "-");
|
|
552
|
+
} else if (token === "Z0" || token === "0000" || token === "@@@@") token = "--";
|
|
553
|
+
|
|
554
|
+
if (frame.node) frame.parent = frame.node;
|
|
555
|
+
frame.node = new ChildNode({
|
|
556
|
+
san: token,
|
|
557
|
+
startingComments: frame.startingComments,
|
|
558
|
+
});
|
|
559
|
+
frame.startingComments = undefined;
|
|
560
|
+
frame.root = false;
|
|
561
|
+
frame.parent.children.push(frame.node);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
case ParserState.Comment: {
|
|
567
|
+
const closeIndex = line.indexOf("}");
|
|
568
|
+
if (closeIndex === -1) {
|
|
569
|
+
this.commentBuf.push(line);
|
|
570
|
+
return;
|
|
571
|
+
} else {
|
|
572
|
+
const endIndex =
|
|
573
|
+
closeIndex > 0 && line[closeIndex - 1] === " " ? closeIndex - 1 : closeIndex;
|
|
574
|
+
this.commentBuf.push(line.slice(0, endIndex));
|
|
575
|
+
this.handleComment();
|
|
576
|
+
line = line.slice(closeIndex);
|
|
577
|
+
this.state = ParserState.Moves;
|
|
578
|
+
freshLine = false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
private handleHeader(name: string, value: string) {
|
|
586
|
+
this.game.headers.set(name, name === "Result" ? makeOutcome(parseOutcome(value)) : value);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
private handleNag(nag: number) {
|
|
590
|
+
this.consumeBudget(50);
|
|
591
|
+
const frame = this.stack[this.stack.length - 1];
|
|
592
|
+
if (frame.node) {
|
|
593
|
+
frame.node.data.nags ||= [];
|
|
594
|
+
frame.node.data.nags.push(nag);
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
private handleComment() {
|
|
599
|
+
this.consumeBudget(100);
|
|
600
|
+
const frame = this.stack[this.stack.length - 1];
|
|
601
|
+
const comment = this.commentBuf.join("\n");
|
|
602
|
+
this.commentBuf = [];
|
|
603
|
+
if (frame.node) {
|
|
604
|
+
frame.node.data.comments ||= [];
|
|
605
|
+
frame.node.data.comments.push(comment);
|
|
606
|
+
} else if (frame.root) {
|
|
607
|
+
this.game.comments ||= [];
|
|
608
|
+
this.game.comments.push(comment);
|
|
609
|
+
} else {
|
|
610
|
+
frame.startingComments ||= [];
|
|
611
|
+
frame.startingComments.push(comment);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
private emit(err: PgnError | undefined) {
|
|
616
|
+
if (this.state === ParserState.Comment) this.handleComment();
|
|
617
|
+
if (err) return this.emitGame(this.game, err);
|
|
618
|
+
if (this.found) this.emitGame(this.game, undefined);
|
|
619
|
+
this.resetGame();
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export const parsePgn = (
|
|
624
|
+
pgn: string,
|
|
625
|
+
initHeaders: () => Map<string, string> = defaultHeaders,
|
|
626
|
+
): Game<PgnNodeData>[] => {
|
|
627
|
+
const games: Game<PgnNodeData>[] = [];
|
|
628
|
+
new PgnParser((game) => games.push(game), initHeaders, NaN).parse(pgn);
|
|
629
|
+
return games;
|
|
630
|
+
};
|
|
631
|
+
|
|
632
|
+
export const parseVariant = (variant: string | undefined): Rules | undefined => {
|
|
633
|
+
switch ((variant || "chess").toLowerCase()) {
|
|
634
|
+
case "chess":
|
|
635
|
+
case "chess960":
|
|
636
|
+
case "chess 960":
|
|
637
|
+
case "standard":
|
|
638
|
+
case "from position":
|
|
639
|
+
case "classical":
|
|
640
|
+
case "normal":
|
|
641
|
+
case "fischerandom": // Cute Chess
|
|
642
|
+
case "fischerrandom":
|
|
643
|
+
case "fischer random":
|
|
644
|
+
case "wild/0":
|
|
645
|
+
case "wild/1":
|
|
646
|
+
case "wild/2":
|
|
647
|
+
case "wild/3":
|
|
648
|
+
case "wild/4":
|
|
649
|
+
case "wild/5":
|
|
650
|
+
case "wild/6":
|
|
651
|
+
case "wild/7":
|
|
652
|
+
case "wild/8":
|
|
653
|
+
case "wild/8a":
|
|
654
|
+
return "chess";
|
|
655
|
+
case "crazyhouse":
|
|
656
|
+
case "crazy house":
|
|
657
|
+
case "house":
|
|
658
|
+
case "zh":
|
|
659
|
+
return "crazyhouse";
|
|
660
|
+
case "king of the hill":
|
|
661
|
+
case "koth":
|
|
662
|
+
case "kingofthehill":
|
|
663
|
+
return "kingofthehill";
|
|
664
|
+
case "three-check":
|
|
665
|
+
case "three check":
|
|
666
|
+
case "threecheck":
|
|
667
|
+
case "three check chess":
|
|
668
|
+
case "3-check":
|
|
669
|
+
case "3 check":
|
|
670
|
+
case "3check":
|
|
671
|
+
return "3check";
|
|
672
|
+
case "antichess":
|
|
673
|
+
case "anti chess":
|
|
674
|
+
case "anti":
|
|
675
|
+
return "antichess";
|
|
676
|
+
case "atomic":
|
|
677
|
+
case "atom":
|
|
678
|
+
case "atomic chess":
|
|
679
|
+
return "atomic";
|
|
680
|
+
case "horde":
|
|
681
|
+
case "horde chess":
|
|
682
|
+
return "horde";
|
|
683
|
+
case "racing kings":
|
|
684
|
+
case "racingkings":
|
|
685
|
+
case "racing":
|
|
686
|
+
case "race":
|
|
687
|
+
return "racingkings";
|
|
688
|
+
default:
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
export const makeVariant = (rules: Rules): string | undefined => {
|
|
694
|
+
switch (rules) {
|
|
695
|
+
case "chess":
|
|
696
|
+
return;
|
|
697
|
+
case "crazyhouse":
|
|
698
|
+
return "Crazyhouse";
|
|
699
|
+
case "racingkings":
|
|
700
|
+
return "Racing Kings";
|
|
701
|
+
case "horde":
|
|
702
|
+
return "Horde";
|
|
703
|
+
case "atomic":
|
|
704
|
+
return "Atomic";
|
|
705
|
+
case "antichess":
|
|
706
|
+
return "Antichess";
|
|
707
|
+
case "3check":
|
|
708
|
+
return "Three-check";
|
|
709
|
+
case "kingofthehill":
|
|
710
|
+
return "King of the Hill";
|
|
711
|
+
}
|
|
712
|
+
};
|
|
713
|
+
|
|
714
|
+
export const startingPosition = (
|
|
715
|
+
headers: Map<string, string>,
|
|
716
|
+
): Result<Position, FenError | PositionError> => {
|
|
717
|
+
const rules = parseVariant(headers.get("Variant"));
|
|
718
|
+
if (!rules) return Result.err(new PositionError(IllegalSetup.Variant));
|
|
719
|
+
const fen = headers.get("FEN");
|
|
720
|
+
if (fen) return parseFen(fen).chain((setup) => setupPosition(rules, setup));
|
|
721
|
+
else return Result.ok(defaultPosition(rules));
|
|
722
|
+
};
|
|
723
|
+
|
|
724
|
+
export const setStartingPosition = (headers: Map<string, string>, pos: Position) => {
|
|
725
|
+
const variant = makeVariant(pos.rules);
|
|
726
|
+
if (variant) headers.set("Variant", variant);
|
|
727
|
+
else headers.delete("Variant");
|
|
728
|
+
|
|
729
|
+
const fen = makeFen(pos.toSetup());
|
|
730
|
+
const defaultFen = makeFen(defaultPosition(pos.rules).toSetup());
|
|
731
|
+
if (fen !== defaultFen) headers.set("FEN", fen);
|
|
732
|
+
else headers.delete("FEN");
|
|
733
|
+
};
|
|
734
|
+
|
|
735
|
+
export type CommentShapeColor = "green" | "red" | "yellow" | "blue";
|
|
736
|
+
|
|
737
|
+
export interface CommentShape {
|
|
738
|
+
color: CommentShapeColor;
|
|
739
|
+
from: Square;
|
|
740
|
+
to: Square;
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
export type EvaluationPawns = { pawns: number; depth?: number };
|
|
744
|
+
export type EvaluationMate = { mate: number; depth?: number };
|
|
745
|
+
export type Evaluation = EvaluationPawns | EvaluationMate;
|
|
746
|
+
|
|
747
|
+
export const isPawns = (ev: Evaluation): ev is EvaluationPawns => "pawns" in ev;
|
|
748
|
+
export const isMate = (ev: Evaluation): ev is EvaluationMate => "mate" in ev;
|
|
749
|
+
|
|
750
|
+
export interface Comment {
|
|
751
|
+
text: string;
|
|
752
|
+
shapes: CommentShape[];
|
|
753
|
+
clock?: number;
|
|
754
|
+
emt?: number;
|
|
755
|
+
evaluation?: Evaluation;
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
const makeClk = (seconds: number): string => {
|
|
759
|
+
seconds = Math.max(0, seconds);
|
|
760
|
+
const hours = Math.floor(seconds / 3600);
|
|
761
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
762
|
+
seconds = (seconds % 3600) % 60;
|
|
763
|
+
return `${hours}:${minutes.toString().padStart(2, "0")}:${seconds.toLocaleString("en", {
|
|
764
|
+
minimumIntegerDigits: 2,
|
|
765
|
+
maximumFractionDigits: 3,
|
|
766
|
+
})}`;
|
|
767
|
+
};
|
|
768
|
+
|
|
769
|
+
const makeCommentShapeColor = (color: CommentShapeColor): "G" | "R" | "Y" | "B" => {
|
|
770
|
+
switch (color) {
|
|
771
|
+
case "green":
|
|
772
|
+
return "G";
|
|
773
|
+
case "red":
|
|
774
|
+
return "R";
|
|
775
|
+
case "yellow":
|
|
776
|
+
return "Y";
|
|
777
|
+
case "blue":
|
|
778
|
+
return "B";
|
|
779
|
+
}
|
|
780
|
+
};
|
|
781
|
+
|
|
782
|
+
function parseCommentShapeColor(str: "G" | "R" | "Y" | "B"): CommentShapeColor;
|
|
783
|
+
function parseCommentShapeColor(str: string): CommentShapeColor | undefined;
|
|
784
|
+
function parseCommentShapeColor(str: string): CommentShapeColor | undefined {
|
|
785
|
+
switch (str) {
|
|
786
|
+
case "G":
|
|
787
|
+
return "green";
|
|
788
|
+
case "R":
|
|
789
|
+
return "red";
|
|
790
|
+
case "Y":
|
|
791
|
+
return "yellow";
|
|
792
|
+
case "B":
|
|
793
|
+
return "blue";
|
|
794
|
+
default:
|
|
795
|
+
return;
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
const makeCommentShape = (shape: CommentShape): string =>
|
|
800
|
+
shape.to === shape.from
|
|
801
|
+
? `${makeCommentShapeColor(shape.color)}${makeSquare(shape.to)}`
|
|
802
|
+
: `${makeCommentShapeColor(shape.color)}${makeSquare(shape.from)}${makeSquare(shape.to)}`;
|
|
803
|
+
|
|
804
|
+
const parseCommentShape = (str: string): CommentShape | undefined => {
|
|
805
|
+
const color = parseCommentShapeColor(str.slice(0, 1));
|
|
806
|
+
const from = parseSquare(str.slice(1, 3));
|
|
807
|
+
const to = parseSquare(str.slice(3, 5));
|
|
808
|
+
if (!color || !defined(from)) return;
|
|
809
|
+
if (str.length === 3) return { color, from, to: from };
|
|
810
|
+
if (str.length === 5 && defined(to)) return { color, from, to };
|
|
811
|
+
return;
|
|
812
|
+
};
|
|
813
|
+
|
|
814
|
+
const makeEval = (ev: Evaluation): string => {
|
|
815
|
+
const str = isMate(ev) ? "#" + ev.mate : ev.pawns.toFixed(2);
|
|
816
|
+
return defined(ev.depth) ? str + "," + ev.depth : str;
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
export const makeComment = (comment: Partial<Comment>): string => {
|
|
820
|
+
const builder = [];
|
|
821
|
+
if (defined(comment.text)) builder.push(comment.text);
|
|
822
|
+
const circles = (comment.shapes || [])
|
|
823
|
+
.filter((shape) => shape.to === shape.from)
|
|
824
|
+
.map(makeCommentShape);
|
|
825
|
+
if (circles.length) builder.push(`[%csl ${circles.join(",")}]`);
|
|
826
|
+
const arrows = (comment.shapes || [])
|
|
827
|
+
.filter((shape) => shape.to !== shape.from)
|
|
828
|
+
.map(makeCommentShape);
|
|
829
|
+
if (arrows.length) builder.push(`[%cal ${arrows.join(",")}]`);
|
|
830
|
+
if (comment.evaluation) builder.push(`[%eval ${makeEval(comment.evaluation)}]`);
|
|
831
|
+
if (defined(comment.emt)) builder.push(`[%emt ${makeClk(comment.emt)}]`);
|
|
832
|
+
if (defined(comment.clock)) builder.push(`[%clk ${makeClk(comment.clock)}]`);
|
|
833
|
+
return builder.join(" ");
|
|
834
|
+
};
|
|
835
|
+
|
|
836
|
+
export const parseComment = (comment: string): Comment => {
|
|
837
|
+
let emt, clock, evaluation;
|
|
838
|
+
const shapes: CommentShape[] = [];
|
|
839
|
+
const text = comment
|
|
840
|
+
.replace(
|
|
841
|
+
/\s?\[%(emt|clk)\s(\d{1,5}):(\d{1,2}):(\d{1,2}(?:\.\d{0,3})?)\]\s?/g,
|
|
842
|
+
(_, annotation, hours, minutes, seconds) => {
|
|
843
|
+
const value = parseInt(hours, 10) * 3600 + parseInt(minutes, 10) * 60 + parseFloat(seconds);
|
|
844
|
+
if (annotation === "emt") emt = value;
|
|
845
|
+
else if (annotation === "clk") clock = value;
|
|
846
|
+
return " ";
|
|
847
|
+
},
|
|
848
|
+
)
|
|
849
|
+
.replace(
|
|
850
|
+
/\s?\[%(?:csl|cal)\s([RGYB][a-h][1-8](?:[a-h][1-8])?(?:,[RGYB][a-h][1-8](?:[a-h][1-8])?)*)\]\s?/g,
|
|
851
|
+
(_, arrows) => {
|
|
852
|
+
for (const arrow of arrows.split(",")) {
|
|
853
|
+
shapes.push(parseCommentShape(arrow)!);
|
|
854
|
+
}
|
|
855
|
+
return " ";
|
|
856
|
+
},
|
|
857
|
+
)
|
|
858
|
+
.replace(
|
|
859
|
+
/\s?\[%eval\s(?:#([+-]?\d{1,5})|([+-]?(?:\d{1,5}|\d{0,5}\.\d{1,2})))(?:,(\d{1,5}))?\]\s?/g,
|
|
860
|
+
(_, mate, pawns, d) => {
|
|
861
|
+
const depth = d && parseInt(d, 10);
|
|
862
|
+
evaluation = mate
|
|
863
|
+
? { mate: parseInt(mate, 10), depth }
|
|
864
|
+
: { pawns: parseFloat(pawns), depth };
|
|
865
|
+
return " ";
|
|
866
|
+
},
|
|
867
|
+
)
|
|
868
|
+
.trim();
|
|
869
|
+
return {
|
|
870
|
+
text,
|
|
871
|
+
shapes,
|
|
872
|
+
emt,
|
|
873
|
+
clock,
|
|
874
|
+
evaluation,
|
|
875
|
+
};
|
|
876
|
+
};
|