@alepot55/chessboardjs 2.2.1 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/README.md +125 -401
  2. package/assets/themes/alepot/theme.json +42 -0
  3. package/assets/themes/default/theme.json +42 -0
  4. package/dist/chessboard.cjs.js +11785 -0
  5. package/dist/chessboard.css +243 -0
  6. package/dist/chessboard.esm.js +11716 -0
  7. package/dist/chessboard.iife.js +11791 -0
  8. package/dist/chessboard.umd.js +11791 -0
  9. package/package.json +33 -3
  10. package/{chessboard.move.js → src/components/Move.js} +3 -3
  11. package/src/components/Piece.js +771 -0
  12. package/{chessboard.square.js → src/components/Square.js} +61 -8
  13. package/src/constants/index.js +15 -0
  14. package/src/constants/positions.js +62 -0
  15. package/src/core/Chessboard.js +2346 -0
  16. package/src/core/ChessboardConfig.js +707 -0
  17. package/src/core/ChessboardFactory.js +385 -0
  18. package/src/core/index.js +141 -0
  19. package/src/errors/ChessboardError.js +133 -0
  20. package/src/errors/index.js +15 -0
  21. package/src/errors/messages.js +189 -0
  22. package/src/index.js +103 -0
  23. package/src/services/AnimationService.js +180 -0
  24. package/src/services/BoardService.js +156 -0
  25. package/src/services/CoordinateService.js +355 -0
  26. package/src/services/EventService.js +955 -0
  27. package/src/services/MoveService.js +567 -0
  28. package/src/services/PieceService.js +339 -0
  29. package/src/services/PositionService.js +237 -0
  30. package/src/services/ValidationService.js +673 -0
  31. package/src/services/index.js +14 -0
  32. package/src/styles/animations.css +46 -0
  33. package/{chessboard.css → src/styles/board.css} +30 -7
  34. package/src/styles/index.css +4 -0
  35. package/src/styles/pieces.css +70 -0
  36. package/src/utils/animations.js +37 -0
  37. package/{chess.js → src/utils/chess.js} +16 -16
  38. package/src/utils/coordinates.js +62 -0
  39. package/src/utils/cross-browser.js +150 -0
  40. package/src/utils/logger.js +422 -0
  41. package/src/utils/performance.js +311 -0
  42. package/src/utils/validation.js +458 -0
  43. package/.babelrc +0 -4
  44. package/chessboard.bundle.js +0 -3422
  45. package/chessboard.config.js +0 -147
  46. package/chessboard.js +0 -979
  47. package/chessboard.piece.js +0 -115
  48. package/jest.config.js +0 -7
  49. package/rollup.config.js +0 -11
  50. package/test/chessboard.test.js +0 -128
  51. /package/{alepot_theme → assets/themes/alepot}/bb.svg +0 -0
  52. /package/{alepot_theme → assets/themes/alepot}/bw.svg +0 -0
  53. /package/{alepot_theme → assets/themes/alepot}/kb.svg +0 -0
  54. /package/{alepot_theme → assets/themes/alepot}/kw.svg +0 -0
  55. /package/{alepot_theme → assets/themes/alepot}/nb.svg +0 -0
  56. /package/{alepot_theme → assets/themes/alepot}/nw.svg +0 -0
  57. /package/{alepot_theme → assets/themes/alepot}/pb.svg +0 -0
  58. /package/{alepot_theme → assets/themes/alepot}/pw.svg +0 -0
  59. /package/{alepot_theme → assets/themes/alepot}/qb.svg +0 -0
  60. /package/{alepot_theme → assets/themes/alepot}/qw.svg +0 -0
  61. /package/{alepot_theme → assets/themes/alepot}/rb.svg +0 -0
  62. /package/{alepot_theme → assets/themes/alepot}/rw.svg +0 -0
  63. /package/{default_pieces → assets/themes/default}/bb.svg +0 -0
  64. /package/{default_pieces → assets/themes/default}/bw.svg +0 -0
  65. /package/{default_pieces → assets/themes/default}/kb.svg +0 -0
  66. /package/{default_pieces → assets/themes/default}/kw.svg +0 -0
  67. /package/{default_pieces → assets/themes/default}/nb.svg +0 -0
  68. /package/{default_pieces → assets/themes/default}/nw.svg +0 -0
  69. /package/{default_pieces → assets/themes/default}/pb.svg +0 -0
  70. /package/{default_pieces → assets/themes/default}/pw.svg +0 -0
  71. /package/{default_pieces → assets/themes/default}/qb.svg +0 -0
  72. /package/{default_pieces → assets/themes/default}/qw.svg +0 -0
  73. /package/{default_pieces → assets/themes/default}/rb.svg +0 -0
  74. /package/{default_pieces → assets/themes/default}/rw.svg +0 -0
