From 3ea2ba18e31163b57b047f783a315ff89795c63e Mon Sep 17 00:00:00 2001 From: Halit Aksoy Date: Fri, 16 Oct 2026 10:49:09 +0300 Subject: [PATCH] fix(core): empty the first pit when animating a single stone move PitAnimator guessed the played pit of a single stone move from the first sowing step and skipped index 0, so playing one stone from player 1's first pit showed it still holding the stone until the animation ended. It now empties the move's own pit (moveIndex). --- packages/core/src/animation/PitAnimator.ts | 19 ++---- .../core/tests/animation/PitAnimator.test.ts | 68 +++++++++++++++++++ 2 files changed, 72 insertions(+), 15 deletions(-) create mode 100644 packages/core/tests/animation/PitAnimator.test.ts diff --git a/packages/core/src/animation/PitAnimator.ts b/packages/core/src/animation/PitAnimator.ts index 219cb8c..035e309 100644 --- a/packages/core/src/animation/PitAnimator.ts +++ b/packages/core/src/animation/PitAnimator.ts @@ -7,6 +7,7 @@ import { GAME_STEP_LAST_STONE_IN_BANK, GAME_STEP_DOUBLE_STONE_IN_PIT, MancalaGame, + MoveHistoryItem, } from "mancala.js"; import { v4 } from "uuid"; import { BoardViewModel } from "../viewmodel/BoardViewModel"; @@ -92,12 +93,6 @@ export class PitAnimator { this.animationIndex++; } - getGameMoveStepCount(historyItem: HistoryItem) { - return historyItem.gameSteps.filter( - (gameStep) => gameStep.type === GAME_STEP_GAME_MOVE - ).length; - } - animatePit( index: number, boardViewModel: BoardViewModel, @@ -106,15 +101,9 @@ export class PitAnimator { if (!this.currentHistoryItem || !this.game || !this.mancalaGame) return; const pitViewModel = boardViewModel.pits[index]; if (this.animationIndex === 0) { - //This is one stone move case, TODO: beautify it later - if (this.getGameMoveStepCount(this.currentHistoryItem) === 1) { - const previousPitIndex = gameStep.index - 1; - if (previousPitIndex > 0) { - boardViewModel.pits[previousPitIndex].stoneCount = 0; - } - } else { - pitViewModel.stoneCount = 0; - } + // The played pit empties before its stones are sown (a single stone goes on to the next pit). + const { moveIndex } = this.currentHistoryItem as MoveHistoryItem; + boardViewModel.pits[moveIndex].stoneCount = 0; } const theme = this.themeManager.theme; if (gameStep.type === GAME_STEP_GAME_MOVE) { diff --git a/packages/core/tests/animation/PitAnimator.test.ts b/packages/core/tests/animation/PitAnimator.test.ts new file mode 100644 index 0000000..7a3f3af --- /dev/null +++ b/packages/core/tests/animation/PitAnimator.test.ts @@ -0,0 +1,68 @@ +import { CommonMancalaGame, MancalaGame } from "mancala.js"; +import { PitAnimator } from "../../src/animation/PitAnimator"; +import { Game } from "../../src/models"; +import { ThemeManager } from "../../src/theme"; +import { BoardViewModel } from "../../src/viewmodel/BoardViewModel"; + +const P1 = "player-1"; +const P2 = "player-2"; + +const memoryStorage = () => { + const values = new Map(); + return { + getString: (key: string) => values.get(key), + setString: (key: string, value: string) => void values.set(key, value), + deleteString: (key: string) => void values.delete(key), + }; +}; + +const asGame = (mancalaGame: MancalaGame): Game => ({ + id: mancalaGame.id, + mancalaGame, + gameUsersConnectionInfo: { + user1ConnectionInfo: { userId: P1, isOnline: true }, + user2ConnectionInfo: { userId: P2, isOnline: true }, + }, +}); + +// Plays the move on a copy of the game and returns the stones the animator shows after each step. +const animateMove = (setUp: (game: MancalaGame) => void, playerId: string, pit: number): number[][] => { + jest.useFakeTimers(); + const createGame = () => { + const game = new CommonMancalaGame("game-1", P1, P2); + game.turnPlayerId = playerId; + setUp(game); + return game; + }; + const before = createGame(); + const after = createGame(); + after.moveByPlayerPit(playerId, pit); + + const frames: number[][] = []; + const animator = new PitAnimator(new ThemeManager(memoryStorage()), (boardViewModel: BoardViewModel) => + frames.push(boardViewModel.pits.map((pitViewModel) => pitViewModel.stoneCount)), 10); + animator.setNewGame(asGame(before)); + frames.length = 0; + animator.setUpdatedGame(asGame(after)); + jest.advanceTimersByTime(10 * (after.history[0].gameSteps.length + 1)); + jest.useRealTimers(); + return frames.slice(1); +}; + +describe("PitAnimator", () => { + it("empties the played pit when a single stone moves on", () => { + for (const pit of [0, 3]) { + const frames = animateMove((game) => { + game.board.pits[pit].stoneCount = 1; + }, P1, pit); + expect(frames[0][pit]).toBe(0); + expect(frames[0][pit + 1]).toBe(5); + } + }); + + it("empties the played pit and sows from it when several stones move", () => { + const frames = animateMove(() => undefined, P1, 0); + expect(frames[0].slice(0, 4)).toEqual([1, 4, 4, 4]); + expect(frames[3].slice(0, 4)).toEqual([1, 5, 5, 5]); + }); +});