quantum-forge 2.7.0 → 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-native/quantum-forge'] || 'not installed';
20
- const engine = deps['quantum-forge-engine'] || deps['@quantum-native/quantum-forge-engine'] || 'not installed';
21
- const corePkg = deps['quantum-forge'] ? 'quantum-forge' : '@quantum-native/quantum-forge';
22
- const enginePkg = deps['quantum-forge-engine'] ? 'quantum-forge-engine' : '@quantum-native/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
 
@@ -42,6 +30,18 @@ useQuantumForgeBuild("qubit"); // before ensureLoaded()
42
30
  await ensureLoaded();
43
31
  ```
44
32
 
33
+ ### Node and headless
34
+
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.
36
+
37
+ ```typescript
38
+ import { useQuantumForgeBuild, ensureLoaded, getVersion, quantum } from "quantum-forge/quantum";
39
+ useQuantumForgeBuild("qubit");
40
+ await ensureLoaded();
41
+ console.log(getVersion()); // the package version
42
+ const coin = quantum([false, true]).superpose();
43
+ ```
44
+
45
45
  ### Vite Plugin
46
46
 
47
47
  ```typescript
@@ -58,10 +58,11 @@ ESM note: if using `vite.config.js` (not `.ts`/`.mjs`), add `"type": "module"` t
58
58
 
59
59
  Quantum Forge gives game objects real quantum state. The usage loop:
60
60
 
61
- 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)
62
62
  2. **Apply gates** to transform state without collapsing it
63
- 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
64
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
65
66
 
66
67
  ### Programmer's Mental Model
67
68
 
@@ -69,301 +70,307 @@ Quantum Forge gives game objects real quantum state. The usage loop:
69
70
 
70
71
  **Gates** are collection algorithms that transform every entry at once. `cycle` increments each value. `hadamard` creates equal superposition.
71
72
 
72
- **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.
73
74
 
74
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.
75
76
 
76
- ## QuantumPropertyManager
77
+ ## Quantum properties
77
78
 
78
- 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.
79
81
 
80
82
  ```typescript
81
- import { QuantumPropertyManager, ensureLoaded } from "quantum-forge/quantum";
83
+ import { ensureLoaded, quantum } from "quantum-forge/quantum";
82
84
 
83
85
  await ensureLoaded();
84
86
 
85
- class QuantumRegistry extends QuantumPropertyManager {
86
- 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
87
90
 
88
- makeQuantum(id: string): void {
89
- const prop = this.acquireProperty();
90
- const m = this.getModule();
91
- m.cycle(prop); // |0⟩ |1⟩
92
- m.hadamard(prop); // → 50/50 superposition
93
- this.setProperty(id, prop);
94
- }
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
+ ```
95
96
 
96
- entangle(id1: string, id2: string): void {
97
- const p1 = this.getProperty(id1);
98
- const p2 = this.getProperty(id2);
99
- if (!p1 || !p2) return;
100
- this.getModule().i_swap(p1, p2, 0.5);
101
- }
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.
102
99
 
103
- getProbability(id: string): number {
104
- const prop = this.getProperty(id);
105
- if (!prop) return 1.0;
106
- const results = this.getModule().probabilities([prop]);
107
- for (const r of results) {
108
- if (r.qudit_values[0] === 1) return r.probability;
109
- }
110
- return 0;
111
- }
100
+ ### Dimension
112
101
 
