Quantum Pong
The example that ships inside quantum-forge: a Pong game where balls that enter a quantum zone split into entangled pairs, carry a real existence probability, and are measured when they reach a goal.
Overview
The quantum zones are two small extrusions at the top and bottom edges of the field. A ball that enters one splits into two entangled balls. Exactly one of the pair exists, and the game does not know which until one of them reaches a goal and is measured. Measuring one collapses both: if it exists it scores, and its twin vanishes at the same moment.
Players can shift the odds by turning a phase dial on their paddle, which changes how entangled balls share probability the next time they interact.
Quantum mechanics used
| Mechanism | Call | Game effect |
|---|---|---|
| Entangle-split | quantum([false, true]), flip(), iSwap(other, 0.5) | Ball splits into an entangled pair |
| Quantum-split | iSwap(fresh, 0.5) on a ball that is already quantum | A ghost ball splits again |
| Balls meeting in a zone | iSwap(other, 0.5) | Entangled balls redistribute probability |
| Phase dial | phase(angle / π) | Paddle hit changes the next interaction |
| Phase indicator | densityMatrix(a, b) | Draws the relative phase between two balls |
| Ghost opacity | probability(true) | Alpha equals the chance the ball exists |
| Goal line | measure(), then dispose() | Scores or leaves a ghost burst |
| Reset | dispose() | Frees the ball's qudit |
Quantum functions
Each quantum ball carries its own existence property: ball.exists is a quantum([false, true]) handle, and true means the ball exists. A ball without a handle is classical and always exists. logic/QuantumRegistry.ts holds every function that touches quantum state. It has no class and no id map; each function takes the balls, or their handles, directly.
import { quantum, densityMatrix, type Quantum } from "quantum-forge/quantum";
type Existence = Quantum<boolean>;
interface QuantumBall {
id: string;
exists?: Existence;
}
// A classical ball enters the zone: (|10⟩ + i|01⟩)/√2
function entangleSplit(): [Existence, Existence] {
const original = quantum([false, true]);
const split = quantum([false, true]);
original.flip().iSwap(split, 0.5);
return [original, split];
}
// A ball that is already quantum splits again
function quantumSplit(original: QuantumBall): Existence | null {
if (!original.exists) return null;
const split = quantum([false, true]);
original.exists.iSwap(split, 0.5);
return split;
}
// Paddle hit: phase(fraction) is the alias for clock(fraction)
function applyPhase(ball: QuantumBall, fraction: number): void {
ball.exists?.phase(fraction);
}
// Read every frame; never collapses anything
function existenceProbability(ball: QuantumBall): number {
return ball.exists ? ball.exists.probability(true) : 1;
}
// Relative phase between two balls, for the dial display
function relativePhase(ref: QuantumBall, target: QuantumBall): number {
if (!ref.exists || !target.exists) return 0;
for (const entry of densityMatrix(ref.exists, target.exists)) {
if (entry.row[0] === true && entry.row[1] === false &&
entry.col[0] === false && entry.col[1] === true) {
return Math.atan2(entry.imag, entry.real);
}
}
return 0;
}
// Goal line: measure, then end the handle's life
function measureExistence(ball: QuantumBall): boolean {
if (!ball.exists) return true;
const value = ball.exists.measure();
ball.exists.dispose();
ball.exists = undefined;
return value;
}The shipped file also catches a WASM throw in entangleSplit, quantumSplit and applySwap and disposes the fresh handle, as a safety net. The capacity check below is what keeps those throws from happening.
Key design patterns
Entangle-split as the core mechanic
The quantum zone is a spatial trigger. When a classical ball crosses it:
- The game declares two properties with
quantum([false, true]) - The original is flipped to
true(exists); the new ball stays atfalse iSwap(split, 0.5)entangles them into(|10⟩ + i|01⟩)/√2- Both balls render at 50% opacity, since each exists with probability 1/2
There is no entangle() call. The iSwap is an interaction between two properties, and entanglement is what it leaves behind.
Phase as player skill
The phase dial runs exists.phase(angle / π) on a ball when it hits a paddle, which turns the relative phase between the entangled pair by angle. Phase does not change whether a ball exists. It changes how entangled balls share probability the next time they iSwap with each other, so a skilled player can raise their chance of scoring. A split with a fresh ball gives the same result whatever the phase was; the dial only pays off when the entangled twins meet again. See Phase & interference.
Measurement as drama
Measurement is the highest-drama moment in the game. The player does not know which ball will score until it crosses a goal line and gets measured. A score burst shows true; a ghost burst shows false. Entangled partners collapse in the same instant, wherever they are on the field.
Capacity and disposal
Every quantum ball holds one qudit, and entangled balls share one state, which holds at most getMaxQudits() qudits: 12 on the default Qutrit Edition, 20 on the Qubit Edition. The max-balls slider stops at that number, so no split asks WASM for more.
The game disposes a ball's handle when it is measured, and also when its existence probability drops to zero. A disposed qudit stops counting against the limit: one that still shares a state with other balls is destroyed, and one that is alone is reset and reused by the next quantum(). That is why a session can run indefinitely. Check capacity before an interaction rather than catching the throw, because each over-limit throw leaks WASM memory. See Lifecycle.
Ideas to try
- Change the split fraction in
entangleSplitfrom0.5to0.25. One twin stays nearly solid while the other goes faint. - Make the phase dial cost something, so steering entangled balls becomes a decision.
- Add a third zone that runs
superpose()instead of a split.
Source
Quantum Pong comes in two builds with the same logic/QuantumRegistry.ts.
Core only. It ships inside quantum-forge at examples/quantum-pong/, draws with the Canvas 2D API, and needs nothing but the core package. Copy it into a new project with:
npx quantum-forge example my-pongIt includes eight tests that play the game headless against the WASM. The command needs core 3.1.0 or newer.
On the engine. The optional quantum-forge-engine package has a fuller build with PixiJS rendering, particles, audio, gamepad input, scenes and 75 tests:
npx quantum-forge-engine init my-game --template quantum-pong