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>
42 lines
1.4 KiB
Python
42 lines
1.4 KiB
Python
#!/usr/bin/env python3
|
|
"""Dev server for Molly Cool.
|
|
|
|
Plain http.server caches ES modules hard enough that you edit a file,
|
|
reload, and still run the old code — which costs more debugging time
|
|
than it has any right to. This just turns caching off.
|
|
"""
|
|
import sys
|
|
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
|
|
|
|
|
import re
|
|
|
|
VERSIONED = re.compile(r"^/v\d+/")
|
|
|
|
|
|
class NoCache(SimpleHTTPRequestHandler):
|
|
def translate_path(self, path):
|
|
# /v1234567/src/main.js -> /src/main.js
|
|
#
|
|
# index.html imports the entry under a fresh /v<timestamp>/ prefix each
|
|
# load, so every relative import inside the graph resolves to a URL the
|
|
# browser has never seen. No-cache headers alone don't do it: browsers
|
|
# cache the *parsed* module by URL, so an edited file can keep running
|
|
# its old code until the URL itself changes.
|
|
return super().translate_path(VERSIONED.sub("/", path))
|
|
|
|
def end_headers(self):
|
|
self.send_header("Cache-Control", "no-store, no-cache, must-revalidate, max-age=0")
|
|
self.send_header("Pragma", "no-cache")
|
|
self.send_header("Expires", "0")
|
|
super().end_headers()
|
|
|
|
def log_message(self, fmt, *args):
|
|
pass # quiet
|
|
|
|
|
|
if __name__ == "__main__":
|
|
port = int(sys.argv[1]) if len(sys.argv) > 1 else 5178
|
|
print(f"molly cool -> http://localhost:{port}")
|
|
ThreadingHTTPServer(("", port), NoCache).serve_forever()
|