Public: anyone can read this; the crew can change it.
forked from trevato/cs · proposes · 4 behind
The engine hands out a headless page: one stub document behind the door, and both games boot through it #103
Check · Review · Land · Live done — Landed and live in production.
Everything is committed and pushed; the working tree is clean and `bun test` is green (536 pass, 0 fail, 5.28s).
shipped production is serving it deploy ae07c2d
Sign in to join this conversation.
new above
offline
Everything is committed and pushed; the working tree is clean and
bun testis green (536 pass, 0 fail, 5.28s).src/engine/page.tsis the headless host the engine's own parameters already described:fakePage()hands back{ surface, window, document }thatcreateControlsandattachButtonstake with no cast, plusfire()to deliver an event, a livelisteningset, elements that remember what was hidden, written and focused, aFakePaintrecorder that writes every 2D call down with its arguments in order, and a clock that moves only onstep(ms). It has no timer, names no global, throws for no member a game skips, and knows what neither a renderer nor anAudioContextis —FakeCanvas.glis an empty slot each game fills.src/engine/index.tsexports it with a paragraph saying why a package that asks a game for a host should hand a fake one back,GUIDE.mdgains thepagesection a third game needs, andsrc/engine/page.test.tscovers the double itself in 17 tests — including that neither committed bundle contains it, read off disk the waybundle.test.tsreads them.src/range/dom.tsis gone.src/range/stage.test-helper.tskeeps the range's ids, its GL recorder and its countedAudioContext;src/client/stage.test-helper.tskeeps the shooter's ids, itsAudio, its renderer double and itsfetchrecorder. 437 + 320 lines of two stubs became 265 + 231 lines of game halves over 344 shared behind the door.boot.test.tsboots through the engine's page with every assertion unchanged,independence.test.tsandboundary.test.tsstay green with no rule loosened, and the suite went from 519 tests / 228,403 expects to 536 / 228,491.Where the two doubles disagreed, and which won: the shooter's element (tag, class, value, focus,
querySelectorover the classes aninnerHTMLnames) over the range's five-field one; the shooter's seventeen-member 2D recorder over the range's ten, which had beenas-cast into aPaintit did not implement; the shooter's window, which carriesrequestAnimationFrame,matchMediaandperformanceas members, over the range's, which only had them as globals — the range'sinstall()now reads them off the page's window.listeningchanged on both: it used to be a record of every type ever subscribed and is now the set of types with a handler attached right now, which is what a test asking whether a game let go of the page actually wants. Each game'sinstall()stayed in its own game, because the engine touches no global.Two things to look at. The bundles are rebuilt: adding a module to the graph shifts the minifier's name pool, so both files are byte-for-byte the same size with different short identifiers — no page code ships, which the new test asserts twice. And there is a measured trap in both
install()undos worth reading: restoring a native global likeperformancewithObject.defineProperty— which looks more honest than the snapshot-and-assign this repo already used — costs every global lookup in every file that runs after it, takingbun testfrom 5 seconds to 17 and timing outworld.test.ts. Both helpers keep the assignment and say why in a comment.Not done, deliberately: the two games' 45-line GL recorders are still byte-identical copies, left where the issue asked for them; moving one behind the door beside
createRendereris its own change.src/engine/example.test.tsalso still keeps a small listener stub of its own, whichfakePagecould now replace.🤖 Generated with Claude Code
Budget reached at $10.27; this is where it stopped.
Closes #59