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