@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.
- package/dist/chessboard.cjs.js +1477 -382
- package/dist/chessboard.css +22 -7
- package/dist/chessboard.esm.js +1477 -382
- package/dist/chessboard.iife.js +1477 -382
- package/dist/chessboard.umd.js +1477 -382
- package/package.json +18 -3
- package/src/components/Piece.js +509 -26
- package/src/components/Square.js +3 -3
- package/src/core/Chessboard.js +625 -218
- package/src/core/ChessboardConfig.js +257 -8
- package/src/services/MoveService.js +37 -99
- package/src/services/PieceService.js +51 -24
- package/src/styles/board.css +22 -3
- package/.eslintrc.json +0 -227
- package/chessboard.bundle.js +0 -4072
- package/config/.babelrc +0 -4
- package/config/jest.config.js +0 -15
- package/config/rollup.config.js +0 -36
- package/jest.config.js +0 -2
- package/rollup.config.js +0 -2
- package/tests/unit/chessboard-config-animations.test.js +0 -106
- package/tests/unit/chessboard-robust.test.js +0 -163
- package/tests/unit/chessboard.test.js +0 -183
package/src/core/Chessboard.js
CHANGED
|
@@ -51,7 +51,8 @@ class Chessboard {
|
|
|
51
51
|
this._handleConstructorError(error);
|
|
52
52
|
}
|
|
53
53
|
this._undoneMoves = [];
|
|
54
|
-
this.
|
|
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
|
-
|
|
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
|
-
//
|
|
446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
700
|
+
// Remove current piece if it doesn't match expected
|
|
693
701
|
if (currentPiece && currentPieceId !== expectedPieceId) {
|
|
694
|
-
|
|
702
|
+
// Always remove synchronously to avoid race condition with addition
|
|
703
|
+
this.pieceService.removePieceFromSquare(square, false);
|
|
695
704
|
}
|
|
696
705
|
|
|
697
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
751
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
839
|
+
// Collect unmatched current pieces (to remove)
|
|
820
840
|
for (let i = 0; i < fromList.length; i++) {
|
|
821
841
|
if (!fromMatched[i]) {
|
|
822
|
-
|
|
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
|
-
//
|
|
846
|
+
// Collect unmatched expected pieces (to add)
|
|
830
847
|
for (let j = 0; j < toList.length; j++) {
|
|
831
848
|
if (!toMatched[j]) {
|
|
832
|
-
|
|
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
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
1085
|
-
true,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1244
|
-
|
|
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
|
-
//
|
|
1297
|
-
|
|
1298
|
-
if (!
|
|
1299
|
-
const 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
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
squareObj.piece = null;
|
|
1440
|
+
if (!this.positionService || !this.positionService.getGame()) {
|
|
1441
|
+
return false;
|
|
1442
|
+
}
|
|
1364
1443
|
const game = this.positionService.getGame();
|
|
1365
|
-
game
|
|
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
|
|
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
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
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.
|
|
1395
|
-
if (
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
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() {
|
|
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) {
|
|
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
|
|
1568
|
-
* @
|
|
1569
|
-
|
|
1570
|
-
|
|
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(
|
|
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 }); }
|