Field Notes · kept as written, during the work
Boom — The Field Notes
The unpolished, append-only log written during the build, as it happened: the fan-out that produced nothing, the playtest loop that produced the game, and the bill. Field Guide No. 5 was generated from this file, not from memory.
_Append-only from day zero, timestamped, unpolished. Guide No. 5 and its post-mortem get generated from this file — write it when it happens._
2026-09-06 — kit
- Repo scaffolded from the FrameWork lead session. Rules of engagement in PLAN.md §0 are distilled from the published Potluck and game-builds post-mortems; rule 2 (tests-with-the-work, no QA lane) is Tommy's direct correction from the Potluck experience.
2026-09-06 — Phase One, lead window opens
- Spec says "single HTML file"; PLAN §0 rule 3 says one writer per file across four lanes. Those can't both be literally true. Chose ES modules in
src/with no bundler (recorded in PLAN §Decisions). The spirit of the spec — no library, no build step — survives; the letter doesn't. node --test test/fails on Node 24 with MODULE_NOT_FOUND: the runner wants a glob, not a directory.node --test "test/**/*.test.js".- Stage 00 shipped with 12 tests. No browser walk yet — the browser tools aren't in this window; the game is exercised via
npm startby Tommy. - Stage 02 tests caught something worth putting in the guide: the naive march *misses walls*. Three of 288 comparison rays disagreed with DDA, and all three were rays passing within ~0.003 of a cell corner. The 0.01 step walks straight over the sliver and threads through the wall; DDA hits it. "Imprecise" in the draft reads as "wobbly textures" — it's also "occasionally sees through corners". Test excludes near-corner rays from equality but keeps "DDA never past naive" unconditional.
- Spec acceptance "iteration count under 20 per ray in a 24x24 map" is geometrically unachievable for every ray: a ray skimming a 22-cell open corridor crosses 22+ grid lines however you traverse. Measured mean 5.1 steps, max 25. Test asserts mean < 20 and max <= W+H; the draft's wording should say "typical ray".
- The draft's
sidewording ("0 = hit a N/S face") is ambiguous about whether a face is named by where it sits or where it faces. Froze it as "which grid line was crossed" in CONTRACTS.md; the draft should adopt that phrasing. - Rule 4 check before reporting Phase One: 3 stage commits + this contracts commit exist in git;
npm test= 29 passing.
2026-09-06 — Phase Two
- Stages 03–05 went in as three commits, 19 new tests, no surprises in the renderer itself. Every failure during the phase was in my *test* geometry, not the code: twice I picked a column that had a stone wall in it, once I asserted perp is affine across the screen when it's 1/perp that is. A top-down view would have caught the first two instantly — which is the stage 00 argument, restated by the tests.
- Render dropped the Float32 ray-vector arrays to Float64 after a 1e-9 tolerance test failed at 1e-8. zBuffer stays Float32 (it's what the sprite lane compares against; sub-1e-6 precision is irrelevant there).
- The debug view now lives on its own 288px canvas overlaid on the 320x200 game canvas, so stage 00's "fixed 12px per cell" survives the switch to low internal resolution. The feel lane's minimap should scale it into the game canvas instead.
- No browser walk this phase either. Tommy:
npm start, open localhost:4173, press M and F. Report anything that looks off. - Rule 4 check: 6 stage commits + 1 contracts commit on main;
npm test48 passing.
2026-09-06 — Phase Three fan-out
- Before fan-out: main at be27c3d, 60 tests green, 9 commits this session. Lead session budget remaining ≈ 14.83M tokens (of 15M); the lead has spent roughly 170K of context to get Phases One and Two plus the seam built serially, in one window.
- Seam design choice worth the guide: fog as a post-pass over a per-pixel depth buffer. The draft says "darken each wall column" — that would put fog inside the textures lane and force sprites to call into it. A depth buffer the lead owns means four lanes and zero cross-lane calls.
- Fan-out attempt 1: four implementer agents spawned at 4 worktrees (verified on disk, all at a7298ba). 14 minutes later: zero commits, zero modified files, in every worktree. Three of four died with "no progress for 600s (stream watchdog did not recover)"; the fourth was idle and got stopped. Earlier this session the lead's own command classifier (a Sonnet model) timed out once, so the likely cause is a model-side outage, not the lanes. Rule 5: checked on disk before deciding. Rule 6: one retry with a different subagent model, then fold.
- Fan-out attempt 2 (Opus subagents, same prompts trimmed, worktrees fast-forwarded to db6b07b): identical result. Each lane's transcript ends at its first sentence ("I'll start by reading the contracts") and the stream watchdog kills it 600s later. Zero commits, zero dirty files, all four, both attempts. Wall clock burned on parallelism: ~68 minutes. Wall clock the lead spent building Phases One and Two serially: less than that.
- FOLDED BACK TO ONE AGENT (rule 6). Stages 06–09 get built by the lead on main, serially, one commit each, tests-with-the-work. The seam (CONTRACTS.md Phase Three section) stays as designed — it's still the right file boundary, and Phase Four can retry a fan-out if the subagent path is healthy by then. Worktrees are left in place, clean.
- The finding for the guide is not "parallel was slower". It's that a subagent that cannot get its first tool call answered is indistinguishable from one that is thinking, for ten minutes, times four. The monitor on the worktrees (commits/dirty files per lane every 45s) was the only honest signal, and it read 0/0 the whole time.
- Stages 07–09 serial: sprites, combat and feel each went in as one commit with 8–11 tests. Three of the four lanes' worth of work took the lead less wall clock than the two failed fan-outs did. Every test failure in these three stages was test geometry again (a wall cell I hadn't checked, an enemy with no line of sight, a pillar whose opaque core was hidden while its transparent margin was "visible"). The map's LAYOUT string is the thing to read before writing a test scene.
- Enemy AI walks-while-turning looked wrong in a test before anyone saw it on screen: an enemy facing away stepped away from the player for a second. Changed to turn-in-place until within 45°.
- Phase Three gate: 100 tests,
npm testgreen, 4 stage commits on main. Not yet done: a browser playtest — Tommy has the URL.
2026-09-06 — Phase Four
- Stage 10, bug the spec didn't list: with a forward 90° cone the cells BEHIND the enemy are unknown, so the enemy's own cell is always the nearest frontier and it never moves. Frontier now excludes cells within 0.75 of the enemy. Second: "stuck" detection by "did any axis move" never fires when one axis creeps by a hair against a wall; it has to be progress vs intended step. Third: nearest-frontier re-picks the same unreachable cell forever without a blacklist (12 s). With those three the corner enemy classifies >95% of reachable cells, zero errors, in under two simulated minutes — and the headless loop finds all of it in ~30 ms of real time.
- The exploration test first "failed" at 78% because the enemy found the bystander player I'd parked in a corner and correctly stopped to hold position. Put the player off-grid for coverage tests.
- Stage 9's "line of sight -> advance" test broke on purpose: under the sensor model an enemy facing away cannot see you. That's the feature.
- Tommy, mid-build: "can we improve the graphics at all, they seem pretty rough... cpu, gpu, disk, bus, network has all improved." Fair. The draft's 320x200 and 64px textures were a *teaching* default — chunky pixels hide math bugs and never make you think about performance. Once the math is proven by tests, the resolution is just a constant. Went to 960x600 with 128px textures and a textured, fogged floor/ceiling; the whole software frame is ~5ms. The interesting number for the guide: the resolution change touched two constants and six test literals, because everything else was already parametric in W/H.
- Also Tommy: hold the API/model mode until the basic game loop is solid, design it separately, and leave a seam so it can be toggled. So stage 11 as spec'd is not what ships; a driver seam is.
- Stage 11 as shipped:
src/driver.jsis the seam (world.driver, key 0),src/agent.jsis the wall-follower + recorder + log-only replay parked behind it. Headless, the wall-follower reaches the exit on its own with no enemies in the way; the log round-trips through JSON and replays frame-identically. No network code exists in the tree. The model mode is a separate conversation with Tommy. - Stage 12 measured for real, headless Chrome with WebGPU, driven over the DevTools protocol (a static --dump-dom fires before async work finishes;
scripts/bench-headless.mjspolls the page instead). First run reported "JS 0.000 ms" everywhere: headless Chrome coarsens performance.now() to 0.1 ms, so single-frame timing is meaningless — batch ten frames per sample. Result: GPU fixed cost ≈ 0.23 ms/frame, JS wins through 7680 columns, GPU wins from 15360. Crossover ~10K columns. The spec's prediction ("probably slower at 320x200, wins around 1920") was right in direction and optimistic by 5x on the crossover — because the JS DDA is *fast*: 3840 columns in 0.09 ms. - The f32 emulation test in Node caught a transcription bug before any GPU ran: I had the hitOffset axis swapped in the JS mirror of the kernel. The WGSL was right. Worth saying in the guide: write the emulation, it is a free second reviewer of the translation.
- 1 of 115,200 columns disagreed on tile/side (not distance): a ray through an exact cell corner where f32 broke the sideX<sideY tie the other way. Same corner-graze family as the stage 02 finding.
2026-09-06 — first playtest, Tommy
- Live feedback while playing, in order: enable mouse by default / "getting killed before I can get it set up" (-> click-to-play start gate + grace window); restart button; more than one level; exit should be a corridor into another room, locked until the level is cleared; real gun sounds; vertical aim; dogs; dogs a different colour on the minimap; everything on by default; ammo pickups + rapid fire; show the exit on the map; higher resolution. "The click to play that captures the mouse is a huge improvement." After the resolution bump: "this one may be the most fun so far."
- None of the above was in the thirteen-stage spec. All of it was one agent, serial, with Tommy playing between commits — which is exactly what PLAN §0 rule 1 says Phase Four should be. The fan-out phase produced nothing; the playtest loop produced the game.
2026-09-06 — playtest loop, round two
- Reported: exit freezes; a dog never chased; sounds vanish after rapid fire; dogs silent; enemies wedge; want finer detail; want a knife; bullets should sound different from the enemy's; pause; more levels; exit as a door that unlocks on clear; dogs attack from behind.
- The exit freeze was a temporal-dead-zone ReferenceError inside an async frame loop: the exception killed the loop silently. Found by pausing the page under the DevTools protocol (Debugger.pause → call stack) rather than by reading code. Then my fix introduced a worse bug — two requestAnimationFrame calls per frame, exponential — which the same probe found in one run. Lesson for the guide: an async render loop needs (a) a try/catch that keeps it alive and shows the error, and (b) a test that asserts exactly one frame is scheduled per frame.
- "Single fire goes silent after machine-gun mode" was an empty magazine with no dry-fire sound. Two symptoms Tommy reported were one cause.
- Wedged enemies: three real causes, found by simulation with a moving player and a metric that excluded intentional holds: (1) sidestepping masked stuck detection (movement ≠ progress → measure distance-to-goal), (2) an enemy spawned facing a corner had zero free cells in its belief and therefore no goal and no reason to turn, (3) nothing to do after exploring → patrol. 64 enemies × 8 levels × 90 s: 0 unexplained stills.
- GPU texture path went to WebGL2, not WebGPU: universal, and the pieces map naturally (full-screen world pass writing depth; sprite quads with hardware depth test; textureGrad with analytic footprints so cell seams don't blow the mip level). First compile failed on
half, a reserved word in GLSL ES — invisible until GL init errors were surfaced on the HUD. Headless Chrome needs `--use-angle=swiftshader --enable-unsafe-swiftshader` to have WebGL2 at all.
2026-09-06 — playtest loop, round three (evening)
- Tommy brought CC0 recordings (db465 weapon pack, pnthr's beast growl, Kodack's monster takes). The manifest-with-slices design paid off: one ten-second growl feeds growl/bite/pant/breath at different rates, one 23-second monster file feeds attack/spring/shriek. Every sample is positional through the same bus.
- Asks answered this round, in order: pause; ammo carry across levels; lives; health kits (25) + a flashing 60-second gold pack every third level; level number on screen; low-health red vignette; the brute (ambush from a barrel, 10x a guard, scales +15 hp/level, flees when hurt and outmatched, regenerates, returns; never loses you once sprung); weapons are collected (pistol+knife first; rifle L2, shotgun L4, sniper L6); first-person weapon art; crosshair at the true aim point; four wall themes with windows; touch controls (iPad/iPhone, verified in emulation); auto native resolution on the GPU path.
- "Board play is very fun, challenging but can be survived at least for a while. You can tell it's getting tougher level by level."
2026-09-06 — round four (late)
- Pushed to GitHub at Tommy's word (70 commits). Deploy to framelogic.ai is his, from a session in the FrameWork repo: `npm run vendor:game -- raycaster` copies ~/code/raycaster into public/games. I stopped when he said stop; nothing in FrameWork was touched.
- Courtyard: the raycaster's "ceiling" is a plane, so an outdoor level can't be a ceiling texture — sky rows become a gradient with clouds by bearing, depth 0 so fog never touches them; fog colour and range come from the theme. Same rule in the shader and the CPU path.
- Hands: the "hand" was a blob. Now a palm, four wrapped fingers with creases, a thumb and a cuff, drawn after the weapon so fingers overlay the grip; a support hand on the long guns. Test counts skin pixels.
- Dark cellar: one lighting function (spotFactor) shared by the world shader, the sprite shader and the CPU post-pass. Eyes are texels with alpha 254 — an "emissive" marker that costs nothing — drawn full-bright and skipped by both darkness and fog. First test run caught fog eating the eyes; the fix was one
continue. - Dynamic resolution measures the rAF interval (which includes GPU time; tick() alone would miss it): drop a notch after 1.5 s of >21 ms frames, climb after 8 s under 12.5 ms, until the player presses 7.
2026-09-07 — the bill
- Named Boom (Tommy), vendored to framelogic.ai/games/boom. Time to close rule 6 honestly: the notes recorded the budget check *before* fan-out (≈170K of context spent for Phases One and Two) and never the after. The record that does exist is the session log itself (
~/.claude/projects/-Users-tommywaddell-code-raycaster/, one lead session plus eight subagent transcripts). Totals below are from that log, usage deduplicated per API response. - Lead window, 2026-09-06 00:58 → 2026-09-07 00:12 EDT (one session, not continuous — Tommy playtested between rounds): 288 API calls, 281 tool calls, 77 commits, 0 → 220 tests. - output tokens: 671,656 - cache writes (new context): 2,348,093 - cache reads (context re-read per call): 140,865,970 - largest single-call context: ~900K
- The eight lanes (four implementers, four retries on a different model) that never made a commit: 5,697 output tokens, 228,558 cache writes, 483,868 cache reads — about 0.7M tokens and 68 minutes for zero code. Cheap in tokens, expensive in wall clock; the monitor was right.
- Build total: 677,353 output · 2,576,651 cache writes · 141,349,838 cache reads. Output — the tokens that became code, tests and notes — is under half a percent of what was billed as reads. That is the shape of one long window: every call re-reads everything before it, so the average call carried ~490K of context. The guide's number is not "677K tokens wrote a shooter"; it's "141M tokens of re-reading did".
- Tommy's verdict on the method, 2026-09-07, after the bill: "I think Boom is our real first success with multi-agent. We did not overuse it, we used it like a scalpel." Recorded verbatim, same as Potluck's verdict was. The lanes wrote nothing; the discipline around them — one fan-out, an on-disk monitor, a 68-minute fold-back — is what he is grading.
- Correction, from the session log (Tommy: "there were several work trees that ran"). The Phase Three note above says each lane's transcript "ends at its first sentence" and that a lane "cannot get its first tool call answered". The lane transcripts say otherwise: all eight ran in their worktrees and made tool calls — between 2 and 12 each, reading CONTRACTS.md, the source they owned, and
git status— before every one of them ends with an interrupt. What holds is the part the monitor measured: no lane edited a file or committed. The lanes ran and read; they never wrote. The 0/0 signal was right; the diagnosis of why was written from the outside and was wrong.