@alepot55/chessboardjs 2.2.2 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -51,7 +51,8 @@ class Chessboard {
51
51
  this._handleConstructorError(error);
52
52
  }
53
53
  this._undoneMoves = [];
54
- this._updateBoardPieces(true, true); // Forza popolamento DOM subito
54
+ this._destroyed = false;
55
+ this._animationTimeouts = [];
55
56
  }
56
57
 
57
58
  /**
@@ -163,6 +164,11 @@ class Chessboard {
163
164
  this._buildSquares();
164
165
  this._addListeners();
165
166
  this._updateBoardPieces(true, true); // Initial position load
167
+
168
+ // Apply flipped class if initial orientation is black
169
+ if (this.coordinateService.getOrientation() === 'b' && this.boardService.element) {
170
+ this.boardService.element.classList.add('flipped');
171
+ }
166
172
  }
167
173
 
168
174
  /**
@@ -191,9 +197,7 @@ class Chessboard {
191
197
  * Best practice: always remove squares (destroy JS/DOM) before clearing the board container.
192
198
  */
193
199
  _buildBoard() {
194
- console.log('CHIAMATO: _buildBoard');
195
200
  if (this._isUndoRedo) {
196
- console.log('SKIP _buildBoard per undo/redo');
197
201
  return;
198
202
  }
199
203
  // Forza la pulizia completa del contenitore board (DOM)
@@ -213,9 +217,7 @@ class Chessboard {
213
217
  * @private
214
218
  */
215
219
  _buildSquares() {
216
- console.log('CHIAMATO: _buildSquares');
217
220
  if (this._isUndoRedo) {
218
- console.log('SKIP _buildSquares per undo/redo');
219
221
  return;
220
222
  }
221
223
  if (this.boardService && this.boardService.removeSquares) {
@@ -428,6 +430,14 @@ class Chessboard {
428
430
  const isEnPassant = this.moveService.isEnPassant(gameMove);
429
431
 
430
432
  if (animate && move.from.piece) {
433
+ // For simultaneous castle, start rook animation alongside the king
434
+ const isSimultaneousCastle = isCastle && this.config.animationStyle === 'simultaneous';
435
+ if (isSimultaneousCastle) {
436
+ setTimeout(() => {
437
+ this._handleCastleMove(gameMove, true);
438
+ }, this.config.simultaneousAnimationDelay);
439
+ }
440
+
431
441
  this.pieceService.translatePiece(
432
442
  move,
433
443
  !!move.to.piece, // was there a capture?
@@ -435,24 +445,22 @@ class Chessboard {
435
445
  this._createDragFunction.bind(this),
436
446
  () => {
437
447
  // After the main piece animation completes...
438
- if (isCastle) {
448
+ // For sequential castle, animate rook AFTER king finishes
449
+ // For simultaneous, rook was already animated above - don't animate again
450
+ if (isCastle && !isSimultaneousCastle) {
439
451
  this._handleSpecialMoveAnimation(gameMove);
440
452
  } else if (isEnPassant) {
441
453
  this._handleSpecialMoveAnimation(gameMove);
442
454
  }
443
455
  // Notify user that the move is fully complete
444
456
  this.config.onMoveEnd(gameMove);
445
- // A final sync to ensure board is perfect
446
- this._updateBoardPieces(false);
457
+ // For simultaneous castle, the rook callback will handle the final sync
458
+ // to avoid interfering with the ongoing rook animation
459
+ if (!isSimultaneousCastle) {
460
+ this._updateBoardPieces(false);
461
+ }
447
462
  }
448
463
  );
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
464
  } else {
457
465
  // If not animating, handle special moves immediately and update the board
458
466
  if (isCastle) {
@@ -505,12 +513,9 @@ class Chessboard {
505
513
  const rookToSquare = this.boardService.getSquare(rookMove.to);
506
514
 
507
515
  if (!rookFromSquare || !rookToSquare || !rookFromSquare.piece) {
508
- console.warn('Castle rook move failed - squares or piece not found');
509
516
  return;
510
517
  }
511
518
 
512
- console.log(`Castle: moving rook from ${rookMove.from} to ${rookMove.to}`);
513
-
514
519
  if (animate) {
515
520
  // Always use translatePiece for smooth sliding animation
516
521
  const rookPiece = rookFromSquare.piece;
@@ -542,12 +547,9 @@ class Chessboard {
542
547
 
543
548
  const capturedSquareObj = this.boardService.getSquare(capturedSquare);
544
549
  if (!capturedSquareObj || !capturedSquareObj.piece) {
545
- console.warn('En passant captured square not found or empty');
546
550
  return;
547
551
  }
548
552
 
549
- console.log(`En passant: removing captured pawn from ${capturedSquare}`);
550
-
551
553
  if (animate) {
552
554
  // Animate the captured pawn removal
553
555
  this.pieceService.removePieceFromSquare(capturedSquareObj, true);
@@ -568,10 +570,9 @@ class Chessboard {
568
570
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load
569
571
  */
570
572
  _updateBoardPieces(animation = false, isPositionLoad = false) {
571
- console.log('CHIAMATO: _updateBoardPieces', { animation, isPositionLoad, isUndoRedo: this._isUndoRedo });
573
+ if (this._destroyed) return;
572
574
  // Check if services are available
573
575
  if (!this.positionService || !this.moveService || !this.eventService) {
574
- console.log('Cannot update board pieces - services not available');
575
576
  return;
576
577
  }
577
578
 
@@ -634,33 +635,24 @@ class Chessboard {
634
635
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load (affects delay)
635
636
  */
636
637
  _doUpdateBoardPieces(animation = false, isPositionLoad = false) {
638
+ if (this._destroyed) return;
637
639
  // Skip update if we're in the middle of a promotion
638
640
  if (this._isPromoting) {
639
- console.log('Skipping board update during promotion');
640
641
  return;
641
642
  }
642
643
 
643
644
  // Check if services are available
644
645
  if (!this.positionService || !this.positionService.getGame()) {
645
- console.log('Cannot update board pieces - position service not available');
646
646
  return;
647
647
  }
648
648
 
649
649
  const squares = this.boardService.getAllSquares();
650
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
651
  const useSimultaneous = this.config.animationStyle === 'simultaneous';
657
- console.log('_doUpdateBoardPieces - useSimultaneous:', useSimultaneous);
658
652
 
659
653
  if (useSimultaneous) {
660
- console.log('Using simultaneous animation');
661
- this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad);
654
+ this._doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad, animation);
662
655
  } else {
663
- console.log('Using sequential animation');
664
656
  this._doSequentialUpdate(squares, gameStateBefore, animation);
665
657
  }
666
658
  }
@@ -673,7 +665,23 @@ class Chessboard {
673
665
  * @param {boolean} animation - Whether to animate
674
666
  */
675
667
  _doSequentialUpdate(squares, gameStateBefore, animation) {
676
- // Mappa: squareId -> expectedPieceId
668
+ // Cancel running animations and clean orphaned elements
669
+ Object.values(squares).forEach(square => {
670
+ const imgs = square.element.querySelectorAll('img.piece');
671
+ imgs.forEach(img => {
672
+ if (img.getAnimations) {
673
+ img.getAnimations().forEach(anim => anim.cancel());
674
+ }
675
+ if (!square.piece || img !== square.piece.element) {
676
+ img.remove();
677
+ }
678
+ });
679
+ if (square.piece && square.piece.element) {
680
+ square.piece.element.style = '';
681
+ square.piece.element.style.opacity = '1';
682
+ }
683
+ });
684
+
677
685
  const expectedMap = {};
678
686
  Object.values(squares).forEach(square => {
679
687
  expectedMap[square.id] = this.positionService.getGamePieceId(square.id);
@@ -689,12 +697,13 @@ class Chessboard {
689
697
  return;
690
698
  }
691
699
 
692
- // Se c'è un pezzo attuale ma non è quello atteso, rimuovilo
700
+ // Remove current piece if it doesn't match expected
693
701
  if (currentPiece && currentPieceId !== expectedPieceId) {
694
- this.pieceService.removePieceFromSquare(square, animation);
702
+ // Always remove synchronously to avoid race condition with addition
703
+ this.pieceService.removePieceFromSquare(square, false);
695
704
  }
696
705
 
697
- // Se c'è un pezzo atteso ma non è quello attuale, aggiungilo
706
+ // Add expected piece if it doesn't match current
698
707
  if (expectedPieceId && currentPieceId !== expectedPieceId) {
699
708
  const newPiece = this.pieceService.convertPiece(expectedPieceId);
700
709
  this.pieceService.addPieceOnSquare(
@@ -719,9 +728,43 @@ class Chessboard {
719
728
  * @param {Object} squares - All squares
720
729
  * @param {string} gameStateBefore - Game state before update
721
730
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load
731
+ * @param {boolean} [animation=true] - Whether to animate
722
732
  */
723
- _doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false) {
724
- // Matching greedy per distanza minima, robusto
733
+ _doSimultaneousUpdate(squares, gameStateBefore, isPositionLoad = false, animation = true) {
734
+ // Increment generation to invalidate stale animation callbacks
735
+ this._updateGeneration = (this._updateGeneration || 0) + 1;
736
+ const generation = this._updateGeneration;
737
+
738
+ // Cancel pending animation timeouts from previous update
739
+ if (this._animationTimeouts) {
740
+ this._animationTimeouts.forEach(tid => clearTimeout(tid));
741
+ this._animationTimeouts = [];
742
+ }
743
+
744
+ // Cancel all running animations and force-sync DOM state
745
+ Object.values(squares).forEach(square => {
746
+ const imgs = square.element.querySelectorAll('img.piece');
747
+ imgs.forEach(img => {
748
+ // Cancel all Web Animations on this element so onfinish callbacks don't fire
749
+ if (img.getAnimations) {
750
+ img.getAnimations().forEach(anim => anim.cancel());
751
+ }
752
+ // Remove orphaned images not matching current piece
753
+ if (!square.piece || img !== square.piece.element) {
754
+ img.remove();
755
+ }
756
+ });
757
+ // Reset current piece element to clean state (remove animation artifacts)
758
+ if (square.piece && square.piece.element) {
759
+ square.piece.element.style = '';
760
+ square.piece.element.style.opacity = '1';
761
+ // Ensure element is attached to correct square
762
+ if (!square.element.contains(square.piece.element)) {
763
+ square.element.appendChild(square.piece.element);
764
+ }
765
+ }
766
+ });
767
+
725
768
  const currentMap = {};
726
769
  const expectedMap = {};
727
770
 
@@ -747,35 +790,16 @@ class Chessboard {
747
790
  const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
748
791
  let animationIndex = 0;
749
792
 
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
- };
793
+ // First pass: compute matching for all piece types
794
+ const allRemovals = [];
795
+ const allAdditions = [];
796
+ const allMoves = [];
773
797
 
774
798
  Object.keys(expectedMap).forEach(key => {
775
799
  const fromList = (currentMap[key] || []).slice();
776
800
  const toList = expectedMap[key].slice();
777
801
 
778
- // 1. Costruisci matrice delle distanze
802
+ // Build distance matrix
779
803
  const distances = [];
780
804
  for (let i = 0; i < fromList.length; i++) {
781
805
  distances[i] = [];
@@ -785,10 +809,9 @@ class Chessboard {
785
809
  }
786
810
  }
787
811
 
788
- // 2. Matching greedy: abbina i più vicini
812
+ // Greedy matching: pair closest pieces
789
813
  const fromMatched = new Array(fromList.length).fill(false);
790
814
  const toMatched = new Array(toList.length).fill(false);
791
- const moves = [];
792
815
 
793
816
  while (true) {
794
817
  let minDist = Infinity, minI = -1, minJ = -1;
@@ -804,58 +827,140 @@ class Chessboard {
804
827
  }
805
828
  }
806
829
  if (minI === -1 || minJ === -1) break;
807
- // Se la posizione è la stessa, non fare nulla (pezzo unchanged)
830
+ fromMatched[minI] = true;
831
+ toMatched[minJ] = true;
832
+ // Skip unchanged pieces (same square)
808
833
  if (fromList[minI].square === toList[minJ].square) {
809
- fromMatched[minI] = true;
810
- toMatched[minJ] = true;
811
834
  continue;
812
835
  }
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;
836
+ allMoves.push({ from: fromList[minI].square, to: toList[minJ].square, piece: fromList[minI].square.piece });
817
837
  }
818
838
 
819
- // 3. Rimuovi i pezzi non abbinati (presenti solo in fromList)
839
+ // Collect unmatched current pieces (to remove)
820
840
  for (let i = 0; i < fromList.length; i++) {
821
841
  if (!fromMatched[i]) {
822
- setTimeout(() => {
823
- this.pieceService.removePieceFromSquare(fromList[i].square, true, onAnimationComplete);
824
- }, animationIndex * animationDelay);
825
- animationIndex++;
842
+ allRemovals.push(fromList[i].square);
826
843
  }
827
844
  }
828
845
 
829
- // 4. Aggiungi i pezzi non abbinati (presenti solo in toList)
846
+ // Collect unmatched expected pieces (to add)
830
847
  for (let j = 0; j < toList.length; j++) {
831
848
  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++;
849
+ allAdditions.push({ square: toList[j].square, key });
843
850
  }
844
851
  }
852
+ });
845
853
 
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++;
854
+ // Also count removals for pieces whose type doesn't exist in expectedMap
855
+ Object.keys(currentMap).forEach(key => {
856
+ if (!expectedMap[key]) {
857
+ currentMap[key].forEach(entry => {
858
+ allRemovals.push(entry.square);
859
+ });
860
+ }
861
+ });
862
+
863
+ // Count only actual animations
864
+ totalAnimations = allRemovals.length + allAdditions.length + allMoves.length;
865
+
866
+ if (totalAnimations === 0) {
867
+ this._addListeners();
868
+ const gameStateAfter = this.positionService.getGame().fen();
869
+ if (gameStateBefore !== gameStateAfter) {
870
+ this.config.onChange(gameStateAfter);
871
+ }
872
+ return;
873
+ }
874
+
875
+ // Detach moving pieces from source squares BEFORE any removals/additions
876
+ // This prevents additions to a move's source square from destroying the piece
877
+ allMoves.forEach(move => {
878
+ if (move.from.piece === move.piece) {
879
+ move.from.removePiece(true); // preserve element, just detach reference
880
+ }
881
+ });
882
+
883
+ // No animation: apply all changes synchronously
884
+ if (!animation) {
885
+ allRemovals.forEach(square => {
886
+ this.pieceService.removePieceFromSquare(square, false);
887
+ });
888
+ allMoves.forEach(move => {
889
+ this.pieceService.translatePiece(
890
+ move, false, false, this._createDragFunction.bind(this)
891
+ );
892
+ });
893
+ allAdditions.forEach(({ square, key }) => {
894
+ const newPiece = this.pieceService.convertPiece(key);
895
+ this.pieceService.addPieceOnSquare(
896
+ square, newPiece, false, this._createDragFunction.bind(this)
897
+ );
858
898
  });
899
+ this._addListeners();
900
+ const gameStateAfter = this.positionService.getGame().fen();
901
+ if (gameStateBefore !== gameStateAfter) {
902
+ this.config.onChange(gameStateAfter);
903
+ }
904
+ return;
905
+ }
906
+
907
+ // Animated path
908
+ if (!this._animationTimeouts) this._animationTimeouts = [];
909
+
910
+ const onAnimationComplete = () => {
911
+ // Ignore callbacks from stale/destroyed boards
912
+ if (this._destroyed || this._updateGeneration !== generation) return;
913
+ animationsCompleted++;
914
+ if (animationsCompleted === totalAnimations) {
915
+ this._addListeners();
916
+ const gameStateAfter = this.positionService.getGame().fen();
917
+ if (gameStateBefore !== gameStateAfter) {
918
+ this.config.onChange(gameStateAfter);
919
+ }
920
+ }
921
+ };
922
+
923
+ // Dispatch moves first (pieces already detached from source)
924
+ allMoves.forEach(move => {
925
+ const tid = setTimeout(() => {
926
+ if (this._destroyed || this._updateGeneration !== generation) return;
927
+ this.pieceService.translatePiece(
928
+ move,
929
+ false,
930
+ true,
931
+ this._createDragFunction.bind(this),
932
+ onAnimationComplete
933
+ );
934
+ }, animationIndex * animationDelay);
935
+ this._animationTimeouts.push(tid);
936
+ animationIndex++;
937
+ });
938
+
939
+ // Dispatch removals
940
+ allRemovals.forEach(square => {
941
+ const tid = setTimeout(() => {
942
+ if (this._destroyed || this._updateGeneration !== generation) return;
943
+ this.pieceService.removePieceFromSquare(square, true, onAnimationComplete);
944
+ }, animationIndex * animationDelay);
945
+ this._animationTimeouts.push(tid);
946
+ animationIndex++;
947
+ });
948
+
949
+ // Dispatch additions
950
+ allAdditions.forEach(({ square, key }) => {
951
+ const tid = setTimeout(() => {
952
+ if (this._destroyed || this._updateGeneration !== generation) return;
953
+ const newPiece = this.pieceService.convertPiece(key);
954
+ this.pieceService.addPieceOnSquare(
955
+ square,
956
+ newPiece,
957
+ true,
958
+ this._createDragFunction.bind(this),
959
+ onAnimationComplete
960
+ );
961
+ }, animationIndex * animationDelay);
962
+ this._animationTimeouts.push(tid);
963
+ animationIndex++;
859
964
  });
860
965
  }
861
966
 
@@ -883,10 +988,6 @@ class Chessboard {
883
988
  }
884
989
  });
885
990
 
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
991
  // Identify different types of changes
891
992
  const moves = []; // Pieces that can slide to new positions
892
993
  const removes = []; // Pieces that need to be removed
@@ -900,8 +1001,6 @@ class Chessboard {
900
1001
  const expectedPieceId = expectedPieces.get(square);
901
1002
 
902
1003
  if (currentPieceId === expectedPieceId) {
903
- // Same piece type on same square - no movement needed
904
- console.log(`UNCHANGED: ${currentPieceId} stays on ${square}`);
905
1004
  unchanged.push({
906
1005
  piece: currentPieceId,
907
1006
  square: square
@@ -923,7 +1022,6 @@ class Chessboard {
923
1022
 
924
1023
  if (availableDestination) {
925
1024
  const [toSquare, expectedId] = availableDestination;
926
- console.log(`MOVE: ${currentPieceId} from ${fromSquare} to ${toSquare}`);
927
1025
  moves.push({
928
1026
  piece: currentPieceId,
929
1027
  from: fromSquare,
@@ -933,8 +1031,6 @@ class Chessboard {
933
1031
  });
934
1032
  processedSquares.add(toSquare);
935
1033
  } else {
936
- // This piece needs to be removed
937
- console.log(`REMOVE: ${currentPieceId} from ${fromSquare}`);
938
1034
  removes.push({
939
1035
  piece: currentPieceId,
940
1036
  square: fromSquare,
@@ -946,7 +1042,6 @@ class Chessboard {
946
1042
  // Third pass: handle pieces that need to be added
947
1043
  expectedPieces.forEach((expectedPieceId, toSquare) => {
948
1044
  if (!processedSquares.has(toSquare)) {
949
- console.log(`ADD: ${expectedPieceId} to ${toSquare}`);
950
1045
  adds.push({
951
1046
  piece: expectedPieceId,
952
1047
  square: toSquare,
@@ -972,26 +1067,13 @@ class Chessboard {
972
1067
  * @param {boolean} [isPositionLoad=false] - Whether this is a position load
973
1068
  */
974
1069
  _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
- }
1070
+ const { moves, removes, adds } = changeAnalysis;
988
1071
 
989
1072
  let animationsCompleted = 0;
990
1073
  const totalAnimations = moves.length + removes.length + adds.length;
991
1074
 
992
1075
  // If no animations are needed, complete immediately
993
1076
  if (totalAnimations === 0) {
994
- console.log('No animations needed, completing immediately');
995
1077
  this._addListeners();
996
1078
 
997
1079
  // Trigger change event if position changed
@@ -1004,9 +1086,7 @@ class Chessboard {
1004
1086
 
1005
1087
  const onAnimationComplete = () => {
1006
1088
  animationsCompleted++;
1007
- console.log(`Animation completed: ${animationsCompleted}/${totalAnimations}`);
1008
1089
  if (animationsCompleted === totalAnimations) {
1009
- console.log('All simultaneous animations completed');
1010
1090
  this._addListeners();
1011
1091
 
1012
1092
  // Trigger change event if position changed
@@ -1017,45 +1097,33 @@ class Chessboard {
1017
1097
  }
1018
1098
  };
1019
1099
 
1020
- // Determine delay: 0 for position loads, configured delay for normal moves
1021
1100
  const animationDelay = isPositionLoad ? 0 : this.config.simultaneousAnimationDelay;
1022
- console.log(`Using animation delay: ${animationDelay}ms (position load: ${isPositionLoad})`);
1023
-
1024
1101
  let animationIndex = 0;
1025
1102
 
1026
- // Process moves (pieces sliding to new positions)
1103
+ // Process moves
1027
1104
  moves.forEach(move => {
1028
1105
  const delay = animationIndex * animationDelay;
1029
- console.log(`Scheduling move ${move.piece} from ${move.from} to ${move.to} with delay ${delay}ms`);
1030
-
1031
1106
  setTimeout(() => {
1032
1107
  this._animatePieceMove(move, onAnimationComplete);
1033
1108
  }, delay);
1034
-
1035
1109
  animationIndex++;
1036
1110
  });
1037
1111
 
1038
- // Process removes (pieces disappearing)
1112
+ // Process removes
1039
1113
  removes.forEach(remove => {
1040
1114
  const delay = animationIndex * animationDelay;
1041
- console.log(`Scheduling removal of ${remove.piece} from ${remove.square} with delay ${delay}ms`);
1042
-
1043
1115
  setTimeout(() => {
1044
1116
  this._animatePieceRemoval(remove, onAnimationComplete);
1045
1117
  }, delay);
1046
-
1047
1118
  animationIndex++;
1048
1119
  });
1049
1120
 
1050
- // Process adds (pieces appearing)
1121
+ // Process adds
1051
1122
  adds.forEach(add => {
1052
1123
  const delay = animationIndex * animationDelay;
1053
- console.log(`Scheduling addition of ${add.piece} to ${add.square} with delay ${delay}ms`);
1054
-
1055
1124
  setTimeout(() => {
1056
1125
  this._animatePieceAddition(add, onAnimationComplete);
1057
1126
  }, delay);
1058
-
1059
1127
  animationIndex++;
1060
1128
  });
1061
1129
  }
@@ -1071,23 +1139,16 @@ class Chessboard {
1071
1139
  const piece = fromSquare.piece;
1072
1140
 
1073
1141
  if (!piece) {
1074
- console.warn(`No piece found on ${move.from} for move animation`);
1075
1142
  onComplete();
1076
1143
  return;
1077
1144
  }
1078
1145
 
1079
- console.log(`Animating piece move: ${move.piece} from ${move.from} to ${move.to}`);
1080
-
1081
- // Use translatePiece for smooth sliding animation
1082
1146
  this.pieceService.translatePiece(
1083
1147
  { from: fromSquare, to: toSquare, piece: piece },
1084
- false, // Assume no capture for now
1085
- true, // Always animate
1148
+ false,
1149
+ true,
1086
1150
  this._createDragFunction.bind(this),
1087
- () => {
1088
- console.log(`Piece move animation completed: ${move.piece} to ${move.to}`);
1089
- onComplete();
1090
- }
1151
+ onComplete
1091
1152
  );
1092
1153
  }
1093
1154
 
@@ -1098,12 +1159,7 @@ class Chessboard {
1098
1159
  * @param {Function} onComplete - Callback when animation completes
1099
1160
  */
1100
1161
  _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
- });
1162
+ this.pieceService.removePieceFromSquare(remove.squareObj, true, onComplete);
1107
1163
  }
1108
1164
 
1109
1165
  /**
@@ -1113,18 +1169,13 @@ class Chessboard {
1113
1169
  * @param {Function} onComplete - Callback when animation completes
1114
1170
  */
1115
1171
  _animatePieceAddition(add, onComplete) {
1116
- console.log(`Animating piece addition: ${add.piece} to ${add.square}`);
1117
-
1118
1172
  const newPiece = this.pieceService.convertPiece(add.piece);
1119
1173
  this.pieceService.addPieceOnSquare(
1120
1174
  add.squareObj,
1121
1175
  newPiece,
1122
1176
  true,
1123
1177
  this._createDragFunction.bind(this),
1124
- () => {
1125
- console.log(`Piece addition animation completed: ${add.piece} to ${add.square}`);
1126
- onComplete();
1127
- }
1178
+ onComplete
1128
1179
  );
1129
1180
  }
1130
1181
 
@@ -1224,7 +1275,7 @@ class Chessboard {
1224
1275
  const animate = opts.animate !== undefined ? opts.animate : true;
1225
1276
  // Use the default starting position from config or fallback
1226
1277
  const startPosition = this.config && this.config.position ? this.config.position : 'start';
1227
- this._updateBoardPieces(animate);
1278
+ // setPosition already calls _updateBoardPieces, don't call it twice
1228
1279
  return this.setPosition(startPosition, { animate });
1229
1280
  }
1230
1281
  /**
@@ -1239,10 +1290,13 @@ class Chessboard {
1239
1290
  return false;
1240
1291
  }
1241
1292
  if (this._clearVisualState) this._clearVisualState();
1293
+
1294
+ // Clear the game state
1242
1295
  this.positionService.getGame().clear();
1243
- if (this._updateBoardPieces) {
1244
- this._updateBoardPieces(animate, true);
1245
- }
1296
+
1297
+ // Let _updateBoardPieces handle removal (no manual loop to avoid race conditions)
1298
+ this._updateBoardPieces(animate, true);
1299
+
1246
1300
  return true;
1247
1301
  }
1248
1302
 
@@ -1279,6 +1333,32 @@ class Chessboard {
1279
1333
  }
1280
1334
  return false;
1281
1335
  }
1336
+ /**
1337
+ * Move a piece from one square to another
1338
+ * @param {string} moveStr - Move in format 'e2e4' or 'e7e8q' (with promotion)
1339
+ * @param {Object} [opts]
1340
+ * @param {boolean} [opts.animate=true]
1341
+ * @returns {Object|boolean} Move result or false if invalid
1342
+ */
1343
+ movePiece(moveStr, opts = {}) {
1344
+ const animate = opts.animate !== false;
1345
+ if (typeof moveStr !== 'string' || moveStr.length < 4) {
1346
+ return false;
1347
+ }
1348
+ const from = moveStr.slice(0, 2);
1349
+ const to = moveStr.slice(2, 4);
1350
+ const promotion = moveStr.length > 4 ? moveStr[4].toLowerCase() : undefined;
1351
+
1352
+ const moveObj = { from, to };
1353
+ if (promotion) moveObj.promotion = promotion;
1354
+
1355
+ const result = this.positionService.getGame().move(moveObj);
1356
+ if (result) {
1357
+ this._updateBoardPieces(animate);
1358
+ }
1359
+ return result || false;
1360
+ }
1361
+
1282
1362
  /**
1283
1363
  * Get legal moves for a square
1284
1364
  * @param {string} square
@@ -1293,10 +1373,10 @@ class Chessboard {
1293
1373
  * @returns {string|null}
1294
1374
  */
1295
1375
  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;
1376
+ // Use game state as source of truth
1377
+ // Returns piece in format 'wq' (color + type)
1378
+ if (!this.positionService || !this.positionService.getGame()) return null;
1379
+ const piece = this.positionService.getGame().get(square);
1300
1380
  if (!piece) return null;
1301
1381
  return (piece.color + piece.type).toLowerCase();
1302
1382
  }
@@ -1357,45 +1437,355 @@ class Chessboard {
1357
1437
  if (!this.validationService.isValidSquare(square)) {
1358
1438
  throw new Error(`[removePiece] Invalid square: ${square}`);
1359
1439
  }
1360
- const squareObj = this.boardService.getSquare(square);
1361
- if (!squareObj) return true;
1362
- if (!squareObj.piece) return true;
1363
- squareObj.piece = null;
1440
+ if (!this.positionService || !this.positionService.getGame()) {
1441
+ return false;
1442
+ }
1364
1443
  const game = this.positionService.getGame();
1365
- game.remove(square);
1444
+ // Remove from game state first (source of truth)
1445
+ const removed = game.remove(square);
1446
+ // Then update the board visually
1447
+ const squareObj = this.boardService.getSquare(square);
1448
+ if (squareObj) {
1449
+ squareObj.piece = null;
1450
+ }
1366
1451
  this._updateBoardPieces(animate);
1367
- return true;
1452
+ return removed !== null;
1368
1453
  }
1369
1454
 
1370
1455
  // --- BOARD CONTROL ---
1371
1456
  /**
1372
1457
  * Flip the board orientation
1373
1458
  * @param {Object} [opts]
1374
- * @param {boolean} [opts.animate=true]
1459
+ * @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
1460
+ * @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
1375
1461
  */
1376
1462
  flipBoard(opts = {}) {
1463
+ const flipMode = opts.mode || this.config.flipMode || 'visual';
1464
+
1465
+ // Update internal orientation state
1377
1466
  if (this.coordinateService && this.coordinateService.flipOrientation) {
1378
1467
  this.coordinateService.flipOrientation();
1379
1468
  }
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());
1469
+
1470
+ const boardElement = this.boardService.element;
1471
+ const isFlipped = this.coordinateService.getOrientation() === 'b';
1472
+
1473
+ switch (flipMode) {
1474
+ case 'visual':
1475
+ // CSS flexbox flip - instant, no piece animation needed
1476
+ this._flipVisual(boardElement, isFlipped);
1477
+ break;
1478
+
1479
+ case 'animate':
1480
+ // Animate pieces to mirrored positions
1481
+ this._flipAnimate(opts.animate !== false);
1482
+ break;
1483
+
1484
+ case 'none':
1485
+ // No visual change - only internal orientation updated
1486
+ // Useful for programmatic orientation without visual feedback
1487
+ break;
1488
+
1489
+ default:
1490
+ this._flipVisual(boardElement, isFlipped);
1491
+ }
1492
+ }
1493
+
1494
+ /**
1495
+ * Visual flip using CSS flexbox (instant)
1496
+ * @private
1497
+ * @param {HTMLElement} boardElement - Board DOM element
1498
+ * @param {boolean} isFlipped - Whether board should be flipped
1499
+ */
1500
+ _flipVisual(boardElement, isFlipped) {
1501
+ if (!boardElement) return;
1502
+
1503
+ if (isFlipped) {
1504
+ boardElement.classList.add('flipped');
1505
+ } else {
1506
+ boardElement.classList.remove('flipped');
1507
+ }
1508
+ }
1509
+
1510
+ /**
1511
+ * Animate flip using FLIP technique (First-Last-Invert-Play)
1512
+ * Same end state as visual mode (CSS flip), but pieces animate smoothly.
1513
+ * @private
1514
+ * @param {boolean} animate - Whether to animate the movement
1515
+ */
1516
+ _flipAnimate(animate) {
1517
+ const boardElement = this.boardService.element;
1518
+ if (!boardElement) return;
1519
+
1520
+ const squares = this.boardService.getAllSquares();
1521
+
1522
+ // FIRST: Record current visual position of every piece
1523
+ const pieceRects = {};
1524
+ for (const [id, square] of Object.entries(squares)) {
1525
+ if (square.piece && square.piece.element) {
1526
+ pieceRects[id] = square.piece.element.getBoundingClientRect();
1527
+ }
1528
+ }
1529
+
1530
+ // LAST: Apply CSS flip (instant) - same as visual mode
1531
+ const isFlipped = this.coordinateService.getOrientation() === 'b';
1532
+ this._flipVisual(boardElement, isFlipped);
1533
+
1534
+ if (!animate || Object.keys(pieceRects).length === 0) return;
1535
+
1536
+ // INVERT + PLAY: Animate each piece from old position to new
1537
+ const duration = this.config.moveTime || 200;
1538
+ const easing = 'cubic-bezier(0.33, 1, 0.68, 1)';
1539
+
1540
+ for (const [id, oldRect] of Object.entries(pieceRects)) {
1541
+ const square = squares[id];
1542
+ if (!square || !square.piece || !square.piece.element) continue;
1543
+
1544
+ const piece = square.piece;
1545
+ const newRect = piece.element.getBoundingClientRect();
1546
+ const dx = oldRect.left - newRect.left;
1547
+ const dy = oldRect.top - newRect.top;
1548
+
1549
+ if (Math.abs(dx) < 1 && Math.abs(dy) < 1) continue;
1550
+
1551
+ if (piece.element.animate) {
1552
+ const anim = piece.element.animate([
1553
+ { transform: `translate(${dx}px, ${dy}px)` },
1554
+ { transform: 'translate(0, 0)' }
1555
+ ], { duration, easing, fill: 'forwards' });
1556
+ anim.onfinish = () => {
1557
+ anim.cancel();
1558
+ if (piece.element) piece.element.style.transform = '';
1559
+ };
1560
+ } else {
1561
+ // setTimeout fallback for jsdom / older browsers
1562
+ piece.element.style.transform = `translate(${dx}px, ${dy}px)`;
1563
+ setTimeout(() => {
1564
+ if (!piece.element) return;
1565
+ piece.element.style.transition = `transform ${duration}ms`;
1566
+ piece.element.style.transform = 'translate(0, 0)';
1567
+ setTimeout(() => {
1568
+ if (!piece.element) return;
1569
+ piece.element.style.transition = '';
1570
+ piece.element.style.transform = '';
1571
+ }, duration);
1572
+ }, 0);
1573
+ }
1574
+ }
1575
+ }
1576
+
1577
+ /**
1578
+ * Set the flip mode at runtime
1579
+ * @param {'visual'|'animate'|'none'} mode - The flip mode to use
1580
+ */
1581
+ setFlipMode(mode) {
1582
+ const validModes = ['visual', 'animate', 'none'];
1583
+ if (!validModes.includes(mode)) {
1584
+ console.warn(`Invalid flip mode: ${mode}. Valid options: ${validModes.join(', ')}`);
1585
+ return;
1586
+ }
1587
+ this.config.flipMode = mode;
1588
+ }
1589
+
1590
+ /**
1591
+ * Get the current flip mode
1592
+ * @returns {string} Current flip mode
1593
+ */
1594
+ getFlipMode() {
1595
+ return this.config.flipMode || 'visual';
1596
+ }
1597
+
1598
+ // --- MOVEMENT CONFIGURATION ---
1599
+
1600
+ /**
1601
+ * Set the movement style
1602
+ * @param {'slide'|'arc'|'hop'|'teleport'|'fade'} style - Movement style
1603
+ */
1604
+ setMoveStyle(style) {
1605
+ const validStyles = ['slide', 'arc', 'hop', 'teleport', 'fade'];
1606
+ if (!validStyles.includes(style)) {
1607
+ console.warn(`Invalid move style: ${style}. Valid: ${validStyles.join(', ')}`);
1608
+ return;
1609
+ }
1610
+ this.config.moveStyle = style;
1611
+ }
1612
+
1613
+ /**
1614
+ * Get the current movement style
1615
+ * @returns {string} Current movement style
1616
+ */
1617
+ getMoveStyle() {
1618
+ return this.config.moveStyle || 'slide';
1619
+ }
1620
+
1621
+ /**
1622
+ * Set the capture animation style
1623
+ * @param {'fade'|'shrink'|'instant'|'explode'} style - Capture style
1624
+ */
1625
+ setCaptureStyle(style) {
1626
+ const validStyles = ['fade', 'shrink', 'instant', 'explode'];
1627
+ if (!validStyles.includes(style)) {
1628
+ console.warn(`Invalid capture style: ${style}. Valid: ${validStyles.join(', ')}`);
1629
+ return;
1630
+ }
1631
+ this.config.captureStyle = style;
1632
+ }
1633
+
1634
+ /**
1635
+ * Get the current capture style
1636
+ * @returns {string} Current capture style
1637
+ */
1638
+ getCaptureStyle() {
1639
+ return this.config.captureStyle || 'fade';
1640
+ }
1641
+
1642
+ /**
1643
+ * Set the appearance animation style
1644
+ * @param {'fade'|'pulse'|'pop'|'drop'|'instant'} style - Appearance style
1645
+ */
1646
+ setAppearanceStyle(style) {
1647
+ const validStyles = ['fade', 'pulse', 'pop', 'drop', 'instant'];
1648
+ if (!validStyles.includes(style)) {
1649
+ console.warn(`Invalid appearance style: ${style}. Valid: ${validStyles.join(', ')}`);
1650
+ return;
1651
+ }
1652
+ this.config.appearanceStyle = style;
1653
+ }
1654
+
1655
+ /**
1656
+ * Get the current appearance style
1657
+ * @returns {string} Current appearance style
1658
+ */
1659
+ getAppearanceStyle() {
1660
+ return this.config.appearanceStyle || 'fade';
1661
+ }
1662
+
1663
+ /**
1664
+ * Set the landing effect
1665
+ * @param {'none'|'bounce'|'pulse'|'settle'} effect - Landing effect
1666
+ */
1667
+ setLandingEffect(effect) {
1668
+ const validEffects = ['none', 'bounce', 'pulse', 'settle'];
1669
+ if (!validEffects.includes(effect)) {
1670
+ console.warn(`Invalid landing effect: ${effect}. Valid: ${validEffects.join(', ')}`);
1671
+ return;
1672
+ }
1673
+ this.config.landingEffect = effect;
1385
1674
  }
1675
+
1676
+ /**
1677
+ * Get the current landing effect
1678
+ * @returns {string} Current landing effect
1679
+ */
1680
+ getLandingEffect() {
1681
+ return this.config.landingEffect || 'none';
1682
+ }
1683
+
1684
+ /**
1685
+ * Set the movement duration
1686
+ * @param {number|string} duration - Duration in ms or preset name ('instant', 'veryFast', 'fast', 'normal', 'slow', 'verySlow')
1687
+ */
1688
+ setMoveTime(duration) {
1689
+ const presets = { instant: 0, veryFast: 100, fast: 200, normal: 400, slow: 600, verySlow: 1000 };
1690
+ if (typeof duration === 'string' && presets[duration] !== undefined) {
1691
+ this.config.moveTime = presets[duration];
1692
+ } else if (typeof duration === 'number' && duration >= 0) {
1693
+ this.config.moveTime = duration;
1694
+ } else {
1695
+ console.warn(`Invalid move time: ${duration}`);
1696
+ }
1697
+ }
1698
+
1699
+ /**
1700
+ * Get the current movement duration
1701
+ * @returns {number} Duration in ms
1702
+ */
1703
+ getMoveTime() {
1704
+ return this.config.moveTime;
1705
+ }
1706
+
1707
+ /**
1708
+ * Set the easing function for movements
1709
+ * @param {string} easing - CSS easing function
1710
+ */
1711
+ setMoveEasing(easing) {
1712
+ const validEasings = ['ease', 'linear', 'ease-in', 'ease-out', 'ease-in-out'];
1713
+ if (!validEasings.includes(easing)) {
1714
+ console.warn(`Invalid easing: ${easing}. Valid: ${validEasings.join(', ')}`);
1715
+ return;
1716
+ }
1717
+ this.config.moveEasing = easing;
1718
+ }
1719
+
1720
+ /**
1721
+ * Configure multiple movement settings at once
1722
+ * @param {Object} options - Movement configuration
1723
+ * @param {string} [options.style] - Movement style
1724
+ * @param {string} [options.captureStyle] - Capture animation style
1725
+ * @param {string} [options.landingEffect] - Landing effect
1726
+ * @param {number|string} [options.duration] - Movement duration
1727
+ * @param {string} [options.easing] - Easing function
1728
+ * @param {number} [options.arcHeight] - Arc height for arc/hop styles (0-1)
1729
+ */
1730
+ configureMovement(options) {
1731
+ if (options.style) this.setMoveStyle(options.style);
1732
+ if (options.captureStyle) this.setCaptureStyle(options.captureStyle);
1733
+ if (options.appearanceStyle) this.setAppearanceStyle(options.appearanceStyle);
1734
+ if (options.landingEffect) this.setLandingEffect(options.landingEffect);
1735
+ if (options.duration !== undefined) this.setMoveTime(options.duration);
1736
+ if (options.easing) this.setMoveEasing(options.easing);
1737
+ if (options.arcHeight !== undefined) {
1738
+ this.config.moveArcHeight = Math.max(0, Math.min(1, options.arcHeight));
1739
+ }
1740
+ }
1741
+
1742
+ /**
1743
+ * Get all movement configuration
1744
+ * @returns {Object} Current movement configuration
1745
+ */
1746
+ getMovementConfig() {
1747
+ return {
1748
+ style: this.config.moveStyle || 'slide',
1749
+ captureStyle: this.config.captureStyle || 'fade',
1750
+ appearanceStyle: this.config.appearanceStyle || 'fade',
1751
+ landingEffect: this.config.landingEffect || 'none',
1752
+ duration: this.config.moveTime,
1753
+ easing: this.config.moveEasing || 'ease',
1754
+ arcHeight: this.config.moveArcHeight || 0.3
1755
+ };
1756
+ }
1757
+
1386
1758
  /**
1387
1759
  * Set the board orientation
1388
1760
  * @param {'w'|'b'} color
1389
1761
  * @param {Object} [opts]
1390
- * @param {boolean} [opts.animate=true]
1762
+ * @param {boolean} [opts.animate=true] - Enable animation (for 'animate' mode)
1763
+ * @param {string} [opts.mode] - Override flip mode ('visual', 'animate', 'none')
1391
1764
  */
1392
1765
  setOrientation(color, opts = {}) {
1393
1766
  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);
1767
+ const currentOrientation = this.coordinateService.getOrientation();
1768
+ if (currentOrientation !== color) {
1769
+ this.coordinateService.setOrientation(color);
1770
+
1771
+ const flipMode = opts.mode || this.config.flipMode || 'visual';
1772
+ const boardElement = this.boardService.element;
1773
+ const isFlipped = color === 'b';
1774
+
1775
+ switch (flipMode) {
1776
+ case 'visual':
1777
+ this._flipVisual(boardElement, isFlipped);
1778
+ break;
1779
+ case 'animate':
1780
+ this._flipAnimate(opts.animate !== false);
1781
+ break;
1782
+ case 'none':
1783
+ // No visual change
1784
+ break;
1785
+ default:
1786
+ this._flipVisual(boardElement, isFlipped);
1787
+ }
1788
+ }
1399
1789
  }
1400
1790
  return this.coordinateService.getOrientation();
1401
1791
  }
@@ -1513,7 +1903,40 @@ class Chessboard {
1513
1903
  /**
1514
1904
  * Destroy the board and cleanup
1515
1905
  */
1516
- destroy() { /* TODO: robust destroy logic */ }
1906
+ destroy() {
1907
+ this._destroyed = true;
1908
+
1909
+ // Remove all event listeners
1910
+ if (this.eventService) {
1911
+ this.eventService.removeAllListeners();
1912
+ this.eventService.destroy();
1913
+ }
1914
+
1915
+ // Clear all timeouts
1916
+ if (this._updateTimeout) {
1917
+ clearTimeout(this._updateTimeout);
1918
+ this._updateTimeout = null;
1919
+ }
1920
+
1921
+ // Clear all animation timeouts
1922
+ if (this._animationTimeouts) {
1923
+ this._animationTimeouts.forEach(tid => clearTimeout(tid));
1924
+ this._animationTimeouts = [];
1925
+ }
1926
+
1927
+ // Destroy services
1928
+ if (this.moveService) this.moveService.destroy();
1929
+ if (this.animationService && this.animationService.destroy) this.animationService.destroy();
1930
+ if (this.pieceService && this.pieceService.destroy) this.pieceService.destroy();
1931
+ if (this.boardService && this.boardService.destroy) this.boardService.destroy();
1932
+ if (this.positionService && this.positionService.destroy) this.positionService.destroy();
1933
+ if (this.coordinateService && this.coordinateService.destroy) this.coordinateService.destroy();
1934
+ if (this.validationService) this.validationService.destroy();
1935
+ if (this.config && this.config.destroy) this.config.destroy();
1936
+
1937
+ // Clear references
1938
+ this._cleanup();
1939
+ }
1517
1940
  /**
1518
1941
  * Rebuild the board
1519
1942
  */
@@ -1529,7 +1952,11 @@ class Chessboard {
1529
1952
  * Set new config
1530
1953
  * @param {Object} newConfig
1531
1954
  */
1532
- setConfig(newConfig) { this.setConfig(newConfig); }
1955
+ setConfig(newConfig) {
1956
+ if (this.config && typeof this.config.update === 'function') {
1957
+ this.config.update(newConfig);
1958
+ }
1959
+ }
1533
1960
 
1534
1961
  // --- ALIASES/DEPRECATED ---
1535
1962
  /**
@@ -1564,17 +1991,10 @@ class Chessboard {
1564
1991
  }
1565
1992
 
1566
1993
  /**
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
1994
+ * Gets or sets the current position
1995
+ * @param {string|Object} [position] - Position to set (FEN or object). If omitted, returns current position.
1996
+ * @param {boolean} [animate=true] - Whether to animate when setting
1997
+ * @returns {Object} Current position object (when getting)
1578
1998
  */
1579
1999
  position(position, animate = true) {
1580
2000
  if (position === undefined) {
@@ -1837,34 +2257,21 @@ class Chessboard {
1837
2257
  // Ensure all public API methods from README are present and routed
1838
2258
  insert(square, piece) { return this.putPiece(piece, square); }
1839
2259
  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 }); }
2260
+ // Note: position() is defined above at line ~1684 with getter/setter functionality
1845
2261
  build() { return this._initialize(); }
1846
2262
  resize(value) { return this.resizeBoard(value); }
1847
- destroy() { return this._cleanup(); }
1848
2263
  piece(square) { return this.getPiece(square); }
1849
- highlight(square) { return true; }
1850
- dehighlight(square) { return true; }
1851
- turn() { return this.positionService.getGame().turn(); }
1852
2264
  ascii() { return this.positionService.getGame().ascii(); }
1853
2265
  board() { return this.positionService.getGame().board(); }
1854
2266
  getCastlingRights(color) { return this.positionService.getGame().getCastlingRights(color); }
1855
2267
  getComment() { return this.positionService.getGame().getComment(); }
1856
2268
  getComments() { return this.positionService.getGame().getComments(); }
1857
- history(options = {}) { return this.positionService.getGame().history(options); }
1858
2269
  lastMove() { return this.positionService.getGame().lastMove(); }
1859
2270
  moveNumber() { return this.positionService.getGame().moveNumber(); }
1860
2271
  moves(options = {}) { return this.positionService.getGame().moves(options); }
1861
- pgn(options = {}) { return this.positionService.getGame().pgn(options); }
1862
2272
  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
2273
  isDrawByFiftyMoves() { return this.positionService.getGame().isDrawByFiftyMoves(); }
1866
2274
  isInsufficientMaterial() { return this.positionService.getGame().isInsufficientMaterial(); }
1867
- isGameOver() { return this.positionService.getGame().isGameOver(); }
1868
2275
  isStalemate() { return this.positionService.getGame().isStalemate(); }
1869
2276
  isThreefoldRepetition() { return this.positionService.getGame().isThreefoldRepetition(); }
1870
2277
  load(fen, options = {}, animation = true) { return this.setPosition(fen, { ...options, animate: animation }); }