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).
This commit is contained in:
Halit Aksoy 2026-10-16 10:49:09 +03:00
parent 27f13e2f9a
commit 3ea2ba18e3
2 changed files with 72 additions and 15 deletions

View File

@ -7,6 +7,7 @@ import {
GAME_STEP_LAST_STONE_IN_BANK, GAME_STEP_LAST_STONE_IN_BANK,
GAME_STEP_DOUBLE_STONE_IN_PIT, GAME_STEP_DOUBLE_STONE_IN_PIT,
MancalaGame, MancalaGame,
MoveHistoryItem,
} from "mancala.js"; } from "mancala.js";
import { v4 } from "uuid"; import { v4 } from "uuid";
import { BoardViewModel } from "../viewmodel/BoardViewModel"; import { BoardViewModel } from "../viewmodel/BoardViewModel";
@ -92,12 +93,6 @@ export class PitAnimator {
this.animationIndex++; this.animationIndex++;
} }
getGameMoveStepCount(historyItem: HistoryItem) {
return historyItem.gameSteps.filter(
(gameStep) => gameStep.type === GAME_STEP_GAME_MOVE
).length;
}
animatePit( animatePit(
index: number, index: number,
boardViewModel: BoardViewModel, boardViewModel: BoardViewModel,
@ -106,15 +101,9 @@ export class PitAnimator {
if (!this.currentHistoryItem || !this.game || !this.mancalaGame) return; if (!this.currentHistoryItem || !this.game || !this.mancalaGame) return;
const pitViewModel = boardViewModel.pits[index]; const pitViewModel = boardViewModel.pits[index];
if (this.animationIndex === 0) { if (this.animationIndex === 0) {
//This is one stone move case, TODO: beautify it later // The played pit empties before its stones are sown (a single stone goes on to the next pit).
if (this.getGameMoveStepCount(this.currentHistoryItem) === 1) { const { moveIndex } = this.currentHistoryItem as MoveHistoryItem;
const previousPitIndex = gameStep.index - 1; boardViewModel.pits[moveIndex].stoneCount = 0;
if (previousPitIndex > 0) {
boardViewModel.pits[previousPitIndex].stoneCount = 0;
}
} else {
pitViewModel.stoneCount = 0;
}
} }
const theme = this.themeManager.theme; const theme = this.themeManager.theme;
if (gameStep.type === GAME_STEP_GAME_MOVE) { if (gameStep.type === GAME_STEP_GAME_MOVE) {

View File

@ -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<string, string>();
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]);
});
});