Problem
The undo stack (introduced in PR #80, commit 55b98c4) deep-clones the entire game state before each move:
// src/Game/index.ts, Game.move() ~line 1009
const snapshot = typeof structuredClone === 'function'
? structuredClone(game)
: JSON.parse(JSON.stringify(game))
ap.undo.frames.push(snapshot)
The full BackgammonGame object includes board (24 points × up to 15 checkers each), both players, dice, cube, activePlay with all moves/possibleMoves, statistics, and metadata. For doubles (4 moves per turn), this means 4 full deep-clones per turn.
Impact
- Memory: Each snapshot is a multi-KB object. With 4 moves per doubles turn, this adds significant memory pressure, especially in batch simulations (
simulateBatch.ts)
- CPU:
structuredClone / JSON.parse(JSON.stringify(...)) on every move is expensive in a hot loop
- Simulations: Robot tournament scripts run thousands of games — undo snapshots are never used by robots but are still created
Implementation Plan
Option A: Skip snapshots for robot moves (quick win)
- In
Game.move(), check game.activePlayer.isRobot before pushing snapshot
- Robots never undo, so this eliminates 100% of undo overhead in simulations
- ~3 lines changed, zero risk to human-player undo
Option B: Store deltas instead of full snapshots (medium effort)
- Instead of cloning the entire game, store only what changes per move:
board.points diff (which point lost a checker, which gained one)
activePlay.moves state changes
board.bar / board.off changes
undoLastInActivePlay would apply the delta in reverse
- Much smaller memory footprint (~100 bytes vs ~10KB per snapshot)
Option C: Store only board + activePlay (pragmatic middle ground)
- Clone only
{ board, activePlay, activePlayer.dice } — the minimum needed to restore pre-move state
- On undo, merge the snapshot back into the current game object
- Reduces snapshot size by ~60-70%
Recommendation
Start with Option A (skip for robots) as an immediate win, then evaluate Option C if human-player undo performance becomes a concern.
Files to modify
src/Game/index.ts — Game.move() snapshot logic
src/Game/index.ts — undoLastInActivePlay() restore logic (if Option B/C)
Problem
The undo stack (introduced in PR #80, commit
55b98c4) deep-clones the entire game state before each move:The full
BackgammonGameobject includes board (24 points × up to 15 checkers each), both players, dice, cube, activePlay with all moves/possibleMoves, statistics, and metadata. For doubles (4 moves per turn), this means 4 full deep-clones per turn.Impact
simulateBatch.ts)structuredClone/JSON.parse(JSON.stringify(...))on every move is expensive in a hot loopImplementation Plan
Option A: Skip snapshots for robot moves (quick win)
Game.move(), checkgame.activePlayer.isRobotbefore pushing snapshotOption B: Store deltas instead of full snapshots (medium effort)
board.pointsdiff (which point lost a checker, which gained one)activePlay.movesstate changesboard.bar/board.offchangesundoLastInActivePlaywould apply the delta in reverseOption C: Store only board + activePlay (pragmatic middle ground)
{ board, activePlay, activePlayer.dice }— the minimum needed to restore pre-move stateRecommendation
Start with Option A (skip for robots) as an immediate win, then evaluate Option C if human-player undo performance becomes a concern.
Files to modify
src/Game/index.ts—Game.move()snapshot logicsrc/Game/index.ts—undoLastInActivePlay()restore logic (if Option B/C)