kgames

KGames — free keyboard games for kids
git clone https://www.keyboard.games/code/kgames.git
Log | Files | Refs | README | LICENSE

commit 8a8590ed8f5b2c7af20880b373500d63452f2594
parent 56f3e0b265473d20f8fe8747ca556207fa592481
Author: Kyle Barlow <kb@kylebarlow.com>
Date:   Sun, 26 Apr 2026 14:21:16 -0700

Getting stuck on this

Diffstat:
Asee-screenshot-of-current-sorted-flute.md | 167+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
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.