Skip to content

Recording & Replay

Quantum state lives in WASM memory and cannot be directly serialized. The QuantumRecorder class solves this by logging every state-mutating quantum operation. On replay, the log recreates identical quantum state. Measurements are forced to their recorded outcomes via forced_measure_properties.

Setup

QuantumRecorder is opt-in. Attach it to a QuantumPropertyManager when you need save/load or replay:

typescript
import { QuantumRecorder } from "quantum-forge/quantum";

const recorder = new QuantumRecorder(registry);
registry.setRecorder(recorder);

Once attached, the recorder automatically logs lifecycle events (acquire, release, setProperty, deleteProperty). For gate operations, record them manually via recorder.recordOp().

Starting a Recording

typescript
// Begin recording. Lifecycle hooks log automatically
recorder.startRecording();

// ... gameplay happens ...
// Gate calls need explicit recording:
const m = registry.getModule();
const index = recorder.getIndex(prop);
m.cycle(prop);
if (index !== undefined) {
  recorder.recordOp({ op: "cycle", index, fraction: 1 });
}

// Stop and get the log
const log: QuantumOperation[] = recorder.stopRecording();

Save / Load

typescript
import type { QuantumOperation } from "quantum-forge/quantum";

// SAVE
const saveData = {
  gameState: engine.getState(),
  quantumLog: recorder.stopRecording(),
};
localStorage.setItem("save", JSON.stringify(saveData));

// LOAD
const loaded = JSON.parse(localStorage.getItem("save")!);
engine.setState(loaded.gameState);
recorder.replayLog(loaded.quantumLog);
// Quantum state is now identical to the moment of save

How Replay Works

replayLog() performs these steps:

  1. Clears all existing properties, ID mappings, and pool on the manager
  2. Replays each operation in order:
    • acquire: creates a fresh property or pops from the replay pool
    • cycle, hadamard, i_swap, etc.: applies the gate to recreated handles
    • measure: calls forced_measure_properties with the recorded outcome
    • release: resets to |0⟩ and returns to pool
    • assign / unassign: restores ID-to-handle mappings
  3. After replay, registry.getProperty("ball-1") returns the correct handle in the correct quantum state

The key insight: because measurements are forced to recorded outcomes, the quantum state evolves identically. The same gates + same measurement results = same final state.

QuantumRecorder API

MethodDescription
startRecording()Begin recording; resets log; assigns indices to existing handles
stopRecording()Stop recording; return QuantumOperation[]
isRecording()Check if recording is active
getOperationLog()Get a copy of the log (works even while recording)
replayLog(ops)Clear manager state and replay from scratch
recordOp(op)Record a gate operation manually
getIndex(prop)Get the recorded index for a property handle
buildWasmPredicates(specs)Build WASM predicate objects from PredicateSpec[]
serializePredicates(specs)Serialize predicates for the operation log

Lifecycle Hooks (automatic when attached)

HookTriggered by
onAcquire(prop)manager.acquireProperty()
onRelease(prop, value)manager.releaseProperty(prop, value)
onSetProperty(id, prop)manager.setProperty(id, prop)
onDeleteProperty(id)manager.deleteProperty(id)

Operation Types

The QuantumOperation union type covers all state mutations:

typescript
type QuantumOperation =
  | { op: "acquire"; index: number }
  | { op: "release"; index: number; value: number }
  | { op: "assign"; index: number; id: string }
  | { op: "unassign"; id: string }
  | { op: "cycle"; index: number; fraction: number }
  | { op: "shift"; index: number; fraction: number }
  | { op: "clock"; index: number; fraction: number }
  | { op: "hadamard"; index: number; fraction: number }
  | { op: "inverse_hadamard"; index: number }
  | { op: "i_swap"; index1: number; index2: number; fraction: number }
  | { op: "swap"; index1: number; index2: number }
  | { op: "phase_rotate"; predicates: SerializedPredicate[]; angle: number }
  | { op: "y"; index: number; fraction: number }
  | { op: "measure"; indices: number[]; outcomes: number[] }
  | { op: "measure_predicate"; predicates: SerializedPredicate[]; outcome: number }
  | { op: "reset"; index: number; value: number };

Each WASM property handle is mapped to a sequential integer index for serialization. On replay, fresh handles are created and mapped back.

Snapshot vs Continuous Recording

You can get a snapshot of the log without stopping recording:

typescript
recorder.startRecording();

// ... gameplay ...

// Periodic auto-save without interrupting recording
const snapshot = recorder.getOperationLog();
autoSave({ gameState: engine.getState(), quantumLog: snapshot });

// ... more gameplay ...

// Final save
const finalLog = recorder.stopRecording();

Tips

When to start recording

Start recording at the beginning of a "saveable" state, typically when the game begins or when entering a new level. Everything before startRecording() is lost.

Recording overhead

Recording adds minimal overhead. It's just appending to an array. Only enable it when you need save/load or replay.

Log size

The log grows linearly with the number of quantum operations. For long game sessions, consider periodic checkpoints: stop recording, save the log, start a new recording.

Powered by Quantum Forge