"It needs more detail" was not a pixel problem. A molecule was a set of
atoms at whatever angles the storm left them — three circles near each
other, never a thing — and the room was a vacuum with objects in it.
VSEPR. Bond springs only ever fixed bond LENGTH. Angular springs now splay
the bonds around each atom as far apart as they can go, which is what
produces the famous silhouettes: water bent at 104.5, CO2 a straight bar,
methane a caltrop, a closed carbon ring a hexagon. Two bonds is where lone
pairs matter, so O/N/S carry an explicit `bend`; three or more is just even
angular spacing, which lands trigonal at 120 and the caltrop at 90 for free.
The springs are deliberately weak (ANGLE_SPRING is ~15% of BOND_SPRING) so
geometry loses to the storm, to a blast and to Molly's hands. A rigid
molecule that ignores being hit reads as fake instantly.
They also needed DAMPING, which was the real bug. Undamped, the mean angle
measured correct while water actually swung through a 61 degree range —
a broken hinge, not a molecule. Damping the tangential relative velocity
(as bondSprings already does radially) cuts the spread from sd 16.5 to 1.3.
Two measurement traps worth recording, since both nearly sent me the wrong
way: a single-frame angle reading is meaningless in a Brownian storm and
showed water at 58 degrees when its mean was 105; and starting a two-arm
molecule at exactly 180 degrees is a degenerate equilibrium where the cross
product vanishes and it cannot pick a side to fold toward, which looked
exactly like a systematic bias.
THE SOLVENT. 600 motes advected by the Coulomb field the sim already
computes. Deliberately NOT particles: no charge, no valence, no collisions,
nothing can touch them, so 600 cost 0.16ms where 116 real atoms cost ~5ms.
They carry what the atoms cannot — you see the field move before anything
charged does. Drawn under everything and never additive, because 600
additive motes would stack into a wash that eats the element hues and the
radicals' white.
13 new tests in test/geometry.test.mjs; 81 passing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The purge silently did nothing. `sed -n 's/.*"id":"\([a-f0-9]\{32\}\)".*/\1/p'`
looks right, but the zones response carries several 32-hex "id" fields
(account, plan, ...) and the leading `.*` is greedy, so it returned the
LAST one — a valid-looking id for the wrong object. The API then rejected
the purge and the old grep-for-success check reported a bare "purge
failed" with no reason.
Parse with python3 (already a dep — serve.py) and surface the API's own
error message when it fails.
Verified end to end against the live zone: cf-cache-status HIT -> purge
-> MISS -> HIT.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Cloudflare fronts monsterrobot.games and serves the modules with
max-age=14400, so a redeploy left you running four-hour-old code — the
same staleness the dev server's /v<timestamp>/ prefix exists to prevent,
just moved upstream. deploy.sh now purges the deployed paths, reading
CF_API_TOKEN from deploy/.env (gitignored) at run time, and warns loudly
rather than failing when it isn't set.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
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>
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>