@weshell/chess.js 0.0.0-stage → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +87 -2
  3. package/dist/attack.d.ts +32 -0
  4. package/dist/attack.d.ts.map +1 -0
  5. package/dist/attack.js +103 -0
  6. package/dist/attack.js.map +1 -0
  7. package/dist/board.d.ts +33 -0
  8. package/dist/board.d.ts.map +1 -0
  9. package/dist/board.js +49 -0
  10. package/dist/board.js.map +1 -0
  11. package/dist/chess.d.ts +232 -0
  12. package/dist/chess.d.ts.map +1 -0
  13. package/dist/chess.js +1335 -0
  14. package/dist/chess.js.map +1 -0
  15. package/dist/fen.d.ts +38 -0
  16. package/dist/fen.d.ts.map +1 -0
  17. package/dist/fen.js +308 -0
  18. package/dist/fen.js.map +1 -0
  19. package/dist/hash.d.ts +34 -0
  20. package/dist/hash.d.ts.map +1 -0
  21. package/dist/hash.js +98 -0
  22. package/dist/hash.js.map +1 -0
  23. package/dist/index.d.ts +3 -0
  24. package/dist/index.d.ts.map +1 -0
  25. package/dist/index.js +2 -0
  26. package/dist/index.js.map +1 -0
  27. package/dist/move.d.ts +55 -0
  28. package/dist/move.d.ts.map +1 -0
  29. package/dist/move.js +79 -0
  30. package/dist/move.js.map +1 -0
  31. package/dist/movegen.d.ts +34 -0
  32. package/dist/movegen.d.ts.map +1 -0
  33. package/dist/movegen.js +242 -0
  34. package/dist/movegen.js.map +1 -0
  35. package/dist/node.d.ts +8 -0
  36. package/dist/node.d.ts.map +1 -0
  37. package/dist/node.js +2 -0
  38. package/dist/node.js.map +1 -0
  39. package/dist/notation.d.ts +33 -0
  40. package/dist/notation.d.ts.map +1 -0
  41. package/dist/notation.js +257 -0
  42. package/dist/notation.js.map +1 -0
  43. package/dist/pgn.d.ts +12 -0
  44. package/dist/pgn.d.ts.map +1 -0
  45. package/dist/pgn.js +1347 -0
  46. package/dist/pgn.js.map +1 -0
  47. package/dist/types.d.ts +118 -0
  48. package/dist/types.d.ts.map +1 -0
  49. package/dist/types.js +237 -0
  50. package/dist/types.js.map +1 -0
  51. package/package.json +45 -4