@@ -0,0 +1,2346 @@
1
+ /**
2
+ * Main Chessboard class - Orchestrates all services and components
3
+ * @module core/Chessboard
4
+ * @since 2.0.0
5
+ */
6
+
7
+ import ChessboardConfig from './ChessboardConfig.js';
8
+ import Move from '../components/Move.js';
9
+ import {
10
+ AnimationService,
11
+ BoardService,
12
+ CoordinateService,
13
+ EventService,
14
+ MoveService,
15
+ PieceService,
16
+ PositionService,
17
+ ValidationService
18
+ } from '../services/index.js';
19
+ import { ERROR_MESSAGES } from '../errors/index.js';
20
+ import { ChessboardError, ValidationError, ConfigurationError } from '../errors/ChessboardError.js';
21
+ import { PerformanceMonitor } from '../utils/performance.js';
22
+
23
+ /**
24
+ * Main Chessboard class responsible for coordinating all services
25
+ * Implements the Facade pattern to provide a unified interface
26
+ * @class
27
+ */
28
+ class Chessboard {
29
+ /**
30
+ * Creates a new Chessboard instance
31
+ * @param {Object} config - Configuration object
32
+ * @throws {ConfigurationError} If configuration is invalid
33
+ */
34
+ constructor(config) {
35
+ try {
36
+ // Initialize performance monitoring
37
+ this._performanceMonitor = new PerformanceMonitor();
38
+ this._performanceMonitor.startMeasure('chessboard-initialization');
39
+
40
+ // Validate and initialize configuration
41
+ this._validateAndInitializeConfig(config);
42
+
43
+ // Initialize services
44
+ this._initializeServices();
45
+
46
+ // Initialize the board
47
+ this._initialize();
48
+
49
+ this._performanceMonitor.endMeasure('chessboard-initialization');
50
+ } catch (error) {
51
+ this._handleConstructorError(error);
52
+ }
53
+ this._undoneMoves = [];
54
+ this._destroyed = false;
55
+ this._animationTimeouts = [];
56
+ }
57
+
58
+ /**
59
+ * Validates and initializes configuration
60
+ * @private
61
+ * @param {Object} config - Raw configuration object
62
+ * @throws {ConfigurationError} If configuration is invalid
63
+ */
64
+ _validateAndInitializeConfig(config) {
65
+ if (!config || typeof config !== 'object') {
66
+ throw new ConfigurationError('Configuration must be an object', 'config', config);
67
+ }
68
+
69
+ this.config = new ChessboardConfig(config);
70
+
71
+ // Validate required configuration
72
+ if (!this.config.id_div) {
73
+ throw new ConfigurationError('Configuration must include id_div', 'id_div', this.config.id_div);
74
+ }
75
+ }
76
+
77
+ /**
78
+ * Handles constructor errors gracefully
79
+ * @private
80
+ * @param {Error} error - Error that occurred during construction
81
+ */
82
+ _handleConstructorError(error) {
83
+ console.error('Chessboard initialization failed:', error);
84
+
85
+ // Clean up any partially initialized resources
86
+ this._cleanup();
87
+
88
+ // Re-throw with additional context
89
+ if (error instanceof ChessboardError) {
90
+ throw error;
91
+ } else {
92
+ throw new ChessboardError('Failed to initialize chessboard', 'INITIALIZATION_ERROR', {
93
+ originalError: error.message,
94
+ stack: error.stack
95
+ });
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Cleans up any partially initialized resources (safe to call multiple times)
101
+ * @private
102
+ */
103
+ _cleanup() {
104
+ // Remove event listeners if present
105
+ if (this.eventService && typeof this.eventService.removeListeners === 'function') {
106
+ this.eventService.removeListeners();
107
+ }
108
+ // Clear timeouts
109
+ if (this._updateTimeout) {
110
+ clearTimeout(this._updateTimeout);
111
+ this._updateTimeout = null;
112
+ }
113
+ // Null all services
114
+ this.validationService = null;
115
+ this.coordinateService = null;
116
+ this.positionService = null;
117
+ this.boardService = null;
118
+ this.pieceService = null;
119
+ this.animationService = null;
120
+ this.moveService = null;
121
+ this.eventService = null;
122
+ }
123
+
124
+ /**
125
+ * Initializes all services
126
+ * @private
127
+ */
128
+ _initializeServices() {
129
+ // Core services
130
+ this.validationService = new ValidationService();
131
+ this.coordinateService = new CoordinateService(this.config);
132
+ this.positionService = new PositionService(this.config);
133
+ this.boardService = new BoardService(this.config);
134
+ this.pieceService = new PieceService(this.config);
135
+ this.animationService = new AnimationService(this.config);
136
+ this.moveService = new MoveService(this.config, this.positionService);
137
+ this.eventService = new EventService(
138
+ this.config,
139
+ this.boardService,
140
+ this.moveService,
141
+ this.coordinateService,
142
+ this
143
+ );
144
+
145
+ // State management
146
+ this._updateTimeout = null;
147
+ this._isAnimating = false;
148
+
149
+ // Bind methods to preserve context
150
+ this._boundUpdateBoardPieces = this._updateBoardPieces.bind(this);
151
+ this._boundOnSquareClick = this._onSquareClick.bind(this);
152
+ this._boundOnPieceHover = this._onPieceHover.bind(this);
153
+ this._boundOnPieceLeave = this._onPieceLeave.bind(this);
154
+ }
155
+
156
+ /**
157
+ * Initializes the board
158
+ * @private
159
+ */
160
+ _initialize() {
161
+ this._initParams();
162
+ this._setGame(this.config.position);
163
+ this._buildBoard();
164
+ this._buildSquares();
165
+ this._addListeners();
166
+ this._updateBoardPieces(true, true); // Initial position load
167
+
168
+ // Apply flipped class if initial orientation is black
169
+ if (this.coordinateService.getOrientation() === 'b' && this.boardService.element) {
170
+ this.boardService.element.classList.add('flipped');
171
+ }
172
+ }
173
+
174
+ /**
175
+ * Initializes parameters and state
176
+ * @private
177
+ */
178
+ _initParams() {
179
+ // Reset state
180
+ this.eventService.setClicked(null);
181
+ this.eventService.setPromoting(false);
182
+ this.eventService.setAnimating(false);
183
+ }
184
+
185
+ /**
186
+ * Sets up the game with initial position
187
+ * @private
188
+ * @param {string|Object} position - Initial position
189
+ */
190
+ _setGame(position) {
191
+ this.positionService.setGame(position);
192
+ }
193
+
194
+ /**
195
+ * Builds the board DOM structure
196
+ * @private
197
+ * Best practice: always remove squares (destroy JS/DOM) before clearing the board container.
198
+ */
199
+ _buildBoard() {
200
+ if (this._isUndoRedo) {
201
+ return;
202
+ }
203
+ // Forza la pulizia completa del contenitore board (DOM)
204
+ const boardContainer = document.getElementById(this.config.id_div);
205
+ if (boardContainer) boardContainer.innerHTML = '';
206
+ // Force remove all pieces from all squares (no animation, best practice)
207
+ if (this.boardService && this.boardService.squares) {
208
+ Object.values(this.boardService.squares).forEach(sq => sq && sq.forceRemoveAllPieces && sq.forceRemoveAllPieces());
209
+ }
210
+ if (this.boardService && this.boardService.removeSquares) this.boardService.removeSquares();
211
+ if (this.boardService && this.boardService.removeBoard) this.boardService.removeBoard();
212
+ this.boardService.buildBoard();
213
+ }
214
+
215
+ /**
216
+ * Builds all squares on the board
217
+ * @private
218
+ */
219
+ _buildSquares() {
220
+ if (this._isUndoRedo) {
221
+ return;
222
+ }
223
+ if (this.boardService && this.boardService.removeSquares) {
224
+ this.boardService.removeSquares();
225
+ }
226
+ this.boardService.buildSquares((row, col) => {
227
+ return this.coordinateService.realCoord(row, col);
228
+ });
229
+ }
230
+
231
+ /**
232
+ * Adds event listeners to squares
233
+ * @private
234
+ */
235
+ _addListeners() {
236
+ this.eventService.addListeners(
237
+ this._boundOnSquareClick,
238
+ this._boundOnPieceHover,
239
+ this._boundOnPieceLeave
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Handles square click events
245
+ * @private
246
+ * @param {Square} square - Clicked square
247
+ * @param {boolean} [animate=true] - Whether to animate
248
+ * @param {boolean} [dragged=false] - Whether triggered by drag
249
+ * @returns {boolean} True if successful
250
+ */
251
+ _onSquareClick(square, animate = true, dragged = false) {
252
+ return this.eventService.onClick(
253
+ square,
254
+ this._onMove.bind(this),
255
+ this._onSelect.bind(this),
256
+ this._onDeselect.bind(this),
257
+ animate,
258
+ dragged
259
+ );
260
+ }
261
+
262
+ /**
263
+ * Handles piece hover events
264
+ * @private
265
+ * @param {Square} square - Hovered square
266
+ */
267
+ _onPieceHover(square) {
268
+ if (this.config.hints && !this.eventService.getClicked()) {
269
+ // Only show hints if no square is selected
270
+ this._hintMoves(square);
271
+ }
272
+ }
273
+
274
+ /**
275
+ * Handles piece leave events
276
+ * @private
277
+ * @param {Square} square - Left square
278
+ */
279
+ _onPieceLeave(square) {
280
+ if (this.config.hints && !this.eventService.getClicked()) {
281
+ // Only remove hints if no square is selected
282
+ this._dehintMoves(square);
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Handles move execution
288
+ * @private
289
+ * @param {Square} fromSquare - Source square
290
+ * @param {Square} toSquare - Target square
291
+ * @param {string} [promotion] - Promotion piece
292
+ * @param {boolean} [animate=true] - Whether to animate
293
+ * @returns {boolean} True if move was successful
294
+ */
295
+ _onMove(fromSquare, toSquare, promotion = null, animate = true) {
296
+ const move = new Move(fromSquare, toSquare, promotion);
297
+
298
+ // 1. Validate the move
299
+ if (!move.check() || (this.config.onlyLegalMoves && !move.isLegal(this.positionService.getGame()))) {
300
+ this._clearVisualState(); // Always clear state on invalid move
301
+ return false;
302
+ }
303
+
304
+ // 2. Check for promotion (if not already provided)
305
+ if (!move.hasPromotion() && this._requiresPromotion(move)) {
306
+ // Promotion is required but not provided, let the promotion handler take over.
307
+ // Do not execute the move here.
308
+ return false;
309
+ }
310
+
311
+ // 3. Execute the move
312
+ // The onMove callback is for the user to approve the move, not to execute it.
313
+ if (this.config.onMove(move)) {
314
+ this._executeMove(move, animate);
315
+ return true;
316
+ }
317
+
318
+ // 4. If user rejects the move, clear state
319
+ this._clearVisualState();
320
+ return false;
321
+ }
322
+
323
+ /**
324
+ * Handles square selection
325
+ * @private
326
+ * @param {Square} square - Selected square
327
+ */
328
+ _onSelect(square) {
329
+ if (this.config.clickable) {
330
+ square.select();
331
+ this._hintMoves(square);
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Handles square deselection
337
+ * @private
338
+ * @param {Square} square - Deselected square
339
+ */
340
+ _onDeselect(square) {
341
+ this._clearVisualState();
342
+ }
343
+
344
+ /**
345
+ * Shows legal move hints for a square
346
+ * @private
347
+ * @param {Square} square - Square to show hints for
348
+ */
349
+ _hintMoves(square) {
350
+ if (!this.moveService.canMove(square)) return;
351
+
352
+ // Clear existing hints first
353
+ this.boardService.applyToAllSquares('removeHint');
354
+
355
+ const moves = this.moveService.getCachedLegalMoves(square);
356
+
357
+ for (const move of moves) {
358
+ if (move.to && move.to.length === 2) {
359
+ const targetSquare = this.boardService.getSquare(move.to);
360
+ if (targetSquare) {
361
+ const hasEnemyPiece = targetSquare.piece &&
362
+ targetSquare.piece.color !== this.positionService.getGame().turn();
363
+ targetSquare.putHint(hasEnemyPiece);
364
+ }
365
+ }
366
+ }
367
+ }
368
+
369
+ /**
370
+ * Removes legal move hints for a square
371
+ * @private
372
+ * @param {Square} square - Square to remove hints for
373
+ */
374
+ _dehintMoves(square) {
375
+ const moves = this.moveService.getCachedLegalMoves(square);
376
+
377
+ for (const move of moves) {
378
+ if (move.to && move.to.length === 2) {
379
+ const targetSquare = this.boardService.getSquare(move.to);
380
+ if (targetSquare) {
381
+ targetSquare.removeHint();
382
+ }
383
+ }
384
+ }
385
+ }
386
+
387
+ /**
388
+ * Checks if a move requires promotion
389
+ * @private
390
+ * @param {Move} move - Move to check
391
+ * @returns {boolean} True if promotion is required
392
+ */
393
+ _requiresPromotion(move) {
394
+ return this.moveService.requiresPromotion(move);
395
+ }
396
+
397
+ /**
398
+ * Executes a move
399
+ * @private
400
+ * @param {Move} move - Move to execute
401
+ * @param {boolean} [animate=true] - Whether to animate
402
+ */
403
+ _executeMove(move, animate = true) {
404
+ // Always clear visual state before executing a move to prevent artifacts
405
+ this._clearVisualState();
406
+
407
+ const game = this.positionService.getGame();
408
+ if (!game) {
409
+ throw new ChessboardError('Game not initialized', 'GAME_ERROR');
410
+ }
411
+
412
+ // Execute the move on the game engine
413
+ const gameMove = this.moveService.executeMove(move);
414
+ if (!gameMove) {
415
+ // This should not happen if validation passed, but as a safeguard:
416
+ console.error('Move execution failed unexpectedly for move:', move);
417
+ this._updateBoardPieces(false); // Sync board with game state
418
+ return;
419
+ }
420
+
421
+ // Clear previous move highlights
422
+ this.boardService.applyToAllSquares('unmoved');
423
+
424
+ // Mark squares as moved for styling
425
+ move.from.moved();
426
+ move.to.moved();
427
+
428
+ // Handle animations and special moves (castle, en-passant)
429
+ const isCastle = this.moveService.isCastle(gameMove);
430
+ const isEnPassant = this.moveService.isEnPassant(gameMove);
431
+
432
+ if (animate && move.from.piece) {
433
+ // For simultaneous castle, start rook animation alongside the king
434
+ const isSimultaneousCastle = isCastle && this.config.animationStyle === 'simultaneous';
435
+ if (isSimultaneousCastle) {
436
+ setTimeout(() => {
437
+ this._handleCastleMove(gameMove, true);
438
+ }, this.config.simultaneousAnimationDelay);
439
+ }
440
+
441
+ this.pieceService.translatePiece(
442
+ move,
443
+ !!move.to.piece, // was there a capture?
444
+ animate,
445
+ this._createDragFunction.bind(this),
446
+ () => {
447
+ // After the main piece animation completes...
448
+ // For sequential castle, animate rook AFTER king finishes
449
+ // For simultaneous, rook was already animated above - don't animate again
450
+ if (isCastle && !isSimultaneousCastle) {
451
+ this._handleSpecialMoveAnimation(gameMove);
452
+ } else if (isEnPassant) {
453
+ this._handleSpecialMoveAnimation(gameMove);
454
+ }
455
+ // Notify user that the move is fully complete
456
+ this.config.onMoveEnd(gameMove);
457
+ // For simultaneous castle, the rook callback will handle the final sync
458
+ // to avoid interfering with the ongoing rook animation
459
+ if (!isSimultaneousCastle) {
460
+ this._updateBoardPieces(false);
461
+ }
462
+ }
463
+ );
464
+ } else {
465
+ // If not animating, handle special moves immediately and update the board
466
+ if (isCastle) {
467
+ this._handleSpecialMove(gameMove);
468
+ } else if (isEnPassant) {
469
+ this._handleSpecialMove(gameMove);
470
+ }
471
+ this._updateBoardPieces(false);
472
+ this.config.onMoveEnd(gameMove);
473
+ }
474
+ }
475
+
476
+ /**
477
+ * Handles special moves (castle, en passant) without animation
478
+ * @private
479
+ * @param {Object} gameMove - Game move object
480
+ */
481
+ _handleSpecialMove(gameMove) {
482
+ if (this.moveService.isCastle(gameMove)) {
483
+ this._handleCastleMove(gameMove, false);
484
+ } else if (this.moveService.isEnPassant(gameMove)) {
485
+ this._handleEnPassantMove(gameMove, false);
486
+ }
487
+ }
488
+
489
+ /**
490
+ * Handles special moves (castle, en passant) with animation
491
+ * @private
492
+ * @param {Object} gameMove - Game move object
493
+ */
494
+ _handleSpecialMoveAnimation(gameMove) {
495
+ if (this.moveService.isCastle(gameMove)) {
496
+ this._handleCastleMove(gameMove, true);
497
+ } else if (this.moveService.isEnPassant(gameMove)) {
498
+ this._handleEnPassantMove(gameMove, true);
499
+ }
500
+ }
501
+
502
+ /**
503
+ * Handles castle move by moving the rook
504
+ * @private
505
+ * @param {Object} gameMove - Game move object
506
+ * @param {boolean} animate - Whether to animate
507
+ */
508
+ _handleCastleMove(gameMove, animate) {
509
+ const rookMove = this.moveService.getCastleRookMove(gameMove);
510
+ if (!rookMove) return;
511
+
512
+ const rookFromSquare = this.boardService.getSquare(rookMove.from);
513
+ const rookToSquare = this.boardService.getSquare(rookMove.to);
514
+
515
+ if (!rookFromSquare || !rookToSquare || !rookFromSquare.piece) {
516
+ return;
517
+ }
518
+
519
+ if (animate) {
520
+ // Always use translatePiece for smooth sliding animation
521
+ const rookPiece = rookFromSquare.piece;
522
+ this.pieceService.translatePiece(
523
+ { from: rookFromSquare, to: rookToSquare, piece: rookPiece },
524
+ false, // No capture for rook in castle
525
+ animate,
526
+ this._createDragFunction.bind(this),
527
+ () => {
528
+ // After rook animation, update board state
529
+ this._updateBoardPieces(false);
530
+ }
531
+ );
532
+ } else {
533
+ // Just update the board state
534
+ this._updateBoardPieces(false);
535
+ }
536
+ }
537
+
538
+ /**
539
+ * Handles en passant move by removing the captured pawn
540
+ * @private
541
+ * @param {Object} gameMove - Game move object
542
+ * @param {boolean} animate - Whether to animate
543
+ */
544
+ _handleEnPassantMove(gameMove, animate) {
545
+ const capturedSquare = this.moveService.getEnPassantCapturedSquare(gameMove);
546
+ if (!capturedSquare) return;
547
+
548
+ const capturedSquareObj = this.boardService.getSquare(capturedSquare);
549
+ if (!capturedSquareObj || !capturedSquareObj.piece) {
550
+ return;
551
+ }
552
+
553
+ if (animate) {
554
+ // Animate the captured pawn removal
555
+ this.pieceService.removePieceFromSquare(capturedSquareObj, true);
556
+ // Update board state after animation
557
+ setTimeout(() => {
558
+ this._updateBoardPieces(false);
559
+ }, this.config.moveTime);
560
+ } else {
561
+ // Just update the board state
562
+ this._updateBoardPieces(false);
563
+ }
564
+ }
565
+
566
+ /**
567
+ * Updates board pieces to match game state
568
+ * @private
569
+ * @param {boolean} [animation=false] - Whether to animate
570
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load
571
+ */
572
+ _updateBoardPieces(animation = false, isPositionLoad = false) {
573
+ if (this._destroyed) return;
574
+ // Check if services are available
575
+ if (!this.positionService || !this.moveService || !this.eventService) {
576
+ return;
577
+ }
578
+
579
+ // Clear any pending update
580
+ if (this._updateTimeout) {
581
+ clearTimeout(this._updateTimeout);
582
+ this._updateTimeout = null;
583
+ }
584
+
585
+ // Clear moves cache
586
+ this.moveService.clearCache();
587
+
588
+ // Add small delay for click-to-move to avoid lag
589
+ if (animation && !this.eventService.getClicked()) {
590
+ this._updateTimeout = setTimeout(() => {
591
+ this._doUpdateBoardPieces(animation, isPositionLoad);
592
+ this._updateTimeout = null;
593
+
594
+ // Ensure hints are available for the next turn
595
+ this._ensureHintsAvailable();
596
+ }, 10);
597
+ } else {
598
+ this._doUpdateBoardPieces(animation, isPositionLoad);
599
+
600
+ // Ensure hints are available for the next turn
601
+ this._ensureHintsAvailable();
602
+ }
603
+ }
604
+
605
+ /**
606
+ * Ensures hints are available for the current turn
607
+ * @private
608
+ */
609
+ _ensureHintsAvailable() {
610
+ if (!this.config.hints) return;
611
+
612
+ // Small delay to ensure the board state is fully updated
613
+ setTimeout(() => {
614
+ // Clear any existing hints
615
+ this.boardService.applyToAllSquares('removeHint');
616
+
617
+ // The hints will be shown when the user hovers over pieces
618
+ // This just ensures the cache is ready
619
+ this.moveService.clearCache();
620
+ }, 50);
621
+ }
622
+
623
+ /**
624
+ * Updates board pieces after a delayed move
625
+ * @private
626
+ */
627
+ _updateBoardPiecesDelayed() {
628
+ this._updateBoardPieces(false);
629
+ }
630
+
631
+ /**
632
+ * Performs the actual board update
633
+ * @private
634
+ * @param {boolean} [animation=false] - Whether to animate
635
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load (affects delay)
636
+ */
637
+ _doUpdateBoardPieces(animation = false, isPositionLoad = false) {
638
+ if (this._destroyed) return;
639
+ // Skip update if we're in the middle of a promotion
640
+ if (this._isPromoting) {
641
+ return;
642
+ }
643
+
644
+ // Check if services are available
645
+ if (!this.positionService || !this.positionService.getGame()) {
646
+ return;
647
+ }
648
+
649
+ const squares = this.boardService.getAllSquares();
650
+ const gameStateBefore = this.positionService.getGame().fen();
651
+ const useSimultaneous = this.config.animationStyle === 'simultaneous';
652
+
653
+ if (useSimultaneous) {
654
+ this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad, animation);
655
+ } else {
656
+ this._doSequentialUpdate(squares, gameStateBefore, animation);
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Performs sequential piece updates (original behavior)
662
+ * @private
663
+ * @param {Object} squares - All squares
664
+ * @param {string} gameStateBefore - Game state before update
665
+ * @param {boolean} animation - Whether to animate
666
+ */
667
+ _doSequentialUpdate(squares, gameStateBefore, animation) {
668
+ // Cancel running animations and clean orphaned elements
669
+ Object.values(squares).forEach(square => {
670
+ const imgs = square.element.querySelectorAll('img.piece');
671
+ imgs.forEach(img => {
672
+ if (img.getAnimations) {
673
+ img.getAnimations().forEach(anim => anim.cancel());
674
+ }
675
+ if (!square.piece || img !== square.piece.element) {
676
+ img.remove();
677
+ }
678
+ });
679
+ if (square.piece && square.piece.element) {
680
+ square.piece.element.style = '';
681
+ square.piece.element.style.opacity = '1';
682
+ }
683
+ });
684
+
685
+ const expectedMap = {};
686
+ Object.values(squares).forEach(square => {
687
+ expectedMap[square.id] = this.positionService.getGamePieceId(square.id);
688
+ });
689
+
690
+ Object.values(squares).forEach(square => {
691
+ const expectedPieceId = expectedMap[square.id];
692
+ const currentPiece = square.piece;
693
+ const currentPieceId = currentPiece ? currentPiece.getId() : null;
694
+
695
+ // Se il pezzo attuale e quello atteso sono identici, non fare nulla
696
+ if (currentPieceId === expectedPieceId) {
697
+ return;
698
+ }
699
+
700
+ // Remove current piece if it doesn't match expected
701
+ if (currentPiece && currentPieceId !== expectedPieceId) {
702
+ // Always remove synchronously to avoid race condition with addition
703
+ this.pieceService.removePieceFromSquare(square, false);
704
+ }
705
+
706
+ // Add expected piece if it doesn't match current
707
+ if (expectedPieceId && currentPieceId !== expectedPieceId) {
708
+ const newPiece = this.pieceService.convertPiece(expectedPieceId);
709
+ this.pieceService.addPieceOnSquare(
710
+ square,
711
+ newPiece,
712
+ animation,
713
+ this._createDragFunction.bind(this)
714
+ );
715
+ }
716
+ });
717
+
718
+ this._addListeners();
719
+ const gameStateAfter = this.positionService.getGame().fen();
720
+ if (gameStateBefore !== gameStateAfter) {
721
+ this.config.onChange(gameStateAfter);
722
+ }
723
+ }
724
+
725
+ /**
726
+ * Performs simultaneous piece updates
727
+ * @private
728
+ * @param {Object} squares - All squares
729
+ * @param {string} gameStateBefore - Game state before update
730
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load
731
+ * @param {boolean} [animation=true] - Whether to animate
732
+ */
733
+ _doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false, animation = true) {
734
+ // Increment generation to invalidate stale animation callbacks
735
+ this._updateGeneration = (this._updateGeneration || 0) + 1;
736
+ const generation = this._updateGeneration;
737
+
738
+ // Cancel pending animation timeouts from previous update
739
+ if (this._animationTimeouts) {
740
+ this._animationTimeouts.forEach(tid => clearTimeout(tid));
741
+ this._animationTimeouts = [];
742
+ }
743
+
744
+ // Cancel all running animations and force-sync DOM state
745
+ Object.values(squares).forEach(square => {
746
+ const imgs = square.element.querySelectorAll('img.piece');
747
+ imgs.forEach(img => {
748
+ // Cancel all Web Animations on this element so onfinish callbacks don't fire
749
+ if (img.getAnimations) {
750
+ img.getAnimations().forEach(anim => anim.cancel());
751
+ }
752
+ // Remove orphaned images not matching current piece
753
+ if (!square.piece || img !== square.piece.element) {
754
+ img.remove();
755
+ }
756
+ });
757
+ // Reset current piece element to clean state (remove animation artifacts)
758
+ if (square.piece && square.piece.element) {
759
+ square.piece.element.style = '';
760
+ square.piece.element.style.opacity = '1';
761
+ // Ensure element is attached to correct square
762
+ if (!square.element.contains(square.piece.element)) {
763
+ square.element.appendChild(square.piece.element);
764
+ }
765
+ }
766
+ });
767
+
768
+ const currentMap = {};
769
+ const expectedMap = {};
770
+
771
+ Object.values(squares).forEach(square => {
772
+ const currentPiece = square.piece;
773
+ const expectedPieceId = this.positionService.getGamePieceId(square.id);
774
+ if (currentPiece) {
775
+ // Normalizza la chiave come 'color+type' lowercase
776
+ const key = (currentPiece.color + currentPiece.type).toLowerCase();
777
+ if (!currentMap[key]) currentMap[key] = [];
778
+ currentMap[key].push({ square, id: square.id });
779
+ }
780
+ if (expectedPieceId) {
781
+ // Normalizza la chiave come 'color+type' lowercase
782
+ const key = expectedPieceId.toLowerCase();
783
+ if (!expectedMap[key]) expectedMap[key] = [];
784
+ expectedMap[key].push({ square, id: square.id });
785
+ }
786
+ });
787
+
788
+ let animationsCompleted = 0;
789
+ let totalAnimations = 0;
790
+ const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
791
+ let animationIndex = 0;
792
+
793
+ // First pass: compute matching for all piece types
794
+ const allRemovals = [];
795
+ const allAdditions = [];
796
+ const allMoves = [];
797
+
798
+ Object.keys(expectedMap).forEach(key => {
799
+ const fromList = (currentMap[key] || []).slice();
800
+ const toList = expectedMap[key].slice();
801
+
802
+ // Build distance matrix
803
+ const distances = [];
804
+ for (let i = 0; i < fromList.length; i++) {
805
+ distances[i] = [];
806
+ for (let j = 0; j < toList.length; j++) {
807
+ distances[i][j] = Math.abs(fromList[i].square.row - toList[j].square.row) +
808
+ Math.abs(fromList[i].square.col - toList[j].square.col);
809
+ }
810
+ }
811
+
812
+ // Greedy matching: pair closest pieces
813
+ const fromMatched = new Array(fromList.length).fill(false);
814
+ const toMatched = new Array(toList.length).fill(false);
815
+
816
+ while (true) {
817
+ let minDist = Infinity, minI = -1, minJ = -1;
818
+ for (let i = 0; i < fromList.length; i++) {
819
+ if (fromMatched[i]) continue;
820
+ for (let j = 0; j < toList.length; j++) {
821
+ if (toMatched[j]) continue;
822
+ if (distances[i][j] < minDist) {
823
+ minDist = distances[i][j];
824
+ minI = i;
825
+ minJ = j;
826
+ }
827
+ }
828
+ }
829
+ if (minI === -1 || minJ === -1) break;
830
+ fromMatched[minI] = true;
831
+ toMatched[minJ] = true;
832
+ // Skip unchanged pieces (same square)
833
+ if (fromList[minI].square === toList[minJ].square) {
834
+ continue;
835
+ }
836
+ allMoves.push({ from: fromList[minI].square, to: toList[minJ].square, piece: fromList[minI].square.piece });
837
+ }
838
+
839
+ // Collect unmatched current pieces (to remove)
840
+ for (let i = 0; i < fromList.length; i++) {
841
+ if (!fromMatched[i]) {
842
+ allRemovals.push(fromList[i].square);
843
+ }
844
+ }
845
+
846
+ // Collect unmatched expected pieces (to add)
847
+ for (let j = 0; j < toList.length; j++) {
848
+ if (!toMatched[j]) {
849
+ allAdditions.push({ square: toList[j].square, key });
850
+ }
851
+ }
852
+ });
853
+
854
+ // Also count removals for pieces whose type doesn't exist in expectedMap
855
+ Object.keys(currentMap).forEach(key => {
856
+ if (!expectedMap[key]) {
857
+ currentMap[key].forEach(entry => {
858
+ allRemovals.push(entry.square);
859
+ });
860
+ }
861
+ });
862
+
863
+ // Count only actual animations
864
+ totalAnimations = allRemovals.length + allAdditions.length + allMoves.length;
865
+
866
+ if (totalAnimations === 0) {
867
+ this._addListeners();
868
+ const gameStateAfter = this.positionService.getGame().fen();
869
+ if (gameStateBefore !== gameStateAfter) {
870
+ this.config.onChange(gameStateAfter);
871
+ }
872
+ return;
873
+ }
874
+
875
+ // Detach moving pieces from source squares BEFORE any removals/additions
876
+ // This prevents additions to a move's source square from destroying the piece
877
+ allMoves.forEach(move => {
878
+ if (move.from.piece === move.piece) {
879
+ move.from.removePiece(true); // preserve element, just detach reference
880
+ }
881
+ });
882
+
883
+ // No animation: apply all changes synchronously
884
+ if (!animation) {
885
+ allRemovals.forEach(square => {
886
+ this.pieceService.removePieceFromSquare(square, false);
887
+ });
888
+ allMoves.forEach(move => {
889
+ this.pieceService.translatePiece(
890
+ move, false, false, this._createDragFunction.bind(this)
891
+ );
892
+ });
893
+ allAdditions.forEach(({ square, key }) => {
894
+ const newPiece = this.pieceService.convertPiece(key);
895
+ this.pieceService.addPieceOnSquare(
896
+ square, newPiece, false, this._createDragFunction.bind(this)
897
+ );
898
+ });
899
+ this._addListeners();
900
+ const gameStateAfter = this.positionService.getGame().fen();
901
+ if (gameStateBefore !== gameStateAfter) {
902
+ this.config.onChange(gameStateAfter);
903
+ }
904
+ return;
905
+ }
906
+
907
+ // Animated path
908
+ if (!this._animationTimeouts) this._animationTimeouts = [];
909
+
910
+ const onAnimationComplete = () => {
911
+ // Ignore callbacks from stale/destroyed boards
912
+ if (this._destroyed || this._updateGeneration !== generation) return;
913
+ animationsCompleted++;
914
+ if (animationsCompleted === totalAnimations) {
915
+ this._addListeners();
916
+ const gameStateAfter = this.positionService.getGame().fen();
917
+ if (gameStateBefore !== gameStateAfter) {
918
+ this.config.onChange(gameStateAfter);
919
+ }
920
+ }
921
+ };
922
+
923
+ // Dispatch moves first (pieces already detached from source)
924
+ allMoves.forEach(move => {
925
+ const tid = setTimeout(() => {
926
+ if (this._destroyed || this._updateGeneration !== generation) return;
927
+ this.pieceService.translatePiece(
928
+ move,
929
+ false,
930
+ true,
931
+ this._createDragFunction.bind(this),
932
+ onAnimationComplete
933
+ );
934
+ }, animationIndex * animationDelay);
935
+ this._animationTimeouts.push(tid);
936
+ animationIndex++;
937
+ });
938
+
939
+ // Dispatch removals
940
+ allRemovals.forEach(square => {
941
+ const tid = setTimeout(() => {
942
+ if (this._destroyed || this._updateGeneration !== generation) return;
943
+ this.pieceService.removePieceFromSquare(square, true, onAnimationComplete);
944
+ }, animationIndex * animationDelay);
945
+ this._animationTimeouts.push(tid);
946
+ animationIndex++;
947
+ });
948
+
949
+ // Dispatch additions
950
+ allAdditions.forEach(({ square, key }) => {
951
+ const tid = setTimeout(() => {
952
+ if (this._destroyed || this._updateGeneration !== generation) return;
953
+ const newPiece = this.pieceService.convertPiece(key);
954
+ this.pieceService.addPieceOnSquare(
955
+ square,
956
+ newPiece,
957
+ true,
958
+ this._createDragFunction.bind(this),
959
+ onAnimationComplete
960
+ );
961
+ }, animationIndex * animationDelay);
962
+ this._animationTimeouts.push(tid);
963
+ animationIndex++;
964
+ });
965
+ }
966
+
967
+ /**
968
+ * Analyzes position changes to determine optimal animation strategy
969
+ * @private
970
+ * @param {Object} squares - All squares
971
+ * @returns {Object} Analysis of changes
972
+ */
973
+ _analyzePositionChanges(squares) {
974
+ const currentPieces = new Map();
975
+ const expectedPieces = new Map();
976
+
977
+ // Map current and expected piece positions
978
+ Object.values(squares).forEach(square => {
979
+ const currentPiece = square.piece;
980
+ const expectedPieceId = this.positionService.getGamePieceId(square.id);
981
+
982
+ if (currentPiece) {
983
+ currentPieces.set(square.id, currentPiece.getId());
984
+ }
985
+
986
+ if (expectedPieceId) {
987
+ expectedPieces.set(square.id, expectedPieceId);
988
+ }
989
+ });
990
+
991
+ // Identify different types of changes
992
+ const moves = []; // Pieces that can slide to new positions
993
+ const removes = []; // Pieces that need to be removed
994
+ const adds = []; // Pieces that need to be added
995
+ const unchanged = []; // Pieces that stay in place
996
+
997
+ // First pass: identify pieces that don't need to move (same piece type on same square)
998
+ const processedSquares = new Set();
999
+
1000
+ currentPieces.forEach((currentPieceId, square) => {
1001
+ const expectedPieceId = expectedPieces.get(square);
1002
+
1003
+ if (currentPieceId === expectedPieceId) {
1004
+ unchanged.push({
1005
+ piece: currentPieceId,
1006
+ square: square
1007
+ });
1008
+ processedSquares.add(square);
1009
+ }
1010
+ });
1011
+
1012
+ // Second pass: handle pieces that need to move or be removed
1013
+ currentPieces.forEach((currentPieceId, fromSquare) => {
1014
+ if (processedSquares.has(fromSquare)) {
1015
+ return; // Already processed as unchanged
1016
+ }
1017
+
1018
+ // Try to find a destination for this piece
1019
+ const availableDestination = Array.from(expectedPieces.entries()).find(([toSquare, expectedId]) =>
1020
+ expectedId === currentPieceId && !processedSquares.has(toSquare)
1021
+ );
1022
+
1023
+ if (availableDestination) {
1024
+ const [toSquare, expectedId] = availableDestination;
1025
+ moves.push({
1026
+ piece: currentPieceId,
1027
+ from: fromSquare,
1028
+ to: toSquare,
1029
+ fromSquare: squares[fromSquare],
1030
+ toSquare: squares[toSquare]
1031
+ });
1032
+ processedSquares.add(toSquare);
1033
+ } else {
1034
+ removes.push({
1035
+ piece: currentPieceId,
1036
+ square: fromSquare,
1037
+ squareObj: squares[fromSquare]
1038
+ });
1039
+ }
1040
+ });
1041
+
1042
+ // Third pass: handle pieces that need to be added
1043
+ expectedPieces.forEach((expectedPieceId, toSquare) => {
1044
+ if (!processedSquares.has(toSquare)) {
1045
+ adds.push({
1046
+ piece: expectedPieceId,
1047
+ square: toSquare,
1048
+ squareObj: squares[toSquare]
1049
+ });
1050
+ }
1051
+ });
1052
+
1053
+ return {
1054
+ moves,
1055
+ removes,
1056
+ adds,
1057
+ unchanged,
1058
+ totalChanges: moves.length + removes.length + adds.length
1059
+ };
1060
+ }
1061
+
1062
+ /**
1063
+ * Executes simultaneous changes based on analysis
1064
+ * @private
1065
+ * @param {Object} changeAnalysis - Analysis of changes
1066
+ * @param {string} gameStateBefore - Game state before update
1067
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load
1068
+ */
1069
+ _executeSimultaneousChanges(changeAnalysis, gameStateBefore, isPositionLoad = false) {
1070
+ const { moves, removes, adds } = changeAnalysis;
1071
+
1072
+ let animationsCompleted = 0;
1073
+ const totalAnimations = moves.length + removes.length + adds.length;
1074
+
1075
+ // If no animations are needed, complete immediately
1076
+ if (totalAnimations === 0) {
1077
+ this._addListeners();
1078
+
1079
+ // Trigger change event if position changed
1080
+ const gameStateAfter = this.positionService.getGame().fen();
1081
+ if (gameStateBefore !== gameStateAfter) {
1082
+ this.config.onChange(gameStateAfter);
1083
+ }
1084
+ return;
1085
+ }
1086
+
1087
+ const onAnimationComplete = () => {
1088
+ animationsCompleted++;
1089
+ if (animationsCompleted === totalAnimations) {
1090
+ this._addListeners();
1091
+
1092
+ // Trigger change event if position changed
1093
+ const gameStateAfter = this.positionService.getGame().fen();
1094
+ if (gameStateBefore !== gameStateAfter) {
1095
+ this.config.onChange(gameStateAfter);
1096
+ }
1097
+ }
1098
+ };
1099
+
1100
+ const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
1101
+ let animationIndex = 0;
1102
+
1103
+ // Process moves
1104
+ moves.forEach(move => {
1105
+ const delay = animationIndex * animationDelay;
1106
+ setTimeout(() => {
1107
+ this._animatePieceMove(move, onAnimationComplete);
1108
+ }, delay);
1109
+ animationIndex++;
1110
+ });
1111
+
1112
+ // Process removes
1113
+ removes.forEach(remove => {
1114
+ const delay = animationIndex * animationDelay;
1115
+ setTimeout(() => {
1116
+ this._animatePieceRemoval(remove, onAnimationComplete);
1117
+ }, delay);
1118
+ animationIndex++;
1119
+ });
1120
+
1121
+ // Process adds
1122
+ adds.forEach(add => {
1123
+ const delay = animationIndex * animationDelay;
1124
+ setTimeout(() => {
1125
+ this._animatePieceAddition(add, onAnimationComplete);
1126
+ }, delay);
1127
+ animationIndex++;
1128
+ });
1129
+ }
1130
+
1131
+ /**
1132
+ * Animates a piece moving from one square to another
1133
+ * @private
1134
+ * @param {Object} move - Move information
1135
+ * @param {Function} onComplete - Callback when animation completes
1136
+ */
1137
+ _animatePieceMove(move, onComplete) {
1138
+ const { fromSquare, toSquare } = move;
1139
+ const piece = fromSquare.piece;
1140
+
1141
+ if (!piece) {
1142
+ onComplete();
1143
+ return;
1144
+ }
1145
+
1146
+ this.pieceService.translatePiece(
1147
+ { from: fromSquare, to: toSquare, piece: piece },
1148
+ false,
1149
+ true,
1150
+ this._createDragFunction.bind(this),
1151
+ onComplete
1152
+ );
1153
+ }
1154
+
1155
+ /**
1156
+ * Animates a piece being removed
1157
+ * @private
1158
+ * @param {Object} remove - Remove information
1159
+ * @param {Function} onComplete - Callback when animation completes
1160
+ */
1161
+ _animatePieceRemoval(remove, onComplete) {
1162
+ this.pieceService.removePieceFromSquare(remove.squareObj, true, onComplete);
1163
+ }
1164
+
1165
+ /**
1166
+ * Animates a piece being added
1167
+ * @private
1168
+ * @param {Object} add - Add information
1169
+ * @param {Function} onComplete - Callback when animation completes
1170
+ */
1171
+ _animatePieceAddition(add, onComplete) {
1172
+ const newPiece = this.pieceService.convertPiece(add.piece);
1173
+ this.pieceService.addPieceOnSquare(
1174
+ add.squareObj,
1175
+ newPiece,
1176
+ true,
1177
+ this._createDragFunction.bind(this),
1178
+ onComplete
1179
+ );
1180
+ }
1181
+
1182
+ /**
1183
+ * Creates a drag function for a piece
1184
+ * @private
1185
+ * @param {Square} square - Square containing the piece
1186
+ * @param {Piece} piece - Piece to create drag function for
1187
+ * @returns {Function} Drag function
1188
+ */
1189
+ _createDragFunction(square, piece) {
1190
+ return this.eventService.createDragFunction(
1191
+ square,
1192
+ piece,
1193
+ this.config.onDragStart,
1194
+ this.config.onDragMove,
1195
+ this.config.onDrop,
1196
+ this._onSnapback.bind(this),
1197
+ this._onMove.bind(this),
1198
+ this._onRemove.bind(this)
1199
+ );
1200
+ }
1201
+
1202
+ /**
1203
+ * Handles snapback animation
1204
+ * @private
1205
+ * @param {Square} square - Square containing the piece
1206
+ * @param {Piece} piece - Piece to snapback
1207
+ */
1208
+ _onSnapback(square, piece) {
1209
+ this.pieceService.snapbackPiece(square, this.config.snapbackAnimation);
1210
+ this.config.onSnapbackEnd(square, piece);
1211
+ }
1212
+
1213
+ /**
1214
+ * Handles piece removal
1215
+ * @private
1216
+ * @param {Square} square - Square containing the piece to remove
1217
+ */
1218
+ _onRemove(square) {
1219
+ this.pieceService.removePieceFromSquare(square, true);
1220
+ this.positionService.getGame().remove(square.id);
1221
+ this._updateBoardPieces(true);
1222
+ }
1223
+
1224
+ /**
1225
+ * Clears all visual state (selections, hints, highlights)
1226
+ * @private
1227
+ */
1228
+ _clearVisualState() {
1229
+ this.boardService.applyToAllSquares('deselect');
1230
+ this.boardService.applyToAllSquares('removeHint');
1231
+ this.boardService.applyToAllSquares('dehighlight');
1232
+ this.eventService.setClicked(null);
1233
+ }
1234
+
1235
+ // -------------------
1236
+ // Public API Methods (Refactored)
1237
+ // -------------------
1238
+
1239
+ // --- POSITION & STATE ---
1240
+ /**
1241
+ * Get the current position as FEN
1242
+ * @returns {string}
1243
+ */
1244
+ getPosition() { return this.fen(); }
1245
+ /**
1246
+ * Set the board position (FEN or object)
1247
+ * @param {string|Object} position
1248
+ * @param {Object} [opts]
1249
+ * @param {boolean} [opts.animate=true]
1250
+ * @returns {boolean}
1251
+ */
1252
+ setPosition(position, opts = {}) {
1253
+ const animate = opts.animate !== undefined ? opts.animate : true;
1254
+ // Remove highlights and selections
1255
+ if (this.boardService && this.boardService.applyToAllSquares) {
1256
+ this.boardService.applyToAllSquares('removeHint');
1257
+ this.boardService.applyToAllSquares('deselect');
1258
+ this.boardService.applyToAllSquares('unmoved');
1259
+ }
1260
+ if (this.positionService && this.positionService.setGame) {
1261
+ this.positionService.setGame(position);
1262
+ }
1263
+ if (this._updateBoardPieces) {
1264
+ this._updateBoardPieces(animate, true);
1265
+ }
1266
+ return true;
1267
+ }
1268
+ /**
1269
+ * Reset the board to the starting position
1270
+ * @param {Object} [opts]
1271
+ * @param {boolean} [opts.animate=true]
1272
+ * @returns {boolean}
1273
+ */
1274
+ reset(opts = {}) {
1275
+ const animate = opts.animate !== undefined ? opts.animate : true;
1276
+ // Use the default starting position from config or fallback
1277
+ const startPosition = this.config && this.config.position ? this.config.position : 'start';
1278
+ // setPosition already calls _updateBoardPieces, don't call it twice
1279
+ return this.setPosition(startPosition, { animate });
1280
+ }
1281
+ /**
1282
+ * Clear the board
1283
+ * @param {Object} [opts]
1284
+ * @param {boolean} [opts.animate=true]
1285
+ * @returns {boolean}
1286
+ */
1287
+ clear(opts = {}) {
1288
+ const animate = opts.animate !== undefined ? opts.animate : true;
1289
+ if (!this.positionService || !this.positionService.getGame()) {
1290
+ return false;
1291
+ }
1292
+ if (this._clearVisualState) this._clearVisualState();
1293
+
1294
+ // Clear the game state
1295
+ this.positionService.getGame().clear();
1296
+
1297
+ // Let _updateBoardPieces handle removal (no manual loop to avoid race conditions)
1298
+ this._updateBoardPieces(animate, true);
1299
+
1300
+ return true;
1301
+ }
1302
+
1303
+ // --- MOVE MANAGEMENT ---
1304
+ /**
1305
+ * Undo last move
1306
+ * @param {Object} [opts]
1307
+ * @param {boolean} [opts.animate=true]
1308
+ * @returns {boolean}
1309
+ */
1310
+ undoMove(opts = {}) {
1311
+ const undone = this.positionService.getGame().undo();
1312
+ if (undone) {
1313
+ this._undoneMoves.push(undone);
1314
+ this._updateBoardPieces(opts.animate !== false);
1315
+ return undone;
1316
+ }
1317
+ return null;
1318
+ }
1319
+ /**
1320
+ * Redo last undone move
1321
+ * @param {Object} [opts]
1322
+ * @param {boolean} [opts.animate=true]
1323
+ * @returns {boolean}
1324
+ */
1325
+ redoMove(opts = {}) {
1326
+ if (this._undoneMoves && this._undoneMoves.length > 0) {
1327
+ const move = this._undoneMoves.pop();
1328
+ const moveObj = { from: move.from, to: move.to };
1329
+ if (move.promotion) moveObj.promotion = move.promotion;
1330
+ const result = this.positionService.getGame().move(moveObj);
1331
+ this._updateBoardPieces(opts.animate !== false);
1332
+ return result;
1333
+ }
1334
+ return false;
1335
+ }
1336
+ /**
1337
+ * Move a piece from one square to another
1338
+ * @param {string} moveStr - Move in format 'e2e4' or 'e7e8q' (with promotion)
1339
+ * @param {Object} [opts]
1340
+ * @param {boolean} [opts.animate=true]
1341
+ * @returns {Object|boolean} Move result or false if invalid
1342
+ */
1343
+ movePiece(moveStr, opts = {}) {
1344
+ const animate = opts.animate !== false;
1345
+ if (typeof moveStr !== 'string' || moveStr.length < 4) {
1346
+ return false;
1347
+ }
1348
+ const from = moveStr.slice(0, 2);
1349
+ const to = moveStr.slice(2, 4);
1350
+ const promotion = moveStr.length > 4 ? moveStr[4].toLowerCase() : undefined;
1351
+
1352
+ const moveObj = { from, to };
1353
+ if (promotion) moveObj.promotion = promotion;
1354
+
1355
+ const result = this.positionService.getGame().move(moveObj);
1356
+ if (result) {
1357
+ this._updateBoardPieces(animate);
1358
+ }
1359
+ return result || false;
1360
+ }
1361
+
1362
+ /**
1363
+ * Get legal moves for a square
1364
+ * @param {string} square
1365
+ * @returns {Array}
1366
+ */
1367
+ getLegalMoves(square) { return this.legalMoves(square); }
1368
+
1369
+ // --- PIECE MANAGEMENT ---
1370
+ /**
1371
+ * Get the piece at a square
1372
+ * @param {string} square
1373
+ * @returns {string|null}
1374
+ */
1375
+ getPiece(square) {
1376
+ // Use game state as source of truth
1377
+ // Returns piece in format 'wq' (color + type)
1378
+ if (!this.positionService || !this.positionService.getGame()) return null;
1379
+ const piece = this.positionService.getGame().get(square);
1380
+ if (!piece) return null;
1381
+ return (piece.color + piece.type).toLowerCase();
1382
+ }
1383
+ /**
1384
+ * Put a piece on a square
1385
+ * @param {string} piece
1386
+ * @param {string} square
1387
+ * @param {Object} [opts]
1388
+ * @param {boolean} [opts.animate=true]
1389
+ * @returns {boolean}
1390
+ */
1391
+ putPiece(piece, square, opts = {}) {
1392
+ const animate = opts.animate !== undefined ? opts.animate : true;
1393
+ let pieceStr = piece;
1394
+ if (typeof piece === 'object' && piece.type && piece.color) {
1395
+ pieceStr = (piece.color + piece.type).toLowerCase();
1396
+ } else if (typeof piece === 'string' && piece.length === 2) {
1397
+ // Accetta sia 'wq' che 'qw', normalizza a 'wq'
1398
+ const a = piece[0].toLowerCase();
1399
+ const b = piece[1].toLowerCase();
1400
+ const types = 'kqrbnp';
1401
+ const colors = 'wb';
1402
+ if (types.includes(a) && colors.includes(b)) {
1403
+ pieceStr = b + a;
1404
+ } else if (colors.includes(a) && types.includes(b)) {
1405
+ pieceStr = a + b;
1406
+ } else {
1407
+ throw new Error(`[putPiece] Invalid piece: ${piece}`);
1408
+ }
1409
+ }
1410
+ const validSquare = this.validationService.isValidSquare(square);
1411
+ const validPiece = this.validationService.isValidPiece(pieceStr);
1412
+ if (!validSquare) throw new Error(`[putPiece] Invalid square: ${square}`);
1413
+ if (!validPiece) throw new Error(`[putPiece] Invalid piece: ${pieceStr}`);
1414
+ if (!this.positionService || !this.positionService.getGame()) {
1415
+ throw new Error('[putPiece] No positionService or game');
1416
+ }
1417
+ const pieceObj = this.pieceService.convertPiece(pieceStr);
1418
+ const squareObj = this.boardService.getSquare(square);
1419
+ if (!squareObj) throw new Error(`[putPiece] Square not found: ${square}`);
1420
+ squareObj.piece = pieceObj;
1421
+ const chessJsPiece = { type: pieceObj.type, color: pieceObj.color };
1422
+ const game = this.positionService.getGame();
1423
+ const result = game.put(chessJsPiece, square);
1424
+ if (!result) throw new Error(`[putPiece] Game.put failed for ${pieceStr} on ${square}`);
1425
+ this._updateBoardPieces(animate);
1426
+ return true;
1427
+ }
1428
+ /**
1429
+ * Remove a piece from a square
1430
+ * @param {string} square
1431
+ * @param {Object} [opts]
1432
+ * @param {boolean} [opts.animate=true]
1433
+ * @returns {string|null}
1434
+ */
1435
+ removePiece(square, opts = {}) {
1436
+ const animate = opts.animate !== undefined ? opts.animate : true;
1437
+ if (!this.validationService.isValidSquare(square)) {
1438
+ throw new Error(`[removePiece] Invalid square: ${square}`);
1439
+ }
1440
+ if (!this.positionService || !this.positionService.getGame()) {
1441
+ return false;
1442
+ }
1443
+ const game = this.positionService.getGame();
1444
+ // Remove from game state first (source of truth)
1445
+ const removed = game.remove(square);
1446
+ // Then update the board visually
1447
+ const squareObj = this.boardService.getSquare(square);
1448
+ if (squareObj) {
1449
+ squareObj.piece = null;
1450
+ }
1451
+ this._updateBoardPieces(animate);
1452
+ return removed !== null;
1453
+ }
1454
+
1455
+ // --- BOARD CONTROL ---
1456
+ /**
1457
+ * Flip the board orientation
1458
+ * @param {Object} [opts]
1459
+ * @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
1460
+ * @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
1461
+ */
1462
+ flipBoard(opts = {}) {
1463
+ const flipMode = opts.mode || this.config.flipMode || 'visual';
1464
+
1465
+ // Update internal orientation state
1466
+ if (this.coordinateService && this.coordinateService.flipOrientation) {
1467
+ this.coordinateService.flipOrientation();
1468
+ }
1469
+
1470
+ const boardElement = this.boardService.element;
1471
+ const isFlipped = this.coordinateService.getOrientation() === 'b';
1472
+
1473
+ switch (flipMode) {
1474
+ case 'visual':
1475
+ // CSS flexbox flip - instant, no piece animation needed
1476
+ this._flipVisual(boardElement, isFlipped);
1477
+ break;
1478
+
1479
+ case 'animate':
1480
+ // Animate pieces to mirrored positions
1481
+ this._flipAnimate(opts.animate !== false);
1482
+ break;
1483
+
1484
+ case 'none':
1485
+ // No visual change - only internal orientation updated
1486
+ // Useful for programmatic orientation without visual feedback
1487
+ break;
1488
+
1489
+ default:
1490
+ this._flipVisual(boardElement, isFlipped);
1491
+ }
1492
+ }
1493
+
1494
+ /**
1495
+ * Visual flip using CSS flexbox (instant)
1496
+ * @private
1497
+ * @param {HTMLElement} boardElement - Board DOM element
1498
+ * @param {boolean} isFlipped - Whether board should be flipped
1499
+ */
1500
+ _flipVisual(boardElement, isFlipped) {
1501
+ if (!boardElement) return;
1502
+
1503
+ if (isFlipped) {
1504
+ boardElement.classList.add('flipped');
1505
+ } else {
1506
+ boardElement.classList.remove('flipped');
1507
+ }
1508
+ }
1509
+
1510
+ /**
1511
+ * Animate flip using FLIP technique (First-Last-Invert-Play)
1512
+ * Same end state as visual mode (CSS flip), but pieces animate smoothly.
1513
+ * @private
1514
+ * @param {boolean} animate - Whether to animate the movement
1515
+ */
1516
+ _flipAnimate(animate) {
1517
+ const boardElement = this.boardService.element;
1518
+ if (!boardElement) return;
1519
+
1520
+ const squares = this.boardService.getAllSquares();
1521
+
1522
+ // FIRST: Record current visual position of every piece
1523
+ const pieceRects = {};
1524
+ for (const [id, square] of Object.entries(squares)) {
1525
+ if (square.piece && square.piece.element) {
1526
+ pieceRects[id] = square.piece.element.getBoundingClientRect();
1527
+ }
1528
+ }
1529
+
1530
+ // LAST: Apply CSS flip (instant) - same as visual mode
1531
+ const isFlipped = this.coordinateService.getOrientation() === 'b';
1532
+ this._flipVisual(boardElement, isFlipped);
1533
+
1534
+ if (!animate || Object.keys(pieceRects).length === 0) return;
1535
+
1536
+ // INVERT + PLAY: Animate each piece from old position to new
1537
+ const duration = this.config.moveTime || 200;
1538
+ const easing = 'cubic-bezier(0.33, 1, 0.68, 1)';
1539
+
1540
+ for (const [id, oldRect] of Object.entries(pieceRects)) {
1541
+ const square = squares[id];
1542
+ if (!square || !square.piece || !square.piece.element) continue;
1543
+
1544
+ const piece = square.piece;
1545
+ const newRect = piece.element.getBoundingClientRect();
1546
+ const dx = oldRect.left - newRect.left;
1547
+ const dy = oldRect.top - newRect.top;
1548
+
1549
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
1550
+
1551
+ if (piece.element.animate) {
1552
+ const anim = piece.element.animate([
1553
+ { transform: `translate(${dx}px, ${dy}px)` },
1554
+ { transform: 'translate(0, 0)' }
1555
+ ], { duration, easing, fill: 'forwards' });
1556
+ anim.onfinish = () => {
1557
+ anim.cancel();
1558
+ if (piece.element) piece.element.style.transform = '';
1559
+ };
1560
+ } else {
1561
+ // setTimeout fallback for jsdom / older browsers
1562
+ piece.element.style.transform = `translate(${dx}px, ${dy}px)`;
1563
+ setTimeout(() => {
1564
+ if (!piece.element) return;
1565
+ piece.element.style.transition = `transform ${duration}ms`;
1566
+ piece.element.style.transform = 'translate(0, 0)';
1567
+ setTimeout(() => {
1568
+ if (!piece.element) return;
1569
+ piece.element.style.transition = '';
1570
+ piece.element.style.transform = '';
1571
+ }, duration);
1572
+ }, 0);
1573
+ }
1574
+ }
1575
+ }
1576
+
1577
+ /**
1578
+ * Set the flip mode at runtime
1579
+ * @param {'visual'|'animate'|'none'} mode - The flip mode to use
1580
+ */
1581
+ setFlipMode(mode) {
1582
+ const validModes = ['visual', 'animate', 'none'];
1583
+ if (!validModes.includes(mode)) {
1584
+ console.warn(`Invalid flip mode: ${mode}. Valid options: ${validModes.join(', ')}`);
1585
+ return;
1586
+ }
1587
+ this.config.flipMode = mode;
1588
+ }
1589
+
1590
+ /**
1591
+ * Get the current flip mode
1592
+ * @returns {string} Current flip mode
1593
+ */
1594
+ getFlipMode() {
1595
+ return this.config.flipMode || 'visual';
1596
+ }
1597
+
1598
+ // --- MOVEMENT CONFIGURATION ---
1599
+
1600
+ /**
1601
+ * Set the movement style
1602
+ * @param {'slide'|'arc'|'hop'|'teleport'|'fade'} style - Movement style
1603
+ */
1604
+ setMoveStyle(style) {
1605
+ const validStyles = ['slide', 'arc', 'hop', 'teleport', 'fade'];
1606
+ if (!validStyles.includes(style)) {
1607
+ console.warn(`Invalid move style: ${style}. Valid: ${validStyles.join(', ')}`);
1608
+ return;
1609
+ }
1610
+ this.config.moveStyle = style;
1611
+ }
1612
+
1613
+ /**
1614
+ * Get the current movement style
1615
+ * @returns {string} Current movement style
1616
+ */
1617
+ getMoveStyle() {
1618
+ return this.config.moveStyle || 'slide';
1619
+ }
1620
+
1621
+ /**
1622
+ * Set the capture animation style
1623
+ * @param {'fade'|'shrink'|'instant'|'explode'} style - Capture style
1624
+ */
1625
+ setCaptureStyle(style) {
1626
+ const validStyles = ['fade', 'shrink', 'instant', 'explode'];
1627
+ if (!validStyles.includes(style)) {
1628
+ console.warn(`Invalid capture style: ${style}. Valid: ${validStyles.join(', ')}`);
1629
+ return;
1630
+ }
1631
+ this.config.captureStyle = style;
1632
+ }
1633
+
1634
+ /**
1635
+ * Get the current capture style
1636
+ * @returns {string} Current capture style
1637
+ */
1638
+ getCaptureStyle() {
1639
+ return this.config.captureStyle || 'fade';
1640
+ }
1641
+
1642
+ /**
1643
+ * Set the appearance animation style
1644
+ * @param {'fade'|'pulse'|'pop'|'drop'|'instant'} style - Appearance style
1645
+ */
1646
+ setAppearanceStyle(style) {
1647
+ const validStyles = ['fade', 'pulse', 'pop', 'drop', 'instant'];
1648
+ if (!validStyles.includes(style)) {
1649
+ console.warn(`Invalid appearance style: ${style}. Valid: ${validStyles.join(', ')}`);
1650
+ return;
1651
+ }
1652
+ this.config.appearanceStyle = style;
1653
+ }
1654
+
1655
+ /**
1656
+ * Get the current appearance style
1657
+ * @returns {string} Current appearance style
1658
+ */
1659
+ getAppearanceStyle() {
1660
+ return this.config.appearanceStyle || 'fade';
1661
+ }
1662
+
1663
+ /**
1664
+ * Set the landing effect
1665
+ * @param {'none'|'bounce'|'pulse'|'settle'} effect - Landing effect
1666
+ */
1667
+ setLandingEffect(effect) {
1668
+ const validEffects = ['none', 'bounce', 'pulse', 'settle'];
1669
+ if (!validEffects.includes(effect)) {
1670
+ console.warn(`Invalid landing effect: ${effect}. Valid: ${validEffects.join(', ')}`);
1671
+ return;
1672
+ }
1673
+ this.config.landingEffect = effect;
1674
+ }
1675
+
1676
+ /**
1677
+ * Get the current landing effect
1678
+ * @returns {string} Current landing effect
1679
+ */
1680
+ getLandingEffect() {
1681
+ return this.config.landingEffect || 'none';
1682
+ }
1683
+
1684
+ /**
1685
+ * Set the movement duration
1686
+ * @param {number|string} duration - Duration in ms or preset name ('instant', 'veryFast', 'fast', 'normal', 'slow', 'verySlow')
1687
+ */
1688
+ setMoveTime(duration) {
1689
+ const presets = { instant: 0, veryFast: 100, fast: 200, normal: 400, slow: 600, verySlow: 1000 };
1690
+ if (typeof duration === 'string' && presets[duration] !== undefined) {
1691
+ this.config.moveTime = presets[duration];
1692
+ } else if (typeof duration === 'number' && duration >= 0) {
1693
+ this.config.moveTime = duration;
1694
+ } else {
1695
+ console.warn(`Invalid move time: ${duration}`);
1696
+ }
1697
+ }
1698
+
1699
+ /**
1700
+ * Get the current movement duration
1701
+ * @returns {number} Duration in ms
1702
+ */
1703
+ getMoveTime() {
1704
+ return this.config.moveTime;
1705
+ }
1706
+
1707
+ /**
1708
+ * Set the easing function for movements
1709
+ * @param {string} easing - CSS easing function
1710
+ */
1711
+ setMoveEasing(easing) {
1712
+ const validEasings = ['ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out'];
1713
+ if (!validEasings.includes(easing)) {
1714
+ console.warn(`Invalid easing: ${easing}. Valid: ${validEasings.join(', ')}`);
1715
+ return;
1716
+ }
1717
+ this.config.moveEasing = easing;
1718
+ }
1719
+
1720
+ /**
1721
+ * Configure multiple movement settings at once
1722
+ * @param {Object} options - Movement configuration
1723
+ * @param {string} [options.style] - Movement style
1724
+ * @param {string} [options.captureStyle] - Capture animation style
1725
+ * @param {string} [options.landingEffect] - Landing effect
1726
+ * @param {number|string} [options.duration] - Movement duration
1727
+ * @param {string} [options.easing] - Easing function
1728
+ * @param {number} [options.arcHeight] - Arc height for arc/hop styles (0-1)
1729
+ */
1730
+ configureMovement(options) {
1731
+ if (options.style) this.setMoveStyle(options.style);
1732
+ if (options.captureStyle) this.setCaptureStyle(options.captureStyle);
1733
+ if (options.appearanceStyle) this.setAppearanceStyle(options.appearanceStyle);
1734
+ if (options.landingEffect) this.setLandingEffect(options.landingEffect);
1735
+ if (options.duration !== undefined) this.setMoveTime(options.duration);
1736
+ if (options.easing) this.setMoveEasing(options.easing);
1737
+ if (options.arcHeight !== undefined) {
1738
+ this.config.moveArcHeight = Math.max(0, Math.min(1, options.arcHeight));
1739
+ }
1740
+ }
1741
+
1742
+ /**
1743
+ * Get all movement configuration
1744
+ * @returns {Object} Current movement configuration
1745
+ */
1746
+ getMovementConfig() {
1747
+ return {
1748
+ style: this.config.moveStyle || 'slide',
1749
+ captureStyle: this.config.captureStyle || 'fade',
1750
+ appearanceStyle: this.config.appearanceStyle || 'fade',
1751
+ landingEffect: this.config.landingEffect || 'none',
1752
+ duration: this.config.moveTime,
1753
+ easing: this.config.moveEasing || 'ease',
1754
+ arcHeight: this.config.moveArcHeight || 0.3
1755
+ };
1756
+ }
1757
+
1758
+ /**
1759
+ * Set the board orientation
1760
+ * @param {'w'|'b'} color
1761
+ * @param {Object} [opts]
1762
+ * @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
1763
+ * @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
1764
+ */
1765
+ setOrientation(color, opts = {}) {
1766
+ if (this.validationService.isValidOrientation(color)) {
1767
+ const currentOrientation = this.coordinateService.getOrientation();
1768
+ if (currentOrientation !== color) {
1769
+ this.coordinateService.setOrientation(color);
1770
+
1771
+ const flipMode = opts.mode || this.config.flipMode || 'visual';
1772
+ const boardElement = this.boardService.element;
1773
+ const isFlipped = color === 'b';
1774
+
1775
+ switch (flipMode) {
1776
+ case 'visual':
1777
+ this._flipVisual(boardElement, isFlipped);
1778
+ break;
1779
+ case 'animate':
1780
+ this._flipAnimate(opts.animate !== false);
1781
+ break;
1782
+ case 'none':
1783
+ // No visual change
1784
+ break;
1785
+ default:
1786
+ this._flipVisual(boardElement, isFlipped);
1787
+ }
1788
+ }
1789
+ }
1790
+ return this.coordinateService.getOrientation();
1791
+ }
1792
+ /**
1793
+ * Get the current orientation
1794
+ * @returns {'w'|'b'}
1795
+ */
1796
+ getOrientation() { return this.orientation(); }
1797
+ /**
1798
+ * Resize the board
1799
+ * @param {number|string} size
1800
+ */
1801
+ resizeBoard(size) {
1802
+ if (size === 'auto') {
1803
+ this.config.size = 'auto';
1804
+ document.documentElement.style.setProperty('--dimBoard', 'auto');
1805
+ this._updateBoardPieces(false);
1806
+ return true;
1807
+ }
1808
+ if (typeof size !== 'number' || size < 50 || size > 3000) {
1809
+ throw new Error(`[resizeBoard] Invalid size: ${size}`);
1810
+ }
1811
+ this.config.size = size;
1812
+ document.documentElement.style.setProperty('--dimBoard', `${size}px`);
1813
+ this._updateBoardPieces(false);
1814
+ return true;
1815
+ }
1816
+
1817
+ // --- HIGHLIGHTING & UI ---
1818
+ /**
1819
+ * Highlight a square
1820
+ * @param {string} square
1821
+ * @param {Object} [opts]
1822
+ */
1823
+ highlight(square, opts = {}) {
1824
+ if (!this.validationService.isValidSquare(square)) return;
1825
+ if (this.boardService && this.boardService.highlightSquare) {
1826
+ this.boardService.highlightSquare(square, opts);
1827
+ } else if (this.eventService && this.eventService.highlightSquare) {
1828
+ this.eventService.highlightSquare(square, opts);
1829
+ }
1830
+ }
1831
+ /**
1832
+ * Remove highlight from a square
1833
+ * @param {string} square
1834
+ * @param {Object} [opts]
1835
+ */
1836
+ dehighlight(square, opts = {}) {
1837
+ if (!this.validationService.isValidSquare(square)) return;
1838
+ if (this.boardService && this.boardService.dehighlightSquare) {
1839
+ this.boardService.dehighlightSquare(square, opts);
1840
+ } else if (this.eventService && this.eventService.dehighlightSquare) {
1841
+ this.eventService.dehighlightSquare(square, opts);
1842
+ }
1843
+ }
1844
+
1845
+ // --- GAME INFO ---
1846
+ /**
1847
+ * Get FEN string
1848
+ * @returns {string}
1849
+ */
1850
+ fen() {
1851
+ // Avoid recursion: call the underlying game object's fen()
1852
+ const game = this.positionService.getGame();
1853
+ if (!game || typeof game.fen !== 'function') return '';
1854
+ return game.fen();
1855
+ }
1856
+ /**
1857
+ * Get current turn
1858
+ * @returns {'w'|'b'}
1859
+ */
1860
+ turn() { return this.positionService.getGame().turn(); }
1861
+ /**
1862
+ * Is the game over?
1863
+ * @returns {boolean}
1864
+ */
1865
+ isGameOver() {
1866
+ const game = this.positionService.getGame();
1867
+ if (!game) return false;
1868
+ if (game.isGameOver) return game.isGameOver();
1869
+ // Fallback: checkmate or draw
1870
+ if (game.isCheckmate && game.isCheckmate()) return true;
1871
+ if (game.isDraw && game.isDraw()) return true;
1872
+ return false;
1873
+ }
1874
+ /**
1875
+ * Is it checkmate?
1876
+ * @returns {boolean}
1877
+ */
1878
+ isCheckmate() {
1879
+ const game = this.positionService.getGame();
1880
+ if (!game) return false;
1881
+ return game.isCheckmate ? game.isCheckmate() : false;
1882
+ }
1883
+ /**
1884
+ * Is it draw?
1885
+ * @returns {boolean}
1886
+ */
1887
+ isDraw() {
1888
+ const game = this.positionService.getGame();
1889
+ if (!game) return false;
1890
+ return game.isDraw ? game.isDraw() : false;
1891
+ }
1892
+ /**
1893
+ * Get move history
1894
+ * @returns {Array}
1895
+ */
1896
+ getHistory() {
1897
+ const game = this.positionService.getGame();
1898
+ if (!game) return [];
1899
+ return game.history ? game.history() : [];
1900
+ }
1901
+
1902
+ // --- LIFECYCLE ---
1903
+ /**
1904
+ * Destroy the board and cleanup
1905
+ */
1906
+ destroy() {
1907
+ this._destroyed = true;
1908
+
1909
+ // Remove all event listeners
1910
+ if (this.eventService) {
1911
+ this.eventService.removeAllListeners();
1912
+ this.eventService.destroy();
1913
+ }
1914
+
1915
+ // Clear all timeouts
1916
+ if (this._updateTimeout) {
1917
+ clearTimeout(this._updateTimeout);
1918
+ this._updateTimeout = null;
1919
+ }
1920
+
1921
+ // Clear all animation timeouts
1922
+ if (this._animationTimeouts) {
1923
+ this._animationTimeouts.forEach(tid => clearTimeout(tid));
1924
+ this._animationTimeouts = [];
1925
+ }
1926
+
1927
+ // Destroy services
1928
+ if (this.moveService) this.moveService.destroy();
1929
+ if (this.animationService && this.animationService.destroy) this.animationService.destroy();
1930
+ if (this.pieceService && this.pieceService.destroy) this.pieceService.destroy();
1931
+ if (this.boardService && this.boardService.destroy) this.boardService.destroy();
1932
+ if (this.positionService && this.positionService.destroy) this.positionService.destroy();
1933
+ if (this.coordinateService && this.coordinateService.destroy) this.coordinateService.destroy();
1934
+ if (this.validationService) this.validationService.destroy();
1935
+ if (this.config && this.config.destroy) this.config.destroy();
1936
+
1937
+ // Clear references
1938
+ this._cleanup();
1939
+ }
1940
+ /**
1941
+ * Rebuild the board
1942
+ */
1943
+ rebuild() { this._initialize(); }
1944
+
1945
+ // --- CONFIGURATION ---
1946
+ /**
1947
+ * Get current config
1948
+ * @returns {Object}
1949
+ */
1950
+ getConfig() { return this.config; }
1951
+ /**
1952
+ * Set new config
1953
+ * @param {Object} newConfig
1954
+ */
1955
+ setConfig(newConfig) {
1956
+ if (this.config && typeof this.config.update === 'function') {
1957
+ this.config.update(newConfig);
1958
+ }
1959
+ }
1960
+
1961
+ // --- ALIASES/DEPRECATED ---
1962
+ /**
1963
+ * Alias for move (deprecated)
1964
+ */
1965
+ move(move, animate = true) {
1966
+ // On any new move, clear the redo stack
1967
+ this._undoneMoves = [];
1968
+ return this.movePiece(move, { animate });
1969
+ }
1970
+ /**
1971
+ * Alias for clear (deprecated)
1972
+ */
1973
+ clearBoard(animate = true) {
1974
+ this._updateBoardPieces(animate);
1975
+ return this.clear({ animate });
1976
+ }
1977
+ /**
1978
+ * Alias for reset (deprecated)
1979
+ */
1980
+ start(animate = true) {
1981
+ this._updateBoardPieces(animate);
1982
+ return this.reset({ animate });
1983
+ }
1984
+
1985
+ /**
1986
+ * Alias for flipBoard (for backward compatibility)
1987
+ */
1988
+ flip(opts = {}) {
1989
+ this._updateBoardPieces(opts.animate !== false);
1990
+ return this.flipBoard(opts);
1991
+ }
1992
+
1993
+ /**
1994
+ * Gets or sets the current position
1995
+ * @param {string|Object} [position] - Position to set (FEN or object). If omitted, returns current position.
1996
+ * @param {boolean} [animate=true] - Whether to animate when setting
1997
+ * @returns {Object} Current position object (when getting)
1998
+ */
1999
+ position(position, animate = true) {
2000
+ if (position === undefined) {
2001
+ return this.positionService.getPosition();
2002
+ }
2003
+ this.load(position, {}, animate); // load() already handles isPositionLoad=true
2004
+ }
2005
+
2006
+ /**
2007
+ * Undoes the last move
2008
+ * @param {boolean} [animate=true] - Whether to animate
2009
+ * @returns {boolean} True if undo was successful
2010
+ */
2011
+ undo(animate = true) {
2012
+ const undone = this.positionService.getGame().undo();
2013
+ if (undone) {
2014
+ this._undoneMoves.push(undone);
2015
+ this._updateBoardPieces(animate);
2016
+ return undone;
2017
+ }
2018
+ return null;
2019
+ }
2020
+
2021
+ /**
2022
+ * Redoes the last undone move
2023
+ * @param {boolean} [animate=true] - Whether to animate
2024
+ * @returns {boolean} True if redo was successful
2025
+ */
2026
+ redo(animate = true) {
2027
+ if (this._undoneMoves && this._undoneMoves.length > 0) {
2028
+ const move = this._undoneMoves.pop();
2029
+ const moveObj = { from: move.from, to: move.to };
2030
+ if (move.promotion) moveObj.promotion = move.promotion;
2031
+ const result = this.positionService.getGame().move(moveObj);
2032
+ this._updateBoardPieces(animate);
2033
+ return result;
2034
+ }
2035
+ return false;
2036
+ }
2037
+
2038
+ /**
2039
+ * Gets the game history
2040
+ * @returns {Array} Array of moves
2041
+ */
2042
+ history() {
2043
+ return this.positionService.getGame().history();
2044
+ }
2045
+
2046
+ /**
2047
+ * Gets the current game state
2048
+ * @returns {Object} Game state object
2049
+ */
2050
+ game() {
2051
+ return this.positionService.getGame();
2052
+ }
2053
+
2054
+ /**
2055
+ * Gets or sets the orientation
2056
+ * @param {string} [orientation] - New orientation
2057
+ * @returns {string} Current orientation
2058
+ */
2059
+ orientation(orientation) {
2060
+ if (orientation === undefined) {
2061
+ return this.coordinateService.getOrientation();
2062
+ }
2063
+
2064
+ if (this.validationService.isValidOrientation(orientation)) {
2065
+ this.coordinateService.setOrientation(orientation);
2066
+ this.flip();
2067
+ }
2068
+
2069
+ return this.coordinateService.getOrientation();
2070
+ }
2071
+
2072
+ /**
2073
+ * Gets or sets the size
2074
+ * @param {number|string} [size] - New size
2075
+ * @returns {number|string} Current size
2076
+ */
2077
+ size(size) {
2078
+ if (size === undefined) {
2079
+ return this.config.size;
2080
+ }
2081
+
2082
+ if (this.validationService.isValidSize(size)) {
2083
+ this.config.size = size;
2084
+ this.resize(size);
2085
+ }
2086
+
2087
+ return this.config.size;
2088
+ }
2089
+
2090
+ /**
2091
+ * Gets legal moves for a square
2092
+ * @param {string} square - Square to get moves for
2093
+ * @returns {Array} Array of legal moves
2094
+ */
2095
+ legalMoves(square) {
2096
+ const squareObj = this.boardService.getSquare(square);
2097
+ if (!squareObj) return [];
2098
+
2099
+ return this.moveService.getCachedLegalMoves(squareObj);
2100
+ }
2101
+
2102
+ /**
2103
+ * Checks if a move is legal
2104
+ * @param {string|Object} move - Move to check
2105
+ * @returns {boolean} True if move is legal
2106
+ */
2107
+ isLegal(move) {
2108
+ const moveObj = typeof move === 'string' ? this.moveService.parseMove(move) : move;
2109
+ if (!moveObj) return false;
2110
+
2111
+ const fromSquare = this.boardService.getSquare(moveObj.from);
2112
+ const toSquare = this.boardService.getSquare(moveObj.to);
2113
+
2114
+ if (!fromSquare || !toSquare) return false;
2115
+
2116
+ const moveInstance = new Move(fromSquare, toSquare, moveObj.promotion);
2117
+ return moveInstance.isLegal(this.positionService.getGame());
2118
+ }
2119
+
2120
+ /**
2121
+ * Checks if the game is over
2122
+ * @returns {boolean} True if game is over
2123
+ */
2124
+ isGameOver() {
2125
+ return this.positionService.getGame().isGameOver();
2126
+ }
2127
+
2128
+ /**
2129
+ * Checks if the current player is in check
2130
+ * @returns {boolean} True if in check
2131
+ */
2132
+ inCheck() {
2133
+ return this.positionService.getGame().inCheck();
2134
+ }
2135
+
2136
+ /**
2137
+ * Checks if the current player is in checkmate
2138
+ * @returns {boolean} True if in checkmate
2139
+ */
2140
+ inCheckmate() {
2141
+ return this.positionService.getGame().isCheckmate();
2142
+ }
2143
+
2144
+ /**
2145
+ * Checks if the game is in stalemate
2146
+ * @returns {boolean} True if in stalemate
2147
+ */
2148
+ inStalemate() {
2149
+ return this.positionService.getGame().isStalemate();
2150
+ }
2151
+
2152
+ /**
2153
+ * Checks if the game is drawn
2154
+ * @returns {boolean} True if drawn
2155
+ */
2156
+ inDraw() {
2157
+ return this.positionService.getGame().isDraw();
2158
+ }
2159
+
2160
+ /**
2161
+ * Checks if position is threefold repetition
2162
+ * @returns {boolean} True if threefold repetition
2163
+ */
2164
+ inThreefoldRepetition() {
2165
+ return this.positionService.getGame().isThreefoldRepetition();
2166
+ }
2167
+
2168
+ /**
2169
+ * Gets the PGN representation of the game
2170
+ * @returns {string} PGN string
2171
+ */
2172
+ pgn() {
2173
+ return this.positionService.getGame().pgn();
2174
+ }
2175
+
2176
+ /**
2177
+ * Loads a PGN string
2178
+ * @param {string} pgn - PGN string to load
2179
+ * @param {boolean} [animate=true] - Whether to animate
2180
+ * @returns {boolean} True if loaded successfully
2181
+ */
2182
+ loadPgn(pgn, animate = true) {
2183
+ try {
2184
+ const success = this.positionService.getGame().loadPgn(pgn);
2185
+ if (success) {
2186
+ this._updateBoardPieces(animate, true); // Position load
2187
+ }
2188
+ return success;
2189
+ } catch (error) {
2190
+ console.error('Error loading PGN:', error);
2191
+ return false;
2192
+ }
2193
+ }
2194
+
2195
+ /**
2196
+ * Gets configuration options
2197
+ * @returns {Object} Configuration object
2198
+ */
2199
+ getConfig() {
2200
+ return this.config.getConfig();
2201
+ }
2202
+
2203
+ /**
2204
+ * Updates configuration options
2205
+ * @param {Object} newConfig - New configuration options
2206
+ */
2207
+ setConfig(newConfig) {
2208
+ this.config.update(newConfig);
2209
+
2210
+ // Rebuild board if necessary
2211
+ if (newConfig.size !== undefined) {
2212
+ this.resize(newConfig.size);
2213
+ }
2214
+
2215
+ if (newConfig.orientation !== undefined) {
2216
+ this.orientation(newConfig.orientation);
2217
+ }
2218
+ }
2219
+
2220
+ /**
2221
+ * Gets or sets the animation style
2222
+ * @param {string} [style] - New animation style ('sequential' or 'simultaneous')
2223
+ * @returns {string} Current animation style
2224
+ */
2225
+ animationStyle(style) {
2226
+ if (style === undefined) {
2227
+ return this.config.animationStyle;
2228
+ }
2229
+
2230
+ if (this.validationService.isValidAnimationStyle(style)) {
2231
+ this.config.animationStyle = style;
2232
+ }
2233
+
2234
+ return this.config.animationStyle;
2235
+ }
2236
+
2237
+ /**
2238
+ * Gets or sets the simultaneous animation delay
2239
+ * @param {number} [delay] - New delay in milliseconds
2240
+ * @returns {number} Current delay
2241
+ */
2242
+ simultaneousAnimationDelay(delay) {
2243
+ if (delay === undefined) {
2244
+ return this.config.simultaneousAnimationDelay;
2245
+ }
2246
+
2247
+ if (typeof delay === 'number' && delay >= 0) {
2248
+ this.config.simultaneousAnimationDelay = delay;
2249
+ }
2250
+
2251
+ return this.config.simultaneousAnimationDelay;
2252
+ }
2253
+
2254
+ // Additional API methods would be added here following the same pattern
2255
+ // This is a good starting point for the refactored architecture
2256
+
2257
+ // Ensure all public API methods from README are present and routed
2258
+ insert(square, piece) { return this.putPiece(piece, square); }
2259
+ get(square) { return this.getPiece(square); }
2260
+ // Note: position() is defined above at line ~1684 with getter/setter functionality
2261
+ build() { return this._initialize(); }
2262
+ resize(value) { return this.resizeBoard(value); }
2263
+ piece(square) { return this.getPiece(square); }
2264
+ ascii() { return this.positionService.getGame().ascii(); }
2265
+ board() { return this.positionService.getGame().board(); }
2266
+ getCastlingRights(color) { return this.positionService.getGame().getCastlingRights(color); }
2267
+ getComment() { return this.positionService.getGame().getComment(); }
2268
+ getComments() { return this.positionService.getGame().getComments(); }
2269
+ lastMove() { return this.positionService.getGame().lastMove(); }
2270
+ moveNumber() { return this.positionService.getGame().moveNumber(); }
2271
+ moves(options = {}) { return this.positionService.getGame().moves(options); }
2272
+ squareColor(squareId) { return this.boardService.getSquare(squareId).isWhite() ? 'light' : 'dark'; }
2273
+ isDrawByFiftyMoves() { return this.positionService.getGame().isDrawByFiftyMoves(); }
2274
+ isInsufficientMaterial() { return this.positionService.getGame().isInsufficientMaterial(); }
2275
+ isStalemate() { return this.positionService.getGame().isStalemate(); }
2276
+ isThreefoldRepetition() { return this.positionService.getGame().isThreefoldRepetition(); }
2277
+ load(fen, options = {}, animation = true) { return this.setPosition(fen, { ...options, animate: animation }); }
2278
+ loadPgn(pgn, options = {}, animation = true) { return this.positionService.getGame().loadPgn(pgn, animation); }
2279
+ put(pieceId, squareId, animation = true) {
2280
+ console.debug('[put] called with:', { pieceId, squareId, animation });
2281
+ let pieceObj = null;
2282
+ // Helper to normalize string like 'wQ', 'Qw', 'Wq', 'qw', etc.
2283
+ function parsePieceString(str) {
2284
+ if (typeof str !== 'string' || str.length !== 2) return null;
2285
+ const a = str[0].toLowerCase();
2286
+ const b = str[1].toLowerCase();
2287
+ const types = 'kqrbnp';
2288
+ const colors = 'wb';
2289
+ if (types.includes(a) && colors.includes(b)) {
2290
+ return { type: a, color: b };
2291
+ } else if (colors.includes(a) && types.includes(b)) {
2292
+ return { type: b, color: a };
2293
+ }
2294
+ return null;
2295
+ }
2296
+ if (typeof pieceId === 'string') {
2297
+ pieceObj = parsePieceString(pieceId);
2298
+ console.debug('[put] parsed piece string:', pieceObj);
2299
+ if (!pieceObj) {
2300
+ console.error(`[put] Invalid piece string: '${pieceId}'. Use e.g. 'wQ', 'Qw', 'bK', 'kb'`);
2301
+ return false;
2302
+ }
2303
+ } else if (typeof pieceId === 'object' && pieceId.type && pieceId.color) {
2304
+ const type = String(pieceId.type).toLowerCase();
2305
+ const color = String(pieceId.color).toLowerCase();
2306
+ if ('kqrbnp'.includes(type) && 'wb'.includes(color)) {
2307
+ pieceObj = { type, color };
2308
+ console.debug('[put] normalized piece object:', pieceObj);
2309
+ } else {
2310
+ console.error(`[put] Invalid piece object: {type: '${pieceId.type}', color: '${pieceId.color}'}`);
2311
+ return false;
2312
+ }
2313
+ } else {
2314
+ console.error('[put] Invalid pieceId:', pieceId);
2315
+ return false;
2316
+ }
2317
+ if (typeof squareId !== 'string' || squareId.length !== 2) {
2318
+ console.error('[put] Invalid squareId:', squareId);
2319
+ return false;
2320
+ }
2321
+ // Call the internal putPiece method
2322
+ const result = this.putPiece(pieceObj, squareId, { animate: animation });
2323
+ console.debug('[put] putPiece result:', result);
2324
+ return result;
2325
+ }
2326
+ remove(squareId, animation = true) { return this.removePiece(squareId, { animate: animation }); }
2327
+ removeComment() { return this.positionService.getGame().removeComment(); }
2328
+ removeComments() { return this.positionService.getGame().removeComments(); }
2329
+ removeHeader(field) { return this.positionService.getGame().removeHeader(field); }
2330
+ setCastlingRights(color, rights) { return this.positionService.getGame().setCastlingRights(color, rights); }
2331
+ setComment(comment) { return this.positionService.getGame().setComment(comment); }
2332
+ setHeader(key, value) { return this.positionService.getGame().setHeader(key, value); }
2333
+ validateFen(fen) { return this.positionService.getGame().validateFen(fen); }
2334
+
2335
+ // Implementazioni reali per highlight/dehighlight
2336
+ highlightSquare(square) {
2337
+ return this.boardService.highlight(square);
2338
+ }
2339
+ dehighlightSquare(square) {
2340
+ return this.boardService.dehighlight(square);
2341
+ }
2342
+ forceSync() { this._updateBoardPieces(true, true); }
2343
+ }
2344
+
2345
+ export { Chessboard };
2346
+ export default Chessboard;