@alepot55/chessboardjs 2.3.3 → 2.3.5
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/config/rollup.config.js +2 -1
- package/dist/chessboard.cjs.js +340 -246
- package/dist/chessboard.css +28 -6
- package/dist/chessboard.esm.js +340 -246
- package/dist/chessboard.iife.js +341 -246
- package/dist/chessboard.umd.js +340 -246
- package/package.json +6 -8
- package/src/components/Piece.js +11 -11
- package/src/components/Square.js +1 -1
- package/src/core/Chessboard.js +87 -86
- package/src/services/BoardService.js +20 -1
- package/src/services/EventService.js +131 -45
- package/src/services/MoveService.js +81 -94
- package/src/services/PieceService.js +9 -8
- package/src/styles/index.css +8 -0
- package/tests/unit/chessboard-robust.test.js +0 -44
- package/tools/build-html-examples.cjs +80 -0
- package/tools/build-html-examples.js +78 -0
- package/chessboard.bundle.js +0 -4072
package/chessboard.bundle.js
DELETED
|
@@ -1,4072 +0,0 @@
|
|
|
1
|
-
var Chessboard = (function (exports) {
|
|
2
|
-
'use strict';
|
|
3
|
-
|
|
4
|
-
/**
|
|
5
|
-
* @license
|
|
6
|
-
* Copyright (c) 2025, Jeff Hlywa (jhlywa@gmail.com)
|
|
7
|
-
* All rights reserved.
|
|
8
|
-
*
|
|
9
|
-
* Redistribution and use in source and binary forms, with or without
|
|
10
|
-
* modification, are permitted provided that the following conditions are met:
|
|
11
|
-
*
|
|
12
|
-
* 1. Redistributions of source code must retain the above copyright notice,
|
|
13
|
-
* this list of conditions and the following disclaimer.
|
|
14
|
-
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
15
|
-
* this list of conditions and the following disclaimer in the documentation
|
|
16
|
-
* and/or other materials provided with the distribution.
|
|
17
|
-
*
|
|
18
|
-
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
19
|
-
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
20
|
-
* IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
21
|
-
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
|
|
22
|
-
* LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
23
|
-
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
24
|
-
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
25
|
-
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
26
|
-
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
27
|
-
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
28
|
-
* POSSIBILITY OF SUCH DAMAGE.
|
|
29
|
-
*/
|
|
30
|
-
const WHITE = 'w';
|
|
31
|
-
const BLACK = 'b';
|
|
32
|
-
const PAWN = 'p';
|
|
33
|
-
const KNIGHT = 'n';
|
|
34
|
-
const BISHOP = 'b';
|
|
35
|
-
const ROOK = 'r';
|
|
36
|
-
const QUEEN = 'q';
|
|
37
|
-
const KING = 'k';
|
|
38
|
-
const DEFAULT_POSITION = 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
39
|
-
let Move$1 = class Move {
|
|
40
|
-
color;
|
|
41
|
-
from;
|
|
42
|
-
to;
|
|
43
|
-
piece;
|
|
44
|
-
captured;
|
|
45
|
-
promotion;
|
|
46
|
-
/**
|
|
47
|
-
* @deprecated This field is deprecated and will be removed in version 2.0.0.
|
|
48
|
-
* Please use move descriptor functions instead: `isCapture`, `isPromotion`,
|
|
49
|
-
* `isEnPassant`, `isKingsideCastle`, `isQueensideCastle`, `isCastle`, and
|
|
50
|
-
* `isBigPawn`
|
|
51
|
-
*/
|
|
52
|
-
flags;
|
|
53
|
-
san;
|
|
54
|
-
lan;
|
|
55
|
-
before;
|
|
56
|
-
after;
|
|
57
|
-
constructor(chess, internal) {
|
|
58
|
-
const { color, piece, from, to, flags, captured, promotion } = internal;
|
|
59
|
-
const fromAlgebraic = algebraic(from);
|
|
60
|
-
const toAlgebraic = algebraic(to);
|
|
61
|
-
this.color = color;
|
|
62
|
-
this.piece = piece;
|
|
63
|
-
this.from = fromAlgebraic;
|
|
64
|
-
this.to = toAlgebraic;
|
|
65
|
-
/*
|
|
66
|
-
* HACK: The chess['_method']() calls below invoke private methods in the
|
|
67
|
-
* Chess class to generate SAN and FEN. It's a bit of a hack, but makes the
|
|
68
|
-
* code cleaner elsewhere.
|
|
69
|
-
*/
|
|
70
|
-
this.san = chess['_moveToSan'](internal, chess['_moves']({ legal: true }));
|
|
71
|
-
this.lan = fromAlgebraic + toAlgebraic;
|
|
72
|
-
this.before = chess.fen();
|
|
73
|
-
// Generate the FEN for the 'after' key
|
|
74
|
-
chess['_makeMove'](internal);
|
|
75
|
-
this.after = chess.fen();
|
|
76
|
-
chess['_undoMove']();
|
|
77
|
-
// Build the text representation of the move flags
|
|
78
|
-
this.flags = '';
|
|
79
|
-
for (const flag in BITS) {
|
|
80
|
-
if (BITS[flag] & flags) {
|
|
81
|
-
this.flags += FLAGS[flag];
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
if (captured) {
|
|
85
|
-
this.captured = captured;
|
|
86
|
-
}
|
|
87
|
-
if (promotion) {
|
|
88
|
-
this.promotion = promotion;
|
|
89
|
-
this.lan += promotion;
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
isCapture() {
|
|
93
|
-
return this.flags.indexOf(FLAGS['CAPTURE']) > -1;
|
|
94
|
-
}
|
|
95
|
-
isPromotion() {
|
|
96
|
-
return this.flags.indexOf(FLAGS['PROMOTION']) > -1;
|
|
97
|
-
}
|
|
98
|
-
isEnPassant() {
|
|
99
|
-
return this.flags.indexOf(FLAGS['EP_CAPTURE']) > -1;
|
|
100
|
-
}
|
|
101
|
-
isKingsideCastle() {
|
|
102
|
-
return this.flags.indexOf(FLAGS['KSIDE_CASTLE']) > -1;
|
|
103
|
-
}
|
|
104
|
-
isQueensideCastle() {
|
|
105
|
-
return this.flags.indexOf(FLAGS['QSIDE_CASTLE']) > -1;
|
|
106
|
-
}
|
|
107
|
-
isBigPawn() {
|
|
108
|
-
return this.flags.indexOf(FLAGS['BIG_PAWN']) > -1;
|
|
109
|
-
}
|
|
110
|
-
};
|
|
111
|
-
const EMPTY = -1;
|
|
112
|
-
const FLAGS = {
|
|
113
|
-
NORMAL: 'n',
|
|
114
|
-
CAPTURE: 'c',
|
|
115
|
-
BIG_PAWN: 'b',
|
|
116
|
-
EP_CAPTURE: 'e',
|
|
117
|
-
PROMOTION: 'p',
|
|
118
|
-
KSIDE_CASTLE: 'k',
|
|
119
|
-
QSIDE_CASTLE: 'q',
|
|
120
|
-
};
|
|
121
|
-
const BITS = {
|
|
122
|
-
NORMAL: 1,
|
|
123
|
-
CAPTURE: 2,
|
|
124
|
-
BIG_PAWN: 4,
|
|
125
|
-
EP_CAPTURE: 8,
|
|
126
|
-
PROMOTION: 16,
|
|
127
|
-
KSIDE_CASTLE: 32,
|
|
128
|
-
QSIDE_CASTLE: 64,
|
|
129
|
-
};
|
|
130
|
-
/*
|
|
131
|
-
* NOTES ABOUT 0x88 MOVE GENERATION ALGORITHM
|
|
132
|
-
* ----------------------------------------------------------------------------
|
|
133
|
-
* From https://github.com/jhlywa/chess.js/issues/230
|
|
134
|
-
*
|
|
135
|
-
* A lot of people are confused when they first see the internal representation
|
|
136
|
-
* of chess.js. It uses the 0x88 Move Generation Algorithm which internally
|
|
137
|
-
* stores the board as an 8x16 array. This is purely for efficiency but has a
|
|
138
|
-
* couple of interesting benefits:
|
|
139
|
-
*
|
|
140
|
-
* 1. 0x88 offers a very inexpensive "off the board" check. Bitwise AND (&) any
|
|
141
|
-
* square with 0x88, if the result is non-zero then the square is off the
|
|
142
|
-
* board. For example, assuming a knight square A8 (0 in 0x88 notation),
|
|
143
|
-
* there are 8 possible directions in which the knight can move. These
|
|
144
|
-
* directions are relative to the 8x16 board and are stored in the
|
|
145
|
-
* PIECE_OFFSETS map. One possible move is A8 - 18 (up one square, and two
|
|
146
|
-
* squares to the left - which is off the board). 0 - 18 = -18 & 0x88 = 0x88
|
|
147
|
-
* (because of two-complement representation of -18). The non-zero result
|
|
148
|
-
* means the square is off the board and the move is illegal. Take the
|
|
149
|
-
* opposite move (from A8 to C7), 0 + 18 = 18 & 0x88 = 0. A result of zero
|
|
150
|
-
* means the square is on the board.
|
|
151
|
-
*
|
|
152
|
-
* 2. The relative distance (or difference) between two squares on a 8x16 board
|
|
153
|
-
* is unique and can be used to inexpensively determine if a piece on a
|
|
154
|
-
* square can attack any other arbitrary square. For example, let's see if a
|
|
155
|
-
* pawn on E7 can attack E2. The difference between E7 (20) - E2 (100) is
|
|
156
|
-
* -80. We add 119 to make the ATTACKS array index non-negative (because the
|
|
157
|
-
* worst case difference is A8 - H1 = -119). The ATTACKS array contains a
|
|
158
|
-
* bitmask of pieces that can attack from that distance and direction.
|
|
159
|
-
* ATTACKS[-80 + 119=39] gives us 24 or 0b11000 in binary. Look at the
|
|
160
|
-
* PIECE_MASKS map to determine the mask for a given piece type. In our pawn
|
|
161
|
-
* example, we would check to see if 24 & 0x1 is non-zero, which it is
|
|
162
|
-
* not. So, naturally, a pawn on E7 can't attack a piece on E2. However, a
|
|
163
|
-
* rook can since 24 & 0x8 is non-zero. The only thing left to check is that
|
|
164
|
-
* there are no blocking pieces between E7 and E2. That's where the RAYS
|
|
165
|
-
* array comes in. It provides an offset (in this case 16) to add to E7 (20)
|
|
166
|
-
* to check for blocking pieces. E7 (20) + 16 = E6 (36) + 16 = E5 (52) etc.
|
|
167
|
-
*/
|
|
168
|
-
// prettier-ignore
|
|
169
|
-
// eslint-disable-next-line
|
|
170
|
-
const Ox88 = {
|
|
171
|
-
a8: 0, b8: 1, c8: 2, d8: 3, e8: 4, f8: 5, g8: 6, h8: 7,
|
|
172
|
-
a7: 16, b7: 17, c7: 18, d7: 19, e7: 20, f7: 21, g7: 22, h7: 23,
|
|
173
|
-
a6: 32, b6: 33, c6: 34, d6: 35, e6: 36, f6: 37, g6: 38, h6: 39,
|
|
174
|
-
a5: 48, b5: 49, c5: 50, d5: 51, e5: 52, f5: 53, g5: 54, h5: 55,
|
|
175
|
-
a4: 64, b4: 65, c4: 66, d4: 67, e4: 68, f4: 69, g4: 70, h4: 71,
|
|
176
|
-
a3: 80, b3: 81, c3: 82, d3: 83, e3: 84, f3: 85, g3: 86, h3: 87,
|
|
177
|
-
a2: 96, b2: 97, c2: 98, d2: 99, e2: 100, f2: 101, g2: 102, h2: 103,
|
|
178
|
-
a1: 112, b1: 113, c1: 114, d1: 115, e1: 116, f1: 117, g1: 118, h1: 119
|
|
179
|
-
};
|
|
180
|
-
const PAWN_OFFSETS = {
|
|
181
|
-
b: [16, 32, 17, 15],
|
|
182
|
-
w: [-16, -32, -17, -15],
|
|
183
|
-
};
|
|
184
|
-
const PIECE_OFFSETS = {
|
|
185
|
-
n: [-18, -33, -31, -14, 18, 33, 31, 14],
|
|
186
|
-
b: [-17, -15, 17, 15],
|
|
187
|
-
r: [-16, 1, 16, -1],
|
|
188
|
-
q: [-17, -16, -15, 1, 17, 16, 15, -1],
|
|
189
|
-
k: [-17, -16, -15, 1, 17, 16, 15, -1],
|
|
190
|
-
};
|
|
191
|
-
// prettier-ignore
|
|
192
|
-
const ATTACKS = [
|
|
193
|
-
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20, 0,
|
|
194
|
-
0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0,
|
|
195
|
-
0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0,
|
|
196
|
-
0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0,
|
|
197
|
-
0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0,
|
|
198
|
-
0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0,
|
|
199
|
-
0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
|
|
200
|
-
24, 24, 24, 24, 24, 24, 56, 0, 56, 24, 24, 24, 24, 24, 24, 0,
|
|
201
|
-
0, 0, 0, 0, 0, 2, 53, 56, 53, 2, 0, 0, 0, 0, 0, 0,
|
|
202
|
-
0, 0, 0, 0, 0, 20, 2, 24, 2, 20, 0, 0, 0, 0, 0, 0,
|
|
203
|
-
0, 0, 0, 0, 20, 0, 0, 24, 0, 0, 20, 0, 0, 0, 0, 0,
|
|
204
|
-
0, 0, 0, 20, 0, 0, 0, 24, 0, 0, 0, 20, 0, 0, 0, 0,
|
|
205
|
-
0, 0, 20, 0, 0, 0, 0, 24, 0, 0, 0, 0, 20, 0, 0, 0,
|
|
206
|
-
0, 20, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 20, 0, 0,
|
|
207
|
-
20, 0, 0, 0, 0, 0, 0, 24, 0, 0, 0, 0, 0, 0, 20
|
|
208
|
-
];
|
|
209
|
-
// prettier-ignore
|
|
210
|
-
const RAYS = [
|
|
211
|
-
17, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 0, 15, 0,
|
|
212
|
-
0, 17, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 0, 15, 0, 0,
|
|
213
|
-
0, 0, 17, 0, 0, 0, 0, 16, 0, 0, 0, 0, 15, 0, 0, 0,
|
|
214
|
-
0, 0, 0, 17, 0, 0, 0, 16, 0, 0, 0, 15, 0, 0, 0, 0,
|
|
215
|
-
0, 0, 0, 0, 17, 0, 0, 16, 0, 0, 15, 0, 0, 0, 0, 0,
|
|
216
|
-
0, 0, 0, 0, 0, 17, 0, 16, 0, 15, 0, 0, 0, 0, 0, 0,
|
|
217
|
-
0, 0, 0, 0, 0, 0, 17, 16, 15, 0, 0, 0, 0, 0, 0, 0,
|
|
218
|
-
1, 1, 1, 1, 1, 1, 1, 0, -1, -1, -1, -1, -1, -1, -1, 0,
|
|
219
|
-
0, 0, 0, 0, 0, 0, -15, -16, -17, 0, 0, 0, 0, 0, 0, 0,
|
|
220
|
-
0, 0, 0, 0, 0, -15, 0, -16, 0, -17, 0, 0, 0, 0, 0, 0,
|
|
221
|
-
0, 0, 0, 0, -15, 0, 0, -16, 0, 0, -17, 0, 0, 0, 0, 0,
|
|
222
|
-
0, 0, 0, -15, 0, 0, 0, -16, 0, 0, 0, -17, 0, 0, 0, 0,
|
|
223
|
-
0, 0, -15, 0, 0, 0, 0, -16, 0, 0, 0, 0, -17, 0, 0, 0,
|
|
224
|
-
0, -15, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, -17, 0, 0,
|
|
225
|
-
-15, 0, 0, 0, 0, 0, 0, -16, 0, 0, 0, 0, 0, 0, -17
|
|
226
|
-
];
|
|
227
|
-
const PIECE_MASKS = { p: 0x1, n: 0x2, b: 0x4, r: 0x8, q: 0x10, k: 0x20 };
|
|
228
|
-
const SYMBOLS = 'pnbrqkPNBRQK';
|
|
229
|
-
const PROMOTIONS = [KNIGHT, BISHOP, ROOK, QUEEN];
|
|
230
|
-
const RANK_1 = 7;
|
|
231
|
-
const RANK_2 = 6;
|
|
232
|
-
/*
|
|
233
|
-
* const RANK_3 = 5
|
|
234
|
-
* const RANK_4 = 4
|
|
235
|
-
* const RANK_5 = 3
|
|
236
|
-
* const RANK_6 = 2
|
|
237
|
-
*/
|
|
238
|
-
const RANK_7 = 1;
|
|
239
|
-
const RANK_8 = 0;
|
|
240
|
-
const SIDES = {
|
|
241
|
-
[KING]: BITS.KSIDE_CASTLE,
|
|
242
|
-
[QUEEN]: BITS.QSIDE_CASTLE,
|
|
243
|
-
};
|
|
244
|
-
const ROOKS = {
|
|
245
|
-
w: [
|
|
246
|
-
{ square: Ox88.a1, flag: BITS.QSIDE_CASTLE },
|
|
247
|
-
{ square: Ox88.h1, flag: BITS.KSIDE_CASTLE },
|
|
248
|
-
],
|
|
249
|
-
b: [
|
|
250
|
-
{ square: Ox88.a8, flag: BITS.QSIDE_CASTLE },
|
|
251
|
-
{ square: Ox88.h8, flag: BITS.KSIDE_CASTLE },
|
|
252
|
-
],
|
|
253
|
-
};
|
|
254
|
-
const SECOND_RANK = { b: RANK_7, w: RANK_2 };
|
|
255
|
-
const TERMINATION_MARKERS = ['1-0', '0-1', '1/2-1/2', '*'];
|
|
256
|
-
// Extracts the zero-based rank of an 0x88 square.
|
|
257
|
-
function rank(square) {
|
|
258
|
-
return square >> 4;
|
|
259
|
-
}
|
|
260
|
-
// Extracts the zero-based file of an 0x88 square.
|
|
261
|
-
function file(square) {
|
|
262
|
-
return square & 0xf;
|
|
263
|
-
}
|
|
264
|
-
function isDigit(c) {
|
|
265
|
-
return '0123456789'.indexOf(c) !== -1;
|
|
266
|
-
}
|
|
267
|
-
// Converts a 0x88 square to algebraic notation.
|
|
268
|
-
function algebraic(square) {
|
|
269
|
-
const f = file(square);
|
|
270
|
-
const r = rank(square);
|
|
271
|
-
return ('abcdefgh'.substring(f, f + 1) +
|
|
272
|
-
'87654321'.substring(r, r + 1));
|
|
273
|
-
}
|
|
274
|
-
function swapColor(color) {
|
|
275
|
-
return color === WHITE ? BLACK : WHITE;
|
|
276
|
-
}
|
|
277
|
-
function validateFen(fen) {
|
|
278
|
-
// 1st criterion: 6 space-seperated fields?
|
|
279
|
-
const tokens = fen.split(/\s+/);
|
|
280
|
-
if (tokens.length !== 6) {
|
|
281
|
-
return {
|
|
282
|
-
ok: false,
|
|
283
|
-
error: 'Invalid FEN: must contain six space-delimited fields',
|
|
284
|
-
};
|
|
285
|
-
}
|
|
286
|
-
// 2nd criterion: move number field is a integer value > 0?
|
|
287
|
-
const moveNumber = parseInt(tokens[5], 10);
|
|
288
|
-
if (isNaN(moveNumber) || moveNumber <= 0) {
|
|
289
|
-
return {
|
|
290
|
-
ok: false,
|
|
291
|
-
error: 'Invalid FEN: move number must be a positive integer',
|
|
292
|
-
};
|
|
293
|
-
}
|
|
294
|
-
// 3rd criterion: half move counter is an integer >= 0?
|
|
295
|
-
const halfMoves = parseInt(tokens[4], 10);
|
|
296
|
-
if (isNaN(halfMoves) || halfMoves < 0) {
|
|
297
|
-
return {
|
|
298
|
-
ok: false,
|
|
299
|
-
error: 'Invalid FEN: half move counter number must be a non-negative integer',
|
|
300
|
-
};
|
|
301
|
-
}
|
|
302
|
-
// 4th criterion: 4th field is a valid e.p.-string?
|
|
303
|
-
if (!/^(-|[abcdefgh][36])$/.test(tokens[3])) {
|
|
304
|
-
return { ok: false, error: 'Invalid FEN: en-passant square is invalid' };
|
|
305
|
-
}
|
|
306
|
-
// 5th criterion: 3th field is a valid castle-string?
|
|
307
|
-
if (/[^kKqQ-]/.test(tokens[2])) {
|
|
308
|
-
return { ok: false, error: 'Invalid FEN: castling availability is invalid' };
|
|
309
|
-
}
|
|
310
|
-
// 6th criterion: 2nd field is "w" (white) or "b" (black)?
|
|
311
|
-
if (!/^(w|b)$/.test(tokens[1])) {
|
|
312
|
-
return { ok: false, error: 'Invalid FEN: side-to-move is invalid' };
|
|
313
|
-
}
|
|
314
|
-
// 7th criterion: 1st field contains 8 rows?
|
|
315
|
-
const rows = tokens[0].split('/');
|
|
316
|
-
if (rows.length !== 8) {
|
|
317
|
-
return {
|
|
318
|
-
ok: false,
|
|
319
|
-
error: "Invalid FEN: piece data does not contain 8 '/'-delimited rows",
|
|
320
|
-
};
|
|
321
|
-
}
|
|
322
|
-
// 8th criterion: every row is valid?
|
|
323
|
-
for (let i = 0; i < rows.length; i++) {
|
|
324
|
-
// check for right sum of fields AND not two numbers in succession
|
|
325
|
-
let sumFields = 0;
|
|
326
|
-
let previousWasNumber = false;
|
|
327
|
-
for (let k = 0; k < rows[i].length; k++) {
|
|
328
|
-
if (isDigit(rows[i][k])) {
|
|
329
|
-
if (previousWasNumber) {
|
|
330
|
-
return {
|
|
331
|
-
ok: false,
|
|
332
|
-
error: 'Invalid FEN: piece data is invalid (consecutive number)',
|
|
333
|
-
};
|
|
334
|
-
}
|
|
335
|
-
sumFields += parseInt(rows[i][k], 10);
|
|
336
|
-
previousWasNumber = true;
|
|
337
|
-
}
|
|
338
|
-
else {
|
|
339
|
-
if (!/^[prnbqkPRNBQK]$/.test(rows[i][k])) {
|
|
340
|
-
return {
|
|
341
|
-
ok: false,
|
|
342
|
-
error: 'Invalid FEN: piece data is invalid (invalid piece)',
|
|
343
|
-
};
|
|
344
|
-
}
|
|
345
|
-
sumFields += 1;
|
|
346
|
-
previousWasNumber = false;
|
|
347
|
-
}
|
|
348
|
-
}
|
|
349
|
-
if (sumFields !== 8) {
|
|
350
|
-
return {
|
|
351
|
-
ok: false,
|
|
352
|
-
error: 'Invalid FEN: piece data is invalid (too many squares in rank)',
|
|
353
|
-
};
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
// 9th criterion: is en-passant square legal?
|
|
357
|
-
if ((tokens[3][1] == '3' && tokens[1] == 'w') ||
|
|
358
|
-
(tokens[3][1] == '6' && tokens[1] == 'b')) {
|
|
359
|
-
return { ok: false, error: 'Invalid FEN: illegal en-passant square' };
|
|
360
|
-
}
|
|
361
|
-
// 10th criterion: does chess position contain exact two kings?
|
|
362
|
-
const kings = [
|
|
363
|
-
{ color: 'white', regex: /K/g },
|
|
364
|
-
{ color: 'black', regex: /k/g },
|
|
365
|
-
];
|
|
366
|
-
for (const { color, regex } of kings) {
|
|
367
|
-
if (!regex.test(tokens[0])) {
|
|
368
|
-
return { ok: false, error: `Invalid FEN: missing ${color} king` };
|
|
369
|
-
}
|
|
370
|
-
if ((tokens[0].match(regex) || []).length > 1) {
|
|
371
|
-
return { ok: false, error: `Invalid FEN: too many ${color} kings` };
|
|
372
|
-
}
|
|
373
|
-
}
|
|
374
|
-
// 11th criterion: are any pawns on the first or eighth rows?
|
|
375
|
-
if (Array.from(rows[0] + rows[7]).some((char) => char.toUpperCase() === 'P')) {
|
|
376
|
-
return {
|
|
377
|
-
ok: false,
|
|
378
|
-
error: 'Invalid FEN: some pawns are on the edge rows',
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
return { ok: true };
|
|
382
|
-
}
|
|
383
|
-
// this function is used to uniquely identify ambiguous moves
|
|
384
|
-
function getDisambiguator(move, moves) {
|
|
385
|
-
const from = move.from;
|
|
386
|
-
const to = move.to;
|
|
387
|
-
const piece = move.piece;
|
|
388
|
-
let ambiguities = 0;
|
|
389
|
-
let sameRank = 0;
|
|
390
|
-
let sameFile = 0;
|
|
391
|
-
for (let i = 0, len = moves.length; i < len; i++) {
|
|
392
|
-
const ambigFrom = moves[i].from;
|
|
393
|
-
const ambigTo = moves[i].to;
|
|
394
|
-
const ambigPiece = moves[i].piece;
|
|
395
|
-
/*
|
|
396
|
-
* if a move of the same piece type ends on the same to square, we'll need
|
|
397
|
-
* to add a disambiguator to the algebraic notation
|
|
398
|
-
*/
|
|
399
|
-
if (piece === ambigPiece && from !== ambigFrom && to === ambigTo) {
|
|
400
|
-
ambiguities++;
|
|
401
|
-
if (rank(from) === rank(ambigFrom)) {
|
|
402
|
-
sameRank++;
|
|
403
|
-
}
|
|
404
|
-
if (file(from) === file(ambigFrom)) {
|
|
405
|
-
sameFile++;
|
|
406
|
-
}
|
|
407
|
-
}
|
|
408
|
-
}
|
|
409
|
-
if (ambiguities > 0) {
|
|
410
|
-
if (sameRank > 0 && sameFile > 0) {
|
|
411
|
-
/*
|
|
412
|
-
* if there exists a similar moving piece on the same rank and file as
|
|
413
|
-
* the move in question, use the square as the disambiguator
|
|
414
|
-
*/
|
|
415
|
-
return algebraic(from);
|
|
416
|
-
}
|
|
417
|
-
else if (sameFile > 0) {
|
|
418
|
-
/*
|
|
419
|
-
* if the moving piece rests on the same file, use the rank symbol as the
|
|
420
|
-
* disambiguator
|
|
421
|
-
*/
|
|
422
|
-
return algebraic(from).charAt(1);
|
|
423
|
-
}
|
|
424
|
-
else {
|
|
425
|
-
// else use the file symbol
|
|
426
|
-
return algebraic(from).charAt(0);
|
|
427
|
-
}
|
|
428
|
-
}
|
|
429
|
-
return '';
|
|
430
|
-
}
|
|
431
|
-
function addMove(moves, color, from, to, piece, captured = undefined, flags = BITS.NORMAL) {
|
|
432
|
-
const r = rank(to);
|
|
433
|
-
if (piece === PAWN && (r === RANK_1 || r === RANK_8)) {
|
|
434
|
-
for (let i = 0; i < PROMOTIONS.length; i++) {
|
|
435
|
-
const promotion = PROMOTIONS[i];
|
|
436
|
-
moves.push({
|
|
437
|
-
color,
|
|
438
|
-
from,
|
|
439
|
-
to,
|
|
440
|
-
piece,
|
|
441
|
-
captured,
|
|
442
|
-
promotion,
|
|
443
|
-
flags: flags | BITS.PROMOTION,
|
|
444
|
-
});
|
|
445
|
-
}
|
|
446
|
-
}
|
|
447
|
-
else {
|
|
448
|
-
moves.push({
|
|
449
|
-
color,
|
|
450
|
-
from,
|
|
451
|
-
to,
|
|
452
|
-
piece,
|
|
453
|
-
captured,
|
|
454
|
-
flags,
|
|
455
|
-
});
|
|
456
|
-
}
|
|
457
|
-
}
|
|
458
|
-
function inferPieceType(san) {
|
|
459
|
-
let pieceType = san.charAt(0);
|
|
460
|
-
if (pieceType >= 'a' && pieceType <= 'h') {
|
|
461
|
-
const matches = san.match(/[a-h]\d.*[a-h]\d/);
|
|
462
|
-
if (matches) {
|
|
463
|
-
return undefined;
|
|
464
|
-
}
|
|
465
|
-
return PAWN;
|
|
466
|
-
}
|
|
467
|
-
pieceType = pieceType.toLowerCase();
|
|
468
|
-
if (pieceType === 'o') {
|
|
469
|
-
return KING;
|
|
470
|
-
}
|
|
471
|
-
return pieceType;
|
|
472
|
-
}
|
|
473
|
-
// parses all of the decorators out of a SAN string
|
|
474
|
-
function strippedSan(move) {
|
|
475
|
-
return move.replace(/=/, '').replace(/[+#]?[?!]*$/, '');
|
|
476
|
-
}
|
|
477
|
-
function trimFen(fen) {
|
|
478
|
-
/*
|
|
479
|
-
* remove last two fields in FEN string as they're not needed when checking
|
|
480
|
-
* for repetition
|
|
481
|
-
*/
|
|
482
|
-
return fen.split(' ').slice(0, 4).join(' ');
|
|
483
|
-
}
|
|
484
|
-
class Chess {
|
|
485
|
-
_board = new Array(128);
|
|
486
|
-
_turn = WHITE;
|
|
487
|
-
_header = {};
|
|
488
|
-
_kings = { w: EMPTY, b: EMPTY };
|
|
489
|
-
_epSquare = -1;
|
|
490
|
-
_halfMoves = 0;
|
|
491
|
-
_moveNumber = 0;
|
|
492
|
-
_history = [];
|
|
493
|
-
_comments = {};
|
|
494
|
-
_castling = { w: 0, b: 0 };
|
|
495
|
-
// tracks number of times a position has been seen for repetition checking
|
|
496
|
-
_positionCount = {};
|
|
497
|
-
constructor(fen = DEFAULT_POSITION) {
|
|
498
|
-
this.load(fen);
|
|
499
|
-
}
|
|
500
|
-
clear({ preserveHeaders = false } = {}) {
|
|
501
|
-
this._board = new Array(128);
|
|
502
|
-
this._kings = { w: EMPTY, b: EMPTY };
|
|
503
|
-
this._turn = WHITE;
|
|
504
|
-
this._castling = { w: 0, b: 0 };
|
|
505
|
-
this._epSquare = EMPTY;
|
|
506
|
-
this._halfMoves = 0;
|
|
507
|
-
this._moveNumber = 1;
|
|
508
|
-
this._history = [];
|
|
509
|
-
this._comments = {};
|
|
510
|
-
this._header = preserveHeaders ? this._header : {};
|
|
511
|
-
this._positionCount = {};
|
|
512
|
-
/*
|
|
513
|
-
* Delete the SetUp and FEN headers (if preserved), the board is empty and
|
|
514
|
-
* these headers don't make sense in this state. They'll get added later
|
|
515
|
-
* via .load() or .put()
|
|
516
|
-
*/
|
|
517
|
-
delete this._header['SetUp'];
|
|
518
|
-
delete this._header['FEN'];
|
|
519
|
-
}
|
|
520
|
-
load(fen, { skipValidation = false, preserveHeaders = false } = {}) {
|
|
521
|
-
let tokens = fen.split(/\s+/);
|
|
522
|
-
// append commonly omitted fen tokens
|
|
523
|
-
if (tokens.length >= 2 && tokens.length < 6) {
|
|
524
|
-
const adjustments = ['-', '-', '0', '1'];
|
|
525
|
-
fen = tokens.concat(adjustments.slice(-(6 - tokens.length))).join(' ');
|
|
526
|
-
}
|
|
527
|
-
tokens = fen.split(/\s+/);
|
|
528
|
-
if (!skipValidation) {
|
|
529
|
-
const { ok, error } = validateFen(fen);
|
|
530
|
-
if (!ok) {
|
|
531
|
-
throw new Error(error);
|
|
532
|
-
}
|
|
533
|
-
}
|
|
534
|
-
const position = tokens[0];
|
|
535
|
-
let square = 0;
|
|
536
|
-
this.clear({ preserveHeaders });
|
|
537
|
-
for (let i = 0; i < position.length; i++) {
|
|
538
|
-
const piece = position.charAt(i);
|
|
539
|
-
if (piece === '/') {
|
|
540
|
-
square += 8;
|
|
541
|
-
}
|
|
542
|
-
else if (isDigit(piece)) {
|
|
543
|
-
square += parseInt(piece, 10);
|
|
544
|
-
}
|
|
545
|
-
else {
|
|
546
|
-
const color = piece < 'a' ? WHITE : BLACK;
|
|
547
|
-
this._put({ type: piece.toLowerCase(), color }, algebraic(square));
|
|
548
|
-
square++;
|
|
549
|
-
}
|
|
550
|
-
}
|
|
551
|
-
this._turn = tokens[1];
|
|
552
|
-
if (tokens[2].indexOf('K') > -1) {
|
|
553
|
-
this._castling.w |= BITS.KSIDE_CASTLE;
|
|
554
|
-
}
|
|
555
|
-
if (tokens[2].indexOf('Q') > -1) {
|
|
556
|
-
this._castling.w |= BITS.QSIDE_CASTLE;
|
|
557
|
-
}
|
|
558
|
-
if (tokens[2].indexOf('k') > -1) {
|
|
559
|
-
this._castling.b |= BITS.KSIDE_CASTLE;
|
|
560
|
-
}
|
|
561
|
-
if (tokens[2].indexOf('q') > -1) {
|
|
562
|
-
this._castling.b |= BITS.QSIDE_CASTLE;
|
|
563
|
-
}
|
|
564
|
-
this._epSquare = tokens[3] === '-' ? EMPTY : Ox88[tokens[3]];
|
|
565
|
-
this._halfMoves = parseInt(tokens[4], 10);
|
|
566
|
-
this._moveNumber = parseInt(tokens[5], 10);
|
|
567
|
-
this._updateSetup(fen);
|
|
568
|
-
this._incPositionCount(fen);
|
|
569
|
-
}
|
|
570
|
-
fen() {
|
|
571
|
-
let empty = 0;
|
|
572
|
-
let fen = '';
|
|
573
|
-
for (let i = Ox88.a8; i <= Ox88.h1; i++) {
|
|
574
|
-
if (this._board[i]) {
|
|
575
|
-
if (empty > 0) {
|
|
576
|
-
fen += empty;
|
|
577
|
-
empty = 0;
|
|
578
|
-
}
|
|
579
|
-
const { color, type: piece } = this._board[i];
|
|
580
|
-
fen += color === WHITE ? piece.toUpperCase() : piece.toLowerCase();
|
|
581
|
-
}
|
|
582
|
-
else {
|
|
583
|
-
empty++;
|
|
584
|
-
}
|
|
585
|
-
if ((i + 1) & 0x88) {
|
|
586
|
-
if (empty > 0) {
|
|
587
|
-
fen += empty;
|
|
588
|
-
}
|
|
589
|
-
if (i !== Ox88.h1) {
|
|
590
|
-
fen += '/';
|
|
591
|
-
}
|
|
592
|
-
empty = 0;
|
|
593
|
-
i += 8;
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
|
-
let castling = '';
|
|
597
|
-
if (this._castling[WHITE] & BITS.KSIDE_CASTLE) {
|
|
598
|
-
castling += 'K';
|
|
599
|
-
}
|
|
600
|
-
if (this._castling[WHITE] & BITS.QSIDE_CASTLE) {
|
|
601
|
-
castling += 'Q';
|
|
602
|
-
}
|
|
603
|
-
if (this._castling[BLACK] & BITS.KSIDE_CASTLE) {
|
|
604
|
-
castling += 'k';
|
|
605
|
-
}
|
|
606
|
-
if (this._castling[BLACK] & BITS.QSIDE_CASTLE) {
|
|
607
|
-
castling += 'q';
|
|
608
|
-
}
|
|
609
|
-
// do we have an empty castling flag?
|
|
610
|
-
castling = castling || '-';
|
|
611
|
-
let epSquare = '-';
|
|
612
|
-
/*
|
|
613
|
-
* only print the ep square if en passant is a valid move (pawn is present
|
|
614
|
-
* and ep capture is not pinned)
|
|
615
|
-
*/
|
|
616
|
-
if (this._epSquare !== EMPTY) {
|
|
617
|
-
const bigPawnSquare = this._epSquare + (this._turn === WHITE ? 16 : -16);
|
|
618
|
-
const squares = [bigPawnSquare + 1, bigPawnSquare - 1];
|
|
619
|
-
for (const square of squares) {
|
|
620
|
-
// is the square off the board?
|
|
621
|
-
if (square & 0x88) {
|
|
622
|
-
continue;
|
|
623
|
-
}
|
|
624
|
-
const color = this._turn;
|
|
625
|
-
// is there a pawn that can capture the epSquare?
|
|
626
|
-
if (this._board[square]?.color === color &&
|
|
627
|
-
this._board[square]?.type === PAWN) {
|
|
628
|
-
// if the pawn makes an ep capture, does it leave it's king in check?
|
|
629
|
-
this._makeMove({
|
|
630
|
-
color,
|
|
631
|
-
from: square,
|
|
632
|
-
to: this._epSquare,
|
|
633
|
-
piece: PAWN,
|
|
634
|
-
captured: PAWN,
|
|
635
|
-
flags: BITS.EP_CAPTURE,
|
|
636
|
-
});
|
|
637
|
-
const isLegal = !this._isKingAttacked(color);
|
|
638
|
-
this._undoMove();
|
|
639
|
-
// if ep is legal, break and set the ep square in the FEN output
|
|
640
|
-
if (isLegal) {
|
|
641
|
-
epSquare = algebraic(this._epSquare);
|
|
642
|
-
break;
|
|
643
|
-
}
|
|
644
|
-
}
|
|
645
|
-
}
|
|
646
|
-
}
|
|
647
|
-
return [
|
|
648
|
-
fen,
|
|
649
|
-
this._turn,
|
|
650
|
-
castling,
|
|
651
|
-
epSquare,
|
|
652
|
-
this._halfMoves,
|
|
653
|
-
this._moveNumber,
|
|
654
|
-
].join(' ');
|
|
655
|
-
}
|
|
656
|
-
/*
|
|
657
|
-
* Called when the initial board setup is changed with put() or remove().
|
|
658
|
-
* modifies the SetUp and FEN properties of the header object. If the FEN
|
|
659
|
-
* is equal to the default position, the SetUp and FEN are deleted the setup
|
|
660
|
-
* is only updated if history.length is zero, ie moves haven't been made.
|
|
661
|
-
*/
|
|
662
|
-
_updateSetup(fen) {
|
|
663
|
-
if (this._history.length > 0)
|
|
664
|
-
return;
|
|
665
|
-
if (fen !== DEFAULT_POSITION) {
|
|
666
|
-
this._header['SetUp'] = '1';
|
|
667
|
-
this._header['FEN'] = fen;
|
|
668
|
-
}
|
|
669
|
-
else {
|
|
670
|
-
delete this._header['SetUp'];
|
|
671
|
-
delete this._header['FEN'];
|
|
672
|
-
}
|
|
673
|
-
}
|
|
674
|
-
reset() {
|
|
675
|
-
this.load(DEFAULT_POSITION);
|
|
676
|
-
}
|
|
677
|
-
get(square) {
|
|
678
|
-
return this._board[Ox88[square]];
|
|
679
|
-
}
|
|
680
|
-
put({ type, color }, square) {
|
|
681
|
-
if (this._put({ type, color }, square)) {
|
|
682
|
-
this._updateCastlingRights();
|
|
683
|
-
this._updateEnPassantSquare();
|
|
684
|
-
this._updateSetup(this.fen());
|
|
685
|
-
return true;
|
|
686
|
-
}
|
|
687
|
-
return false;
|
|
688
|
-
}
|
|
689
|
-
_put({ type, color }, square) {
|
|
690
|
-
// check for piece
|
|
691
|
-
if (SYMBOLS.indexOf(type.toLowerCase()) === -1) {
|
|
692
|
-
return false;
|
|
693
|
-
}
|
|
694
|
-
// check for valid square
|
|
695
|
-
if (!(square in Ox88)) {
|
|
696
|
-
return false;
|
|
697
|
-
}
|
|
698
|
-
const sq = Ox88[square];
|
|
699
|
-
// don't let the user place more than one king
|
|
700
|
-
if (type == KING &&
|
|
701
|
-
!(this._kings[color] == EMPTY || this._kings[color] == sq)) {
|
|
702
|
-
return false;
|
|
703
|
-
}
|
|
704
|
-
const currentPieceOnSquare = this._board[sq];
|
|
705
|
-
// if one of the kings will be replaced by the piece from args, set the `_kings` respective entry to `EMPTY`
|
|
706
|
-
if (currentPieceOnSquare && currentPieceOnSquare.type === KING) {
|
|
707
|
-
this._kings[currentPieceOnSquare.color] = EMPTY;
|
|
708
|
-
}
|
|
709
|
-
this._board[sq] = { type: type, color: color };
|
|
710
|
-
if (type === KING) {
|
|
711
|
-
this._kings[color] = sq;
|
|
712
|
-
}
|
|
713
|
-
return true;
|
|
714
|
-
}
|
|
715
|
-
remove(square) {
|
|
716
|
-
const piece = this.get(square);
|
|
717
|
-
delete this._board[Ox88[square]];
|
|
718
|
-
if (piece && piece.type === KING) {
|
|
719
|
-
this._kings[piece.color] = EMPTY;
|
|
720
|
-
}
|
|
721
|
-
this._updateCastlingRights();
|
|
722
|
-
this._updateEnPassantSquare();
|
|
723
|
-
this._updateSetup(this.fen());
|
|
724
|
-
return piece;
|
|
725
|
-
}
|
|
726
|
-
_updateCastlingRights() {
|
|
727
|
-
const whiteKingInPlace = this._board[Ox88.e1]?.type === KING &&
|
|
728
|
-
this._board[Ox88.e1]?.color === WHITE;
|
|
729
|
-
const blackKingInPlace = this._board[Ox88.e8]?.type === KING &&
|
|
730
|
-
this._board[Ox88.e8]?.color === BLACK;
|
|
731
|
-
if (!whiteKingInPlace ||
|
|
732
|
-
this._board[Ox88.a1]?.type !== ROOK ||
|
|
733
|
-
this._board[Ox88.a1]?.color !== WHITE) {
|
|
734
|
-
this._castling.w &= -65;
|
|
735
|
-
}
|
|
736
|
-
if (!whiteKingInPlace ||
|
|
737
|
-
this._board[Ox88.h1]?.type !== ROOK ||
|
|
738
|
-
this._board[Ox88.h1]?.color !== WHITE) {
|
|
739
|
-
this._castling.w &= -33;
|
|
740
|
-
}
|
|
741
|
-
if (!blackKingInPlace ||
|
|
742
|
-
this._board[Ox88.a8]?.type !== ROOK ||
|
|
743
|
-
this._board[Ox88.a8]?.color !== BLACK) {
|
|
744
|
-
this._castling.b &= -65;
|
|
745
|
-
}
|
|
746
|
-
if (!blackKingInPlace ||
|
|
747
|
-
this._board[Ox88.h8]?.type !== ROOK ||
|
|
748
|
-
this._board[Ox88.h8]?.color !== BLACK) {
|
|
749
|
-
this._castling.b &= -33;
|
|
750
|
-
}
|
|
751
|
-
}
|
|
752
|
-
_updateEnPassantSquare() {
|
|
753
|
-
if (this._epSquare === EMPTY) {
|
|
754
|
-
return;
|
|
755
|
-
}
|
|
756
|
-
const startSquare = this._epSquare + (this._turn === WHITE ? -16 : 16);
|
|
757
|
-
const currentSquare = this._epSquare + (this._turn === WHITE ? 16 : -16);
|
|
758
|
-
const attackers = [currentSquare + 1, currentSquare - 1];
|
|
759
|
-
if (this._board[startSquare] !== null ||
|
|
760
|
-
this._board[this._epSquare] !== null ||
|
|
761
|
-
this._board[currentSquare]?.color !== swapColor(this._turn) ||
|
|
762
|
-
this._board[currentSquare]?.type !== PAWN) {
|
|
763
|
-
this._epSquare = EMPTY;
|
|
764
|
-
return;
|
|
765
|
-
}
|
|
766
|
-
const canCapture = (square) => !(square & 0x88) &&
|
|
767
|
-
this._board[square]?.color === this._turn &&
|
|
768
|
-
this._board[square]?.type === PAWN;
|
|
769
|
-
if (!attackers.some(canCapture)) {
|
|
770
|
-
this._epSquare = EMPTY;
|
|
771
|
-
}
|
|
772
|
-
}
|
|
773
|
-
_attacked(color, square, verbose) {
|
|
774
|
-
const attackers = [];
|
|
775
|
-
for (let i = Ox88.a8; i <= Ox88.h1; i++) {
|
|
776
|
-
// did we run off the end of the board
|
|
777
|
-
if (i & 0x88) {
|
|
778
|
-
i += 7;
|
|
779
|
-
continue;
|
|
780
|
-
}
|
|
781
|
-
// if empty square or wrong color
|
|
782
|
-
if (this._board[i] === undefined || this._board[i].color !== color) {
|
|
783
|
-
continue;
|
|
784
|
-
}
|
|
785
|
-
const piece = this._board[i];
|
|
786
|
-
const difference = i - square;
|
|
787
|
-
// skip - to/from square are the same
|
|
788
|
-
if (difference === 0) {
|
|
789
|
-
continue;
|
|
790
|
-
}
|
|
791
|
-
const index = difference + 119;
|
|
792
|
-
if (ATTACKS[index] & PIECE_MASKS[piece.type]) {
|
|
793
|
-
if (piece.type === PAWN) {
|
|
794
|
-
if ((difference > 0 && piece.color === WHITE) ||
|
|
795
|
-
(difference <= 0 && piece.color === BLACK)) {
|
|
796
|
-
if (!verbose) {
|
|
797
|
-
return true;
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
attackers.push(algebraic(i));
|
|
801
|
-
}
|
|
802
|
-
}
|
|
803
|
-
continue;
|
|
804
|
-
}
|
|
805
|
-
// if the piece is a knight or a king
|
|
806
|
-
if (piece.type === 'n' || piece.type === 'k') {
|
|
807
|
-
if (!verbose) {
|
|
808
|
-
return true;
|
|
809
|
-
}
|
|
810
|
-
else {
|
|
811
|
-
attackers.push(algebraic(i));
|
|
812
|
-
continue;
|
|
813
|
-
}
|
|
814
|
-
}
|
|
815
|
-
const offset = RAYS[index];
|
|
816
|
-
let j = i + offset;
|
|
817
|
-
let blocked = false;
|
|
818
|
-
while (j !== square) {
|
|
819
|
-
if (this._board[j] != null) {
|
|
820
|
-
blocked = true;
|
|
821
|
-
break;
|
|
822
|
-
}
|
|
823
|
-
j += offset;
|
|
824
|
-
}
|
|
825
|
-
if (!blocked) {
|
|
826
|
-
if (!verbose) {
|
|
827
|
-
return true;
|
|
828
|
-
}
|
|
829
|
-
else {
|
|
830
|
-
attackers.push(algebraic(i));
|
|
831
|
-
continue;
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
}
|
|
835
|
-
}
|
|
836
|
-
if (verbose) {
|
|
837
|
-
return attackers;
|
|
838
|
-
}
|
|
839
|
-
else {
|
|
840
|
-
return false;
|
|
841
|
-
}
|
|
842
|
-
}
|
|
843
|
-
attackers(square, attackedBy) {
|
|
844
|
-
if (!attackedBy) {
|
|
845
|
-
return this._attacked(this._turn, Ox88[square], true);
|
|
846
|
-
}
|
|
847
|
-
else {
|
|
848
|
-
return this._attacked(attackedBy, Ox88[square], true);
|
|
849
|
-
}
|
|
850
|
-
}
|
|
851
|
-
_isKingAttacked(color) {
|
|
852
|
-
const square = this._kings[color];
|
|
853
|
-
return square === -1 ? false : this._attacked(swapColor(color), square);
|
|
854
|
-
}
|
|
855
|
-
isAttacked(square, attackedBy) {
|
|
856
|
-
return this._attacked(attackedBy, Ox88[square]);
|
|
857
|
-
}
|
|
858
|
-
isCheck() {
|
|
859
|
-
return this._isKingAttacked(this._turn);
|
|
860
|
-
}
|
|
861
|
-
inCheck() {
|
|
862
|
-
return this.isCheck();
|
|
863
|
-
}
|
|
864
|
-
isCheckmate() {
|
|
865
|
-
return this.isCheck() && this._moves().length === 0;
|
|
866
|
-
}
|
|
867
|
-
isStalemate() {
|
|
868
|
-
return !this.isCheck() && this._moves().length === 0;
|
|
869
|
-
}
|
|
870
|
-
isInsufficientMaterial() {
|
|
871
|
-
/*
|
|
872
|
-
* k.b. vs k.b. (of opposite colors) with mate in 1:
|
|
873
|
-
* 8/8/8/8/1b6/8/B1k5/K7 b - - 0 1
|
|
874
|
-
*
|
|
875
|
-
* k.b. vs k.n. with mate in 1:
|
|
876
|
-
* 8/8/8/8/1n6/8/B7/K1k5 b - - 2 1
|
|
877
|
-
*/
|
|
878
|
-
const pieces = {
|
|
879
|
-
b: 0,
|
|
880
|
-
n: 0,
|
|
881
|
-
r: 0,
|
|
882
|
-
q: 0,
|
|
883
|
-
k: 0,
|
|
884
|
-
p: 0,
|
|
885
|
-
};
|
|
886
|
-
const bishops = [];
|
|
887
|
-
let numPieces = 0;
|
|
888
|
-
let squareColor = 0;
|
|
889
|
-
for (let i = Ox88.a8; i <= Ox88.h1; i++) {
|
|
890
|
-
squareColor = (squareColor + 1) % 2;
|
|
891
|
-
if (i & 0x88) {
|
|
892
|
-
i += 7;
|
|
893
|
-
continue;
|
|
894
|
-
}
|
|
895
|
-
const piece = this._board[i];
|
|
896
|
-
if (piece) {
|
|
897
|
-
pieces[piece.type] = piece.type in pieces ? pieces[piece.type] + 1 : 1;
|
|
898
|
-
if (piece.type === BISHOP) {
|
|
899
|
-
bishops.push(squareColor);
|
|
900
|
-
}
|
|
901
|
-
numPieces++;
|
|
902
|
-
}
|
|
903
|
-
}
|
|
904
|
-
// k vs. k
|
|
905
|
-
if (numPieces === 2) {
|
|
906
|
-
return true;
|
|
907
|
-
}
|
|
908
|
-
else if (
|
|
909
|
-
// k vs. kn .... or .... k vs. kb
|
|
910
|
-
numPieces === 3 &&
|
|
911
|
-
(pieces[BISHOP] === 1 || pieces[KNIGHT] === 1)) {
|
|
912
|
-
return true;
|
|
913
|
-
}
|
|
914
|
-
else if (numPieces === pieces[BISHOP] + 2) {
|
|
915
|
-
// kb vs. kb where any number of bishops are all on the same color
|
|
916
|
-
let sum = 0;
|
|
917
|
-
const len = bishops.length;
|
|
918
|
-
for (let i = 0; i < len; i++) {
|
|
919
|
-
sum += bishops[i];
|
|
920
|
-
}
|
|
921
|
-
if (sum === 0 || sum === len) {
|
|
922
|
-
return true;
|
|
923
|
-
}
|
|
924
|
-
}
|
|
925
|
-
return false;
|
|
926
|
-
}
|
|
927
|
-
isThreefoldRepetition() {
|
|
928
|
-
return this._getPositionCount(this.fen()) >= 3;
|
|
929
|
-
}
|
|
930
|
-
isDrawByFiftyMoves() {
|
|
931
|
-
return this._halfMoves >= 100; // 50 moves per side = 100 half moves
|
|
932
|
-
}
|
|
933
|
-
isDraw() {
|
|
934
|
-
return (this.isDrawByFiftyMoves() ||
|
|
935
|
-
this.isStalemate() ||
|
|
936
|
-
this.isInsufficientMaterial() ||
|
|
937
|
-
this.isThreefoldRepetition());
|
|
938
|
-
}
|
|
939
|
-
isGameOver() {
|
|
940
|
-
return this.isCheckmate() || this.isStalemate() || this.isDraw();
|
|
941
|
-
}
|
|
942
|
-
moves({ verbose = false, square = undefined, piece = undefined, } = {}) {
|
|
943
|
-
const moves = this._moves({ square, piece });
|
|
944
|
-
if (verbose) {
|
|
945
|
-
return moves.map((move) => new Move$1(this, move));
|
|
946
|
-
}
|
|
947
|
-
else {
|
|
948
|
-
return moves.map((move) => this._moveToSan(move, moves));
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
_moves({ legal = true, piece = undefined, square = undefined, } = {}) {
|
|
952
|
-
const forSquare = square ? square.toLowerCase() : undefined;
|
|
953
|
-
const forPiece = piece?.toLowerCase();
|
|
954
|
-
const moves = [];
|
|
955
|
-
const us = this._turn;
|
|
956
|
-
const them = swapColor(us);
|
|
957
|
-
let firstSquare = Ox88.a8;
|
|
958
|
-
let lastSquare = Ox88.h1;
|
|
959
|
-
let singleSquare = false;
|
|
960
|
-
// are we generating moves for a single square?
|
|
961
|
-
if (forSquare) {
|
|
962
|
-
// illegal square, return empty moves
|
|
963
|
-
if (!(forSquare in Ox88)) {
|
|
964
|
-
return [];
|
|
965
|
-
}
|
|
966
|
-
else {
|
|
967
|
-
firstSquare = lastSquare = Ox88[forSquare];
|
|
968
|
-
singleSquare = true;
|
|
969
|
-
}
|
|
970
|
-
}
|
|
971
|
-
for (let from = firstSquare; from <= lastSquare; from++) {
|
|
972
|
-
// did we run off the end of the board
|
|
973
|
-
if (from & 0x88) {
|
|
974
|
-
from += 7;
|
|
975
|
-
continue;
|
|
976
|
-
}
|
|
977
|
-
// empty square or opponent, skip
|
|
978
|
-
if (!this._board[from] || this._board[from].color === them) {
|
|
979
|
-
continue;
|
|
980
|
-
}
|
|
981
|
-
const { type } = this._board[from];
|
|
982
|
-
let to;
|
|
983
|
-
if (type === PAWN) {
|
|
984
|
-
if (forPiece && forPiece !== type)
|
|
985
|
-
continue;
|
|
986
|
-
// single square, non-capturing
|
|
987
|
-
to = from + PAWN_OFFSETS[us][0];
|
|
988
|
-
if (!this._board[to]) {
|
|
989
|
-
addMove(moves, us, from, to, PAWN);
|
|
990
|
-
// double square
|
|
991
|
-
to = from + PAWN_OFFSETS[us][1];
|
|
992
|
-
if (SECOND_RANK[us] === rank(from) && !this._board[to]) {
|
|
993
|
-
addMove(moves, us, from, to, PAWN, undefined, BITS.BIG_PAWN);
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
// pawn captures
|
|
997
|
-
for (let j = 2; j < 4; j++) {
|
|
998
|
-
to = from + PAWN_OFFSETS[us][j];
|
|
999
|
-
if (to & 0x88)
|
|
1000
|
-
continue;
|
|
1001
|
-
if (this._board[to]?.color === them) {
|
|
1002
|
-
addMove(moves, us, from, to, PAWN, this._board[to].type, BITS.CAPTURE);
|
|
1003
|
-
}
|
|
1004
|
-
else if (to === this._epSquare) {
|
|
1005
|
-
addMove(moves, us, from, to, PAWN, PAWN, BITS.EP_CAPTURE);
|
|
1006
|
-
}
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
else {
|
|
1010
|
-
if (forPiece && forPiece !== type)
|
|
1011
|
-
continue;
|
|
1012
|
-
for (let j = 0, len = PIECE_OFFSETS[type].length; j < len; j++) {
|
|
1013
|
-
const offset = PIECE_OFFSETS[type][j];
|
|
1014
|
-
to = from;
|
|
1015
|
-
while (true) {
|
|
1016
|
-
to += offset;
|
|
1017
|
-
if (to & 0x88)
|
|
1018
|
-
break;
|
|
1019
|
-
if (!this._board[to]) {
|
|
1020
|
-
addMove(moves, us, from, to, type);
|
|
1021
|
-
}
|
|
1022
|
-
else {
|
|
1023
|
-
// own color, stop loop
|
|
1024
|
-
if (this._board[to].color === us)
|
|
1025
|
-
break;
|
|
1026
|
-
addMove(moves, us, from, to, type, this._board[to].type, BITS.CAPTURE);
|
|
1027
|
-
break;
|
|
1028
|
-
}
|
|
1029
|
-
/* break, if knight or king */
|
|
1030
|
-
if (type === KNIGHT || type === KING)
|
|
1031
|
-
break;
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
}
|
|
1036
|
-
/*
|
|
1037
|
-
* check for castling if we're:
|
|
1038
|
-
* a) generating all moves, or
|
|
1039
|
-
* b) doing single square move generation on the king's square
|
|
1040
|
-
*/
|
|
1041
|
-
if (forPiece === undefined || forPiece === KING) {
|
|
1042
|
-
if (!singleSquare || lastSquare === this._kings[us]) {
|
|
1043
|
-
// king-side castling
|
|
1044
|
-
if (this._castling[us] & BITS.KSIDE_CASTLE) {
|
|
1045
|
-
const castlingFrom = this._kings[us];
|
|
1046
|
-
const castlingTo = castlingFrom + 2;
|
|
1047
|
-
if (!this._board[castlingFrom + 1] &&
|
|
1048
|
-
!this._board[castlingTo] &&
|
|
1049
|
-
!this._attacked(them, this._kings[us]) &&
|
|
1050
|
-
!this._attacked(them, castlingFrom + 1) &&
|
|
1051
|
-
!this._attacked(them, castlingTo)) {
|
|
1052
|
-
addMove(moves, us, this._kings[us], castlingTo, KING, undefined, BITS.KSIDE_CASTLE);
|
|
1053
|
-
}
|
|
1054
|
-
}
|
|
1055
|
-
// queen-side castling
|
|
1056
|
-
if (this._castling[us] & BITS.QSIDE_CASTLE) {
|
|
1057
|
-
const castlingFrom = this._kings[us];
|
|
1058
|
-
const castlingTo = castlingFrom - 2;
|
|
1059
|
-
if (!this._board[castlingFrom - 1] &&
|
|
1060
|
-
!this._board[castlingFrom - 2] &&
|
|
1061
|
-
!this._board[castlingFrom - 3] &&
|
|
1062
|
-
!this._attacked(them, this._kings[us]) &&
|
|
1063
|
-
!this._attacked(them, castlingFrom - 1) &&
|
|
1064
|
-
!this._attacked(them, castlingTo)) {
|
|
1065
|
-
addMove(moves, us, this._kings[us], castlingTo, KING, undefined, BITS.QSIDE_CASTLE);
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
}
|
|
1069
|
-
}
|
|
1070
|
-
/*
|
|
1071
|
-
* return all pseudo-legal moves (this includes moves that allow the king
|
|
1072
|
-
* to be captured)
|
|
1073
|
-
*/
|
|
1074
|
-
if (!legal || this._kings[us] === -1) {
|
|
1075
|
-
return moves;
|
|
1076
|
-
}
|
|
1077
|
-
// filter out illegal moves
|
|
1078
|
-
const legalMoves = [];
|
|
1079
|
-
for (let i = 0, len = moves.length; i < len; i++) {
|
|
1080
|
-
this._makeMove(moves[i]);
|
|
1081
|
-
if (!this._isKingAttacked(us)) {
|
|
1082
|
-
legalMoves.push(moves[i]);
|
|
1083
|
-
}
|
|
1084
|
-
this._undoMove();
|
|
1085
|
-
}
|
|
1086
|
-
return legalMoves;
|
|
1087
|
-
}
|
|
1088
|
-
move(move, { strict = false } = {}) {
|
|
1089
|
-
/*
|
|
1090
|
-
* The move function can be called with in the following parameters:
|
|
1091
|
-
*
|
|
1092
|
-
* .move('Nxb7') <- argument is a case-sensitive SAN string
|
|
1093
|
-
*
|
|
1094
|
-
* .move({ from: 'h7', <- argument is a move object
|
|
1095
|
-
* to :'h8',
|
|
1096
|
-
* promotion: 'q' })
|
|
1097
|
-
*
|
|
1098
|
-
*
|
|
1099
|
-
* An optional strict argument may be supplied to tell chess.js to
|
|
1100
|
-
* strictly follow the SAN specification.
|
|
1101
|
-
*/
|
|
1102
|
-
let moveObj = null;
|
|
1103
|
-
if (typeof move === 'string') {
|
|
1104
|
-
moveObj = this._moveFromSan(move, strict);
|
|
1105
|
-
}
|
|
1106
|
-
else if (typeof move === 'object') {
|
|
1107
|
-
const moves = this._moves();
|
|
1108
|
-
// convert the pretty move object to an ugly move object
|
|
1109
|
-
for (let i = 0, len = moves.length; i < len; i++) {
|
|
1110
|
-
if (move.from === algebraic(moves[i].from) &&
|
|
1111
|
-
move.to === algebraic(moves[i].to) &&
|
|
1112
|
-
(!('promotion' in moves[i]) || move.promotion === moves[i].promotion)) {
|
|
1113
|
-
moveObj = moves[i];
|
|
1114
|
-
break;
|
|
1115
|
-
}
|
|
1116
|
-
}
|
|
1117
|
-
}
|
|
1118
|
-
// failed to find move
|
|
1119
|
-
if (!moveObj) {
|
|
1120
|
-
if (typeof move === 'string') {
|
|
1121
|
-
throw new Error(`Invalid move: ${move}`);
|
|
1122
|
-
}
|
|
1123
|
-
else {
|
|
1124
|
-
throw new Error(`Invalid move: ${JSON.stringify(move)}`);
|
|
1125
|
-
}
|
|
1126
|
-
}
|
|
1127
|
-
/*
|
|
1128
|
-
* need to make a copy of move because we can't generate SAN after the move
|
|
1129
|
-
* is made
|
|
1130
|
-
*/
|
|
1131
|
-
const prettyMove = new Move$1(this, moveObj);
|
|
1132
|
-
this._makeMove(moveObj);
|
|
1133
|
-
this._incPositionCount(prettyMove.after);
|
|
1134
|
-
return prettyMove;
|
|
1135
|
-
}
|
|
1136
|
-
_push(move) {
|
|
1137
|
-
this._history.push({
|
|
1138
|
-
move,
|
|
1139
|
-
kings: { b: this._kings.b, w: this._kings.w },
|
|
1140
|
-
turn: this._turn,
|
|
1141
|
-
castling: { b: this._castling.b, w: this._castling.w },
|
|
1142
|
-
epSquare: this._epSquare,
|
|
1143
|
-
halfMoves: this._halfMoves,
|
|
1144
|
-
moveNumber: this._moveNumber,
|
|
1145
|
-
});
|
|
1146
|
-
}
|
|
1147
|
-
_makeMove(move) {
|
|
1148
|
-
const us = this._turn;
|
|
1149
|
-
const them = swapColor(us);
|
|
1150
|
-
this._push(move);
|
|
1151
|
-
this._board[move.to] = this._board[move.from];
|
|
1152
|
-
delete this._board[move.from];
|
|
1153
|
-
// if ep capture, remove the captured pawn
|
|
1154
|
-
if (move.flags & BITS.EP_CAPTURE) {
|
|
1155
|
-
if (this._turn === BLACK) {
|
|
1156
|
-
delete this._board[move.to - 16];
|
|
1157
|
-
}
|
|
1158
|
-
else {
|
|
1159
|
-
delete this._board[move.to + 16];
|
|
1160
|
-
}
|
|
1161
|
-
}
|
|
1162
|
-
// if pawn promotion, replace with new piece
|
|
1163
|
-
if (move.promotion) {
|
|
1164
|
-
this._board[move.to] = { type: move.promotion, color: us };
|
|
1165
|
-
}
|
|
1166
|
-
// if we moved the king
|
|
1167
|
-
if (this._board[move.to].type === KING) {
|
|
1168
|
-
this._kings[us] = move.to;
|
|
1169
|
-
// if we castled, move the rook next to the king
|
|
1170
|
-
if (move.flags & BITS.KSIDE_CASTLE) {
|
|
1171
|
-
const castlingTo = move.to - 1;
|
|
1172
|
-
const castlingFrom = move.to + 1;
|
|
1173
|
-
this._board[castlingTo] = this._board[castlingFrom];
|
|
1174
|
-
delete this._board[castlingFrom];
|
|
1175
|
-
}
|
|
1176
|
-
else if (move.flags & BITS.QSIDE_CASTLE) {
|
|
1177
|
-
const castlingTo = move.to + 1;
|
|
1178
|
-
const castlingFrom = move.to - 2;
|
|
1179
|
-
this._board[castlingTo] = this._board[castlingFrom];
|
|
1180
|
-
delete this._board[castlingFrom];
|
|
1181
|
-
}
|
|
1182
|
-
// turn off castling
|
|
1183
|
-
this._castling[us] = 0;
|
|
1184
|
-
}
|
|
1185
|
-
// turn off castling if we move a rook
|
|
1186
|
-
if (this._castling[us]) {
|
|
1187
|
-
for (let i = 0, len = ROOKS[us].length; i < len; i++) {
|
|
1188
|
-
if (move.from === ROOKS[us][i].square &&
|
|
1189
|
-
this._castling[us] & ROOKS[us][i].flag) {
|
|
1190
|
-
this._castling[us] ^= ROOKS[us][i].flag;
|
|
1191
|
-
break;
|
|
1192
|
-
}
|
|
1193
|
-
}
|
|
1194
|
-
}
|
|
1195
|
-
// turn off castling if we capture a rook
|
|
1196
|
-
if (this._castling[them]) {
|
|
1197
|
-
for (let i = 0, len = ROOKS[them].length; i < len; i++) {
|
|
1198
|
-
if (move.to === ROOKS[them][i].square &&
|
|
1199
|
-
this._castling[them] & ROOKS[them][i].flag) {
|
|
1200
|
-
this._castling[them] ^= ROOKS[them][i].flag;
|
|
1201
|
-
break;
|
|
1202
|
-
}
|
|
1203
|
-
}
|
|
1204
|
-
}
|
|
1205
|
-
// if big pawn move, update the en passant square
|
|
1206
|
-
if (move.flags & BITS.BIG_PAWN) {
|
|
1207
|
-
if (us === BLACK) {
|
|
1208
|
-
this._epSquare = move.to - 16;
|
|
1209
|
-
}
|
|
1210
|
-
else {
|
|
1211
|
-
this._epSquare = move.to + 16;
|
|
1212
|
-
}
|
|
1213
|
-
}
|
|
1214
|
-
else {
|
|
1215
|
-
this._epSquare = EMPTY;
|
|
1216
|
-
}
|
|
1217
|
-
// reset the 50 move counter if a pawn is moved or a piece is captured
|
|
1218
|
-
if (move.piece === PAWN) {
|
|
1219
|
-
this._halfMoves = 0;
|
|
1220
|
-
}
|
|
1221
|
-
else if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
|
|
1222
|
-
this._halfMoves = 0;
|
|
1223
|
-
}
|
|
1224
|
-
else {
|
|
1225
|
-
this._halfMoves++;
|
|
1226
|
-
}
|
|
1227
|
-
if (us === BLACK) {
|
|
1228
|
-
this._moveNumber++;
|
|
1229
|
-
}
|
|
1230
|
-
this._turn = them;
|
|
1231
|
-
}
|
|
1232
|
-
undo() {
|
|
1233
|
-
const move = this._undoMove();
|
|
1234
|
-
if (move) {
|
|
1235
|
-
const prettyMove = new Move$1(this, move);
|
|
1236
|
-
this._decPositionCount(prettyMove.after);
|
|
1237
|
-
return prettyMove;
|
|
1238
|
-
}
|
|
1239
|
-
return null;
|
|
1240
|
-
}
|
|
1241
|
-
_undoMove() {
|
|
1242
|
-
const old = this._history.pop();
|
|
1243
|
-
if (old === undefined) {
|
|
1244
|
-
return null;
|
|
1245
|
-
}
|
|
1246
|
-
const move = old.move;
|
|
1247
|
-
this._kings = old.kings;
|
|
1248
|
-
this._turn = old.turn;
|
|
1249
|
-
this._castling = old.castling;
|
|
1250
|
-
this._epSquare = old.epSquare;
|
|
1251
|
-
this._halfMoves = old.halfMoves;
|
|
1252
|
-
this._moveNumber = old.moveNumber;
|
|
1253
|
-
const us = this._turn;
|
|
1254
|
-
const them = swapColor(us);
|
|
1255
|
-
this._board[move.from] = this._board[move.to];
|
|
1256
|
-
this._board[move.from].type = move.piece; // to undo any promotions
|
|
1257
|
-
delete this._board[move.to];
|
|
1258
|
-
if (move.captured) {
|
|
1259
|
-
if (move.flags & BITS.EP_CAPTURE) {
|
|
1260
|
-
// en passant capture
|
|
1261
|
-
let index;
|
|
1262
|
-
if (us === BLACK) {
|
|
1263
|
-
index = move.to - 16;
|
|
1264
|
-
}
|
|
1265
|
-
else {
|
|
1266
|
-
index = move.to + 16;
|
|
1267
|
-
}
|
|
1268
|
-
this._board[index] = { type: PAWN, color: them };
|
|
1269
|
-
}
|
|
1270
|
-
else {
|
|
1271
|
-
// regular capture
|
|
1272
|
-
this._board[move.to] = { type: move.captured, color: them };
|
|
1273
|
-
}
|
|
1274
|
-
}
|
|
1275
|
-
if (move.flags & (BITS.KSIDE_CASTLE | BITS.QSIDE_CASTLE)) {
|
|
1276
|
-
let castlingTo, castlingFrom;
|
|
1277
|
-
if (move.flags & BITS.KSIDE_CASTLE) {
|
|
1278
|
-
castlingTo = move.to + 1;
|
|
1279
|
-
castlingFrom = move.to - 1;
|
|
1280
|
-
}
|
|
1281
|
-
else {
|
|
1282
|
-
castlingTo = move.to - 2;
|
|
1283
|
-
castlingFrom = move.to + 1;
|
|
1284
|
-
}
|
|
1285
|
-
this._board[castlingTo] = this._board[castlingFrom];
|
|
1286
|
-
delete this._board[castlingFrom];
|
|
1287
|
-
}
|
|
1288
|
-
return move;
|
|
1289
|
-
}
|
|
1290
|
-
pgn({ newline = '\n', maxWidth = 0, } = {}) {
|
|
1291
|
-
/*
|
|
1292
|
-
* using the specification from http://www.chessclub.com/help/PGN-spec
|
|
1293
|
-
* example for html usage: .pgn({ max_width: 72, newline_char: "<br />" })
|
|
1294
|
-
*/
|
|
1295
|
-
const result = [];
|
|
1296
|
-
let headerExists = false;
|
|
1297
|
-
/* add the PGN header information */
|
|
1298
|
-
for (const i in this._header) {
|
|
1299
|
-
/*
|
|
1300
|
-
* TODO: order of enumerated properties in header object is not
|
|
1301
|
-
* guaranteed, see ECMA-262 spec (section 12.6.4)
|
|
1302
|
-
*/
|
|
1303
|
-
result.push('[' + i + ' "' + this._header[i] + '"]' + newline);
|
|
1304
|
-
headerExists = true;
|
|
1305
|
-
}
|
|
1306
|
-
if (headerExists && this._history.length) {
|
|
1307
|
-
result.push(newline);
|
|
1308
|
-
}
|
|
1309
|
-
const appendComment = (moveString) => {
|
|
1310
|
-
const comment = this._comments[this.fen()];
|
|
1311
|
-
if (typeof comment !== 'undefined') {
|
|
1312
|
-
const delimiter = moveString.length > 0 ? ' ' : '';
|
|
1313
|
-
moveString = `${moveString}${delimiter}{${comment}}`;
|
|
1314
|
-
}
|
|
1315
|
-
return moveString;
|
|
1316
|
-
};
|
|
1317
|
-
// pop all of history onto reversed_history
|
|
1318
|
-
const reversedHistory = [];
|
|
1319
|
-
while (this._history.length > 0) {
|
|
1320
|
-
reversedHistory.push(this._undoMove());
|
|
1321
|
-
}
|
|
1322
|
-
const moves = [];
|
|
1323
|
-
let moveString = '';
|
|
1324
|
-
// special case of a commented starting position with no moves
|
|
1325
|
-
if (reversedHistory.length === 0) {
|
|
1326
|
-
moves.push(appendComment(''));
|
|
1327
|
-
}
|
|
1328
|
-
// build the list of moves. a move_string looks like: "3. e3 e6"
|
|
1329
|
-
while (reversedHistory.length > 0) {
|
|
1330
|
-
moveString = appendComment(moveString);
|
|
1331
|
-
const move = reversedHistory.pop();
|
|
1332
|
-
// make TypeScript stop complaining about move being undefined
|
|
1333
|
-
if (!move) {
|
|
1334
|
-
break;
|
|
1335
|
-
}
|
|
1336
|
-
// if the position started with black to move, start PGN with #. ...
|
|
1337
|
-
if (!this._history.length && move.color === 'b') {
|
|
1338
|
-
const prefix = `${this._moveNumber}. ...`;
|
|
1339
|
-
// is there a comment preceding the first move?
|
|
1340
|
-
moveString = moveString ? `${moveString} ${prefix}` : prefix;
|
|
1341
|
-
}
|
|
1342
|
-
else if (move.color === 'w') {
|
|
1343
|
-
// store the previous generated move_string if we have one
|
|
1344
|
-
if (moveString.length) {
|
|
1345
|
-
moves.push(moveString);
|
|
1346
|
-
}
|
|
1347
|
-
moveString = this._moveNumber + '.';
|
|
1348
|
-
}
|
|
1349
|
-
moveString =
|
|
1350
|
-
moveString + ' ' + this._moveToSan(move, this._moves({ legal: true }));
|
|
1351
|
-
this._makeMove(move);
|
|
1352
|
-
}
|
|
1353
|
-
// are there any other leftover moves?
|
|
1354
|
-
if (moveString.length) {
|
|
1355
|
-
moves.push(appendComment(moveString));
|
|
1356
|
-
}
|
|
1357
|
-
// is there a result?
|
|
1358
|
-
if (typeof this._header.Result !== 'undefined') {
|
|
1359
|
-
moves.push(this._header.Result);
|
|
1360
|
-
}
|
|
1361
|
-
/*
|
|
1362
|
-
* history should be back to what it was before we started generating PGN,
|
|
1363
|
-
* so join together moves
|
|
1364
|
-
*/
|
|
1365
|
-
if (maxWidth === 0) {
|
|
1366
|
-
return result.join('') + moves.join(' ');
|
|
1367
|
-
}
|
|
1368
|
-
// TODO (jah): huh?
|
|
1369
|
-
const strip = function () {
|
|
1370
|
-
if (result.length > 0 && result[result.length - 1] === ' ') {
|
|
1371
|
-
result.pop();
|
|
1372
|
-
return true;
|
|
1373
|
-
}
|
|
1374
|
-
return false;
|
|
1375
|
-
};
|
|
1376
|
-
// NB: this does not preserve comment whitespace.
|
|
1377
|
-
const wrapComment = function (width, move) {
|
|
1378
|
-
for (const token of move.split(' ')) {
|
|
1379
|
-
if (!token) {
|
|
1380
|
-
continue;
|
|
1381
|
-
}
|
|
1382
|
-
if (width + token.length > maxWidth) {
|
|
1383
|
-
while (strip()) {
|
|
1384
|
-
width--;
|
|
1385
|
-
}
|
|
1386
|
-
result.push(newline);
|
|
1387
|
-
width = 0;
|
|
1388
|
-
}
|
|
1389
|
-
result.push(token);
|
|
1390
|
-
width += token.length;
|
|
1391
|
-
result.push(' ');
|
|
1392
|
-
width++;
|
|
1393
|
-
}
|
|
1394
|
-
if (strip()) {
|
|
1395
|
-
width--;
|
|
1396
|
-
}
|
|
1397
|
-
return width;
|
|
1398
|
-
};
|
|
1399
|
-
// wrap the PGN output at max_width
|
|
1400
|
-
let currentWidth = 0;
|
|
1401
|
-
for (let i = 0; i < moves.length; i++) {
|
|
1402
|
-
if (currentWidth + moves[i].length > maxWidth) {
|
|
1403
|
-
if (moves[i].includes('{')) {
|
|
1404
|
-
currentWidth = wrapComment(currentWidth, moves[i]);
|
|
1405
|
-
continue;
|
|
1406
|
-
}
|
|
1407
|
-
}
|
|
1408
|
-
// if the current move will push past max_width
|
|
1409
|
-
if (currentWidth + moves[i].length > maxWidth && i !== 0) {
|
|
1410
|
-
// don't end the line with whitespace
|
|
1411
|
-
if (result[result.length - 1] === ' ') {
|
|
1412
|
-
result.pop();
|
|
1413
|
-
}
|
|
1414
|
-
result.push(newline);
|
|
1415
|
-
currentWidth = 0;
|
|
1416
|
-
}
|
|
1417
|
-
else if (i !== 0) {
|
|
1418
|
-
result.push(' ');
|
|
1419
|
-
currentWidth++;
|
|
1420
|
-
}
|
|
1421
|
-
result.push(moves[i]);
|
|
1422
|
-
currentWidth += moves[i].length;
|
|
1423
|
-
}
|
|
1424
|
-
return result.join('');
|
|
1425
|
-
}
|
|
1426
|
-
/*
|
|
1427
|
-
* @deprecated Use `setHeader` and `getHeaders` instead.
|
|
1428
|
-
*/
|
|
1429
|
-
header(...args) {
|
|
1430
|
-
for (let i = 0; i < args.length; i += 2) {
|
|
1431
|
-
if (typeof args[i] === 'string' && typeof args[i + 1] === 'string') {
|
|
1432
|
-
this._header[args[i]] = args[i + 1];
|
|
1433
|
-
}
|
|
1434
|
-
}
|
|
1435
|
-
return this._header;
|
|
1436
|
-
}
|
|
1437
|
-
setHeader(key, value) {
|
|
1438
|
-
this._header[key] = value;
|
|
1439
|
-
return this._header;
|
|
1440
|
-
}
|
|
1441
|
-
removeHeader(key) {
|
|
1442
|
-
if (key in this._header) {
|
|
1443
|
-
delete this._header[key];
|
|
1444
|
-
return true;
|
|
1445
|
-
}
|
|
1446
|
-
return false;
|
|
1447
|
-
}
|
|
1448
|
-
getHeaders() {
|
|
1449
|
-
return this._header;
|
|
1450
|
-
}
|
|
1451
|
-
loadPgn(pgn, { strict = false, newlineChar = '\r?\n', } = {}) {
|
|
1452
|
-
function mask(str) {
|
|
1453
|
-
return str.replace(/\\/g, '\\');
|
|
1454
|
-
}
|
|
1455
|
-
function parsePgnHeader(header) {
|
|
1456
|
-
const headerObj = {};
|
|
1457
|
-
const headers = header.split(new RegExp(mask(newlineChar)));
|
|
1458
|
-
let key = '';
|
|
1459
|
-
let value = '';
|
|
1460
|
-
for (let i = 0; i < headers.length; i++) {
|
|
1461
|
-
const regex = /^\s*\[\s*([A-Za-z]+)\s*"(.*)"\s*\]\s*$/;
|
|
1462
|
-
key = headers[i].replace(regex, '$1');
|
|
1463
|
-
value = headers[i].replace(regex, '$2');
|
|
1464
|
-
if (key.trim().length > 0) {
|
|
1465
|
-
headerObj[key] = value;
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
return headerObj;
|
|
1469
|
-
}
|
|
1470
|
-
// strip whitespace from head/tail of PGN block
|
|
1471
|
-
pgn = pgn.trim();
|
|
1472
|
-
/*
|
|
1473
|
-
* RegExp to split header. Takes advantage of the fact that header and movetext
|
|
1474
|
-
* will always have a blank line between them (ie, two newline_char's). Handles
|
|
1475
|
-
* case where movetext is empty by matching newlineChar until end of string is
|
|
1476
|
-
* matched - effectively trimming from the end extra newlineChar.
|
|
1477
|
-
*
|
|
1478
|
-
* With default newline_char, will equal:
|
|
1479
|
-
* /^(\[((?:\r?\n)|.)*\])((?:\s*\r?\n){2}|(?:\s*\r?\n)*$)/
|
|
1480
|
-
*/
|
|
1481
|
-
const headerRegex = new RegExp('^(\\[((?:' +
|
|
1482
|
-
mask(newlineChar) +
|
|
1483
|
-
')|.)*\\])' +
|
|
1484
|
-
'((?:\\s*' +
|
|
1485
|
-
mask(newlineChar) +
|
|
1486
|
-
'){2}|(?:\\s*' +
|
|
1487
|
-
mask(newlineChar) +
|
|
1488
|
-
')*$)');
|
|
1489
|
-
// If no header given, begin with moves.
|
|
1490
|
-
const headerRegexResults = headerRegex.exec(pgn);
|
|
1491
|
-
const headerString = headerRegexResults
|
|
1492
|
-
? headerRegexResults.length >= 2
|
|
1493
|
-
? headerRegexResults[1]
|
|
1494
|
-
: ''
|
|
1495
|
-
: '';
|
|
1496
|
-
// Put the board in the starting position
|
|
1497
|
-
this.reset();
|
|
1498
|
-
// parse PGN header
|
|
1499
|
-
const headers = parsePgnHeader(headerString);
|
|
1500
|
-
let fen = '';
|
|
1501
|
-
for (const key in headers) {
|
|
1502
|
-
// check to see user is including fen (possibly with wrong tag case)
|
|
1503
|
-
if (key.toLowerCase() === 'fen') {
|
|
1504
|
-
fen = headers[key];
|
|
1505
|
-
}
|
|
1506
|
-
this.header(key, headers[key]);
|
|
1507
|
-
}
|
|
1508
|
-
/*
|
|
1509
|
-
* the permissive parser should attempt to load a fen tag, even if it's the
|
|
1510
|
-
* wrong case and doesn't include a corresponding [SetUp "1"] tag
|
|
1511
|
-
*/
|
|
1512
|
-
if (!strict) {
|
|
1513
|
-
if (fen) {
|
|
1514
|
-
this.load(fen, { preserveHeaders: true });
|
|
1515
|
-
}
|
|
1516
|
-
}
|
|
1517
|
-
else {
|
|
1518
|
-
/*
|
|
1519
|
-
* strict parser - load the starting position indicated by [Setup '1']
|
|
1520
|
-
* and [FEN position]
|
|
1521
|
-
*/
|
|
1522
|
-
if (headers['SetUp'] === '1') {
|
|
1523
|
-
if (!('FEN' in headers)) {
|
|
1524
|
-
throw new Error('Invalid PGN: FEN tag must be supplied with SetUp tag');
|
|
1525
|
-
}
|
|
1526
|
-
// don't clear the headers when loading
|
|
1527
|
-
this.load(headers['FEN'], { preserveHeaders: true });
|
|
1528
|
-
}
|
|
1529
|
-
}
|
|
1530
|
-
/*
|
|
1531
|
-
* NB: the regexes below that delete move numbers, recursive annotations,
|
|
1532
|
-
* and numeric annotation glyphs may also match text in comments. To
|
|
1533
|
-
* prevent this, we transform comments by hex-encoding them in place and
|
|
1534
|
-
* decoding them again after the other tokens have been deleted.
|
|
1535
|
-
*
|
|
1536
|
-
* While the spec states that PGN files should be ASCII encoded, we use
|
|
1537
|
-
* {en,de}codeURIComponent here to support arbitrary UTF8 as a convenience
|
|
1538
|
-
* for modern users
|
|
1539
|
-
*/
|
|
1540
|
-
function toHex(s) {
|
|
1541
|
-
return Array.from(s)
|
|
1542
|
-
.map(function (c) {
|
|
1543
|
-
/*
|
|
1544
|
-
* encodeURI doesn't transform most ASCII characters, so we handle
|
|
1545
|
-
* these ourselves
|
|
1546
|
-
*/
|
|
1547
|
-
return c.charCodeAt(0) < 128
|
|
1548
|
-
? c.charCodeAt(0).toString(16)
|
|
1549
|
-
: encodeURIComponent(c).replace(/%/g, '').toLowerCase();
|
|
1550
|
-
})
|
|
1551
|
-
.join('');
|
|
1552
|
-
}
|
|
1553
|
-
function fromHex(s) {
|
|
1554
|
-
return s.length == 0
|
|
1555
|
-
? ''
|
|
1556
|
-
: decodeURIComponent('%' + (s.match(/.{1,2}/g) || []).join('%'));
|
|
1557
|
-
}
|
|
1558
|
-
const encodeComment = function (s) {
|
|
1559
|
-
s = s.replace(new RegExp(mask(newlineChar), 'g'), ' ');
|
|
1560
|
-
return `{${toHex(s.slice(1, s.length - 1))}}`;
|
|
1561
|
-
};
|
|
1562
|
-
const decodeComment = function (s) {
|
|
1563
|
-
if (s.startsWith('{') && s.endsWith('}')) {
|
|
1564
|
-
return fromHex(s.slice(1, s.length - 1));
|
|
1565
|
-
}
|
|
1566
|
-
};
|
|
1567
|
-
// delete header to get the moves
|
|
1568
|
-
let ms = pgn
|
|
1569
|
-
.replace(headerString, '')
|
|
1570
|
-
.replace(
|
|
1571
|
-
// encode comments so they don't get deleted below
|
|
1572
|
-
new RegExp(`({[^}]*})+?|;([^${mask(newlineChar)}]*)`, 'g'), function (_match, bracket, semicolon) {
|
|
1573
|
-
return bracket !== undefined
|
|
1574
|
-
? encodeComment(bracket)
|
|
1575
|
-
: ' ' + encodeComment(`{${semicolon.slice(1)}}`);
|
|
1576
|
-
})
|
|
1577
|
-
.replace(new RegExp(mask(newlineChar), 'g'), ' ');
|
|
1578
|
-
// delete recursive annotation variations
|
|
1579
|
-
const ravRegex = /(\([^()]+\))+?/g;
|
|
1580
|
-
while (ravRegex.test(ms)) {
|
|
1581
|
-
ms = ms.replace(ravRegex, '');
|
|
1582
|
-
}
|
|
1583
|
-
// delete move numbers
|
|
1584
|
-
ms = ms.replace(/\d+\.(\.\.)?/g, '');
|
|
1585
|
-
// delete ... indicating black to move
|
|
1586
|
-
ms = ms.replace(/\.\.\./g, '');
|
|
1587
|
-
/* delete numeric annotation glyphs */
|
|
1588
|
-
ms = ms.replace(/\$\d+/g, '');
|
|
1589
|
-
// trim and get array of moves
|
|
1590
|
-
let moves = ms.trim().split(new RegExp(/\s+/));
|
|
1591
|
-
// delete empty entries
|
|
1592
|
-
moves = moves.filter((move) => move !== '');
|
|
1593
|
-
let result = '';
|
|
1594
|
-
for (let halfMove = 0; halfMove < moves.length; halfMove++) {
|
|
1595
|
-
const comment = decodeComment(moves[halfMove]);
|
|
1596
|
-
if (comment !== undefined) {
|
|
1597
|
-
this._comments[this.fen()] = comment;
|
|
1598
|
-
continue;
|
|
1599
|
-
}
|
|
1600
|
-
const move = this._moveFromSan(moves[halfMove], strict);
|
|
1601
|
-
// invalid move
|
|
1602
|
-
if (move == null) {
|
|
1603
|
-
// was the move an end of game marker
|
|
1604
|
-
if (TERMINATION_MARKERS.indexOf(moves[halfMove]) > -1) {
|
|
1605
|
-
result = moves[halfMove];
|
|
1606
|
-
}
|
|
1607
|
-
else {
|
|
1608
|
-
throw new Error(`Invalid move in PGN: ${moves[halfMove]}`);
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
else {
|
|
1612
|
-
// reset the end of game marker if making a valid move
|
|
1613
|
-
result = '';
|
|
1614
|
-
this._makeMove(move);
|
|
1615
|
-
this._incPositionCount(this.fen());
|
|
1616
|
-
}
|
|
1617
|
-
}
|
|
1618
|
-
/*
|
|
1619
|
-
* Per section 8.2.6 of the PGN spec, the Result tag pair must match match
|
|
1620
|
-
* the termination marker. Only do this when headers are present, but the
|
|
1621
|
-
* result tag is missing
|
|
1622
|
-
*/
|
|
1623
|
-
if (result && Object.keys(this._header).length && !this._header['Result']) {
|
|
1624
|
-
this.header('Result', result);
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
/*
|
|
1628
|
-
* Convert a move from 0x88 coordinates to Standard Algebraic Notation
|
|
1629
|
-
* (SAN)
|
|
1630
|
-
*
|
|
1631
|
-
* @param {boolean} strict Use the strict SAN parser. It will throw errors
|
|
1632
|
-
* on overly disambiguated moves (see below):
|
|
1633
|
-
*
|
|
1634
|
-
* r1bqkbnr/ppp2ppp/2n5/1B1pP3/4P3/8/PPPP2PP/RNBQK1NR b KQkq - 2 4
|
|
1635
|
-
* 4. ... Nge7 is overly disambiguated because the knight on c6 is pinned
|
|
1636
|
-
* 4. ... Ne7 is technically the valid SAN
|
|
1637
|
-
*/
|
|
1638
|
-
_moveToSan(move, moves) {
|
|
1639
|
-
let output = '';
|
|
1640
|
-
if (move.flags & BITS.KSIDE_CASTLE) {
|
|
1641
|
-
output = 'O-O';
|
|
1642
|
-
}
|
|
1643
|
-
else if (move.flags & BITS.QSIDE_CASTLE) {
|
|
1644
|
-
output = 'O-O-O';
|
|
1645
|
-
}
|
|
1646
|
-
else {
|
|
1647
|
-
if (move.piece !== PAWN) {
|
|
1648
|
-
const disambiguator = getDisambiguator(move, moves);
|
|
1649
|
-
output += move.piece.toUpperCase() + disambiguator;
|
|
1650
|
-
}
|
|
1651
|
-
if (move.flags & (BITS.CAPTURE | BITS.EP_CAPTURE)) {
|
|
1652
|
-
if (move.piece === PAWN) {
|
|
1653
|
-
output += algebraic(move.from)[0];
|
|
1654
|
-
}
|
|
1655
|
-
output += 'x';
|
|
1656
|
-
}
|
|
1657
|
-
output += algebraic(move.to);
|
|
1658
|
-
if (move.promotion) {
|
|
1659
|
-
output += '=' + move.promotion.toUpperCase();
|
|
1660
|
-
}
|
|
1661
|
-
}
|
|
1662
|
-
this._makeMove(move);
|
|
1663
|
-
if (this.isCheck()) {
|
|
1664
|
-
if (this.isCheckmate()) {
|
|
1665
|
-
output += '#';
|
|
1666
|
-
}
|
|
1667
|
-
else {
|
|
1668
|
-
output += '+';
|
|
1669
|
-
}
|
|
1670
|
-
}
|
|
1671
|
-
this._undoMove();
|
|
1672
|
-
return output;
|
|
1673
|
-
}
|
|
1674
|
-
// convert a move from Standard Algebraic Notation (SAN) to 0x88 coordinates
|
|
1675
|
-
_moveFromSan(move, strict = false) {
|
|
1676
|
-
// strip off any move decorations: e.g Nf3+?! becomes Nf3
|
|
1677
|
-
const cleanMove = strippedSan(move);
|
|
1678
|
-
let pieceType = inferPieceType(cleanMove);
|
|
1679
|
-
let moves = this._moves({ legal: true, piece: pieceType });
|
|
1680
|
-
// strict parser
|
|
1681
|
-
for (let i = 0, len = moves.length; i < len; i++) {
|
|
1682
|
-
if (cleanMove === strippedSan(this._moveToSan(moves[i], moves))) {
|
|
1683
|
-
return moves[i];
|
|
1684
|
-
}
|
|
1685
|
-
}
|
|
1686
|
-
// the strict parser failed
|
|
1687
|
-
if (strict) {
|
|
1688
|
-
return null;
|
|
1689
|
-
}
|
|
1690
|
-
let piece = undefined;
|
|
1691
|
-
let matches = undefined;
|
|
1692
|
-
let from = undefined;
|
|
1693
|
-
let to = undefined;
|
|
1694
|
-
let promotion = undefined;
|
|
1695
|
-
/*
|
|
1696
|
-
* The default permissive (non-strict) parser allows the user to parse
|
|
1697
|
-
* non-standard chess notations. This parser is only run after the strict
|
|
1698
|
-
* Standard Algebraic Notation (SAN) parser has failed.
|
|
1699
|
-
*
|
|
1700
|
-
* When running the permissive parser, we'll run a regex to grab the piece, the
|
|
1701
|
-
* to/from square, and an optional promotion piece. This regex will
|
|
1702
|
-
* parse common non-standard notation like: Pe2-e4, Rc1c4, Qf3xf7,
|
|
1703
|
-
* f7f8q, b1c3
|
|
1704
|
-
*
|
|
1705
|
-
* NOTE: Some positions and moves may be ambiguous when using the permissive
|
|
1706
|
-
* parser. For example, in this position: 6k1/8/8/B7/8/8/8/BN4K1 w - - 0 1,
|
|
1707
|
-
* the move b1c3 may be interpreted as Nc3 or B1c3 (a disambiguated bishop
|
|
1708
|
-
* move). In these cases, the permissive parser will default to the most
|
|
1709
|
-
* basic interpretation (which is b1c3 parsing to Nc3).
|
|
1710
|
-
*/
|
|
1711
|
-
let overlyDisambiguated = false;
|
|
1712
|
-
matches = cleanMove.match(/([pnbrqkPNBRQK])?([a-h][1-8])x?-?([a-h][1-8])([qrbnQRBN])?/);
|
|
1713
|
-
if (matches) {
|
|
1714
|
-
piece = matches[1];
|
|
1715
|
-
from = matches[2];
|
|
1716
|
-
to = matches[3];
|
|
1717
|
-
promotion = matches[4];
|
|
1718
|
-
if (from.length == 1) {
|
|
1719
|
-
overlyDisambiguated = true;
|
|
1720
|
-
}
|
|
1721
|
-
}
|
|
1722
|
-
else {
|
|
1723
|
-
/*
|
|
1724
|
-
* The [a-h]?[1-8]? portion of the regex below handles moves that may be
|
|
1725
|
-
* overly disambiguated (e.g. Nge7 is unnecessary and non-standard when
|
|
1726
|
-
* there is one legal knight move to e7). In this case, the value of
|
|
1727
|
-
* 'from' variable will be a rank or file, not a square.
|
|
1728
|
-
*/
|
|
1729
|
-
matches = cleanMove.match(/([pnbrqkPNBRQK])?([a-h]?[1-8]?)x?-?([a-h][1-8])([qrbnQRBN])?/);
|
|
1730
|
-
if (matches) {
|
|
1731
|
-
piece = matches[1];
|
|
1732
|
-
from = matches[2];
|
|
1733
|
-
to = matches[3];
|
|
1734
|
-
promotion = matches[4];
|
|
1735
|
-
if (from.length == 1) {
|
|
1736
|
-
overlyDisambiguated = true;
|
|
1737
|
-
}
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
pieceType = inferPieceType(cleanMove);
|
|
1741
|
-
moves = this._moves({
|
|
1742
|
-
legal: true,
|
|
1743
|
-
piece: piece ? piece : pieceType,
|
|
1744
|
-
});
|
|
1745
|
-
if (!to) {
|
|
1746
|
-
return null;
|
|
1747
|
-
}
|
|
1748
|
-
for (let i = 0, len = moves.length; i < len; i++) {
|
|
1749
|
-
if (!from) {
|
|
1750
|
-
// if there is no from square, it could be just 'x' missing from a capture
|
|
1751
|
-
if (cleanMove ===
|
|
1752
|
-
strippedSan(this._moveToSan(moves[i], moves)).replace('x', '')) {
|
|
1753
|
-
return moves[i];
|
|
1754
|
-
}
|
|
1755
|
-
// hand-compare move properties with the results from our permissive regex
|
|
1756
|
-
}
|
|
1757
|
-
else if ((!piece || piece.toLowerCase() == moves[i].piece) &&
|
|
1758
|
-
Ox88[from] == moves[i].from &&
|
|
1759
|
-
Ox88[to] == moves[i].to &&
|
|
1760
|
-
(!promotion || promotion.toLowerCase() == moves[i].promotion)) {
|
|
1761
|
-
return moves[i];
|
|
1762
|
-
}
|
|
1763
|
-
else if (overlyDisambiguated) {
|
|
1764
|
-
/*
|
|
1765
|
-
* SPECIAL CASE: we parsed a move string that may have an unneeded
|
|
1766
|
-
* rank/file disambiguator (e.g. Nge7). The 'from' variable will
|
|
1767
|
-
*/
|
|
1768
|
-
const square = algebraic(moves[i].from);
|
|
1769
|
-
if ((!piece || piece.toLowerCase() == moves[i].piece) &&
|
|
1770
|
-
Ox88[to] == moves[i].to &&
|
|
1771
|
-
(from == square[0] || from == square[1]) &&
|
|
1772
|
-
(!promotion || promotion.toLowerCase() == moves[i].promotion)) {
|
|
1773
|
-
return moves[i];
|
|
1774
|
-
}
|
|
1775
|
-
}
|
|
1776
|
-
}
|
|
1777
|
-
return null;
|
|
1778
|
-
}
|
|
1779
|
-
ascii() {
|
|
1780
|
-
let s = ' +------------------------+\n';
|
|
1781
|
-
for (let i = Ox88.a8; i <= Ox88.h1; i++) {
|
|
1782
|
-
// display the rank
|
|
1783
|
-
if (file(i) === 0) {
|
|
1784
|
-
s += ' ' + '87654321'[rank(i)] + ' |';
|
|
1785
|
-
}
|
|
1786
|
-
if (this._board[i]) {
|
|
1787
|
-
const piece = this._board[i].type;
|
|
1788
|
-
const color = this._board[i].color;
|
|
1789
|
-
const symbol = color === WHITE ? piece.toUpperCase() : piece.toLowerCase();
|
|
1790
|
-
s += ' ' + symbol + ' ';
|
|
1791
|
-
}
|
|
1792
|
-
else {
|
|
1793
|
-
s += ' . ';
|
|
1794
|
-
}
|
|
1795
|
-
if ((i + 1) & 0x88) {
|
|
1796
|
-
s += '|\n';
|
|
1797
|
-
i += 8;
|
|
1798
|
-
}
|
|
1799
|
-
}
|
|
1800
|
-
s += ' +------------------------+\n';
|
|
1801
|
-
s += ' a b c d e f g h';
|
|
1802
|
-
return s;
|
|
1803
|
-
}
|
|
1804
|
-
perft(depth) {
|
|
1805
|
-
const moves = this._moves({ legal: false });
|
|
1806
|
-
let nodes = 0;
|
|
1807
|
-
const color = this._turn;
|
|
1808
|
-
for (let i = 0, len = moves.length; i < len; i++) {
|
|
1809
|
-
this._makeMove(moves[i]);
|
|
1810
|
-
if (!this._isKingAttacked(color)) {
|
|
1811
|
-
if (depth - 1 > 0) {
|
|
1812
|
-
nodes += this.perft(depth - 1);
|
|
1813
|
-
}
|
|
1814
|
-
else {
|
|
1815
|
-
nodes++;
|
|
1816
|
-
}
|
|
1817
|
-
}
|
|
1818
|
-
this._undoMove();
|
|
1819
|
-
}
|
|
1820
|
-
return nodes;
|
|
1821
|
-
}
|
|
1822
|
-
turn() {
|
|
1823
|
-
return this._turn;
|
|
1824
|
-
}
|
|
1825
|
-
board() {
|
|
1826
|
-
const output = [];
|
|
1827
|
-
let row = [];
|
|
1828
|
-
for (let i = Ox88.a8; i <= Ox88.h1; i++) {
|
|
1829
|
-
if (this._board[i] == null) {
|
|
1830
|
-
row.push(null);
|
|
1831
|
-
}
|
|
1832
|
-
else {
|
|
1833
|
-
row.push({
|
|
1834
|
-
square: algebraic(i),
|
|
1835
|
-
type: this._board[i].type,
|
|
1836
|
-
color: this._board[i].color,
|
|
1837
|
-
});
|
|
1838
|
-
}
|
|
1839
|
-
if ((i + 1) & 0x88) {
|
|
1840
|
-
output.push(row);
|
|
1841
|
-
row = [];
|
|
1842
|
-
i += 8;
|
|
1843
|
-
}
|
|
1844
|
-
}
|
|
1845
|
-
return output;
|
|
1846
|
-
}
|
|
1847
|
-
squareColor(square) {
|
|
1848
|
-
if (square in Ox88) {
|
|
1849
|
-
const sq = Ox88[square];
|
|
1850
|
-
return (rank(sq) + file(sq)) % 2 === 0 ? 'light' : 'dark';
|
|
1851
|
-
}
|
|
1852
|
-
return null;
|
|
1853
|
-
}
|
|
1854
|
-
history({ verbose = false } = {}) {
|
|
1855
|
-
const reversedHistory = [];
|
|
1856
|
-
const moveHistory = [];
|
|
1857
|
-
while (this._history.length > 0) {
|
|
1858
|
-
reversedHistory.push(this._undoMove());
|
|
1859
|
-
}
|
|
1860
|
-
while (true) {
|
|
1861
|
-
const move = reversedHistory.pop();
|
|
1862
|
-
if (!move) {
|
|
1863
|
-
break;
|
|
1864
|
-
}
|
|
1865
|
-
if (verbose) {
|
|
1866
|
-
moveHistory.push(new Move$1(this, move));
|
|
1867
|
-
}
|
|
1868
|
-
else {
|
|
1869
|
-
moveHistory.push(this._moveToSan(move, this._moves()));
|
|
1870
|
-
}
|
|
1871
|
-
this._makeMove(move);
|
|
1872
|
-
}
|
|
1873
|
-
return moveHistory;
|
|
1874
|
-
}
|
|
1875
|
-
/*
|
|
1876
|
-
* Keeps track of position occurrence counts for the purpose of repetition
|
|
1877
|
-
* checking. All three methods (`_inc`, `_dec`, and `_get`) trim the
|
|
1878
|
-
* irrelevent information from the fen, initialising new positions, and
|
|
1879
|
-
* removing old positions from the record if their counts are reduced to 0.
|
|
1880
|
-
*/
|
|
1881
|
-
_getPositionCount(fen) {
|
|
1882
|
-
const trimmedFen = trimFen(fen);
|
|
1883
|
-
return this._positionCount[trimmedFen] || 0;
|
|
1884
|
-
}
|
|
1885
|
-
_incPositionCount(fen) {
|
|
1886
|
-
const trimmedFen = trimFen(fen);
|
|
1887
|
-
if (this._positionCount[trimmedFen] === undefined) {
|
|
1888
|
-
this._positionCount[trimmedFen] = 0;
|
|
1889
|
-
}
|
|
1890
|
-
this._positionCount[trimmedFen] += 1;
|
|
1891
|
-
}
|
|
1892
|
-
_decPositionCount(fen) {
|
|
1893
|
-
const trimmedFen = trimFen(fen);
|
|
1894
|
-
if (this._positionCount[trimmedFen] === 1) {
|
|
1895
|
-
delete this._positionCount[trimmedFen];
|
|
1896
|
-
}
|
|
1897
|
-
else {
|
|
1898
|
-
this._positionCount[trimmedFen] -= 1;
|
|
1899
|
-
}
|
|
1900
|
-
}
|
|
1901
|
-
_pruneComments() {
|
|
1902
|
-
const reversedHistory = [];
|
|
1903
|
-
const currentComments = {};
|
|
1904
|
-
const copyComment = (fen) => {
|
|
1905
|
-
if (fen in this._comments) {
|
|
1906
|
-
currentComments[fen] = this._comments[fen];
|
|
1907
|
-
}
|
|
1908
|
-
};
|
|
1909
|
-
while (this._history.length > 0) {
|
|
1910
|
-
reversedHistory.push(this._undoMove());
|
|
1911
|
-
}
|
|
1912
|
-
copyComment(this.fen());
|
|
1913
|
-
while (true) {
|
|
1914
|
-
const move = reversedHistory.pop();
|
|
1915
|
-
if (!move) {
|
|
1916
|
-
break;
|
|
1917
|
-
}
|
|
1918
|
-
this._makeMove(move);
|
|
1919
|
-
copyComment(this.fen());
|
|
1920
|
-
}
|
|
1921
|
-
this._comments = currentComments;
|
|
1922
|
-
}
|
|
1923
|
-
getComment() {
|
|
1924
|
-
return this._comments[this.fen()];
|
|
1925
|
-
}
|
|
1926
|
-
setComment(comment) {
|
|
1927
|
-
this._comments[this.fen()] = comment.replace('{', '[').replace('}', ']');
|
|
1928
|
-
}
|
|
1929
|
-
/**
|
|
1930
|
-
* @deprecated Renamed to `removeComment` for consistency
|
|
1931
|
-
*/
|
|
1932
|
-
deleteComment() {
|
|
1933
|
-
return this.removeComment();
|
|
1934
|
-
}
|
|
1935
|
-
removeComment() {
|
|
1936
|
-
const comment = this._comments[this.fen()];
|
|
1937
|
-
delete this._comments[this.fen()];
|
|
1938
|
-
return comment;
|
|
1939
|
-
}
|
|
1940
|
-
getComments() {
|
|
1941
|
-
this._pruneComments();
|
|
1942
|
-
return Object.keys(this._comments).map((fen) => {
|
|
1943
|
-
return { fen: fen, comment: this._comments[fen] };
|
|
1944
|
-
});
|
|
1945
|
-
}
|
|
1946
|
-
/**
|
|
1947
|
-
* @deprecated Renamed to `removeComments` for consistency
|
|
1948
|
-
*/
|
|
1949
|
-
deleteComments() {
|
|
1950
|
-
return this.removeComments();
|
|
1951
|
-
}
|
|
1952
|
-
removeComments() {
|
|
1953
|
-
this._pruneComments();
|
|
1954
|
-
return Object.keys(this._comments).map((fen) => {
|
|
1955
|
-
const comment = this._comments[fen];
|
|
1956
|
-
delete this._comments[fen];
|
|
1957
|
-
return { fen: fen, comment: comment };
|
|
1958
|
-
});
|
|
1959
|
-
}
|
|
1960
|
-
setCastlingRights(color, rights) {
|
|
1961
|
-
for (const side of [KING, QUEEN]) {
|
|
1962
|
-
if (rights[side] !== undefined) {
|
|
1963
|
-
if (rights[side]) {
|
|
1964
|
-
this._castling[color] |= SIDES[side];
|
|
1965
|
-
}
|
|
1966
|
-
else {
|
|
1967
|
-
this._castling[color] &= ~SIDES[side];
|
|
1968
|
-
}
|
|
1969
|
-
}
|
|
1970
|
-
}
|
|
1971
|
-
this._updateCastlingRights();
|
|
1972
|
-
const result = this.getCastlingRights(color);
|
|
1973
|
-
return ((rights[KING] === undefined || rights[KING] === result[KING]) &&
|
|
1974
|
-
(rights[QUEEN] === undefined || rights[QUEEN] === result[QUEEN]));
|
|
1975
|
-
}
|
|
1976
|
-
getCastlingRights(color) {
|
|
1977
|
-
return {
|
|
1978
|
-
[KING]: (this._castling[color] & SIDES[KING]) !== 0,
|
|
1979
|
-
[QUEEN]: (this._castling[color] & SIDES[QUEEN]) !== 0,
|
|
1980
|
-
};
|
|
1981
|
-
}
|
|
1982
|
-
moveNumber() {
|
|
1983
|
-
return this._moveNumber;
|
|
1984
|
-
}
|
|
1985
|
-
}
|
|
1986
|
-
|
|
1987
|
-
const animationTime = {
|
|
1988
|
-
'fast': 200,
|
|
1989
|
-
'slow': 600,
|
|
1990
|
-
'normal': 400,
|
|
1991
|
-
'verySlow': 1000,
|
|
1992
|
-
'veryFast': 100
|
|
1993
|
-
};
|
|
1994
|
-
|
|
1995
|
-
const boolValues = {
|
|
1996
|
-
'true': true,
|
|
1997
|
-
'false': false,
|
|
1998
|
-
'none': false,
|
|
1999
|
-
1: true,
|
|
2000
|
-
0: false
|
|
2001
|
-
};
|
|
2002
|
-
|
|
2003
|
-
const transitionFunctions = {
|
|
2004
|
-
'ease': 'ease',
|
|
2005
|
-
'linear': 'linear',
|
|
2006
|
-
'ease-in': 'ease-in',
|
|
2007
|
-
'ease-out': 'ease-out',
|
|
2008
|
-
'ease-in-out': 'ease-in-out',
|
|
2009
|
-
'none': null
|
|
2010
|
-
};
|
|
2011
|
-
|
|
2012
|
-
class ChessboardConfig {
|
|
2013
|
-
constructor(settings) {
|
|
2014
|
-
const defaults = {
|
|
2015
|
-
id: 'board',
|
|
2016
|
-
position: 'start',
|
|
2017
|
-
orientation: 'w',
|
|
2018
|
-
mode: 'normal',
|
|
2019
|
-
size: 'auto',
|
|
2020
|
-
draggable: true,
|
|
2021
|
-
hints: true,
|
|
2022
|
-
clickable: true,
|
|
2023
|
-
movableColors: 'both',
|
|
2024
|
-
moveHighlight: true,
|
|
2025
|
-
overHighlight: true,
|
|
2026
|
-
moveAnimation: 'ease',
|
|
2027
|
-
moveTime: 'fast',
|
|
2028
|
-
dropOffBoard: 'snapback',
|
|
2029
|
-
snapbackTime: 'fast',
|
|
2030
|
-
snapbackAnimation: 'ease',
|
|
2031
|
-
fadeTime: 'fast',
|
|
2032
|
-
fadeAnimation: 'ease',
|
|
2033
|
-
ratio: 0.9,
|
|
2034
|
-
piecesPath: '../assets/themes/default',
|
|
2035
|
-
onMove: () => true,
|
|
2036
|
-
onMoveEnd: () => true,
|
|
2037
|
-
onChange: () => true,
|
|
2038
|
-
onDragStart: () => true,
|
|
2039
|
-
onDragMove: () => true,
|
|
2040
|
-
onDrop: () => true,
|
|
2041
|
-
onSnapbackEnd: () => true,
|
|
2042
|
-
whiteSquare: '#f0d9b5',
|
|
2043
|
-
blackSquare: '#b58863',
|
|
2044
|
-
highlight: 'yellow',
|
|
2045
|
-
selectedSquareWhite: '#ababaa',
|
|
2046
|
-
selectedSquareBlack: '#ababaa',
|
|
2047
|
-
movedSquareWhite: '#f1f1a0',
|
|
2048
|
-
movedSquareBlack: '#e9e981',
|
|
2049
|
-
choiceSquare: 'white',
|
|
2050
|
-
coverSquare: 'black',
|
|
2051
|
-
hintColor: '#ababaa'
|
|
2052
|
-
};
|
|
2053
|
-
|
|
2054
|
-
const config = Object.assign({}, defaults, settings);
|
|
2055
|
-
|
|
2056
|
-
this.id_div = config.id;
|
|
2057
|
-
this.position = config.position;
|
|
2058
|
-
this.orientation = config.orientation;
|
|
2059
|
-
this.mode = config.mode;
|
|
2060
|
-
this.dropOffBoard = config.dropOffBoard;
|
|
2061
|
-
this.size = config.size;
|
|
2062
|
-
this.movableColors = config.movableColors;
|
|
2063
|
-
this.piecesPath = config.piecesPath;
|
|
2064
|
-
this.onMove = config.onMove;
|
|
2065
|
-
this.onMoveEnd = config.onMoveEnd;
|
|
2066
|
-
this.onChange = config.onChange;
|
|
2067
|
-
this.onDragStart = config.onDragStart;
|
|
2068
|
-
this.onDragMove = config.onDragMove;
|
|
2069
|
-
this.onDrop = config.onDrop;
|
|
2070
|
-
this.onSnapbackEnd = config.onSnapbackEnd;
|
|
2071
|
-
|
|
2072
|
-
this.moveAnimation = this.setTransitionFunction(config.moveAnimation);
|
|
2073
|
-
this.snapbackAnimation = this.setTransitionFunction(config.snapbackAnimation);
|
|
2074
|
-
this.fadeAnimation = this.setTransitionFunction(config.fadeAnimation);
|
|
2075
|
-
|
|
2076
|
-
this.hints = this.setBoolean(config.hints);
|
|
2077
|
-
this.clickable = this.setBoolean(config.clickable);
|
|
2078
|
-
this.draggable = this.setBoolean(config.draggable);
|
|
2079
|
-
this.moveHighlight = this.setBoolean(config.moveHighlight);
|
|
2080
|
-
this.overHighlight = this.setBoolean(config.overHighlight);
|
|
2081
|
-
|
|
2082
|
-
this.moveTime = this.setTime(config.moveTime);
|
|
2083
|
-
this.snapbackTime = this.setTime(config.snapbackTime);
|
|
2084
|
-
this.fadeTime = this.setTime(config.fadeTime);
|
|
2085
|
-
|
|
2086
|
-
this.setCSSProperty('pieceRatio', config.ratio);
|
|
2087
|
-
this.setCSSProperty('whiteSquare', config.whiteSquare);
|
|
2088
|
-
this.setCSSProperty('blackSquare', config.blackSquare);
|
|
2089
|
-
this.setCSSProperty('highlightSquare', config.highlight);
|
|
2090
|
-
this.setCSSProperty('selectedSquareWhite', config.selectedSquareWhite);
|
|
2091
|
-
this.setCSSProperty('selectedSquareBlack', config.selectedSquareBlack);
|
|
2092
|
-
this.setCSSProperty('movedSquareWhite', config.movedSquareWhite);
|
|
2093
|
-
this.setCSSProperty('movedSquareBlack', config.movedSquareBlack);
|
|
2094
|
-
this.setCSSProperty('choiceSquare', config.choiceSquare);
|
|
2095
|
-
this.setCSSProperty('coverSquare', config.coverSquare);
|
|
2096
|
-
this.setCSSProperty('hintColor', config.hintColor);
|
|
2097
|
-
|
|
2098
|
-
// Configure modes
|
|
2099
|
-
if (this.mode === 'creative') {
|
|
2100
|
-
this.onlyLegalMoves = false;
|
|
2101
|
-
this.hints = false;
|
|
2102
|
-
} else if (this.mode === 'normal') {
|
|
2103
|
-
this.onlyLegalMoves = true;
|
|
2104
|
-
}
|
|
2105
|
-
}
|
|
2106
|
-
|
|
2107
|
-
setCSSProperty(property, value) {
|
|
2108
|
-
document.documentElement.style.setProperty(`--${property}`, value);
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
setOrientation(orientation) {
|
|
2112
|
-
this.orientation = orientation;
|
|
2113
|
-
return this;
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
setTime(value) {
|
|
2117
|
-
if (typeof value === 'number') return value;
|
|
2118
|
-
if (value in animationTime) return animationTime[value];
|
|
2119
|
-
throw new Error('Invalid time value');
|
|
2120
|
-
}
|
|
2121
|
-
|
|
2122
|
-
setBoolean(value) {
|
|
2123
|
-
if (typeof value === 'boolean') return value;
|
|
2124
|
-
if (value in boolValues) return boolValues[value];
|
|
2125
|
-
throw new Error('Invalid boolean value');
|
|
2126
|
-
}
|
|
2127
|
-
|
|
2128
|
-
setTransitionFunction(value) {
|
|
2129
|
-
// Handle boolean values - true means use default 'ease', false/null means no animation
|
|
2130
|
-
if (typeof value === 'boolean') {
|
|
2131
|
-
return value ? transitionFunctions['ease'] : null;
|
|
2132
|
-
}
|
|
2133
|
-
|
|
2134
|
-
// Handle string values
|
|
2135
|
-
if (typeof value === 'string' && value in transitionFunctions) {
|
|
2136
|
-
return transitionFunctions[value];
|
|
2137
|
-
}
|
|
2138
|
-
|
|
2139
|
-
// Handle null/undefined
|
|
2140
|
-
if (value === null || value === undefined) {
|
|
2141
|
-
return null;
|
|
2142
|
-
}
|
|
2143
|
-
|
|
2144
|
-
throw new Error('Invalid transition function');
|
|
2145
|
-
}
|
|
2146
|
-
}
|
|
2147
|
-
|
|
2148
|
-
class Piece {
|
|
2149
|
-
constructor(color, type, src, opacity = 1) {
|
|
2150
|
-
this.color = color;
|
|
2151
|
-
this.type = type;
|
|
2152
|
-
this.id = this.getId();
|
|
2153
|
-
this.src = src;
|
|
2154
|
-
this.element = this.createElement(src, opacity);
|
|
2155
|
-
|
|
2156
|
-
this.check();
|
|
2157
|
-
}
|
|
2158
|
-
|
|
2159
|
-
getId() { return this.type + this.color }
|
|
2160
|
-
|
|
2161
|
-
createElement(opacity) {
|
|
2162
|
-
let element = document.createElement("img");
|
|
2163
|
-
element.classList.add("piece");
|
|
2164
|
-
element.id = this.id;
|
|
2165
|
-
element.src = this.src;
|
|
2166
|
-
element.style.opacity = opacity;
|
|
2167
|
-
return element;
|
|
2168
|
-
}
|
|
2169
|
-
|
|
2170
|
-
visible() { this.element.style.opacity = 1; }
|
|
2171
|
-
|
|
2172
|
-
invisible() { this.element.style.opacity = 0; }
|
|
2173
|
-
|
|
2174
|
-
fadeIn(duration, speed, transition_f) {
|
|
2175
|
-
let start = performance.now();
|
|
2176
|
-
let opacity = 0;
|
|
2177
|
-
let piece = this;
|
|
2178
|
-
let fade = function () {
|
|
2179
|
-
let elapsed = performance.now() - start;
|
|
2180
|
-
opacity = transition_f(elapsed, duration, speed);
|
|
2181
|
-
piece.element.style.opacity = opacity;
|
|
2182
|
-
if (elapsed < duration) {
|
|
2183
|
-
requestAnimationFrame(fade);
|
|
2184
|
-
} else {
|
|
2185
|
-
piece.element.style.opacity = 1;
|
|
2186
|
-
}
|
|
2187
|
-
};
|
|
2188
|
-
fade();
|
|
2189
|
-
}
|
|
2190
|
-
|
|
2191
|
-
fadeOut(duration, speed, transition_f) {
|
|
2192
|
-
let start = performance.now();
|
|
2193
|
-
let opacity = 1;
|
|
2194
|
-
let piece = this;
|
|
2195
|
-
let fade = function () {
|
|
2196
|
-
let elapsed = performance.now() - start;
|
|
2197
|
-
opacity = 1 - transition_f(elapsed, duration, speed);
|
|
2198
|
-
piece.element.style.opacity = opacity;
|
|
2199
|
-
if (elapsed < duration) {
|
|
2200
|
-
requestAnimationFrame(fade);
|
|
2201
|
-
} else {
|
|
2202
|
-
piece.element.style.opacity = 0;
|
|
2203
|
-
}
|
|
2204
|
-
};
|
|
2205
|
-
fade();
|
|
2206
|
-
}
|
|
2207
|
-
|
|
2208
|
-
setDrag(f) {
|
|
2209
|
-
this.element.ondragstart = (e) => { e.preventDefault(); };
|
|
2210
|
-
this.element.onmousedown = f;
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
destroy() {
|
|
2214
|
-
this.element.remove();
|
|
2215
|
-
}
|
|
2216
|
-
|
|
2217
|
-
translate(to, duration, transition_f, speed, callback = null) {
|
|
2218
|
-
|
|
2219
|
-
let sourceRect = this.element.getBoundingClientRect();
|
|
2220
|
-
let targetRect = to.getBoundingClientRect();
|
|
2221
|
-
let x_start = sourceRect.left + sourceRect.width / 2;
|
|
2222
|
-
let y_start = sourceRect.top + sourceRect.height / 2;
|
|
2223
|
-
let x_end = targetRect.left + targetRect.width / 2;
|
|
2224
|
-
let y_end = targetRect.top + targetRect.height / 2;
|
|
2225
|
-
let dx = x_end - x_start;
|
|
2226
|
-
let dy = y_end - y_start;
|
|
2227
|
-
|
|
2228
|
-
let keyframes = [
|
|
2229
|
-
{ transform: 'translate(0, 0)' },
|
|
2230
|
-
{ transform: `translate(${dx}px, ${dy}px)` }
|
|
2231
|
-
];
|
|
2232
|
-
|
|
2233
|
-
if (this.element.animate) {
|
|
2234
|
-
let animation = this.element.animate(keyframes, {
|
|
2235
|
-
duration: duration,
|
|
2236
|
-
easing: 'ease',
|
|
2237
|
-
fill: 'none'
|
|
2238
|
-
});
|
|
2239
|
-
|
|
2240
|
-
animation.onfinish = () => {
|
|
2241
|
-
if (callback) callback();
|
|
2242
|
-
this.element.style = '';
|
|
2243
|
-
};
|
|
2244
|
-
} else {
|
|
2245
|
-
this.element.style.transition = `transform ${duration}ms ease`;
|
|
2246
|
-
this.element.style.transform = `translate(${dx}px, ${dy}px)`;
|
|
2247
|
-
if (callback) callback();
|
|
2248
|
-
this.element.style = '';
|
|
2249
|
-
}
|
|
2250
|
-
}
|
|
2251
|
-
|
|
2252
|
-
check() {
|
|
2253
|
-
if (['p', 'r', 'n', 'b', 'q', 'k'].indexOf(this.type) === -1) {
|
|
2254
|
-
throw new Error("Invalid piece type");
|
|
2255
|
-
}
|
|
2256
|
-
|
|
2257
|
-
if (['w', 'b'].indexOf(this.color) === -1) {
|
|
2258
|
-
throw new Error("Invalid piece color");
|
|
2259
|
-
}
|
|
2260
|
-
}
|
|
2261
|
-
}
|
|
2262
|
-
|
|
2263
|
-
class Square {
|
|
2264
|
-
|
|
2265
|
-
constructor(row, col) {
|
|
2266
|
-
this.row = row;
|
|
2267
|
-
this.col = col;
|
|
2268
|
-
this.id = this.getId();
|
|
2269
|
-
this.element = this.createElement();
|
|
2270
|
-
this.piece = null;
|
|
2271
|
-
}
|
|
2272
|
-
|
|
2273
|
-
getPiece() {
|
|
2274
|
-
return this.piece;
|
|
2275
|
-
}
|
|
2276
|
-
|
|
2277
|
-
opposite() {
|
|
2278
|
-
this.row = 9 - this.row;
|
|
2279
|
-
this.col = 9 - this.col;
|
|
2280
|
-
this.id = this.getId();
|
|
2281
|
-
this.element = this.resetElement();
|
|
2282
|
-
}
|
|
2283
|
-
|
|
2284
|
-
isWhite() {
|
|
2285
|
-
return (this.row + this.col) % 2 === 0;
|
|
2286
|
-
}
|
|
2287
|
-
|
|
2288
|
-
getId() {
|
|
2289
|
-
let letters = 'abcdefgh';
|
|
2290
|
-
let letter = letters[this.col - 1];
|
|
2291
|
-
return letter + this.row;
|
|
2292
|
-
}
|
|
2293
|
-
|
|
2294
|
-
resetElement() {
|
|
2295
|
-
this.element.id = this.id;
|
|
2296
|
-
this.element.className = '';
|
|
2297
|
-
this.element.classList.add('square');
|
|
2298
|
-
this.element.classList.add(this.isWhite() ? 'whiteSquare' : 'blackSquare');
|
|
2299
|
-
}
|
|
2300
|
-
|
|
2301
|
-
createElement() {
|
|
2302
|
-
let element = document.createElement('div');
|
|
2303
|
-
element.id = this.id;
|
|
2304
|
-
element.classList.add('square');
|
|
2305
|
-
element.classList.add(this.isWhite() ? 'whiteSquare' : 'blackSquare');
|
|
2306
|
-
return element;
|
|
2307
|
-
}
|
|
2308
|
-
|
|
2309
|
-
getElement() {
|
|
2310
|
-
return this.element;
|
|
2311
|
-
}
|
|
2312
|
-
|
|
2313
|
-
getBoundingClientRect() {
|
|
2314
|
-
return this.element.getBoundingClientRect();
|
|
2315
|
-
}
|
|
2316
|
-
|
|
2317
|
-
removePiece() {
|
|
2318
|
-
if (!this.piece) {
|
|
2319
|
-
return null;
|
|
2320
|
-
}
|
|
2321
|
-
this.element.removeChild(this.piece.element);
|
|
2322
|
-
const piece = this.piece;
|
|
2323
|
-
this.piece = null;
|
|
2324
|
-
return piece;
|
|
2325
|
-
}
|
|
2326
|
-
|
|
2327
|
-
addEventListener(event, callback) {
|
|
2328
|
-
this.element.addEventListener(event, callback);
|
|
2329
|
-
}
|
|
2330
|
-
|
|
2331
|
-
putPiece(piece) {
|
|
2332
|
-
this.piece = piece;
|
|
2333
|
-
this.element.appendChild(piece.element);
|
|
2334
|
-
}
|
|
2335
|
-
|
|
2336
|
-
putHint(catchable) {
|
|
2337
|
-
if (this.element.querySelector('.hint')) {
|
|
2338
|
-
return;
|
|
2339
|
-
}
|
|
2340
|
-
let hint = document.createElement("div");
|
|
2341
|
-
hint.classList.add('hint');
|
|
2342
|
-
this.element.appendChild(hint);
|
|
2343
|
-
if (catchable) {
|
|
2344
|
-
hint.classList.add('catchable');
|
|
2345
|
-
}
|
|
2346
|
-
}
|
|
2347
|
-
|
|
2348
|
-
removeHint() {
|
|
2349
|
-
let hint = this.element.querySelector('.hint');
|
|
2350
|
-
if (hint) {
|
|
2351
|
-
this.element.removeChild(hint);
|
|
2352
|
-
}
|
|
2353
|
-
}
|
|
2354
|
-
|
|
2355
|
-
select() {
|
|
2356
|
-
this.element.classList.add(this.isWhite() ? 'selectedSquareWhite' : 'selectedSquareBlack');
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
deselect() {
|
|
2360
|
-
this.element.classList.remove('selectedSquareWhite');
|
|
2361
|
-
this.element.classList.remove('selectedSquareBlack');
|
|
2362
|
-
}
|
|
2363
|
-
|
|
2364
|
-
moved() {
|
|
2365
|
-
this.element.classList.add(this.isWhite() ? 'movedSquareWhite' : 'movedSquareBlack');
|
|
2366
|
-
}
|
|
2367
|
-
|
|
2368
|
-
unmoved() {
|
|
2369
|
-
this.element.classList.remove('movedSquareWhite');
|
|
2370
|
-
this.element.classList.remove('movedSquareBlack');
|
|
2371
|
-
}
|
|
2372
|
-
|
|
2373
|
-
highlight() {
|
|
2374
|
-
this.element.classList.add('highlighted');
|
|
2375
|
-
}
|
|
2376
|
-
|
|
2377
|
-
dehighlight() {
|
|
2378
|
-
this.element.classList.remove('highlighted');
|
|
2379
|
-
}
|
|
2380
|
-
|
|
2381
|
-
putCover(callback) {
|
|
2382
|
-
let cover = document.createElement("div");
|
|
2383
|
-
cover.classList.add('square');
|
|
2384
|
-
cover.classList.add('cover');
|
|
2385
|
-
this.element.appendChild(cover);
|
|
2386
|
-
cover.addEventListener('click', (e) => {
|
|
2387
|
-
e.stopPropagation();
|
|
2388
|
-
callback();
|
|
2389
|
-
});
|
|
2390
|
-
}
|
|
2391
|
-
|
|
2392
|
-
removeCover() {
|
|
2393
|
-
let cover = this.element.querySelector('.cover');
|
|
2394
|
-
if (cover) {
|
|
2395
|
-
this.element.removeChild(cover);
|
|
2396
|
-
}
|
|
2397
|
-
}
|
|
2398
|
-
|
|
2399
|
-
putPromotion(src, callback) {
|
|
2400
|
-
let choice = document.createElement("div");
|
|
2401
|
-
choice.classList.add('square');
|
|
2402
|
-
choice.classList.add('choice');
|
|
2403
|
-
this.element.appendChild(choice);
|
|
2404
|
-
let img = document.createElement("img");
|
|
2405
|
-
img.classList.add("piece");
|
|
2406
|
-
img.classList.add("choicable");
|
|
2407
|
-
img.src = src;
|
|
2408
|
-
choice.appendChild(img);
|
|
2409
|
-
choice.addEventListener('click', (e) => {
|
|
2410
|
-
e.stopPropagation();
|
|
2411
|
-
callback();
|
|
2412
|
-
});
|
|
2413
|
-
|
|
2414
|
-
}
|
|
2415
|
-
|
|
2416
|
-
removePromotion() {
|
|
2417
|
-
let choice = this.element.querySelector('.choice');
|
|
2418
|
-
if (choice) {
|
|
2419
|
-
choice.removeChild(choice.firstChild);
|
|
2420
|
-
this.element.removeChild(choice);
|
|
2421
|
-
}
|
|
2422
|
-
}
|
|
2423
|
-
|
|
2424
|
-
destroy() {
|
|
2425
|
-
this.element.remove();
|
|
2426
|
-
}
|
|
2427
|
-
|
|
2428
|
-
hasPiece() {
|
|
2429
|
-
return this.piece !== null;
|
|
2430
|
-
}
|
|
2431
|
-
|
|
2432
|
-
getColor() {
|
|
2433
|
-
return this.piece.getColor();
|
|
2434
|
-
}
|
|
2435
|
-
|
|
2436
|
-
check() {
|
|
2437
|
-
if (this.row < 1 || this.row > 8) {
|
|
2438
|
-
throw new Error("Invalid square: row is out of bounds");
|
|
2439
|
-
}
|
|
2440
|
-
if (this.col < 1 || this.col > 8) {
|
|
2441
|
-
throw new Error("Invalid square: col is out of bounds");
|
|
2442
|
-
}
|
|
2443
|
-
|
|
2444
|
-
}
|
|
2445
|
-
}
|
|
2446
|
-
|
|
2447
|
-
class Move {
|
|
2448
|
-
|
|
2449
|
-
constructor(from, to, promotion = null, check = false) {
|
|
2450
|
-
this.piece = from ? from.getPiece() : null;
|
|
2451
|
-
this.from = from;
|
|
2452
|
-
this.to = to;
|
|
2453
|
-
this.promotion = promotion;
|
|
2454
|
-
|
|
2455
|
-
if (check) this.check();
|
|
2456
|
-
}
|
|
2457
|
-
|
|
2458
|
-
hasPromotion() {
|
|
2459
|
-
return this.promotion !== null;
|
|
2460
|
-
}
|
|
2461
|
-
|
|
2462
|
-
setPromotion(promotion) {
|
|
2463
|
-
this.promotion = promotion;
|
|
2464
|
-
}
|
|
2465
|
-
|
|
2466
|
-
check() {
|
|
2467
|
-
if (this.piece === null) return false;
|
|
2468
|
-
if (!(this.piece instanceof Piece)) return false;
|
|
2469
|
-
if (['q', 'r', 'b', 'n', null].indexOf(this.promotion) === -1) return false;
|
|
2470
|
-
if (!(this.from instanceof Square)) return false;
|
|
2471
|
-
if (!(this.to instanceof Square)) return false;
|
|
2472
|
-
if (!this.to) return false;
|
|
2473
|
-
if (!this.from) return false;
|
|
2474
|
-
if (this.from === this.to) return false;
|
|
2475
|
-
return true;
|
|
2476
|
-
}
|
|
2477
|
-
|
|
2478
|
-
isLegal(game) {
|
|
2479
|
-
let destinations = game.moves({ square: this.from.id, verbose: true }).map(move => move.to);
|
|
2480
|
-
return destinations.indexOf(this.to.id) !== -1;
|
|
2481
|
-
}
|
|
2482
|
-
|
|
2483
|
-
|
|
2484
|
-
}
|
|
2485
|
-
|
|
2486
|
-
/**
|
|
2487
|
-
* Performance utilities for smooth interactions
|
|
2488
|
-
*/
|
|
2489
|
-
|
|
2490
|
-
/**
|
|
2491
|
-
* Throttle function to limit how often a function can be called
|
|
2492
|
-
* @param {Function} func - Function to throttle
|
|
2493
|
-
* @param {number} limit - Time limit in milliseconds
|
|
2494
|
-
* @returns {Function} Throttled function
|
|
2495
|
-
*/
|
|
2496
|
-
function throttle(func, limit) {
|
|
2497
|
-
let inThrottle;
|
|
2498
|
-
return function() {
|
|
2499
|
-
const args = arguments;
|
|
2500
|
-
const context = this;
|
|
2501
|
-
if (!inThrottle) {
|
|
2502
|
-
func.apply(context, args);
|
|
2503
|
-
inThrottle = true;
|
|
2504
|
-
setTimeout(() => inThrottle = false, limit);
|
|
2505
|
-
}
|
|
2506
|
-
};
|
|
2507
|
-
}
|
|
2508
|
-
|
|
2509
|
-
/**
|
|
2510
|
-
* Request animation frame throttle for smooth animations
|
|
2511
|
-
* @param {Function} func - Function to throttle
|
|
2512
|
-
* @returns {Function} RAF throttled function
|
|
2513
|
-
*/
|
|
2514
|
-
function rafThrottle(func) {
|
|
2515
|
-
let isThrottled = false;
|
|
2516
|
-
return function() {
|
|
2517
|
-
if (isThrottled) return;
|
|
2518
|
-
|
|
2519
|
-
const args = arguments;
|
|
2520
|
-
const context = this;
|
|
2521
|
-
|
|
2522
|
-
isThrottled = true;
|
|
2523
|
-
requestAnimationFrame(() => {
|
|
2524
|
-
func.apply(context, args);
|
|
2525
|
-
isThrottled = false;
|
|
2526
|
-
});
|
|
2527
|
-
};
|
|
2528
|
-
}
|
|
2529
|
-
|
|
2530
|
-
/**
|
|
2531
|
-
* High performance transform utility
|
|
2532
|
-
* @param {HTMLElement} element - Element to transform
|
|
2533
|
-
* @param {number} x - X coordinate
|
|
2534
|
-
* @param {number} y - Y coordinate
|
|
2535
|
-
* @param {number} scale - Scale factor
|
|
2536
|
-
*/
|
|
2537
|
-
function setTransform(element, x, y, scale = 1) {
|
|
2538
|
-
// Use transform3d for hardware acceleration
|
|
2539
|
-
element.style.transform = `translate3d(${x}px, ${y}px, 0) scale(${scale})`;
|
|
2540
|
-
}
|
|
2541
|
-
|
|
2542
|
-
/**
|
|
2543
|
-
* Reset element position efficiently
|
|
2544
|
-
* @param {HTMLElement} element - Element to reset
|
|
2545
|
-
*/
|
|
2546
|
-
function resetTransform(element) {
|
|
2547
|
-
element.style.transform = '';
|
|
2548
|
-
element.style.left = '';
|
|
2549
|
-
element.style.top = '';
|
|
2550
|
-
}
|
|
2551
|
-
|
|
2552
|
-
/**
|
|
2553
|
-
* Cross-browser utilities for consistent drag & drop behavior
|
|
2554
|
-
*/
|
|
2555
|
-
|
|
2556
|
-
/**
|
|
2557
|
-
* Detect browser type and version
|
|
2558
|
-
* @returns {Object} Browser information
|
|
2559
|
-
*/
|
|
2560
|
-
function getBrowserInfo() {
|
|
2561
|
-
const ua = navigator.userAgent;
|
|
2562
|
-
const isChrome = ua.includes('Chrome') && !ua.includes('Edg');
|
|
2563
|
-
const isFirefox = ua.includes('Firefox');
|
|
2564
|
-
const isSafari = ua.includes('Safari') && !ua.includes('Chrome');
|
|
2565
|
-
const isEdge = ua.includes('Edg');
|
|
2566
|
-
|
|
2567
|
-
return {
|
|
2568
|
-
isChrome,
|
|
2569
|
-
isFirefox,
|
|
2570
|
-
isSafari,
|
|
2571
|
-
isEdge,
|
|
2572
|
-
devicePixelRatio: window.devicePixelRatio || 1,
|
|
2573
|
-
userAgent: ua
|
|
2574
|
-
};
|
|
2575
|
-
}
|
|
2576
|
-
|
|
2577
|
-
/**
|
|
2578
|
-
* Browser-specific drag optimizations
|
|
2579
|
-
*/
|
|
2580
|
-
const DragOptimizations = {
|
|
2581
|
-
/**
|
|
2582
|
-
* Apply browser-specific optimizations to an element
|
|
2583
|
-
* @param {HTMLElement} element - Element to optimize
|
|
2584
|
-
*/
|
|
2585
|
-
enableForDrag(element) {
|
|
2586
|
-
const browserInfo = getBrowserInfo();
|
|
2587
|
-
|
|
2588
|
-
// Base optimizations for all browsers
|
|
2589
|
-
element.style.willChange = 'left, top';
|
|
2590
|
-
element.style.pointerEvents = 'none'; // Prevent conflicts
|
|
2591
|
-
|
|
2592
|
-
// Chrome-specific optimizations
|
|
2593
|
-
if (browserInfo.isChrome) {
|
|
2594
|
-
element.style.transform = 'translateZ(0)'; // Force hardware acceleration
|
|
2595
|
-
}
|
|
2596
|
-
|
|
2597
|
-
// Firefox-specific optimizations
|
|
2598
|
-
if (browserInfo.isFirefox) {
|
|
2599
|
-
element.style.backfaceVisibility = 'hidden';
|
|
2600
|
-
}
|
|
2601
|
-
},
|
|
2602
|
-
|
|
2603
|
-
/**
|
|
2604
|
-
* Clean up optimizations after drag
|
|
2605
|
-
* @param {HTMLElement} element - Element to clean up
|
|
2606
|
-
*/
|
|
2607
|
-
cleanupAfterDrag(element) {
|
|
2608
|
-
element.style.willChange = 'auto';
|
|
2609
|
-
element.style.pointerEvents = '';
|
|
2610
|
-
element.style.transform = '';
|
|
2611
|
-
element.style.backfaceVisibility = '';
|
|
2612
|
-
}
|
|
2613
|
-
};
|
|
2614
|
-
|
|
2615
|
-
let Chessboard$1 = class Chessboard {
|
|
2616
|
-
|
|
2617
|
-
standard_positions = {
|
|
2618
|
-
'start': 'start',
|
|
2619
|
-
'default': 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1',
|
|
2620
|
-
}
|
|
2621
|
-
|
|
2622
|
-
error_messages = {
|
|
2623
|
-
'invalid_position': 'Invalid position - ',
|
|
2624
|
-
'invalid_id_div': 'Board id not found - ',
|
|
2625
|
-
'invalid_value': 'Invalid value - ',
|
|
2626
|
-
'invalid_piece': 'Invalid piece - ',
|
|
2627
|
-
'invalid_square': 'Invalid square - ',
|
|
2628
|
-
'invalid_fen': 'Invalid fen - ',
|
|
2629
|
-
'invalid_orientation': 'Invalid orientation - ',
|
|
2630
|
-
'invalid_color': 'Invalid color - ',
|
|
2631
|
-
'invalid_mode': 'Invalid mode - ',
|
|
2632
|
-
'invalid_dropOffBoard': 'Invalid dropOffBoard - ',
|
|
2633
|
-
'invalid_snapbackTime': 'Invalid snapbackTime - ',
|
|
2634
|
-
'invalid_snapbackAnimation': 'Invalid snapbackAnimation - ',
|
|
2635
|
-
'invalid_fadeTime': 'Invalid fadeTime - ',
|
|
2636
|
-
'invalid_fadeAnimation': 'Invalid fadeAnimation - ',
|
|
2637
|
-
'invalid_ratio': 'Invalid ratio - ',
|
|
2638
|
-
'invalid_piecesPath': 'Invalid piecesPath - ',
|
|
2639
|
-
'invalid_onMove': 'Invalid onMove - ',
|
|
2640
|
-
'invalid_onMoveEnd': 'Invalid onMoveEnd - ',
|
|
2641
|
-
'invalid_onChange': 'Invalid onChange - ',
|
|
2642
|
-
'invalid_onDragStart': 'Invalid onDragStart - ',
|
|
2643
|
-
'invalid_onDragMove': 'Invalid onDragMove - ',
|
|
2644
|
-
'invalid_onDrop': 'Invalid onDrop - ',
|
|
2645
|
-
'invalid_onSnapbackEnd': 'Invalid onSnapbackEnd - ',
|
|
2646
|
-
'invalid_whiteSquare': 'Invalid whiteSquare - ',
|
|
2647
|
-
'invalid_blackSquare': 'Invalid blackSquare - ',
|
|
2648
|
-
'invalid_highlight': 'Invalid highlight - ',
|
|
2649
|
-
'invalid_selectedSquareWhite': 'Invalid selectedSquareWhite - ',
|
|
2650
|
-
'invalid_selectedSquareBlack': 'Invalid selectedSquareBlack - ',
|
|
2651
|
-
'invalid_movedSquareWhite': 'Invalid movedSquareWhite - ',
|
|
2652
|
-
'invalid_movedSquareBlack': 'Invalid movedSquareBlack - ',
|
|
2653
|
-
'invalid_choiceSquare': 'Invalid choiceSquare - ',
|
|
2654
|
-
'invalid_coverSquare': 'Invalid coverSquare - ',
|
|
2655
|
-
'invalid_hintColor': 'Invalid hintColor - ',
|
|
2656
|
-
}
|
|
2657
|
-
|
|
2658
|
-
// -------------------
|
|
2659
|
-
// Initialization
|
|
2660
|
-
// -------------------
|
|
2661
|
-
constructor(config) {
|
|
2662
|
-
// Debug: log the config to see what we're receiving
|
|
2663
|
-
console.log('Chessboard constructor received config:', config);
|
|
2664
|
-
this.config = new ChessboardConfig(config);
|
|
2665
|
-
console.log('Processed config.id_div:', this.config.id_div);
|
|
2666
|
-
this.init();
|
|
2667
|
-
}
|
|
2668
|
-
|
|
2669
|
-
init() {
|
|
2670
|
-
this.initParams();
|
|
2671
|
-
this.setGame(this.config.position);
|
|
2672
|
-
this.buildBoard();
|
|
2673
|
-
this.buildSquares();
|
|
2674
|
-
this.addListeners();
|
|
2675
|
-
this.updateBoardPieces();
|
|
2676
|
-
}
|
|
2677
|
-
|
|
2678
|
-
initParams() {
|
|
2679
|
-
this.element = null;
|
|
2680
|
-
this.squares = {};
|
|
2681
|
-
this.promoting = false;
|
|
2682
|
-
this.clicked = null;
|
|
2683
|
-
this.mosseIndietro = [];
|
|
2684
|
-
this.clicked = null;
|
|
2685
|
-
this._updateTimeout = null; // For debouncing board updates
|
|
2686
|
-
this._movesCache = new Map(); // Cache per le mosse per migliorare le prestazioni
|
|
2687
|
-
this._cacheTimeout = null; // Timeout per pulire la cache
|
|
2688
|
-
this._isAnimating = false; // Flag to track if animations are in progress
|
|
2689
|
-
}
|
|
2690
|
-
|
|
2691
|
-
// -------------------
|
|
2692
|
-
// Board Setup
|
|
2693
|
-
// -------------------
|
|
2694
|
-
buildBoard() {
|
|
2695
|
-
console.log('buildBoard: Looking for element with ID:', this.config.id_div, 'Type:', typeof this.config.id_div);
|
|
2696
|
-
this.element = document.getElementById(this.config.id_div);
|
|
2697
|
-
if (!this.element) {
|
|
2698
|
-
throw new Error(this.error_messages['invalid_id_div'] + this.config.id_div);
|
|
2699
|
-
}
|
|
2700
|
-
this.resize(this.config.size);
|
|
2701
|
-
this.element.className = "board";
|
|
2702
|
-
}
|
|
2703
|
-
|
|
2704
|
-
buildSquares() {
|
|
2705
|
-
|
|
2706
|
-
for (let row = 0; row < 8; row++) {
|
|
2707
|
-
for (let col = 0; col < 8; col++) {
|
|
2708
|
-
|
|
2709
|
-
let [square_row, square_col] = this.realCoord(row, col);
|
|
2710
|
-
let square = new Square(square_row, square_col);
|
|
2711
|
-
this.squares[square.getId()] = square;
|
|
2712
|
-
|
|
2713
|
-
this.element.appendChild(square.element);
|
|
2714
|
-
}
|
|
2715
|
-
}
|
|
2716
|
-
}
|
|
2717
|
-
|
|
2718
|
-
removeBoard() {
|
|
2719
|
-
|
|
2720
|
-
this.element.innerHTML = '';
|
|
2721
|
-
}
|
|
2722
|
-
|
|
2723
|
-
removeSquares() {
|
|
2724
|
-
for (const square of Object.values(this.squares)) {
|
|
2725
|
-
this.element.removeChild(square.element);
|
|
2726
|
-
square.destroy();
|
|
2727
|
-
|
|
2728
|
-
}
|
|
2729
|
-
this.squares = {};
|
|
2730
|
-
}
|
|
2731
|
-
|
|
2732
|
-
resize(value) {
|
|
2733
|
-
if (value === 'auto') {
|
|
2734
|
-
let size;
|
|
2735
|
-
if (this.element.offsetWidth === 0) {
|
|
2736
|
-
size = this.element.offsetHeight;
|
|
2737
|
-
} else if (this.element.offsetHeight === 0) {
|
|
2738
|
-
size = this.element.offsetWidth;
|
|
2739
|
-
} else {
|
|
2740
|
-
size = Math.min(this.element.offsetWidth, this.element.offsetHeight);
|
|
2741
|
-
}
|
|
2742
|
-
this.resize(size);
|
|
2743
|
-
} else if (typeof value !== 'number') {
|
|
2744
|
-
throw new Error(this.error_messages['invalid_value'] + value);
|
|
2745
|
-
} else {
|
|
2746
|
-
document.documentElement.style.setProperty('--dimBoard', value + 'px');
|
|
2747
|
-
this.updateBoardPieces();
|
|
2748
|
-
}
|
|
2749
|
-
}
|
|
2750
|
-
|
|
2751
|
-
// -------------------
|
|
2752
|
-
// Game/Position Functions
|
|
2753
|
-
// -------------------
|
|
2754
|
-
convertFen(position) {
|
|
2755
|
-
if (typeof position === 'string') {
|
|
2756
|
-
if (position == 'start') return 'rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1';
|
|
2757
|
-
if (this.validateFen(position)) return position;
|
|
2758
|
-
else if (this.standard_positions[position]) return this.standard_positions[position];
|
|
2759
|
-
else throw new Error('Invalid position -' + position);
|
|
2760
|
-
} else if (typeof position === 'object') {
|
|
2761
|
-
let parts = [];
|
|
2762
|
-
for (let row = 0; row < 8; row++) {
|
|
2763
|
-
let rowParts = [];
|
|
2764
|
-
let empty = 0;
|
|
2765
|
-
for (let col = 0; col < 8; col++) {
|
|
2766
|
-
let square = this.getSquareID(row, col);
|
|
2767
|
-
let piece = position[square];
|
|
2768
|
-
if (piece) {
|
|
2769
|
-
if (empty > 0) {
|
|
2770
|
-
rowParts.push(empty);
|
|
2771
|
-
empty = 0;
|
|
2772
|
-
}
|
|
2773
|
-
// Convert piece notation: white pieces become uppercase, black remain lowercase.
|
|
2774
|
-
let fenPiece = piece[1] === 'w' ? piece[0].toUpperCase() : piece[0].toLowerCase();
|
|
2775
|
-
rowParts.push(fenPiece);
|
|
2776
|
-
} else {
|
|
2777
|
-
empty++;
|
|
2778
|
-
}
|
|
2779
|
-
}
|
|
2780
|
-
if (empty > 0) rowParts.push(empty);
|
|
2781
|
-
parts.push(rowParts.join(''));
|
|
2782
|
-
}
|
|
2783
|
-
return parts.join('/') + ' w KQkq - 0 1'; } else {
|
|
2784
|
-
throw new Error('Invalid position -' + position);
|
|
2785
|
-
}
|
|
2786
|
-
}
|
|
2787
|
-
|
|
2788
|
-
setGame(position, options = undefined) {
|
|
2789
|
-
const fen = this.convertFen(position);
|
|
2790
|
-
if (this.game) this.game.load(fen, options);
|
|
2791
|
-
else this.game = new Chess(fen);
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
// -------------------
|
|
2795
|
-
// Piece Functions
|
|
2796
|
-
// -------------------
|
|
2797
|
-
getPiecePath(piece) {
|
|
2798
|
-
if (typeof this.config.piecesPath === 'string')
|
|
2799
|
-
return this.config.piecesPath + '/' + piece + '.svg';
|
|
2800
|
-
else if (typeof this.config.piecesPath === 'object')
|
|
2801
|
-
return this.config.piecesPath[piece];
|
|
2802
|
-
else if (typeof this.config.piecesPath === 'function')
|
|
2803
|
-
return this.config.piecesPath(piece);
|
|
2804
|
-
else
|
|
2805
|
-
throw new Error(this.error_messages['invalid_piecesPath']);
|
|
2806
|
-
}
|
|
2807
|
-
|
|
2808
|
-
convertPiece(piece) {
|
|
2809
|
-
if (piece instanceof Piece) return piece;
|
|
2810
|
-
if (typeof piece === 'string') {
|
|
2811
|
-
let [type, color] = piece.split('');
|
|
2812
|
-
return new Piece(color, type, this.getPiecePath(piece));
|
|
2813
|
-
}
|
|
2814
|
-
throw new Error(this.error_messages['invalid_piece'] + piece);
|
|
2815
|
-
}
|
|
2816
|
-
|
|
2817
|
-
addPieceOnSquare(square, piece, fade = true) {
|
|
2818
|
-
|
|
2819
|
-
square.putPiece(piece);
|
|
2820
|
-
piece.setDrag(this.dragFunction(square, piece));
|
|
2821
|
-
|
|
2822
|
-
if (fade) piece.fadeIn(
|
|
2823
|
-
this.config.fadeTime,
|
|
2824
|
-
this.config.fadeAnimation,
|
|
2825
|
-
this.transitionTimingFunction
|
|
2826
|
-
);
|
|
2827
|
-
|
|
2828
|
-
piece.visible();
|
|
2829
|
-
}
|
|
2830
|
-
|
|
2831
|
-
removePieceFromSquare(square, fade = true) {
|
|
2832
|
-
|
|
2833
|
-
square = this.convertSquare(square);
|
|
2834
|
-
square.check();
|
|
2835
|
-
|
|
2836
|
-
let piece = square.piece;
|
|
2837
|
-
|
|
2838
|
-
if (!piece) throw Error('Square has no piece to remove.')
|
|
2839
|
-
|
|
2840
|
-
if (fade) piece.fadeOut(
|
|
2841
|
-
this.config.fadeTime,
|
|
2842
|
-
this.config.fadeAnimation,
|
|
2843
|
-
this.transitionTimingFunction);
|
|
2844
|
-
|
|
2845
|
-
square.removePiece();
|
|
2846
|
-
|
|
2847
|
-
return piece;
|
|
2848
|
-
}
|
|
2849
|
-
|
|
2850
|
-
movePiece(piece, to, duration, callback) {
|
|
2851
|
-
if (!piece) {
|
|
2852
|
-
console.warn('movePiece: piece is null, skipping animation');
|
|
2853
|
-
if (callback) callback();
|
|
2854
|
-
return;
|
|
2855
|
-
}
|
|
2856
|
-
|
|
2857
|
-
piece.translate(to, duration, this.transitionTimingFunction, this.config.moveAnimation, callback);
|
|
2858
|
-
}
|
|
2859
|
-
|
|
2860
|
-
translatePiece(move, removeTo, animate, callback = null) {
|
|
2861
|
-
if (!move.piece) {
|
|
2862
|
-
console.warn('translatePiece: move.piece is null, skipping translation');
|
|
2863
|
-
if (callback) callback();
|
|
2864
|
-
return;
|
|
2865
|
-
}
|
|
2866
|
-
|
|
2867
|
-
if (removeTo) {
|
|
2868
|
-
// Deselect the captured piece before removing it
|
|
2869
|
-
move.to.deselect();
|
|
2870
|
-
this.removePieceFromSquare(move.to, false);
|
|
2871
|
-
}
|
|
2872
|
-
|
|
2873
|
-
let change_square = () => {
|
|
2874
|
-
// Check if piece still exists and is on the source square
|
|
2875
|
-
if (move.from.piece === move.piece) {
|
|
2876
|
-
move.from.removePiece();
|
|
2877
|
-
}
|
|
2878
|
-
// Only put piece if destination square doesn't already have it
|
|
2879
|
-
if (move.to.piece !== move.piece) {
|
|
2880
|
-
move.to.putPiece(move.piece);
|
|
2881
|
-
move.piece.setDrag(this.dragFunction(move.to, move.piece));
|
|
2882
|
-
}
|
|
2883
|
-
if (callback) callback();
|
|
2884
|
-
};
|
|
2885
|
-
|
|
2886
|
-
let duration = animate ? this.config.moveTime : 0;
|
|
2887
|
-
|
|
2888
|
-
this.movePiece(move.piece, move.to, duration, change_square);
|
|
2889
|
-
|
|
2890
|
-
}
|
|
2891
|
-
|
|
2892
|
-
snapbackPiece(square, animate = this.config.snapbackAnimation) {
|
|
2893
|
-
if (!square || !square.piece) {
|
|
2894
|
-
return;
|
|
2895
|
-
}
|
|
2896
|
-
|
|
2897
|
-
const piece = square.piece;
|
|
2898
|
-
|
|
2899
|
-
// Use the piece's translate method to properly animate back to the square
|
|
2900
|
-
const duration = animate ? this.config.snapbackTime : 0;
|
|
2901
|
-
|
|
2902
|
-
// The translate method will calculate the proper distance from current visual position
|
|
2903
|
-
// back to the square's position
|
|
2904
|
-
piece.translate(square, duration, this.transitionTimingFunction, animate);
|
|
2905
|
-
}
|
|
2906
|
-
|
|
2907
|
-
// -------------------
|
|
2908
|
-
// Board Update Functions
|
|
2909
|
-
// -------------------
|
|
2910
|
-
updateBoardPieces(animation = false) {
|
|
2911
|
-
// Clear any pending update to avoid duplicate calls
|
|
2912
|
-
if (this._updateTimeout) {
|
|
2913
|
-
clearTimeout(this._updateTimeout);
|
|
2914
|
-
this._updateTimeout = null;
|
|
2915
|
-
}
|
|
2916
|
-
|
|
2917
|
-
// Pulisce la cache delle mosse quando la posizione cambia
|
|
2918
|
-
this._movesCache.clear();
|
|
2919
|
-
if (this._cacheTimeout) {
|
|
2920
|
-
clearTimeout(this._cacheTimeout);
|
|
2921
|
-
this._cacheTimeout = null;
|
|
2922
|
-
}
|
|
2923
|
-
|
|
2924
|
-
// For click-to-move, add a small delay to avoid lag
|
|
2925
|
-
if (animation && this.clicked === null) {
|
|
2926
|
-
this._updateTimeout = setTimeout(() => {
|
|
2927
|
-
this._doUpdateBoardPieces(animation);
|
|
2928
|
-
this._updateTimeout = null;
|
|
2929
|
-
}, 10);
|
|
2930
|
-
} else {
|
|
2931
|
-
this._doUpdateBoardPieces(animation);
|
|
2932
|
-
}
|
|
2933
|
-
}
|
|
2934
|
-
|
|
2935
|
-
_doUpdateBoardPieces(animation = false) {
|
|
2936
|
-
let { updatedFlags, escapeFlags, movableFlags, pendingTranslations } = this.prepareBoardUpdateData();
|
|
2937
|
-
|
|
2938
|
-
let change = Object.values(updatedFlags).some(flag => !flag);
|
|
2939
|
-
|
|
2940
|
-
this.identifyPieceTranslations(updatedFlags, escapeFlags, movableFlags, pendingTranslations);
|
|
2941
|
-
|
|
2942
|
-
this.executePieceTranslations(pendingTranslations, escapeFlags, animation);
|
|
2943
|
-
|
|
2944
|
-
this.processRemainingPieceUpdates(updatedFlags, animation);
|
|
2945
|
-
|
|
2946
|
-
if (change) this.config.onChange(this.fen());
|
|
2947
|
-
}
|
|
2948
|
-
|
|
2949
|
-
prepareBoardUpdateData() {
|
|
2950
|
-
let updatedFlags = {};
|
|
2951
|
-
let escapeFlags = {};
|
|
2952
|
-
let movableFlags = {};
|
|
2953
|
-
let pendingTranslations = [];
|
|
2954
|
-
|
|
2955
|
-
for (let squareId in this.squares) {
|
|
2956
|
-
let cellPiece = this.squares[squareId].piece;
|
|
2957
|
-
let cellPieceId = cellPiece ? cellPiece.getId() : null;
|
|
2958
|
-
updatedFlags[squareId] = this.getGamePieceId(squareId) === cellPieceId;
|
|
2959
|
-
escapeFlags[squareId] = false;
|
|
2960
|
-
movableFlags[squareId] = cellPiece ? this.getGamePieceId(squareId) !== cellPieceId : false;
|
|
2961
|
-
}
|
|
2962
|
-
|
|
2963
|
-
return { updatedFlags, escapeFlags, movableFlags, pendingTranslations };
|
|
2964
|
-
}
|
|
2965
|
-
|
|
2966
|
-
identifyPieceTranslations(updatedFlags, escapeFlags, movableFlags, pendingTranslations) {
|
|
2967
|
-
Object.values(this.squares).forEach(targetSquare => {
|
|
2968
|
-
const newPieceId = this.getGamePieceId(targetSquare.id);
|
|
2969
|
-
const newPiece = newPieceId && this.convertPiece(newPieceId);
|
|
2970
|
-
const currentPiece = targetSquare.piece;
|
|
2971
|
-
const currentPieceId = currentPiece ? currentPiece.getId() : null;
|
|
2972
|
-
|
|
2973
|
-
if (currentPieceId === newPieceId || updatedFlags[targetSquare.id]) return;
|
|
2974
|
-
|
|
2975
|
-
this.evaluateTranslationCandidates(
|
|
2976
|
-
targetSquare,
|
|
2977
|
-
newPiece,
|
|
2978
|
-
currentPiece,
|
|
2979
|
-
updatedFlags,
|
|
2980
|
-
escapeFlags,
|
|
2981
|
-
movableFlags,
|
|
2982
|
-
pendingTranslations
|
|
2983
|
-
);
|
|
2984
|
-
});
|
|
2985
|
-
}
|
|
2986
|
-
|
|
2987
|
-
evaluateTranslationCandidates(targetSquare, newPiece, oldPiece, updatedFlags, escapeFlags, movableFlags, pendingTranslations) {
|
|
2988
|
-
if (!newPiece) return;
|
|
2989
|
-
const newPieceId = newPiece.getId();
|
|
2990
|
-
|
|
2991
|
-
for (const sourceSquare of Object.values(this.squares)) {
|
|
2992
|
-
if (sourceSquare.id === targetSquare.id || updatedFlags[targetSquare.id]) continue;
|
|
2993
|
-
|
|
2994
|
-
const sourcePiece = sourceSquare.piece;
|
|
2995
|
-
if (!sourcePiece || !movableFlags[sourceSquare.id] || this.isPiece(newPieceId, sourceSquare.id)) continue;
|
|
2996
|
-
|
|
2997
|
-
if (sourcePiece.id === newPieceId) {
|
|
2998
|
-
this.handleTranslationMovement(targetSquare, sourceSquare, oldPiece, sourcePiece, updatedFlags, escapeFlags, movableFlags, pendingTranslations);
|
|
2999
|
-
break;
|
|
3000
|
-
}
|
|
3001
|
-
}
|
|
3002
|
-
}
|
|
3003
|
-
|
|
3004
|
-
handleTranslationMovement(targetSquare, sourceSquare, oldPiece, currentSource, updatedFlags, escapeFlags, movableFlags, pendingTranslations) {
|
|
3005
|
-
// Verifica il caso specifico "en passant"
|
|
3006
|
-
let lastMove = this.lastMove();
|
|
3007
|
-
if (!oldPiece && lastMove && lastMove['captured'] === 'p') {
|
|
3008
|
-
this.removePieceFromSquare(this.squares[targetSquare.id[0] + sourceSquare.id[1]]);
|
|
3009
|
-
}
|
|
3010
|
-
|
|
3011
|
-
pendingTranslations.push([currentSource, sourceSquare, targetSquare]);
|
|
3012
|
-
|
|
3013
|
-
if (!this.getGamePieceId(sourceSquare.id)) updatedFlags[sourceSquare.id] = true;
|
|
3014
|
-
|
|
3015
|
-
escapeFlags[sourceSquare.id] = true;
|
|
3016
|
-
movableFlags[sourceSquare.id] = false;
|
|
3017
|
-
updatedFlags[targetSquare.id] = true;
|
|
3018
|
-
}
|
|
3019
|
-
|
|
3020
|
-
executePieceTranslations(pendingTranslations, escapeFlags, animation) {
|
|
3021
|
-
for (let [_, sourceSquare, targetSquare] of pendingTranslations) {
|
|
3022
|
-
let removeTarget = !escapeFlags[targetSquare.id] && targetSquare.piece;
|
|
3023
|
-
let moveObj = new Move(sourceSquare, targetSquare);
|
|
3024
|
-
this.translatePiece(moveObj, removeTarget, animation);
|
|
3025
|
-
}
|
|
3026
|
-
}
|
|
3027
|
-
|
|
3028
|
-
// Gestisce gli aggiornamenti residui per ogni cella che non è ancora stata correttamente aggiornata
|
|
3029
|
-
processRemainingPieceUpdates(updatedFlags, animation) {
|
|
3030
|
-
for (const square of Object.values(this.squares)) {
|
|
3031
|
-
let newPieceId = this.getGamePieceId(square.id);
|
|
3032
|
-
let newPiece = newPieceId ? this.convertPiece(newPieceId) : null;
|
|
3033
|
-
let currentPiece = square.piece;
|
|
3034
|
-
let currentPieceId = currentPiece ? currentPiece.getId() : null;
|
|
3035
|
-
|
|
3036
|
-
if (currentPieceId !== newPieceId && !updatedFlags[square.id]) {
|
|
3037
|
-
this.updateSinglePiece(square, newPiece, updatedFlags, animation);
|
|
3038
|
-
}
|
|
3039
|
-
}
|
|
3040
|
-
}
|
|
3041
|
-
|
|
3042
|
-
// Aggiorna il pezzo in una cella specifica. Gestisce anche il caso di promozione
|
|
3043
|
-
updateSinglePiece(square, newPiece, updatedFlags, animation) {
|
|
3044
|
-
if (!updatedFlags[square.id]) {
|
|
3045
|
-
let lastMove = this.lastMove();
|
|
3046
|
-
|
|
3047
|
-
if (lastMove?.promotion) {
|
|
3048
|
-
if (lastMove['to'] === square.id) {
|
|
3049
|
-
|
|
3050
|
-
let move = new Move(this.squares[lastMove['from']], square);
|
|
3051
|
-
this.translatePiece(move, true, animation
|
|
3052
|
-
, () => {
|
|
3053
|
-
move.to.removePiece();
|
|
3054
|
-
this.addPieceOnSquare(square, newPiece);
|
|
3055
|
-
});
|
|
3056
|
-
}
|
|
3057
|
-
} else {
|
|
3058
|
-
if (square.piece) this.removePieceFromSquare(square);
|
|
3059
|
-
if (newPiece) this.addPieceOnSquare(square, newPiece);
|
|
3060
|
-
}
|
|
3061
|
-
}
|
|
3062
|
-
}
|
|
3063
|
-
|
|
3064
|
-
// -------------------
|
|
3065
|
-
// Event Handlers and Drag
|
|
3066
|
-
// -------------------
|
|
3067
|
-
dragFunction(square, piece) {
|
|
3068
|
-
|
|
3069
|
-
return (event) => {
|
|
3070
|
-
|
|
3071
|
-
event.preventDefault();
|
|
3072
|
-
|
|
3073
|
-
if (!this.config.draggable || !piece) return;
|
|
3074
|
-
|
|
3075
|
-
// Store the original from square for the entire drag operation
|
|
3076
|
-
const originalFrom = square;
|
|
3077
|
-
let prec;
|
|
3078
|
-
let from = originalFrom;
|
|
3079
|
-
let to = square;
|
|
3080
|
-
|
|
3081
|
-
const img = piece.element;
|
|
3082
|
-
|
|
3083
|
-
if (!this.canMove(from)) return;
|
|
3084
|
-
|
|
3085
|
-
// Track if this is actually a drag operation or just a click
|
|
3086
|
-
let isDragging = false;
|
|
3087
|
-
let startX = event.clientX || (event.touches && event.touches[0] ? event.touches[0].clientX : 0);
|
|
3088
|
-
let startY = event.clientY || (event.touches && event.touches[0] ? event.touches[0].clientY : 0);
|
|
3089
|
-
|
|
3090
|
-
// Don't interfere with click system immediately
|
|
3091
|
-
console.log('dragFunction: mousedown detected, waiting to see if it becomes drag');
|
|
3092
|
-
|
|
3093
|
-
const moveAt = (event) => {
|
|
3094
|
-
const squareSize = this.element.offsetWidth / 8;
|
|
3095
|
-
|
|
3096
|
-
// Get mouse coordinates - use clientX/Y for better Chrome compatibility
|
|
3097
|
-
let clientX, clientY;
|
|
3098
|
-
if (event.touches && event.touches[0]) {
|
|
3099
|
-
clientX = event.touches[0].clientX;
|
|
3100
|
-
clientY = event.touches[0].clientY;
|
|
3101
|
-
} else {
|
|
3102
|
-
clientX = event.clientX;
|
|
3103
|
-
clientY = event.clientY;
|
|
3104
|
-
}
|
|
3105
|
-
|
|
3106
|
-
// Get board position using getBoundingClientRect for accuracy
|
|
3107
|
-
const boardRect = this.element.getBoundingClientRect();
|
|
3108
|
-
|
|
3109
|
-
// Calculate position relative to board with piece centered on cursor
|
|
3110
|
-
// Add window scroll offset for correct positioning
|
|
3111
|
-
window.pageXOffset || document.documentElement.scrollLeft;
|
|
3112
|
-
window.pageYOffset || document.documentElement.scrollTop;
|
|
3113
|
-
|
|
3114
|
-
const x = clientX - boardRect.left - (squareSize / 2);
|
|
3115
|
-
const y = clientY - boardRect.top - (squareSize / 2);
|
|
3116
|
-
|
|
3117
|
-
img.style.left = x + 'px';
|
|
3118
|
-
img.style.top = y + 'px';
|
|
3119
|
-
return true;
|
|
3120
|
-
};
|
|
3121
|
-
|
|
3122
|
-
const onMouseMove = (event) => {
|
|
3123
|
-
// Check if mouse has moved enough to be considered a drag
|
|
3124
|
-
let currentX = event.clientX;
|
|
3125
|
-
let currentY = event.clientY;
|
|
3126
|
-
let deltaX = Math.abs(currentX - startX);
|
|
3127
|
-
let deltaY = Math.abs(currentY - startY);
|
|
3128
|
-
|
|
3129
|
-
// Only start dragging if mouse moved more than 3 pixels
|
|
3130
|
-
if (!isDragging && (deltaX > 3 || deltaY > 3)) {
|
|
3131
|
-
console.log('dragFunction: starting actual drag operation');
|
|
3132
|
-
isDragging = true;
|
|
3133
|
-
|
|
3134
|
-
// Now set up drag state
|
|
3135
|
-
if (!this.config.clickable) {
|
|
3136
|
-
this.clicked = null;
|
|
3137
|
-
this.clicked = from;
|
|
3138
|
-
} else if (!this.clicked) {
|
|
3139
|
-
this.clicked = from;
|
|
3140
|
-
}
|
|
3141
|
-
console.log('dragFunction: clicked state after drag activation =', this.clicked ? this.clicked.id : 'none');
|
|
3142
|
-
|
|
3143
|
-
// Highlight the source square and show hints
|
|
3144
|
-
if (this.config.clickable) {
|
|
3145
|
-
from.select();
|
|
3146
|
-
this.hintMoves(from);
|
|
3147
|
-
}
|
|
3148
|
-
|
|
3149
|
-
img.style.position = 'absolute';
|
|
3150
|
-
img.style.zIndex = 100;
|
|
3151
|
-
img.classList.add('dragging');
|
|
3152
|
-
|
|
3153
|
-
DragOptimizations.enableForDrag(img);
|
|
3154
|
-
}
|
|
3155
|
-
|
|
3156
|
-
if (!isDragging) return;
|
|
3157
|
-
|
|
3158
|
-
if (!this.config.onDragStart(square, piece)) return;
|
|
3159
|
-
if (!moveAt(event)) ;
|
|
3160
|
-
|
|
3161
|
-
const boardRect = this.element.getBoundingClientRect();
|
|
3162
|
-
const { offsetWidth: boardWidth, offsetHeight: boardHeight } = this.element;
|
|
3163
|
-
const x = event.clientX - boardRect.left;
|
|
3164
|
-
const y = event.clientY - boardRect.top;
|
|
3165
|
-
|
|
3166
|
-
let newTo = null;
|
|
3167
|
-
if (x >= 0 && x <= boardWidth && y >= 0 && y <= boardHeight) {
|
|
3168
|
-
const col = Math.floor(x / (boardWidth / 8));
|
|
3169
|
-
const row = Math.floor(y / (boardHeight / 8));
|
|
3170
|
-
newTo = this.squares[this.getSquareID(row, col)];
|
|
3171
|
-
}
|
|
3172
|
-
|
|
3173
|
-
to = newTo;
|
|
3174
|
-
this.config.onDragMove(from, to, piece);
|
|
3175
|
-
|
|
3176
|
-
if (to !== prec) {
|
|
3177
|
-
to?.highlight();
|
|
3178
|
-
prec?.dehighlight();
|
|
3179
|
-
prec = to;
|
|
3180
|
-
}
|
|
3181
|
-
};
|
|
3182
|
-
|
|
3183
|
-
const onMouseUp = () => {
|
|
3184
|
-
prec?.dehighlight();
|
|
3185
|
-
document.removeEventListener('mousemove', onMouseMove);
|
|
3186
|
-
window.removeEventListener('mouseup', onMouseUp);
|
|
3187
|
-
|
|
3188
|
-
// If this was just a click (not a drag), don't interfere
|
|
3189
|
-
if (!isDragging) {
|
|
3190
|
-
console.log('dragFunction: was just a click, not interfering');
|
|
3191
|
-
return;
|
|
3192
|
-
}
|
|
3193
|
-
|
|
3194
|
-
console.log('dragFunction: ending drag operation');
|
|
3195
|
-
img.style.zIndex = 20;
|
|
3196
|
-
img.classList.remove('dragging');
|
|
3197
|
-
img.style.willChange = 'auto';
|
|
3198
|
-
|
|
3199
|
-
const dropResult = this.config.onDrop(originalFrom, to, piece);
|
|
3200
|
-
const isTrashDrop = !to && (this.config.dropOffBoard === 'trash' || dropResult === 'trash');
|
|
3201
|
-
|
|
3202
|
-
if (isTrashDrop) {
|
|
3203
|
-
this.allSquares("unmoved");
|
|
3204
|
-
this.allSquares('removeHint');
|
|
3205
|
-
originalFrom.deselect();
|
|
3206
|
-
this.remove(originalFrom);
|
|
3207
|
-
} else if (!to) {
|
|
3208
|
-
// No target square - snapback
|
|
3209
|
-
if (originalFrom && originalFrom.piece) {
|
|
3210
|
-
this.snapbackPiece(originalFrom);
|
|
3211
|
-
if (to !== originalFrom) this.config.onSnapbackEnd(originalFrom, piece);
|
|
3212
|
-
}
|
|
3213
|
-
} else {
|
|
3214
|
-
// Set clicked to originalFrom before attempting move
|
|
3215
|
-
this.clicked = originalFrom;
|
|
3216
|
-
// Try to make the move
|
|
3217
|
-
const onClickResult = this.onClick(to, true, true);
|
|
3218
|
-
if (!onClickResult) {
|
|
3219
|
-
// Move failed - snapback
|
|
3220
|
-
if (originalFrom && originalFrom.piece) {
|
|
3221
|
-
this.snapbackPiece(originalFrom);
|
|
3222
|
-
if (to !== originalFrom) this.config.onSnapbackEnd(originalFrom, piece);
|
|
3223
|
-
}
|
|
3224
|
-
}
|
|
3225
|
-
}
|
|
3226
|
-
};
|
|
3227
|
-
|
|
3228
|
-
window.addEventListener('mouseup', onMouseUp, { once: true });
|
|
3229
|
-
document.addEventListener('mousemove', onMouseMove);
|
|
3230
|
-
img.addEventListener('mouseup', onMouseUp, { once: true });
|
|
3231
|
-
}
|
|
3232
|
-
}
|
|
3233
|
-
|
|
3234
|
-
addListeners() {
|
|
3235
|
-
for (const square of Object.values(this.squares)) {
|
|
3236
|
-
|
|
3237
|
-
square.piece;
|
|
3238
|
-
|
|
3239
|
-
// Applica throttling ai listener di mouseover e mouseout per migliori prestazioni
|
|
3240
|
-
const throttledHintMoves = rafThrottle((e) => {
|
|
3241
|
-
if (!this.clicked && this.config.hints) this.hintMoves(square);
|
|
3242
|
-
});
|
|
3243
|
-
|
|
3244
|
-
const throttledDehintMoves = rafThrottle((e) => {
|
|
3245
|
-
if (!this.clicked && this.config.hints) this.dehintMoves(square);
|
|
3246
|
-
});
|
|
3247
|
-
|
|
3248
|
-
square.element.addEventListener("mouseover", throttledHintMoves);
|
|
3249
|
-
square.element.addEventListener("mouseout", throttledDehintMoves);
|
|
3250
|
-
|
|
3251
|
-
const handleClick = (e) => {
|
|
3252
|
-
e.stopPropagation();
|
|
3253
|
-
if (this.config.clickable) {
|
|
3254
|
-
this.onClick(square);
|
|
3255
|
-
}
|
|
3256
|
-
};
|
|
3257
|
-
|
|
3258
|
-
square.element.addEventListener("click", handleClick);
|
|
3259
|
-
square.element.addEventListener("touch", handleClick);
|
|
3260
|
-
}
|
|
3261
|
-
}
|
|
3262
|
-
|
|
3263
|
-
onClick(square, animation = this.config.moveAnimation, dragged = false) {
|
|
3264
|
-
|
|
3265
|
-
console.log('onClick START: square =', square.id, 'clicked =', this.clicked ? this.clicked.id : 'none');
|
|
3266
|
-
|
|
3267
|
-
let from = this.clicked;
|
|
3268
|
-
|
|
3269
|
-
let promotion = null;
|
|
3270
|
-
|
|
3271
|
-
if (this.promoting) {
|
|
3272
|
-
if (this.promoting === 'none') from = null;
|
|
3273
|
-
else promotion = this.promoting;
|
|
3274
|
-
|
|
3275
|
-
this.promoting = false;
|
|
3276
|
-
this.allSquares("removePromotion");
|
|
3277
|
-
this.allSquares("removeCover");
|
|
3278
|
-
}
|
|
3279
|
-
|
|
3280
|
-
console.log('onClick: from =', from ? from.id : 'none');
|
|
3281
|
-
|
|
3282
|
-
if (!from) {
|
|
3283
|
-
console.log('onClick: no from, trying to select piece');
|
|
3284
|
-
if (this.canMove(square)) {
|
|
3285
|
-
console.log('onClick: canMove = true, selecting');
|
|
3286
|
-
if (this.config.clickable) {
|
|
3287
|
-
square.select();
|
|
3288
|
-
this.hintMoves(square);
|
|
3289
|
-
}
|
|
3290
|
-
this.clicked = square;
|
|
3291
|
-
console.log('onClick: *** CLICKED SET TO ***', square.id);
|
|
3292
|
-
console.log('onClick: set clicked to', square.id);
|
|
3293
|
-
} else {
|
|
3294
|
-
console.log('onClick: canMove = false');
|
|
3295
|
-
}
|
|
3296
|
-
return false;
|
|
3297
|
-
}
|
|
3298
|
-
|
|
3299
|
-
// If clicking on the same square that's already selected, deselect it
|
|
3300
|
-
if (this.clicked === square) {
|
|
3301
|
-
console.log('onClick: deselecting same square');
|
|
3302
|
-
square.deselect();
|
|
3303
|
-
this.allSquares("removeHint");
|
|
3304
|
-
this.clicked = null;
|
|
3305
|
-
console.log('onClick: *** CLICKED RESET TO NULL (deselect) ***');
|
|
3306
|
-
return false;
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
console.log('onClick: attempting move from', from.id, 'to', square.id);
|
|
3310
|
-
let move = new Move(from, square, promotion);
|
|
3311
|
-
|
|
3312
|
-
if (!move.check()) {
|
|
3313
|
-
console.log('onClick: move check FAILED');
|
|
3314
|
-
from.deselect();
|
|
3315
|
-
this.allSquares("removeHint");
|
|
3316
|
-
this.clicked = null;
|
|
3317
|
-
return false;
|
|
3318
|
-
}
|
|
3319
|
-
|
|
3320
|
-
if (this.config.onlyLegalMoves && !move.isLegal(this.game)) {
|
|
3321
|
-
console.log('onClick: move is NOT LEGAL');
|
|
3322
|
-
from.deselect();
|
|
3323
|
-
this.allSquares("removeHint");
|
|
3324
|
-
this.clicked = null;
|
|
3325
|
-
return false;
|
|
3326
|
-
}
|
|
3327
|
-
|
|
3328
|
-
if (!move.hasPromotion() && this.promote(move)) {
|
|
3329
|
-
console.log('onClick: promotion required');
|
|
3330
|
-
return false;
|
|
3331
|
-
}
|
|
3332
|
-
|
|
3333
|
-
console.log('onClick: calling onMove');
|
|
3334
|
-
if (this.config.onMove(move)) {
|
|
3335
|
-
console.log('onClick: SUCCESS - move executed');
|
|
3336
|
-
// Clean up UI state
|
|
3337
|
-
from.deselect();
|
|
3338
|
-
this.allSquares("removeHint");
|
|
3339
|
-
this.clicked = null;
|
|
3340
|
-
console.log('onClick: *** CLICKED RESET TO NULL (success) ***');
|
|
3341
|
-
this.move(move, animation);
|
|
3342
|
-
return true;
|
|
3343
|
-
}
|
|
3344
|
-
|
|
3345
|
-
console.log('onClick: onMove returned FALSE');
|
|
3346
|
-
from.deselect();
|
|
3347
|
-
this.allSquares("removeHint");
|
|
3348
|
-
this.clicked = null;
|
|
3349
|
-
return false;
|
|
3350
|
-
}
|
|
3351
|
-
|
|
3352
|
-
// -------------------
|
|
3353
|
-
// Move Functions
|
|
3354
|
-
// -------------------
|
|
3355
|
-
canMove(square) {
|
|
3356
|
-
if (!square.piece) return false;
|
|
3357
|
-
if (this.config.movableColors === 'none') return false;
|
|
3358
|
-
if (this.config.movableColors === 'w' && square.piece.color === 'b') return false;
|
|
3359
|
-
if (this.config.movableColors === 'b' && square.piece.color === 'w') return false;
|
|
3360
|
-
if (!this.config.onlyLegalMoves) return true;
|
|
3361
|
-
return square.piece.color == this.turn();
|
|
3362
|
-
}
|
|
3363
|
-
|
|
3364
|
-
convertMove(move) {
|
|
3365
|
-
if (move instanceof Move) return move;
|
|
3366
|
-
if (typeof move == 'string') {
|
|
3367
|
-
let fromId = move.slice(0, 2);
|
|
3368
|
-
let toId = move.slice(2, 4);
|
|
3369
|
-
let promotion = move.slice(4, 5) ? move.slice(4, 5) : null;
|
|
3370
|
-
return new Move(this.squares[fromId], this.squares[toId], promotion);
|
|
3371
|
-
}
|
|
3372
|
-
throw new Error("Invalid move format");
|
|
3373
|
-
}
|
|
3374
|
-
|
|
3375
|
-
legalMove(move) {
|
|
3376
|
-
let legal_moves = this.legalMoves(move.from.id);
|
|
3377
|
-
|
|
3378
|
-
for (let i in legal_moves) {
|
|
3379
|
-
if (legal_moves[i]['to'] === move.to.id &&
|
|
3380
|
-
move.promotion == legal_moves[i]['promotion'])
|
|
3381
|
-
return true;
|
|
3382
|
-
}
|
|
3383
|
-
|
|
3384
|
-
return false;
|
|
3385
|
-
}
|
|
3386
|
-
|
|
3387
|
-
legalMoves(from = null, verb = true) {
|
|
3388
|
-
if (from) return this.game.moves({ square: from, verbose: verb });
|
|
3389
|
-
return this.game.moves({ verbose: verb });
|
|
3390
|
-
}
|
|
3391
|
-
|
|
3392
|
-
move(move, animation = true) {
|
|
3393
|
-
move = this.convertMove(move);
|
|
3394
|
-
move.check();
|
|
3395
|
-
|
|
3396
|
-
let from = move.from;
|
|
3397
|
-
let to = move.to;
|
|
3398
|
-
|
|
3399
|
-
// Store the current state to avoid unnecessary recalculations
|
|
3400
|
-
const gameStateBefore = this.game.fen();
|
|
3401
|
-
|
|
3402
|
-
if (!this.config.onlyLegalMoves) {
|
|
3403
|
-
let piece = this.getGamePieceId(from.id);
|
|
3404
|
-
this.game.remove(from.id);
|
|
3405
|
-
this.game.remove(to.id);
|
|
3406
|
-
this.game.put({ type: move.hasPromotion() ? move.promotion : piece[0], color: piece[1] }, to.id);
|
|
3407
|
-
} else {
|
|
3408
|
-
this.allSquares("unmoved");
|
|
3409
|
-
|
|
3410
|
-
move = this.game.move({
|
|
3411
|
-
from: from.id,
|
|
3412
|
-
to: to.id,
|
|
3413
|
-
promotion: move.hasPromotion() ? move.promotion : undefined
|
|
3414
|
-
});
|
|
3415
|
-
|
|
3416
|
-
if (move === null) {
|
|
3417
|
-
throw new Error("Invalid move: move could not be executed");
|
|
3418
|
-
}
|
|
3419
|
-
|
|
3420
|
-
from.moved();
|
|
3421
|
-
to.moved();
|
|
3422
|
-
this.allSquares("removeHint");
|
|
3423
|
-
}
|
|
3424
|
-
|
|
3425
|
-
// Only update the board if the game state actually changed
|
|
3426
|
-
const gameStateAfter = this.game.fen();
|
|
3427
|
-
if (gameStateBefore !== gameStateAfter) {
|
|
3428
|
-
this.updateBoardPieces(animation);
|
|
3429
|
-
}
|
|
3430
|
-
|
|
3431
|
-
if (this.config.onlyLegalMoves) {
|
|
3432
|
-
this.config.onMoveEnd(move);
|
|
3433
|
-
}
|
|
3434
|
-
}
|
|
3435
|
-
|
|
3436
|
-
// -------------------
|
|
3437
|
-
// Miscellaneous Functions
|
|
3438
|
-
// -------------------
|
|
3439
|
-
hint(squareId) {
|
|
3440
|
-
let square = this.squares[squareId];
|
|
3441
|
-
if (!this.config.hints || !square) return;
|
|
3442
|
-
square.putHint(square.piece && square.piece.color !== this.turn());
|
|
3443
|
-
}
|
|
3444
|
-
|
|
3445
|
-
hintMoves(square) {
|
|
3446
|
-
if (!this.canMove(square)) return;
|
|
3447
|
-
|
|
3448
|
-
// Usa la cache per evitare calcoli ripetuti delle mosse
|
|
3449
|
-
const cacheKey = `${square.id}-${this.game.fen()}`;
|
|
3450
|
-
let mosse = this._movesCache.get(cacheKey);
|
|
3451
|
-
|
|
3452
|
-
if (!mosse) {
|
|
3453
|
-
mosse = this.game.moves({ square: square.id, verbose: true });
|
|
3454
|
-
this._movesCache.set(cacheKey, mosse);
|
|
3455
|
-
|
|
3456
|
-
// Pulisci la cache dopo un breve ritardo per evitare accumulo di memoria
|
|
3457
|
-
if (this._cacheTimeout) clearTimeout(this._cacheTimeout);
|
|
3458
|
-
this._cacheTimeout = setTimeout(() => {
|
|
3459
|
-
this._movesCache.clear();
|
|
3460
|
-
}, 1000);
|
|
3461
|
-
}
|
|
3462
|
-
|
|
3463
|
-
for (let mossa of mosse) {
|
|
3464
|
-
if (mossa['to'].length === 2) this.hint(mossa['to']);
|
|
3465
|
-
}
|
|
3466
|
-
}
|
|
3467
|
-
|
|
3468
|
-
dehintMoves(square) {
|
|
3469
|
-
// Usa la cache anche per dehint per coerenza
|
|
3470
|
-
const cacheKey = `${square.id}-${this.game.fen()}`;
|
|
3471
|
-
let mosse = this._movesCache.get(cacheKey);
|
|
3472
|
-
|
|
3473
|
-
if (!mosse) {
|
|
3474
|
-
mosse = this.game.moves({ square: square.id, verbose: true });
|
|
3475
|
-
this._movesCache.set(cacheKey, mosse);
|
|
3476
|
-
}
|
|
3477
|
-
|
|
3478
|
-
for (let mossa of mosse) {
|
|
3479
|
-
let to = this.squares[mossa['to']];
|
|
3480
|
-
to.removeHint();
|
|
3481
|
-
}
|
|
3482
|
-
}
|
|
3483
|
-
|
|
3484
|
-
allSquares(method) {
|
|
3485
|
-
for (const square of Object.values(this.squares)) {
|
|
3486
|
-
square[method]();
|
|
3487
|
-
this.squares[square.id] = square;
|
|
3488
|
-
}
|
|
3489
|
-
}
|
|
3490
|
-
|
|
3491
|
-
promote(move) {
|
|
3492
|
-
|
|
3493
|
-
if (!this.config.onlyLegalMoves) return false;
|
|
3494
|
-
|
|
3495
|
-
let to = move.to;
|
|
3496
|
-
let from = move.from;
|
|
3497
|
-
let pezzo = this.game.get(from.id);
|
|
3498
|
-
let choichable = ['q', 'r', 'b', 'n'];
|
|
3499
|
-
|
|
3500
|
-
if (pezzo['type'] !== 'p' || !(to.row === 1 || to.row === 8)) return false;
|
|
3501
|
-
|
|
3502
|
-
for (const square of Object.values(this.squares)) {
|
|
3503
|
-
let distance = Math.abs(to.row - square.row);
|
|
3504
|
-
|
|
3505
|
-
if (to.col === square.col && distance <= 3) {
|
|
3506
|
-
|
|
3507
|
-
let pieceId = choichable[distance] + pezzo['color'];
|
|
3508
|
-
|
|
3509
|
-
square.putPromotion(
|
|
3510
|
-
this.getPiecePath(pieceId),
|
|
3511
|
-
() => {
|
|
3512
|
-
this.promoting = pieceId[0];
|
|
3513
|
-
this.clicked = from;
|
|
3514
|
-
this.onClick(to);
|
|
3515
|
-
}
|
|
3516
|
-
);
|
|
3517
|
-
} else
|
|
3518
|
-
square.putCover(
|
|
3519
|
-
() => {
|
|
3520
|
-
this.promoting = 'none';
|
|
3521
|
-
this.onClick(square);
|
|
3522
|
-
});
|
|
3523
|
-
}
|
|
3524
|
-
|
|
3525
|
-
this.clicked = from.id;
|
|
3526
|
-
|
|
3527
|
-
return true;
|
|
3528
|
-
}
|
|
3529
|
-
|
|
3530
|
-
transitionTimingFunction(elapsed, duration, type = 'ease') {
|
|
3531
|
-
let x = elapsed / duration;
|
|
3532
|
-
switch (type) {
|
|
3533
|
-
case 'linear':
|
|
3534
|
-
return x;
|
|
3535
|
-
case 'ease':
|
|
3536
|
-
return (x ** 2) * (3 - 2 * x);
|
|
3537
|
-
case 'ease-in':
|
|
3538
|
-
return x ** 2;
|
|
3539
|
-
case 'ease-out':
|
|
3540
|
-
return -1 * (x - 1) ** 2 + 1;
|
|
3541
|
-
case 'ease-in-out':
|
|
3542
|
-
return (x < 0.5) ? 2 * x ** 2 : 4 * x - 2 * x ** 2 - 1;
|
|
3543
|
-
}
|
|
3544
|
-
}
|
|
3545
|
-
|
|
3546
|
-
clearSquares() {
|
|
3547
|
-
this.allSquares('removeHint');
|
|
3548
|
-
this.allSquares("deselect");
|
|
3549
|
-
this.allSquares("unmoved");
|
|
3550
|
-
}
|
|
3551
|
-
|
|
3552
|
-
getGamePieceId(squareId) {
|
|
3553
|
-
let piece = this.game.get(squareId);
|
|
3554
|
-
return piece ? piece['type'] + piece['color'] : null;
|
|
3555
|
-
}
|
|
3556
|
-
|
|
3557
|
-
isPiece(piece, square) { return this.getGamePieceId(square) === piece }
|
|
3558
|
-
|
|
3559
|
-
realCoord(row, col) {
|
|
3560
|
-
if (this.isWhiteOriented()) row = 7 - row;
|
|
3561
|
-
else col = 7 - col;
|
|
3562
|
-
return [row + 1, col + 1];
|
|
3563
|
-
}
|
|
3564
|
-
|
|
3565
|
-
getSquareID(row, col) {
|
|
3566
|
-
row = parseInt(row);
|
|
3567
|
-
col = parseInt(col);
|
|
3568
|
-
if (this.isWhiteOriented()) {
|
|
3569
|
-
row = 8 - row;
|
|
3570
|
-
col = col + 1;
|
|
3571
|
-
} else {
|
|
3572
|
-
row = row + 1;
|
|
3573
|
-
col = 8 - col;
|
|
3574
|
-
}
|
|
3575
|
-
let letters = 'abcdefgh';
|
|
3576
|
-
let letter = letters[col - 1];
|
|
3577
|
-
return letter + row;
|
|
3578
|
-
}
|
|
3579
|
-
|
|
3580
|
-
convertSquare(square) {
|
|
3581
|
-
if (square instanceof Square) return square;
|
|
3582
|
-
if (typeof square === 'string' && this.squares[square]) return this.squares[square];
|
|
3583
|
-
throw new Error(this.error_messages['invalid_square'] + square);
|
|
3584
|
-
}
|
|
3585
|
-
|
|
3586
|
-
// -------------------
|
|
3587
|
-
// User API and Chess.js Integration
|
|
3588
|
-
// -------------------
|
|
3589
|
-
getOrientation() {
|
|
3590
|
-
return this.config.orientation;
|
|
3591
|
-
}
|
|
3592
|
-
|
|
3593
|
-
setOrientation(color, animation = true) {
|
|
3594
|
-
if (['w', 'b'].includes(color)) {
|
|
3595
|
-
if (color !== this.config.orientation) {
|
|
3596
|
-
this.flip(animation);
|
|
3597
|
-
}
|
|
3598
|
-
} else {
|
|
3599
|
-
throw new Error(this.error_messages['invalid_orientation'] + color);
|
|
3600
|
-
}
|
|
3601
|
-
}
|
|
3602
|
-
|
|
3603
|
-
highlight(squareId) {
|
|
3604
|
-
let square = this.convertSquare(squareId);
|
|
3605
|
-
square.check();
|
|
3606
|
-
square.highlight();
|
|
3607
|
-
}
|
|
3608
|
-
|
|
3609
|
-
dehighlight(squareId) {
|
|
3610
|
-
let square = this.convertSquare(squareId);
|
|
3611
|
-
square.check();
|
|
3612
|
-
square.dehighlight();
|
|
3613
|
-
}
|
|
3614
|
-
|
|
3615
|
-
lastMove() {
|
|
3616
|
-
const moves = this.history({ verbose: true });
|
|
3617
|
-
return moves[moves.length - 1];
|
|
3618
|
-
}
|
|
3619
|
-
|
|
3620
|
-
flip() {
|
|
3621
|
-
this.config.orientation = this.config.orientation === 'w' ? 'b' : 'w';
|
|
3622
|
-
this.destroy();
|
|
3623
|
-
this.initParams();
|
|
3624
|
-
this.build();
|
|
3625
|
-
}
|
|
3626
|
-
|
|
3627
|
-
build() {
|
|
3628
|
-
if (this.element) this.destroy();
|
|
3629
|
-
this.init();
|
|
3630
|
-
}
|
|
3631
|
-
|
|
3632
|
-
destroy() {
|
|
3633
|
-
this.removeSquares();
|
|
3634
|
-
this.removeBoard();
|
|
3635
|
-
}
|
|
3636
|
-
|
|
3637
|
-
ascii() {
|
|
3638
|
-
return this.game.ascii();
|
|
3639
|
-
}
|
|
3640
|
-
|
|
3641
|
-
board() {
|
|
3642
|
-
let dict = {};
|
|
3643
|
-
for (let squareId in this.squares) {
|
|
3644
|
-
let piece = this.getGamePieceId(squareId);
|
|
3645
|
-
if (piece) dict[squareId] = piece;
|
|
3646
|
-
}
|
|
3647
|
-
return dict;
|
|
3648
|
-
}
|
|
3649
|
-
|
|
3650
|
-
clear(options = {}, animation = true) {
|
|
3651
|
-
this.game.clear(options);
|
|
3652
|
-
this.updateBoardPieces(animation);
|
|
3653
|
-
}
|
|
3654
|
-
|
|
3655
|
-
fen() {
|
|
3656
|
-
return this.game.fen();
|
|
3657
|
-
}
|
|
3658
|
-
|
|
3659
|
-
get(squareId) {
|
|
3660
|
-
const square = this.convertSquare(squareId);
|
|
3661
|
-
square.check();
|
|
3662
|
-
return square.piece;
|
|
3663
|
-
}
|
|
3664
|
-
|
|
3665
|
-
getCastlingRights(color) {
|
|
3666
|
-
return this.game.getCastlingRights(color);
|
|
3667
|
-
}
|
|
3668
|
-
|
|
3669
|
-
getComment() {
|
|
3670
|
-
return this.game.getComment();
|
|
3671
|
-
}
|
|
3672
|
-
|
|
3673
|
-
getComments() {
|
|
3674
|
-
return this.game.getComments();
|
|
3675
|
-
}
|
|
3676
|
-
|
|
3677
|
-
history(options = {}) {
|
|
3678
|
-
return this.game.history(options);
|
|
3679
|
-
}
|
|
3680
|
-
|
|
3681
|
-
isCheckmate() {
|
|
3682
|
-
return this.game.isCheckmate();
|
|
3683
|
-
}
|
|
3684
|
-
|
|
3685
|
-
isDraw() {
|
|
3686
|
-
return this.game.isDraw();
|
|
3687
|
-
}
|
|
3688
|
-
|
|
3689
|
-
isDrawByFiftyMoves() {
|
|
3690
|
-
return this.game.isDrawByFiftyMoves();
|
|
3691
|
-
}
|
|
3692
|
-
|
|
3693
|
-
isInsufficientMaterial() {
|
|
3694
|
-
return this.game.isInsufficientMaterial();
|
|
3695
|
-
}
|
|
3696
|
-
|
|
3697
|
-
isGameOver() {
|
|
3698
|
-
return this.game.isGameOver();
|
|
3699
|
-
}
|
|
3700
|
-
|
|
3701
|
-
isStalemate() {
|
|
3702
|
-
return this.game.isStalemate();
|
|
3703
|
-
}
|
|
3704
|
-
|
|
3705
|
-
isThreefoldRepetition() {
|
|
3706
|
-
return this.game.isThreefoldRepetition();
|
|
3707
|
-
}
|
|
3708
|
-
|
|
3709
|
-
load(position, options = {}, animation = true) {
|
|
3710
|
-
this.clearSquares();
|
|
3711
|
-
this.setGame(position, options);
|
|
3712
|
-
this.updateBoardPieces(animation);
|
|
3713
|
-
}
|
|
3714
|
-
|
|
3715
|
-
loadPgn(pgn, options = {}, animation = true) {
|
|
3716
|
-
this.clearSquares();
|
|
3717
|
-
this.game.loadPgn(pgn, options);
|
|
3718
|
-
this.updateBoardPieces();
|
|
3719
|
-
}
|
|
3720
|
-
|
|
3721
|
-
moveNumber() {
|
|
3722
|
-
return this.game.moveNumber();
|
|
3723
|
-
}
|
|
3724
|
-
|
|
3725
|
-
moves(options = {}) {
|
|
3726
|
-
return this.game.moves(options);
|
|
3727
|
-
}
|
|
3728
|
-
|
|
3729
|
-
pgn(options = {}) {
|
|
3730
|
-
return this.game.pgn(options);
|
|
3731
|
-
}
|
|
3732
|
-
|
|
3733
|
-
put(pieceId, squareId, animation = true) {
|
|
3734
|
-
const [type, color] = pieceId.split('');
|
|
3735
|
-
const success = this.game.put({ type: type, color: color }, squareId);
|
|
3736
|
-
if (success) this.updateBoardPieces(animation);
|
|
3737
|
-
return success;
|
|
3738
|
-
}
|
|
3739
|
-
|
|
3740
|
-
remove(squareId, animation = true) {
|
|
3741
|
-
const removedPiece = this.game.remove(squareId);
|
|
3742
|
-
this.updateBoardPieces(animation);
|
|
3743
|
-
return removedPiece;
|
|
3744
|
-
}
|
|
3745
|
-
|
|
3746
|
-
removeComment() {
|
|
3747
|
-
return this.game.removeComment();
|
|
3748
|
-
}
|
|
3749
|
-
|
|
3750
|
-
removeComments() {
|
|
3751
|
-
return this.game.removeComments();
|
|
3752
|
-
}
|
|
3753
|
-
|
|
3754
|
-
removeHeader(field) {
|
|
3755
|
-
return this.game.removeHeader(field);
|
|
3756
|
-
}
|
|
3757
|
-
|
|
3758
|
-
reset(animation = true) {
|
|
3759
|
-
this.game.reset();
|
|
3760
|
-
this.updateBoardPieces(animation);
|
|
3761
|
-
}
|
|
3762
|
-
|
|
3763
|
-
setCastlingRights(color, rights) {
|
|
3764
|
-
return this.game.setCastlingRights(color, rights);
|
|
3765
|
-
}
|
|
3766
|
-
|
|
3767
|
-
setComment(comment) {
|
|
3768
|
-
this.game.setComment(comment);
|
|
3769
|
-
}
|
|
3770
|
-
|
|
3771
|
-
setHeader(key, value) {
|
|
3772
|
-
return this.game.setHeader(key, value);
|
|
3773
|
-
}
|
|
3774
|
-
|
|
3775
|
-
squareColor(squareId) {
|
|
3776
|
-
return this.game.squareColor(squareId);
|
|
3777
|
-
}
|
|
3778
|
-
|
|
3779
|
-
turn() {
|
|
3780
|
-
return this.game.turn();
|
|
3781
|
-
}
|
|
3782
|
-
|
|
3783
|
-
undo() {
|
|
3784
|
-
const move = this.game.undo();
|
|
3785
|
-
if (move) this.updateBoardPieces();
|
|
3786
|
-
return move;
|
|
3787
|
-
}
|
|
3788
|
-
|
|
3789
|
-
validateFen(fen) {
|
|
3790
|
-
return validateFen(fen);
|
|
3791
|
-
}
|
|
3792
|
-
|
|
3793
|
-
// -------------------
|
|
3794
|
-
// Other Utility Functions
|
|
3795
|
-
// -------------------
|
|
3796
|
-
chageFenTurn(fen, color) {
|
|
3797
|
-
let parts = fen.split(' ');
|
|
3798
|
-
parts[1] = color;
|
|
3799
|
-
return parts.join(' ');
|
|
3800
|
-
}
|
|
3801
|
-
|
|
3802
|
-
changeFenColor(fen) {
|
|
3803
|
-
let parts = fen.split(' ');
|
|
3804
|
-
parts[1] = parts[1] === 'w' ? 'b' : 'w';
|
|
3805
|
-
return parts.join(' ');
|
|
3806
|
-
}
|
|
3807
|
-
|
|
3808
|
-
isWhiteOriented() { return this.config.orientation === 'w' }
|
|
3809
|
-
|
|
3810
|
-
};
|
|
3811
|
-
|
|
3812
|
-
/**
|
|
3813
|
-
* Chessboard.js - A beautiful, customizable chessboard widget
|
|
3814
|
-
* Entry point for the core library
|
|
3815
|
-
*/
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
// Factory function to maintain backward compatibility
|
|
3819
|
-
function Chessboard(containerElm, config = {}) {
|
|
3820
|
-
// If first parameter is an object, treat it as config
|
|
3821
|
-
if (typeof containerElm === 'object' && containerElm !== null) {
|
|
3822
|
-
return new Chessboard$1(containerElm);
|
|
3823
|
-
}
|
|
3824
|
-
|
|
3825
|
-
// Otherwise, treat first parameter as element ID
|
|
3826
|
-
const fullConfig = { ...config, id: containerElm };
|
|
3827
|
-
return new Chessboard$1(fullConfig);
|
|
3828
|
-
}
|
|
3829
|
-
|
|
3830
|
-
// Wrapper class that handles both calling conventions
|
|
3831
|
-
class ChessboardWrapper extends Chessboard$1 {
|
|
3832
|
-
constructor(containerElm, config = {}) {
|
|
3833
|
-
// If first parameter is an object, treat it as config
|
|
3834
|
-
if (typeof containerElm === 'object' && containerElm !== null) {
|
|
3835
|
-
super(containerElm);
|
|
3836
|
-
} else {
|
|
3837
|
-
// Otherwise, treat first parameter as element ID
|
|
3838
|
-
const fullConfig = { ...config, id: containerElm };
|
|
3839
|
-
super(fullConfig);
|
|
3840
|
-
}
|
|
3841
|
-
}
|
|
3842
|
-
}
|
|
3843
|
-
|
|
3844
|
-
// Attach the class to the factory function for direct access
|
|
3845
|
-
Chessboard.Class = ChessboardWrapper;
|
|
3846
|
-
Chessboard.Chessboard = ChessboardWrapper;
|
|
3847
|
-
|
|
3848
|
-
/**
|
|
3849
|
-
* Coordinate utilities for Chessboard.js
|
|
3850
|
-
*/
|
|
3851
|
-
|
|
3852
|
-
/**
|
|
3853
|
-
* Convert algebraic notation to array coordinates
|
|
3854
|
-
* @param {string} square - Square in algebraic notation (e.g., 'a1', 'h8')
|
|
3855
|
-
* @returns {Object} Object with row and col properties
|
|
3856
|
-
*/
|
|
3857
|
-
function algebraicToCoords(square) {
|
|
3858
|
-
const file = square.charCodeAt(0) - 97; // 'a' = 0, 'b' = 1, etc.
|
|
3859
|
-
const rank = parseInt(square[1]) - 1; // '1' = 0, '2' = 1, etc.
|
|
3860
|
-
|
|
3861
|
-
return { row: 7 - rank, col: file };
|
|
3862
|
-
}
|
|
3863
|
-
|
|
3864
|
-
/**
|
|
3865
|
-
* Convert array coordinates to algebraic notation
|
|
3866
|
-
* @param {number} row - Row index (0-7)
|
|
3867
|
-
* @param {number} col - Column index (0-7)
|
|
3868
|
-
* @returns {string} Square in algebraic notation
|
|
3869
|
-
*/
|
|
3870
|
-
function coordsToAlgebraic(row, col) {
|
|
3871
|
-
const file = String.fromCharCode(97 + col); // 0 = 'a', 1 = 'b', etc.
|
|
3872
|
-
const rank = (8 - row).toString(); // 0 = '8', 1 = '7', etc.
|
|
3873
|
-
|
|
3874
|
-
return file + rank;
|
|
3875
|
-
}
|
|
3876
|
-
|
|
3877
|
-
/**
|
|
3878
|
-
* Get the color of a square
|
|
3879
|
-
* @param {string} square - Square in algebraic notation
|
|
3880
|
-
* @returns {string} 'light' or 'dark'
|
|
3881
|
-
*/
|
|
3882
|
-
function getSquareColor(square) {
|
|
3883
|
-
const { row, col } = algebraicToCoords(square);
|
|
3884
|
-
return (row + col) % 2 === 0 ? 'dark' : 'light';
|
|
3885
|
-
}
|
|
3886
|
-
|
|
3887
|
-
/**
|
|
3888
|
-
* Check if coordinates are valid
|
|
3889
|
-
* @param {number} row - Row index
|
|
3890
|
-
* @param {number} col - Column index
|
|
3891
|
-
* @returns {boolean} True if coordinates are valid
|
|
3892
|
-
*/
|
|
3893
|
-
function isValidCoords(row, col) {
|
|
3894
|
-
return row >= 0 && row <= 7 && col >= 0 && col <= 7;
|
|
3895
|
-
}
|
|
3896
|
-
|
|
3897
|
-
/**
|
|
3898
|
-
* Check if algebraic notation is valid
|
|
3899
|
-
* @param {string} square - Square in algebraic notation
|
|
3900
|
-
* @returns {boolean} True if square notation is valid
|
|
3901
|
-
*/
|
|
3902
|
-
function isValidSquare$1(square) {
|
|
3903
|
-
if (typeof square !== 'string' || square.length !== 2) return false;
|
|
3904
|
-
|
|
3905
|
-
const file = square[0];
|
|
3906
|
-
const rank = square[1];
|
|
3907
|
-
|
|
3908
|
-
return file >= 'a' && file <= 'h' && rank >= '1' && rank <= '8';
|
|
3909
|
-
}
|
|
3910
|
-
|
|
3911
|
-
/**
|
|
3912
|
-
* Validation utilities for Chessboard.js
|
|
3913
|
-
*/
|
|
3914
|
-
|
|
3915
|
-
/**
|
|
3916
|
-
* Validate piece notation
|
|
3917
|
-
* @param {string} piece - Piece notation (e.g., 'wP', 'bK')
|
|
3918
|
-
* @returns {boolean} True if piece notation is valid
|
|
3919
|
-
*/
|
|
3920
|
-
function isValidPiece(piece) {
|
|
3921
|
-
if (typeof piece !== 'string' || piece.length !== 2) return false;
|
|
3922
|
-
|
|
3923
|
-
const color = piece[0];
|
|
3924
|
-
const type = piece[1];
|
|
3925
|
-
|
|
3926
|
-
return ['w', 'b'].includes(color) && ['P', 'R', 'N', 'B', 'Q', 'K'].includes(type);
|
|
3927
|
-
}
|
|
3928
|
-
|
|
3929
|
-
/**
|
|
3930
|
-
* Validate position object
|
|
3931
|
-
* @param {Object} position - Position object with square-piece mappings
|
|
3932
|
-
* @returns {boolean} True if position is valid
|
|
3933
|
-
*/
|
|
3934
|
-
function isValidPosition(position) {
|
|
3935
|
-
if (typeof position !== 'object' || position === null) return false;
|
|
3936
|
-
|
|
3937
|
-
for (const [square, piece] of Object.entries(position)) {
|
|
3938
|
-
if (!isValidSquare(square) || !isValidPiece(piece)) {
|
|
3939
|
-
return false;
|
|
3940
|
-
}
|
|
3941
|
-
}
|
|
3942
|
-
|
|
3943
|
-
return true;
|
|
3944
|
-
}
|
|
3945
|
-
|
|
3946
|
-
/**
|
|
3947
|
-
* Validate FEN string format
|
|
3948
|
-
* @param {string} fen - FEN string
|
|
3949
|
-
* @returns {Object} Validation result with success and error properties
|
|
3950
|
-
*/
|
|
3951
|
-
function validateFenFormat(fen) {
|
|
3952
|
-
if (typeof fen !== 'string') {
|
|
3953
|
-
return { success: false, error: 'FEN must be a string' };
|
|
3954
|
-
}
|
|
3955
|
-
|
|
3956
|
-
const parts = fen.split(' ');
|
|
3957
|
-
if (parts.length !== 6) {
|
|
3958
|
-
return { success: false, error: 'FEN must have 6 parts separated by spaces' };
|
|
3959
|
-
}
|
|
3960
|
-
|
|
3961
|
-
// Validate piece placement
|
|
3962
|
-
const ranks = parts[0].split('/');
|
|
3963
|
-
if (ranks.length !== 8) {
|
|
3964
|
-
return { success: false, error: 'Piece placement must have 8 ranks' };
|
|
3965
|
-
}
|
|
3966
|
-
|
|
3967
|
-
return { success: true };
|
|
3968
|
-
}
|
|
3969
|
-
|
|
3970
|
-
/**
|
|
3971
|
-
* Validate configuration object
|
|
3972
|
-
* @param {Object} config - Configuration object
|
|
3973
|
-
* @returns {Object} Validation result with success and errors array
|
|
3974
|
-
*/
|
|
3975
|
-
function validateConfig(config) {
|
|
3976
|
-
const errors = [];
|
|
3977
|
-
|
|
3978
|
-
if (config.orientation && !['white', 'black', 'w', 'b'].includes(config.orientation)) {
|
|
3979
|
-
errors.push('Invalid orientation. Must be "white", "black", "w", or "b"');
|
|
3980
|
-
}
|
|
3981
|
-
|
|
3982
|
-
if (config.position && config.position !== 'start' && typeof config.position !== 'object') {
|
|
3983
|
-
errors.push('Invalid position. Must be "start" or a position object');
|
|
3984
|
-
}
|
|
3985
|
-
|
|
3986
|
-
if (config.size && typeof config.size !== 'string' && typeof config.size !== 'number') {
|
|
3987
|
-
errors.push('Invalid size. Must be a string or number');
|
|
3988
|
-
}
|
|
3989
|
-
|
|
3990
|
-
return {
|
|
3991
|
-
success: errors.length === 0,
|
|
3992
|
-
errors
|
|
3993
|
-
};
|
|
3994
|
-
}
|
|
3995
|
-
|
|
3996
|
-
/**
|
|
3997
|
-
* Animation utilities for Chessboard.js
|
|
3998
|
-
*/
|
|
3999
|
-
|
|
4000
|
-
/**
|
|
4001
|
-
* Get the CSS transition duration in milliseconds
|
|
4002
|
-
* @param {string|number} time - Time value ('fast', 'slow', or number in ms)
|
|
4003
|
-
* @returns {number} Duration in milliseconds
|
|
4004
|
-
*/
|
|
4005
|
-
function parseTime(time) {
|
|
4006
|
-
if (typeof time === 'number') return time;
|
|
4007
|
-
|
|
4008
|
-
switch (time) {
|
|
4009
|
-
case 'fast': return 150;
|
|
4010
|
-
case 'slow': return 500;
|
|
4011
|
-
default: return 200;
|
|
4012
|
-
}
|
|
4013
|
-
}
|
|
4014
|
-
|
|
4015
|
-
/**
|
|
4016
|
-
* Get the CSS transition function
|
|
4017
|
-
* @param {string} animation - Animation type ('ease', 'linear', etc.)
|
|
4018
|
-
* @returns {string} CSS transition function
|
|
4019
|
-
*/
|
|
4020
|
-
function parseAnimation(animation) {
|
|
4021
|
-
const validAnimations = ['ease', 'ease-in', 'ease-out', 'ease-in-out', 'linear'];
|
|
4022
|
-
return validAnimations.includes(animation) ? animation : 'ease';
|
|
4023
|
-
}
|
|
4024
|
-
|
|
4025
|
-
/**
|
|
4026
|
-
* Create a promise that resolves after animation completion
|
|
4027
|
-
* @param {number} duration - Duration in milliseconds
|
|
4028
|
-
* @returns {Promise} Promise that resolves after the duration
|
|
4029
|
-
*/
|
|
4030
|
-
function animationPromise(duration) {
|
|
4031
|
-
return new Promise(resolve => setTimeout(resolve, duration));
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4034
|
-
/**
|
|
4035
|
-
* Chessboard.js - A beautiful, customizable chessboard widget
|
|
4036
|
-
* Main entry point for the library
|
|
4037
|
-
*
|
|
4038
|
-
* @version 2.2.1
|
|
4039
|
-
* @author alepot55
|
|
4040
|
-
* @license ISC
|
|
4041
|
-
*/
|
|
4042
|
-
|
|
4043
|
-
exports.Chess = Chess;
|
|
4044
|
-
exports.Chessboard = Chessboard;
|
|
4045
|
-
exports.ChessboardConfig = ChessboardConfig;
|
|
4046
|
-
exports.Move = Move;
|
|
4047
|
-
exports.Piece = Piece;
|
|
4048
|
-
exports.Square = Square;
|
|
4049
|
-
exports.algebraicToCoords = algebraicToCoords;
|
|
4050
|
-
exports.animationPromise = animationPromise;
|
|
4051
|
-
exports.coordsToAlgebraic = coordsToAlgebraic;
|
|
4052
|
-
exports.default = Chessboard;
|
|
4053
|
-
exports.getSquareColor = getSquareColor;
|
|
4054
|
-
exports.isValidCoords = isValidCoords;
|
|
4055
|
-
exports.isValidPiece = isValidPiece;
|
|
4056
|
-
exports.isValidPosition = isValidPosition;
|
|
4057
|
-
exports.isValidSquare = isValidSquare$1;
|
|
4058
|
-
exports.parseAnimation = parseAnimation;
|
|
4059
|
-
exports.parseTime = parseTime;
|
|
4060
|
-
exports.rafThrottle = rafThrottle;
|
|
4061
|
-
exports.resetTransform = resetTransform;
|
|
4062
|
-
exports.setTransform = setTransform;
|
|
4063
|
-
exports.throttle = throttle;
|
|
4064
|
-
exports.validateConfig = validateConfig;
|
|
4065
|
-
exports.validateFen = validateFen;
|
|
4066
|
-
exports.validateFenFormat = validateFenFormat;
|
|
4067
|
-
|
|
4068
|
-
Object.defineProperty(exports, '__esModule', { value: true });
|
|
4069
|
-
|
|
4070
|
-
return exports;
|
|
4071
|
-
|
|
4072
|
-
})({});
|