quantum-forge 2.7.1 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/QUANTUM_FORGE.md CHANGED
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: quantum-forge
3
- description: Quantum Forge framework reference quantum operations, gates, entanglement, rendering, input. Use when writing game code, debugging WASM/imports, or understanding quantum mechanics in games.
3
+ description: Quantum Forge 3.0 reference for game code that imports quantum-forge or quantum-forge-engine. Covers quantum() handles, gates and their aliases, predicates, entanglement through interaction, measurement, dispose, QuantumRecorder, and the engine's rendering and input. Use when writing or debugging quantum game code or WASM loading.
4
4
  allowed-tools: Read Glob Grep
5
5
  user-invocable: true
6
6
  argument-hint: [topic]
@@ -8,24 +8,12 @@ argument-hint: [topic]
8
8
 
9
9
  # Quantum Forge Reference
10
10
 
11
- ## Project Configuration
12
-
13
- ```!
14
- node -e "
15
- const fs = require('fs');
16
- try {
17
- const p = JSON.parse(fs.readFileSync('package.json', 'utf-8'));
18
- const deps = { ...p.dependencies, ...p.devDependencies };
19
- const core = deps['quantum-forge'] || deps['quantum-forge'] || 'not installed';
20
- const engine = deps['quantum-forge-engine'] || deps['quantum-forge-engine'] || 'not installed';
21
- const corePkg = deps['quantum-forge'] ? 'quantum-forge' : 'quantum-forge';
22
- const enginePkg = deps['quantum-forge-engine'] ? 'quantum-forge-engine' : 'quantum-forge-engine';
23
- console.log('Core: ' + corePkg + '@' + core);
24
- if (engine !== 'not installed') console.log('Engine: ' + enginePkg + '@' + engine);
25
- console.log('Type: ' + (p.type || 'commonjs'));
26
- } catch { console.log('No package.json found'); }
27
- " 2>/dev/null
28
- ```
11
+ ## Version
12
+
13
+ This reference covers core 3.0 and newer, where every quantum property is a `quantum()`
14
+ handle. Check the core version in the project's `package.json`. A 2.x install has no handles:
15
+ upgrade it rather than writing `QuantumPropertyManager` code, which is deprecated through 3.x
16
+ and removed in 4.0.
29
17
 
30
18
  ## Initialization
31
19
 
@@ -47,11 +35,11 @@ await ensureLoaded();
47
35
  No Vite plugin is needed in Node (tests, servers, agents). The loader finds the WASM inside the installed package, and `useQuantumForgeBuild("qubit")` selects the variant the same way as in a page. Use `setWasmBasePath(fileUrl)` only for WASM files kept outside the package. Node 22 or newer.
48
36
 
49
37
  ```typescript
50
- import { useQuantumForgeBuild, ensureLoaded, getModule, getVersion } from "quantum-forge/quantum";
38
+ import { useQuantumForgeBuild, ensureLoaded, getVersion, quantum } from "quantum-forge/quantum";
51
39
  useQuantumForgeBuild("qubit");
52
40
  await ensureLoaded();
53
41
  console.log(getVersion()); // the package version
54
- const m = getModule();
42
+ const coin = quantum([false, true]).superpose();
55
43
  ```
56
44
 
57
45
  ### Vite Plugin
@@ -70,10 +58,11 @@ ESM note: if using `vite.config.js` (not `.ts`/`.mjs`), add `"type": "module"` t
70
58
 
71
59
  Quantum Forge gives game objects real quantum state. The usage loop:
72
60
 
73
- 1. **Attach quantum properties** to things each property is a qudit (d-dimensional quantum digit)
61
+ 1. **Declare quantum properties** on things with `quantum([...values])`. Each property is a qudit (d-dimensional quantum digit)
74
62
  2. **Apply gates** to transform state without collapsing it
75
- 3. **Use predicated gates** to create entanglement correlations between properties
63
+ 3. **Let properties interact** (a two-property gate, or a gate predicated on another property). Entanglement is what an interaction leaves behind
76
64
  4. **Measure** to collapse quantum state to a classical value (the game moment)
65
+ 5. **Dispose** the handle when the thing it describes is gone
77
66
 
