A game that only gets smarter when it can prove it learned you.
Worm is a snake/Tron duel that studies one specific player across rounds — and it may commit harder to an intercept only after its forecast provably beats a simple rival based on your own habits. No mystery difficulty ramp. No flattering accuracy counter. You watch the claim form — or fail — in public.
Play it: worm.robertgpt.ai — or
cargo run --release for the terminal build.
Jump to: Playing it · The claim, and how to falsify it · Development · Decisions (23 ADRs)
Most "adaptive" games can congratulate themselves for nothing. Imagine a rival that predicts straight while you are trapped in a corridor, then calls the inevitable move proof that it understands you. That number can look excellent even when the board supplied the answer — in worm, roughly 95% of frames are routine straight travel. A clock-based difficulty ramp has the same problem: time passed, but no intelligence was earned. The consequence is personal: you cannot tell whether the opponent adapted to you or merely got stronger behind a confident dashboard.
So worm removes every cheap way its opponent could appear intelligent: forecasts are scored only on genuine decisions, published (hash-sealed) before your input lands, and compared against your own online base rate rather than imaginary uniform chance. The refusal that anchors everything: if a random coin-flip player ever looks learnable, the evidence harness is leaking the answer — and every other number in this repository is void.
You steer one luminous cycle. The computer steers the other — and keeps a field notebook about the choices only you could have made. Before a meaningful turn, the opponent commits a forecast. After you move, worm compares that forecast with both reality and a deliberately simple rival based on your own usual moves. Evidence that survives the controls becomes an earned read. That read can increase commitment to an intercept; it never switches off the rules that keep the computer alive.
What worm counts, and refuses:
| Signal | Counts as learning? | Why |
|---|---|---|
| A voluntary left turn | Potentially | You had a real alternative |
| Straight through one open corridor | Not by itself | The board made it easy |
| Random coin-flip play | Must stay at chance | There is no stable habit to read |
The opponent gets dangerous by surviving an audit of its predictions — not by waiting for level two.
cargo test --test persona_learning -- --nocaptureScripted personas play the game with a known habit. The CPU's forecasts are scored only on the frames where the persona actually exercised that habit — the frames where the answer was a fact about the player rather than a fact about the board. Continuing down a corridor, or turning because there was only one way out, is free accuracy and is excluded.
The suite is built around controls, because a test that cannot fail proves nothing:
| persona | what it establishes | |
|---|---|---|
| POSITIVE | compass — prefers a fixed direction |
An absolute habit the model provably can hold. If this stops being learned, the pipeline has regressed. |
| NULL | coinflip — no habit at all |
There is nothing to learn. If the CPU ever reads this above chance, the harness is leaking the answer and every other number here is void. |
| ACCEPTANCE | lefty — "break left when cornered" |
Heading-relative, and the actual product goal. |
Latest measured:
compass (POSITIVE) n=10052 read 95.3% chance 34.3% z= +128.9
coinflip (NULL) n=15292 read 34.8% chance 35.0% z= -0.5
lefty (ACCEPTANCE) n=144 read 74.3% chance 50.0% z= +5.8
The null control is what makes the other two rows evidence: the same CPU that
reads lefty at 74% remains unable to read an opponent with no habit, so the
suite is not leaking the answer into the forecast.
The claim is not that the CPU predicts you — it is that predicting you
makes it beat you. cargo test --test domination holds board,
seeds, and opponent fixed and varies only whether the CPU may remember.
Since ADR-022 the memory
invariant runs as five paired 90-game arms scored by expected points
(a draw counts half in both arms), asserting mean and median paired
gap ≤ 5 points with every per-seed gap published — one pathological
seed stays visible on the record instead of vetoing the doctrine or
being re-baselined away. A survival-competent persona is beaten ~80-90%
by default play, so memory's job here is non-inferiority plus a
genuinely earned read; the conversion claim is carried by the
acceptance tests (a strict alternator — a habit no modal baseline can
call — read far above chance end-to-end). The evidence rebuild that
made these numbers honest, and the fabricated 81% they replaced, are
ADR-020; the earlier failures and
reverts are ADR-009,
ADR-012, and
ADR-014.
A read is only worth something if it reaches the wheel. read_rate is
lift over the player's own base rate (scaled by which temperament is
winning — see the Exp3 portfolio in the code), and it drives how much
survival margin the CPU will spend on an intercept. Confidence buys
commitment, never safety: the survival floor is untouched at every read
rate, and the dodge, ring-evacuation and wall-follow layers never consult
the hunt floor. A well-read player faces a CPU that takes intercepts it
would otherwise decline. They never face one that suicides. See
ADR-007.
The habit family alone settles around 0.80 lift: a single global turn prior learns "this player breaks left" but not "…when the wall is three cells away". The headroom turned out to be why the player moves, not finer habit features: the ensemble now carries six errand models — {food, hunt, weapon} × {holds-their-line, weaves} — each a BFS route step toward an observed goal, elected by the same fixed-share weights as everything else. Against a committed forager persona the voluntary-turn read went from 6% (habit models only) to ~76-85% (ADR-012, ADR-014). Compensating with a difficulty multiplier would just restore the arbitrary clock ramp ADR-007 deleted.
The whole stack's report card, session by session against the one player with a long history (153 rounds; peak earned read = the statistically-proven evidence the difficulty spends):
| session | rounds | CPU win % | peak earned read |
|---|---|---|---|
| 1 (pre-honest era) | 48 | 10% | 0.00 |
| 2 | 20 | 75% | 0.00 |
| 3 (honest evidence lands) | 13 | 38% | 0.41 |
| 4 | 13 | 46% | 0.63 |
| 5 | 59 | 80% | 0.62 |
Honest caveats attached: sessions 1–2 ran on older builds, so the full
arc conflates build improvements with learning; the clean single-build
evidence is session 5 alone — 47 of 59 under one binary with the
earned read pinned at 0.62. Meanwhile the drift alarm — its own
anytime-valid evidence family — LATCHED on real play when the owner
started scrambling his turn timing to fight the read, and the
between-round ratchet (an exact minimum cut over the situation cells'
co-movement graph, examples/drift_partition.rs) named exactly which
region of his game moved. His timing entropy went up; the side read
held at 0.71; the CPU kept winning through the noise he was
deliberately generating. That sentence is the product.
ADR-021 turned "where else can it learn?" into nine measured surfaces, built kata-by-kata under two external design consults: a tactic ledger with Thompson-sampled preferences (which intercept kills YOU), a weapon opportunity book with a bounded bait-exploration floor, a per-cause death ledger that raises its escape floors against players who box it in, a persisted swerve grammar (your rhythm is read from round one when you return), a drift alarm (its own anytime-valid evidence family) that notices when you change your game and says so, an epistemic self-map of the situations it has never seen you in, and an envelopment alarm that feels its space collapsing before the box closes. Two more are deliberately gated: fleet warm-starting (policy written, waits for a second returning human) and any neural challenger (a plateau detector measures its entry gate weekly — and currently reports the target MOVING: the first player is measurably adapting to being read, which holds that door shut). Every aggressive consumer spends only round-boundary snapshots of family-gated evidence; everything else is narration. A committed golden brain file makes wiping a player's learning a test failure instead of an accident. The full prose tour of every method lives in docs/learning-methods.md, and the post-round 📓 TELL ME MORE button now writes from the CPU's complete dossier — the round's events set against every round you have ever played, ledgers quoted by name.
The owner's own 45-round ghost corpus then exposed the next wall
(ADR-020): global model selection let
straight-frame volume (88% of play) crown always-straight experts, so
the published forecast scored 9% on his voluntary turns while unelected
models in the same ensemble scored 55% on those exact frames. The fix
is class-conditional selection — a turn-HAZARD model (context cells
over swerve cadence, food alignment, and pursuit pressure) for WHEN,
a turn-scored book over the same roster for WHICH WAY, a no-knob
derived gate deciding what gets published, and a fourteenth model
(alt, the rhythm reader — a variable-order model over voluntary
swerves) that the book elects on the frames it exists for. The book's
precommitted side calls are scored through the same honest machinery
as everything else, and they are where the CPU earned its first
statistically proven read of the owner's play — his alternation,
called at 71% on genuine choices across a thousand prequential
events.
The obvious fix — compare against uniform chance, as rps.shaal.dev does with its stated 33% baseline — fails for a subtler reason. An RPS player really is near-uniform; they are trying to be random. A worm player is not. Against a straight-driving opponent, "always predict Straight" scores 98%, which against a 33% baseline reads as "it has found a pattern". It has found nothing, and the error is biased toward flattering the CPU.
So the baseline is the player's own base rate — the realized accuracy of an online "predict their commonest move so far" rival — and significance is an exact McNemar test over the frames where the CPU and that rival actually disagreed. Against a very predictable player the two agree almost always, and thousands of frames can carry a dozen frames of real evidence. That gets reported honestly rather than as a large green number.
Punch a hole in the arena wall and slip into the outer corridor and time itself takes sides: the corridor worm runs at 25% of original speed while the arena worm runs at 4× — with the reaction tax priced symmetrically, because a body moving 4× with a normal-rate mind rams trails exactly like a human would (the CPU re-decides at its normal wall-clock rate while accelerated, wall reflexes on, trails unseen). Bolts fly at full speed regardless: light does not slow down. The screen drops into a hyperspace field — streaks tearing outward from your worm, chromatic warp rings, a focus vignette — and every rule is symmetric: if the CPU takes the corridor, the same physics serve you.
The corridor is two lanes wide — turning and overtaking out there are real maneuvers. The laser ricochets off the arena walls, kills what it visibly touches (the beam exists across the frame it fires into: anything that steps onto the line during the discharge frame is hit — solid beam means hot, the fading afterimage means spent, a low clank means you truly whiffed), and its fifth wall strike punches a hole. The bomb is a fifteen-second decoy: indistinguishable from food until its final two seconds, when it flashes — softly, then hard — and detonates, punching walls. You dodge it by memory: I planted that. The tri-shot is napalm: three full-length bolts at double speed — a fired bolt cannot be outrun — and anything they touch catches fire on contact, dropping burning ground where they stop; a worm aflame burns down on a wall-clock schedule, up to five segments in the first second, three in the next, one in the last, shedding embers as it shrinks; burned past the head is dead. One law covers all three: you are immune to your own discharged weapon — your laser, your blast, your fire. The counterplay to everything else is attention.
World rules are versioned (v1–v11) and every recorded ghost replays under the exact physics it was played on.
Worm is useful at three altitudes: as a finished game, as a transparent adaptive-system study, and as a test harness for claims that are usually hand-waved.
- Meet a rival that remembers your tells — play several rounds in the terminal or browser; the opponent persists its per-player brain, notices genuine turn habits and rhythm changes, and can explain which evidence it earned.
- Try to prove the learning claim false — run the persona suite: a
fixed-direction player is the positive control, a coin-flip player
the null, a corner-left player the acceptance case. A harness that
cannot say no proves nothing.
cargo test --test persona_learning -- --nocapture - Study an honest adaptive-difficulty loop — trace how forecast evidence is separated from board knowledge, how aggression spends only round-boundary snapshots, and how schema changes preserve a returning player's learned history.
Terminal
cargo run --releaseWASD or arrow keys to steer · Space fires a held power-up · P pause ·
Q quit. The brain persists to $XDG_DATA_HOME/worm/worm_brain.bin (override
with WORM_BRAIN).
Browser
wasm-pack build --target web --out-dir web/pkg --features wasm
python3 scripts/serve.py 8080 # local dev; no-store headers, so rebuilds never serve staleThe browser build keeps a per-player brain in IndexedDB, keyed by an identity stored beside it in the same database — deliberately, so the two share an eviction fate. See ADR-005 for why that mattered.
From your keypress to an earned intercept: on each game update, the world identifies whether you had a real choice, captures your local situation, and scores the forecast already committed for this frame. The brain updates its predictors and evidence ledgers, then the decision layers choose a legal move without dropping the survival floor. At round boundaries, the earned read is snapshotted and the sectioned brain persisted. The browser exposes the same decisions and evidence through versioned state — the canvas and the terminal cannot tell two different stories.
For the deep map — bounded contexts, invariants, and the academic lineage of every mechanism — see docs/architecture.md.
scripts/eval.sh # THE GAUNTLET — run before every merge
cargo test # unit + integration
cargo test --test persona_learning -- --nocapture # the claim, ~45s
cargo run --release --example engagement_probe -- browser # engagement + death census
cargo run --release --example intent_probe -- 24 1500 20260805 # errand-model reads
node scripts/page_probe.mjs # the SERVED page, headless (needs playwright)
cargo run --release --example ghost_eval -- rounds.json # read of the REAL player (EXPORT MY ROUNDS)
cargo bench --bench cpu_ai_bench # CPU benchmarksThe gauntlet is the improvement flywheel
(ADR-013): champion = main,
candidates prove themselves against the fixed seeds, and the measured
numbers go in the commit message — the receipts are the ledger.
Layout: src/lib/game.rs (rules, board, power-ups), src/lib/cpu_ai.rs
(opponent model, decision layers, brain persistence), src/lib/web_state.rs
(browser wire format), src/main.rs (terminal client), web/ (canvas
client), tests/ (the falsification suites — the claim lives here),
examples/ (the eval probes), scripts/ (gauntlet, dev server, page probe),
benches/ (CPU benchmarks).
The brain is serialized in a sectioned format so that a schema change costs only the section it invalidates. What the CPU has learned about you — habit priors, head-to-head record — is encoding-independent and survives every future change to the feature space. That is a hard requirement, not an optimisation: a game whose premise is "it remembers you" cannot afford a release that resets everyone.
Architecture decisions live in docs/adrs/ and are treated as
living documents — if one disagrees with the code, the ADR is the bug.
- ADR-001 — opponent-centric CPU
- ADR-002 — responsive browser arena
- ADR-003 — truthful CPU telemetry
- ADR-004 — fixing the CPU's fundamentals
- ADR-005 — durable identity and brain integrity
- ADR-006 — measuring against the player's own base rate
- ADR-007 — difficulty earned by reading you, not by the clock
- ADR-008 — the bomb becomes a mine disguised as food
- ADR-009 — learning must convert into winning
- ADR-010 — what the CPU learns from, and what it may know
- ADR-011 — intent inference and an engaged CPU
- ADR-012 — errand twins, kinematic traps, and honest silence
- ADR-013 — the improvement flywheel
- ADR-014 — the codex corrections: what survived measurement
- ADR-015 — nightly Darwin: continual improvement with human promotion
- ADR-016 — ghost replay: the real human becomes the benchmark
- ADR-017 — automatic round collection: every visitor feeds the flywheel
- ADR-018 — the beatable opening: wits are earned, not given
- ADR-019 — the CPU's notebook: LLM depth, on request only
- ADR-020 — the turn book: reading the frames that decide games
- ADR-021 — the learning program: nine measured surfaces
- ADR-022 — the world-rules program: corridor, decoy, napalm, input queue, reach
- ADR-023 — laser simultaneity: the beam exists across its frame
Win-rate and read-rate figures are quoted from seeded runs, and the seeds are in the harnesses. Where a change turned out to be measurement-neutral it is recorded as measurement-neutral rather than credited with a delta — several fixes in ADR-004 are exactly that, kept because a test proves them rather than because a number moved. Where a claim has been withdrawn, the withdrawal sits in the ADR next to the original claim.
Numbers measured against anyone other than the author are a separate class. Since the v9 build every round played in the browser records a ghost log — the complete input streams of both worms plus the round seed (ADR-016) — captured locally in the player's own browser and exported only by their hand (EXPORT MY ROUNDS). No such exported number appears in this repo yet; when one does it will be quoted with the export's date and round count, and marked as measured against a real player rather than a scripted persona. (The owner's own sessions above are the author's data, collected on his own builds — a different class again, quoted as such.)