113
- measure(id: string): number {
114
- const prop = this.getProperty(id);
115
- if (!prop) return 1;
116
- const [value] = this.getModule().measure_properties([prop]);
117
- this.deleteProperty(id);
118
- this.releaseProperty(prop, value); // reset to |0⟩, return to pool
119
- return value;
120
- }
121
- }
122
- ```
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 |
123
106
 
124
- ### 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.
125
109
 
126
- | Dimension | States | Use Case |
127
- |-----------|--------|----------|
128
- | 2 (qubit) | `\|0⟩`, `\|1⟩` | Binary: exists/doesn't, alive/dead |
129
- | 3 (qutrit) | `\|0⟩` – `\|2⟩` | Three-way: rock/paper/scissors |
110
+ ### Values and indices
130
111
 
131
- 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.
132
115
 
133
116
  ## Gates
134
117
 
135
- All gates are called via `getModule()`. Every gate accepts optional `predicates` for conditional execution (creates entanglement). Omitting `fraction` applies the discrete gate; passing a fraction (even `1.0`) applies the continuous fractional variant, which is a different operation.
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.
136
120
 
137
- | Gate | Method | Dim | Description |
138
- |------|--------|-----|-------------|
139
- | Cycle | `m.cycle(prop, fraction?, preds?)` | Any | Cyclic permutation. `\|0⟩→\|1⟩→\|0⟩` for dim=2 (NOT gate) |
140
- | Shift / X | `m.shift(prop, fraction?, preds?)` | Any | Inverse of cycle. Same as cycle for dim=2 |
141
- | Hadamard | `m.hadamard(prop, fraction?, preds?)` | Any | Equal superposition. The main "make it quantum" gate |
142
- | Inv. Hadamard | `m.inverse_hadamard(prop, preds?)` | Any | Reverse of hadamard |
143
- | Clock / Z | `m.clock(prop, fraction, preds?)` | Any | Phase rotation. Invisible until interaction |
144
- | Y | `m.y(prop, fraction?, preds?)` | **2 only** | Pauli Y. Throws if dimension != 2 |
145
- | iSwap | `m.i_swap(p1, p2, fraction, preds?)` | Any | Anti-correlated entanglement |
146
- | Swap | `m.swap(p1, p2, preds?)` | Any | Direct state exchange, no entanglement |
147
- | 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 |
148
133
 
149
- **Fractional gates**: `fraction=1` is the full gate, `0.5` is the square root, `0.1` barely moves state. Key for gradual quantum effects.
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.
150
137
 
151
- ### 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.
152
141
 
153
- 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)
154
143
 
155
- **Gotcha:** predicates are the third argument. To get the discrete (non-fractional) gate, pass `undefined` as the fraction passing `1` selects the *fractional* gate, which is a different operation.
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.
156
146
 
157
147
  ```typescript
158
- // CNOT: flip target only when control is |1⟩
159
- m.shift(target, undefined, [control.is(1)]);
160
- // 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
161
151
 
162
- // Controlled Hadamard: target enters superposition conditionally
163
- m.hadamard(target, undefined, [control.is(1)]);
152
+ // CZ
153
+ target.phase({ when: [control.is(true)] });
164
154
 
165
- // Predicate types:
166
- prop.is(value) // true when property is |value⟩
167
- prop.is_not(value) // true when property is NOT |value⟩
155
+ // Controlled Hadamard, and a fractional controlled gate
156
+ target.superpose({ when: [control.is(true)] });
157
+ target.flip(0.5, { when: [control.is(true)] });
158
+
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
168
162
 
169
163
  // Multiple predicates are AND'd
170
- m.shift(target, undefined, [controlA.is(1), controlB.is(1)]);
164
+ target.flip({ when: [controlA.is(true), controlB.is(true)] });
171
165
  ```
172
166
 
173
- For `PredicateSpec` objects (used in some APIs):
167
+ ## Entanglement patterns
174
168
 
175
- ```typescript
176
- import type { PredicateSpec } from "quantum-forge/quantum";
177
- const specs: PredicateSpec[] = [
178
- { property: controlProp, value: 1, isEqual: true },
179
- ];
180
- const wasmPreds = specs.map(s =>
181
- s.isEqual ? s.property.is(s.value) : s.property.is_not(s.value)
182
- );
183
- ```
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.
184
172
 
185
- ## Entanglement Patterns
173
+ ### Entangle-split (iSwap)
186
174
 
187
- ### Entangle-Split (iSwap)
188
-
189
- Object splits into anti-correlated pair. Exactly one is real.
175
+ Object splits into an anti-correlated pair. Exactly one is real.
190
176
 
191
177
  ```typescript
192
- entangleSplit(originalId: string, newId: string): void {
193
- const prop1 = this.acquireProperty();
194
- const prop2 = this.acquireProperty();
195
- const m = this.getModule();
196
- m.cycle(prop1); // |1⟩ (exists)
197
- m.i_swap(prop1, prop2, 0.5); // entangle
198
- this.setProperty(originalId, prop1);
199
- 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);
200
182
  }
