@alepot55/chessboardjs 2.2.1 → 2.3.2
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/.eslintrc.json +227 -0
- package/.github/instructions/copilot-instuctions.md +1671 -0
- package/README.md +125 -401
- package/assets/themes/alepot/theme.json +42 -0
- package/assets/themes/default/theme.json +42 -0
- package/chessboard.bundle.js +708 -58
- package/config/jest.config.js +15 -0
- package/config/rollup.config.js +35 -0
- package/dist/chessboard.cjs.js +10476 -0
- package/dist/chessboard.css +197 -0
- package/dist/chessboard.esm.js +10407 -0
- package/dist/chessboard.iife.js +10481 -0
- package/dist/chessboard.umd.js +10482 -0
- package/jest.config.js +2 -7
- package/package.json +18 -3
- package/rollup.config.js +2 -11
- package/{chessboard.move.js → src/components/Move.js} +3 -3
- package/src/components/Piece.js +273 -0
- package/{chessboard.square.js → src/components/Square.js} +60 -7
- package/src/constants/index.js +15 -0
- package/src/constants/positions.js +62 -0
- package/src/core/Chessboard.js +1930 -0
- package/src/core/ChessboardConfig.js +458 -0
- package/src/core/ChessboardFactory.js +385 -0
- package/src/core/index.js +141 -0
- package/src/errors/ChessboardError.js +133 -0
- package/src/errors/index.js +15 -0
- package/src/errors/messages.js +189 -0
- package/src/index.js +103 -0
- package/src/services/AnimationService.js +180 -0
- package/src/services/BoardService.js +156 -0
- package/src/services/CoordinateService.js +355 -0
- package/src/services/EventService.js +807 -0
- package/src/services/MoveService.js +594 -0
- package/src/services/PieceService.js +303 -0
- package/src/services/PositionService.js +237 -0
- package/src/services/ValidationService.js +673 -0
- package/src/services/index.js +14 -0
- package/src/styles/animations.css +46 -0
- package/{chessboard.css → src/styles/board.css} +3 -0
- package/src/styles/index.css +4 -0
- package/src/styles/pieces.css +66 -0
- package/src/utils/animations.js +37 -0
- package/{chess.js → src/utils/chess.js} +16 -16
- package/src/utils/coordinates.js +62 -0
- package/src/utils/cross-browser.js +150 -0
- package/src/utils/logger.js +422 -0
- package/src/utils/performance.js +311 -0
- package/src/utils/validation.js +458 -0
- package/tests/unit/chessboard-config-animations.test.js +106 -0
- package/tests/unit/chessboard-robust.test.js +163 -0
- package/tests/unit/chessboard.test.js +183 -0
- package/chessboard.config.js +0 -147
- package/chessboard.js +0 -979
- package/chessboard.piece.js +0 -115
- package/test/chessboard.test.js +0 -128
- /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
- /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
- /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
- /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
- /package/{.babelrc → config/.babelrc} +0 -0
|
@@ -0,0 +1,594 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for managing chess moves and move validation
|
|
3
|
+
* @module services/MoveService
|
|
4
|
+
* @since 2.0.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import Move from '../components/Move.js';
|
|
8
|
+
import { MoveError, ValidationError } from '../errors/ChessboardError.js';
|
|
9
|
+
import { ERROR_MESSAGES } from '../errors/messages.js';
|
|
10
|
+
import { PROMOTION_PIECES } from '../constants/positions.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Service responsible for move management and validation
|
|
14
|
+
* @class
|
|
15
|
+
*/
|
|
16
|
+
export class MoveService {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new MoveService instance
|
|
19
|
+
* @param {ChessboardConfig} config - Board configuration
|
|
20
|
+
* @param {PositionService} positionService - Position service instance
|
|
21
|
+
*/
|
|
22
|
+
constructor(config, positionService) {
|
|
23
|
+
this.config = config;
|
|
24
|
+
this.positionService = positionService;
|
|
25
|
+
this._movesCache = new Map();
|
|
26
|
+
this._cacheTimeout = null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Checks if a piece on a square can move
|
|
31
|
+
* @param {Square} square - Square to check
|
|
32
|
+
* @returns {boolean} True if piece can move
|
|
33
|
+
*/
|
|
34
|
+
canMove(square) {
|
|
35
|
+
if (!square.piece) return false;
|
|
36
|
+
|
|
37
|
+
const { movableColors, onlyLegalMoves } = this.config;
|
|
38
|
+
|
|
39
|
+
if (movableColors === 'none') return false;
|
|
40
|
+
if (movableColors === 'w' && square.piece.color === 'b') return false;
|
|
41
|
+
if (movableColors === 'b' && square.piece.color === 'w') return false;
|
|
42
|
+
|
|
43
|
+
if (!onlyLegalMoves) return true;
|
|
44
|
+
|
|
45
|
+
// Check if position service and game are available
|
|
46
|
+
if (!this.positionService || !this.positionService.getGame()) {
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const game = this.positionService.getGame();
|
|
51
|
+
return square.piece.color === game.turn();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Converts various move formats to a Move instance
|
|
56
|
+
* @param {string|Move} move - Move in various formats
|
|
57
|
+
* @param {Object} squares - All board squares
|
|
58
|
+
* @returns {Move} Move instance
|
|
59
|
+
* @throws {MoveError} When move format is invalid
|
|
60
|
+
*/
|
|
61
|
+
convertMove(move, squares) {
|
|
62
|
+
if (move instanceof Move) {
|
|
63
|
+
return move;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (typeof move === 'string' && move.length >= 4) {
|
|
67
|
+
const fromId = move.slice(0, 2);
|
|
68
|
+
const toId = move.slice(2, 4);
|
|
69
|
+
const promotion = move.slice(4, 5) || null;
|
|
70
|
+
|
|
71
|
+
if (!squares[fromId] || !squares[toId]) {
|
|
72
|
+
throw new MoveError(ERROR_MESSAGES.invalid_move_format, fromId, toId);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return new Move(squares[fromId], squares[toId], promotion);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
throw new MoveError(ERROR_MESSAGES.invalid_move_format, 'unknown', 'unknown');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Checks if a move is legal
|
|
83
|
+
* @param {Move} move - Move to check
|
|
84
|
+
* @returns {boolean} True if move is legal
|
|
85
|
+
*/
|
|
86
|
+
isLegalMove(move) {
|
|
87
|
+
const legalMoves = this.getLegalMoves(move.from.id);
|
|
88
|
+
|
|
89
|
+
return legalMoves.some(legalMove =>
|
|
90
|
+
legalMove.to === move.to.id &&
|
|
91
|
+
move.promotion === legalMove.promotion
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Gets all legal moves for a square or the entire position
|
|
97
|
+
* @param {string} [from] - Square to get moves from (optional)
|
|
98
|
+
* @param {boolean} [verbose=true] - Whether to return verbose move objects
|
|
99
|
+
* @returns {Array} Array of legal moves
|
|
100
|
+
*/
|
|
101
|
+
getLegalMoves(from = null, verbose = true) {
|
|
102
|
+
// Check if position service and game are available
|
|
103
|
+
if (!this.positionService || !this.positionService.getGame()) {
|
|
104
|
+
return [];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const game = this.positionService.getGame();
|
|
108
|
+
|
|
109
|
+
if (!game) return [];
|
|
110
|
+
|
|
111
|
+
const options = { verbose };
|
|
112
|
+
if (from) {
|
|
113
|
+
options.square = from;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return game.moves(options);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Gets legal moves with caching for performance
|
|
121
|
+
* @param {Square} square - Square to get moves from
|
|
122
|
+
* @returns {Array} Array of legal moves
|
|
123
|
+
*/
|
|
124
|
+
getCachedLegalMoves(square) {
|
|
125
|
+
// Check if position service and game are available
|
|
126
|
+
if (!this.positionService || !this.positionService.getGame()) {
|
|
127
|
+
return [];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const game = this.positionService.getGame();
|
|
131
|
+
if (!game) return [];
|
|
132
|
+
|
|
133
|
+
const cacheKey = `${square.id}-${game.fen()}`;
|
|
134
|
+
let moves = this._movesCache.get(cacheKey);
|
|
135
|
+
|
|
136
|
+
if (!moves) {
|
|
137
|
+
moves = game.moves({ square: square.id, verbose: true });
|
|
138
|
+
this._movesCache.set(cacheKey, moves);
|
|
139
|
+
|
|
140
|
+
// Clear cache after a short delay to prevent memory buildup
|
|
141
|
+
if (this._cacheTimeout) {
|
|
142
|
+
clearTimeout(this._cacheTimeout);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
this._cacheTimeout = setTimeout(() => {
|
|
146
|
+
this._movesCache.clear();
|
|
147
|
+
}, 1000);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return moves;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Executes a move on the game
|
|
155
|
+
* @param {Move} move - Move to execute
|
|
156
|
+
* @returns {Object|null} Move result from chess.js or null if invalid
|
|
157
|
+
*/
|
|
158
|
+
executeMove(move) {
|
|
159
|
+
// Check if position service and game are available
|
|
160
|
+
if (!this.positionService || !this.positionService.getGame()) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const game = this.positionService.getGame();
|
|
165
|
+
if (!game) return null;
|
|
166
|
+
|
|
167
|
+
const moveOptions = {
|
|
168
|
+
from: move.from.id,
|
|
169
|
+
to: move.to.id
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
console.log('executeMove - move.promotion:', move.promotion);
|
|
173
|
+
console.log('executeMove - move.hasPromotion():', move.hasPromotion());
|
|
174
|
+
|
|
175
|
+
if (move.hasPromotion()) {
|
|
176
|
+
moveOptions.promotion = move.promotion;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
console.log('executeMove - moveOptions:', moveOptions);
|
|
180
|
+
|
|
181
|
+
const result = game.move(moveOptions);
|
|
182
|
+
console.log('executeMove - result:', result);
|
|
183
|
+
|
|
184
|
+
// Check what's actually on the board after the move
|
|
185
|
+
if (result) {
|
|
186
|
+
const pieceOnDestination = game.get(move.to.id);
|
|
187
|
+
console.log('executeMove - piece on destination after move:', pieceOnDestination);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return result;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Checks if a move requires promotion
|
|
195
|
+
* @param {Move} move - Move to check
|
|
196
|
+
* @returns {boolean} True if promotion is required
|
|
197
|
+
*/
|
|
198
|
+
requiresPromotion(move) {
|
|
199
|
+
console.log('Checking if move requires promotion:', move.from.id, '->', move.to.id);
|
|
200
|
+
|
|
201
|
+
if (!this.config.onlyLegalMoves) {
|
|
202
|
+
console.log('Not in legal moves mode, no promotion required');
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const game = this.positionService.getGame();
|
|
207
|
+
if (!game) {
|
|
208
|
+
console.log('No game instance available');
|
|
209
|
+
return false;
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
const piece = game.get(move.from.id);
|
|
213
|
+
if (!piece || piece.type !== 'p') {
|
|
214
|
+
console.log('Not a pawn move, no promotion required');
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const targetRank = move.to.row;
|
|
219
|
+
if (targetRank !== 1 && targetRank !== 8) {
|
|
220
|
+
console.log('Not reaching promotion rank, no promotion required');
|
|
221
|
+
return false;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
console.log('Pawn reaching promotion rank, validating move...');
|
|
225
|
+
|
|
226
|
+
// Additional validation: check if the pawn can actually reach this square
|
|
227
|
+
if (!this._isPawnMoveValid(move.from, move.to, piece.color)) {
|
|
228
|
+
console.log('Pawn move not valid, no promotion required');
|
|
229
|
+
return false;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// First check if the move is legal without promotion
|
|
233
|
+
const simpleMoveObj = {
|
|
234
|
+
from: move.from.id,
|
|
235
|
+
to: move.to.id
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
console.log('Testing move without promotion:', simpleMoveObj);
|
|
240
|
+
// Test if the move is legal without promotion first
|
|
241
|
+
const testMove = game.move(simpleMoveObj);
|
|
242
|
+
if (testMove) {
|
|
243
|
+
// Move was successful, but check if it was a promotion
|
|
244
|
+
const wasPromotion = testMove.promotion;
|
|
245
|
+
|
|
246
|
+
// Undo the test move
|
|
247
|
+
game.undo();
|
|
248
|
+
|
|
249
|
+
console.log('Move successful without promotion, was promotion:', wasPromotion !== undefined);
|
|
250
|
+
|
|
251
|
+
// If it was a promotion, return true
|
|
252
|
+
return wasPromotion !== undefined;
|
|
253
|
+
}
|
|
254
|
+
} catch (error) {
|
|
255
|
+
console.log('Move failed without promotion, trying with promotion:', error.message);
|
|
256
|
+
|
|
257
|
+
// If simple move fails, try with promotion
|
|
258
|
+
const promotionMoveObj = {
|
|
259
|
+
from: move.from.id,
|
|
260
|
+
to: move.to.id,
|
|
261
|
+
promotion: 'q' // test with queen
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
try {
|
|
265
|
+
console.log('Testing move with promotion:', promotionMoveObj);
|
|
266
|
+
const testMove = game.move(promotionMoveObj);
|
|
267
|
+
if (testMove) {
|
|
268
|
+
// Undo the test move
|
|
269
|
+
game.undo();
|
|
270
|
+
console.log('Move successful with promotion, promotion required');
|
|
271
|
+
return true;
|
|
272
|
+
}
|
|
273
|
+
} catch (promotionError) {
|
|
274
|
+
console.log('Move failed even with promotion:', promotionError.message);
|
|
275
|
+
// Move is not legal even with promotion
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
console.log('Move validation complete, no promotion required');
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validates if a pawn move is theoretically possible
|
|
286
|
+
* @private
|
|
287
|
+
* @param {Square} from - Source square
|
|
288
|
+
* @param {Square} to - Target square
|
|
289
|
+
* @param {string} color - Pawn color ('w' or 'b')
|
|
290
|
+
* @returns {boolean} True if the move is valid for a pawn
|
|
291
|
+
*/
|
|
292
|
+
_isPawnMoveValid(from, to, color) {
|
|
293
|
+
const fromRank = from.row;
|
|
294
|
+
const toRank = to.row;
|
|
295
|
+
const fromFile = from.col;
|
|
296
|
+
const toFile = to.col;
|
|
297
|
+
|
|
298
|
+
console.log(`Validating pawn move: ${from.id} -> ${to.id} (${color})`);
|
|
299
|
+
console.log(`Ranks: ${fromRank} -> ${toRank}, Files: ${fromFile} -> ${toFile}`);
|
|
300
|
+
|
|
301
|
+
// Direction of pawn movement
|
|
302
|
+
const direction = color === 'w' ? 1 : -1;
|
|
303
|
+
const rankDiff = toRank - fromRank;
|
|
304
|
+
const fileDiff = Math.abs(toFile - fromFile);
|
|
305
|
+
|
|
306
|
+
// Pawn can only move forward
|
|
307
|
+
if (rankDiff * direction <= 0) {
|
|
308
|
+
console.log('Invalid: Pawn cannot move backward or stay in place');
|
|
309
|
+
return false;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Pawn can only move 1 rank at a time (except for double move from starting position)
|
|
313
|
+
if (Math.abs(rankDiff) > 2) {
|
|
314
|
+
console.log('Invalid: Pawn cannot move more than 2 ranks');
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// If moving 2 ranks, must be from starting position
|
|
319
|
+
if (Math.abs(rankDiff) === 2) {
|
|
320
|
+
const startingRank = color === 'w' ? 2 : 7;
|
|
321
|
+
if (fromRank !== startingRank) {
|
|
322
|
+
console.log(`Invalid: Pawn cannot move 2 ranks from rank ${fromRank}`);
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Pawn can only move to adjacent files (diagonal capture) or same file (forward move)
|
|
328
|
+
if (fileDiff > 1) {
|
|
329
|
+
console.log('Invalid: Pawn cannot move more than 1 file');
|
|
330
|
+
return false;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
console.log('Pawn move validation passed');
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
/**
|
|
338
|
+
* Handles promotion UI setup
|
|
339
|
+
* @param {Move} move - Move requiring promotion
|
|
340
|
+
* @param {Object} squares - All board squares
|
|
341
|
+
* @param {Function} onPromotionSelect - Callback when promotion piece is selected
|
|
342
|
+
* @param {Function} onPromotionCancel - Callback when promotion is cancelled
|
|
343
|
+
* @returns {boolean} True if promotion UI was set up
|
|
344
|
+
*/
|
|
345
|
+
setupPromotion(move, squares, onPromotionSelect, onPromotionCancel) {
|
|
346
|
+
if (!this.requiresPromotion(move)) return false;
|
|
347
|
+
|
|
348
|
+
// Check if position service and game are available
|
|
349
|
+
if (!this.positionService || !this.positionService.getGame()) {
|
|
350
|
+
return false;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
const game = this.positionService.getGame();
|
|
354
|
+
const piece = game.get(move.from.id);
|
|
355
|
+
const targetSquare = move.to;
|
|
356
|
+
|
|
357
|
+
// Clear any existing promotion UI
|
|
358
|
+
Object.values(squares).forEach(square => {
|
|
359
|
+
square.removePromotion();
|
|
360
|
+
square.removeCover();
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
// Always show promotion choices in a column
|
|
364
|
+
this._showPromotionInColumn(targetSquare, piece, squares, onPromotionSelect, onPromotionCancel);
|
|
365
|
+
|
|
366
|
+
return true;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/**
|
|
370
|
+
* Shows promotion choices in a column
|
|
371
|
+
* @private
|
|
372
|
+
*/
|
|
373
|
+
_showPromotionInColumn(targetSquare, piece, squares, onPromotionSelect, onPromotionCancel) {
|
|
374
|
+
console.log('Setting up promotion for', targetSquare.id, 'piece color:', piece.color);
|
|
375
|
+
|
|
376
|
+
// Set up promotion choices starting from border row
|
|
377
|
+
PROMOTION_PIECES.forEach((pieceType, index) => {
|
|
378
|
+
const choiceSquare = this._findPromotionSquare(targetSquare, index, squares);
|
|
379
|
+
|
|
380
|
+
if (choiceSquare) {
|
|
381
|
+
const pieceId = pieceType + piece.color;
|
|
382
|
+
const piecePath = this._getPiecePathForPromotion(pieceId);
|
|
383
|
+
|
|
384
|
+
console.log('Setting up promotion choice:', pieceType, 'on square:', choiceSquare.id);
|
|
385
|
+
|
|
386
|
+
choiceSquare.putPromotion(piecePath, () => {
|
|
387
|
+
console.log('Promotion choice selected:', pieceType);
|
|
388
|
+
onPromotionSelect(pieceType);
|
|
389
|
+
});
|
|
390
|
+
} else {
|
|
391
|
+
console.log('Could not find square for promotion choice:', pieceType, 'index:', index);
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
// Set up cover squares (for cancellation)
|
|
396
|
+
Object.values(squares).forEach(square => {
|
|
397
|
+
if (!square.hasPromotion()) {
|
|
398
|
+
square.putCover(() => {
|
|
399
|
+
onPromotionCancel();
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
return true;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/**
|
|
408
|
+
* Finds the appropriate square for a promotion piece
|
|
409
|
+
* @private
|
|
410
|
+
* @param {Square} targetSquare - Target square of the promotion move
|
|
411
|
+
* @param {number} distance - Distance from target square
|
|
412
|
+
* @param {Object} squares - All board squares
|
|
413
|
+
* @returns {Square|null} Square for promotion piece or null
|
|
414
|
+
*/
|
|
415
|
+
_findPromotionSquare(targetSquare, index, squares) {
|
|
416
|
+
const col = targetSquare.col;
|
|
417
|
+
const baseRow = targetSquare.row;
|
|
418
|
+
|
|
419
|
+
console.log('Looking for promotion square - target:', targetSquare.id, 'index:', index, 'col:', col, 'baseRow:', baseRow);
|
|
420
|
+
|
|
421
|
+
// Calculate row based on index and promotion direction
|
|
422
|
+
// Start from the border row (1 or 8) and go inward
|
|
423
|
+
let row;
|
|
424
|
+
if (baseRow === 8) {
|
|
425
|
+
// White promotion: start from row 8 and go down
|
|
426
|
+
row = 8 - index;
|
|
427
|
+
} else if (baseRow === 1) {
|
|
428
|
+
// Black promotion: start from row 1 and go up
|
|
429
|
+
row = 1 + index;
|
|
430
|
+
} else {
|
|
431
|
+
console.log('Invalid promotion row:', baseRow);
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
console.log('Calculated row:', row);
|
|
436
|
+
|
|
437
|
+
// Ensure row is within bounds
|
|
438
|
+
if (row < 1 || row > 8) {
|
|
439
|
+
console.log('Row out of bounds:', row);
|
|
440
|
+
return null;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
// Find square by row/col
|
|
444
|
+
for (const square of Object.values(squares)) {
|
|
445
|
+
if (square.col === col && square.row === row) {
|
|
446
|
+
console.log('Found promotion square:', square.id);
|
|
447
|
+
return square;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
console.log('No square found for col:', col, 'row:', row);
|
|
452
|
+
return null;
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/**
|
|
456
|
+
* Gets piece path for promotion UI
|
|
457
|
+
* @private
|
|
458
|
+
* @param {string} pieceId - Piece identifier
|
|
459
|
+
* @returns {string} Path to piece asset
|
|
460
|
+
*/
|
|
461
|
+
_getPiecePathForPromotion(pieceId) {
|
|
462
|
+
// This would typically use the PieceService
|
|
463
|
+
// For now, we'll use a simple implementation
|
|
464
|
+
const { piecesPath } = this.config;
|
|
465
|
+
|
|
466
|
+
if (typeof piecesPath === 'string') {
|
|
467
|
+
return `${piecesPath}/${pieceId}.svg`;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// Fallback for other path types
|
|
471
|
+
return `assets/pieces/${pieceId}.svg`;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* Parses a move string into a move object
|
|
476
|
+
* @param {string} moveString - Move string (e.g., 'e2e4', 'e7e8q')
|
|
477
|
+
* @returns {Object|null} Move object or null if invalid
|
|
478
|
+
*/
|
|
479
|
+
parseMove(moveString) {
|
|
480
|
+
if (typeof moveString !== 'string' || moveString.length < 4 || moveString.length > 5) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const from = moveString.slice(0, 2);
|
|
485
|
+
const to = moveString.slice(2, 4);
|
|
486
|
+
const promotion = moveString.slice(4, 5);
|
|
487
|
+
|
|
488
|
+
// Basic validation
|
|
489
|
+
if (!/^[a-h][1-8]$/.test(from) || !/^[a-h][1-8]$/.test(to)) {
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (promotion && !['q', 'r', 'b', 'n'].includes(promotion.toLowerCase())) {
|
|
494
|
+
return null;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return {
|
|
498
|
+
from: from,
|
|
499
|
+
to: to,
|
|
500
|
+
promotion: promotion || null
|
|
501
|
+
};
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/**
|
|
505
|
+
* Checks if a move is a castle move
|
|
506
|
+
* @param {Object} gameMove - Game move object from chess.js
|
|
507
|
+
* @returns {boolean} True if move is castle
|
|
508
|
+
*/
|
|
509
|
+
isCastle(gameMove) {
|
|
510
|
+
return gameMove && (gameMove.isKingsideCastle() || gameMove.isQueensideCastle());
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Gets the rook move for a castle move
|
|
515
|
+
* @param {Object} gameMove - Game move object from chess.js
|
|
516
|
+
* @returns {Object|null} Rook move object or null if not castle
|
|
517
|
+
*/
|
|
518
|
+
getCastleRookMove(gameMove) {
|
|
519
|
+
if (!this.isCastle(gameMove)) {
|
|
520
|
+
return null;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const isKingSide = gameMove.isKingsideCastle();
|
|
524
|
+
const isWhite = gameMove.color === 'w';
|
|
525
|
+
|
|
526
|
+
if (isKingSide) {
|
|
527
|
+
// King side castle
|
|
528
|
+
if (isWhite) {
|
|
529
|
+
return { from: 'h1', to: 'f1' };
|
|
530
|
+
} else {
|
|
531
|
+
return { from: 'h8', to: 'f8' };
|
|
532
|
+
}
|
|
533
|
+
} else {
|
|
534
|
+
// Queen side castle
|
|
535
|
+
if (isWhite) {
|
|
536
|
+
return { from: 'a1', to: 'd1' };
|
|
537
|
+
} else {
|
|
538
|
+
return { from: 'a8', to: 'd8' };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
/**
|
|
544
|
+
* Checks if a move is en passant
|
|
545
|
+
* @param {Object} gameMove - Game move object from chess.js
|
|
546
|
+
* @returns {boolean} True if move is en passant
|
|
547
|
+
*/
|
|
548
|
+
isEnPassant(gameMove) {
|
|
549
|
+
return gameMove && gameMove.isEnPassant();
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Gets the captured pawn square for en passant
|
|
554
|
+
* @param {Object} gameMove - Game move object from chess.js
|
|
555
|
+
* @returns {string|null} Square of captured pawn or null if not en passant
|
|
556
|
+
*/
|
|
557
|
+
getEnPassantCapturedSquare(gameMove) {
|
|
558
|
+
if (!this.isEnPassant(gameMove)) {
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const toSquare = gameMove.to;
|
|
563
|
+
const rank = parseInt(toSquare[1]);
|
|
564
|
+
const file = toSquare[0];
|
|
565
|
+
|
|
566
|
+
// The captured pawn is on the same file but different rank
|
|
567
|
+
if (gameMove.color === 'w') {
|
|
568
|
+
// White captures black pawn one rank below
|
|
569
|
+
return file + (rank - 1);
|
|
570
|
+
} else {
|
|
571
|
+
// Black captures white pawn one rank above
|
|
572
|
+
return file + (rank + 1);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* Clears the moves cache
|
|
578
|
+
*/
|
|
579
|
+
clearCache() {
|
|
580
|
+
this._movesCache.clear();
|
|
581
|
+
if (this._cacheTimeout) {
|
|
582
|
+
clearTimeout(this._cacheTimeout);
|
|
583
|
+
this._cacheTimeout = null;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
/**
|
|
588
|
+
* Cleans up resources
|
|
589
|
+
*/
|
|
590
|
+
destroy() {
|
|
591
|
+
this.clearCache();
|
|
592
|
+
this.positionService = null;
|
|
593
|
+
}
|
|
594
|
+
}
|