commit 8a8590ed8f5b2c7af20880b373500d63452f2594
parent 56f3e0b265473d20f8fe8747ca556207fa592481
Author: Kyle Barlow <kb@kylebarlow.com>
Date: Sun, 26 Apr 2026 14:21:16 -0700
Getting stuck on this
Diffstat:
1 file changed, 167 insertions(+), 0 deletions(-)
diff --git a/see-screenshot-of-current-sorted-flute.md b/see-screenshot-of-current-sorted-flute.md
@@ -0,0 +1,167 @@
+# Fire-Truck Map & Road Generation Overhaul
+
+## Context
+
+The current fire-truck map (see `Screenshot 2026-04-26 at 8.47.40 AM.png`) has two visible defects:
+
+1. **Phantom alleys inside building blocks.** Buildings are drawn at `-0.38 * cell` offset with size `0.76 * cell` (`game.js:183-189`), leaving a ~24% gap between adjacent buildings that reads as a road grid inside every block.
+2. **Malformed intersections.** `buildCity` (`lib.js:140-211`) emits horizontal/vertical line indices and connects every selected line **across the entire bounded rectangle**. So every chosen y crosses every chosen x, every road cell self-stubs to its own four edges, and intersections at boundaries collapse oddly.
+
+Additionally the world is a fixed-bounded rectangle around an initial `buildRoute(64)` route; the route-extension hook (`game.js:431-443`) extends the truck's route but **not** the rendered city, so the truck can leave the map.
+
+The user wants:
+- A finite, **pre-generated 50×50 island** surrounded by water with a 1-cell beach border (no more "infinite" extension).
+- An **outer ring road** around the buildings.
+- A **procedurally generated inner road network** that reads like the reference sketch (`untitled.png`) — a mix of through-streets, T-intersections, 4-ways, and 90° rounded corners, not a uniform grid.
+- Clean rendering with no phantom alleys and no double intersections.
+
+## Approach (high level)
+
+Replace `buildRoute` + `buildCity` with a single deterministic generator `buildIsland(opts, rng)` that:
+
+1. Lays out cell types (`water`/`beach`/`road`/`building`).
+2. Builds the road graph using **recursive rectangular subdivision** (subdivision → connectivity is structurally guaranteed; matches the regular look of the reference; no orphan-rescue pass needed).
+3. Adds variety with **probabilistic T-cuts** (some splits stop short of the far parent, producing T-intersections) and a small number of **chord spurs** (short segments that must start and end on existing road — never spikes).
+4. Generates the truck's `route` by walking the finished road graph, biased toward degree ≥ 3 intersections.
+5. Returns a unified `{ grid, cells, route, bounds, water, beach, buildings, roads }` payload.
+
+Renderer is rewritten to fill cells fully and use chamfered/arc'd corners on non-exit edges so T-intersections, corners, and 4-ways look clean.
+
+## Algorithm detail
+
+### `buildIsland({ width=50, height=50, seed }, rng)`
+
+```
+1. Allocate cells[width][height], type = 'building' for all.
+2. Mark perimeter ring cells as 'road' with N/E/S/W exits forming a closed loop.
+ (Corner ring cells are 90° corners; mid-edge ring cells are 'straight'.)
+3. Recursive subdivide(rect):
+ // rect is an axis-aligned rectangle of building cells bounded by road on all 4 sides.
+ if min(rect.w, rect.h) < MIN_BLOCK (default 4): return.
+ pick split axis = the longer side (random tiebreak).
+ pick split position uniformly in [2 .. side-2] (so each child rect is ≥ 2 thick).
+ draw straight road along that line, connecting the two parent roads.
+ with probability T_CHANCE (default 0.30): stop the cut 1 cell short of one parent
+ → creates a T-intersection at the parent that survives, dead-pull at the other end is
+ avoided by ensuring the truncated end aligns with an existing perpendicular cell
+ (we just don't extend that last cell, and the parent road absorbs no new exit there).
+ recurse into both child rects.
+ Run subdivide on the inner rectangle (interior to the outer ring).
+4. Stitch cell exits: for each road cell, set N/E/S/W exits based on the four neighbors that are road.
+5. Cleanup pass: any road cell with degree 1 (a true dead-end produced by a T-cut going wrong)
+ gets its terminal edge clipped (cell reverted to 'building') OR extended to the nearest road,
+ whichever is shorter. This pass is small because subdivision rarely produces dead-ends; it
+ only catches the truncated-T case.
+6. Optional chord spurs (CHORD_COUNT default 6): pick random road cell, pick perpendicular
+ direction with empty 1st neighbor, walk straight until hitting another road cell. If
+ walk exceeds MAX_SPUR (default 8) without hitting road, **discard**. Apply min-spacing
+ ≥ 2 perpendicular cells from any parallel road. (Pure chord rule — no spikes ever.)
+7. Final connectivity check: BFS from ring; assert all road cells reachable. Throw if not
+ (subdivision guarantees this; the throw is a regression sentinel, not a fallback).
+```
+
+**Why subdivision over growth:** connectivity is guaranteed by construction; block sizes are bounded; no rejection-storm starvation at high density. Plan-agent critique flagged growth's failure modes (orphan clusters, parallel-road thinness, branch-spike connectivity violations) — subdivision sidesteps all of them.
+
+**Why include T-chance + chord spurs:** pure subdivision produces almost-all 4-ways (every cut meets two parents). T-chance and the cleanup pass introduce T-intersections; chord spurs add organic variation so the output doesn't look like a strict gridiron.
+
+### Truck route: `buildRouteOnGraph(grid, { count=200, startCell, startHeading }, rng)`
+
+```
+1. Pick start cell on the outer ring (or use provided startCell).
+2. Walk along current heading until reaching an intersection (degree ≥ 3) or corner (degree 2 turn).
+3. Choose a legal move biased toward the move that leads to the lowest-visit-count exit.
+4. Record { x, y, headingIn, headingOut, move, exitX, exitY } using the same shape game.js
+ already consumes. Enforce step-gap ≥ 2 by walking at least 2 cells before recording the
+ next intersection (skip degree-2 straight cells; only count true intersections/corners).
+5. Repeat for `count` steps. No revisit-thrash defense beyond visit-count bias because
+ subdivision guarantees a connected graph with many cycles.
+```
+
+A new helper `extendRouteOnGraph(grid, route, additional, rng)` is added so the in-game extender (`game.js:431-443`) can keep working from the truck's last `(exitX, exitY, headingOut)`. No more `buildRoute(36)` from `(0,0,east)`.
+
+### Renderer rewrite (in `game.js`)
+
+Rip out the offset-building approach. Per cell:
+
+| Cell type | Drawing |
+|---|---|
+| `water` | Solid blue (`#1D7CF2` darkened ~20%) over its full cell footprint. |
+| `beach` | Sand color (`#F2E2B6`) over full cell. |
+| `building`| Pastel fill over full cell, optional 2-px darker inset border for visual separation. Color seeded by `(x*13 + y*17) % palette.length` (preserves existing palette). |
+| `road` | Asphalt fill over full cell, then **chamfer the non-exit corners**: for each pair of adjacent edges that don't both have an exit, fill the inside corner with the neighboring cell's color so a corner cell becomes a quarter-arc, a T becomes a clean trident, a 4-way is unmodified. Lane stripes drawn only on `straight` cells (degree-2 with opposite exits), skipping the 0.5 cell nearest each end so stripes never enter intersections. |
+
+The chamfer is what gives the rounded-90° look in the reference sketch. Implementation: draw the asphalt rectangle, then for each of the 4 corners check if both adjacent cell-edges are non-exits — if so, paint a small quarter-disc (or square chamfer for cheaper) of the corner-diagonal cell's color over that corner. This is baked once per cell into the chunk texture, so it's free at runtime.
+
+**Lane stripes detail:** existing `BAKE_ROAD` width is preserved. Stripes only on through cells. Yellow dashed center line drawn from `cell_edge + 0.1*cell` to `opposite_edge - 0.1*cell` so adjacent intersections don't get stripe leakage.
+
+**Camera:** `cameras.main.setBounds(islandBounds)` so the truck cannot leave the island; `setBackgroundColor(WATER_COLOR)` for the area outside the island bake.
+
+## Files to change
+
+| Path | Change |
+|---|---|
+| `games/fire-truck/lib.js` | Replace `buildRoute`/`buildCity` with `buildIsland`, `buildRouteOnGraph`, `extendRouteOnGraph`. Keep `HEADINGS`, `turnHeading`, `getRelativeMove`, `stepPosition`, `classifyIntersection`, `getLegalMoves`, `pickPromptMove`, `summarizeKinds` exported. Add `CELL_TYPES` enum. |
+| `games/fire-truck/game.js` | Replace `buildRoute(64)` + `buildCity(route)` with a single `buildIsland(...)` call (see `game.js:48-49`). Rewrite `renderCity` → `renderIsland` to handle 4 cell types and chamfered corners. Update `extendRouteIfNeeded` (`game.js:431-443`) to call `extendRouteOnGraph`. Update camera to `setBounds`. Drop the `BAKE_CELL * 0.38` building offset entirely (`game.js:183-189`). |
+| `tests/unit/route.test.js` | Rewrite for new API: `buildIsland` determinism, route step-gap ≥ 2, route stays on road graph, `extendRouteOnGraph` continues smoothly from end-of-route state. |
+| `tests/unit/projection.test.js` | Rewrite for `buildIsland`: t-ratio ≥ 0.04, four-way-ratio ≥ 0.04, largest connected component = 100% (subdivision guarantees), avg degree ≥ 1.5, alternate-connections ≥ route.length, no dead-ends, building cells fill the full inner rectangle minus roads. |
+| `tests/browser/runner.js` | Add fire-truck cases: (a) island bounds visible (truck cannot leave water boundary), (b) `__FT_SCENE__.cellAt(x, y)` returns expected type, (c) screenshot regression smoke. |
+| `tests/browser/screenshot.js` *(new)* | Standalone Playwright script for the implementation agent — see next section. |
+| `package.json` | Add `npm run screenshot` → `node tests/browser/screenshot.js`. |
+
+## Headless screenshot harness for the implementation agent
+
+The implementation agent should be able to **see** what it built without a human running the browser. Add a new script `tests/browser/screenshot.js` that:
+
+1. Boots the same `tests/serve.js` server used by the test runner.
+2. Launches Playwright headless Chromium with `viewport: 1280×800`, `deviceScaleFactor: 1`.
+3. Runs four shots and saves them to `tests/screenshots/fire-truck-*.png`:
+ - `fire-truck-overview.png` — set `window.__FT_SCENE__.cameras.main.setZoom(0.05)` and centre on the island so the entire 50×50 map fits in the viewport. Confirms ring road, water, beach, and the inner road network in one frame.
+ - `fire-truck-gameplay.png` — default camera zoom, captured 1.5 s after load, with `GAME_SPEED_MULTIPLIER = 0.0` (truck frozen) so the frame is deterministic.
+ - `fire-truck-intersection.png` — programmatically position the truck at the first 4-way along its route (`__FT_SCENE__.snapTruckToRouteIndex(k)` helper added in `game.js`) so the screenshot always shows a 4-way + a T + a corner in one frame.
+ - `fire-truck-portal.png` — portal index page, for sanity.
+4. Prints the saved paths to stdout so the agent's tool result includes them.
+
+The script must be runnable headless under the existing `playwright` install (`npm install` already ships Chromium). No new deps. Implementation agent runs `npm run screenshot` and then uses the `Read` tool on the PNG files to inspect — `Read` accepts image files and presents them to the model.
+
+Add `tests/screenshots/` to `.gitignore` (these are debugging artifacts).
+
+## Tunable parameters (with defaults that should pass tests)
+
+```
+WIDTH = 50 HEIGHT = 50
+MIN_BLOCK = 4 T_CHANCE = 0.30
+MAX_SPUR = 8 CHORD_COUNT = 6
+ROUTE_COUNT = 200 ROUTE_GAP = 2
+PALETTE = existing 6-color pastel set
+WATER_COLOR = #6BC4E8 (lighter than primary blue, kid-friendly)
+BEACH_COLOR = #F2E2B6
+ASPHALT_COLOR = #2A2D34 (existing)
+STRIPE_COLOR = #FFD23F (existing)
+```
+
+These are exposed on `lib.js` exports so tests can override.
+
+## Verification
+
+Run in this order — each must pass before moving on:
+
+1. `npm run test:unit` — `route.test.js` + `projection.test.js` cover determinism, intersection ratios, connectivity, dead-end absence, and route step-gap.
+2. `npm run test:browser` — existing fire-truck browser cases plus new island-bounds + screenshot smoke.
+3. `npm run screenshot` — generates the 4 PNGs. **Implementation agent must `Read` `tests/screenshots/fire-truck-overview.png` and confirm visually:** outer ring road is closed, no phantom alleys inside blocks, mix of T's / 4-ways / corners visible, water surrounds the island, beach is a 1-cell strip.
+4. Manual smoke (optional, if dev server is running): `npm run dev` and play for 30s — truck never leaves the island, prompts still fire at intersections, no console errors.
+
+## Critical files (open these first when implementing)
+
+- `/home/kyle/gits/kgames/games/fire-truck/lib.js` — full rewrite of generator section; keep helpers.
+- `/home/kyle/gits/kgames/games/fire-truck/game.js` — replace `renderCity` (~line 132–220), the `buildRoute`/`buildCity` call site (~line 48-49), the building-draw block (~line 183-189), and `extendRouteIfNeeded` (~line 431-443). Camera `setBounds` near `create()` (~line 89-109).
+- `/home/kyle/gits/kgames/tests/unit/projection.test.js` — current assertions to preserve (or strengthen): t/four ratios, largest-CC, avg degree, alt-connections.
+- `/home/kyle/gits/kgames/tests/unit/route.test.js` — current step-gap ≥ 2 assertion (line ~56-60) must still hold.
+- `/home/kyle/gits/kgames/tests/browser/runner.js:212-267` — `__FT_SCENE__` shape that browser tests assert on; preserve `truckX, truckY, debugDistancePx, promptDir, state, segmentEnd, truck, targetSpeed, speed, lastStepTime, promptResolved` and add `cellAt`, `snapTruckToRouteIndex`.
+- `/home/kyle/gits/kgames/tests/serve.js` — reused by the new screenshot script.
+
+## What stays unchanged
+
+- All movement logic in `game.js` (approach/exit phases, speed/accel, prompt UI, fail/resume) — the new generator emits the same `route` shape.
+- `index.html`, audio helpers, prompt overlay, FPS HUD.
+- The shared color palette and Fredoka font.
+- The CDN-loaded Phaser/Tone — no build tooling change.