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:
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
// 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
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 saveHow Replay Works
replayLog() performs these steps:
- Clears all existing properties, ID mappings, and pool on the manager
- Replays each operation in order:
acquire: creates a fresh property or pops from the replay poolcycle,hadamard,i_swap, etc.: applies the gate to recreated handlesmeasure: callsforced_measure_propertieswith the recorded outcomerelease: resets to|0⟩and returns to poolassign/unassign: restores ID-to-handle mappings
- 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
| Method | Description |
|---|---|
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)
| Hook | Triggered 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:
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:
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.