mollycool/README.md
type-two 6263672134 Molly Cool: arcade science-action vertical slice
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>
2026-07-19 15:36:54 +10:00

104 lines
4.1 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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.