@alepot55/chessboardjs 1.1.0 → 2.0.3

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/chess.js ADDED
@@ -0,0 +1,1994 @@
1
+ /**
2
+ * @license
3
+ * Copyright (c) 2025, Jeff Hlywa (jhlywa@gmail.com)
4
+ * All rights reserved.
5
+ *
6
+ * Redistribution and use in source and binary forms, with or without
7
+ * modification, are permitted provided that the following conditions are met:
8
+ *
9
+ * 1. Redistributions of source code must retain the above copyright notice,
10
+ * this list of conditions and the following disclaimer.
11
+ * 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ * this list of conditions and the following disclaimer in the documentation
13
+ * and/or other materials provided with the distribution.
14
+ *
15
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
16
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
17
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
18
+ * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
19
+ * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
20
+ * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
21
+ * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
22
+ * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
23
+ * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
24
+ * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
25
+ * POSSIBILITY OF SUCH DAMAGE.
26
+ */
27
+ export const WHITE = 'w';
28
+ export const BLACK = 'b';
29
+ export const PAWN = 'p';
30
+ export const KNIGHT = 'n';
31
+ export const BISHOP = 'b';
32
+ export const ROOK = 'r';
33
+ export const QUEEN = 'q';
34
+ export const KING = 'k';
35
+ export const DEFAULT_POSITION = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
36
+ export class Move {
37
+ color;
38
+ from;
39
+ to;
40
+ piece;
41
+ captured;
42
+ promotion;
43
+ /**
44
+ * @deprecated This field is deprecated and will be removed in version 2.0.0.
45
+ * Please use move descriptor functions instead: `isCapture`, `isPromotion`,
46
+ * `isEnPassant`, `isKingsideCastle`, `isQueensideCastle`, `isCastle`, and
47
+ * `isBigPawn`
48
+ */
49
+ flags;
50
+ san;
51
+ lan;
52
+ before;
53
+ after;
54
+ constructor(chess, internal) {
55
+ const { color, piece, from, to, flags, captured, promotion } = internal;
56
+ const fromAlgebraic = algebraic(from);
57
+ const toAlgebraic = algebraic(to);
58
+ this.color = color;
59
+ this.piece = piece;
60
+ this.from = fromAlgebraic;
61
+ this.to = toAlgebraic;
62
+ /*
63
+ * HACK: The chess['_method']() calls below invoke private methods in the
64
+ * Chess class to generate SAN and FEN. It's a bit of a hack, but makes the
65
+ * code cleaner elsewhere.
66
+ */
67
+ this.san = chess['_moveToSan'](internal, chess['_moves']({ legal: true }));
68
+ this.lan = fromAlgebraic + toAlgebraic;
69
+ this.before = chess.fen();
70
+ // Generate the FEN for the 'after' key
71
+ chess['_makeMove'](internal);
72
+ this.after = chess.fen();
73
+ chess['_undoMove']();
74
+ // Build the text representation of the move flags
75
+ this.flags = '';
76
+ for (const flag in BITS) {
77
+ if (BITS[flag] & flags) {
78
+ this.flags += FLAGS[flag];
79
+ }
80
+ }
81
+ if (captured) {
82
+ this.captured = captured;
83
+ }
84
+ if (promotion) {
85
+ this.promotion = promotion;
86
+ this.lan += promotion;
87
+ }
88
+ }
89
+ isCapture() {
90
+ return this.flags.indexOf(FLAGS['CAPTURE']) > -1;
91
+ }
92
+ isPromotion() {
93
+ return this.flags.indexOf(FLAGS['PROMOTION']) > -1;
94
+ }
95
+ isEnPassant() {
96
+ return this.flags.indexOf(FLAGS['EP_CAPTURE']) > -1;
97
+ }
98
+ isKingsideCastle() {
99
+ return this.flags.indexOf(FLAGS['KSIDE_CASTLE']) > -1;
100
+ }
101
+ isQueensideCastle() {
102
+ return this.flags.indexOf(FLAGS['QSIDE_CASTLE']) > -1;
103
+ }
104
+ isBigPawn() {
105
+ return this.flags.indexOf(FLAGS['BIG_PAWN']) > -1;
106
+ }
107
+ }
108
+ const EMPTY = -1;
109
+ const FLAGS = {
110
+ NORMAL: 'n',
111
+ CAPTURE: 'c',
112
+ BIG_PAWN: 'b',
113
+ EP_CAPTURE: 'e',
114
+ PROMOTION: 'p',
115
+ KSIDE_CASTLE: 'k',
116
+ QSIDE_CASTLE: 'q',
117
+ };
118
+ // prettier-ignore
119
+ export const SQUARES = [
120
+ 'a8', 'b8', 'c8', 'd8', 'e8', 'f8', 'g8', 'h8',
121
+ 'a7', 'b7', 'c7', 'd7', 'e7', 'f7', 'g7', 'h7',
122
+ 'a6', 'b6', 'c6', 'd6', 'e6', 'f6', 'g6', 'h6',
123
+ 'a5', 'b5', 'c5', 'd5', 'e5', 'f5', 'g5', 'h5',
124
+ 'a4', 'b4', 'c4', 'd4', 'e4', 'f4', 'g4', 'h4',
125
+ 'a3', 'b3', 'c3', 'd3', 'e3', 'f3', 'g3', 'h3',
126
+ 'a2', 'b2', 'c2', 'd2', 'e2', 'f2', 'g2', 'h2',
127
+ 'a1', 'b1', 'c1', 'd1', 'e1', 'f1', 'g1', 'h1'
128
+ ];
129
+ const BITS = {
130
+ NORMAL: 1,
131
+ CAPTURE: 2,
132
+ BIG_PAWN: 4,
133
+ EP_CAPTURE: 8,
134
+ PROMOTION: 16,
135
+ KSIDE_CASTLE: 32,
136
+ QSIDE_CASTLE: 64,
137
+ };
138
+ /*
139
+ * NOTES ABOUT 0x88 MOVE GENERATION ALGORITHM
140
+ * ----------------------------------------------------------------------------
141
+ * From https://github.com/jhlywa/chess.js/issues/230
142
+ *
143
+ * A lot of people are confused when they first see the internal representation
144
+ * of chess.js. It uses the 0x88 Move Generation Algorithm which internally
145
+ * stores the board as an 8x16 array. This is purely for efficiency but has a
146
+ * couple of interesting benefits:
147
+ *
148
+ * 1. 0x88 offers a very inexpensive "off the board" check. Bitwise AND (&) any
149
+ * square with 0x88, if the result is non-zero then the square is off the
150
+ * board. For example, assuming a knight square A8 (0 in 0x88 notation),
151
+ * there are 8 possible directions in which the knight can move. These
152
+ * directions are relative to the 8x16 board and are stored in the
153
+ * PIECE_OFFSETS map. One possible move is A8 - 18 (up one square, and two
154
+ * squares to the left - which is off the board). 0 - 18 = -18 & 0x88 = 0x88
155
+ * (because of two-complement representation of -18). The non-zero result
156
+ * means the square is off the board and the move is illegal. Take the
157
+ * opposite move (from A8 to C7), 0 + 18 = 18 & 0x88 = 0. A result of zero
158
+ * means the square is on the board.
159
+ *
160
+ * 2. The relative distance (or difference) between two squares on a 8x16 board
161
+ * is unique and can be used to inexpensively determine if a piece on a
162
+ * square can attack any other arbitrary square. For example, let's see if a
163
+ * pawn on E7 can attack E2. The difference between E7 (20) - E2 (100) is
164
+ * -80. We add 119 to make the ATTACKS array index non-negative (because the
165
+ * worst case difference is A8 - H1 = -119). The ATTACKS array contains a
166
+ * bitmask of pieces that can attack from that distance and direction.
167
+ * ATTACKS[-80 + 119=39] gives us 24 or 0b11000 in binary. Look at the
168
+ * PIECE_MASKS map to determine the mask for a given piece type. In our pawn
169
+ * example, we would check to see if 24 & 0x1 is non-zero, which it is
170
+ * not. So, naturally, a pawn on E7 can't attack a piece on E2. However, a
171
+ * rook can since 24 & 0x8 is non-zero. The only thing left to check is that
172
+ * there are no blocking pieces between E7 and E2. That's where the RAYS
173
+ * array comes in. It provides an offset (in this case 16) to add to E7 (20)
174
+ * to check for blocking pieces. E7 (20) + 16 = E6 (36) + 16 = E5 (52) etc.
175
+ */
176
+ // prettier-ignore
177
+ // eslint-disable-next-line
178
+ const Ox88 = {
179
+ a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7,
180
+ a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23,
181
+ a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39,
182
+ a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55,
183
+ a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71,
184
+ a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87,
185
+ a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103,
186
+ a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119
187
+ };
188
+ const PAWN_OFFSETS = {
189
+ b: [16, 32, 17, 15],
190
+ w: [-16, -32, -17, -15],
191
+ };
192
+ const PIECE_OFFSETS = {
193
+ n: [-18, -33, -31, -14, 18, 33, 31, 14],
194
+ b: [-17, -15, 17, 15],
195
+ r: [-16, 1, 16, -1],
196
+ q: [-17, -16, -15, 1, 17, 16, 15, -1],
197
+ k: [-17, -16, -15, 1, 17, 16, 15, -1],
198
+ };
199
+ // prettier-ignore
200
+ const ATTACKS = [
201
+ 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0,
202
+ 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0,
203
+ 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0,
204
+ 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0,
205
+ 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0,
206
+ 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0,
207
+ 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
208
+ 24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24, 0,
209
+ 0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
210
+ 0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0,
211
+ 0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0,
212
+ 0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0,
213
+ 0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0,
214
+ 0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0,
215
+ 20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20
216
+ ];
217
+ // prettier-ignore
218
+ const RAYS = [
219
+ 17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0,
220
+ 0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0,
221
+ 0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0,
222
+ 0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0,
223
+ 0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0,
224
+ 0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0,
225
+ 0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0,
226
+ 1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0,
227
+ 0, 0, 0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0,
228
+ 0, 0, 0, 0, 0, -15, 0, -16, 0, -17, 0, 0, 0, 0, 0, 0,
229
+ 0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17, 0, 0, 0, 0, 0,
230
+ 0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0,
231
+ 0, 0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0,
232
+ 0, -15, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -17, 0, 0,
233
+ -15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -17
234
+ ];
235
+ const PIECE_MASKS = { p: 0x1, n: 0x2, b: 0x4, r: 0x8, q: 0x10, k: 0x20 };
236
+ const SYMBOLS = 'pnbrqkPNBRQK';
237
+ const PROMOTIONS = [KNIGHT, BISHOP, ROOK, QUEEN];
238
+ const RANK_1 = 7;
239
+ const RANK_2 = 6;
240
+ /*
241
+ * const RANK_3 = 5
242
+ * const RANK_4 = 4
243
+ * const RANK_5 = 3
244
+ * const RANK_6 = 2
245
+ */
246
+ const RANK_7 = 1;
247
+ const RANK_8 = 0;
248
+ const SIDES = {
249
+ [KING]: BITS.KSIDE_CASTLE,
250
+ [QUEEN]: BITS.QSIDE_CASTLE,
251
+ };
252
+ const ROOKS = {
253
+ w: [
254
+ { square: Ox88.a1, flag: BITS.QSIDE_CASTLE },
255
+ { square: Ox88.h1, flag: BITS.KSIDE_CASTLE },
256
+ ],
257
+ b: [
258
+ { square: Ox88.a8, flag: BITS.QSIDE_CASTLE },
259
+ { square: Ox88.h8, flag: BITS.KSIDE_CASTLE },
260
+ ],
261
+ };
262
+ const SECOND_RANK = { b: RANK_7, w: RANK_2 };
263
+ const TERMINATION_MARKERS = ['1-0', '0-1', '1/2-1/2', '*'];
264
+ // Extracts the zero-based rank of an 0x88 square.
265
+ function rank(square) {
266
+ return square >> 4;
267
+ }
268
+ // Extracts the zero-based file of an 0x88 square.
269
+ function file(square) {
270
+ return square & 0xf;
271
+ }
272
+ function isDigit(c) {
273
+ return '0123456789'.indexOf(c) !== -1;
274
+ }
275
+ // Converts a 0x88 square to algebraic notation.
276
+ function algebraic(square) {
277
+ const f = file(square);
278
+ const r = rank(square);
279
+ return ('abcdefgh'.substring(f, f + 1) +
280
+ '87654321'.substring(r, r + 1));
281
+ }
282
+ function swapColor(color) {
283
+ return color === WHITE ? BLACK : WHITE;
284
+ }
285
+ export function validateFen(fen) {
286
+ // 1st criterion: 6 space-seperated fields?
287
+ const tokens = fen.split(/\s+/);
288
+ if (tokens.length !== 6) {
289
+ return {
290
+ ok: false,
291
+ error: 'Invalid FEN: must contain six space-delimited fields',
292
+ };
293
+ }
294
+ // 2nd criterion: move number field is a integer value > 0?
295
+ const moveNumber = parseInt(tokens[5], 10);
296
+ if (isNaN(moveNumber) || moveNumber <= 0) {
297
+ return {
298
+ ok: false,
299
+ error: 'Invalid FEN: move number must be a positive integer',
300
+ };
301
+ }
302
+ // 3rd criterion: half move counter is an integer >= 0?
303
+ const halfMoves = parseInt(tokens[4], 10);
304
+ if (isNaN(halfMoves) || halfMoves < 0) {
305
+ return {
306
+ ok: false,
307
+ error: 'Invalid FEN: half move counter number must be a non-negative integer',
308
+ };
309
+ }
310
+ // 4th criterion: 4th field is a valid e.p.-string?
311
+ if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) {
312
+ return { ok: false, error: 'Invalid FEN: en-passant square is invalid' };
313
+ }
314
+ // 5th criterion: 3th field is a valid castle-string?
315
+ if (/[^kKqQ-]/.test(tokens[2])) {
316
+ return { ok: false, error: 'Invalid FEN: castling availability is invalid' };
317
+ }
318
+ // 6th criterion: 2nd field is "w" (white) or "b" (black)?
319
+ if (!/^(w|b)$/.test(tokens[1])) {
320
+ return { ok: false, error: 'Invalid FEN: side-to-move is invalid' };
321
+ }
322
+ // 7th criterion: 1st field contains 8 rows?
323
+ const rows = tokens[0].split('/');
324
+ if (rows.length !== 8) {
325
+ return {
326
+ ok: false,
327
+ error: "Invalid FEN: piece data does not contain 8 '/'-delimited rows",
328
+ };
329
+ }
330
+ // 8th criterion: every row is valid?
331
+ for (let i = 0; i < rows.length; i++) {
332
+ // check for right sum of fields AND not two numbers in succession
333
+ let sumFields = 0;
334
+ let previousWasNumber = false;
335
+ for (let k = 0; k < rows[i].length; k++) {
336
+ if (isDigit(rows[i][k])) {
337
+ if (previousWasNumber) {
338
+ return {
339
+ ok: false,
340
+ error: 'Invalid FEN: piece data is invalid (consecutive number)',
341
+ };
342
+ }
343
+ sumFields += parseInt(rows[i][k], 10);
344
+ previousWasNumber = true;
345
+ }
346
+ else {
347
+ if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) {
348
+ return {
349
+ ok: false,
350
+ error: 'Invalid FEN: piece data is invalid (invalid piece)',
351
+ };
352
+ }
353
+ sumFields += 1;
354
+ previousWasNumber = false;
355
+ }
356
+ }
357
+ if (sumFields !== 8) {
358
+ return {
359
+ ok: false,
360
+ error: 'Invalid FEN: piece data is invalid (too many squares in rank)',
361
+ };
362
+ }
363
+ }
364
+ // 9th criterion: is en-passant square legal?
365
+ if ((tokens[3][1] == '3' && tokens[1] == 'w') ||
366
+ (tokens[3][1] == '6' && tokens[1] == 'b')) {
367
+ return { ok: false, error: 'Invalid FEN: illegal en-passant square' };
368
+ }
369
+ // 10th criterion: does chess position contain exact two kings?
370
+ const kings = [
371
+ { color: 'white', regex: /K/g },
372
+ { color: 'black', regex: /k/g },
373
+ ];
374
+ for (const { color, regex } of kings) {
375
+ if (!regex.test(tokens[0])) {
376
+ return { ok: false, error: `Invalid FEN: missing ${color} king` };
377
+ }
378
+ if ((tokens[0].match(regex) || []).length > 1) {
379
+ return { ok: false, error: `Invalid FEN: too many ${color} kings` };
380
+ }
381
+ }
382
+ // 11th criterion: are any pawns on the first or eighth rows?
383
+ if (Array.from(rows[0] + rows[7]).some((char) => char.toUpperCase() === 'P')) {
384
+ return {
385
+ ok: false,
386
+ error: 'Invalid FEN: some pawns are on the edge rows',
387
+ };
388
+ }
389
+ return { ok: true };
390
+ }
391
+ // this function is used to uniquely identify ambiguous moves
392
+ function getDisambiguator(move, moves) {
393
+ const from = move.from;
394
+ const to = move.to;
395
+ const piece = move.piece;
396
+ let ambiguities = 0;
397
+ let sameRank = 0;
398
+ let sameFile = 0;
399
+ for (let i = 0, len = moves.length; i < len; i++) {
400
+ const ambigFrom = moves[i].from;
401
+ const ambigTo = moves[i].to;
402
+ const ambigPiece = moves[i].piece;
403
+ /*
404
+ * if a move of the same piece type ends on the same to square, we'll need
405
+ * to add a disambiguator to the algebraic notation
406
+ */
407
+ if (piece === ambigPiece && from !== ambigFrom && to === ambigTo) {
408
+ ambiguities++;
409
+ if (rank(from) === rank(ambigFrom)) {
410
+ sameRank++;
411
+ }
412
+ if (file(from) === file(ambigFrom)) {
413
+ sameFile++;
414
+ }
415
+ }
416
+ }
417
+ if (ambiguities > 0) {
418
+ if (sameRank > 0 && sameFile > 0) {
419
+ /*
420
+ * if there exists a similar moving piece on the same rank and file as
421
+ * the move in question, use the square as the disambiguator
422
+ */
423
+ return algebraic(from);
424
+ }
425
+ else if (sameFile > 0) {
426
+ /*
427
+ * if the moving piece rests on the same file, use the rank symbol as the
428
+ * disambiguator
429
+ */
430
+ return algebraic(from).charAt(1);
431
+ }
432
+ else {
433
+ // else use the file symbol
434
+ return algebraic(from).charAt(0);
435
+ }
436
+ }
437
+ return '';
438
+ }
439
+ function addMove(moves, color, from, to, piece, captured = undefined, flags = BITS.NORMAL) {
440
+ const r = rank(to);
441
+ if (piece === PAWN && (r === RANK_1 || r === RANK_8)) {
442
+ for (let i = 0; i < PROMOTIONS.length; i++) {
443
+ const promotion = PROMOTIONS[i];
444
+ moves.push({
445
+ color,
446
+ from,
447
+ to,
448
+ piece,
449
+ captured,
450
+ promotion,
451
+ flags: flags | BITS.PROMOTION,
452
+ });
453
+ }
454
+ }
455
+ else {
456
+ moves.push({
457
+ color,
458
+ from,
459
+ to,
460
+ piece,
461
+ captured,
462
+ flags,
463
+ });
464
+ }
465
+ }
466
+ function inferPieceType(san) {
467
+ let pieceType = san.charAt(0);
468
+ if (pieceType >= 'a' && pieceType <= 'h') {
469
+ const matches = san.match(/[a-h]\d.*[a-h]\d/);
470
+ if (matches) {
471
+ return undefined;
472
+ }
473
+ return PAWN;
474
+ }
475
+ pieceType = pieceType.toLowerCase();
476
+ if (pieceType === 'o') {
477
+ return KING;
478
+ }
479
+ return pieceType;
480
+ }
481
+ // parses all of the decorators out of a SAN string
482
+ function strippedSan(move) {
483
+ return move.replace(/=/, '').replace(/[+#]?[?!]*$/, '');
484
+ }
485
+ function trimFen(fen) {
486
+ /*
487
+ * remove last two fields in FEN string as they're not needed when checking
488
+ * for repetition
489
+ */
490
+ return fen.split(' ').slice(0, 4).join(' ');
491
+ }
492
+ export class Chess {
493
+ _board = new Array(128);
494
+ _turn = WHITE;
495
+ _header = {};
496
+ _kings = { w: EMPTY, b: EMPTY };
497
+ _epSquare = -1;
498
+ _halfMoves = 0;
499
+ _moveNumber = 0;
500
+ _history = [];
501
+ _comments = {};
502
+ _castling = { w: 0, b: 0 };
503
+ // tracks number of times a position has been seen for repetition checking
504
+ _positionCount = {};
505
+ constructor(fen = DEFAULT_POSITION) {
506
+ this.load(fen);
507
+ }
508
+ clear({ preserveHeaders = false } = {}) {
509
+ this._board = new Array(128);
510
+ this._kings = { w: EMPTY, b: EMPTY };
511
+ this._turn = WHITE;
512
+ this._castling = { w: 0, b: 0 };
513
+ this._epSquare = EMPTY;
514
+ this._halfMoves = 0;
515
+ this._moveNumber = 1;
516
+ this._history = [];
517
+ this._comments = {};
518
+ this._header = preserveHeaders ? this._header : {};
519
+ this._positionCount = {};
520
+ /*
521
+ * Delete the SetUp and FEN headers (if preserved), the board is empty and
522
+ * these headers don't make sense in this state. They'll get added later
523
+ * via .load() or .put()
524
+ */
525
+ delete this._header['SetUp'];
526
+ delete this._header['FEN'];
527
+ }
528
+ load(fen, { skipValidation = false, preserveHeaders = false } = {}) {
529
+ let tokens = fen.split(/\s+/);
530
+ // append commonly omitted fen tokens
531
+ if (tokens.length >= 2 && tokens.length < 6) {
532
+ const adjustments = ['-', '-', '0', '1'];
533
+ fen = tokens.concat(adjustments.slice(-(6 - tokens.length))).join(' ');
534
+ }
535
+ tokens = fen.split(/\s+/);
536
+ if (!skipValidation) {
537
+ const { ok, error } = validateFen(fen);
538
+ if (!ok) {
539
+ throw new Error(error);
540
+ }
541
+ }
542
+ const position = tokens[0];
543
+ let square = 0;
544
+ this.clear({ preserveHeaders });
545
+ for (let i = 0; i < position.length; i++) {
546
+ const piece = position.charAt(i);
547
+ if (piece === '/') {
548
+ square += 8;
549
+ }
550
+ else if (isDigit(piece)) {
551
+ square += parseInt(piece, 10);
552
+ }
553
+ else {
554
+ const color = piece < 'a' ? WHITE : BLACK;
555
+ this._put({ type: piece.toLowerCase(), color }, algebraic(square));
556
+ square++;
557
+ }
558
+ }
559
+ this._turn = tokens[1];
560
+ if (tokens[2].indexOf('K') > -1) {
561
+ this._castling.w |= BITS.KSIDE_CASTLE;
562
+ }
563
+ if (tokens[2].indexOf('Q') > -1) {
564
+ this._castling.w |= BITS.QSIDE_CASTLE;
565
+ }
566
+ if (tokens[2].indexOf('k') > -1) {
567
+ this._castling.b |= BITS.KSIDE_CASTLE;
568
+ }
569
+ if (tokens[2].indexOf('q') > -1) {
570
+ this._castling.b |= BITS.QSIDE_CASTLE;
571
+ }
572
+ this._epSquare = tokens[3] === '-' ? EMPTY : Ox88[tokens[3]];
573
+ this._halfMoves = parseInt(tokens[4], 10);
574
+ this._moveNumber = parseInt(tokens[5], 10);
575
+ this._updateSetup(fen);
576
+ this._incPositionCount(fen);
577
+ }
578
+ fen() {
579
+ let empty = 0;
580
+ let fen = '';
581
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
582
+ if (this._board[i]) {
583
+ if (empty > 0) {
584
+ fen += empty;
585
+ empty = 0;
586
+ }
587
+ const { color, type: piece } = this._board[i];
588
+ fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase();
589
+ }
590
+ else {
591
+ empty++;
592
+ }
593
+ if ((i + 1) & 0x88) {
594
+ if (empty > 0) {
595
+ fen += empty;
596
+ }
597
+ if (i !== Ox88.h1) {
598
+ fen += '/';
599
+ }
600
+ empty = 0;
601
+ i += 8;
602
+ }
603
+ }
604
+ let castling = '';
605
+ if (this._castling[WHITE] & BITS.KSIDE_CASTLE) {
606
+ castling += 'K';
607
+ }
608
+ if (this._castling[WHITE] & BITS.QSIDE_CASTLE) {
609
+ castling += 'Q';
610
+ }
611
+ if (this._castling[BLACK] & BITS.KSIDE_CASTLE) {
612
+ castling += 'k';
613
+ }
614
+ if (this._castling[BLACK] & BITS.QSIDE_CASTLE) {
615
+ castling += 'q';
616
+ }
617
+ // do we have an empty castling flag?
618
+ castling = castling || '-';
619
+ let epSquare = '-';
620
+ /*
621
+ * only print the ep square if en passant is a valid move (pawn is present
622
+ * and ep capture is not pinned)
623
+ */
624
+ if (this._epSquare !== EMPTY) {
625
+ const bigPawnSquare = this._epSquare + (this._turn === WHITE ? 16 : -16);
626
+ const squares = [bigPawnSquare + 1, bigPawnSquare - 1];
627
+ for (const square of squares) {
628
+ // is the square off the board?
629
+ if (square & 0x88) {
630
+ continue;
631
+ }
632
+ const color = this._turn;
633
+ // is there a pawn that can capture the epSquare?
634
+ if (this._board[square]?.color === color &&
635
+ this._board[square]?.type === PAWN) {
636
+ // if the pawn makes an ep capture, does it leave it's king in check?
637
+ this._makeMove({
638
+ color,
639
+ from: square,
640
+ to: this._epSquare,
641
+ piece: PAWN,
642
+ captured: PAWN,
643
+ flags: BITS.EP_CAPTURE,
644
+ });
645
+ const isLegal = !this._isKingAttacked(color);
646
+ this._undoMove();
647
+ // if ep is legal, break and set the ep square in the FEN output
648
+ if (isLegal) {
649
+ epSquare = algebraic(this._epSquare);
650
+ break;
651
+ }
652
+ }
653
+ }
654
+ }
655
+ return [
656
+ fen,
657
+ this._turn,
658
+ castling,
659
+ epSquare,
660
+ this._halfMoves,
661
+ this._moveNumber,
662
+ ].join(' ');
663
+ }
664
+ /*
665
+ * Called when the initial board setup is changed with put() or remove().
666
+ * modifies the SetUp and FEN properties of the header object. If the FEN
667
+ * is equal to the default position, the SetUp and FEN are deleted the setup
668
+ * is only updated if history.length is zero, ie moves haven't been made.
669
+ */
670
+ _updateSetup(fen) {
671
+ if (this._history.length > 0)
672
+ return;
673
+ if (fen !== DEFAULT_POSITION) {
674
+ this._header['SetUp'] = '1';
675
+ this._header['FEN'] = fen;
676
+ }
677
+ else {
678
+ delete this._header['SetUp'];
679
+ delete this._header['FEN'];
680
+ }
681
+ }
682
+ reset() {
683
+ this.load(DEFAULT_POSITION);
684
+ }
685
+ get(square) {
686
+ return this._board[Ox88[square]];
687
+ }
688
+ put({ type, color }, square) {
689
+ if (this._put({ type, color }, square)) {
690
+ this._updateCastlingRights();
691
+ this._updateEnPassantSquare();
692
+ this._updateSetup(this.fen());
693
+ return true;
694
+ }
695
+ return false;
696
+ }
697
+ _put({ type, color }, square) {
698
+ // check for piece
699
+ if (SYMBOLS.indexOf(type.toLowerCase()) === -1) {
700
+ return false;
701
+ }
702
+ // check for valid square
703
+ if (!(square in Ox88)) {
704
+ return false;
705
+ }
706
+ const sq = Ox88[square];
707
+ // don't let the user place more than one king
708
+ if (type == KING &&
709
+ !(this._kings[color] == EMPTY || this._kings[color] == sq)) {
710
+ return false;
711
+ }
712
+ const currentPieceOnSquare = this._board[sq];
713
+ // if one of the kings will be replaced by the piece from args, set the `_kings` respective entry to `EMPTY`
714
+ if (currentPieceOnSquare && currentPieceOnSquare.type === KING) {
715
+ this._kings[currentPieceOnSquare.color] = EMPTY;
716
+ }
717
+ this._board[sq] = { type: type, color: color };
718
+ if (type === KING) {
719
+ this._kings[color] = sq;
720
+ }
721
+ return true;
722
+ }
723
+ remove(square) {
724
+ const piece = this.get(square);
725
+ delete this._board[Ox88[square]];
726
+ if (piece && piece.type === KING) {
727
+ this._kings[piece.color] = EMPTY;
728
+ }
729
+ this._updateCastlingRights();
730
+ this._updateEnPassantSquare();
731
+ this._updateSetup(this.fen());
732
+ return piece;
733
+ }
734
+ _updateCastlingRights() {
735
+ const whiteKingInPlace = this._board[Ox88.e1]?.type === KING &&
736
+ this._board[Ox88.e1]?.color === WHITE;
737
+ const blackKingInPlace = this._board[Ox88.e8]?.type === KING &&
738
+ this._board[Ox88.e8]?.color === BLACK;
739
+ if (!whiteKingInPlace ||
740
+ this._board[Ox88.a1]?.type !== ROOK ||
741
+ this._board[Ox88.a1]?.color !== WHITE) {
742
+ this._castling.w &= ~BITS.QSIDE_CASTLE;
743
+ }
744
+ if (!whiteKingInPlace ||
745
+ this._board[Ox88.h1]?.type !== ROOK ||
746
+ this._board[Ox88.h1]?.color !== WHITE) {
747
+ this._castling.w &= ~BITS.KSIDE_CASTLE;
748
+ }
749
+ if (!blackKingInPlace ||
750
+ this._board[Ox88.a8]?.type !== ROOK ||
751
+ this._board[Ox88.a8]?.color !== BLACK) {
752
+ this._castling.b &= ~BITS.QSIDE_CASTLE;
753
+ }
754
+ if (!blackKingInPlace ||
755
+ this._board[Ox88.h8]?.type !== ROOK ||
756
+ this._board[Ox88.h8]?.color !== BLACK) {
757
+ this._castling.b &= ~BITS.KSIDE_CASTLE;
758
+ }
759
+ }
760
+ _updateEnPassantSquare() {
761
+ if (this._epSquare === EMPTY) {
762
+ return;
763
+ }
764
+ const startSquare = this._epSquare + (this._turn === WHITE ? -16 : 16);
765
+ const currentSquare = this._epSquare + (this._turn === WHITE ? 16 : -16);
766
+ const attackers = [currentSquare + 1, currentSquare - 1];
767
+ if (this._board[startSquare] !== null ||
768
+ this._board[this._epSquare] !== null ||
769
+ this._board[currentSquare]?.color !== swapColor(this._turn) ||
770
+ this._board[currentSquare]?.type !== PAWN) {
771
+ this._epSquare = EMPTY;
772
+ return;
773
+ }
774
+ const canCapture = (square) => !(square & 0x88) &&
775
+ this._board[square]?.color === this._turn &&
776
+ this._board[square]?.type === PAWN;
777
+ if (!attackers.some(canCapture)) {
778
+ this._epSquare = EMPTY;
779
+ }
780
+ }
781
+ _attacked(color, square, verbose) {
782
+ const attackers = [];
783
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
784
+ // did we run off the end of the board
785
+ if (i & 0x88) {
786
+ i += 7;
787
+ continue;
788
+ }
789
+ // if empty square or wrong color
790
+ if (this._board[i] === undefined || this._board[i].color !== color) {
791
+ continue;
792
+ }
793
+ const piece = this._board[i];
794
+ const difference = i - square;
795
+ // skip - to/from square are the same
796
+ if (difference === 0) {
797
+ continue;
798
+ }
799
+ const index = difference + 119;
800
+ if (ATTACKS[index] & PIECE_MASKS[piece.type]) {
801
+ if (piece.type === PAWN) {
802
+ if ((difference > 0 && piece.color === WHITE) ||
803
+ (difference <= 0 && piece.color === BLACK)) {
804
+ if (!verbose) {
805
+ return true;
806
+ }
807
+ else {
808
+ attackers.push(algebraic(i));
809
+ }
810
+ }
811
+ continue;
812
+ }
813
+ // if the piece is a knight or a king
814
+ if (piece.type === 'n' || piece.type === 'k') {
815
+ if (!verbose) {
816
+ return true;
817
+ }
818
+ else {
819
+ attackers.push(algebraic(i));
820
+ continue;
821
+ }
822
+ }
823
+ const offset = RAYS[index];
824
+ let j = i + offset;
825
+ let blocked = false;
826
+ while (j !== square) {
827
+ if (this._board[j] != null) {
828
+ blocked = true;
829
+ break;
830
+ }
831
+ j += offset;
832
+ }
833
+ if (!blocked) {
834
+ if (!verbose) {
835
+ return true;
836
+ }
837
+ else {
838
+ attackers.push(algebraic(i));
839
+ continue;
840
+ }
841
+ }
842
+ }
843
+ }
844
+ if (verbose) {
845
+ return attackers;
846
+ }
847
+ else {
848
+ return false;
849
+ }
850
+ }
851
+ attackers(square, attackedBy) {
852
+ if (!attackedBy) {
853
+ return this._attacked(this._turn, Ox88[square], true);
854
+ }
855
+ else {
856
+ return this._attacked(attackedBy, Ox88[square], true);
857
+ }
858
+ }
859
+ _isKingAttacked(color) {
860
+ const square = this._kings[color];
861
+ return square === -1 ? false : this._attacked(swapColor(color), square);
862
+ }
863
+ isAttacked(square, attackedBy) {
864
+ return this._attacked(attackedBy, Ox88[square]);
865
+ }
866
+ isCheck() {
867
+ return this._isKingAttacked(this._turn);
868
+ }
869
+ inCheck() {
870
+ return this.isCheck();
871
+ }
872
+ isCheckmate() {
873
+ return this.isCheck() && this._moves().length === 0;
874
+ }
875
+ isStalemate() {
876
+ return !this.isCheck() && this._moves().length === 0;
877
+ }
878
+ isInsufficientMaterial() {
879
+ /*
880
+ * k.b. vs k.b. (of opposite colors) with mate in 1:
881
+ * 8/8/8/8/1b6/8/B1k5/K7 b - - 0 1
882
+ *
883
+ * k.b. vs k.n. with mate in 1:
884
+ * 8/8/8/8/1n6/8/B7/K1k5 b - - 2 1
885
+ */
886
+ const pieces = {
887
+ b: 0,
888
+ n: 0,
889
+ r: 0,
890
+ q: 0,
891
+ k: 0,
892
+ p: 0,
893
+ };
894
+ const bishops = [];
895
+ let numPieces = 0;
896
+ let squareColor = 0;
897
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
898
+ squareColor = (squareColor + 1) % 2;
899
+ if (i & 0x88) {
900
+ i += 7;
901
+ continue;
902
+ }
903
+ const piece = this._board[i];
904
+ if (piece) {
905
+ pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1;
906
+ if (piece.type === BISHOP) {
907
+ bishops.push(squareColor);
908
+ }
909
+ numPieces++;
910
+ }
911
+ }
912
+ // k vs. k
913
+ if (numPieces === 2) {
914
+ return true;
915
+ }
916
+ else if (
917
+ // k vs. kn .... or .... k vs. kb
918
+ numPieces === 3 &&
919
+ (pieces[BISHOP] === 1 || pieces[KNIGHT] === 1)) {
920
+ return true;
921
+ }
922
+ else if (numPieces === pieces[BISHOP] + 2) {
923
+ // kb vs. kb where any number of bishops are all on the same color
924
+ let sum = 0;
925
+ const len = bishops.length;
926
+ for (let i = 0; i < len; i++) {
927
+ sum += bishops[i];
928
+ }
929
+ if (sum === 0 || sum === len) {
930
+ return true;
931
+ }
932
+ }
933
+ return false;
934
+ }
935
+ isThreefoldRepetition() {
936
+ return this._getPositionCount(this.fen()) >= 3;
937
+ }
938
+ isDrawByFiftyMoves() {
939
+ return this._halfMoves >= 100; // 50 moves per side = 100 half moves
940
+ }
941
+ isDraw() {
942
+ return (this.isDrawByFiftyMoves() ||
943
+ this.isStalemate() ||
944
+ this.isInsufficientMaterial() ||
945
+ this.isThreefoldRepetition());
946
+ }
947
+ isGameOver() {
948
+ return this.isCheckmate() || this.isStalemate() || this.isDraw();
949
+ }
950
+ moves({ verbose = false, square = undefined, piece = undefined, } = {}) {
951
+ const moves = this._moves({ square, piece });
952
+ if (verbose) {
953
+ return moves.map((move) => new Move(this, move));
954
+ }
955
+ else {
956
+ return moves.map((move) => this._moveToSan(move, moves));
957
+ }
958
+ }
959
+ _moves({ legal = true, piece = undefined, square = undefined, } = {}) {
960
+ const forSquare = square ? square.toLowerCase() : undefined;
961
+ const forPiece = piece?.toLowerCase();
962
+ const moves = [];
963
+ const us = this._turn;
964
+ const them = swapColor(us);
965
+ let firstSquare = Ox88.a8;
966
+ let lastSquare = Ox88.h1;
967
+ let singleSquare = false;
968
+ // are we generating moves for a single square?
969
+ if (forSquare) {
970
+ // illegal square, return empty moves
971
+ if (!(forSquare in Ox88)) {
972
+ return [];
973
+ }
974
+ else {
975
+ firstSquare = lastSquare = Ox88[forSquare];
976
+ singleSquare = true;
977
+ }
978
+ }
979
+ for (let from = firstSquare; from <= lastSquare; from++) {
980
+ // did we run off the end of the board
981
+ if (from & 0x88) {
982
+ from += 7;
983
+ continue;
984
+ }
985
+ // empty square or opponent, skip
986
+ if (!this._board[from] || this._board[from].color === them) {
987
+ continue;
988
+ }
989
+ const { type } = this._board[from];
990
+ let to;
991
+ if (type === PAWN) {
992
+ if (forPiece && forPiece !== type)
993
+ continue;
994
+ // single square, non-capturing
995
+ to = from + PAWN_OFFSETS[us][0];
996
+ if (!this._board[to]) {
997
+ addMove(moves, us, from, to, PAWN);
998
+ // double square
999
+ to = from + PAWN_OFFSETS[us][1];
1000
+ if (SECOND_RANK[us] === rank(from) && !this._board[to]) {
1001
+ addMove(moves, us, from, to, PAWN, undefined, BITS.BIG_PAWN);
1002
+ }
1003
+ }
1004
+ // pawn captures
1005
+ for (let j = 2; j < 4; j++) {
1006
+ to = from + PAWN_OFFSETS[us][j];
1007
+ if (to & 0x88)
1008
+ continue;
1009
+ if (this._board[to]?.color === them) {
1010
+ addMove(moves, us, from, to, PAWN, this._board[to].type, BITS.CAPTURE);
1011
+ }
1012
+ else if (to === this._epSquare) {
1013
+ addMove(moves, us, from, to, PAWN, PAWN, BITS.EP_CAPTURE);
1014
+ }
1015
+ }
1016
+ }
1017
+ else {
1018
+ if (forPiece && forPiece !== type)
1019
+ continue;
1020
+ for (let j = 0, len = PIECE_OFFSETS[type].length; j < len; j++) {
1021
+ const offset = PIECE_OFFSETS[type][j];
1022
+ to = from;
1023
+ while (true) {
1024
+ to += offset;
1025
+ if (to & 0x88)
1026
+ break;
1027
+ if (!this._board[to]) {
1028
+ addMove(moves, us, from, to, type);
1029
+ }
1030
+ else {
1031
+ // own color, stop loop
1032
+ if (this._board[to].color === us)
1033
+ break;
1034
+ addMove(moves, us, from, to, type, this._board[to].type, BITS.CAPTURE);
1035
+ break;
1036
+ }
1037
+ /* break, if knight or king */
1038
+ if (type === KNIGHT || type === KING)
1039
+ break;
1040
+ }
1041
+ }
1042
+ }
1043
+ }
1044
+ /*
1045
+ * check for castling if we're:
1046
+ * a) generating all moves, or
1047
+ * b) doing single square move generation on the king's square
1048
+ */
1049
+ if (forPiece === undefined || forPiece === KING) {
1050
+ if (!singleSquare || lastSquare === this._kings[us]) {
1051
+ // king-side castling
1052
+ if (this._castling[us] & BITS.KSIDE_CASTLE) {
1053
+ const castlingFrom = this._kings[us];
1054
+ const castlingTo = castlingFrom + 2;
1055
+ if (!this._board[castlingFrom + 1] &&
1056
+ !this._board[castlingTo] &&
1057
+ !this._attacked(them, this._kings[us]) &&
1058
+ !this._attacked(them, castlingFrom + 1) &&
1059
+ !this._attacked(them, castlingTo)) {
1060
+ addMove(moves, us, this._kings[us], castlingTo, KING, undefined, BITS.KSIDE_CASTLE);
1061
+ }
1062
+ }
1063
+ // queen-side castling
1064
+ if (this._castling[us] & BITS.QSIDE_CASTLE) {
1065
+ const castlingFrom = this._kings[us];
1066
+ const castlingTo = castlingFrom - 2;
1067
+ if (!this._board[castlingFrom - 1] &&
1068
+ !this._board[castlingFrom - 2] &&
1069
+ !this._board[castlingFrom - 3] &&
1070
+ !this._attacked(them, this._kings[us]) &&
1071
+ !this._attacked(them, castlingFrom - 1) &&
1072
+ !this._attacked(them, castlingTo)) {
1073
+ addMove(moves, us, this._kings[us], castlingTo, KING, undefined, BITS.QSIDE_CASTLE);
1074
+ }
1075
+ }
1076
+ }
1077
+ }
1078
+ /*
1079
+ * return all pseudo-legal moves (this includes moves that allow the king
1080
+ * to be captured)
1081
+ */
1082
+ if (!legal || this._kings[us] === -1) {
1083
+ return moves;
1084
+ }
1085
+ // filter out illegal moves
1086
+ const legalMoves = [];
1087
+ for (let i = 0, len = moves.length; i < len; i++) {
1088
+ this._makeMove(moves[i]);
1089
+ if (!this._isKingAttacked(us)) {
1090
+ legalMoves.push(moves[i]);
1091
+ }
1092
+ this._undoMove();
1093
+ }
1094
+ return legalMoves;
1095
+ }
1096
+ move(move, { strict = false } = {}) {
1097
+ /*
1098
+ * The move function can be called with in the following parameters:
1099
+ *
1100
+ * .move('Nxb7') <- argument is a case-sensitive SAN string
1101
+ *
1102
+ * .move({ from: 'h7', <- argument is a move object
1103
+ * to :'h8',
1104
+ * promotion: 'q' })
1105
+ *
1106
+ *
1107
+ * An optional strict argument may be supplied to tell chess.js to
1108
+ * strictly follow the SAN specification.
1109
+ */
1110
+ let moveObj = null;
1111
+ if (typeof move === 'string') {
1112
+ moveObj = this._moveFromSan(move, strict);
1113
+ }
1114
+ else if (typeof move === 'object') {
1115
+ const moves = this._moves();
1116
+ // convert the pretty move object to an ugly move object
1117
+ for (let i = 0, len = moves.length; i < len; i++) {
1118
+ if (move.from === algebraic(moves[i].from) &&
1119
+ move.to === algebraic(moves[i].to) &&
1120
+ (!('promotion' in moves[i]) || move.promotion === moves[i].promotion)) {
1121
+ moveObj = moves[i];
1122
+ break;
1123
+ }
1124
+ }
1125
+ }
1126
+ // failed to find move
1127
+ if (!moveObj) {
1128
+ if (typeof move === 'string') {
1129
+ throw new Error(`Invalid move: ${move}`);
1130
+ }
1131
+ else {
1132
+ throw new Error(`Invalid move: ${JSON.stringify(move)}`);
1133
+ }
1134
+ }
1135
+ /*
1136
+ * need to make a copy of move because we can't generate SAN after the move
1137
+ * is made
1138
+ */
1139
+ const prettyMove = new Move(this, moveObj);
1140
+ this._makeMove(moveObj);
1141
+ this._incPositionCount(prettyMove.after);
1142
+ return prettyMove;
1143
+ }
1144
+ _push(move) {
1145
+ this._history.push({
1146
+ move,
1147
+ kings: { b: this._kings.b, w: this._kings.w },
1148
+ turn: this._turn,
1149
+ castling: { b: this._castling.b, w: this._castling.w },
1150
+ epSquare: this._epSquare,
1151
+ halfMoves: this._halfMoves,
1152
+ moveNumber: this._moveNumber,
1153
+ });
1154
+ }
1155
+ _makeMove(move) {
1156
+ const us = this._turn;
1157
+ const them = swapColor(us);
1158
+ this._push(move);
1159
+ this._board[move.to] = this._board[move.from];
1160
+ delete this._board[move.from];
1161
+ // if ep capture, remove the captured pawn
1162
+ if (move.flags & BITS.EP_CAPTURE) {
1163
+ if (this._turn === BLACK) {
1164
+ delete this._board[move.to - 16];
1165
+ }
1166
+ else {
1167
+ delete this._board[move.to + 16];
1168
+ }
1169
+ }
1170
+ // if pawn promotion, replace with new piece
1171
+ if (move.promotion) {
1172
+ this._board[move.to] = { type: move.promotion, color: us };
1173
+ }
1174
+ // if we moved the king
1175
+ if (this._board[move.to].type === KING) {
1176
+ this._kings[us] = move.to;
1177
+ // if we castled, move the rook next to the king
1178
+ if (move.flags & BITS.KSIDE_CASTLE) {
1179
+ const castlingTo = move.to - 1;
1180
+ const castlingFrom = move.to + 1;
1181
+ this._board[castlingTo] = this._board[castlingFrom];
1182
+ delete this._board[castlingFrom];
1183
+ }
1184
+ else if (move.flags & BITS.QSIDE_CASTLE) {
1185
+ const castlingTo = move.to + 1;
1186
+ const castlingFrom = move.to - 2;
1187
+ this._board[castlingTo] = this._board[castlingFrom];
1188
+ delete this._board[castlingFrom];
1189
+ }
1190
+ // turn off castling
1191
+ this._castling[us] = 0;
1192
+ }
1193
+ // turn off castling if we move a rook
1194
+ if (this._castling[us]) {
1195
+ for (let i = 0, len = ROOKS[us].length; i < len; i++) {
1196
+ if (move.from === ROOKS[us][i].square &&
1197
+ this._castling[us] & ROOKS[us][i].flag) {
1198
+ this._castling[us] ^= ROOKS[us][i].flag;
1199
+ break;
1200
+ }
1201
+ }
1202
+ }
1203
+ // turn off castling if we capture a rook
1204
+ if (this._castling[them]) {
1205
+ for (let i = 0, len = ROOKS[them].length; i < len; i++) {
1206
+ if (move.to === ROOKS[them][i].square &&
1207
+ this._castling[them] & ROOKS[them][i].flag) {
1208
+ this._castling[them] ^= ROOKS[them][i].flag;
1209
+ break;
1210
+ }
1211
+ }
1212
+ }
1213
+ // if big pawn move, update the en passant square
1214
+ if (move.flags & BITS.BIG_PAWN) {
1215
+ if (us === BLACK) {
1216
+ this._epSquare = move.to - 16;
1217
+ }
1218
+ else {
1219
+ this._epSquare = move.to + 16;
1220
+ }
1221
+ }
1222
+ else {
1223
+ this._epSquare = EMPTY;
1224
+ }
1225
+ // reset the 50 move counter if a pawn is moved or a piece is captured
1226
+ if (move.piece === PAWN) {
1227
+ this._halfMoves = 0;
1228
+ }
1229
+ else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
1230
+ this._halfMoves = 0;
1231
+ }
1232
+ else {
1233
+ this._halfMoves++;
1234
+ }
1235
+ if (us === BLACK) {
1236
+ this._moveNumber++;
1237
+ }
1238
+ this._turn = them;
1239
+ }
1240
+ undo() {
1241
+ const move = this._undoMove();
1242
+ if (move) {
1243
+ const prettyMove = new Move(this, move);
1244
+ this._decPositionCount(prettyMove.after);
1245
+ return prettyMove;
1246
+ }
1247
+ return null;
1248
+ }
1249
+ _undoMove() {
1250
+ const old = this._history.pop();
1251
+ if (old === undefined) {
1252
+ return null;
1253
+ }
1254
+ const move = old.move;
1255
+ this._kings = old.kings;
1256
+ this._turn = old.turn;
1257
+ this._castling = old.castling;
1258
+ this._epSquare = old.epSquare;
1259
+ this._halfMoves = old.halfMoves;
1260
+ this._moveNumber = old.moveNumber;
1261
+ const us = this._turn;
1262
+ const them = swapColor(us);
1263
+ this._board[move.from] = this._board[move.to];
1264
+ this._board[move.from].type = move.piece; // to undo any promotions
1265
+ delete this._board[move.to];
1266
+ if (move.captured) {
1267
+ if (move.flags & BITS.EP_CAPTURE) {
1268
+ // en passant capture
1269
+ let index;
1270
+ if (us === BLACK) {
1271
+ index = move.to - 16;
1272
+ }
1273
+ else {
1274
+ index = move.to + 16;
1275
+ }
1276
+ this._board[index] = { type: PAWN, color: them };
1277
+ }
1278
+ else {
1279
+ // regular capture
1280
+ this._board[move.to] = { type: move.captured, color: them };
1281
+ }
1282
+ }
1283
+ if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) {
1284
+ let castlingTo, castlingFrom;
1285
+ if (move.flags & BITS.KSIDE_CASTLE) {
1286
+ castlingTo = move.to + 1;
1287
+ castlingFrom = move.to - 1;
1288
+ }
1289
+ else {
1290
+ castlingTo = move.to - 2;
1291
+ castlingFrom = move.to + 1;
1292
+ }
1293
+ this._board[castlingTo] = this._board[castlingFrom];
1294
+ delete this._board[castlingFrom];
1295
+ }
1296
+ return move;
1297
+ }
1298
+ pgn({ newline = '\n', maxWidth = 0, } = {}) {
1299
+ /*
1300
+ * using the specification from http://www.chessclub.com/help/PGN-spec
1301
+ * example for html usage: .pgn({ max_width: 72, newline_char: "<br />" })
1302
+ */
1303
+ const result = [];
1304
+ let headerExists = false;
1305
+ /* add the PGN header information */
1306
+ for (const i in this._header) {
1307
+ /*
1308
+ * TODO: order of enumerated properties in header object is not
1309
+ * guaranteed, see ECMA-262 spec (section 12.6.4)
1310
+ */
1311
+ result.push('[' + i + ' "' + this._header[i] + '"]' + newline);
1312
+ headerExists = true;
1313
+ }
1314
+ if (headerExists && this._history.length) {
1315
+ result.push(newline);
1316
+ }
1317
+ const appendComment = (moveString) => {
1318
+ const comment = this._comments[this.fen()];
1319
+ if (typeof comment !== 'undefined') {
1320
+ const delimiter = moveString.length > 0 ? ' ' : '';
1321
+ moveString = `${moveString}${delimiter}{${comment}}`;
1322
+ }
1323
+ return moveString;
1324
+ };
1325
+ // pop all of history onto reversed_history
1326
+ const reversedHistory = [];
1327
+ while (this._history.length > 0) {
1328
+ reversedHistory.push(this._undoMove());
1329
+ }
1330
+ const moves = [];
1331
+ let moveString = '';
1332
+ // special case of a commented starting position with no moves
1333
+ if (reversedHistory.length === 0) {
1334
+ moves.push(appendComment(''));
1335
+ }
1336
+ // build the list of moves. a move_string looks like: "3. e3 e6"
1337
+ while (reversedHistory.length > 0) {
1338
+ moveString = appendComment(moveString);
1339
+ const move = reversedHistory.pop();
1340
+ // make TypeScript stop complaining about move being undefined
1341
+ if (!move) {
1342
+ break;
1343
+ }
1344
+ // if the position started with black to move, start PGN with #. ...
1345
+ if (!this._history.length && move.color === 'b') {
1346
+ const prefix = `${this._moveNumber}. ...`;
1347
+ // is there a comment preceding the first move?
1348
+ moveString = moveString ? `${moveString} ${prefix}` : prefix;
1349
+ }
1350
+ else if (move.color === 'w') {
1351
+ // store the previous generated move_string if we have one
1352
+ if (moveString.length) {
1353
+ moves.push(moveString);
1354
+ }
1355
+ moveString = this._moveNumber + '.';
1356
+ }
1357
+ moveString =
1358
+ moveString + ' ' + this._moveToSan(move, this._moves({ legal: true }));
1359
+ this._makeMove(move);
1360
+ }
1361
+ // are there any other leftover moves?
1362
+ if (moveString.length) {
1363
+ moves.push(appendComment(moveString));
1364
+ }
1365
+ // is there a result?
1366
+ if (typeof this._header.Result !== 'undefined') {
1367
+ moves.push(this._header.Result);
1368
+ }
1369
+ /*
1370
+ * history should be back to what it was before we started generating PGN,
1371
+ * so join together moves
1372
+ */
1373
+ if (maxWidth === 0) {
1374
+ return result.join('') + moves.join(' ');
1375
+ }
1376
+ // TODO (jah): huh?
1377
+ const strip = function () {
1378
+ if (result.length > 0 && result[result.length - 1] === ' ') {
1379
+ result.pop();
1380
+ return true;
1381
+ }
1382
+ return false;
1383
+ };
1384
+ // NB: this does not preserve comment whitespace.
1385
+ const wrapComment = function (width, move) {
1386
+ for (const token of move.split(' ')) {
1387
+ if (!token) {
1388
+ continue;
1389
+ }
1390
+ if (width + token.length > maxWidth) {
1391
+ while (strip()) {
1392
+ width--;
1393
+ }
1394
+ result.push(newline);
1395
+ width = 0;
1396
+ }
1397
+ result.push(token);
1398
+ width += token.length;
1399
+ result.push(' ');
1400
+ width++;
1401
+ }
1402
+ if (strip()) {
1403
+ width--;
1404
+ }
1405
+ return width;
1406
+ };
1407
+ // wrap the PGN output at max_width
1408
+ let currentWidth = 0;
1409
+ for (let i = 0; i < moves.length; i++) {
1410
+ if (currentWidth + moves[i].length > maxWidth) {
1411
+ if (moves[i].includes('{')) {
1412
+ currentWidth = wrapComment(currentWidth, moves[i]);
1413
+ continue;
1414
+ }
1415
+ }
1416
+ // if the current move will push past max_width
1417
+ if (currentWidth + moves[i].length > maxWidth && i !== 0) {
1418
+ // don't end the line with whitespace
1419
+ if (result[result.length - 1] === ' ') {
1420
+ result.pop();
1421
+ }
1422
+ result.push(newline);
1423
+ currentWidth = 0;
1424
+ }
1425
+ else if (i !== 0) {
1426
+ result.push(' ');
1427
+ currentWidth++;
1428
+ }
1429
+ result.push(moves[i]);
1430
+ currentWidth += moves[i].length;
1431
+ }
1432
+ return result.join('');
1433
+ }
1434
+ /*
1435
+ * @deprecated Use `setHeader` and `getHeaders` instead.
1436
+ */
1437
+ header(...args) {
1438
+ for (let i = 0; i < args.length; i += 2) {
1439
+ if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') {
1440
+ this._header[args[i]] = args[i + 1];
1441
+ }
1442
+ }
1443
+ return this._header;
1444
+ }
1445
+ setHeader(key, value) {
1446
+ this._header[key] = value;
1447
+ return this._header;
1448
+ }
1449
+ removeHeader(key) {
1450
+ if (key in this._header) {
1451
+ delete this._header[key];
1452
+ return true;
1453
+ }
1454
+ return false;
1455
+ }
1456
+ getHeaders() {
1457
+ return this._header;
1458
+ }
1459
+ loadPgn(pgn, { strict = false, newlineChar = '\r?\n', } = {}) {
1460
+ function mask(str) {
1461
+ return str.replace(/\\/g, '\\');
1462
+ }
1463
+ function parsePgnHeader(header) {
1464
+ const headerObj = {};
1465
+ const headers = header.split(new RegExp(mask(newlineChar)));
1466
+ let key = '';
1467
+ let value = '';
1468
+ for (let i = 0; i < headers.length; i++) {
1469
+ const regex = /^\s*\[\s*([A-Za-z]+)\s*"(.*)"\s*\]\s*$/;
1470
+ key = headers[i].replace(regex, '$1');
1471
+ value = headers[i].replace(regex, '$2');
1472
+ if (key.trim().length > 0) {
1473
+ headerObj[key] = value;
1474
+ }
1475
+ }
1476
+ return headerObj;
1477
+ }
1478
+ // strip whitespace from head/tail of PGN block
1479
+ pgn = pgn.trim();
1480
+ /*
1481
+ * RegExp to split header. Takes advantage of the fact that header and movetext
1482
+ * will always have a blank line between them (ie, two newline_char's). Handles
1483
+ * case where movetext is empty by matching newlineChar until end of string is
1484
+ * matched - effectively trimming from the end extra newlineChar.
1485
+ *
1486
+ * With default newline_char, will equal:
1487
+ * /^(\[((?:\r?\n)|.)*\])((?:\s*\r?\n){2}|(?:\s*\r?\n)*$)/
1488
+ */
1489
+ const headerRegex = new RegExp('^(\\[((?:' +
1490
+ mask(newlineChar) +
1491
+ ')|.)*\\])' +
1492
+ '((?:\\s*' +
1493
+ mask(newlineChar) +
1494
+ '){2}|(?:\\s*' +
1495
+ mask(newlineChar) +
1496
+ ')*$)');
1497
+ // If no header given, begin with moves.
1498
+ const headerRegexResults = headerRegex.exec(pgn);
1499
+ const headerString = headerRegexResults
1500
+ ? headerRegexResults.length >= 2
1501
+ ? headerRegexResults[1]
1502
+ : ''
1503
+ : '';
1504
+ // Put the board in the starting position
1505
+ this.reset();
1506
+ // parse PGN header
1507
+ const headers = parsePgnHeader(headerString);
1508
+ let fen = '';
1509
+ for (const key in headers) {
1510
+ // check to see user is including fen (possibly with wrong tag case)
1511
+ if (key.toLowerCase() === 'fen') {
1512
+ fen = headers[key];
1513
+ }
1514
+ this.header(key, headers[key]);
1515
+ }
1516
+ /*
1517
+ * the permissive parser should attempt to load a fen tag, even if it's the
1518
+ * wrong case and doesn't include a corresponding [SetUp "1"] tag
1519
+ */
1520
+ if (!strict) {
1521
+ if (fen) {
1522
+ this.load(fen, { preserveHeaders: true });
1523
+ }
1524
+ }
1525
+ else {
1526
+ /*
1527
+ * strict parser - load the starting position indicated by [Setup '1']
1528
+ * and [FEN position]
1529
+ */
1530
+ if (headers['SetUp'] === '1') {
1531
+ if (!('FEN' in headers)) {
1532
+ throw new Error('Invalid PGN: FEN tag must be supplied with SetUp tag');
1533
+ }
1534
+ // don't clear the headers when loading
1535
+ this.load(headers['FEN'], { preserveHeaders: true });
1536
+ }
1537
+ }
1538
+ /*
1539
+ * NB: the regexes below that delete move numbers, recursive annotations,
1540
+ * and numeric annotation glyphs may also match text in comments. To
1541
+ * prevent this, we transform comments by hex-encoding them in place and
1542
+ * decoding them again after the other tokens have been deleted.
1543
+ *
1544
+ * While the spec states that PGN files should be ASCII encoded, we use
1545
+ * {en,de}codeURIComponent here to support arbitrary UTF8 as a convenience
1546
+ * for modern users
1547
+ */
1548
+ function toHex(s) {
1549
+ return Array.from(s)
1550
+ .map(function (c) {
1551
+ /*
1552
+ * encodeURI doesn't transform most ASCII characters, so we handle
1553
+ * these ourselves
1554
+ */
1555
+ return c.charCodeAt(0) < 128
1556
+ ? c.charCodeAt(0).toString(16)
1557
+ : encodeURIComponent(c).replace(/%/g, '').toLowerCase();
1558
+ })
1559
+ .join('');
1560
+ }
1561
+ function fromHex(s) {
1562
+ return s.length == 0
1563
+ ? ''
1564
+ : decodeURIComponent('%' + (s.match(/.{1,2}/g) || []).join('%'));
1565
+ }
1566
+ const encodeComment = function (s) {
1567
+ s = s.replace(new RegExp(mask(newlineChar), 'g'), ' ');
1568
+ return `{${toHex(s.slice(1, s.length - 1))}}`;
1569
+ };
1570
+ const decodeComment = function (s) {
1571
+ if (s.startsWith('{') && s.endsWith('}')) {
1572
+ return fromHex(s.slice(1, s.length - 1));
1573
+ }
1574
+ };
1575
+ // delete header to get the moves
1576
+ let ms = pgn
1577
+ .replace(headerString, '')
1578
+ .replace(
1579
+ // encode comments so they don't get deleted below
1580
+ new RegExp(`({[^}]*})+?|;([^${mask(newlineChar)}]*)`, 'g'), function (_match, bracket, semicolon) {
1581
+ return bracket !== undefined
1582
+ ? encodeComment(bracket)
1583
+ : ' ' + encodeComment(`{${semicolon.slice(1)}}`);
1584
+ })
1585
+ .replace(new RegExp(mask(newlineChar), 'g'), ' ');
1586
+ // delete recursive annotation variations
1587
+ const ravRegex = /(\([^()]+\))+?/g;
1588
+ while (ravRegex.test(ms)) {
1589
+ ms = ms.replace(ravRegex, '');
1590
+ }
1591
+ // delete move numbers
1592
+ ms = ms.replace(/\d+\.(\.\.)?/g, '');
1593
+ // delete ... indicating black to move
1594
+ ms = ms.replace(/\.\.\./g, '');
1595
+ /* delete numeric annotation glyphs */
1596
+ ms = ms.replace(/\$\d+/g, '');
1597
+ // trim and get array of moves
1598
+ let moves = ms.trim().split(new RegExp(/\s+/));
1599
+ // delete empty entries
1600
+ moves = moves.filter((move) => move !== '');
1601
+ let result = '';
1602
+ for (let halfMove = 0; halfMove < moves.length; halfMove++) {
1603
+ const comment = decodeComment(moves[halfMove]);
1604
+ if (comment !== undefined) {
1605
+ this._comments[this.fen()] = comment;
1606
+ continue;
1607
+ }
1608
+ const move = this._moveFromSan(moves[halfMove], strict);
1609
+ // invalid move
1610
+ if (move == null) {
1611
+ // was the move an end of game marker
1612
+ if (TERMINATION_MARKERS.indexOf(moves[halfMove]) > -1) {
1613
+ result = moves[halfMove];
1614
+ }
1615
+ else {
1616
+ throw new Error(`Invalid move in PGN: ${moves[halfMove]}`);
1617
+ }
1618
+ }
1619
+ else {
1620
+ // reset the end of game marker if making a valid move
1621
+ result = '';
1622
+ this._makeMove(move);
1623
+ this._incPositionCount(this.fen());
1624
+ }
1625
+ }
1626
+ /*
1627
+ * Per section 8.2.6 of the PGN spec, the Result tag pair must match match
1628
+ * the termination marker. Only do this when headers are present, but the
1629
+ * result tag is missing
1630
+ */
1631
+ if (result && Object.keys(this._header).length && !this._header['Result']) {
1632
+ this.header('Result', result);
1633
+ }
1634
+ }
1635
+ /*
1636
+ * Convert a move from 0x88 coordinates to Standard Algebraic Notation
1637
+ * (SAN)
1638
+ *
1639
+ * @param {boolean} strict Use the strict SAN parser. It will throw errors
1640
+ * on overly disambiguated moves (see below):
1641
+ *
1642
+ * r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
1643
+ * 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
1644
+ * 4. ... Ne7 is technically the valid SAN
1645
+ */
1646
+ _moveToSan(move, moves) {
1647
+ let output = '';
1648
+ if (move.flags & BITS.KSIDE_CASTLE) {
1649
+ output = 'O-O';
1650
+ }
1651
+ else if (move.flags & BITS.QSIDE_CASTLE) {
1652
+ output = 'O-O-O';
1653
+ }
1654
+ else {
1655
+ if (move.piece !== PAWN) {
1656
+ const disambiguator = getDisambiguator(move, moves);
1657
+ output += move.piece.toUpperCase() + disambiguator;
1658
+ }
1659
+ if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
1660
+ if (move.piece === PAWN) {
1661
+ output += algebraic(move.from)[0];
1662
+ }
1663
+ output += 'x';
1664
+ }
1665
+ output += algebraic(move.to);
1666
+ if (move.promotion) {
1667
+ output += '=' + move.promotion.toUpperCase();
1668
+ }
1669
+ }
1670
+ this._makeMove(move);
1671
+ if (this.isCheck()) {
1672
+ if (this.isCheckmate()) {
1673
+ output += '#';
1674
+ }
1675
+ else {
1676
+ output += '+';
1677
+ }
1678
+ }
1679
+ this._undoMove();
1680
+ return output;
1681
+ }
1682
+ // convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
1683
+ _moveFromSan(move, strict = false) {
1684
+ // strip off any move decorations: e.g Nf3+?! becomes Nf3
1685
+ const cleanMove = strippedSan(move);
1686
+ let pieceType = inferPieceType(cleanMove);
1687
+ let moves = this._moves({ legal: true, piece: pieceType });
1688
+ // strict parser
1689
+ for (let i = 0, len = moves.length; i < len; i++) {
1690
+ if (cleanMove === strippedSan(this._moveToSan(moves[i], moves))) {
1691
+ return moves[i];
1692
+ }
1693
+ }
1694
+ // the strict parser failed
1695
+ if (strict) {
1696
+ return null;
1697
+ }
1698
+ let piece = undefined;
1699
+ let matches = undefined;
1700
+ let from = undefined;
1701
+ let to = undefined;
1702
+ let promotion = undefined;
1703
+ /*
1704
+ * The default permissive (non-strict) parser allows the user to parse
1705
+ * non-standard chess notations. This parser is only run after the strict
1706
+ * Standard Algebraic Notation (SAN) parser has failed.
1707
+ *
1708
+ * When running the permissive parser, we'll run a regex to grab the piece, the
1709
+ * to/from square, and an optional promotion piece. This regex will
1710
+ * parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7,
1711
+ * f7f8q, b1c3
1712
+ *
1713
+ * NOTE: Some positions and moves may be ambiguous when using the permissive
1714
+ * parser. For example, in this position: 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1,
1715
+ * the move b1c3 may be interpreted as Nc3 or B1c3 (a disambiguated bishop
1716
+ * move). In these cases, the permissive parser will default to the most
1717
+ * basic interpretation (which is b1c3 parsing to Nc3).
1718
+ */
1719
+ let overlyDisambiguated = false;
1720
+ matches = cleanMove.match(/([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/);
1721
+ if (matches) {
1722
+ piece = matches[1];
1723
+ from = matches[2];
1724
+ to = matches[3];
1725
+ promotion = matches[4];
1726
+ if (from.length == 1) {
1727
+ overlyDisambiguated = true;
1728
+ }
1729
+ }
1730
+ else {
1731
+ /*
1732
+ * The [a-h]?[1-8]? portion of the regex below handles moves that may be
1733
+ * overly disambiguated (e.g. Nge7 is unnecessary and non-standard when
1734
+ * there is one legal knight move to e7). In this case, the value of
1735
+ * 'from' variable will be a rank or file, not a square.
1736
+ */
1737
+ matches = cleanMove.match(/([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?/);
1738
+ if (matches) {
1739
+ piece = matches[1];
1740
+ from = matches[2];
1741
+ to = matches[3];
1742
+ promotion = matches[4];
1743
+ if (from.length == 1) {
1744
+ overlyDisambiguated = true;
1745
+ }
1746
+ }
1747
+ }
1748
+ pieceType = inferPieceType(cleanMove);
1749
+ moves = this._moves({
1750
+ legal: true,
1751
+ piece: piece ? piece : pieceType,
1752
+ });
1753
+ if (!to) {
1754
+ return null;
1755
+ }
1756
+ for (let i = 0, len = moves.length; i < len; i++) {
1757
+ if (!from) {
1758
+ // if there is no from square, it could be just 'x' missing from a capture
1759
+ if (cleanMove ===
1760
+ strippedSan(this._moveToSan(moves[i], moves)).replace('x', '')) {
1761
+ return moves[i];
1762
+ }
1763
+ // hand-compare move properties with the results from our permissive regex
1764
+ }
1765
+ else if ((!piece || piece.toLowerCase() == moves[i].piece) &&
1766
+ Ox88[from] == moves[i].from &&
1767
+ Ox88[to] == moves[i].to &&
1768
+ (!promotion || promotion.toLowerCase() == moves[i].promotion)) {
1769
+ return moves[i];
1770
+ }
1771
+ else if (overlyDisambiguated) {
1772
+ /*
1773
+ * SPECIAL CASE: we parsed a move string that may have an unneeded
1774
+ * rank/file disambiguator (e.g. Nge7). The 'from' variable will
1775
+ */
1776
+ const square = algebraic(moves[i].from);
1777
+ if ((!piece || piece.toLowerCase() == moves[i].piece) &&
1778
+ Ox88[to] == moves[i].to &&
1779
+ (from == square[0] || from == square[1]) &&
1780
+ (!promotion || promotion.toLowerCase() == moves[i].promotion)) {
1781
+ return moves[i];
1782
+ }
1783
+ }
1784
+ }
1785
+ return null;
1786
+ }
1787
+ ascii() {
1788
+ let s = ' +------------------------+\n';
1789
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
1790
+ // display the rank
1791
+ if (file(i) === 0) {
1792
+ s += ' ' + '87654321'[rank(i)] + ' |';
1793
+ }
1794
+ if (this._board[i]) {
1795
+ const piece = this._board[i].type;
1796
+ const color = this._board[i].color;
1797
+ const symbol = color === WHITE ? piece.toUpperCase() : piece.toLowerCase();
1798
+ s += ' ' + symbol + ' ';
1799
+ }
1800
+ else {
1801
+ s += ' . ';
1802
+ }
1803
+ if ((i + 1) & 0x88) {
1804
+ s += '|\n';
1805
+ i += 8;
1806
+ }
1807
+ }
1808
+ s += ' +------------------------+\n';
1809
+ s += ' a b c d e f g h';
1810
+ return s;
1811
+ }
1812
+ perft(depth) {
1813
+ const moves = this._moves({ legal: false });
1814
+ let nodes = 0;
1815
+ const color = this._turn;
1816
+ for (let i = 0, len = moves.length; i < len; i++) {
1817
+ this._makeMove(moves[i]);
1818
+ if (!this._isKingAttacked(color)) {
1819
+ if (depth - 1 > 0) {
1820
+ nodes += this.perft(depth - 1);
1821
+ }
1822
+ else {
1823
+ nodes++;
1824
+ }
1825
+ }
1826
+ this._undoMove();
1827
+ }
1828
+ return nodes;
1829
+ }
1830
+ turn() {
1831
+ return this._turn;
1832
+ }
1833
+ board() {
1834
+ const output = [];
1835
+ let row = [];
1836
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
1837
+ if (this._board[i] == null) {
1838
+ row.push(null);
1839
+ }
1840
+ else {
1841
+ row.push({
1842
+ square: algebraic(i),
1843
+ type: this._board[i].type,
1844
+ color: this._board[i].color,
1845
+ });
1846
+ }
1847
+ if ((i + 1) & 0x88) {
1848
+ output.push(row);
1849
+ row = [];
1850
+ i += 8;
1851
+ }
1852
+ }
1853
+ return output;
1854
+ }
1855
+ squareColor(square) {
1856
+ if (square in Ox88) {
1857
+ const sq = Ox88[square];
1858
+ return (rank(sq) + file(sq)) % 2 === 0 ? 'light' : 'dark';
1859
+ }
1860
+ return null;
1861
+ }
1862
+ history({ verbose = false } = {}) {
1863
+ const reversedHistory = [];
1864
+ const moveHistory = [];
1865
+ while (this._history.length > 0) {
1866
+ reversedHistory.push(this._undoMove());
1867
+ }
1868
+ while (true) {
1869
+ const move = reversedHistory.pop();
1870
+ if (!move) {
1871
+ break;
1872
+ }
1873
+ if (verbose) {
1874
+ moveHistory.push(new Move(this, move));
1875
+ }
1876
+ else {
1877
+ moveHistory.push(this._moveToSan(move, this._moves()));
1878
+ }
1879
+ this._makeMove(move);
1880
+ }
1881
+ return moveHistory;
1882
+ }
1883
+ /*
1884
+ * Keeps track of position occurrence counts for the purpose of repetition
1885
+ * checking. All three methods (`_inc`, `_dec`, and `_get`) trim the
1886
+ * irrelevent information from the fen, initialising new positions, and
1887
+ * removing old positions from the record if their counts are reduced to 0.
1888
+ */
1889
+ _getPositionCount(fen) {
1890
+ const trimmedFen = trimFen(fen);
1891
+ return this._positionCount[trimmedFen] || 0;
1892
+ }
1893
+ _incPositionCount(fen) {
1894
+ const trimmedFen = trimFen(fen);
1895
+ if (this._positionCount[trimmedFen] === undefined) {
1896
+ this._positionCount[trimmedFen] = 0;
1897
+ }
1898
+ this._positionCount[trimmedFen] += 1;
1899
+ }
1900
+ _decPositionCount(fen) {
1901
+ const trimmedFen = trimFen(fen);
1902
+ if (this._positionCount[trimmedFen] === 1) {
1903
+ delete this._positionCount[trimmedFen];
1904
+ }
1905
+ else {
1906
+ this._positionCount[trimmedFen] -= 1;
1907
+ }
1908
+ }
1909
+ _pruneComments() {
1910
+ const reversedHistory = [];
1911
+ const currentComments = {};
1912
+ const copyComment = (fen) => {
1913
+ if (fen in this._comments) {
1914
+ currentComments[fen] = this._comments[fen];
1915
+ }
1916
+ };
1917
+ while (this._history.length > 0) {
1918
+ reversedHistory.push(this._undoMove());
1919
+ }
1920
+ copyComment(this.fen());
1921
+ while (true) {
1922
+ const move = reversedHistory.pop();
1923
+ if (!move) {
1924
+ break;
1925
+ }
1926
+ this._makeMove(move);
1927
+ copyComment(this.fen());
1928
+ }
1929
+ this._comments = currentComments;
1930
+ }
1931
+ getComment() {
1932
+ return this._comments[this.fen()];
1933
+ }
1934
+ setComment(comment) {
1935
+ this._comments[this.fen()] = comment.replace('{', '[').replace('}', ']');
1936
+ }
1937
+ /**
1938
+ * @deprecated Renamed to `removeComment` for consistency
1939
+ */
1940
+ deleteComment() {
1941
+ return this.removeComment();
1942
+ }
1943
+ removeComment() {
1944
+ const comment = this._comments[this.fen()];
1945
+ delete this._comments[this.fen()];
1946
+ return comment;
1947
+ }
1948
+ getComments() {
1949
+ this._pruneComments();
1950
+ return Object.keys(this._comments).map((fen) => {
1951
+ return { fen: fen, comment: this._comments[fen] };
1952
+ });
1953
+ }
1954
+ /**
1955
+ * @deprecated Renamed to `removeComments` for consistency
1956
+ */
1957
+ deleteComments() {
1958
+ return this.removeComments();
1959
+ }
1960
+ removeComments() {
1961
+ this._pruneComments();
1962
+ return Object.keys(this._comments).map((fen) => {
1963
+ const comment = this._comments[fen];
1964
+ delete this._comments[fen];
1965
+ return { fen: fen, comment: comment };
1966
+ });
1967
+ }
1968
+ setCastlingRights(color, rights) {
1969
+ for (const side of [KING, QUEEN]) {
1970
+ if (rights[side] !== undefined) {
1971
+ if (rights[side]) {
1972
+ this._castling[color] |= SIDES[side];
1973
+ }
1974
+ else {
1975
+ this._castling[color] &= ~SIDES[side];
1976
+ }
1977
+ }
1978
+ }
1979
+ this._updateCastlingRights();
1980
+ const result = this.getCastlingRights(color);
1981
+ return ((rights[KING] === undefined || rights[KING] === result[KING]) &&
1982
+ (rights[QUEEN] === undefined || rights[QUEEN] === result[QUEEN]));
1983
+ }
1984
+ getCastlingRights(color) {
1985
+ return {
1986
+ [KING]: (this._castling[color] & SIDES[KING]) !== 0,
1987
+ [QUEEN]: (this._castling[color] & SIDES[QUEEN]) !== 0,
1988
+ };
1989
+ }
1990
+ moveNumber() {
1991
+ return this._moveNumber;
1992
+ }
1993
+ }
1994
+ //# sourceMappingURL=chess.js.map