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)

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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