Problem
Both Game.move() (line ~959) and Game.executeAndRecalculate() (line ~1395) push pre-move snapshots onto activePlay.undo.frames. Since executeAndRecalculate() calls Game.move() internally (line ~1409), two snapshots are pushed for a single logical move when the move is initiated through executeAndRecalculate.
This means:
- Calling
undoLastInActivePlay() once only pops the inner snapshot — the game state is still post-move
- Users/robots must call undo twice to revert a single move made through
executeAndRecalculate
- The undo stack grows at 2x the expected rate, wasting memory
Reproduction
- Call
Game.executeAndRecalculate(game, originId) to make a move
- Inspect
game.activePlay.undo.frames.length — it has 2 entries instead of 1
- Call
Game.undoLastInActivePlay(game) — the board state doesn't match the pre-move state
Affected code
src/Game/index.ts — Game.move() snapshot push (~line 959)
src/Game/index.ts — Game.executeAndRecalculate() snapshot push (~line 1395)
Implementation Plan
- Remove the snapshot push from
Game.executeAndRecalculate() (lines ~1394-1407) — let Game.move() be the single source of truth for undo snapshots
- Add a unit test that verifies: after calling
executeAndRecalculate once, undo.frames.length === 1
- Add a test that verifies: calling
undoLastInActivePlay after a single executeAndRecalculate move restores the exact pre-move board state
- Verify
switchDice gating still works correctly with single-snapshot undo stack
Files to modify
src/Game/index.ts — remove duplicate snapshot in executeAndRecalculate
src/Game/__tests__/undo-stack.test.ts — new test file
Problem
Both
Game.move()(line ~959) andGame.executeAndRecalculate()(line ~1395) push pre-move snapshots ontoactivePlay.undo.frames. SinceexecuteAndRecalculate()callsGame.move()internally (line ~1409), two snapshots are pushed for a single logical move when the move is initiated throughexecuteAndRecalculate.This means:
undoLastInActivePlay()once only pops the inner snapshot — the game state is still post-moveexecuteAndRecalculateReproduction
Game.executeAndRecalculate(game, originId)to make a movegame.activePlay.undo.frames.length— it has 2 entries instead of 1Game.undoLastInActivePlay(game)— the board state doesn't match the pre-move stateAffected code
src/Game/index.ts—Game.move()snapshot push (~line 959)src/Game/index.ts—Game.executeAndRecalculate()snapshot push (~line 1395)Implementation Plan
Game.executeAndRecalculate()(lines ~1394-1407) — letGame.move()be the single source of truth for undo snapshotsexecuteAndRecalculateonce,undo.frames.length === 1undoLastInActivePlayafter a singleexecuteAndRecalculatemove restores the exact pre-move board stateswitchDicegating still works correctly with single-snapshot undo stackFiles to modify
src/Game/index.ts— remove duplicate snapshot inexecuteAndRecalculatesrc/Game/__tests__/undo-stack.test.ts— new test file