@alepot55/chessboardjs 2.2.0 → 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 +127 -403
  4. package/assets/themes/alepot/theme.json +42 -0
  5. package/assets/themes/default/theme.json +42 -0
  6. package/chessboard.bundle.js +782 -119
  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 -981
  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,1930 @@
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._updateBoardPieces(true, true); // Forza popolamento DOM subito
55
+ }
56
+
57
+ /**
58
+ * Validates and initializes configuration
59
+ * @private
60
+ * @param {Object} config - Raw configuration object
61
+ * @throws {ConfigurationError} If configuration is invalid
62
+ */
63
+ _validateAndInitializeConfig(config) {
64
+ if (!config || typeof config !== 'object') {
65
+ throw new ConfigurationError('Configuration must be an object', 'config', config);
66
+ }
67
+
68
+ this.config = new ChessboardConfig(config);
69
+
70
+ // Validate required configuration
71
+ if (!this.config.id_div) {
72
+ throw new ConfigurationError('Configuration must include id_div', 'id_div', this.config.id_div);
73
+ }
74
+ }
75
+
76
+ /**
77
+ * Handles constructor errors gracefully
78
+ * @private
79
+ * @param {Error} error - Error that occurred during construction
80
+ */
81
+ _handleConstructorError(error) {
82
+ console.error('Chessboard initialization failed:', error);
83
+
84
+ // Clean up any partially initialized resources
85
+ this._cleanup();
86
+
87
+ // Re-throw with additional context
88
+ if (error instanceof ChessboardError) {
89
+ throw error;
90
+ } else {
91
+ throw new ChessboardError('Failed to initialize chessboard', 'INITIALIZATION_ERROR', {
92
+ originalError: error.message,
93
+ stack: error.stack
94
+ });
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Cleans up any partially initialized resources (safe to call multiple times)
100
+ * @private
101
+ */
102
+ _cleanup() {
103
+ // Remove event listeners if present
104
+ if (this.eventService && typeof this.eventService.removeListeners === 'function') {
105
+ this.eventService.removeListeners();
106
+ }
107
+ // Clear timeouts
108
+ if (this._updateTimeout) {
109
+ clearTimeout(this._updateTimeout);
110
+ this._updateTimeout = null;
111
+ }
112
+ // Null all services
113
+ this.validationService = null;
114
+ this.coordinateService = null;
115
+ this.positionService = null;
116
+ this.boardService = null;
117
+ this.pieceService = null;
118
+ this.animationService = null;
119
+ this.moveService = null;
120
+ this.eventService = null;
121
+ }
122
+
123
+ /**
124
+ * Initializes all services
125
+ * @private
126
+ */
127
+ _initializeServices() {
128
+ // Core services
129
+ this.validationService = new ValidationService();
130
+ this.coordinateService = new CoordinateService(this.config);
131
+ this.positionService = new PositionService(this.config);
132
+ this.boardService = new BoardService(this.config);
133
+ this.pieceService = new PieceService(this.config);
134
+ this.animationService = new AnimationService(this.config);
135
+ this.moveService = new MoveService(this.config, this.positionService);
136
+ this.eventService = new EventService(
137
+ this.config,
138
+ this.boardService,
139
+ this.moveService,
140
+ this.coordinateService,
141
+ this
142
+ );
143
+
144
+ // State management
145
+ this._updateTimeout = null;
146
+ this._isAnimating = false;
147
+
148
+ // Bind methods to preserve context
149
+ this._boundUpdateBoardPieces = this._updateBoardPieces.bind(this);
150
+ this._boundOnSquareClick = this._onSquareClick.bind(this);
151
+ this._boundOnPieceHover = this._onPieceHover.bind(this);
152
+ this._boundOnPieceLeave = this._onPieceLeave.bind(this);
153
+ }
154
+
155
+ /**
156
+ * Initializes the board
157
+ * @private
158
+ */
159
+ _initialize() {
160
+ this._initParams();
161
+ this._setGame(this.config.position);
162
+ this._buildBoard();
163
+ this._buildSquares();
164
+ this._addListeners();
165
+ this._updateBoardPieces(true, true); // Initial position load
166
+ }
167
+
168
+ /**
169
+ * Initializes parameters and state
170
+ * @private
171
+ */
172
+ _initParams() {
173
+ // Reset state
174
+ this.eventService.setClicked(null);
175
+ this.eventService.setPromoting(false);
176
+ this.eventService.setAnimating(false);
177
+ }
178
+
179
+ /**
180
+ * Sets up the game with initial position
181
+ * @private
182
+ * @param {string|Object} position - Initial position
183
+ */
184
+ _setGame(position) {
185
+ this.positionService.setGame(position);
186
+ }
187
+
188
+ /**
189
+ * Builds the board DOM structure
190
+ * @private
191
+ * Best practice: always remove squares (destroy JS/DOM) before clearing the board container.
192
+ */
193
+ _buildBoard() {
194
+ console.log('CHIAMATO: _buildBoard');
195
+ if (this._isUndoRedo) {
196
+ console.log('SKIP _buildBoard per undo/redo');
197
+ return;
198
+ }
199
+ // Forza la pulizia completa del contenitore board (DOM)
200
+ const boardContainer = document.getElementById(this.config.id_div);
201
+ if (boardContainer) boardContainer.innerHTML = '';
202
+ // Force remove all pieces from all squares (no animation, best practice)
203
+ if (this.boardService && this.boardService.squares) {
204
+ Object.values(this.boardService.squares).forEach(sq => sq && sq.forceRemoveAllPieces && sq.forceRemoveAllPieces());
205
+ }
206
+ if (this.boardService && this.boardService.removeSquares) this.boardService.removeSquares();
207
+ if (this.boardService && this.boardService.removeBoard) this.boardService.removeBoard();
208
+ this.boardService.buildBoard();
209
+ }
210
+
211
+ /**
212
+ * Builds all squares on the board
213
+ * @private
214
+ */
215
+ _buildSquares() {
216
+ console.log('CHIAMATO: _buildSquares');
217
+ if (this._isUndoRedo) {
218
+ console.log('SKIP _buildSquares per undo/redo');
219
+ return;
220
+ }
221
+ if (this.boardService && this.boardService.removeSquares) {
222
+ this.boardService.removeSquares();
223
+ }
224
+ this.boardService.buildSquares((row, col) => {
225
+ return this.coordinateService.realCoord(row, col);
226
+ });
227
+ }
228
+
229
+ /**
230
+ * Adds event listeners to squares
231
+ * @private
232
+ */
233
+ _addListeners() {
234
+ this.eventService.addListeners(
235
+ this._boundOnSquareClick,
236
+ this._boundOnPieceHover,
237
+ this._boundOnPieceLeave
238
+ );
239
+ }
240
+
241
+ /**
242
+ * Handles square click events
243
+ * @private
244
+ * @param {Square} square - Clicked square
245
+ * @param {boolean} [animate=true] - Whether to animate
246
+ * @param {boolean} [dragged=false] - Whether triggered by drag
247
+ * @returns {boolean} True if successful
248
+ */
249
+ _onSquareClick(square, animate = true, dragged = false) {
250
+ return this.eventService.onClick(
251
+ square,
252
+ this._onMove.bind(this),
253
+ this._onSelect.bind(this),
254
+ this._onDeselect.bind(this),
255
+ animate,
256
+ dragged
257
+ );
258
+ }
259
+
260
+ /**
261
+ * Handles piece hover events
262
+ * @private
263
+ * @param {Square} square - Hovered square
264
+ */
265
+ _onPieceHover(square) {
266
+ if (this.config.hints && !this.eventService.getClicked()) {
267
+ // Only show hints if no square is selected
268
+ this._hintMoves(square);
269
+ }
270
+ }
271
+
272
+ /**
273
+ * Handles piece leave events
274
+ * @private
275
+ * @param {Square} square - Left square
276
+ */
277
+ _onPieceLeave(square) {
278
+ if (this.config.hints && !this.eventService.getClicked()) {
279
+ // Only remove hints if no square is selected
280
+ this._dehintMoves(square);
281
+ }
282
+ }
283
+
284
+ /**
285
+ * Handles move execution
286
+ * @private
287
+ * @param {Square} fromSquare - Source square
288
+ * @param {Square} toSquare - Target square
289
+ * @param {string} [promotion] - Promotion piece
290
+ * @param {boolean} [animate=true] - Whether to animate
291
+ * @returns {boolean} True if move was successful
292
+ */
293
+ _onMove(fromSquare, toSquare, promotion = null, animate = true) {
294
+ const move = new Move(fromSquare, toSquare, promotion);
295
+
296
+ if (!move.check()) {
297
+ // Clear state on failed move
298
+ this._clearVisualState();
299
+ return false;
300
+ }
301
+
302
+ if (this.config.onlyLegalMoves && !move.isLegal(this.positionService.getGame())) {
303
+ // Clear state on illegal move
304
+ this._clearVisualState();
305
+ return false;
306
+ }
307
+
308
+ if (!move.hasPromotion() && this._requiresPromotion(move)) {
309
+ // Don't clear state for promotion - it's handled elsewhere
310
+ return false;
311
+ }
312
+
313
+ if (this.config.onMove(move)) {
314
+ // Clear state before executing move
315
+ this._clearVisualState();
316
+ this._executeMove(move, animate);
317
+ return true;
318
+ }
319
+
320
+ // Clear state on rejected move
321
+ this._clearVisualState();
322
+ return false;
323
+ }
324
+
325
+ /**
326
+ * Handles square selection
327
+ * @private
328
+ * @param {Square} square - Selected square
329
+ */
330
+ _onSelect(square) {
331
+ if (this.config.clickable) {
332
+ square.select();
333
+ this._hintMoves(square);
334
+ }
335
+ }
336
+
337
+ /**
338
+ * Handles square deselection
339
+ * @private
340
+ * @param {Square} square - Deselected square
341
+ */
342
+ _onDeselect(square) {
343
+ this._clearVisualState();
344
+ }
345
+
346
+ /**
347
+ * Shows legal move hints for a square
348
+ * @private
349
+ * @param {Square} square - Square to show hints for
350
+ */
351
+ _hintMoves(square) {
352
+ if (!this.moveService.canMove(square)) return;
353
+
354
+ // Clear existing hints first
355
+ this.boardService.applyToAllSquares('removeHint');
356
+
357
+ const moves = this.moveService.getCachedLegalMoves(square);
358
+
359
+ for (const move of moves) {
360
+ if (move.to && move.to.length === 2) {
361
+ const targetSquare = this.boardService.getSquare(move.to);
362
+ if (targetSquare) {
363
+ const hasEnemyPiece = targetSquare.piece &&
364
+ targetSquare.piece.color !== this.positionService.getGame().turn();
365
+ targetSquare.putHint(hasEnemyPiece);
366
+ }
367
+ }
368
+ }
369
+ }
370
+
371
+ /**
372
+ * Removes legal move hints for a square
373
+ * @private
374
+ * @param {Square} square - Square to remove hints for
375
+ */
376
+ _dehintMoves(square) {
377
+ const moves = this.moveService.getCachedLegalMoves(square);
378
+
379
+ for (const move of moves) {
380
+ if (move.to && move.to.length === 2) {
381
+ const targetSquare = this.boardService.getSquare(move.to);
382
+ if (targetSquare) {
383
+ targetSquare.removeHint();
384
+ }
385
+ }
386
+ }
387
+ }
388
+
389
+ /**
390
+ * Checks if a move requires promotion
391
+ * @private
392
+ * @param {Move} move - Move to check
393
+ * @returns {boolean} True if promotion is required
394
+ */
395
+ _requiresPromotion(move) {
396
+ return this.moveService.requiresPromotion(move);
397
+ }
398
+
399
+ /**
400
+ * Executes a move
401
+ * @private
402
+ * @param {Move} move - Move to execute
403
+ * @param {boolean} [animate=true] - Whether to animate
404
+ */
405
+ _executeMove(move, animate = true) {
406
+ const gameStateBefore = this.positionService.getGame().fen();
407
+
408
+ if (this.config.onlyLegalMoves) {
409
+ this.boardService.applyToAllSquares('unmoved');
410
+
411
+ const gameMove = this.moveService.executeMove(move);
412
+ if (!gameMove) {
413
+ throw new Error('Move execution failed');
414
+ }
415
+
416
+ move.from.moved();
417
+ move.to.moved();
418
+
419
+ // Check for special moves that need additional handling
420
+ const isCastleMove = this.moveService.isCastle(gameMove);
421
+ const isEnPassantMove = this.moveService.isEnPassant(gameMove);
422
+
423
+ // Animate the move if requested
424
+ if (animate && move.from.piece) {
425
+ const capturedPiece = move.to.piece;
426
+
427
+ // For castle moves in simultaneous mode, we need to coordinate both animations
428
+ if (isCastleMove && this.config.animationStyle === 'simultaneous') {
429
+ // Start king animation
430
+ this.pieceService.translatePiece(
431
+ move,
432
+ !!capturedPiece,
433
+ animate,
434
+ this._createDragFunction.bind(this),
435
+ () => {
436
+ // King animation completed, trigger change event
437
+ this.config.onMoveEnd(gameMove);
438
+ }
439
+ );
440
+
441
+ // Start rook animation simultaneously (with small delay)
442
+ setTimeout(() => {
443
+ this._handleCastleMove(gameMove, true);
444
+ }, this.config.simultaneousAnimationDelay);
445
+ } else {
446
+ // Regular move or sequential castle
447
+ this.pieceService.translatePiece(
448
+ move,
449
+ !!capturedPiece,
450
+ animate,
451
+ this._createDragFunction.bind(this),
452
+ () => {
453
+ // After animation, handle special moves and trigger change event
454
+ if (isCastleMove) {
455
+ this._handleSpecialMoveAnimation(gameMove);
456
+ } else if (isEnPassantMove) {
457
+ this._handleSpecialMoveAnimation(gameMove);
458
+ }
459
+ this.config.onMoveEnd(gameMove);
460
+ }
461
+ );
462
+ }
463
+ } else {
464
+ // For non-animated moves, handle special moves immediately
465
+ if (isCastleMove) {
466
+ this._handleSpecialMove(gameMove);
467
+ } else if (isEnPassantMove) {
468
+ this._handleSpecialMove(gameMove);
469
+ }
470
+ this._updateBoardPieces(false);
471
+ this.config.onMoveEnd(gameMove);
472
+ }
473
+ } else {
474
+ // Handle non-legal mode
475
+ const piece = this.positionService.getGamePieceId(move.from.id);
476
+ const game = this.positionService.getGame();
477
+
478
+ game.remove(move.from.id);
479
+ game.remove(move.to.id);
480
+ game.put({
481
+ type: move.hasPromotion() ? move.promotion : piece[0],
482
+ color: piece[1]
483
+ }, move.to.id);
484
+
485
+ // Update board for non-legal mode
486
+ this._updateBoardPieces(animate);
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Handles special moves (castle, en passant) without animation
492
+ * @private
493
+ * @param {Object} gameMove - Game move object
494
+ */
495
+ _handleSpecialMove(gameMove) {
496
+ if (this.moveService.isCastle(gameMove)) {
497
+ this._handleCastleMove(gameMove, false);
498
+ } else if (this.moveService.isEnPassant(gameMove)) {
499
+ this._handleEnPassantMove(gameMove, false);
500
+ }
501
+ }
502
+
503
+ /**
504
+ * Handles special moves (castle, en passant) with animation
505
+ * @private
506
+ * @param {Object} gameMove - Game move object
507
+ */
508
+ _handleSpecialMoveAnimation(gameMove) {
509
+ if (this.moveService.isCastle(gameMove)) {
510
+ this._handleCastleMove(gameMove, true);
511
+ } else if (this.moveService.isEnPassant(gameMove)) {
512
+ this._handleEnPassantMove(gameMove, true);
513
+ }
514
+ }
515
+
516
+ /**
517
+ * Handles castle move by moving the rook
518
+ * @private
519
+ * @param {Object} gameMove - Game move object
520
+ * @param {boolean} animate - Whether to animate
521
+ */
522
+ _handleCastleMove(gameMove, animate) {
523
+ const rookMove = this.moveService.getCastleRookMove(gameMove);
524
+ if (!rookMove) return;
525
+
526
+ const rookFromSquare = this.boardService.getSquare(rookMove.from);
527
+ const rookToSquare = this.boardService.getSquare(rookMove.to);
528
+
529
+ if (!rookFromSquare || !rookToSquare || !rookFromSquare.piece) {
530
+ console.warn('Castle rook move failed - squares or piece not found');
531
+ return;
532
+ }
533
+
534
+ console.log(`Castle: moving rook from ${rookMove.from} to ${rookMove.to}`);
535
+
536
+ if (animate) {
537
+ // Always use translatePiece for smooth sliding animation
538
+ const rookPiece = rookFromSquare.piece;
539
+ this.pieceService.translatePiece(
540
+ { from: rookFromSquare, to: rookToSquare, piece: rookPiece },
541
+ false, // No capture for rook in castle
542
+ animate,
543
+ this._createDragFunction.bind(this),
544
+ () => {
545
+ // After rook animation, update board state
546
+ this._updateBoardPieces(false);
547
+ }
548
+ );
549
+ } else {
550
+ // Just update the board state
551
+ this._updateBoardPieces(false);
552
+ }
553
+ }
554
+
555
+ /**
556
+ * Handles en passant move by removing the captured pawn
557
+ * @private
558
+ * @param {Object} gameMove - Game move object
559
+ * @param {boolean} animate - Whether to animate
560
+ */
561
+ _handleEnPassantMove(gameMove, animate) {
562
+ const capturedSquare = this.moveService.getEnPassantCapturedSquare(gameMove);
563
+ if (!capturedSquare) return;
564
+
565
+ const capturedSquareObj = this.boardService.getSquare(capturedSquare);
566
+ if (!capturedSquareObj || !capturedSquareObj.piece) {
567
+ console.warn('En passant captured square not found or empty');
568
+ return;
569
+ }
570
+
571
+ console.log(`En passant: removing captured pawn from ${capturedSquare}`);
572
+
573
+ if (animate) {
574
+ // Animate the captured pawn removal
575
+ this.pieceService.removePieceFromSquare(capturedSquareObj, true);
576
+ // Update board state after animation
577
+ setTimeout(() => {
578
+ this._updateBoardPieces(false);
579
+ }, this.config.moveTime);
580
+ } else {
581
+ // Just update the board state
582
+ this._updateBoardPieces(false);
583
+ }
584
+ }
585
+
586
+ /**
587
+ * Updates board pieces to match game state
588
+ * @private
589
+ * @param {boolean} [animation=false] - Whether to animate
590
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load
591
+ */
592
+ _updateBoardPieces(animation = false, isPositionLoad = false) {
593
+ console.log('CHIAMATO: _updateBoardPieces', { animation, isPositionLoad, isUndoRedo: this._isUndoRedo });
594
+ // Check if services are available
595
+ if (!this.positionService || !this.moveService || !this.eventService) {
596
+ console.log('Cannot update board pieces - services not available');
597
+ return;
598
+ }
599
+
600
+ // Clear any pending update
601
+ if (this._updateTimeout) {
602
+ clearTimeout(this._updateTimeout);
603
+ this._updateTimeout = null;
604
+ }
605
+
606
+ // Clear moves cache
607
+ this.moveService.clearCache();
608
+
609
+ // Add small delay for click-to-move to avoid lag
610
+ if (animation && !this.eventService.getClicked()) {
611
+ this._updateTimeout = setTimeout(() => {
612
+ this._doUpdateBoardPieces(animation, isPositionLoad);
613
+ this._updateTimeout = null;
614
+
615
+ // Ensure hints are available for the next turn
616
+ this._ensureHintsAvailable();
617
+ }, 10);
618
+ } else {
619
+ this._doUpdateBoardPieces(animation, isPositionLoad);
620
+
621
+ // Ensure hints are available for the next turn
622
+ this._ensureHintsAvailable();
623
+ }
624
+ }
625
+
626
+ /**
627
+ * Ensures hints are available for the current turn
628
+ * @private
629
+ */
630
+ _ensureHintsAvailable() {
631
+ if (!this.config.hints) return;
632
+
633
+ // Small delay to ensure the board state is fully updated
634
+ setTimeout(() => {
635
+ // Clear any existing hints
636
+ this.boardService.applyToAllSquares('removeHint');
637
+
638
+ // The hints will be shown when the user hovers over pieces
639
+ // This just ensures the cache is ready
640
+ this.moveService.clearCache();
641
+ }, 50);
642
+ }
643
+
644
+ /**
645
+ * Updates board pieces after a delayed move
646
+ * @private
647
+ */
648
+ _updateBoardPiecesDelayed() {
649
+ this._updateBoardPieces(false);
650
+ }
651
+
652
+ /**
653
+ * Performs the actual board update
654
+ * @private
655
+ * @param {boolean} [animation=false] - Whether to animate
656
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load (affects delay)
657
+ */
658
+ _doUpdateBoardPieces(animation = false, isPositionLoad = false) {
659
+ // Skip update if we're in the middle of a promotion
660
+ if (this._isPromoting) {
661
+ console.log('Skipping board update during promotion');
662
+ return;
663
+ }
664
+
665
+ // Check if services are available
666
+ if (!this.positionService || !this.positionService.getGame()) {
667
+ console.log('Cannot update board pieces - position service not available');
668
+ return;
669
+ }
670
+
671
+ const squares = this.boardService.getAllSquares();
672
+ const gameStateBefore = this.positionService.getGame().fen();
673
+
674
+ console.log('_doUpdateBoardPieces - current FEN:', gameStateBefore);
675
+ console.log('_doUpdateBoardPieces - animation:', animation, 'style:', this.config.animationStyle, 'isPositionLoad:', isPositionLoad);
676
+
677
+ // Determine which animation style to use
678
+ const useSimultaneous = this.config.animationStyle === 'simultaneous';
679
+ console.log('_doUpdateBoardPieces - useSimultaneous:', useSimultaneous);
680
+
681
+ if (useSimultaneous) {
682
+ console.log('Using simultaneous animation');
683
+ this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad);
684
+ } else {
685
+ console.log('Using sequential animation');
686
+ this._doSequentialUpdate(squares, gameStateBefore, animation);
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Performs sequential piece updates (original behavior)
692
+ * @private
693
+ * @param {Object} squares - All squares
694
+ * @param {string} gameStateBefore - Game state before update
695
+ * @param {boolean} animation - Whether to animate
696
+ */
697
+ _doSequentialUpdate(squares, gameStateBefore, animation) {
698
+ // Mappa: squareId -> expectedPieceId
699
+ const expectedMap = {};
700
+ Object.values(squares).forEach(square => {
701
+ expectedMap[square.id] = this.positionService.getGamePieceId(square.id);
702
+ });
703
+
704
+ Object.values(squares).forEach(square => {
705
+ const expectedPieceId = expectedMap[square.id];
706
+ const currentPiece = square.piece;
707
+ const currentPieceId = currentPiece ? currentPiece.getId() : null;
708
+
709
+ // Se il pezzo attuale e quello atteso sono identici, non fare nulla
710
+ if (currentPieceId === expectedPieceId) {
711
+ return;
712
+ }
713
+
714
+ // Se c'è un pezzo attuale ma non è quello atteso, rimuovilo
715
+ if (currentPiece && currentPieceId !== expectedPieceId) {
716
+ this.pieceService.removePieceFromSquare(square, animation);
717
+ }
718
+
719
+ // Se c'è un pezzo atteso ma non è quello attuale, aggiungilo
720
+ if (expectedPieceId && currentPieceId !== expectedPieceId) {
721
+ const newPiece = this.pieceService.convertPiece(expectedPieceId);
722
+ this.pieceService.addPieceOnSquare(
723
+ square,
724
+ newPiece,
725
+ animation,
726
+ this._createDragFunction.bind(this)
727
+ );
728
+ }
729
+ });
730
+
731
+ this._addListeners();
732
+ const gameStateAfter = this.positionService.getGame().fen();
733
+ if (gameStateBefore !== gameStateAfter) {
734
+ this.config.onChange(gameStateAfter);
735
+ }
736
+ }
737
+
738
+ /**
739
+ * Performs simultaneous piece updates
740
+ * @private
741
+ * @param {Object} squares - All squares
742
+ * @param {string} gameStateBefore - Game state before update
743
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load
744
+ */
745
+ _doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false) {
746
+ // Matching greedy per distanza minima, robusto
747
+ const currentMap = {};
748
+ const expectedMap = {};
749
+
750
+ Object.values(squares).forEach(square => {
751
+ const currentPiece = square.piece;
752
+ const expectedPieceId = this.positionService.getGamePieceId(square.id);
753
+ if (currentPiece) {
754
+ // Normalizza la chiave come 'color+type' lowercase
755
+ const key = (currentPiece.color + currentPiece.type).toLowerCase();
756
+ if (!currentMap[key]) currentMap[key] = [];
757
+ currentMap[key].push({ square, id: square.id });
758
+ }
759
+ if (expectedPieceId) {
760
+ // Normalizza la chiave come 'color+type' lowercase
761
+ const key = expectedPieceId.toLowerCase();
762
+ if (!expectedMap[key]) expectedMap[key] = [];
763
+ expectedMap[key].push({ square, id: square.id });
764
+ }
765
+ });
766
+
767
+ let animationsCompleted = 0;
768
+ let totalAnimations = 0;
769
+ const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
770
+ let animationIndex = 0;
771
+
772
+ Object.keys(expectedMap).forEach(key => {
773
+ totalAnimations += Math.max((currentMap[key] || []).length, expectedMap[key].length);
774
+ });
775
+
776
+ if (totalAnimations === 0) {
777
+ this._addListeners();
778
+ const gameStateAfter = this.positionService.getGame().fen();
779
+ if (gameStateBefore !== gameStateAfter) {
780
+ this.config.onChange(gameStateAfter);
781
+ }
782
+ return;
783
+ }
784
+
785
+ const onAnimationComplete = () => {
786
+ animationsCompleted++;
787
+ if (animationsCompleted === totalAnimations) {
788
+ this._addListeners();
789
+ const gameStateAfter = this.positionService.getGame().fen();
790
+ if (gameStateBefore !== gameStateAfter) {
791
+ this.config.onChange(gameStateAfter);
792
+ }
793
+ }
794
+ };
795
+
796
+ Object.keys(expectedMap).forEach(key => {
797
+ const fromList = (currentMap[key] || []).slice();
798
+ const toList = expectedMap[key].slice();
799
+
800
+ // 1. Costruisci matrice delle distanze
801
+ const distances = [];
802
+ for (let i = 0; i < fromList.length; i++) {
803
+ distances[i] = [];
804
+ for (let j = 0; j < toList.length; j++) {
805
+ distances[i][j] = Math.abs(fromList[i].square.row - toList[j].square.row) +
806
+ Math.abs(fromList[i].square.col - toList[j].square.col);
807
+ }
808
+ }
809
+
810
+ // 2. Matching greedy: abbina i più vicini
811
+ const fromMatched = new Array(fromList.length).fill(false);
812
+ const toMatched = new Array(toList.length).fill(false);
813
+ const moves = [];
814
+
815
+ while (true) {
816
+ let minDist = Infinity, minI = -1, minJ = -1;
817
+ for (let i = 0; i < fromList.length; i++) {
818
+ if (fromMatched[i]) continue;
819
+ for (let j = 0; j < toList.length; j++) {
820
+ if (toMatched[j]) continue;
821
+ if (distances[i][j] < minDist) {
822
+ minDist = distances[i][j];
823
+ minI = i;
824
+ minJ = j;
825
+ }
826
+ }
827
+ }
828
+ if (minI === -1 || minJ === -1) break;
829
+ // Se la posizione è la stessa, non fare nulla (pezzo unchanged)
830
+ if (fromList[minI].square === toList[minJ].square) {
831
+ fromMatched[minI] = true;
832
+ toMatched[minJ] = true;
833
+ continue;
834
+ }
835
+ // Altrimenti, sposta il pezzo
836
+ moves.push({ from: fromList[minI].square, to: toList[minJ].square, piece: fromList[minI].square.piece });
837
+ fromMatched[minI] = true;
838
+ toMatched[minJ] = true;
839
+ }
840
+
841
+ // 3. Rimuovi i pezzi non abbinati (presenti solo in fromList)
842
+ for (let i = 0; i < fromList.length; i++) {
843
+ if (!fromMatched[i]) {
844
+ setTimeout(() => {
845
+ this.pieceService.removePieceFromSquare(fromList[i].square, true, onAnimationComplete);
846
+ }, animationIndex * animationDelay);
847
+ animationIndex++;
848
+ }
849
+ }
850
+
851
+ // 4. Aggiungi i pezzi non abbinati (presenti solo in toList)
852
+ for (let j = 0; j < toList.length; j++) {
853
+ if (!toMatched[j]) {
854
+ setTimeout(() => {
855
+ const newPiece = this.pieceService.convertPiece(key);
856
+ this.pieceService.addPieceOnSquare(
857
+ toList[j].square,
858
+ newPiece,
859
+ true,
860
+ this._createDragFunction.bind(this),
861
+ onAnimationComplete
862
+ );
863
+ }, animationIndex * animationDelay);
864
+ animationIndex++;
865
+ }
866
+ }
867
+
868
+ // 5. Anima i movimenti
869
+ moves.forEach(move => {
870
+ setTimeout(() => {
871
+ this.pieceService.translatePiece(
872
+ move,
873
+ false,
874
+ true,
875
+ this._createDragFunction.bind(this),
876
+ onAnimationComplete
877
+ );
878
+ }, animationIndex * animationDelay);
879
+ animationIndex++;
880
+ });
881
+ });
882
+ }
883
+
884
+ /**
885
+ * Analyzes position changes to determine optimal animation strategy
886
+ * @private
887
+ * @param {Object} squares - All squares
888
+ * @returns {Object} Analysis of changes
889
+ */
890
+ _analyzePositionChanges(squares) {
891
+ const currentPieces = new Map();
892
+ const expectedPieces = new Map();
893
+
894
+ // Map current and expected piece positions
895
+ Object.values(squares).forEach(square => {
896
+ const currentPiece = square.piece;
897
+ const expectedPieceId = this.positionService.getGamePieceId(square.id);
898
+
899
+ if (currentPiece) {
900
+ currentPieces.set(square.id, currentPiece.getId());
901
+ }
902
+
903
+ if (expectedPieceId) {
904
+ expectedPieces.set(square.id, expectedPieceId);
905
+ }
906
+ });
907
+
908
+ console.log('Position Analysis:');
909
+ console.log('Current pieces:', Array.from(currentPieces.entries()));
910
+ console.log('Expected pieces:', Array.from(expectedPieces.entries()));
911
+
912
+ // Identify different types of changes
913
+ const moves = []; // Pieces that can slide to new positions
914
+ const removes = []; // Pieces that need to be removed
915
+ const adds = []; // Pieces that need to be added
916
+ const unchanged = []; // Pieces that stay in place
917
+
918
+ // First pass: identify pieces that don't need to move (same piece type on same square)
919
+ const processedSquares = new Set();
920
+
921
+ currentPieces.forEach((currentPieceId, square) => {
922
+ const expectedPieceId = expectedPieces.get(square);
923
+
924
+ if (currentPieceId === expectedPieceId) {
925
+ // Same piece type on same square - no movement needed
926
+ console.log(`UNCHANGED: ${currentPieceId} stays on ${square}`);
927
+ unchanged.push({
928
+ piece: currentPieceId,
929
+ square: square
930
+ });
931
+ processedSquares.add(square);
932
+ }
933
+ });
934
+
935
+ // Second pass: handle pieces that need to move or be removed
936
+ currentPieces.forEach((currentPieceId, fromSquare) => {
937
+ if (processedSquares.has(fromSquare)) {
938
+ return; // Already processed as unchanged
939
+ }
940
+
941
+ // Try to find a destination for this piece
942
+ const availableDestination = Array.from(expectedPieces.entries()).find(([toSquare, expectedId]) =>
943
+ expectedId === currentPieceId && !processedSquares.has(toSquare)
944
+ );
945
+
946
+ if (availableDestination) {
947
+ const [toSquare, expectedId] = availableDestination;
948
+ console.log(`MOVE: ${currentPieceId} from ${fromSquare} to ${toSquare}`);
949
+ moves.push({
950
+ piece: currentPieceId,
951
+ from: fromSquare,
952
+ to: toSquare,
953
+ fromSquare: squares[fromSquare],
954
+ toSquare: squares[toSquare]
955
+ });
956
+ processedSquares.add(toSquare);
957
+ } else {
958
+ // This piece needs to be removed
959
+ console.log(`REMOVE: ${currentPieceId} from ${fromSquare}`);
960
+ removes.push({
961
+ piece: currentPieceId,
962
+ square: fromSquare,
963
+ squareObj: squares[fromSquare]
964
+ });
965
+ }
966
+ });
967
+
968
+ // Third pass: handle pieces that need to be added
969
+ expectedPieces.forEach((expectedPieceId, toSquare) => {
970
+ if (!processedSquares.has(toSquare)) {
971
+ console.log(`ADD: ${expectedPieceId} to ${toSquare}`);
972
+ adds.push({
973
+ piece: expectedPieceId,
974
+ square: toSquare,
975
+ squareObj: squares[toSquare]
976
+ });
977
+ }
978
+ });
979
+
980
+ return {
981
+ moves,
982
+ removes,
983
+ adds,
984
+ unchanged,
985
+ totalChanges: moves.length + removes.length + adds.length
986
+ };
987
+ }
988
+
989
+ /**
990
+ * Executes simultaneous changes based on analysis
991
+ * @private
992
+ * @param {Object} changeAnalysis - Analysis of changes
993
+ * @param {string} gameStateBefore - Game state before update
994
+ * @param {boolean} [isPositionLoad=false] - Whether this is a position load
995
+ */
996
+ _executeSimultaneousChanges(changeAnalysis, gameStateBefore, isPositionLoad = false) {
997
+ const { moves, removes, adds, unchanged } = changeAnalysis;
998
+
999
+ console.log(`Position changes analysis:`, {
1000
+ moves: moves.length,
1001
+ removes: removes.length,
1002
+ adds: adds.length,
1003
+ unchanged: unchanged.length
1004
+ });
1005
+
1006
+ // Log unchanged pieces for debugging
1007
+ if (unchanged.length > 0) {
1008
+ console.log('Pieces staying in place:', unchanged.map(u => `${u.piece} on ${u.square}`));
1009
+ }
1010
+
1011
+ let animationsCompleted = 0;
1012
+ const totalAnimations = moves.length + removes.length + adds.length;
1013
+
1014
+ // If no animations are needed, complete immediately
1015
+ if (totalAnimations === 0) {
1016
+ console.log('No animations needed, completing immediately');
1017
+ this._addListeners();
1018
+
1019
+ // Trigger change event if position changed
1020
+ const gameStateAfter = this.positionService.getGame().fen();
1021
+ if (gameStateBefore !== gameStateAfter) {
1022
+ this.config.onChange(gameStateAfter);
1023
+ }
1024
+ return;
1025
+ }
1026
+
1027
+ const onAnimationComplete = () => {
1028
+ animationsCompleted++;
1029
+ console.log(`Animation completed: ${animationsCompleted}/${totalAnimations}`);
1030
+ if (animationsCompleted === totalAnimations) {
1031
+ console.log('All simultaneous animations completed');
1032
+ this._addListeners();
1033
+
1034
+ // Trigger change event if position changed
1035
+ const gameStateAfter = this.positionService.getGame().fen();
1036
+ if (gameStateBefore !== gameStateAfter) {
1037
+ this.config.onChange(gameStateAfter);
1038
+ }
1039
+ }
1040
+ };
1041
+
1042
+ // Determine delay: 0 for position loads, configured delay for normal moves
1043
+ const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
1044
+ console.log(`Using animation delay: ${animationDelay}ms (position load: ${isPositionLoad})`);
1045
+
1046
+ let animationIndex = 0;
1047
+
1048
+ // Process moves (pieces sliding to new positions)
1049
+ moves.forEach(move => {
1050
+ const delay = animationIndex * animationDelay;
1051
+ console.log(`Scheduling move ${move.piece} from ${move.from} to ${move.to} with delay ${delay}ms`);
1052
+
1053
+ setTimeout(() => {
1054
+ this._animatePieceMove(move, onAnimationComplete);
1055
+ }, delay);
1056
+
1057
+ animationIndex++;
1058
+ });
1059
+
1060
+ // Process removes (pieces disappearing)
1061
+ removes.forEach(remove => {
1062
+ const delay = animationIndex * animationDelay;
1063
+ console.log(`Scheduling removal of ${remove.piece} from ${remove.square} with delay ${delay}ms`);
1064
+
1065
+ setTimeout(() => {
1066
+ this._animatePieceRemoval(remove, onAnimationComplete);
1067
+ }, delay);
1068
+
1069
+ animationIndex++;
1070
+ });
1071
+
1072
+ // Process adds (pieces appearing)
1073
+ adds.forEach(add => {
1074
+ const delay = animationIndex * animationDelay;
1075
+ console.log(`Scheduling addition of ${add.piece} to ${add.square} with delay ${delay}ms`);
1076
+
1077
+ setTimeout(() => {
1078
+ this._animatePieceAddition(add, onAnimationComplete);
1079
+ }, delay);
1080
+
1081
+ animationIndex++;
1082
+ });
1083
+ }
1084
+
1085
+ /**
1086
+ * Animates a piece moving from one square to another
1087
+ * @private
1088
+ * @param {Object} move - Move information
1089
+ * @param {Function} onComplete - Callback when animation completes
1090
+ */
1091
+ _animatePieceMove(move, onComplete) {
1092
+ const { fromSquare, toSquare } = move;
1093
+ const piece = fromSquare.piece;
1094
+
1095
+ if (!piece) {
1096
+ console.warn(`No piece found on ${move.from} for move animation`);
1097
+ onComplete();
1098
+ return;
1099
+ }
1100
+
1101
+ console.log(`Animating piece move: ${move.piece} from ${move.from} to ${move.to}`);
1102
+
1103
+ // Use translatePiece for smooth sliding animation
1104
+ this.pieceService.translatePiece(
1105
+ { from: fromSquare, to: toSquare, piece: piece },
1106
+ false, // Assume no capture for now
1107
+ true, // Always animate
1108
+ this._createDragFunction.bind(this),
1109
+ () => {
1110
+ console.log(`Piece move animation completed: ${move.piece} to ${move.to}`);
1111
+ onComplete();
1112
+ }
1113
+ );
1114
+ }
1115
+
1116
+ /**
1117
+ * Animates a piece being removed
1118
+ * @private
1119
+ * @param {Object} remove - Remove information
1120
+ * @param {Function} onComplete - Callback when animation completes
1121
+ */
1122
+ _animatePieceRemoval(remove, onComplete) {
1123
+ console.log(`Animating piece removal: ${remove.piece} from ${remove.square}`);
1124
+
1125
+ this.pieceService.removePieceFromSquare(remove.squareObj, true, () => {
1126
+ console.log(`Piece removal animation completed: ${remove.piece} from ${remove.square}`);
1127
+ onComplete();
1128
+ });
1129
+ }
1130
+
1131
+ /**
1132
+ * Animates a piece being added
1133
+ * @private
1134
+ * @param {Object} add - Add information
1135
+ * @param {Function} onComplete - Callback when animation completes
1136
+ */
1137
+ _animatePieceAddition(add, onComplete) {
1138
+ console.log(`Animating piece addition: ${add.piece} to ${add.square}`);
1139
+
1140
+ const newPiece = this.pieceService.convertPiece(add.piece);
1141
+ this.pieceService.addPieceOnSquare(
1142
+ add.squareObj,
1143
+ newPiece,
1144
+ true,
1145
+ this._createDragFunction.bind(this),
1146
+ () => {
1147
+ console.log(`Piece addition animation completed: ${add.piece} to ${add.square}`);
1148
+ onComplete();
1149
+ }
1150
+ );
1151
+ }
1152
+
1153
+ /**
1154
+ * Creates a drag function for a piece
1155
+ * @private
1156
+ * @param {Square} square - Square containing the piece
1157
+ * @param {Piece} piece - Piece to create drag function for
1158
+ * @returns {Function} Drag function
1159
+ */
1160
+ _createDragFunction(square, piece) {
1161
+ return this.eventService.createDragFunction(
1162
+ square,
1163
+ piece,
1164
+ this.config.onDragStart,
1165
+ this.config.onDragMove,
1166
+ this.config.onDrop,
1167
+ this._onSnapback.bind(this),
1168
+ this._onMove.bind(this),
1169
+ this._onRemove.bind(this)
1170
+ );
1171
+ }
1172
+
1173
+ /**
1174
+ * Handles snapback animation
1175
+ * @private
1176
+ * @param {Square} square - Square containing the piece
1177
+ * @param {Piece} piece - Piece to snapback
1178
+ */
1179
+ _onSnapback(square, piece) {
1180
+ this.pieceService.snapbackPiece(square, this.config.snapbackAnimation);
1181
+ this.config.onSnapbackEnd(square, piece);
1182
+ }
1183
+
1184
+ /**
1185
+ * Handles piece removal
1186
+ * @private
1187
+ * @param {Square} square - Square containing the piece to remove
1188
+ */
1189
+ _onRemove(square) {
1190
+ this.pieceService.removePieceFromSquare(square, true);
1191
+ this.positionService.getGame().remove(square.id);
1192
+ this._updateBoardPieces(true);
1193
+ }
1194
+
1195
+ /**
1196
+ * Clears all visual state (selections, hints, highlights)
1197
+ * @private
1198
+ */
1199
+ _clearVisualState() {
1200
+ this.boardService.applyToAllSquares('deselect');
1201
+ this.boardService.applyToAllSquares('removeHint');
1202
+ this.boardService.applyToAllSquares('dehighlight');
1203
+ this.eventService.setClicked(null);
1204
+ }
1205
+
1206
+ // -------------------
1207
+ // Public API Methods (Refactored)
1208
+ // -------------------
1209
+
1210
+ // --- POSITION & STATE ---
1211
+ /**
1212
+ * Get the current position as FEN
1213
+ * @returns {string}
1214
+ */
1215
+ getPosition() { return this.fen(); }
1216
+ /**
1217
+ * Set the board position (FEN or object)
1218
+ * @param {string|Object} position
1219
+ * @param {Object} [opts]
1220
+ * @param {boolean} [opts.animate=true]
1221
+ * @returns {boolean}
1222
+ */
1223
+ setPosition(position, opts = {}) {
1224
+ const animate = opts.animate !== undefined ? opts.animate : true;
1225
+ // Remove highlights and selections
1226
+ if (this.boardService && this.boardService.applyToAllSquares) {
1227
+ this.boardService.applyToAllSquares('removeHint');
1228
+ this.boardService.applyToAllSquares('deselect');
1229
+ this.boardService.applyToAllSquares('unmoved');
1230
+ }
1231
+ if (this.positionService && this.positionService.setGame) {
1232
+ this.positionService.setGame(position);
1233
+ }
1234
+ if (this._updateBoardPieces) {
1235
+ this._updateBoardPieces(animate, true);
1236
+ }
1237
+ return true;
1238
+ }
1239
+ /**
1240
+ * Reset the board to the starting position
1241
+ * @param {Object} [opts]
1242
+ * @param {boolean} [opts.animate=true]
1243
+ * @returns {boolean}
1244
+ */
1245
+ reset(opts = {}) {
1246
+ const animate = opts.animate !== undefined ? opts.animate : true;
1247
+ // Use the default starting position from config or fallback
1248
+ const startPosition = this.config && this.config.position ? this.config.position : 'start';
1249
+ this._updateBoardPieces(animate);
1250
+ return this.setPosition(startPosition, { animate });
1251
+ }
1252
+ /**
1253
+ * Clear the board
1254
+ * @param {Object} [opts]
1255
+ * @param {boolean} [opts.animate=true]
1256
+ * @returns {boolean}
1257
+ */
1258
+ clear(opts = {}) {
1259
+ const animate = opts.animate !== undefined ? opts.animate : true;
1260
+ if (!this.positionService || !this.positionService.getGame()) {
1261
+ return false;
1262
+ }
1263
+ if (this._clearVisualState) this._clearVisualState();
1264
+ this.positionService.getGame().clear();
1265
+ if (this._updateBoardPieces) {
1266
+ this._updateBoardPieces(animate, true);
1267
+ }
1268
+ return true;
1269
+ }
1270
+
1271
+ // --- MOVE MANAGEMENT ---
1272
+ /**
1273
+ * Undo last move
1274
+ * @param {Object} [opts]
1275
+ * @param {boolean} [opts.animate=true]
1276
+ * @returns {boolean}
1277
+ */
1278
+ undoMove(opts = {}) {
1279
+ const undone = this.positionService.getGame().undo();
1280
+ if (undone) {
1281
+ this._undoneMoves.push(undone);
1282
+ this._updateBoardPieces(opts.animate !== false);
1283
+ return undone;
1284
+ }
1285
+ return null;
1286
+ }
1287
+ /**
1288
+ * Redo last undone move
1289
+ * @param {Object} [opts]
1290
+ * @param {boolean} [opts.animate=true]
1291
+ * @returns {boolean}
1292
+ */
1293
+ redoMove(opts = {}) {
1294
+ if (this._undoneMoves && this._undoneMoves.length > 0) {
1295
+ const move = this._undoneMoves.pop();
1296
+ const moveObj = { from: move.from, to: move.to };
1297
+ if (move.promotion) moveObj.promotion = move.promotion;
1298
+ const result = this.positionService.getGame().move(moveObj);
1299
+ this._updateBoardPieces(opts.animate !== false);
1300
+ return result;
1301
+ }
1302
+ return false;
1303
+ }
1304
+ /**
1305
+ * Get legal moves for a square
1306
+ * @param {string} square
1307
+ * @returns {Array}
1308
+ */
1309
+ getLegalMoves(square) { return this.legalMoves(square); }
1310
+
1311
+ // --- PIECE MANAGEMENT ---
1312
+ /**
1313
+ * Get the piece at a square
1314
+ * @param {string} square
1315
+ * @returns {string|null}
1316
+ */
1317
+ getPiece(square) {
1318
+ // Restituisce sempre 'wq' (colore prima, tipo dopo, lowercase) o null
1319
+ const sq = this.boardService.getSquare(square);
1320
+ if (!sq) return null;
1321
+ const piece = sq.piece;
1322
+ if (!piece) return null;
1323
+ return (piece.color + piece.type).toLowerCase();
1324
+ }
1325
+ /**
1326
+ * Put a piece on a square
1327
+ * @param {string} piece
1328
+ * @param {string} square
1329
+ * @param {Object} [opts]
1330
+ * @param {boolean} [opts.animate=true]
1331
+ * @returns {boolean}
1332
+ */
1333
+ putPiece(piece, square, opts = {}) {
1334
+ const animate = opts.animate !== undefined ? opts.animate : true;
1335
+ let pieceStr = piece;
1336
+ if (typeof piece === 'object' && piece.type && piece.color) {
1337
+ pieceStr = (piece.color + piece.type).toLowerCase();
1338
+ } else if (typeof piece === 'string' && piece.length === 2) {
1339
+ // Accetta sia 'wq' che 'qw', normalizza a 'wq'
1340
+ const a = piece[0].toLowerCase();
1341
+ const b = piece[1].toLowerCase();
1342
+ const types = 'kqrbnp';
1343
+ const colors = 'wb';
1344
+ if (types.includes(a) && colors.includes(b)) {
1345
+ pieceStr = b + a;
1346
+ } else if (colors.includes(a) && types.includes(b)) {
1347
+ pieceStr = a + b;
1348
+ } else {
1349
+ throw new Error(`[putPiece] Invalid piece: ${piece}`);
1350
+ }
1351
+ }
1352
+ const validSquare = this.validationService.isValidSquare(square);
1353
+ const validPiece = this.validationService.isValidPiece(pieceStr);
1354
+ if (!validSquare) throw new Error(`[putPiece] Invalid square: ${square}`);
1355
+ if (!validPiece) throw new Error(`[putPiece] Invalid piece: ${pieceStr}`);
1356
+ if (!this.positionService || !this.positionService.getGame()) {
1357
+ throw new Error('[putPiece] No positionService or game');
1358
+ }
1359
+ const pieceObj = this.pieceService.convertPiece(pieceStr);
1360
+ const squareObj = this.boardService.getSquare(square);
1361
+ if (!squareObj) throw new Error(`[putPiece] Square not found: ${square}`);
1362
+ squareObj.piece = pieceObj;
1363
+ const chessJsPiece = { type: pieceObj.type, color: pieceObj.color };
1364
+ const game = this.positionService.getGame();
1365
+ const result = game.put(chessJsPiece, square);
1366
+ if (!result) throw new Error(`[putPiece] Game.put failed for ${pieceStr} on ${square}`);
1367
+ this._updateBoardPieces(animate);
1368
+ return true;
1369
+ }
1370
+ /**
1371
+ * Remove a piece from a square
1372
+ * @param {string} square
1373
+ * @param {Object} [opts]
1374
+ * @param {boolean} [opts.animate=true]
1375
+ * @returns {string|null}
1376
+ */
1377
+ removePiece(square, opts = {}) {
1378
+ const animate = opts.animate !== undefined ? opts.animate : true;
1379
+ if (!this.validationService.isValidSquare(square)) {
1380
+ throw new Error(`[removePiece] Invalid square: ${square}`);
1381
+ }
1382
+ const squareObj = this.boardService.getSquare(square);
1383
+ if (!squareObj) return true;
1384
+ if (!squareObj.piece) return true;
1385
+ squareObj.piece = null;
1386
+ const game = this.positionService.getGame();
1387
+ game.remove(square);
1388
+ this._updateBoardPieces(animate);
1389
+ return true;
1390
+ }
1391
+
1392
+ // --- BOARD CONTROL ---
1393
+ /**
1394
+ * Flip the board orientation
1395
+ * @param {Object} [opts]
1396
+ * @param {boolean} [opts.animate=true]
1397
+ */
1398
+ flipBoard(opts = {}) {
1399
+ if (this.coordinateService && this.coordinateService.flipOrientation) {
1400
+ this.coordinateService.flipOrientation();
1401
+ }
1402
+ if (this._buildBoard) this._buildBoard();
1403
+ if (this._buildSquares) this._buildSquares();
1404
+ if (this._addListeners) this._addListeners();
1405
+ if (this._updateBoardPieces) this._updateBoardPieces(opts.animate !== false);
1406
+ console.log('FEN dopo flip:', this.fen(), 'Orientamento:', this.coordinateService.getOrientation());
1407
+ }
1408
+ /**
1409
+ * Set the board orientation
1410
+ * @param {'w'|'b'} color
1411
+ * @param {Object} [opts]
1412
+ * @param {boolean} [opts.animate=true]
1413
+ */
1414
+ setOrientation(color, opts = {}) {
1415
+ if (this.validationService.isValidOrientation(color)) {
1416
+ this.coordinateService.setOrientation(color);
1417
+ if (this._buildBoard) this._buildBoard();
1418
+ if (this._buildSquares) this._buildSquares();
1419
+ if (this._addListeners) this._addListeners();
1420
+ if (this._updateBoardPieces) this._updateBoardPieces(opts.animate !== false);
1421
+ }
1422
+ return this.coordinateService.getOrientation();
1423
+ }
1424
+ /**
1425
+ * Get the current orientation
1426
+ * @returns {'w'|'b'}
1427
+ */
1428
+ getOrientation() { return this.orientation(); }
1429
+ /**
1430
+ * Resize the board
1431
+ * @param {number|string} size
1432
+ */
1433
+ resizeBoard(size) {
1434
+ if (size === 'auto') {
1435
+ this.config.size = 'auto';
1436
+ document.documentElement.style.setProperty('--dimBoard', 'auto');
1437
+ this._updateBoardPieces(false);
1438
+ return true;
1439
+ }
1440
+ if (typeof size !== 'number' || size < 50 || size > 3000) {
1441
+ throw new Error(`[resizeBoard] Invalid size: ${size}`);
1442
+ }
1443
+ this.config.size = size;
1444
+ document.documentElement.style.setProperty('--dimBoard', `${size}px`);
1445
+ this._updateBoardPieces(false);
1446
+ return true;
1447
+ }
1448
+
1449
+ // --- HIGHLIGHTING & UI ---
1450
+ /**
1451
+ * Highlight a square
1452
+ * @param {string} square
1453
+ * @param {Object} [opts]
1454
+ */
1455
+ highlight(square, opts = {}) {
1456
+ if (!this.validationService.isValidSquare(square)) return;
1457
+ if (this.boardService && this.boardService.highlightSquare) {
1458
+ this.boardService.highlightSquare(square, opts);
1459
+ } else if (this.eventService && this.eventService.highlightSquare) {
1460
+ this.eventService.highlightSquare(square, opts);
1461
+ }
1462
+ }
1463
+ /**
1464
+ * Remove highlight from a square
1465
+ * @param {string} square
1466
+ * @param {Object} [opts]
1467
+ */
1468
+ dehighlight(square, opts = {}) {
1469
+ if (!this.validationService.isValidSquare(square)) return;
1470
+ if (this.boardService && this.boardService.dehighlightSquare) {
1471
+ this.boardService.dehighlightSquare(square, opts);
1472
+ } else if (this.eventService && this.eventService.dehighlightSquare) {
1473
+ this.eventService.dehighlightSquare(square, opts);
1474
+ }
1475
+ }
1476
+
1477
+ // --- GAME INFO ---
1478
+ /**
1479
+ * Get FEN string
1480
+ * @returns {string}
1481
+ */
1482
+ fen() {
1483
+ // Avoid recursion: call the underlying game object's fen()
1484
+ const game = this.positionService.getGame();
1485
+ if (!game || typeof game.fen !== 'function') return '';
1486
+ return game.fen();
1487
+ }
1488
+ /**
1489
+ * Get current turn
1490
+ * @returns {'w'|'b'}
1491
+ */
1492
+ turn() { return this.positionService.getGame().turn(); }
1493
+ /**
1494
+ * Is the game over?
1495
+ * @returns {boolean}
1496
+ */
1497
+ isGameOver() {
1498
+ const game = this.positionService.getGame();
1499
+ if (!game) return false;
1500
+ if (game.isGameOver) return game.isGameOver();
1501
+ // Fallback: checkmate or draw
1502
+ if (game.isCheckmate && game.isCheckmate()) return true;
1503
+ if (game.isDraw && game.isDraw()) return true;
1504
+ return false;
1505
+ }
1506
+ /**
1507
+ * Is it checkmate?
1508
+ * @returns {boolean}
1509
+ */
1510
+ isCheckmate() {
1511
+ const game = this.positionService.getGame();
1512
+ if (!game) return false;
1513
+ return game.isCheckmate ? game.isCheckmate() : false;
1514
+ }
1515
+ /**
1516
+ * Is it draw?
1517
+ * @returns {boolean}
1518
+ */
1519
+ isDraw() {
1520
+ const game = this.positionService.getGame();
1521
+ if (!game) return false;
1522
+ return game.isDraw ? game.isDraw() : false;
1523
+ }
1524
+ /**
1525
+ * Get move history
1526
+ * @returns {Array}
1527
+ */
1528
+ getHistory() {
1529
+ const game = this.positionService.getGame();
1530
+ if (!game) return [];
1531
+ return game.history ? game.history() : [];
1532
+ }
1533
+
1534
+ // --- LIFECYCLE ---
1535
+ /**
1536
+ * Destroy the board and cleanup
1537
+ */
1538
+ destroy() { /* TODO: robust destroy logic */ }
1539
+ /**
1540
+ * Rebuild the board
1541
+ */
1542
+ rebuild() { this._initialize(); }
1543
+
1544
+ // --- CONFIGURATION ---
1545
+ /**
1546
+ * Get current config
1547
+ * @returns {Object}
1548
+ */
1549
+ getConfig() { return this.config; }
1550
+ /**
1551
+ * Set new config
1552
+ * @param {Object} newConfig
1553
+ */
1554
+ setConfig(newConfig) { this.setConfig(newConfig); }
1555
+
1556
+ // --- ALIASES/DEPRECATED ---
1557
+ // Note: These methods are now implemented as aliases at the end of the class
1558
+
1559
+ /**
1560
+ * Alias for flipBoard (for backward compatibility)
1561
+ */
1562
+ flip(opts = {}) {
1563
+ this._updateBoardPieces(opts.animate !== false);
1564
+ return this.flipBoard(opts);
1565
+ }
1566
+
1567
+ /**
1568
+ * Gets the current position as an object
1569
+ * @returns {Object} Position object
1570
+ */
1571
+ position() {
1572
+ return this.positionService.getPosition();
1573
+ }
1574
+
1575
+ /**
1576
+ * Sets a new position
1577
+ * @param {string|Object} position - New position
1578
+ * @param {boolean} [animate=true] - Whether to animate
1579
+ */
1580
+ position(position, animate = true) {
1581
+ if (position === undefined) {
1582
+ return this.positionService.getPosition();
1583
+ }
1584
+ this.load(position, {}, animate); // load() already handles isPositionLoad=true
1585
+ }
1586
+
1587
+ /**
1588
+ * Undoes the last move
1589
+ * @param {boolean} [animate=true] - Whether to animate
1590
+ * @returns {boolean} True if undo was successful
1591
+ */
1592
+ undo(animate = true) {
1593
+ const undone = this.positionService.getGame().undo();
1594
+ if (undone) {
1595
+ this._undoneMoves.push(undone);
1596
+ this._updateBoardPieces(animate);
1597
+ return undone;
1598
+ }
1599
+ return null;
1600
+ }
1601
+
1602
+ /**
1603
+ * Redoes the last undone move
1604
+ * @param {boolean} [animate=true] - Whether to animate
1605
+ * @returns {boolean} True if redo was successful
1606
+ */
1607
+ redo(animate = true) {
1608
+ if (this._undoneMoves && this._undoneMoves.length > 0) {
1609
+ const move = this._undoneMoves.pop();
1610
+ const moveObj = { from: move.from, to: move.to };
1611
+ if (move.promotion) moveObj.promotion = move.promotion;
1612
+ const result = this.positionService.getGame().move(moveObj);
1613
+ this._updateBoardPieces(animate);
1614
+ return result;
1615
+ }
1616
+ return false;
1617
+ }
1618
+
1619
+ /**
1620
+ * Gets the game history
1621
+ * @returns {Array} Array of moves
1622
+ */
1623
+ history() {
1624
+ return this.positionService.getGame().history();
1625
+ }
1626
+
1627
+ /**
1628
+ * Gets the current game state
1629
+ * @returns {Object} Game state object
1630
+ */
1631
+ game() {
1632
+ return this.positionService.getGame();
1633
+ }
1634
+
1635
+ /**
1636
+ * Gets or sets the orientation
1637
+ * @param {string} [orientation] - New orientation
1638
+ * @returns {string} Current orientation
1639
+ */
1640
+ orientation(orientation) {
1641
+ if (orientation === undefined) {
1642
+ return this.coordinateService.getOrientation();
1643
+ }
1644
+
1645
+ if (this.validationService.isValidOrientation(orientation)) {
1646
+ this.coordinateService.setOrientation(orientation);
1647
+ this.flip();
1648
+ }
1649
+
1650
+ return this.coordinateService.getOrientation();
1651
+ }
1652
+
1653
+ /**
1654
+ * Gets or sets the size
1655
+ * @param {number|string} [size] - New size
1656
+ * @returns {number|string} Current size
1657
+ */
1658
+ size(size) {
1659
+ if (size === undefined) {
1660
+ return this.config.size;
1661
+ }
1662
+
1663
+ if (this.validationService.isValidSize(size)) {
1664
+ this.config.size = size;
1665
+ this.resize(size);
1666
+ }
1667
+
1668
+ return this.config.size;
1669
+ }
1670
+
1671
+ /**
1672
+ * Gets legal moves for a square
1673
+ * @param {string} square - Square to get moves for
1674
+ * @returns {Array} Array of legal moves
1675
+ */
1676
+ legalMoves(square) {
1677
+ const squareObj = this.boardService.getSquare(square);
1678
+ if (!squareObj) return [];
1679
+
1680
+ return this.moveService.getCachedLegalMoves(squareObj);
1681
+ }
1682
+
1683
+ /**
1684
+ * Checks if a move is legal
1685
+ * @param {string|Object} move - Move to check
1686
+ * @returns {boolean} True if move is legal
1687
+ */
1688
+ isLegal(move) {
1689
+ const moveObj = typeof move === 'string' ? this.moveService.parseMove(move) : move;
1690
+ if (!moveObj) return false;
1691
+
1692
+ const fromSquare = this.boardService.getSquare(moveObj.from);
1693
+ const toSquare = this.boardService.getSquare(moveObj.to);
1694
+
1695
+ if (!fromSquare || !toSquare) return false;
1696
+
1697
+ const moveInstance = new Move(fromSquare, toSquare, moveObj.promotion);
1698
+ return moveInstance.isLegal(this.positionService.getGame());
1699
+ }
1700
+
1701
+ /**
1702
+ * Checks if the game is over
1703
+ * @returns {boolean} True if game is over
1704
+ */
1705
+ isGameOver() {
1706
+ return this.positionService.getGame().isGameOver();
1707
+ }
1708
+
1709
+ /**
1710
+ * Checks if the current player is in check
1711
+ * @returns {boolean} True if in check
1712
+ */
1713
+ inCheck() {
1714
+ return this.positionService.getGame().inCheck();
1715
+ }
1716
+
1717
+ /**
1718
+ * Checks if the current player is in checkmate
1719
+ * @returns {boolean} True if in checkmate
1720
+ */
1721
+ inCheckmate() {
1722
+ return this.positionService.getGame().isCheckmate();
1723
+ }
1724
+
1725
+ /**
1726
+ * Checks if the game is in stalemate
1727
+ * @returns {boolean} True if in stalemate
1728
+ */
1729
+ inStalemate() {
1730
+ return this.positionService.getGame().isStalemate();
1731
+ }
1732
+
1733
+ /**
1734
+ * Checks if the game is drawn
1735
+ * @returns {boolean} True if drawn
1736
+ */
1737
+ inDraw() {
1738
+ return this.positionService.getGame().isDraw();
1739
+ }
1740
+
1741
+ /**
1742
+ * Checks if position is threefold repetition
1743
+ * @returns {boolean} True if threefold repetition
1744
+ */
1745
+ inThreefoldRepetition() {
1746
+ return this.positionService.getGame().isThreefoldRepetition();
1747
+ }
1748
+
1749
+ /**
1750
+ * Gets the PGN representation of the game
1751
+ * @returns {string} PGN string
1752
+ */
1753
+ pgn() {
1754
+ return this.positionService.getGame().pgn();
1755
+ }
1756
+
1757
+ /**
1758
+ * Loads a PGN string
1759
+ * @param {string} pgn - PGN string to load
1760
+ * @param {boolean} [animate=true] - Whether to animate
1761
+ * @returns {boolean} True if loaded successfully
1762
+ */
1763
+ loadPgn(pgn, animate = true) {
1764
+ try {
1765
+ const success = this.positionService.getGame().loadPgn(pgn);
1766
+ if (success) {
1767
+ this._updateBoardPieces(animate, true); // Position load
1768
+ }
1769
+ return success;
1770
+ } catch (error) {
1771
+ console.error('Error loading PGN:', error);
1772
+ return false;
1773
+ }
1774
+ }
1775
+
1776
+ /**
1777
+ * Gets configuration options
1778
+ * @returns {Object} Configuration object
1779
+ */
1780
+ getConfig() {
1781
+ return this.config.getConfig();
1782
+ }
1783
+
1784
+ /**
1785
+ * Updates configuration options
1786
+ * @param {Object} newConfig - New configuration options
1787
+ */
1788
+ setConfig(newConfig) {
1789
+ this.config.update(newConfig);
1790
+
1791
+ // Rebuild board if necessary
1792
+ if (newConfig.size !== undefined) {
1793
+ this.resize(newConfig.size);
1794
+ }
1795
+
1796
+ if (newConfig.orientation !== undefined) {
1797
+ this.orientation(newConfig.orientation);
1798
+ }
1799
+ }
1800
+
1801
+ /**
1802
+ * Gets or sets the animation style
1803
+ * @param {string} [style] - New animation style ('sequential' or 'simultaneous')
1804
+ * @returns {string} Current animation style
1805
+ */
1806
+ animationStyle(style) {
1807
+ if (style === undefined) {
1808
+ return this.config.animationStyle;
1809
+ }
1810
+
1811
+ if (this.validationService.isValidAnimationStyle(style)) {
1812
+ this.config.animationStyle = style;
1813
+ }
1814
+
1815
+ return this.config.animationStyle;
1816
+ }
1817
+
1818
+ /**
1819
+ * Gets or sets the simultaneous animation delay
1820
+ * @param {number} [delay] - New delay in milliseconds
1821
+ * @returns {number} Current delay
1822
+ */
1823
+ simultaneousAnimationDelay(delay) {
1824
+ if (delay === undefined) {
1825
+ return this.config.simultaneousAnimationDelay;
1826
+ }
1827
+
1828
+ if (typeof delay === 'number' && delay >= 0) {
1829
+ this.config.simultaneousAnimationDelay = delay;
1830
+ }
1831
+
1832
+ return this.config.simultaneousAnimationDelay;
1833
+ }
1834
+
1835
+ // Additional API methods would be added here following the same pattern
1836
+ // This is a good starting point for the refactored architecture
1837
+
1838
+ // Additional API methods and aliases for backward compatibility
1839
+ insert(square, piece) { return this.putPiece(piece, square); }
1840
+ build() { return this._initialize(); }
1841
+ ascii() { return this.positionService.getGame().ascii(); }
1842
+ board() { return this.positionService.getGame().board(); }
1843
+ getCastlingRights(color) { return this.positionService.getGame().getCastlingRights(color); }
1844
+ getComment() { return this.positionService.getGame().getComment(); }
1845
+ getComments() { return this.positionService.getGame().getComments(); }
1846
+ lastMove() { return this.positionService.getGame().lastMove(); }
1847
+ moveNumber() { return this.positionService.getGame().moveNumber(); }
1848
+ moves(options = {}) { return this.positionService.getGame().moves(options); }
1849
+ squareColor(squareId) { return this.boardService.getSquare(squareId).isWhite() ? 'light' : 'dark'; }
1850
+ isDrawByFiftyMoves() { return this.positionService.getGame().isDrawByFiftyMoves(); }
1851
+ isInsufficientMaterial() { return this.positionService.getGame().isInsufficientMaterial(); }
1852
+ removeComment() { return this.positionService.getGame().removeComment(); }
1853
+ removeComments() { return this.positionService.getGame().removeComments(); }
1854
+ removeHeader(field) { return this.positionService.getGame().removeHeader(field); }
1855
+ setCastlingRights(color, rights) { return this.positionService.getGame().setCastlingRights(color, rights); }
1856
+ setComment(comment) { return this.positionService.getGame().setComment(comment); }
1857
+ setHeader(key, value) { return this.positionService.getGame().setHeader(key, value); }
1858
+ validateFen(fen) { return this.positionService.getGame().validateFen(fen); }
1859
+
1860
+ // Implementazioni reali per highlight/dehighlight
1861
+ highlightSquare(square) {
1862
+ return this.boardService.highlight(square);
1863
+ }
1864
+ dehighlightSquare(square) {
1865
+ return this.boardService.dehighlight(square);
1866
+ }
1867
+ forceSync() { this._updateBoardPieces(true, true); }
1868
+
1869
+ // Metodi mancanti che causano fallimenti nei test
1870
+ /**
1871
+ * Move a piece from one square to another
1872
+ * @param {string|Object} move - Move in format 'e2e4' or {from: 'e2', to: 'e4'}
1873
+ * @param {Object} [opts] - Options
1874
+ * @param {boolean} [opts.animate=true] - Whether to animate
1875
+ * @returns {boolean} True if move was successful
1876
+ */
1877
+ movePiece(move, opts = {}) {
1878
+ const animate = opts.animate !== undefined ? opts.animate : true;
1879
+
1880
+ // Parse move string if needed
1881
+ let fromSquare, toSquare, promotion;
1882
+ if (typeof move === 'string') {
1883
+ if (move.length === 4) {
1884
+ fromSquare = move.substring(0, 2);
1885
+ toSquare = move.substring(2, 4);
1886
+ } else if (move.length === 5) {
1887
+ fromSquare = move.substring(0, 2);
1888
+ toSquare = move.substring(2, 4);
1889
+ promotion = move.substring(4, 5);
1890
+ } else {
1891
+ throw new Error(`Invalid move format: ${move}`);
1892
+ }
1893
+ } else if (typeof move === 'object' && move.from && move.to) {
1894
+ fromSquare = move.from;
1895
+ toSquare = move.to;
1896
+ promotion = move.promotion;
1897
+ } else {
1898
+ throw new Error(`Invalid move: ${move}`);
1899
+ }
1900
+
1901
+ // Get square objects
1902
+ const fromSquareObj = this.boardService.getSquare(fromSquare);
1903
+ const toSquareObj = this.boardService.getSquare(toSquare);
1904
+
1905
+ if (!fromSquareObj || !toSquareObj) {
1906
+ throw new Error(`Invalid squares: ${fromSquare} or ${toSquare}`);
1907
+ }
1908
+
1909
+ // Execute the move
1910
+ return this._onMove(fromSquareObj, toSquareObj, promotion, animate);
1911
+ }
1912
+
1913
+ // Aliases for backward compatibility
1914
+ move(move, animate = true) {
1915
+ // On any new move, clear the redo stack
1916
+ this._undoneMoves = [];
1917
+ return this.movePiece(move, { animate });
1918
+ }
1919
+ get(square) { return this.getPiece(square); }
1920
+ piece(square) { return this.getPiece(square); }
1921
+ put(piece, square, opts = {}) { return this.putPiece(piece, square, opts); }
1922
+ remove(square, opts = {}) { return this.removePiece(square, opts); }
1923
+ load(position, opts = {}) { return this.setPosition(position, opts); }
1924
+ resize(size) { return this.resizeBoard(size); }
1925
+ start(opts = {}) { return this.reset(opts); }
1926
+ clearBoard(opts = {}) { return this.clear(opts); }
1927
+ }
1928
+
1929
+ export { Chessboard };
1930
+ export default Chessboard;