Skip to content

perf: Undo stack uses full game deep-clone per move — consider lighter snapshots #99

Description

@nodots

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)

  1. In Game.move(), check game.activePlayer.isRobot before pushing snapshot
  2. Robots never undo, so this eliminates 100% of undo overhead in simulations
  3. ~3 lines changed, zero risk to human-player undo

Option B: Store deltas instead of full snapshots (medium effort)

  1. 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
  2. undoLastInActivePlay would apply the delta in reverse
  3. Much smaller memory footprint (~100 bytes vs ~10KB per snapshot)

Option C: Store only board + activePlay (pragmatic middle ground)

  1. Clone only { board, activePlay, activePlayer.dice } — the minimum needed to restore pre-move state
  2. On undo, merge the snapshot back into the current game object
  3. 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.tsGame.move() snapshot logic
  • src/Game/index.tsundoLastInActivePlay() restore logic (if Option B/C)

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions