The philosophy: one file, no build
Why the whole game is a single hand-written file with no framework, no build step, and no client dependencies -- and what that buys you.
Four constraints shape every line of this codebase. They are worth stating up front because most of the "why is it done this way" answers trace back to one of them.
No build step, no framework, no client dependencies
The whole game is one hand-written assets/game.js, styled by assets/game.css, mounted by a 34-line index.html. You edit a file and refresh — that is the entire dev loop. There is no bundler, no transpile, no node_modules on the client. The only npm dependency in the whole project (ioredis) is used solely by the serverless leaderboard function, never shipped to the browser.
The payoff: the source you read is the source that runs. No sourcemaps, no "which of 40 modules is this," no framework lifecycle to reason around. The cost: you write your own rendering and input plumbing — which turns out to be about 40 lines (see The state machine).
Data-driven
Content — roles, tours, events, items, crew, perks, achievements — lives in plain array/object catalogs near the top of the file. The engine reads them generically, so adding content means adding a data entry, not writing control flow.
556 var LOT_ITEMS = [ 557 { id: "cheese", who: "Miracle Mike", name: "Grilled Cheese", desc: "American cheese, American dream.", price: 2, 558 apply: function (s) { s.grub = clamp(s.grub + 6, 0, CAP.grub); return "+6 Grub. Mike nods sagely."; } }, 559 { id: "burrito", who: "Sunflower", name: "Veggie Burrito of Uncertain Origin", desc: "Filling. Mostly.", price: 3, 560 apply: function (s) { 561 if (chance(0.2)) { s.vibes = clamp(s.vibes - 12, 0, CAP.vibes); return "The beans had OTHER ideas. -12 Vibes."; } 562 s.grub = clamp(s.grub + 10, 0, CAP.grub); s.vibes = clamp(s.vibes + 4, 0, CAP.vibes); 563 return "+10 Grub, +4 Vibes. Delicious, actually."; 564 } },
That apply(s) shape repeats across catalogs (events use {line, good}, hustles use run(s), van parts use buff(b)), and it is the reason Data-driven design can promise that a new event or item is almost always just an array entry.
One body of logic, two runtimes
Covered in Anatomy: the same file runs in the browser and headless under Node. This is what makes the game testable without a browser, and it disciplines the whole codebase — anything that isn't pure logic has to guard on HAS_DOM.
Small on the wire
A gzipped-size ratchet (test/sizes.js) fails the build if the payload grows past a budget. It keeps the "just add another dependency" reflex in check and is why the ASCII art, the audio synth, and the whole game still fit in a small download.
- No build/framework/deps on the client — the source is the artifact.
- Content is data with a small mutate-and-report contract; the engine stays generic.
- Two runtimes from one file, and a size ratchet, are load-bearing constraints, not trivia.