78
67
  ### Programmer's Mental Model
79
68
 
@@ -81,301 +70,307 @@ Quantum Forge gives game objects real quantum state. The usage loop:
81
70
 
82
71
  **Gates** are collection algorithms that transform every entry at once. `cycle` increments each value. `hadamard` creates equal superposition.
83
72
 
84
- **Predicated gates** apply a filter: "if property A is |1⟩, flip property B." This creates entanglement correlated entries in the collection.
73
+ **Predicated gates** apply a filter: "if property A is true, flip property B." This creates entanglement, correlated entries in the collection.
85
74
 
86
75
  The weights are **complex amplitudes**, not probabilities. When two entries with the same value meet, their amplitudes add. In phase = constructive interference (probability increases). Out of phase = destructive interference (probability cancels). Probability of a value = squared magnitude of its amplitude.
87
76
 
88
- ## QuantumPropertyManager
77
+ ## Quantum properties
89
78
 
90
- Extend this class to build your game's quantum system. It manages property lifecycle, pooling, and WASM access.
79
+ A property is declared by the values it can take. The dimension is the number of values, and
80
+ the property starts at the first one.
91
81
 
92
82
  ```typescript
93
- import { QuantumPropertyManager, ensureLoaded } from "quantum-forge/quantum";
83
+ import { ensureLoaded, quantum } from "quantum-forge/quantum";
94
84
 
95
85
  await ensureLoaded();
96
86
 
97
- class QuantumRegistry extends QuantumPropertyManager {
98
- constructor(logger?: any) { super({ dimension: 2, logger }); }
87
+ const color = quantum(["red", "green", "blue"]); // qutrit, starts "red"
88
+ const alive = quantum([false, true]); // qubit, starts false
89
+ const die = quantum(3); // numeric form: values 0, 1, 2
99
90
 
100
- makeQuantum(id: string): void {
101
- const prop = this.acquireProperty();
102
- const m = this.getModule();
103
- m.cycle(prop); // |0⟩ |1⟩
104
- m.hadamard(prop); // → 50/50 superposition
105
- this.setProperty(id, prop);
106
- }
91
+ color.superpose(); // alias for hadamard()
92
+ color.probability("green"); // 1/3, no collapse
93
+ const seen = color.measure(); // "red" | "green" | "blue"
94
+ color.dispose(); // end of life; the qudit is cached for reuse
95
+ ```
107
96
 
108
- entangle(id1: string, id2: string): void {
109
- const p1 = this.getProperty(id1);
110
- const p2 = this.getProperty(id2);
111
- if (!p1 || !p2) return;
112
- this.getModule().i_swap(p1, p2, 0.5);
113
- }
97
+ Keep each handle on the entity it describes (`ball.exists = quantum([false, true])`). A ball
98
+ without a handle is classical. There is no id map and no manager.
114
99
 
115
- getProbability(id: string): number {
116
- const prop = this.getProperty(id);
117
- if (!prop) return 1.0;
118
- const results = this.getModule().probabilities([prop]);
119
- for (const r of results) {
120
- if (r.qudit_values[0] === 1) return r.probability;
121
- }
122
- return 0;
123
- }
100
+ ### Dimension
124
101
 
