mollycool/README.md
m3ultra 77615b23bd Saturation is the firebreak: make bonding matter
The arena ran ten seconds at 116 atoms and formed exactly ZERO bonds.
Valence, the twelve elements, the open sockets and the bond wrestle —
the best idea in the codebase — were all decorative, because building
something had no effect on anything.

A radical now abstracts from an OPEN VALENCE SLOT, so an atom with every
slot spoken for is a wall the wound cannot cross. Bonded H2 is the
cheapest firebreak in the game; a lone carbon with four open sockets is
the best kindling. Three things had to follow:

- The wall must be breakable or one good build wins forever. A blast
  snaps a bond and both halves come away radical; Q breaks every bond in
  radius with 40% going homolytic, so it is finally the igniter the
  design doc describes rather than a free crowbar.
- Walling a radical in kills it in 3.8s and credits you. That is the
  payoff for building, and the only way to kill the LAST radical, since
  clearing needs zero live and terminating needs a pair.
- Two starve timers, far apart. Starving anything with no prey deleted
  the meltdown outright: at 34 atoms over 1280x720 the mean spacing is
  ~130px against a 70px reach, so nearly every radical is momentarily
  alone. Boxed-in is 3.8s, merely adrift is 11s.

Legibility. Radicals wore element colour — the one enemy arrived in
twelve of them and at 30 on screen you could not tell threat from fuel.
They now draw as a dark hole with a white rim. Molly drew BEFORE the
radicals and was buried under thirty additive white glows; she draws
last, over the fire, with a hard dark ring that more white cannot wash
out. Bonded groups render as one silhouette, teal when sealed.

The field grid. The flat lattice is now displaced by the Coulomb field
the sim already computes. 0.41ms/frame at worst case.

The score is the player's. CHAIN counted radical hops, so the one big
number on screen went up only while you were losing. It counts
terminations now, with a streak that stacks and lapses.

Neon was defined but never spawned. Now that saturation stops fire, a
zero-slot atom is a free firebreak — drawn from the spice table the
densest arena rolled 3 to 9 of them, and at 9 it stopped melting down at
all. Fixed per-arena counts; all 8 seeds now melt in 17-32s.

Also: prod-safe module loading (the absolute /v<ts>/ prefix fell through
nginx to the lander's index.html and was served as text/html, a silent
dead title screen), an opt-in F frame-time readout, a deploy script, and
removal of a full O(n^2) scan that ran every frame to angle some 3px
rings.

29 new tests in test/firebreak.test.mjs; 68 passing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-07-25 23:13:08 +10:00

121 lines
5.3 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 every bond in radius (40% go homolytic) |
| **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.
- **Saturation is the firebreak.** A radical takes an atom by grabbing an
*open valence slot*. Close every slot on something and the fire can't cross
it. A bonded pair of hydrogens is the cheapest wall in the game; a lone
carbon has four open sockets and is the best kindling. Wall a radical in
with nothing left to take and it goes out on its own in a few seconds —
which is the only way to kill the last one, since terminating needs a pair.
- **Nothing you build is permanent.** A detonation snaps a bond and *both*
halves come away radical. **Q breaks every bond it touches** and 40% of
those splits go homolytic — two new radicals. The button that unsticks a
jam is the button that opens a hole in your own wall.
- **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.
The one number on screen is **terminations**, and back-to-back kills stack a
streak that lapses after three quiet seconds. No label, no praise, no record.
## 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
node test/firebreak.test.mjs saturation, homolysis, the score, arena both-ends
```
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.
Some 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); `RAD_SPLIT_R` must stay above the field's natural
packing distance (or outbreaks never bloom); and `RAD_STARVE_TIME` /
`RAD_DRIFT_TIME` must stay far apart — a radical walled in by matter you built
should die fast, but one merely adrift in a sparse early arena must not, or
the outbreak evaporates unattended and the meltdown lose state disappears.
## 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.