201
183
  ```
202
184
 
203
- ### Correlated Pair (CNOT)
185
+ ### Correlated pair (CNOT)
204
186
 
205
- Both match both alive or both dead.
187
+ Both match: both alive or both dead.
206
188
 
207
189
  ```typescript
208
- createLinkedPair(id1: string, id2: string): void {
209
- const prop1 = this.acquireProperty();
210
- const prop2 = this.acquireProperty();
211
- const m = this.getModule();
212
- m.cycle(prop1);
213
- m.hadamard(prop1);
214
- m.shift(prop2, 1, [prop1.is(1)]); // CNOT
215
- this.setProperty(id1, prop1);
216
- 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)] });
217
194
  }
218
195
  ```
219
196
 
220
- ### Quantum-Split
197
+ ### Quantum-split
221
198
 
222
- 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.
223
202
 
224
203
  ```typescript
225
- quantumSplit(originalId: string, newId: string): boolean {
226
- const prop1 = this.getProperty(originalId);
227
- if (!prop1) return false;
228
- const prop2 = this.acquireProperty();
229
- try {
230
- this.getModule().i_swap(prop1, prop2, 0.5);
231
- } catch {
232
- this.releaseProperty(prop2, 0); // qudit limit
233
- return false;
234
- }
235
- 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;
236
212
  return true;
237
213
  }
238
214
  ```
239
215
 
240
- ### Overlap Entanglement
216
+ ### Overlap entanglement
241
217
 
242
- Objects that spatially overlap become entangled:
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.
243
220
 
244
221
  ```typescript