125
- measure(id: string): number {
126
- const prop = this.getProperty(id);
127
- if (!prop) return 1;
128
- const [value] = this.getModule().measure_properties([prop]);
129
- this.deleteProperty(id);
130
- this.releaseProperty(prop, value); // reset to |0⟩, return to pool
131
- return value;
132
- }
133
- }
134
- ```
102
+ | Dimension | Declaration | Use Case |
103
+ |-----------|-------------|----------|
104
+ | 2 (qubit) | `quantum([false, true])` | Binary: exists/doesn't, alive/dead |
105
+ | 3 (qutrit) | `quantum(["rock", "paper", "scissors"])` | Three-way choices |
135
106
 
136
- ### Dimension
107
+ Start with two values unless your design needs more. The shipped Qutrit Edition allows up to
108
+ three; `quantum()` throws past the build's maximum.
137
109
 
138
- | Dimension | States | Use Case |
139
- |-----------|--------|----------|
140
- | 2 (qubit) | `\|0⟩`, `\|1⟩` | Binary: exists/doesn't, alive/dead |
141
- | 3 (qutrit) | `\|0⟩` – `\|2⟩` | Three-way: rock/paper/scissors |
110
+ ### Values and indices
142
111
 
143
- Start with dimension 2 unless your design needs more. Shipped package supports up to 3.
112
+ Every method that takes a value (`is`, `isNot`, `probability`, `forcedMeasure`) accepts the
113
+ declared value or its index. `alive.is(true)` and `alive.is(1)` are the same predicate. A value
114
+ that is not declared throws at the call. `measure()` returns the declared value.
144
115
 
145
116
  ## Gates
146
117
 
147
- All gates are called via `getModule()`. Every gate accepts optional `predicates` for conditional execution (creates entanglement). Omit `fraction` for the discrete gate. Passing a number, `1` included, selects the continuous rotation instead: at `1` it lands on the same state as the discrete gate, but by the slower rotation path. Write `undefined` when you mean the discrete gate.
118
+ Gates are methods on the handle and return it, so they chain. Physics names are primary; each
119
+ alias runs exactly the same gate.
148
120
 
149
- | Gate | Method | Dim | Description |
150
- |------|--------|-----|-------------|
151
- | Cycle | `m.cycle(prop, fraction?, preds?)` | Any | Cyclic permutation. `\|0⟩→\|1⟩→\|0⟩` for dim=2 (NOT gate) |
152
- | Shift / X | `m.shift(prop, fraction?, preds?)` | Any | Inverse of cycle. Same as cycle for dim=2 |
153
- | Hadamard | `m.hadamard(prop, fraction?, preds?)` | Any | Equal superposition. The main "make it quantum" gate |
154
- | Inv. Hadamard | `m.inverse_hadamard(prop, preds?)` | Any | Reverse of hadamard |
155
- | Clock / Z | `m.clock(prop, fraction?, preds?)` | Any | Phase rotation. Invisible until interaction |
156
- | Y | `m.y(prop, fraction?, preds?)` | **2 only** | Pauli Y. Throws if dimension != 2 |
157
- | iSwap | `m.i_swap(p1, p2, fraction, preds?)` | Any | Anti-correlated entanglement |
158
- | Swap | `m.swap(p1, p2, preds?)` | Any | Direct state exchange, no entanglement |
159
- | Phase Rotate | `m.phase_rotate(preds, angle)` | Any | Phase rotation conditioned on predicates (required) |
121
+ | Physics call | Alias | Dim | Description |
122
+ |--------------|-------|-----|-------------|
123
+ | `hadamard(f?, opts?)` | `superpose` | Any | Equal superposition. The main "make it quantum" gate |
124
+ | `inverseHadamard(opts?)` | | Any | Reverse of hadamard |
125
+ | `cycle(f?, opts?)` | `next`; `flip` on qubits | Any | Next value, wrapping. NOT at dimension 2 |
126
+ | `shift(f?, opts?)` | `previous` | Any | Previous value, wrapping. Same as cycle at dimension 2 |
127
+ | `clock(f?, opts?)` | `phase` | Any | Phase rotation. Shows on a superposition at the next gate that mixes values |
128
+ | `x(f?, opts?)` / `z(f?, opts?)` | | Any | Pauli X (= shift) and Pauli Z (= clock) |
129
+ | `y(f?, opts?)` | | **2 only** | Pauli Y. Throws if dimension != 2 |
130
+ | `a.iSwap(b, f, opts?)` | | Any | Anti-correlated entanglement. Fraction required |
131
+ | `a.swap(b, opts?)` | | Any | Direct state exchange |
132
+ | `phaseRotate(angle, { when })` | | Any | Free function. Phase on the states where the predicates hold |
160
133
 
161
- **Fractional gates**: `0.5` is the square root of the gate, `0.1` barely moves the state, `1` completes the rotation. Apply a small fraction every frame for a gradual effect.
134
+ `flip` throws on anything but two values; use `next` on a qutrit. `swap` and `iSwap` throw
135
+ when passed the handle they are called on. There is no `entangle()`. A property needs at least
136
+ two values, and `quantum()` is the only way to make one; the `Quantum` constructor is private.
162
137
 
163
- ### Predicates (Conditional Gates)
138
+ Every gate except `inverseHadamard`, `swap` and `iSwap` takes an optional fraction first. An
139
+ omitted fraction, or exactly 1, is the discrete gate; any other number is the continuous version, and 0.5 is the square root of the gate. `0.1` barely moves the state.
140
+ Apply a small fraction every frame for a gradual effect.
164
141
 
165
- Every gate accepts predicates that condition it on other properties' states. **This is how entanglement works** — a gate that depends on another property's state correlates them.
142
+ ### Predicates (conditional gates)
166
143
 
167
- **Gotcha:** predicates are the third argument, so the discrete gate with predicates is `m.shift(target, undefined, [control.is(1)])`. Passing `1` runs the fractional path instead (same state, slower).
144
+ Pass predicates in `{ when: [...] }`. The gate acts only where every predicate holds. A
145
+ predicate on another property makes the gate an interaction, which entangles the two.
168
146
 
169
147
  ```typescript
