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>
24 KiB
MOLLY COOL — NEXT LEVEL
1. The diagnosis
What you built is genuinely good and it is genuinely quiet. The charge system, the bond wrestle, the membrane and the zones all work and all feel great — the problem is that nothing in the world ever pushes back, nothing on screen has a name, and Molly has to physically swim over and hug everything she wants to touch. Three colours, eighteen objects, no opposition, no range: that's not an art problem or a taste problem, it's four missing systems, and the previous read of "make it more austere" pointed the fix in exactly the wrong direction. The abstraction also quietly ate the chemistry — you can't tell you're doing science any more because charge became the only noun, and one scalar can't be spectacular. Give it teeth, give it hands that reach across the room, give it a periodic table for a palette, and the exact same physics becomes an arcade game.
2. The direction — MOLLY COOL: CRITICAL
You are fourteen angstroms tall with a proton in one hand and a vacuum in the other, and you have just worked out that one hydroxyl radical dropped into a dense enough field of matter will eat an entire room in eleven seconds. You don't swim over and hug atoms any more — you shoot. Right hand fires a hot-pink proton bolt that visibly curves through the charge field; left hand fires a cyan vacuum bolt that rips a proton out and drags it screaming back into your palm. But the bolts aren't the weapon. The weapon is ignition: overload something past +3 and it goes white, screams up a sawtooth, and detonates — and everything it splashes goes radical. A radical has one unpaired electron and no mind at all. Every 0.32 seconds it grabs the nearest atom, completes itself, and hands the wound to its victim. One becomes two becomes forty. And the only way to stop it is the mechanic you already built: radical–radical recombination is barrierless, so you cross the streams, tether one radical in each fist, and crush them together in 0.4 seconds while a third one closes on your back. Light the fuse, ride it as long as you dare, and terminate it half a second before it terminates you. Geometry Wars if the grid were made of methane. Everything you already love gets promoted, not replaced — the wrestle stops being a puzzle payoff and becomes the kill move.
3. LASERHANDS
The insight: give and take are already a perfect asymmetric dual-wield. They've just been trapped at melee range. Tap = bolt. Hold = beam. That's it — the verb count stays at two.
TAP RMB — PROTON BOLT. Hot pink, r4, 940 px/s (travel time, never hitscan — travel is what makes it read as pyew), 6-frame tracer, 0.16s cooldown. On hit: target +1 charge, 45ms hitstop, 0.15 trauma, 8 sparks along the impact normal. Molly recoils 40 px/s backward, the hand sprite kicks 6px along −aim for 80ms, the camera kicks 4px opposite the shot. The bolt is itself a charged body, so run it through the existing Coulomb solver at 0.3× K_CHARGE — it curves around charged atoms. Bank shots are a skill. Costs one pip.
TAP LMB — VACUUM BOLT. Cyan, same speed, inverted trail. Rips one proton out (−1) and the stolen proton flies back at 1300 px/s and joins your ring. Pulls Molly 30 px/s toward the target. Give spends, take earns — you physically cannot spam one hand.
THE AMMO RING. 8 hot-pink pips orbiting the right hand at r18, 1.2 rad/s, spinning faster as it empties (nervous). Empty = dry click and an impotent puff. Zero HUD, zero text. The left hand always works, always, for free — you can never be locked out.
HOLD EITHER >110ms — TETHER. Beam to 280px, latches the first body within 22px of the line, springs it (k=380, damp=22) to a point 90px ahead of the hand. Right beam is a jagged 5-segment pink lightning polyline re-jittered ±6px every frame. Left beam is a smooth cyan hose with beads travelling backward into Molly. Tethered bodies carry real momentum — whip the mouse and you sling them.
TAP THE OTHER HAND WHILE TETHERED — PUNT. 1400 px/s along aim, 110ms hitstop, 0.4 trauma, bass thunk, 120 px/s recoil on Molly. The punted body is HOT for 0.6s: on impact it dumps its own charge sign into whatever it hits and knocks a proton loose. A +3 oxygen punted into a crowd is a charge grenade. Every object on screen is now ammo. (This is the gravity-gun lesson: grabbing is a chore, throwing is a weapon.)
BOTH HANDS TETHERED, PULL TOGETHER — CROSS THE STREAMS. The bond wrestle, unchanged, now fed by beam tension instead of body proximity. barrierHump() runs exactly as written. Beams whiten and physically shorten, dragging Molly into the middle of her own snap so the tactility survives. Both hands committed = you cannot shoot = you are naked. 0.85s ranged / 0.32s melee (the hug stays, and stays fastest — safe-and-showy vs fast-and-dangerous) / 0.40s for radical–radical / 0.28s inside a catalyst field. That radical-pair number is load-bearing: 1.8s of squeezing is a wonderful puzzle payoff and a fatal combat move. Ship it in the same commit as the radical or you'll conclude the whole direction failed on a tuning bug.
SNAP CANCEL WINDOW. For 0.28s after a snap, bolts are free and do +2, and beams re-latch with no hold delay. Hands flare white for exactly that window. Snap → free +2 bolt → target hits +3 → unstable → detonation. That's a learnable, executable string, and it's ten lines.
HITSTOP LADDER, written before any of this ships: bolt 45ms, snap 130ms, detonation 160ms, hard-capped at 200ms accumulated per 500ms window. Trauma 0.15 / 0.45 / 0.7, clamped to 1.0. Without the cap a cascade feels like the game crashed.
The bolt never deals damage. Its payload is CHARGE. The moment a bolt does HP, the chemistry becomes set dressing and the simulation stops being the weapon. Also: bolts and beams are charged, so the membrane blocks them — shots splash on it in an oily rainbow bloom. But a punted neutral object sails straight through. The heist room is now a trick shot from across the room.
4. The threat
THE RADICAL (•) — one enemy, one boolean, no bestiary. A particle with a permanently unpaired slot: the only pure-white thing in the game, jagged corona, 3× jitter, hard motion trail. No AI, no pathing, no plan. Every 0.32s on a deterministic tick (never per-frame random — the Brownian storm already supplies chaos and stacking randomness on top reads as unfair rather than dangerous) it finds the nearest bondable neighbour within 70px, telegraphs with a 0.2s white flicker + a thin red hunt-thread, then takes it. It completes itself and goes dim; the victim becomes the radical. Population is conserved per hop — but the wound moves, and in a dense pocket it takes two at once and the branch factor crosses 1.0. Hard cap 120. You cannot shoot it dead. You terminate it: two radicals wrestled together annihilate in a white flash 3× a normal snap, with a quench ring that puts out everything it touches.
OVERCHARGE DETONATION. Charge clamp widens to −3..+3. Push a body past +3 and it goes white-hot, spikes double, a sawtooth rises over 0.9s, then it detonates: 130px radial pulse, +1 to everything caught, 160ms hitstop, a white ring that physically scatters particles. Anything already at +3 in the blast goes unstable with a shortened 0.45s fuse. Chains cascade. The biggest payoff in the game is expressed entirely in the currency the game already had.
YOUR OWN TOOLS ARE THE INITIATORS. Q stops being a crowbar and becomes the igniter: 40% of bonds it breaks split homolytically into two radicals instead of separating cleanly (bonds about to go flicker white for 0.2s, so you can always read the risk before you commit). There is no free action in this game. That single change costs six lines and does more for "rad" than any art pass.
MEMBRANE BURN. Rebuild the membrane as ~40 lipid segments. A radical touching one unzips it in both directions at 110 px/s, segments blackening and curling. Burnt segments stop blocking charge. Run one out and the room's rules dissolve. This is real lipid peroxidation, and it means the best object in the game can finally be threatened.
MOLLY'S HEALTH IS THREE ELECTRON SLOTS, drawn as three dots orbiting her core. No bar, no number. Radical contact steals one (0.7s i-frames, detuned hit, shake). At zero she becomes a radical herself: white, spiky, and contagious for 4 seconds — everything she touches propagates, including what she was protecting. She can still act, and she can still terminate by ramming. Most dangerous and most powerful state in the game. If it expires she denatures: strands unfold over 0.6s, hard cut, playable again in under 400ms. No menu, no message, no "try again."
THE OXIDATIVE FRONT — the late-arena pressure. A slow crimson shimmer sweeping the room, 12%/sec radical conversion for anything inside. Unfightable. You survive it positionally, hopping the dim olive tocopherol pools that quench radicals on contact.
No boss in v1. The cascade is the boss.
5. Cool shapes and colours
One law, never violated: ELEMENT OWNS THE FILL. CHARGE OWNS THE EDGE AND THE LIGHT. #FFFFFF IS RESERVED FOR RADICALS AND NOTHING ELSE. That third rule is what lets 120 sparks on screen stay readable.
drawBody() currently fills #0b1118 and strokes with chargeColor(). Swap which channel gets which — six lines, one atomic commit, and it frees hue for element identity while charge keeps the two shape channels it already half-uses: spiky hot corona (+), smooth pulsing halo (−), flat hairline (neutral). Charge then survives greyscale and deuteranopia, which the current colour-only coding does not.
Palette from real emission spectra, not CPK (CPK's black carbon and white hydrogen both die on a dark ground, and white is spoken for):
H #EAF4FF |
C #9B8CFF |
N #4D7BFF |
O #2BE86B |
Na #FF9500 |
Cl #D8FF3B |
S #FFE23D |
Mg #D9DEE6 |
Fe #9FB4C4 |
Cu #23D9C0 |
K #C77DFF |
Ne #FF5E3A |
Fills are flat, matte, ~35% luminance, never bloom. Charge is #FF2D9B positive / #2DB8FF negative and it is the only thing that goes additive. Ground is not black — #0B0716 deep ink-indigo with a #1A1030 graph lattice at 32px. Near-black reads as a void; ink-indigo reads as a medium.
Free slots become OPEN SOCKETS — small rings with dark centres, pulsing at 2Hz, leaning toward the nearest compatible partner within 120px. Carbon with four open sockets reads as visibly hungry. Completed methane reads as satisfied. That's valence made legible in peripheral vision at speed.
Molecules render as ONE object — flood-fill the bond adjacency each frame (it's cheap), stroke a smoothed hull 6px out with a shared aura. Water is a thing, not three circles near each other. On completion the atoms snap to real VSEPR geometry over 120ms with a hard ease-out (weak angular springs at 15% of BOND_SPRING hold them there), the full silhouette flashes solid white for 4 frames, and it rings like a struck FM bell — bigger molecule, lower note. Water visibly bent at 104.5°. CO₂ a straight bar. Methane a caltrop. Benzene a hexagon with a counter-rotating delocalised inner ring — the most beautiful object in the game and the most recognisable silhouette in science.
The background stops being empty: the FIELD GRID. A half-res lattice of short segments displaced by the summed Coulomb field — which you already compute. Positives bulge it, negatives dent it, a snap sends a visible ring across it. Real field lines, near-free, and it turns the void into a charged medium. Highest visual-impact-per-line item on this whole page.
Post chain (post.js, all drawImage): bloom (glow layer only, offscreen at ¼ res, two downscale/upscale passes, composited lighter) → event-driven chromatic split decaying over 120ms → 256px baked grain tile at overlay 0.06 → vignette. Impact frames on every snap/bolt/detonation: affected objects drawn solid #FFFFFF for exactly 2 frames, then one inverted frame, then hitstop. Cheapest rad in existence. Cap inversions to one per 500ms and ship a reduce-flashing toggle day one.
Static world printed, chemistry lit: membrane, grid and walls get a 1px riso misregistration (drawn twice, 1px offset, #FF3B6B and #2BE86B at 25% alpha) and never bloom. Charged matter blooms and never misregisters. Nobody else's game looks like this, and it dodges the synthwave-neon default entirely.
PERMANENCE — the single cheapest fix for austerity. Scorch marks stay. Soot stays. Spent proton husks fall and linger 10s. Broken bond fragments drift. The room accumulates evidence of what you did to it instead of resetting to tasteful emptiness.
Sound is 60% of perceived tone. 130 BPM acid techno, live-synthesised: 909 kick, 16th hats, a TB-303 saw through a resonant lowpass whose cutoff tracks total radical count on screen, dub pad. Stems gate on activity. Every player impact quantises to the nearest 16th; the world stays deliberately off-grid — that contrast is what makes you feel like the coolest thing in the room. The wrestle rumble is a rising resonant sweep that resolves on the snap. Ban elastic/bounce easing globally. Ban five-point-star particles — hard shards and rings only. No fanfare, no praise, no confetti. A successful bond gets a bass thunk and silence.
6. The science, made visible again
Twelve elements. Each is a fill colour, a valence count, a radius, and a verb.
- H
#EAF4FF· 1 slot · r7 · lightest, jitters fastest. The tinder — radicals eat it first, so H-dense fields are where cascades run away. - C
#9B8CFF· 4 slots · r13. The hub. Four open sockets. Chains, rings, benzene. The builder. - N
#4D7BFF· 3 slots · r11. N₂'s triple bond is the toughest wrestle in the game; cracking one releases a huge shock. - O
#2BE86B· 2 slots · r11. The oxidiser. O₂ thrown into a fire makes every fire on screen twice as big. It is not the extinguisher. It will kill you once and you will never forget it. - Na
#FF9500· 1 slot · r14. Punt sodium into a water pool → water tears apart into hydrogen → the heat lights the hydrogen. A two-stage explosion you set up from across the room. Best spectacle in the build; worth hand-authoring as a special case. - Cl
#D8FF3B· 1 slot · r13. Electron thief — drags charge off bonded neighbours. Gas-wisp trail. - S
#FFE23D· 2 slots · r13. Burns slow, leaves a lingering acid haze that keeps shoving things positive. - Mg
#D9DEE6· 2 slots · r13. Flashbang — burns blinding white and stuns radicals. And magnesium keeps burning inside CO₂, so the extinguisher you just deployed is the fuel. - Fe
#9FB4C4· 3 slots · r15. Heaviest, magnetic, best punt ammo — a punted iron is a wrecking ball. - Cu
#23D9C0· 2 slots · r14. Conducts — passes charge to everything it's bonded to. Build a copper chain and one bolt overcharges six things. - K
#C77DFF· 1 slot · r15. Potassium. Sodium's worse cousin. Do not take it near water unless you mean it. - Ne
#FF5E3A· 0 slots · r9. Noble. Refuses to bond no matter how hard you wrestle. Permanent hard neon glow nothing else has. It's a pinball, it's a punt-shield, and Molly stripped neutral looks like this — inert stops meaning "dim grey" and starts meaning "Vegas sign."
Build-to-use, never build-to-score. A molecule is a tool with its real property: H₂O douses burning membrane and quenches radicals · CO₂ sinks and smothers fire but does nothing to radicals · O₂ doubles every fire on screen · NH₃ neutralises a sour zone · C₆H₆ benzene is rigid enough to survive a slam — a shield and a platform · C₆₀ buckyball is a cage: shove a radical inside and it's gone forever, and now you're carrying a bomb. Learning chemistry pays out as power, never as a certificate. That's the whole anti-edutainment test, passed.
Unlocks are molecules, not menus. Build NaCl and sodium lights up on the table. Close a benzene ring and carbon unlocks. Finish an arena having formed zero bonds and you get Neon. Nothing is ever explained; the tile just lights up.
The accuracy licence, written down now so nobody re-derails us: directionally real, gameplay-tuned, fictional entries permitted where the game needs a verb. Real colours, real geometry, real names, knowingly fake energetics. When the fun number and the accurate number disagree, ship the fun number and do not litigate it.
7. Structure
The seven scripted rooms ship completely unchanged as the cold open. Six or seven minutes, contemplative, wordless, no radicals, no arcade layer. They're the tutorial and they're the contrast that makes everything after them land — escalation only reads as escalation if there's a floor. Gate the entire arcade layer behind them; a stray cascade in the heist room would soft-lock the lesson.
The scale reveal becomes the tonal cut. Camera pulls back, the acid track drops in for the first time, and the first radical is born from a visible heat flicker.
Then CASCADE. Eight arenas of 90–120s, then endless. Every arena runs the same curve, communicated entirely in bass pitch and background tint, no numbers: 0–8s the room is clean, one radical is born, you learn its rhythm and terminate it. 8–33s fuel density rises, branch factor climbs past 1.0, you're terminating pairs under pressure. 33–40s you get the last one and the room goes silent — a deliberate false calm, and it's what makes the next part land. 40–90s a heat spike initiates five at once inside a hydrogen field and the oxidative front starts moving.
Progression is the periodic table, one element per arena, each adding a coloured pip to a ring around Molly. Arena 1–2: H, O. Arena 3: C and organic chemistry blows the fuel densities open. 4: N. 5: Na + Cl — salt punted into water dissociates into free Na⁺ and Cl⁻ with visible solvation shells, and that's your ammo economy. 6: S, Mg. 7–8: the metals.
Score is chain length, shown as orbiting pips up to 8 and then a single hard-condensed numeral. This is the one deliberate break of the zero-text rule and you should commit to it rather than fudge it — an arcade game with a hidden score is an art piece, which is the exact accusation you're making about the current build. Banking a chain of 8+ refunds a quench charge. One persistent number at the end of a run: best chain. No label, no comparison, no "NEW RECORD."
Run length 12–15 minutes. Death to playable in under 400ms. The one-more-go hook isn't the score — it's that you lost to a fire you started, and you're now certain you know where to put the seed next time. That's the KSP trick: losing is louder, funnier and more shareable than winning.
Plus SANDBOX — open arena, element palette unlocked by arena progress, no goals, reset key. Near-free once the sim exists, and it's where the clips come from.
8. What we keep, what we cut
KEEP, UNTOUCHED. The Brownian jitter storm (JITTER_STRENGTH 90) — it's the life of the world and it's also what makes grabbing a radical genuinely hard. The Coulomb solver (K_CHARGE 1.2e7). barrierHump() and BARRIER_FORCE 3400 — the single best idea in the codebase; every new system feeds it rather than replacing it. The whole snap payoff path: SNAP_TRAUMA 0.62, HITSTOP 0.05, SNAP_SHOCKWAVE 620 — reuse it verbatim for detonations at 1.5×. The zones. Trauma camera. WebAudio synthesis. All seven tutorial rooms, verbatim.
KEEP, PROMOTED. Particle.slots / freeSlots / drawSlots() is already a complete, working, enforced valence system with no element names attached — "real molecules" is a naming pass, not a rewrite. grace is already a per-particle i-frame timer. The step() pipeline takes a radicals(world, dt) pass cleanly between zones and membranes. The melee grab survives at GRAB_RANGE 130 as the fast-and-risky option so range never orphans the tactility. The membrane keeps its blocking rule and gains destructibility.
CHANGE — one line each. barrierHump() gets an early return at 0.15× for radical–radical pairs. drawBody() swaps fill and stroke channels. The charge clamp widens from ±1 to ±3.
CUT. The E light scalpel keybind — the bolt is the light scalpel now, at range, with travel time; one less key, one clearer mental model. Q as a crowbar (it's the igniter). Melee-only as the primary verb. The 1–18 particle sparseness. The "nothing can go wrong" state — that is the #1 structural signal of a kids' toy and it's the build's biggest flaw.
DELIBERATELY NOT DOING (yet). No 30-entry reaction table — hand-author the three that matter (Na+H₂O, Mg+CO₂, H₂+O₂) and skip the engine that would take months to tune. No pH axis — element hue and charge shape already own the channels, and a fourth state variable is how the screen becomes soup. No wand/modifier loadout. No roguelite map. No boss. No formula text anywhere in the world. No face on Molly. Ever.
9. The build order
1 — TONIGHT: THE BOLTS. New file src/bolts.js, ~80 lines. Tap RMB → pink projectile at 940 px/s, +1 on hit, 45ms hitstop, 8 sparks, muzzle flash, 6px hand kickback, 40 px/s recoil on Molly. Tap LMB → cyan, −1, proton flies home. Write the hitstop/trauma accumulator cap in this same file before anything else can stack on it. Two hours. Load room 2 (opposites-grab) unchanged and shoot the atoms across the room. It will feel like a completely different game before midnight.
2 — THE COLOUR SWAP. src/elements.js — a twelve-entry data table. Add this.el to Particle, derive slots/radius/mass/colour from it, and do the drawBody() fill/stroke swap. Convert rooms.data.js from {x,y,slots:2} to {x,y,el:'O'}. Check it in a deuteranopia simulator before building anything on top. One evening, and the screenshot goes from three colours to twelve.
3 — TETHER + PUNT. Hold past 110ms to beam out to 280px, spring-latch, tap the other hand to punt at 1400 px/s with the 0.6s HOT flag. Add the ammo ring. Now every object in the room is ammo and Molly is an operator instead of a swimmer.
4 — CROSS THE STREAMS. Feed the existing bond system from beam tension instead of body proximity. Retune: 0.85s ranged, 0.32s melee. Beams whiten and shorten so you're dragged into your own snap. Add the 0.28s snap-cancel window while you're in there — it's ten lines and it's the whole combo system.
5 — THE RADICAL. This is the go/no-go. src/radicals.js, ~40 lines. One flag on Particle, deterministic 0.32s tick, nearest bondable neighbour within 70px, 0.2s white telegraph + red hunt-thread, victim becomes the radical. Hard cap 120. Draw it pure white with a jagged corona. Ship the barrierHump() early return in the same commit so terminating a pair takes 0.40s. Add Molly's three electron slots and the 400ms death. Then spawn 60 particles and one radical and play with it for twenty minutes. If grabbing two flailing sparks out of the storm while a third closes on you is thrilling, everything downstream is proven. If it's fiddly, you found out in one evening.
6 — IGNITION. Charge clamp to ±3, overcharge detonation with the 0.9s sawtooth fuse, chain counter, the cascade colour grade (desaturate everything except radicals and charged bodies, timescale 1.0 → 0.62), one pentatonic step per link. Q's 40% homolytic split. Now the game has its screen-clearing payoff and its worst mistake, in the same system.
7 — DENSITY. Uniform spatial hash (cell = 2× CHARGE_RANGE) on the Coulomb broad-phase, pre-rendered glow sprites blitted with drawImage — never shadowBlur, never per-frame createRadialGradient. Then go to 180 interactive bodies plus 600 non-interactive solvent motes that only get advected by the field. Add the field grid and permanence (scorch, husks, soot). This is where austere finally dies.
8 — THE ARENA. post.js (bloom → chromatic split → grain → vignette, plus impact frames), the 130 BPM acid transport with player impacts quantised to 16ths, molecule detection + VSEPR snap + silhouette flash, build-to-use payloads, membrane burn, the eight arenas, sandbox mode.
Steps 1–5 are one week of evenings and they are already a game. Everything after is escalation.