Entanglement
Entanglement is the mechanic that makes quantum games different from classical ones. When two properties are entangled, they share a quantum state, and measuring one settles the other. No classical system can do this.
How entanglement happens
There is no entangle() call. Entanglement is what an interaction leaves behind.
A gate on one property is evolution: it changes that property and nothing else. A gate that touches two properties, or one whose predicate reads another property, is an interaction. After an interaction the two properties can no longer be described separately. Two kinds of call interact:
- Predicated gates: any gate conditioned on another property's value, such as CNOT
- Two-property gates:
iSwap(andswap) act on two properties at once
Both work. Which you use depends on the correlations you want.
Predicated gates (CNOT and friends)
A predicated gate acts only where a control property meets a condition. Because the control is in superposition, the gate creates a correlation, and correlation between quantum properties is entanglement.
The simplest example: a predicated flip on a qubit is the CNOT gate.
import { ensureLoaded, quantum } from "quantum-forge/quantum";
await ensureLoaded();
const control = quantum([false, true]);
const target = quantum([false, true]);
control.superpose(); // (|0⟩ + |1⟩)/√2
// CNOT: flip target where control is true
target.flip({ when: [control.is(true)] });
// Result: (|00⟩ + |11⟩)/√2
// Measuring control as false means target is false
// Measuring control as true means target is true
// Positively correlatedThis generalizes to any gate. A predicated superpose puts the target into superposition only where the control allows it. A predicated phase rotates phase conditionally: target.phase({ when: [control.is(true)] }) is the CZ gate. Each creates a different kind of entanglement with different correlations.
// Controlled Hadamard: target enters superposition only where control is true
target.superpose({ when: [control.is(true)] });
// Two controls: the gate acts only where both are true
target.flip({ when: [controlA.is(true), controlB.is(true)] });
// Negative predicate: the gate acts where control is NOT false
target.next({ when: [control.isNot(false)] });A predicate may not read the property the gate acts on. prop.flip({ when: [prop.is(true)] }) throws. Controls and targets have to be different properties.
On a control with a definite value, a predicated gate entangles nothing: CNOT on a control that is definitely true is a plain flip, and on one that is definitely false it does nothing. The control has to be in superposition for the pair to become correlated.
iSwap
iSwap is a two-property entangling gate. With a fraction of 0.5, starting from |10⟩ (a true, b false):
|10⟩ → (|10⟩ + i|01⟩) / √2This means:
- 50% chance a is
trueand b isfalse - 50% chance a is
falseand b istrue - These outcomes are anti-correlated. Exactly one of the pair is
true - Measuring either one settles the other
a.iSwap(b, fraction);Where predicated gates create positive correlations (both the same, or one depending on the other), iSwap naturally creates anti-correlations (exactly one of two). Use it for "the object splits into two ghosts, and only one is real." On two equal values iSwap does nothing, so the pair must differ: one set, one not.
iSwap takes predicates too, for controlled entanglement:
// Only split where the gate property is true
a.iSwap(b, 0.5, { when: [gate.is(true)] });Choosing between them
| Mechanism | Correlation | Typical use |
|---|---|---|
b.flip({ when: [a.is(true)] }) (CNOT) | Positive, both match | Linked states: both alive or both dead |
b.superpose({ when: [a.is(true)] }) | Conditional superposition | One object's "quantumness" depends on another |
a.iSwap(b, 0.5) | Anti-correlated, exactly one | Object splits into two ghosts |
iSwap(0.5), phase(f), iSwap(0.5) | Tunable bias | Player-influenced split probability |
Entanglement patterns
The patterns below keep each handle on the game object it describes: ball.exists, enemy.alive. An object without a handle is classical.
Pattern 1: entangle-split (iSwap)
A classical object enters a quantum zone and splits into an entangled pair. One property starts at true (exists), the other at false (doesn't exist), then iSwap(0.5) creates an anti-correlated superposition.
function entangleSplit(ball: Ball, newBall: Ball): void {
ball.exists = quantum([false, true]).flip(); // exists
newBall.exists = quantum([false, true]); // does not exist
ball.exists.iSwap(newBall.exists, 0.5); // (|10⟩ + i|01⟩)/√2
}After the split:
- Each ball exists with probability 1/2
- Measuring one as existing settles the other as not existing
- The game shows both balls as semi-transparent ghosts
Pattern 2: correlated pair (CNOT)
Two objects whose states always match: both alive or both dead. This uses a predicated flip instead of iSwap.
function linkPair(a: Enemy, b: Enemy): void {
a.alive = quantum([false, true]).superpose();
b.alive = quantum([false, true]);
b.alive.flip({ when: [a.alive.is(true)] }); // CNOT: b matches a
}After the CNOT:
- The state is
(|00⟩ + |11⟩)/√2 - Measuring one as alive means the other is also alive
- Measuring one as dead means the other is also dead
Pattern 3: quantum-split (iSwap)
Split an object that is already quantum. The new property starts at false and entangles with the existing one. The split adds one qudit to the original's shared state, so check that there is room first:
import { getMaxQudits, quantum } from "quantum-forge/quantum";
function quantumSplit(original: Ball, clone: Ball): boolean {
if (!original.exists) return false;
if (original.exists.numActiveQudits() >= getMaxQudits()) return false; // state is full
const split = quantum([false, true]);
original.exists.iSwap(split, 0.5);
clone.exists = split;
return true;
}Check capacity before the interaction rather than catching the throw. Each caught over-limit throw leaks WASM memory, and after enough of them every later call fails.
Pattern 4: overlap entanglement
Objects that overlap in space become entangled: adjacent hexes at shared boundaries, overlapping pieces in a puzzle, balls that pass through each other.
function overlap(a: Ball, b: Ball): void {
if (a.exists && b.exists) a.exists.iSwap(b.exists, 0.5);
}If a and b each came from a different split, this joins both pairs into one shared state of four qudits. And remember that iSwap on two equal values does nothing, so the overlap only matters where the two can differ.
Pattern 5: conditional superposition
A control property decides whether another object enters superposition:
function conditionalQuantum(target: Ghost, control: Switch): void {
if (!target.here || !control.on) return;
// target.here enters superposition only where control.on is true
target.here.superpose({ when: [control.on.is(true)] });
}Shared state growth
When an interaction connects properties from different shared states, the simulator merges them into one combined state with a tensor product. The combined state vector can grow as large as dimension ^ qudits entries, and the merge is the most expensive operation in quantum simulation.
What triggers it:
a.iSwap(b, ...)ora.swap(b)whereaandbare in different shared states- Any predicated gate whose control and target are in different shared states
- A multi-property read (
probabilities(a, b),densityMatrix(a, b),probabilityWhen(...)) over properties in different shared states
What keeps it small:
- Dispose handles when the object they describe is gone. A disposed property stops counting against the qudit limit: a qudit alone in its state is reset and cached for the next
quantum(), and one that still shares a state is destroyed, which takes it out of that state. The nextquantum()always starts from a qudit of its own - Keep handles on entities that the engine's
EntityManagerremoves; it disposes them for you - Use two values unless your design needs three
What happens when you run out:
- The ceiling depends on the edition you ship: 12 qudits in one shared state in the Qutrit Edition (the default), 20 qubits in the Qubit Edition. See Performance: shipped limits
- An interaction that would pass the limit throws
- Check
numActiveQudits()againstgetMaxQudits()before the interaction, as thequantumSplitexample above does, instead of catching the throw
Entanglement and measurement
When you measure one property of an entangled pair:
- That property collapses to a definite value
- The entangled partner collapses in the same step
- For anti-correlated pairs (
iSwap(0.5)from|10⟩), measuring one astrueleaves the otherfalse, every time - For positively correlated pairs (CNOT from
(|00⟩+|11⟩)/√2), measuring one astrueleaves the othertruetoo
Disposing measures, so disposing one handle of an entangled pair also settles its partner. This is what makes quantum scoring work in Quantum Pong: measuring one ball's existence settles the other ball's existence.
Visualizing entanglement
The common pattern: read prop.probability(true) and map it to opacity, size, color intensity, or glow. Reading never collapses anything, so it is safe in a render loop. In Quantum Pong, entangled balls are drawn as semi-transparent ghosts whose opacity tracks their existence probability.