package/dist/chess.js ADDED
@@ -0,0 +1,1335 @@
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
+ import { parse } from './pgn.js';
28
+ import { BISHOP, BITS, BLACK, DEFAULT_POSITION, EMPTY, HEADER_TEMPLATE, KING, KNIGHT, Ox88, PAWN, QUEEN, RANK_1, RANK_8, ROOK, SEVEN_TAG_ROSTER, SIDES, SAN_NULLMOVE, SYMBOLS, WHITE, } from './types.js';
29
+ import { algebraic, file, isDigit, rank, swapColor } from './board.js';
30
+ import { castlingKey, computeHash, epKey, pieceKey, SIDE_KEY, } from './hash.js';
31
+ import { validateFen, makeCastlingFen, parseCastlingFen, serializeBoard } from './fen.js';
32
+ import { attacked, isKingAttacked } from './attack.js';
33
+ import { generatePseudoMoves } from './movegen.js';
34
+ import { moveFromSan, moveToSan } from './notation.js';
35
+ import { Move } from './move.js';
36
+ // === Re-exports for backward compatibility ===
37
+ export { WHITE, BLACK, PAWN, KNIGHT, BISHOP, ROOK, QUEEN, KING, DEFAULT_POSITION, SQUARES, SEVEN_TAG_ROSTER, } from './types.js';
38
+ export { xoroshiro128 } from './hash.js';
39
+ export { validateFen } from './fen.js';
40
+ export { Move } from './move.js';
41
+ /* === Chess class === */
42
+ export class Chess {
43
+ constructor(fen = DEFAULT_POSITION, { skipValidation = false } = {}) {
44
+ this._board = new Array(128);
45
+ this._turn = WHITE;
46
+ this._header = {};
47
+ this._kings = { w: EMPTY, b: EMPTY };
48
+ this._epSquare = -1;
49
+ this._halfMoves = 0;
50
+ this._moveNumber = 0;
51
+ this._history = [];
52
+ this._comments = {};
53
+ this._castling = { w: 0, b: 0 };
54
+ // CHESS960: origin squares of the rooks holding the castling rights
55
+ this._castlingRooks = {
56
+ w: { kside: EMPTY, qside: EMPTY },
57
+ b: { kside: EMPTY, qside: EMPTY },
58
+ };
59
+ this._hash = 0n;
60
+ // tracks number of times a position has been seen for repetition checking
61
+ this._positionCount = new Map();
62
+ this.load(fen, { skipValidation });
63
+ }
64
+ clear({ preserveHeaders = false } = {}) {
65
+ this._board = new Array(128);
66
+ this._kings = { w: EMPTY, b: EMPTY };
67
+ this._turn = WHITE;
68
+ this._castling = { w: 0, b: 0 };
69
+ this._castlingRooks = {
70
+ w: { kside: EMPTY, qside: EMPTY },
71
+ b: { kside: EMPTY, qside: EMPTY },
72
+ };
73
+ this._epSquare = EMPTY;
74
+ this._halfMoves = 0;
75
+ this._moveNumber = 1;
76
+ this._history = [];
77
+ this._comments = {};
78
+ this._header = preserveHeaders ? this._header : { ...HEADER_TEMPLATE };
79
+ this._hash = this._computeHash();
80
+ this._positionCount = new Map();
81
+ /*
82
+ * Delete the SetUp and FEN headers (if preserved), the board is empty and
83
+ * these headers don't make sense in this state. They'll get added later
84
+ * via .load() or .put()
85
+ */
86
+ this._header['SetUp'] = null;
87
+ this._header['FEN'] = null;
88
+ }
89
+ load(fen, { skipValidation = false, preserveHeaders = false } = {}) {
90
+ let tokens = fen.split(/\s+/);
91
+ // append commonly omitted fen tokens
92
+ if (tokens.length >= 2 && tokens.length < 6) {
93
+ const adjustments = ['-', '-', '0', '1'];
94
+ fen = tokens.concat(adjustments.slice(-(6 - tokens.length))).join(' ');
95
+ }
96
+ tokens = fen.split(/\s+/);
97
+ if (!skipValidation) {
98
+ const { ok, error } = validateFen(fen);
99
+ if (!ok) {
100
+ throw new Error(error);
101
+ }
102
+ }
103
+ const position = tokens[0];
104
+ let square = 0;
105
+ this.clear({ preserveHeaders });
106
+ for (let i = 0; i < position.length; i++) {
107
+ const piece = position.charAt(i);
108
+ if (piece === '/') {
109
+ square += 8;
110
+ }
111
+ else if (isDigit(piece)) {
112
+ square += parseInt(piece, 10);
113
+ }
114
+ else {
115
+ const color = piece < 'a' ? WHITE : BLACK;
116
+ this._put({ type: piece.toLowerCase(), color }, algebraic(square));
117
+ square++;
118
+ }
119
+ }
120
+ this._turn = tokens[1];
121
+ /*
122
+ * CHESS960: parse the castling field. Accepts X-FEN (KQkq), Shredder-FEN
123
+ * file letters (HAha), and mixed forms. Rights that cannot be mapped to
124
+ * a rook on the backrank are silently dropped (chessops-style).
125
+ */
126
+ this._loadCastling(tokens[2]);
127
+ this._epSquare = tokens[3] === '-' ? EMPTY : Ox88[tokens[3]];
128
+ this._halfMoves = parseInt(tokens[4], 10);
129
+ this._moveNumber = parseInt(tokens[5], 10);
130
+ this._hash = this._computeHash();
131
+ this._updateSetup(fen);
132
+ this._incPositionCount();
133
+ }
134
+ /*
135
+ * CHESS960: parse the castling field of a FEN string. Accepts X-FEN
136
+ * (KQkq), Shredder-FEN file letters (HAha), and mixed forms, mirroring
137
+ * chessops parseCastlingFen + Castles.fromSetup:
138
+ *
139
+ * - A file letter marks the rook square directly.
140
+ * - 'K'/'Q' mark the outermost rook-or-king on the backrank (falling back
141
+ * to the h/a file square when that is not a rook).
142
+ * - A right survives only if the king is on the backrank and the marked
143
+ * square holds a rook of the moving color, positioned on the correct
144
+ * side of the king; otherwise it is silently dropped.
145
+ */
146
+ _loadCastling(castling) {
147
+ const parsed = parseCastlingFen(castling, this._board, this._kings);
148
+ this._castling = parsed.castling;
149
+ this._castlingRooks = parsed.castlingRooks;
150
+ }
151
+ fen({ forceEnpassantSquare = false, } = {}) {
152
+ const fen = serializeBoard(this._board);
153
+ /*
154
+ * CHESS960: mirror chessops makeCastlingFen — emit X-FEN letters (K/Q)
155
+ * when the rook is the outermost rook on its side of the king, and
156
+ * Shredder-FEN file letters otherwise. Output order is h-file to a-file,
157
+ * kingside before queenside, which matches standard 'KQkq' byte-for-byte.
158
+ */
159
+ const castling = makeCastlingFen(this._board, this._kings, this._castling, this._castlingRooks) || '-';
160
+ let epSquare = '-';
161
+ /*
162
+ * only print the ep square if en passant is a valid move (pawn is present
163
+ * and ep capture is not pinned)
164
+ */
165
+ if (this._epSquare !== EMPTY) {
166
+ if (forceEnpassantSquare) {
167
+ epSquare = algebraic(this._epSquare);
168
+ }
169
+ else {
170
+ const bigPawnSquare = this._epSquare + (this._turn === WHITE ? 16 : -16);
171
+ const squares = [bigPawnSquare + 1, bigPawnSquare - 1];
172
+ for (const square of squares) {
173
+ // is the square off the board?
174
+ if (square & 0x88) {
175
+ continue;
176
+ }
177
+ const color = this._turn;
178
+ // is there a pawn that can capture the epSquare?
179
+ if (this._board[square]?.color === color &&
180
+ this._board[square]?.type === PAWN) {
181
+ // if the pawn makes an ep capture, does it leave its king in check?
182
+ this._makeMove({
183
+ color,
184
+ from: square,
185
+ to: this._epSquare,
186
+ piece: PAWN,
187
+ captured: PAWN,
188
+ flags: BITS.EP_CAPTURE,
189
+ });
190
+ const isLegal = !this._isKingAttacked(color);
191
+ this._undoMove();
192
+ // if ep is legal, break and set the ep square in the FEN output
193
+ if (isLegal) {
194
+ epSquare = algebraic(this._epSquare);
195
+ break;
196
+ }
197
+ }
198
+ }
199
+ }
200
+ }
201
+ return [
202
+ fen,
203
+ this._turn,
204
+ castling,
205
+ epSquare,
206
+ this._halfMoves,
207
+ this._moveNumber,
208
+ ].join(' ');
209
+ }
210
+ _pieceKey(i) {
211
+ return pieceKey(this._board, i);
212
+ }
213
+ _epKey() {
214
+ return epKey(this._epSquare);
215
+ }
216
+ _castlingKey() {
217
+ return castlingKey(this._castling);
218
+ }
219
+ _computeHash() {
220
+ return computeHash(this._board, this._epSquare, this._castling, this._turn);
221
+ }
222
+ /*
223
+ * Called when the initial board setup is changed with put() or remove().
224
+ * modifies the SetUp and FEN properties of the header object. If the FEN
225
+ * is equal to the default position, the SetUp and FEN are deleted the setup
226
+ * is only updated if history.length is zero, ie moves haven't been made.
227
+ */
228
+ _updateSetup(fen) {
229
+ if (this._history.length > 0)
230
+ return;
231
+ if (fen !== DEFAULT_POSITION) {
232
+ this._header['SetUp'] = '1';
233
+ this._header['FEN'] = fen;
234
+ }
235
+ else {
236
+ this._header['SetUp'] = null;
237
+ this._header['FEN'] = null;
238
+ }
239
+ }
240
+ reset() {
241
+ this.load(DEFAULT_POSITION);
242
+ }
243
+ get(square) {
244
+ return this._board[Ox88[square]];
245
+ }
246
+ findPiece(piece) {
247
+ const squares = [];
248
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
249
+ // did we run off the end of the board
250
+ if (i & 0x88) {
251
+ i += 7;
252
+ continue;
253
+ }
254
+ // if empty square or wrong color
255
+ if (!this._board[i] || this._board[i]?.color !== piece.color) {
256
+ continue;
257
+ }
258
+ // check if square contains the requested piece
259
+ if (this._board[i].color === piece.color &&
260
+ this._board[i].type === piece.type) {
261
+ squares.push(algebraic(i));
262
+ }
263
+ }
264
+ return squares;
265
+ }
266
+ put({ type, color }, square) {
267
+ if (this._put({ type, color }, square)) {
268
+ this._updateCastlingRights();
269
+ this._updateEnPassantSquare();
270
+ this._updateSetup(this.fen());
271
+ return true;
272
+ }
273
+ return false;
274
+ }
275
+ _set(sq, piece) {
276
+ this._hash ^= this._pieceKey(sq);
277
+ this._board[sq] = piece;
278
+ this._hash ^= this._pieceKey(sq);
279
+ }
280
+ _put({ type, color }, square) {
281
+ // check for piece
282
+ if (SYMBOLS.indexOf(type.toLowerCase()) === -1) {
283
+ return false;
284
+ }
285
+ // check for valid square
286
+ if (!(square in Ox88)) {
287
+ return false;
288
+ }
289
+ const sq = Ox88[square];
290
+ // don't let the user place more than one king
291
+ if (type == KING &&
292
+ !(this._kings[color] == EMPTY || this._kings[color] == sq)) {
293
+ return false;
294
+ }
295
+ const currentPieceOnSquare = this._board[sq];
296
+ // if one of the kings will be replaced by the piece from args, set the `_kings` respective entry to `EMPTY`
297
+ if (currentPieceOnSquare && currentPieceOnSquare.type === KING) {
298
+ this._kings[currentPieceOnSquare.color] = EMPTY;
299
+ }
300
+ this._set(sq, { type: type, color: color });
301
+ if (type === KING) {
302
+ this._kings[color] = sq;
303
+ }
304
+ return true;
305
+ }
306
+ _clear(sq) {
307
+ this._hash ^= this._pieceKey(sq);
308
+ delete this._board[sq];
309
+ }
310
+ remove(square) {
311
+ const piece = this.get(square);
312
+ this._clear(Ox88[square]);
313
+ if (piece && piece.type === KING) {
314
+ this._kings[piece.color] = EMPTY;
315
+ }
316
+ this._updateCastlingRights();
317
+ this._updateEnPassantSquare();
318
+ this._updateSetup(this.fen());
319
+ return piece;
320
+ }
321
+ _updateCastlingRights() {
322
+ this._hash ^= this._castlingKey();
323
+ /*
324
+ * CHESS960: a castling right survives only while the king remains on
325
+ * its backrank and the rights rook remains a rook of the same color on
326
+ * its original square.
327
+ */
328
+ for (const color of [WHITE, BLACK]) {
329
+ const backrank = color === WHITE ? RANK_1 : RANK_8;
330
+ const kingSquare = this._kings[color];
331
+ const kingOk = kingSquare !== EMPTY && rank(kingSquare) === backrank;
332
+ for (const side of [BITS.KSIDE_CASTLE, BITS.QSIDE_CASTLE]) {
333
+ if (!(this._castling[color] & side)) {
334
+ continue;
335
+ }
336
+ const rookSquare = side === BITS.KSIDE_CASTLE
337
+ ? this._castlingRooks[color].kside
338
+ : this._castlingRooks[color].qside;
339
+ const rookPiece = rookSquare === EMPTY ? undefined : this._board[rookSquare];
340
+ if (!kingOk ||
341
+ rookPiece?.type !== ROOK ||
342
+ rookPiece.color !== color) {
343
+ this._castling[color] ^= side;
344
+ if (side === BITS.KSIDE_CASTLE) {
345
+ this._castlingRooks[color].kside = EMPTY;
346
+ }
347
+ else {
348
+ this._castlingRooks[color].qside = EMPTY;
349
+ }
350
+ }
351
+ }
352
+ }
353
+ this._hash ^= this._castlingKey();
354
+ }
355
+ _updateEnPassantSquare() {
356
+ if (this._epSquare === EMPTY) {
357
+ return;
358
+ }
359
+ const startSquare = this._epSquare + (this._turn === WHITE ? -16 : 16);
360
+ const currentSquare = this._epSquare + (this._turn === WHITE ? 16 : -16);
361
+ const attackers = [currentSquare + 1, currentSquare - 1];
362
+ if (this._board[startSquare] !== null ||
363
+ this._board[this._epSquare] !== null ||
364
+ this._board[currentSquare]?.color !== swapColor(this._turn) ||
365
+ this._board[currentSquare]?.type !== PAWN) {
366
+ this._hash ^= this._epKey();
367
+ this._epSquare = EMPTY;
368
+ return;
369
+ }
370
+ const canCapture = (square) => !(square & 0x88) &&
371
+ this._board[square]?.color === this._turn &&
372
+ this._board[square]?.type === PAWN;
373
+ if (!attackers.some(canCapture)) {
374
+ this._hash ^= this._epKey();
375
+ this._epSquare = EMPTY;
376
+ }
377
+ }
378
+ attackers(square, attackedBy) {
379
+ if (!attackedBy) {
380
+ return attacked(this._board, this._turn, Ox88[square], true);
381
+ }
382
+ else {
383
+ return attacked(this._board, attackedBy, Ox88[square], true);
384
+ }
385
+ }
386
+ _isKingAttacked(color) {
387
+ return isKingAttacked(this._board, this._kings, color);
388
+ }
389
+ hash() {
390
+ return this._hash.toString(16);
391
+ }
392
+ isAttacked(square, attackedBy) {
393
+ return attacked(this._board, attackedBy, Ox88[square]);
394
+ }
395
+ isCheck() {
396
+ return this._isKingAttacked(this._turn);
397
+ }
398
+ inCheck() {
399
+ return this.isCheck();
400
+ }
401
+ isCheckmate() {
402
+ return this.isCheck() && this._moves().length === 0;
403
+ }
404
+ isStalemate() {
405
+ return !this.isCheck() && this._moves().length === 0;
406
+ }
407
+ isInsufficientMaterial() {
408
+ /*
409
+ * k.b. vs k.b. (of opposite colors) with mate in 1:
410
+ * 8/8/8/8/1b6/8/B1k5/K7 b - - 0 1
411
+ *
412
+ * k.b. vs k.n. with mate in 1:
413
+ * 8/8/8/8/1n6/8/B7/K1k5 b - - 2 1
414
+ */
415
+ const pieces = {
416
+ b: 0,
417
+ n: 0,
418
+ r: 0,
419
+ q: 0,
420
+ k: 0,
421
+ p: 0,
422
+ };
423
+ const bishops = [];
424
+ let numPieces = 0;
425
+ let squareColor = 0;
426
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
427
+ squareColor = (squareColor + 1) % 2;
428
+ if (i & 0x88) {
429
+ i += 7;
430
+ continue;
431
+ }
432
+ const piece = this._board[i];
433
+ if (piece) {
434
+ pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1;
435
+ if (piece.type === BISHOP) {
436
+ bishops.push(squareColor);
437
+ }
438
+ numPieces++;
439
+ }
440
+ }
441
+ // k vs. k
442
+ if (numPieces === 2) {
443
+ return true;
444
+ }
445
+ else if (
446
+ // k vs. kn .... or .... k vs. kb
447
+ numPieces === 3 &&
448
+ (pieces[BISHOP] === 1 || pieces[KNIGHT] === 1)) {
449
+ return true;
450
+ }
451
+ else if (numPieces === pieces[BISHOP] + 2) {
452
+ // kb vs. kb where any number of bishops are all on the same color
453
+ let sum = 0;
454
+ const len = bishops.length;
455
+ for (let i = 0; i < len; i++) {
456
+ sum += bishops[i];
457
+ }
458
+ if (sum === 0 || sum === len) {
459
+ return true;
460
+ }
461
+ }
462
+ return false;
463
+ }
464
+ isThreefoldRepetition() {
465
+ return this._getPositionCount(this._hash) >= 3;
466
+ }
467
+ isDrawByFiftyMoves() {
468
+ return this._halfMoves >= 100; // 50 moves per side = 100 half moves
469
+ }
470
+ isDraw() {
471
+ return (this.isDrawByFiftyMoves() ||
472
+ this.isStalemate() ||
473
+ this.isInsufficientMaterial() ||
474
+ this.isThreefoldRepetition());
475
+ }
476
+ isGameOver() {
477
+ return this.isCheckmate() || this.isDraw();
478
+ }
479
+ moves({ verbose = false, square = undefined, piece = undefined, } = {}) {
480
+ const moves = this._moves({ square, piece });
481
+ if (verbose) {
482
+ return moves.map((move) => this._createMove(move));
483
+ }
484
+ else {
485
+ return moves.map((move) => this._moveToSan(move, moves));
486
+ }
487
+ }
488
+ _moves({ legal = true, piece = undefined, square = undefined, } = {}) {
489
+ const moves = generatePseudoMoves(this._board, this._turn, this._epSquare, this._kings, this._castling, this._castlingRooks, { piece, square });
490
+ /*
491
+ * return all pseudo-legal moves (this includes moves that allow the king
492
+ * to be captured)
493
+ */
494
+ if (!legal || this._kings[this._turn] === -1) {
495
+ return moves;
496
+ }
497
+ const us = this._turn;
498
+ // filter out illegal moves
499
+ const legalMoves = [];
500
+ for (let i = 0, len = moves.length; i < len; i++) {
501
+ this._makeMove(moves[i]);
502
+ if (!this._isKingAttacked(us)) {
503
+ legalMoves.push(moves[i]);
504
+ }
505
+ this._undoMove();
506
+ }
507
+ return legalMoves;
508
+ }
509
+ move(move, { strict = false } = {}) {
510
+ /*
511
+ * The move function can be called with in the following parameters:
512
+ *
513
+ * .move('Nxb7') <- argument is a case-sensitive SAN string
514
+ *
515
+ * .move({ from: 'h7', <- argument is a move object
516
+ * to :'h8',
517
+ * promotion: 'q' })
518
+ *
519
+ *
520
+ * An optional strict argument may be supplied to tell chess.js to
521
+ * strictly follow the SAN specification.
522
+ */
523
+ let moveObj = null;
524
+ if (typeof move === 'string') {
525
+ moveObj = this._moveFromSan(move, strict);
526
+ }
527
+ else if (move === null) {
528
+ moveObj = this._moveFromSan(SAN_NULLMOVE, strict);
529
+ }
530
+ else if (typeof move === 'object') {
531
+ const moves = this._moves();
532
+ // convert the pretty move object to an ugly move object
533
+ for (let i = 0, len = moves.length; i < len; i++) {
534
+ if (move.from === algebraic(moves[i].from) &&
535
+ move.to === algebraic(moves[i].to) &&
536
+ (!('promotion' in moves[i]) || move.promotion === moves[i].promotion)) {
537
+ moveObj = moves[i];
538
+ break;
539
+ }
540
+ }
541
+ /*
542
+ * CHESS960: the king may have been dropped onto the castling rook
543
+ * ("king takes rook" castling notation). Match it against the
544
+ * generated castling moves.
545
+ */
546
+ if (!moveObj) {
547
+ const kingSquare = this._kings[this._turn];
548
+ const toSquare = Ox88[move.to];
549
+ if (kingSquare !== EMPTY &&
550
+ move.from === algebraic(kingSquare) &&
551
+ toSquare !== undefined &&
552
+ this._board[toSquare]?.type === ROOK &&
553
+ this._board[toSquare]?.color === this._turn) {
554
+ for (let i = 0, len = moves.length; i < len; i++) {
555
+ if (moves[i].flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE) &&
556
+ moves[i].rookFrom === toSquare) {
557
+ moveObj = moves[i];
558
+ break;
559
+ }
560
+ }
561
+ }
562
+ }
563
+ }
564
+ // failed to find move
565
+ if (!moveObj) {
566
+ if (typeof move === 'string') {
567
+ throw new Error(`Invalid move: ${move}`);
568
+ }
569
+ else {
570
+ throw new Error(`Invalid move: ${JSON.stringify(move)}`);
571
+ }
572
+ }
573
+ //disallow null moves when in check
574
+ if (this.isCheck() && moveObj.flags & BITS.NULL_MOVE) {
575
+ throw new Error('Null move not allowed when in check');
576
+ }
577
+ /*
578
+ * need to make a copy of move because we can't generate SAN after the move
579
+ * is made
580
+ */
581
+ const prettyMove = this._createMove(moveObj);
582
+ this._makeMove(moveObj);
583
+ this._incPositionCount();
584
+ return prettyMove;
585
+ }
586
+ _createMove(internal) {
587
+ const san = this._moveToSan(internal, this._moves({ legal: true }));
588
+ const before = this.fen();
589
+ // Generate the FEN for the 'after' key
590
+ this._makeMove(internal);
591
+ const after = this.fen();
592
+ this._undoMove();
593
+ return new Move(internal, san, before, after);
594
+ }
595
+ _push(move) {
596
+ this._history.push({
597
+ move,
598
+ kings: { b: this._kings.b, w: this._kings.w },
599
+ turn: this._turn,
600
+ castling: { b: this._castling.b, w: this._castling.w },
601
+ castlingRooks: {
602
+ w: {
603
+ kside: this._castlingRooks.w.kside,
604
+ qside: this._castlingRooks.w.qside,
605
+ },
606
+ b: {
607
+ kside: this._castlingRooks.b.kside,
608
+ qside: this._castlingRooks.b.qside,
609
+ },
610
+ },
611
+ epSquare: this._epSquare,
612
+ halfMoves: this._halfMoves,
613
+ moveNumber: this._moveNumber,
614
+ });
615
+ }
616
+ _movePiece(from, to) {
617
+ this._hash ^= this._pieceKey(from);
618
+ this._board[to] = this._board[from];
619
+ delete this._board[from];
620
+ this._hash ^= this._pieceKey(to);
621
+ }
622
+ _makeMove(move) {
623
+ const us = this._turn;
624
+ const them = swapColor(us);
625
+ this._push(move);
626
+ if (move.flags & BITS.NULL_MOVE) {
627
+ if (us === BLACK) {
628
+ this._moveNumber++;
629
+ }
630
+ this._halfMoves++;
631
+ this._turn = them;
632
+ this._epSquare = EMPTY;
633
+ return;
634
+ }
635
+ this._hash ^= this._epKey();
636
+ this._hash ^= this._castlingKey();
637
+ if (move.captured) {
638
+ this._hash ^= this._pieceKey(move.to);
639
+ }
640
+ if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) {
641
+ // CHESS960: relocate king and rook explicitly, which also handles the
642
+ // overlap cases (king passing over or landing on the rook's square)
643
+ const side = move.flags & BITS.KSIDE_CASTLE
644
+ ? BITS.KSIDE_CASTLE
645
+ : BITS.QSIDE_CASTLE;
646
+ const rookFrom = move.rookFrom !== undefined
647
+ ? move.rookFrom
648
+ : side === BITS.KSIDE_CASTLE
649
+ ? this._castlingRooks[us].kside
650
+ : this._castlingRooks[us].qside;
651
+ const rookTo = move.flags & BITS.KSIDE_CASTLE ? move.to - 1 : move.to + 1;
652
+ const kingPiece = this._board[move.from];
653
+ const rookPiece = rookFrom !== EMPTY ? this._board[rookFrom] : undefined;
654
+ let hashOut = this._pieceKey(move.from);
655
+ delete this._board[move.from];
656
+ if (rookPiece) {
657
+ hashOut ^= this._pieceKey(rookFrom);
658
+ delete this._board[rookFrom];
659
+ }
660
+ this._board[move.to] = kingPiece;
661
+ this._hash ^= hashOut ^ this._pieceKey(move.to);
662
+ if (rookPiece) {
663
+ this._board[rookTo] = rookPiece;
664
+ this._hash ^= this._pieceKey(rookTo);
665
+ }
666
+ }
667
+ else {
668
+ this._movePiece(move.from, move.to);
669
+ }
670
+ // if ep capture, remove the captured pawn
671
+ if (move.flags & BITS.EP_CAPTURE) {
672
+ if (this._turn === BLACK) {
673
+ this._clear(move.to - 16);
674
+ }
675
+ else {
676
+ this._clear(move.to + 16);
677
+ }
678
+ }
679
+ // if pawn promotion, replace with new piece
680
+ if (move.promotion) {
681
+ this._clear(move.to);
682
+ this._set(move.to, { type: move.promotion, color: us });
683
+ }
684
+ // if we moved the king
685
+ if (this._board[move.to].type === KING) {
686
+ this._kings[us] = move.to;
687
+ // turn off castling
688
+ this._castling[us] = 0;
689
+ this._castlingRooks[us] = { kside: EMPTY, qside: EMPTY };
690
+ }
691
+ // turn off castling if we move a rook
692
+ if (this._castling[us]) {
693
+ if (this._castlingRooks[us].kside !== EMPTY &&
694
+ move.from === this._castlingRooks[us].kside) {
695
+ this._castling[us] ^= BITS.KSIDE_CASTLE;
696
+ this._castlingRooks[us].kside = EMPTY;
697
+ }
698
+ if (this._castlingRooks[us].qside !== EMPTY &&
699
+ move.from === this._castlingRooks[us].qside) {
700
+ this._castling[us] ^= BITS.QSIDE_CASTLE;
701
+ this._castlingRooks[us].qside = EMPTY;
702
+ }
703
+ }
704
+ // turn off castling if we capture a rook
705
+ if (this._castling[them]) {
706
+ if (this._castlingRooks[them].kside !== EMPTY &&
707
+ move.to === this._castlingRooks[them].kside) {
708
+ this._castling[them] ^= BITS.KSIDE_CASTLE;
709
+ this._castlingRooks[them].kside = EMPTY;
710
+ }
711
+ if (this._castlingRooks[them].qside !== EMPTY &&
712
+ move.to === this._castlingRooks[them].qside) {
713
+ this._castling[them] ^= BITS.QSIDE_CASTLE;
714
+ this._castlingRooks[them].qside = EMPTY;
715
+ }
716
+ }
717
+ this._hash ^= this._castlingKey();
718
+ // if big pawn move, update the en passant square
719
+ if (move.flags & BITS.BIG_PAWN) {
720
+ let epSquare;
721
+ if (us === BLACK) {
722
+ epSquare = move.to - 16;
723
+ }
724
+ else {
725
+ epSquare = move.to + 16;
726
+ }
727
+ if ((!((move.to - 1) & 0x88) &&
728
+ this._board[move.to - 1]?.type === PAWN &&
729
+ this._board[move.to - 1]?.color === them) ||
730
+ (!((move.to + 1) & 0x88) &&
731
+ this._board[move.to + 1]?.type === PAWN &&
732
+ this._board[move.to + 1]?.color === them)) {
733
+ this._epSquare = epSquare;
734
+ this._hash ^= this._epKey();
735
+ }
736
+ else {
737
+ this._epSquare = EMPTY;
738
+ }
739
+ }
740
+ else {
741
+ this._epSquare = EMPTY;
742
+ }
743
+ // reset the 50 move counter if a pawn is moved or a piece is captured
744
+ if (move.piece === PAWN) {
745
+ this._halfMoves = 0;
746
+ }
747
+ else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
748
+ this._halfMoves = 0;
749
+ }
750
+ else {
751
+ this._halfMoves++;
752
+ }
753
+ if (us === BLACK) {
754
+ this._moveNumber++;
755
+ }
756
+ this._turn = them;
757
+ this._hash ^= SIDE_KEY;
758
+ }
759
+ undo() {
760
+ const hash = this._hash;
761
+ const move = this._undoMove();
762
+ if (move) {
763
+ const prettyMove = this._createMove(move);
764
+ this._decPositionCount(hash);
765
+ return prettyMove;
766
+ }
767
+ return null;
768
+ }
769
+ _undoMove() {
770
+ const old = this._history.pop();
771
+ if (old === undefined) {
772
+ return null;
773
+ }
774
+ this._hash ^= this._epKey();
775
+ this._hash ^= this._castlingKey();
776
+ const move = old.move;
777
+ this._kings = old.kings;
778
+ this._turn = old.turn;
779
+ this._castling = old.castling;
780
+ this._castlingRooks = {
781
+ w: {
782
+ kside: old.castlingRooks.w.kside,
783
+ qside: old.castlingRooks.w.qside,
784
+ },
785
+ b: {
786
+ kside: old.castlingRooks.b.kside,
787
+ qside: old.castlingRooks.b.qside,
788
+ },
789
+ };
790
+ this._epSquare = old.epSquare;
791
+ this._halfMoves = old.halfMoves;
792
+ this._moveNumber = old.moveNumber;
793
+ this._hash ^= this._epKey();
794
+ this._hash ^= this._castlingKey();
795
+ this._hash ^= SIDE_KEY;
796
+ const us = this._turn;
797
+ const them = swapColor(us);
798
+ if (move.flags & BITS.NULL_MOVE) {
799
+ return move;
800
+ }
801
+ if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) {
802
+ // CHESS960: reverse the explicit king/rook relocation, which also
803
+ // handles the overlap cases (rook returning to the king's square)
804
+ const side = move.flags & BITS.KSIDE_CASTLE
805
+ ? BITS.KSIDE_CASTLE
806
+ : BITS.QSIDE_CASTLE;
807
+ const rookFrom = move.rookFrom !== undefined
808
+ ? move.rookFrom
809
+ : side === BITS.KSIDE_CASTLE
810
+ ? this._castlingRooks[us].kside
811
+ : this._castlingRooks[us].qside;
812
+ const rookTo = move.flags & BITS.KSIDE_CASTLE ? move.to - 1 : move.to + 1;
813
+ const kingPiece = this._board[move.to];
814
+ const rookPiece = rookFrom !== EMPTY ? this._board[rookTo] : undefined;
815
+ let hashOut = this._pieceKey(move.to);
816
+ delete this._board[move.to];
817
+ if (rookPiece) {
818
+ hashOut ^= this._pieceKey(rookTo);
819
+ delete this._board[rookTo];
820
+ }
821
+ this._board[move.from] = kingPiece;
822
+ this._hash ^= hashOut ^ this._pieceKey(move.from);
823
+ if (rookPiece) {
824
+ this._board[rookFrom] = rookPiece;
825
+ this._hash ^= this._pieceKey(rookFrom);
826
+ }
827
+ }
828
+ else {
829
+ this._movePiece(move.to, move.from);
830
+ }
831
+ // to undo any promotions
832
+ if (move.piece) {
833
+ this._clear(move.from);
834
+ this._set(move.from, { type: move.piece, color: us });
835
+ }
836
+ if (move.captured) {
837
+ if (move.flags & BITS.EP_CAPTURE) {
838
+ // en passant capture
839
+ let index;
840
+ if (us === BLACK) {
841
+ index = move.to - 16;
842
+ }
843
+ else {
844
+ index = move.to + 16;
845
+ }
846
+ this._set(index, { type: PAWN, color: them });
847
+ }
848
+ else {
849
+ // regular capture
850
+ this._set(move.to, { type: move.captured, color: them });
851
+ }
852
+ }
853
+ return move;
854
+ }
855
+ pgn({ newline = '\n', maxWidth = 0, } = {}) {
856
+ /*
857
+ * using the specification from http://www.chessclub.com/help/PGN-spec
858
+ * example for html usage: .pgn({ max_width: 72, newline_char: "<br />" })
859
+ */
860
+ const result = [];
861
+ let headerExists = false;
862
+ /* add the PGN header information */
863
+ for (const i in this._header) {
864
+ /*
865
+ * TODO: order of enumerated properties in header object is not
866
+ * guaranteed, see ECMA-262 spec (section 12.6.4)
867
+ *
868
+ * By using HEADER_TEMPLATE, the order of tags should be preserved; we
869
+ * do have to check for null placeholders, though, and omit them
870
+ */
871
+ const headerTag = this._header[i];
872
+ if (headerTag)
873
+ result.push(`[${i} "${this._header[i]}"]` + newline);
874
+ headerExists = true;
875
+ }
876
+ if (headerExists && this._history.length) {
877
+ result.push(newline);
878
+ }
879
+ const appendComment = (moveString) => {
880
+ const comment = this._comments[this.fen()];
881
+ if (typeof comment !== 'undefined') {
882
+ const delimiter = moveString.length > 0 ? ' ' : '';
883
+ moveString = `${moveString}${delimiter}{${comment}}`;
884
+ }
885
+ return moveString;
886
+ };
887
+ // pop all of history onto reversed_history
888
+ const reversedHistory = [];
889
+ while (this._history.length > 0) {
890
+ reversedHistory.push(this._undoMove());
891
+ }
892
+ const moves = [];
893
+ let moveString = '';
894
+ // special case of a commented starting position with no moves
895
+ if (reversedHistory.length === 0) {
896
+ moves.push(appendComment(''));
897
+ }
898
+ // build the list of moves. a move_string looks like: "3. e3 e6"
899
+ while (reversedHistory.length > 0) {
900
+ moveString = appendComment(moveString);
901
+ const move = reversedHistory.pop();
902
+ // make TypeScript stop complaining about move being undefined
903
+ if (!move) {
904
+ break;
905
+ }
906
+ // if the position started with black to move, start PGN with #. ...
907
+ if (!this._history.length && move.color === 'b') {
908
+ const prefix = `${this._moveNumber}. ...`;
909
+ // is there a comment preceding the first move?
910
+ moveString = moveString ? `${moveString} ${prefix}` : prefix;
911
+ }
912
+ else if (move.color === 'w') {
913
+ // store the previous generated move_string if we have one
914
+ if (moveString.length) {
915
+ moves.push(moveString);
916
+ }
917
+ moveString = this._moveNumber + '.';
918
+ }
919
+ moveString =
920
+ moveString + ' ' + this._moveToSan(move, this._moves({ legal: true }));
921
+ this._makeMove(move);
922
+ }
923
+ // are there any other leftover moves?
924
+ if (moveString.length) {
925
+ moves.push(appendComment(moveString));
926
+ }
927
+ // is there a result? (there ALWAYS has to be a result according to spec; see Seven Tag Roster)
928
+ moves.push(this._header.Result || '*');
929
+ /*
930
+ * history should be back to what it was before we started generating PGN,
931
+ * so join together moves
932
+ */
933
+ if (maxWidth === 0) {
934
+ return result.join('') + moves.join(' ');
935
+ }
936
+ // TODO (jah): huh?
937
+ const strip = function () {
938
+ if (result.length > 0 && result[result.length - 1] === ' ') {
939
+ result.pop();
940
+ return true;
941
+ }
942
+ return false;
943
+ };
944
+ // NB: this does not preserve comment whitespace.
945
+ const wrapComment = function (width, move) {
946
+ for (const token of move.split(' ')) {
947
+ if (!token) {
948
+ continue;
949
+ }
950
+ if (width + token.length > maxWidth) {
951
+ while (strip()) {
952
+ width--;
953
+ }
954
+ result.push(newline);
955
+ width = 0;
956
+ }
957
+ result.push(token);
958
+ width += token.length;
959
+ result.push(' ');
960
+ width++;
961
+ }
962
+ if (strip()) {
963
+ width--;
964
+ }
965
+ return width;
966
+ };
967
+ // wrap the PGN output at max_width
968
+ let currentWidth = 0;
969
+ for (let i = 0; i < moves.length; i++) {
970
+ if (currentWidth + moves[i].length > maxWidth) {
971
+ if (moves[i].includes('{')) {
972
+ currentWidth = wrapComment(currentWidth, moves[i]);
973
+ continue;
974
+ }
975
+ }
976
+ // if the current move will push past max_width
977
+ if (currentWidth + moves[i].length > maxWidth && i !== 0) {
978
+ // don't end the line with whitespace
979
+ if (result[result.length - 1] === ' ') {
980
+ result.pop();
981
+ }
982
+ result.push(newline);
983
+ currentWidth = 0;
984
+ }
985
+ else if (i !== 0) {
986
+ result.push(' ');
987
+ currentWidth++;
988
+ }
989
+ result.push(moves[i]);
990
+ currentWidth += moves[i].length;
991
+ }
992
+ return result.join('');
993
+ }
994
+ /**
995
+ * @deprecated Use `setHeader` and `getHeaders` instead. This method will return null header tags (which is not what you want)
996
+ */
997
+ header(...args) {
998
+ for (let i = 0; i < args.length; i += 2) {
999
+ if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') {
1000
+ this._header[args[i]] = args[i + 1];
1001
+ }
1002
+ }
1003
+ return this._header;
1004
+ }
1005
+ // TODO: value validation per spec
1006
+ setHeader(key, value) {
1007
+ this._header[key] = value ?? SEVEN_TAG_ROSTER[key] ?? null;
1008
+ return this.getHeaders();
1009
+ }
1010
+ removeHeader(key) {
1011
+ if (key in this._header) {
1012
+ this._header[key] = SEVEN_TAG_ROSTER[key] || null;
1013
+ return true;
1014
+ }
1015
+ return false;
1016
+ }
1017
+ // return only non-null headers (omit placemarker nulls)
1018
+ getHeaders() {
1019
+ const nonNullHeaders = {};
1020
+ for (const [key, value] of Object.entries(this._header)) {
1021
+ if (value !== null) {
1022
+ nonNullHeaders[key] = value;
1023
+ }
1024
+ }
1025
+ return nonNullHeaders;
1026
+ }
1027
+ loadPgn(pgn, { strict = false, newlineChar = '\r?\n', } = {}) {
1028
+ // If newlineChar is not the default, replace all instances with \n
1029
+ if (newlineChar !== '\r?\n') {
1030
+ pgn = pgn.replace(new RegExp(newlineChar, 'g'), '\n');
1031
+ }
1032
+ const parsedPgn = parse(pgn);
1033
+ // Put the board in the starting position
1034
+ this.reset();
1035
+ // parse PGN header
1036
+ const headers = parsedPgn.headers;
1037
+ let fen = '';
1038
+ for (const key in headers) {
1039
+ // check to see user is including fen (possibly with wrong tag case)
1040
+ if (key.toLowerCase() === 'fen') {
1041
+ fen = headers[key];
1042
+ }
1043
+ this.header(key, headers[key]);
1044
+ }
1045
+ /*
1046
+ * the permissive parser should attempt to load a fen tag, even if it's the
1047
+ * wrong case and doesn't include a corresponding [SetUp "1"] tag
1048
+ */
1049
+ if (!strict) {
1050
+ if (fen) {
1051
+ this.load(fen, { preserveHeaders: true });
1052
+ }
1053
+ }
1054
+ else {
1055
+ /*
1056
+ * strict parser - load the starting position indicated by [Setup '1']
1057
+ * and [FEN position]
1058
+ */
1059
+ if (headers['SetUp'] === '1') {
1060
+ if (!('FEN' in headers)) {
1061
+ throw new Error('Invalid PGN: FEN tag must be supplied with SetUp tag');
1062
+ }
1063
+ // don't clear the headers when loading
1064
+ this.load(headers['FEN'], { preserveHeaders: true });
1065
+ }
1066
+ }
1067
+ let node = parsedPgn.root;
1068
+ while (node) {
1069
+ if (node.move) {
1070
+ const move = this._moveFromSan(node.move, strict);
1071
+ if (move == null) {
1072
+ throw new Error(`Invalid move in PGN: ${node.move}`);
1073
+ }
1074
+ else {
1075
+ this._makeMove(move);
1076
+ this._incPositionCount();
1077
+ }
1078
+ }
1079
+ if (node.comment !== undefined) {
1080
+ this._comments[this.fen()] = node.comment;
1081
+ }
1082
+ node = node.variations[0];
1083
+ }
1084
+ /*
1085
+ * Per section 8.2.6 of the PGN spec, the Result tag pair must match match
1086
+ * the termination marker. Only do this when headers are present, but the
1087
+ * result tag is missing
1088
+ */
1089
+ const result = parsedPgn.result;
1090
+ if (result &&
1091
+ Object.keys(this._header).length &&
1092
+ this._header['Result'] !== result) {
1093
+ this.setHeader('Result', result);
1094
+ }
1095
+ }
1096
+ /*
1097
+ * Convert a move from 0x88 coordinates to Standard Algebraic Notation
1098
+ * (SAN)
1099
+ *
1100
+ * @param {boolean} strict Use the strict SAN parser. It will throw errors
1101
+ * on overly disambiguated moves (see below):
1102
+ *
1103
+ * r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
1104
+ * 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
1105
+ * 4. ... Ne7 is technically the valid SAN
1106
+ */
1107
+ _moveToSan(move, moves) {
1108
+ if (move.flags & BITS.NULL_MOVE) {
1109
+ return SAN_NULLMOVE;
1110
+ }
1111
+ let output = moveToSan(move, moves);
1112
+ this._makeMove(move);
1113
+ if (this.isCheck()) {
1114
+ if (this.isCheckmate()) {
1115
+ output += '#';
1116
+ }
1117
+ else {
1118
+ output += '+';
1119
+ }
1120
+ }
1121
+ this._undoMove();
1122
+ return output;
1123
+ }
1124
+ // convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
1125
+ _moveFromSan(move, strict = false) {
1126
+ return moveFromSan(move, this._turn, this._moves(), strict);
1127
+ }
1128
+ ascii() {
1129
+ let s = ' +------------------------+\n';
1130
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
1131
+ // display the rank
1132
+ if (file(i) === 0) {
1133
+ s += ' ' + '87654321'[rank(i)] + ' |';
1134
+ }
1135
+ if (this._board[i]) {
1136
+ const piece = this._board[i].type;
1137
+ const color = this._board[i].color;
1138
+ const symbol = color === WHITE ? piece.toUpperCase() : piece.toLowerCase();
1139
+ s += ' ' + symbol + ' ';
1140
+ }
1141
+ else {
1142
+ s += ' . ';
1143
+ }
1144
+ if ((i + 1) & 0x88) {
1145
+ s += '|\n';
1146
+ i += 8;
1147
+ }
1148
+ }
1149
+ s += ' +------------------------+\n';
1150
+ s += ' a b c d e f g h';
1151
+ return s;
1152
+ }
1153
+ perft(depth) {
1154
+ const moves = this._moves({ legal: false });
1155
+ let nodes = 0;
1156
+ const color = this._turn;
1157
+ for (let i = 0, len = moves.length; i < len; i++) {
1158
+ this._makeMove(moves[i]);
1159
+ if (!this._isKingAttacked(color)) {
1160
+ if (depth - 1 > 0) {
1161
+ nodes += this.perft(depth - 1);
1162
+ }
1163
+ else {
1164
+ nodes++;
1165
+ }
1166
+ }
1167
+ this._undoMove();
1168
+ }
1169
+ return nodes;
1170
+ }
1171
+ setTurn(color) {
1172
+ if (this._turn == color) {
1173
+ return false;
1174
+ }
1175
+ this.move('--');
1176
+ return true;
1177
+ }
1178
+ turn() {
1179
+ return this._turn;
1180
+ }
1181
+ board() {
1182
+ const output = [];
1183
+ let row = [];
1184
+ for (let i = Ox88.a8; i <= Ox88.h1; i++) {
1185
+ if (this._board[i] == null) {
1186
+ row.push(null);
1187
+ }
1188
+ else {
1189
+ row.push({
1190
+ square: algebraic(i),
1191
+ type: this._board[i].type,
1192
+ color: this._board[i].color,
1193
+ });
1194
+ }
1195
+ if ((i + 1) & 0x88) {
1196
+ output.push(row);
1197
+ row = [];
1198
+ i += 8;
1199
+ }
1200
+ }
1201
+ return output;
1202
+ }
1203
+ squareColor(square) {
1204
+ if (square in Ox88) {
1205
+ const sq = Ox88[square];
1206
+ return (rank(sq) + file(sq)) % 2 === 0 ? 'light' : 'dark';
1207
+ }
1208
+ return null;
1209
+ }
1210
+ history({ verbose = false } = {}) {
1211
+ const reversedHistory = [];
1212
+ const moveHistory = [];
1213
+ while (this._history.length > 0) {
1214
+ reversedHistory.push(this._undoMove());
1215
+ }
1216
+ while (true) {
1217
+ const move = reversedHistory.pop();
1218
+ if (!move) {
1219
+ break;
1220
+ }
1221
+ if (verbose) {
1222
+ moveHistory.push(this._createMove(move));
1223
+ }
1224
+ else {
1225
+ moveHistory.push(this._moveToSan(move, this._moves()));
1226
+ }
1227
+ this._makeMove(move);
1228
+ }
1229
+ return moveHistory;
1230
+ }
1231
+ /*
1232
+ * Keeps track of position occurrence counts for the purpose of repetition
1233
+ * checking. Old positions are removed from the map if their counts are reduced to 0.
1234
+ */
1235
+ _getPositionCount(hash) {
1236
+ return this._positionCount.get(hash) ?? 0;
1237
+ }
1238
+ _incPositionCount() {
1239
+ this._positionCount.set(this._hash, (this._positionCount.get(this._hash) ?? 0) + 1);
1240
+ }
1241
+ _decPositionCount(hash) {
1242
+ const currentCount = this._positionCount.get(hash) ?? 0;
1243
+ if (currentCount === 1) {
1244
+ this._positionCount.delete(hash);
1245
+ }
1246
+ else {
1247
+ this._positionCount.set(hash, currentCount - 1);
1248
+ }
1249
+ }
1250
+ _pruneComments() {
1251
+ const reversedHistory = [];
1252
+ const currentComments = {};
1253
+ const copyComment = (fen) => {
1254
+ if (fen in this._comments) {
1255
+ currentComments[fen] = this._comments[fen];
1256
+ }
1257
+ };
1258
+ while (this._history.length > 0) {
1259
+ reversedHistory.push(this._undoMove());
1260
+ }
1261
+ copyComment(this.fen());
1262
+ while (true) {
1263
+ const move = reversedHistory.pop();
1264
+ if (!move) {
1265
+ break;
1266
+ }
1267
+ this._makeMove(move);
1268
+ copyComment(this.fen());
1269
+ }
1270
+ this._comments = currentComments;
1271
+ }
1272
+ getComment() {
1273
+ return this._comments[this.fen()];
1274
+ }
1275
+ setComment(comment) {
1276
+ this._comments[this.fen()] = comment.replace('{', '[').replace('}', ']');
1277
+ }
1278
+ /**
1279
+ * @deprecated Renamed to `removeComment` for consistency
1280
+ */
1281
+ deleteComment() {
1282
+ return this.removeComment();
1283
+ }
1284
+ removeComment() {
1285
+ const comment = this._comments[this.fen()];
1286
+ delete this._comments[this.fen()];
1287
+ return comment;
1288
+ }
1289
+ getComments() {
1290
+ this._pruneComments();
1291
+ return Object.keys(this._comments).map((fen) => {
1292
+ return { fen: fen, comment: this._comments[fen] };
1293
+ });
1294
+ }
1295
+ /**
1296
+ * @deprecated Renamed to `removeComments` for consistency
1297
+ */
1298
+ deleteComments() {
1299
+ return this.removeComments();
1300
+ }
1301
+ removeComments() {
1302
+ this._pruneComments();
1303
+ return Object.keys(this._comments).map((fen) => {
1304
+ const comment = this._comments[fen];
1305
+ delete this._comments[fen];
1306
+ return { fen: fen, comment: comment };
1307
+ });
1308
+ }
1309
+ setCastlingRights(color, rights) {
1310
+ for (const side of [KING, QUEEN]) {
1311
+ if (rights[side] !== undefined) {
1312
+ if (rights[side]) {
1313
+ this._castling[color] |= SIDES[side];
1314
+ }
1315
+ else {
1316
+ this._castling[color] &= ~SIDES[side];
1317
+ }
1318
+ }
1319
+ }
1320
+ this._updateCastlingRights();
1321
+ const result = this.getCastlingRights(color);
1322
+ return ((rights[KING] === undefined || rights[KING] === result[KING]) &&
1323
+ (rights[QUEEN] === undefined || rights[QUEEN] === result[QUEEN]));
1324
+ }
1325
+ getCastlingRights(color) {
1326
+ return {
1327
+ [KING]: (this._castling[color] & SIDES[KING]) !== 0,
1328
+ [QUEEN]: (this._castling[color] & SIDES[QUEEN]) !== 0,
1329
+ };
1330
+ }
1331
+ moveNumber() {
1332
+ return this._moveNumber;
1333
+ }
1334
+ }
1335
+ //# sourceMappingURL=chess.js.map