170
- // CNOT: flip target only when control is |1⟩
171
- m.shift(target, undefined, [control.is(1)]);
172
- // Result: (|00⟩ + |11⟩)/√2 positively correlated
148
+ // CNOT: flip target only when control is true
149
+ target.flip({ when: [control.is(true)] });
150
+ // Result: (|00⟩ + |11⟩)/√2, positively correlated
151
+
152
+ // CZ
153
+ target.phase({ when: [control.is(true)] });
173
154
 
174
- // Controlled Hadamard: target enters superposition conditionally
175
- m.hadamard(target, undefined, [control.is(1)]);
155
+ // Controlled Hadamard, and a fractional controlled gate
156
+ target.superpose({ when: [control.is(true)] });
157
+ target.flip(0.5, { when: [control.is(true)] });
176
158
 
177
- // Predicate types:
178
- prop.is(value) // true when property is |value
179
- prop.is_not(value) // true when property is NOT |value⟩
159
+ // Predicate types
160
+ prop.is(value) // holds when the property equals value (declared value or index)
161
+ prop.isNot(value) // holds when it does not
180
162
 
181
163
  // Multiple predicates are AND'd
182
- m.shift(target, undefined, [controlA.is(1), controlB.is(1)]);
164
+ target.flip({ when: [controlA.is(true), controlB.is(true)] });
183
165
  ```
184
166
 
185
- For `PredicateSpec` objects (used in some APIs):
186
-
187
- ```typescript
188
- import type { PredicateSpec } from "quantum-forge/quantum";
189
- const specs: PredicateSpec[] = [
190
- { property: controlProp, value: 1, isEqual: true },
191
- ];
192
- const wasmPreds = specs.map(s =>
193
- s.isEqual ? s.property.is(s.value) : s.property.is_not(s.value)
194
- );
195
- ```
167
+ ## Entanglement patterns
196
168
 
197
- ## Entanglement Patterns
169
+ Entanglement is what an interaction leaves behind. A gate that touches two properties, or one
170
+ whose predicate reads another property, is an interaction. Measuring one entangled property
171
+ settles the other.
198
172
 
199
- ### Entangle-Split (iSwap)
173
+ ### Entangle-split (iSwap)
200
174
 
201
- Object splits into anti-correlated pair. Exactly one is real.
175
+ Object splits into an anti-correlated pair. Exactly one is real.
202
176
 
203
177
  ```typescript
