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>
104 lines
4.1 KiB
Markdown
104 lines
4.1 KiB
Markdown
# 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:
|
||
|
||
1. **`JITTER_STRENGTH`** — the life of the world.
|
||
2. **`SQUEEZE_RATE` / `BARRIER_FORCE`** — the wrestle. The whole game's feel in
|
||
two numbers.
|
||
3. **`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.
|