245
- this.getModule().i_swap(propA, propB, 0.5);
246
- ```
247
-
248
- ### Conditional Superposition
249
-
250
- A control property determines whether a target enters superposition:
251
-
252
- ```typescript
253
- m.hadamard(target, 1, [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
+ }
254
225
  ```
255
226
 
256
- ### Choosing Between Mechanisms
227
+ ### Choosing between mechanisms
257
228
 
258
229
  | Mechanism | Correlation | Use |
259
230
  |-----------|-------------|-----|
260
- | Predicated shift (CNOT) | Positive both match | Linked states: both alive or both dead |
261
- | Predicated hadamard | Conditional superposition | One object's quantumness depends on another |
262
- | `i_swap(0.5)` | Anti-correlated exactly one | Object splits into two ghosts |
263
- | `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 |
264
235
 
265
236
  ## Measurement
266
237
 
267
- Measurement collapses superposition to a definite value. Entangled partners collapse instantly too.
238
+ Measurement collapses superposition to a definite value. Entangled partners collapse too.
268
239
 
269
240
  ```typescript
270
- const [value] = m.measure_properties([prop]); // probabilistic collapse
271
- // Always pool after:
272
- this.deleteProperty(id);
273
- this.releaseProperty(prop, value);
241
+ const value = prop.measure(); // declared value, e.g. true or false
274
242
  ```
275
243
 
276
- ### 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
277
248
 
278
249
  ```typescript
279
- 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);
280
252
  ```
281
253
 
282
- ### Reading State (No Collapse)
254
+ ### Reading state (no collapse)
283
255
 
284
256
  ```typescript
285
- // Probabilities read-only, no state change
286
- const results = m.probabilities([prop]);
287
- // [{ 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 }, ...]
262
+
263
+ // Reduced density matrix: phase and correlation info, labelled with declared values
264
+ densityMatrix(a, b); // [{ row: [...], col: [...], real, imag }, ...]
288
265
 
289
- // Reduced density matrix phase and correlation info
290
- const rdm = m.reduced_density_matrix([prop1, prop2]);
291
- // Off-diagonal entries carry relative phase
266
+ probabilityWhen([a.is(true), b.is(false)]); // probability all predicates hold
292
267
 
293
- // Measure predicate projective measurement
294
- const outcome = m.measure_predicate([prop1.is(1), prop2.is(0)]);
295
- // 1 = satisfied, 0 = not
268
+ // Predicate measurement: collapses to agree, returns a boolean
269
+ measureWhen([a.is(true), b.is(false)]);
296
270
 
297
- // Forced measurement replay/save-load only
298
- const [value] = m.forced_measure_properties([prop], [1]);
271
+ // Forced measurement: replay and tests only. Throws if the value has zero probability.
272
+ prop.forcedMeasure(true);
273
+ forcedMeasure([a, b], [true, false]);
299
274
  ```
300
275
 
301
276
  ## Phase & Interference
302
277
 
303
- 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:
304
279
 
305
280
  ```typescript
306
- // Apply phase bias
307
- m.clock(prop, 0.5); // π/2 phase on |1⟩
308
-
309
- // Later, entangle phase redistributes probability
310
- m.i_swap(propA, propB, 0.5);
311
- // Same phase = constructive interference (probability concentrates)
312
- // 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
313
287
  ```
314
288
 
289
+ A phase before a single split changes nothing. It needs a later superpose or interaction to turn into probability.
290
+
315
291
  **Grover oracle** (walls/barriers): mark states with π phase, then diffuse with fractional Hadamard. Probability "bounces off" marked states.
316
292
 
317
293
  ```typescript
318
- m.phase_rotate([hexProp.is(wallState)], Math.PI); // mark
319
- m.hadamard(hexProp, 0.1); // diffuse
294
+ phaseRotate(Math.PI, { when: [hex.is(wallState)] }); // mark
295
+ hex.superpose(0.1); // diffuse
320
296
  ```
321
297
 
322
- **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.
323
299
 
324
- ### Visualizing Phase
300
+ ### Visualizing phase
325
301
 
326
302
  Extract from the reduced density matrix:
327
303
 
328
304
  ```typescript
329
- const rdm = m.reduced_density_matrix([propRef, propTarget]);
330
- for (const entry of rdm) {
331
- if (entry.row_values[0] === 1 && entry.row_values[1] === 0 &&
332
- entry.col_values[0] === 0 && entry.col_values[1] === 1) {
333
- 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);
334
309
  // Map to dial rotation, color hue, force magnitude, compass direction
335
310
  }
336
311
  }
337
312
  ```
338
313
 
339
- ## 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.
340
333
 
341
334
  ```typescript
342
- import { QuantumRecorder } from "quantum-forge/quantum";
335
+ import { QuantumRecorder, quantum, measure } from "quantum-forge/quantum";
343
336
 
344
- const recorder = new QuantumRecorder(qpm);
337
+ const recorder = new QuantumRecorder();
345
338
  recorder.startRecording();
346
- // ... operations ...
347
- const log = recorder.getOperationLog(); // serializable JSON (also returned by stopRecording())
348
- // Save: localStorage.setItem("quantum-save", JSON.stringify(log));
349
- // 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);
350
348
  ```
351
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
+
352
360
  ## Performance
353
361
 
354
- **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.
355
365
 
356
366
  Shipped limits: Qutrit Edition has dimension 3, 12 max qudits. Qubit Edition has dimension 2, 20 max qudits.
357
367
 
358
- | Active Properties | Performance |
359
- |-------------------|-------------|
360
- | 1–4 | No issues |
361
- | 4–8 | Good with pooling |
362
- | 8–12 | At the limit, aggressive pooling required |
363
-
364
- **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.
365
370
 
366
- **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.
367
374
 
368
375
  ```typescript
369
376
  // Monitor WASM memory
@@ -453,6 +460,7 @@ loop.start();
453
460
 
454
461
  Map probability to visual properties (opacity, size, color):
455
462
 
463
+ <!-- sample:skip: method shown outside its class -->
456
464
  ```typescript
457
465
  protected draw(state: GameState) {
458
466
  for (const ball of state.balls) {
@@ -499,7 +507,7 @@ input.destroy();
499
507
 
500
508
  | Export | Key APIs |
501
509
  |--------|----------|
502
- | `./quantum` | `ensureLoaded`, `QuantumPropertyManager`, `QuantumRecorder`, `useQuantumForgeBuild` |
510
+ | `./quantum` | `quantum`, `Quantum`, `measure`, `probabilities`, `densityMatrix`, `measureWhen`, `phaseRotate`, `ensureLoaded`, `QuantumRecorder`, `useQuantumForgeBuild`, `setWasmBasePath` |
503
511
  | `./logging` | `Logger` |
504
512
  | `./vite-plugin` | `quantumForgeVitePlugin` |
505
513