204
- entangleSplit(originalId: string, newId: string): void {
205
- const prop1 = this.acquireProperty();
206
- const prop2 = this.acquireProperty();
207
- const m = this.getModule();
208
- m.cycle(prop1); // |1⟩ (exists)
209
- m.i_swap(prop1, prop2, 0.5); // entangle
210
- this.setProperty(originalId, prop1);
211
- this.setProperty(newId, prop2);
178
+ function entangleSplit(original: Ball, clone: Ball): void {
179
+ original.exists = quantum([false, true]).flip(); // exists
180
+ clone.exists = quantum([false, true]); // does not
181
+ original.exists.iSwap(clone.exists, 0.5);
212
182
  }
213
183
  ```
214
184
 
215
- ### Correlated Pair (CNOT)
185
+ ### Correlated pair (CNOT)
216
186
 
217
- Both match both alive or both dead.
187
+ Both match: both alive or both dead.
218
188
 
219
189
  ```typescript
220
- createLinkedPair(id1: string, id2: string): void {
221
- const prop1 = this.acquireProperty();
222
- const prop2 = this.acquireProperty();
223
- const m = this.getModule();
224
- m.cycle(prop1);
225
- m.hadamard(prop1);
226
- m.shift(prop2, undefined, [prop1.is(1)]); // CNOT
227
- this.setProperty(id1, prop1);
228
- this.setProperty(id2, prop2);
190
+ function linkPair(a: Enemy, b: Enemy): void {
191
+ a.alive = quantum([false, true]).superpose();
192
+ b.alive = quantum([false, true]);
193
+ b.alive.flip({ when: [a.alive.is(true)] });
229
194
  }
230
195
  ```
231
196
 
232
- ### Quantum-Split
197
+ ### Quantum-split
233
198
 
234
- Split an already-quantum object. Pooled properties preferred (no tensor product growth).
199
+ Split an already-quantum object. The split adds one qudit to the original's shared state, so
200
+ check that there is room first. Catching the over-limit throw instead leaks WASM memory on
201
+ every catch.
235
202
 
236
203
  ```typescript
237
- quantumSplit(originalId: string, newId: string): boolean {
238
- const prop1 = this.getProperty(originalId);
239
- if (!prop1) return false;
240
- const prop2 = this.acquireProperty();
241
- try {
242
- this.getModule().i_swap(prop1, prop2, 0.5);
243
- } catch {
244
- this.releaseProperty(prop2, 0); // qudit limit
245
- return false;
246
- }
247
- this.setProperty(newId, prop2);
204
+ import { getMaxQudits, quantum } from "quantum-forge/quantum";
205
+
206
+ function quantumSplit(original: Ball, clone: Ball): boolean {
207
+ if (!original.exists) return false;
208
+ if (original.exists.numActiveQudits() >= getMaxQudits()) return false; // state is full
209
+ const split = quantum([false, true]);
210
+ original.exists.iSwap(split, 0.5);
211
+ clone.exists = split;
248
212
  return true;
249
213
  }
250
214
  ```
251
215
 
252
- ### Overlap Entanglement
253
-
254
- Objects that spatially overlap become entangled:
255
-
256
- ```typescript
257
- this.getModule().i_swap(propA, propB, 0.5);
258
- ```
259
-
260
- ### Conditional Superposition
216
+ ### Overlap entanglement
261
217
 
262
- A control property determines whether a target enters superposition:
218
+ Quantum objects that spatially overlap become entangled. Both need a handle, and the pair
219
+ must differ somewhere: iSwap on two equal values does nothing.
263
220
 
264
221
  ```typescript
265
- m.hadamard(target, undefined, [control.is(1)]);
222
+ function overlap(a: Ball, b: Ball): void {
223
+ if (a.exists && b.exists) a.exists.iSwap(b.exists, 0.5);
224
+ }
266
225
  ```
267
226
 
268
- ### Choosing Between Mechanisms
227
+ ### Choosing between mechanisms
269
228
 
270
229
  | Mechanism | Correlation | Use |
271
230
  |-----------|-------------|-----|
272
- | Predicated shift (CNOT) | Positive both match | Linked states: both alive or both dead |
273
- | Predicated hadamard | Conditional superposition | One object's quantumness depends on another |
274
- | `i_swap(0.5)` | Anti-correlated exactly one | Object splits into two ghosts |
275
- | `i_swap` + phase | Tunable bias | Player-influenced split probability |
231
+ | `b.flip({ when: [a.is(true)] })` (CNOT) | Positive, both match | Linked states: both alive or both dead |
232
+ | `b.superpose({ when: [a.is(true)] })` | Conditional superposition | One object's quantumness depends on another |
233
+ | `a.iSwap(b, 0.5)` | Anti-correlated, exactly one | Object splits into two ghosts |
234
+ | `iSwap(0.5)`, `phase(f)`, `iSwap(0.5)` | Tunable bias | Player-influenced split probability |
276
235
 
277
236
  ## Measurement
278
237
 
279
- Measurement collapses superposition to a definite value. Entangled partners collapse instantly too.
238
+ Measurement collapses superposition to a definite value. Entangled partners collapse too.
280
239
 
281
240
  ```typescript
282
- const [value] = m.measure_properties([prop]); // probabilistic collapse
283
- // Always pool after:
284
- this.deleteProperty(id);
285
- this.releaseProperty(prop, value);
241
+ const value = prop.measure(); // declared value, e.g. true or false
286
242
  ```
287
243
 
288
- ### Batch Measurement
244
+ A measured handle is still usable. It holds a definite value and can go back into
245
+ superposition. When the object is gone, `dispose()` it.
246
+
247
+ ### Joint measurement
289
248
 
290
249
  ```typescript
291
- const [va, vb, vc] = m.measure_properties([propA, propB, propC]);
250
+ import { measure } from "quantum-forge/quantum";
251
+ const [va, vb, vc] = measure(propA, propB, propC);
292
252
  ```
293
253
 
294
- ### Reading State (No Collapse)
254
+ ### Reading state (no collapse)
295
255
 
296
256
  ```typescript
297
- // Probabilities read-only, no state change
298
- const results = m.probabilities([prop]);
299
- // [{ probability: 0.5, qudit_values: [0] }, { probability: 0.5, qudit_values: [1] }]
257
+ import { probabilities, densityMatrix, probabilityWhen, measureWhen, forcedMeasure } from "quantum-forge/quantum";
258
+
259
+ prop.probability(true); // one value
260
+ prop.probabilities(); // [{ value: false, probability: 0.5 }, { value: true, probability: 0.5 }]
261
+ probabilities(a, b); // joint: [{ values: [false, true], probability }, ...]
300
262
 
301
- // Reduced density matrix phase and correlation info
302
- const rdm = m.reduced_density_matrix([prop1, prop2]);
303
- // Off-diagonal entries carry relative phase
263
+ // Reduced density matrix: phase and correlation info, labelled with declared values
264
+ densityMatrix(a, b); // [{ row: [...], col: [...], real, imag }, ...]
304
265
 
305
- // Measure predicate projective measurement
306
- const outcome = m.measure_predicate([prop1.is(1), prop2.is(0)]);
307
- // 1 = satisfied, 0 = not
266
+ probabilityWhen([a.is(true), b.is(false)]); // probability all predicates hold
308
267
 
309
- // Forced measurement replay/save-load only
310
- const [value] = m.forced_measure_properties([prop], [1]);
268
+ // Predicate measurement: collapses to agree, returns a boolean
269
+ measureWhen([a.is(true), b.is(false)]);
270
+
271
+ // Forced measurement: replay and tests only. Throws if the value has zero probability.
272
+ prop.forcedMeasure(true);
273
+ forcedMeasure([a, b], [true, false]);
311
274
  ```
312
275
 
313
276
  ## Phase & Interference
314
277
 
315
- Phase is invisible to measurement but changes how properties interact. Phase + iSwap = probability redistribution through interference.
278
+ Phase is invisible to measurement on its own. A definite value ignores it. On a superposition it shows up at the next gate that mixes values, such as superpose or a partial iSwap, and steers that gate through interference. A phase between two half-splits decides where the object ends up:
316
279
 
317
280
  ```typescript
318
- // Apply phase bias
319
- m.clock(prop, 0.5); // π/2 phase on |1⟩
320
-
321
- // Later, entangle phase redistributes probability
322
- m.i_swap(propA, propB, 0.5);
323
- // Same phase = constructive interference (probability concentrates)
324
- // Opposite phase = destructive interference (probability cancels)
281
+ const a = quantum([false, true]).flip(); // exists
282
+ const b = quantum([false, true]); // does not
283
+ a.iSwap(b, 0.5); // split
284
+ a.phase(bias); // bias from 0 to 1; 0.5 is a π/2 phase on true
285
+ a.iSwap(b, 0.5); // split again: the phase now moves probability
286
+ // a.probability(true) is 0 at bias 0, 0.5 at bias 0.5, 1 at bias 1
325
287
  ```
326
288
 
289
+ A phase before a single split changes nothing. It needs a later superpose or interaction to turn into probability.
290
+
327
291
  **Grover oracle** (walls/barriers): mark states with π phase, then diffuse with fractional Hadamard. Probability "bounces off" marked states.
328
292
 
329
293
  ```typescript
330
- m.phase_rotate([hexProp.is(wallState)], Math.PI); // mark
331
- m.hadamard(hexProp, 0.1); // diffuse
294
+ phaseRotate(Math.PI, { when: [hex.is(wallState)] }); // mark
295
+ hex.superpose(0.1); // diffuse
332
296
  ```
333
297
 
334
- **Key insight:** Phase is the control knob that lets players influence quantum outcomes without directly choosing them. The player can't pick which ball exists, but they can bias the odds by applying phase before the next entanglement interaction.
298
+ **Key insight:** Phase is the control knob that lets players influence quantum outcomes without directly choosing them. The player can't pick which ball exists, but they can bias the odds by applying phase between two interactions.
335
299
 
336
- ### Visualizing Phase
300
+ ### Visualizing phase
337
301
 
338
302
  Extract from the reduced density matrix:
339
303
 
340
304
  ```typescript
341
- const rdm = m.reduced_density_matrix([propRef, propTarget]);
342
- for (const entry of rdm) {
343
- if (entry.row_values[0] === 1 && entry.row_values[1] === 0 &&
344
- entry.col_values[0] === 0 && entry.col_values[1] === 1) {
345
- const phase = Math.atan2(entry.value.imag, entry.value.real);
305
+ for (const entry of densityMatrix(ref, target)) {
306
+ if (entry.row[0] === true && entry.row[1] === false &&
307
+ entry.col[0] === false && entry.col[1] === true) {
308
+ const phase = Math.atan2(entry.imag, entry.real);
346
309
  // Map to dial rotation, color hue, force magnitude, compass direction
347
310
  }
348
311
  }
349
312
  ```
350
313
 
351
- ## Recording & Replay
314
+ ## Lifecycle
315
+
316
+ `dispose()` is the only lifecycle call. It measures the property, which collapses anything
317
+ entangled with it. A qudit that is then alone is reset and cached for the next `quantum()` of
318
+ the same dimension; one that still shares a state is destroyed, which splits it out of the
319
+ group. A reused handle never carries an old group.
320
+
321
+ A `using` declaration disposes at scope exit. It needs TypeScript 5.2+ with `"ESNext.Disposable"`
322
+ in the tsconfig `lib`; scaffolded projects have both.
323
+
324
+ The engine's `EntityManager.remove()` and `clear()` dispose handles stored directly on an entity
325
+ (string or symbol keys) or in an array field on it, and `add()` disposes the handles of an
326
+ entity it replaces that the new entity no longer carries. Removing an entity therefore
327
+ collapses whatever its handles were entangled with. A handle belongs to one entity. Pass
328
+ `{ disposeQuantum: false }` to keep the handles.
329
+
330
+ ## Recording and replay
331
+
332
+ `QuantumRecorder` logs every operation on `quantum()` handles so you can save a session and rebuild its quantum state later, for replays, bug reports or deterministic tests. It listens through `observeQuantum()`, so the game makes no extra calls once recording has started.
352
333
 
353
334
  ```typescript
354
- import { QuantumRecorder } from "quantum-forge/quantum";
335
+ import { QuantumRecorder, quantum, measure } from "quantum-forge/quantum";
355
336
 
356
- const recorder = new QuantumRecorder(qpm);
337
+ const recorder = new QuantumRecorder();
357
338
  recorder.startRecording();
358
- // ... operations ...
359
- const log = recorder.getOperationLog(); // serializable JSON (also returned by stopRecording())
360
- // Save: localStorage.setItem("quantum-save", JSON.stringify(log));
361
- // Load: recorder.replayLog(JSON.parse(saved)); // forced measurements
339
+ const a = quantum([false, true]);
340
+ const b = quantum([false, true]);
341
+ a.superpose();
342
+ b.flip({ when: [a.is(true)] });
343
+ measure(a, b);
344
+ const saved = QuantumRecorder.serialize(recorder.stopRecording());
345
+
346
+ const handles = QuantumRecorder.replay(QuantumRecorder.deserialize(saved));
347
+ const replayedA = handles.get(a.id);
362
348
  ```
363
349
 
350
+ The log is plain JSON: `{ version: 1, entries: [...] }`. Entries name handles by their `id`, values by basis index, and operations by their physics name, so `superpose()` is logged as `hadamard` and `flip()` as `cycle`. `deserialize()` and `replay()` reject a log without that envelope or with any other version, and `deserialize()` checks every entry and throws on anything malformed.
351
+
352
+ Replay creates a fresh handle for every `create` entry and returns a map from the logged id to the new handle. Handles the log disposes are not in the map. Every measurement is replayed as a forced measurement with the recorded outcome, so the rebuilt state matches the original, including entangled pairs. A recorder that is running during `replay()` records what the replay did, so a game can load a save and keep recording into one log.
353
+
354
+ Start recording before creating the handles you want to replay. A handle created earlier has no `create` entry. The recorder warns once for each such handle and lists its id in the log's `untrackedIds`, and `replay()` refuses that log with an error naming the ids.
355
+
356
+ `new QuantumRecorder()` takes no arguments. Passing one, as 2.x code did with a manager, throws and points to `LegacyQuantumRecorder`.
357
+
358
+ Observers, the recorder included, cannot break the game. An error thrown inside an observer is caught and reported through `reportError()`, or `console.error` where the runtime lacks it, and the game call that triggered it goes on as normal. Observers receive operations in the order they ran.
359
+
364
360
  ## Performance
365
361
 
366
- **The one rule: pool your properties.** Measure delete release. Every time.
362
+ Dispose handles when their object is gone, or let `EntityManager` do it. A disposed property
363
+ stops counting against the qudit limit, so a game that keeps spawning and removing objects
364
+ stays within it.
367
365
 
368
366
  Shipped limits: Qutrit Edition has dimension 3, 12 max qudits. Qubit Edition has dimension 2, 20 max qudits.
369
367
 
370
- | Active Properties | Performance |
371
- |-------------------|-------------|
372
- | 1–4 | No issues |
373
- | 4–8 | Good with pooling |
374
- | 8–12 | At the limit, aggressive pooling required |
375
-
376
- **Most expensive operation**: tensor product (triggered by iSwap between properties in different shared states). Pooled properties avoid this — they're already in the shared state.
368
+ The most expensive operation is the tensor product, triggered the first time two properties in
369
+ different shared states interact.
377
370
 
378
- **Strategies**: aggressive pooling, limit concurrent quantum objects, use dimension 2 unless needed, separate registries for independent systems, batch measurements, throttle probability queries.
371
+ To stay fast: dispose promptly, limit concurrent quantum objects, use two values unless you
372
+ need more, measure several properties in one `measure(a, b, c)` call, throttle probability
373
+ queries. `prop.numActiveQudits()` and `prop.stateVectorSize()` report how big the shared state is.
379
374
 
380
375
  ```typescript
381
376
  // Monitor WASM memory
@@ -465,6 +460,7 @@ loop.start();
465
460
 
466
461
  Map probability to visual properties (opacity, size, color):
467
462
 
463
+ <!-- sample:skip: method shown outside its class -->
468
464
  ```typescript
469
465
  protected draw(state: GameState) {
470
466
  for (const ball of state.balls) {
@@ -511,7 +507,7 @@ input.destroy();
511
507
 
512
508
  | Export | Key APIs |
513
509
  |--------|----------|
514
- | `./quantum` | `ensureLoaded`, `getModule`, `QuantumPropertyManager`, `QuantumRecorder`, `useQuantumForgeBuild`, `setWasmBasePath` |
510
+ | `./quantum` | `quantum`, `Quantum`, `measure`, `probabilities`, `densityMatrix`, `measureWhen`, `phaseRotate`, `ensureLoaded`, `QuantumRecorder`, `useQuantumForgeBuild`, `setWasmBasePath` |
515
511
  | `./logging` | `Logger` |
516
512
  | `./vite-plugin` | `quantumForgeVitePlugin` |
517
513