@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.
- package/LICENSE +23 -0
- package/README.md +87 -2
- package/dist/attack.d.ts +32 -0
- package/dist/attack.d.ts.map +1 -0
- package/dist/attack.js +103 -0
- package/dist/attack.js.map +1 -0
- package/dist/board.d.ts +33 -0
- package/dist/board.d.ts.map +1 -0
- package/dist/board.js +49 -0
- package/dist/board.js.map +1 -0
- package/dist/chess.d.ts +232 -0
- package/dist/chess.d.ts.map +1 -0
- package/dist/chess.js +1335 -0
- package/dist/chess.js.map +1 -0
- package/dist/fen.d.ts +38 -0
- package/dist/fen.d.ts.map +1 -0
- package/dist/fen.js +308 -0
- package/dist/fen.js.map +1 -0
- package/dist/hash.d.ts +34 -0
- package/dist/hash.d.ts.map +1 -0
- package/dist/hash.js +98 -0
- package/dist/hash.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/move.d.ts +55 -0
- package/dist/move.d.ts.map +1 -0
- package/dist/move.js +79 -0
- package/dist/move.js.map +1 -0
- package/dist/movegen.d.ts +34 -0
- package/dist/movegen.d.ts.map +1 -0
- package/dist/movegen.js +242 -0
- package/dist/movegen.js.map +1 -0
- package/dist/node.d.ts +8 -0
- package/dist/node.d.ts.map +1 -0
- package/dist/node.js +2 -0
- package/dist/node.js.map +1 -0
- package/dist/notation.d.ts +33 -0
- package/dist/notation.d.ts.map +1 -0
- package/dist/notation.js +257 -0
- package/dist/notation.js.map +1 -0
- package/dist/pgn.d.ts +12 -0
- package/dist/pgn.d.ts.map +1 -0
- package/dist/pgn.js +1347 -0
- package/dist/pgn.js.map +1 -0
- package/dist/types.d.ts +118 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +237 -0
- package/dist/types.js.map +1 -0
- 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
|