Skip to content

Core concepts ​

Quantum Forge gives game objects real quantum state. The core usage loop is:

  1. Declare quantum properties on things with quantum([...values]), giving them quantum state
  2. Apply gates to change that state. Gates transform it without collapsing it
  3. Let properties interact. A gate that touches two properties, or one conditioned on another property, leaves them entangled
  4. Measure to collapse quantum state into a classical value (the dramatic game moment)
  5. 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:

typescript
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, 2

The 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:

typescript
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 ​

DimensionDeclarationUse 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:

typescript
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 true

Physics names are the primary names. Each game-word alias runs exactly the same gate:

Physics callAliasWhat it doesCommon use
cycle()next(), and flip() on qubitsNext value, wrapping (|0⟩→|1⟩→...→|d-1⟩→|0⟩)Start an object at "exists"
shift()previous()Previous value, wrapping. The inverse of cycleStep back
hadamard()superpose()Spread the property evenly across every valuePut an object "into quantum"
clock()phase()Rotate the phase of each value by its indexBias future interactions
x(), z()Pauli X (the same gate as shift) and Pauli Z (the same as clock)
y()Pauli Y, qubits onlyBloch sphere rotation
a.iSwap(b, fraction)Anti-correlated entanglement between two propertiesSplit 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:

typescript
// 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.

typescript
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.

typescript
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 exists

Where 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:

typescript
// Only split if the gate property is true
a.iSwap(b, 0.5, { when: [gate.is(true)] });

Predicate types ​

typescript
prop.is(value)     // holds where the property equals value
prop.isNot(value)  // holds where it does not

Both 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.

typescript
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:

typescript
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:

typescript
ball.exists.forcedMeasure(true); // collapses to true

Forcing 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 ​

typescript
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:

typescript
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:

typescript
// 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:

typescript
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:

typescript
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

Powered by Quantum Forge