@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.
Files changed (81) hide show
  1. package/.eslintrc.json +227 -0
  2. package/.github/instructions/copilot-instuctions.md +1671 -0
  3. package/README.md +125 -401
  4. package/assets/themes/alepot/theme.json +42 -0
  5. package/assets/themes/default/theme.json +42 -0
  6. package/chessboard.bundle.js +708 -58
  7. package/config/jest.config.js +15 -0
  8. package/config/rollup.config.js +35 -0
  9. package/dist/chessboard.cjs.js +10476 -0
  10. package/dist/chessboard.css +197 -0
  11. package/dist/chessboard.esm.js +10407 -0
  12. package/dist/chessboard.iife.js +10481 -0
  13. package/dist/chessboard.umd.js +10482 -0
  14. package/jest.config.js +2 -7
  15. package/package.json +18 -3
  16. package/rollup.config.js +2 -11
  17. package/{chessboard.move.js → src/components/Move.js} +3 -3
  18. package/src/components/Piece.js +273 -0
  19. package/{chessboard.square.js → src/components/Square.js} +60 -7
  20. package/src/constants/index.js +15 -0
  21. package/src/constants/positions.js +62 -0
  22. package/src/core/Chessboard.js +1930 -0
  23. package/src/core/ChessboardConfig.js +458 -0
  24. package/src/core/ChessboardFactory.js +385 -0
  25. package/src/core/index.js +141 -0
  26. package/src/errors/ChessboardError.js +133 -0
  27. package/src/errors/index.js +15 -0
  28. package/src/errors/messages.js +189 -0
  29. package/src/index.js +103 -0
  30. package/src/services/AnimationService.js +180 -0
  31. package/src/services/BoardService.js +156 -0
  32. package/src/services/CoordinateService.js +355 -0
  33. package/src/services/EventService.js +807 -0
  34. package/src/services/MoveService.js +594 -0
  35. package/src/services/PieceService.js +303 -0
  36. package/src/services/PositionService.js +237 -0
  37. package/src/services/ValidationService.js +673 -0
  38. package/src/services/index.js +14 -0
  39. package/src/styles/animations.css +46 -0
  40. package/{chessboard.css → src/styles/board.css} +3 -0
  41. package/src/styles/index.css +4 -0
  42. package/src/styles/pieces.css +66 -0
  43. package/src/utils/animations.js +37 -0
  44. package/{chess.js → src/utils/chess.js} +16 -16
  45. package/src/utils/coordinates.js +62 -0
  46. package/src/utils/cross-browser.js +150 -0
  47. package/src/utils/logger.js +422 -0
  48. package/src/utils/performance.js +311 -0
  49. package/src/utils/validation.js +458 -0
  50. package/tests/unit/chessboard-config-animations.test.js +106 -0
  51. package/tests/unit/chessboard-robust.test.js +163 -0
  52. package/tests/unit/chessboard.test.js +183 -0
  53. package/chessboard.config.js +0 -147
  54. package/chessboard.js +0 -979
  55. package/chessboard.piece.js +0 -115
  56. package/test/chessboard.test.js +0 -128
  57. /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
  58. /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
  59. /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
  60. /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
  61. /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
  62. /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
  63. /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
  64. /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
  65. /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
  66. /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
  67. /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
  68. /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
  69. /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
  70. /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
  71. /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
  72. /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
  73. /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
  74. /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
  75. /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
  76. /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
  77. /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
  78. /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
  79. /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
  80. /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
  81. /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
+ }