Skip to content

Commit b844916

Browse files
fix: promoting moves are animated (#221)
1 parent eef199d commit b844916

File tree

2 files changed

+144
-0
lines changed

2 files changed

+144
-0
lines changed

src/ChessboardProvider.tsx

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import {
2323
import {
2424
fenStringToPositionObject,
2525
generateBoard,
26+
getPromotionUpdates,
2627
getPositionUpdates,
2728
} from './utils';
2829
import {
@@ -390,6 +391,34 @@ export function ChessboardProvider({
390391
return;
391392
}
392393

394+
// this extra position check will only run if the position HAS changed, but we have been unable to identify the pieces that have moved, reducing the impact of the performance overhead
395+
if (Object.keys(positionUpdates).length === 0) {
396+
const promotionUpdates = getPromotionUpdates(
397+
currentWaitingForAnimationPosition ?? currentPosition,
398+
newPosition,
399+
chessboardRows,
400+
chessboardColumns,
401+
);
402+
// Play the animation only if one promoting move was found and no updates were found
403+
// This does not animate in cases like:
404+
// - position changes where non promoting pieces moved
405+
// - multiple pawn promotions or promotion reversals
406+
// - removing one pawn from the board and promoting one nearby. would result in both pawns animating to the same promotion square
407+
if (Object.keys(promotionUpdates).length === 1) {
408+
setPositionDifferences(promotionUpdates);
409+
setWaitingForAnimationPosition(newPosition);
410+
411+
const newTimeout = setTimeout(() => {
412+
setCurrentPosition(newPosition);
413+
setPositionDifferences({});
414+
setWaitingForAnimationPosition(null);
415+
}, animationDurationInMs);
416+
417+
animationTimeoutRef.current = newTimeout;
418+
return;
419+
}
420+
}
421+
393422
// new position was a result of an external move
394423

395424
setPositionDifferences(positionUpdates);

src/utils.ts

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,121 @@ export function getPositionUpdates(
269269
return updates;
270270
}
271271

272+
/*
273+
* Returns all position changes that can be explained by a pawn promotion or pawn promotion reversal
274+
*/
275+
export function getPromotionUpdates(
276+
oldPosition: PositionDataType,
277+
newPosition: PositionDataType,
278+
noOfRows: number,
279+
noOfColumns: number,
280+
): { [square: string]: string } {
281+
const promotionUpdates: { [square: string]: string } = {};
282+
for (const newSquare in newPosition) {
283+
// new piece hasn't moved
284+
if (
285+
oldPosition[newSquare]?.pieceType === newPosition[newSquare].pieceType
286+
) {
287+
continue;
288+
}
289+
for (const oldSquare in oldPosition) {
290+
// old piece hasn't moved
291+
if (
292+
newPosition[oldSquare]?.pieceType === oldPosition[oldSquare].pieceType
293+
) {
294+
continue;
295+
}
296+
if (
297+
pawnPromotesAtSquares(
298+
oldPosition,
299+
newPosition,
300+
oldSquare,
301+
newSquare,
302+
noOfRows,
303+
noOfColumns,
304+
)
305+
) {
306+
promotionUpdates[oldSquare] = newSquare;
307+
}
308+
}
309+
}
310+
return promotionUpdates;
311+
}
312+
313+
/*
314+
Helper method that determines if the changes at oldSquare and newSquare
315+
can be explained by a pawn promotion or pawn promotion reversal
316+
317+
Criteria:
318+
promotion can only happen in adjacent files or same file
319+
white promotes from P to [Q, R, B, N] from rank n - 1 to n
320+
black promotes from P to [Q, R, B, N] from rank 2 to 1
321+
*/
322+
function pawnPromotesAtSquares(
323+
oldPosition: PositionDataType,
324+
newPosition: PositionDataType,
325+
oldSquare: string,
326+
newSquare: string,
327+
noOfRows: number,
328+
noOfColumns: number,
329+
): boolean {
330+
const oldPiece = oldPosition[oldSquare].pieceType;
331+
const newPiece = newPosition[newSquare].pieceType;
332+
const promotablePieces = new Set(['Q', 'R', 'B', 'N']);
333+
334+
let pawnSquare;
335+
let promotionSquare;
336+
if (oldPiece[1] === 'P' && promotablePieces.has(newPiece[1])) {
337+
// promotion
338+
pawnSquare = oldSquare;
339+
promotionSquare = newSquare;
340+
} else if (newPiece[1] === 'P' && promotablePieces.has(oldPiece[1])) {
341+
// promotion reversal
342+
pawnSquare = newSquare;
343+
promotionSquare = oldSquare;
344+
} else {
345+
return false;
346+
}
347+
348+
const columnDifference = Math.abs(
349+
chessColumnToColumnIndex(pawnSquare[0], noOfColumns, 'black') -
350+
chessColumnToColumnIndex(promotionSquare[0], noOfColumns, 'black'),
351+
);
352+
353+
// ensure piece has moved at most 1 column away and is the same colour
354+
if (columnDifference > 1 || oldPiece[0] !== newPiece[0]) {
355+
return false;
356+
}
357+
358+
const pawnSquareRowIndex = chessRowToRowIndex(
359+
pawnSquare[1],
360+
noOfRows,
361+
'black',
362+
);
363+
const promotionSquareRowIndex = chessRowToRowIndex(
364+
promotionSquare[1],
365+
noOfRows,
366+
'black',
367+
);
368+
if (
369+
oldPiece[0] === 'b' &&
370+
pawnSquareRowIndex === 1 &&
371+
promotionSquareRowIndex === 0
372+
) {
373+
// bP promotion or promotion reversal
374+
return true;
375+
} else if (
376+
oldPiece[0] === 'w' &&
377+
pawnSquareRowIndex === noOfRows - 2 &&
378+
promotionSquareRowIndex === noOfRows - 1
379+
) {
380+
// wP promotion or promotion reversal
381+
return true;
382+
} else {
383+
return false;
384+
}
385+
}
386+
272387
/**
273388
* Retrieves the coordinates at the centre of the requested square, relative to the top left of the board (0, 0).
274389
*/

0 commit comments

Comments
 (0)