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

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

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

opened this

Everything is committed and pushed; the working tree is clean and bun test is green (536 pass, 0 fail, 5.28s).

src/engine/page.ts is the headless host the engine's own parameters already described: fakePage() hands back { surface, window, document } that createControls and attachButtons take with no cast, plus fire() to deliver an event, a live listening set, elements that remember what was hidden, written and focused, a FakePaint recorder that writes every 2D call down with its arguments in order, and a clock that moves only on step(ms). It has no timer, names no global, throws for no member a game skips, and knows what neither a renderer nor an AudioContext is — FakeCanvas.gl is an empty slot each game fills. src/engine/index.ts exports it with a paragraph saying why a package that asks a game for a host should hand a fake one back, GUIDE.md gains the page section a third game needs, and src/engine/page.test.ts covers the double itself in 17 tests — including that neither committed bundle contains it, read off disk the way bundle.test.ts reads them.

src/range/dom.ts is gone. src/range/stage.test-helper.ts keeps the range's ids, its GL recorder and its counted AudioContext; src/client/stage.test-helper.ts keeps the shooter's ids, its Audio, its renderer double and its fetch recorder. 437 + 320 lines of two stubs became 265 + 231 lines of game halves over 344 shared behind the door. boot.test.ts boots through the engine's page with every assertion unchanged, independence.test.ts and boundary.test.ts stay 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, querySelector over the classes an innerHTML names) over the range's five-field one; the shooter's seventeen-member 2D recorder over the range's ten, which had been as-cast into a Paint it did not implement; the shooter's window, which carries requestAnimationFrame, matchMedia and performance as members, over the range's, which only had them as globals — the range's install() now reads them off the page's window. listening changed 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's install() 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 like performance with Object.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, taking bun test from 5 seconds to 17 and timing out world.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 createRenderer is its own change. src/engine/example.test.ts also still keeps a small listener stub of its own, which fakePage could now replace.

🤖 Generated with Claude Code

Budget reached at $10.27; this is where it stopped.

Closes #59

Sign in to join this conversation.

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs