Core concepts
Quantum Forge gives game objects real quantum state. The core usage loop is:
- Declare quantum properties on things with
quantum([...values]), giving them quantum state - Apply gates to change that state. Gates transform it without collapsing it
- Let properties interact. A gate that touches two properties, or one conditioned on another property, leaves them entangled
- Measure to collapse quantum state into a classical value (the dramatic game moment)
- Dispose the property when the thing it describes is gone
Everything else (recording, entity cleanup, diagnostics) is tooling around this loop.
Coming from 2.x?
The 3.0 API replaces QuantumPropertyManager and getModule() gate calls with one handle per property. Migrating to 3.0 maps every old call to its new form.
Programmer's mental model
If you know how to work with collections, you already have the intuition for quantum state.
Superposition is a weighted collection of values. Each entry is a basis state (a value, or a combination of values when several properties are involved), paired with a weight. A qubit in equal superposition is a collection with two entries, |0⟩ and |1⟩, each weighted equally.
Gates are collection algorithms. They can change the values, the weights, or both, and they apply to every entry at once. cycle is "increment each value mod d". shift is "decrement each value mod d" (the inverse). You're running a transform over the whole collection.
Predicated gates are the same thing with a filter on each entry's value. "If the mth value is 1, flip the nth." The gate only fires for entries where the condition holds. This is how entanglement happens: a predicated gate correlates two properties because it changes some entries and not others, based on value combinations.
The quantum part is what the weights actually are. They aren't probabilities. They're complex amplitudes. This matters when a gate produces two entries with the same value: their amplitudes add. If they're in phase, the weights reinforce (constructive interference). If they're out of phase, the weights cancel (destructive interference). This is where quantum behavior parts ways with classical randomness.
The probability of getting a particular value on measurement is the squared magnitude of its amplitude. That's it. Build up amplitude on values you want, cancel amplitude on values you don't.
Quantum properties
A quantum property is one qudit: a d-dimensional quantum digit. You declare it by the values it can take, and quantum() returns a handle to it:
import { ensureLoaded, quantum } from "quantum-forge/quantum";
await ensureLoaded(); // load the WASM simulator once, before the first quantum() call
const alive = quantum([false, true]); // a qubit, starts false
const color = quantum(["red", "green", "blue"]); // a qutrit, starts "red"
const die = quantum(3); // numeric form: values 0, 1, 2The number of values is the dimension, and every property starts at its first declared value. Values can be strings, numbers or booleans, and each must appear once. quantum() is the only way to make a handle.
Store the handle on the game object it describes. There is no id map and no manager:
ball.exists = quantum([false, true]);A ball without a handle is classical.
Values and indices
Every method that takes a value (is, isNot, probability, forcedMeasure) accepts either a declared value or its index. On color, color.is("green") and color.is(1) build the same predicate. A value that is not declared throws at the call. Measurement always returns the declared value.
In ket notation, |0⟩ is the first declared value, |1⟩ the second, and so on. On quantum([false, true]), |0⟩ is false and |1⟩ is true.
Dimension
| Dimension | Declaration | Use case |
|---|---|---|
| 2 (qubit) | quantum([false, true]) | Binary: exists/doesn't, alive/dead, left/right |
| 3 (qutrit) | quantum(["rock", "paper", "scissors"]) | Three-way: rock/paper/scissors, left/center/right |
The package ships two editions: the Qutrit Edition (default) allows up to three values per property and 12 qudits in one shared state, while the Qubit Edition allows two values per property and up to 20 qubits. You choose your edition during npx quantum-forge-engine init. quantum() throws when a property has fewer than two values or more than the loaded build allows. Start with two values unless your design needs three. See Quantum setup: editions.
Gates
Gates are methods on the handle. They change the state without collapsing it, and they return the handle, so they chain:
const coin = quantum([false, true]);
coin.flip(); // false → true
coin.superpose(); // true → (|0⟩ − |1⟩)/√2, an equal superposition
// Rotate phase (invisible to measurement, but it changes what the next gate does)
coin.phase(0.5); // a π/2 phase on truePhysics names are the primary names. Each game-word alias runs exactly the same gate:
| Physics call | Alias | What it does | Common use |
|---|---|---|---|
cycle() | next(), and flip() on qubits | Next value, wrapping (|0⟩→|1⟩→...→|d-1⟩→|0⟩) | Start an object at "exists" |
shift() | previous() | Previous value, wrapping. The inverse of cycle | Step back |
hadamard() | superpose() | Spread the property evenly across every value | Put an object "into quantum" |
clock() | phase() | Rotate the phase of each value by its index | Bias future interactions |
x(), z() | Pauli X (the same gate as shift) and Pauli Z (the same as clock) | ||
y() | Pauli Y, qubits only | Bloch sphere rotation | |
a.iSwap(b, fraction) | Anti-correlated entanglement between two properties | Split one object into two |
At dimension 2, increment and decrement mod 2 are the same, so cycle and shift coincide and either one is the NOT gate. From dimension 3 up they move the state in opposite directions: cycle() sends |0⟩ to |1⟩, shift() sends |0⟩ to |2⟩. x() is the same gate as shift(), so on a qutrit it moves backward too.
Most gates take an optional fraction first for partial application. flip(0.5) is the square root of NOT, and flip(0.1) barely moves the state. Leave the fraction out for the whole gate. See Gates for the complete reference.
Predicated operations
This is the mechanism that makes quantum games work. Every gate accepts predicates in { when: [...] } that condition it on the value of other properties:
// This Hadamard only acts where control is true
target.superpose({ when: [control.is(true)] });
// A fraction goes first, the options last
target.flip(0.5, { when: [control.is(true)] });When a gate is predicated on another property, the two properties become entangled. Their quantum states are now correlated. Entanglement in Quantum Forge is not a special operation. There is no entangle() call. It is what an interaction leaves behind.
Predicated gates create entanglement
Before a predicated gate, the control and target are independent. After it, they share a quantum state, and measuring one settles the other.
The simplest example: a predicated flip on a qubit is the CNOT gate, the most basic entangling operation in quantum computing.
const control = quantum([false, true]);
const target = quantum([false, true]);
control.superpose(); // (|0⟩ + |1⟩)/√2
// CNOT: flip target only where control is true
target.flip({ when: [control.is(true)] });
// Now they're entangled. The state is (|00⟩ + |11⟩)/√2:
// measuring control as false means target is false,
// measuring control as true means target is true.
// Positively correlated.Any predicated gate that reads a property in superposition can entangle. A predicated superpose puts the target into superposition only where the control holds, and a predicated phase rotates phase conditionally (target.phase({ when: [control.is(true)] }) is the CZ gate). Each produces different correlations.
iSwap: anti-correlated entanglement
iSwap is a two-property gate that creates anti-correlated entanglement: exactly one of the pair will measure true. Use it when an object splits into two ghosts.
const a = quantum([false, true]).flip(); // a starts true (exists)
const b = quantum([false, true]); // b starts false (doesn't exist)
a.iSwap(b, 0.5);
// Result: (|10⟩ + i|01⟩)/√2
// 50% chance a is true and b is false
// 50% chance a is false and b is true
// Anti-correlated: exactly one existsWhere CNOT creates positive correlation (both match), iSwap creates anti-correlation (exactly one). Which you use depends on the game mechanic you want. See Entanglement for the full pattern catalog.
iSwap takes predicates too, for controlled entanglement:
// Only split if the gate property is true
a.iSwap(b, 0.5, { when: [gate.is(true)] });Predicate types
prop.is(value) // holds where the property equals value
prop.isNot(value) // holds where it does notBoth accept a declared value or its index. Multiple predicates are AND'd: all must hold for the gate to act. A predicate may not read the property the gate acts on; that throws. See Gates: conditional gates.
Measurement
Measurement collapses quantum state to a definite classical value. This is the moment quantum becomes real.
const value = ball.exists.measure(); // true or false, the declared value
// The property has collapsed. Entangled partners collapse too.Key behaviors:
- The outcome is probabilistic, sampled from the quantum state's probability distribution
- The state collapses to the measured value
- Entangled partners collapse in the same step. Measuring one settles the other
The handle stays usable after a measurement. It holds a definite value and can go back into superposition with another gate. Measuring does not end its life; dispose() does.
Disposing
When the object a property describes is gone, dispose the handle:
ball.exists.dispose();dispose() measures the property, which also collapses anything entangled with it, then frees the qudit. A qudit that is alone in its state is reset and cached for the next quantum() of the same dimension; one that still shares a state is destroyed, which takes it out of that state. A using declaration disposes at the end of a scope, and the engine's EntityManager disposes handles when it removes an entity. Disposing keeps a game that spawns and removes objects within the qudit limit (12 in the Qutrit Edition, 20 in the Qubit Edition). See Lifecycle.
Forced measurement
For replay and save/load, you can force a measurement to a specific outcome:
ball.exists.forcedMeasure(true); // collapses to trueForcing an outcome whose probability is zero throws. QuantumRecorder uses forced measurement to replay a log. See Recording and replay.
Reading quantum state
You can inspect quantum state without collapsing it. These reads do not change the state or the statistics of anything, so a renderer can call them every frame.
A read over several properties that live in separate states merges those states into one first. The statistics are the same afterward, but the combined state is larger and later gates on it are slower. Reading one property at a time, or only properties you know are already entangled, avoids this. See Measurement.
Probabilities
ball.exists.probability(true);
// 0.5 for a superposed coin
ball.exists.probabilities();
// [{ value: false, probability: 0.5 }, { value: true, probability: 0.5 }]Use these to:
- Render ghost objects at their existence probability (opacity = probability)
- Show probability meters in the UI
- Make AI decisions based on quantum state
Density matrix
For phase, coherence, and correlations between properties:
import { densityMatrix } from "quantum-forge/quantum";
densityMatrix(a, b);
// [{ row: [true, false], col: [false, true], real, imag }, ...]Rows and columns are labelled with declared values. The off-diagonal entries carry relative phase, invisible to probabilities() but central to how properties interact. Games read phase to drive visual effects:
// Relative phase between two entangled properties
for (const entry of densityMatrix(a, b)) {
if (entry.row[0] === true && entry.row[1] === false &&
entry.col[0] === false && entry.col[1] === true) {
const phase = Math.atan2(entry.imag, entry.real);
// Map to dial rotation, color hue, force magnitude, etc.
}
}Measuring a condition
measureWhen asks whether every predicate holds, collapses the state to agree with the answer, and returns a boolean. It does not have to collapse the properties to single values:
import { measureWhen } from "quantum-forge/quantum";
const both = measureWhen([a.is(true), b.is(false)]);Putting it together
Here's the whole loop for a game where enemies can be "maybe there" and can split into entangled twins:
import { ensureLoaded, quantum, type Quantum } from "quantum-forge/quantum";
await ensureLoaded();
interface Enemy {
id: string;
alive?: Quantum<boolean>; // no handle: a classical enemy that is simply there
}
/** Give an enemy quantum state: an equal superposition of there and not there. */
function makeQuantum(enemy: Enemy): void {
enemy.alive = quantum([false, true]).superpose();
}
/** Split one real enemy into two ghosts, exactly one of which is real. */
function split(enemy: Enemy, twin: Enemy): void {
enemy.alive = quantum([false, true]).flip(); // there
twin.alive = quantum([false, true]); // not there
enemy.alive.iSwap(twin.alive, 0.5);
}
/** Read existence probability without collapsing. */
function opacity(enemy: Enemy): number {
return enemy.alive ? enemy.alive.probability(true) : 1;
}
/** Collapse. Returns whether the enemy was there. */
function observe(enemy: Enemy): boolean {
if (!enemy.alive) return true;
const here = enemy.alive.measure();
enemy.alive.dispose();
enemy.alive = undefined;
return here;
}This covers the full loop: declare properties, apply gates, let them interact, read state, measure and dispose. The specifics of which gates to apply and what measurement means vary from game to game, but the structure is the same.
What's next
- Gates: the full gate reference with predicates and fractions
- Measurement: collapse, joint measurement, measuring a condition
- Entanglement: entanglement patterns (split, correlated pairs, overlap, conditional superposition)
- Phase and interference: phase rotation, interference, the Grover oracle
- Lifecycle: dispose,
using, entities, state budget queries - Error handling: typed JS errors, the OOM guard, graceful degradation
- Performance: qudit limits and how to stay within them