A nanoscale arcade game. Shoot charge across the room, wrestle atoms together until they snap, and contain a chain reaction before it eats the field. Vanilla JS + Canvas 2D, no dependencies, no build step. Core systems: - Charge: give/take as the only two verbs. Opposites attract, likes repel. - Laserhands: ranged proton/vacuum bolts with real travel time. Bolts are charged bodies, so they curve through the field — bank shots are real. Give spends from an 8-proton ring, take earns them back. - The wrestle: bonds can't form on their own. Every pair has an activation barrier that parks them at arm's length; Molly's hands are the only way over it. Hold, squeeze, release -> SNAP. - Radicals: the enemy, with no mind. Every 0.32s one grabs its nearest neighbour, completes itself, and hands the wound on. Can't be shot dead — terminated by wrestling two together (barrierless, so 0.38s vs 1.56s). Two that drift into contact self-quench, which gives outbreaks a natural equilibrium instead of unbounded growth. - Overload: keep feeding one atom and it goes critical. The blast tips its neighbours, so density decides whether you get one pop (34 atoms) or 84 detonations from a single seed (116 atoms). - CASCADE: eight arenas of escalating density with finite ignition waves. Clear the field to advance; sustained neglect melts it down. - Membrane, sour/soapy zones, and the seven wordless tutorial rooms. Non-obvious constraints, documented at each constant: - BARRIER_FORCE must exceed MAX_CHARGE_FORCE, or atoms bond themselves and the wrestle (the whole game) gets skipped. - Bond lock distance scales with atom radius. A flat value meant two carbons could never bond at all, since collision held them further apart than the lock — invisible because hydrogen worked fine. - RAD_SPLIT_R must exceed the field's natural packing distance (set by the barrier), or outbreaks can only crawl and never bloom. Testing: 39 headless sim tests run the real physics in Node — no browser, no canvas. The sim draws all randomness from a seeded RNG so runs are reproducible. Perf is 0.32ms/sim-step at 116 atoms. Dev server strips a /v<timestamp>/ path prefix that index.html adds to its entry import, because browsers cache the *parsed* ES module by URL and no-cache headers alone don't touch it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> |
||
|---|---|---|
| .claude | ||
| src | ||
| test | ||
| .gitignore | ||
| BUILD_PLAN.md | ||
| index.html | ||
| NEXT_LEVEL.md | ||
| README.md | ||
| serve.py | ||
MOLLY COOL
You are fourteen angstroms tall with a proton in one hand and a vacuum in the other. Shoot charge across the room, wrestle atoms together until they snap, and try to put out a chain reaction before it eats the field — and you.
Play: python3 serve.py → http://localhost:5178
No dependencies. No build step. Vanilla JS + Canvas 2D.
Controls
| WASD | move |
| mouse | aim |
| left click | proton bolt — pushes charge in (+) |
| right click | vacuum bolt — rips charge out (−) |
| space (hold) | grab two things and squeeze them together |
| Q | heat — area shove, breaks bonds |
| R | retry |
| shift+A | jump straight to CASCADE |
Aim at your own feet to charge yourself — that's how you go neutral and slip through a membrane.
The rules, none of which the game will tell you
- Opposites attract, likes repel. Bolts are charged too, so they curve through the field. Bank shots are real.
- Give spends, take earns. Eight protons orbit your hand. Fire them all and you're dry — the left hand always works, for free.
- Bonds can't form on their own. Every pair has an activation barrier that parks them at arm's length. Your hands are the only way over it.
- The membrane blocks anything charged and ignores anything neutral. It has no guard and no keyhole. You get through by becoming the right kind of thing.
- Sour air keeps shoving protons onto everything. Including you.
- Keep feeding one atom and it goes critical. A fuse, then a blast that charges everything nearby — which can tip those over. In a dense field one seed can take the whole screen.
- Radicals are the only pure white thing in the game. They have no mind: every 0.32s a radical grabs its nearest neighbour, completes itself, and hands the wound on. You can't shoot one dead. You terminate it by wrestling two together — radical pairs recombine barrierlessly, so it's fast.
- Two radicals that drift into each other quench on their own. Herding is a legitimate tactic.
Structure
Seven wordless tutorial rooms (the cold open), then CASCADE — eight arenas of escalating density. Each arena spends a fixed number of ignition waves; put the fire out and it clears. Let it burn and the field melts down.
Density is the difficulty dial. Everything else follows from it.
Layout
src/config.js ALL feel constants. Tune the game here.
src/physics.js storm, charge, the activation barrier, zones, membrane
src/bonds.js the wrestle — grab, squeeze, SNAP
src/bolts.js laserhands
src/radicals.js the enemy
src/overload.js fuses, detonations, the cascade
src/arena.js CASCADE — waves, meltdown, clear
src/elements.js twelve elements: colour, valence, radius, mass
src/juice.js hitstop/trauma budget (a cascade must never freeze the screen)
test/*.test.mjs headless sim tests — `node test/sim.test.mjs`
Tests
node test/sim.test.mjs core physics, radicals, molly, regression
node test/arena.test.mjs arena waves, win/lose, difficulty ramp
node test/overload.test.mjs fuses, cascade, juice budget, perf
The sim is seedable (seedRandom), so runs are reproducible. Tests drive the
real physics headlessly in Node — no browser, no canvas.
Tuning
The three knobs that matter, in order:
JITTER_STRENGTH— the life of the world.SQUEEZE_RATE/BARRIER_FORCE— the wrestle. The whole game's feel in two numbers.SNAP_TRAUMA/HITSTOP— the payoff.
Two constants have non-obvious constraints, documented where they live:
BARRIER_FORCE must stay above MAX_CHARGE_FORCE (or atoms bond themselves
and the wrestle is skipped), and RAD_SPLIT_R must stay above the field's
natural packing distance (or outbreaks never bloom).
Dev note
serve.py strips a /v<timestamp>/ prefix off request paths, and index.html
imports the entry under a fresh one each load. Browsers cache the parsed ES
module by URL, so without this you can edit a file, hard-reload, and still be
running yesterday's code.