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