# Excitebike — build plan Living plan doc. Survives context clears: **read this file first**, pick the next unchecked item, update the checkboxes and the Status line as work lands. ## Status **Waves 0–4 done 2026-07-29.** Ten tracks in the bank — the five original NES courses transcribed from their track maps, plus five new ones. SELECTION A, SELECTION B and DESIGN all implemented. `node tools/verifyExcitebike.js` → **579 checks green**. Wave 5 (art polish, icon frame 91) is **NEXT**. **Never opened in a browser.** Brian playtests; nothing here is signed off until he has. ## Decisions taken up front | Question | Answer | |---|---| | Scope | Single player, faithful to the 1984 NES original | | Modes | SELECTION A (solo vs the clock), SELECTION B (vs rivals), DESIGN — the original's three | | Rivals | Anonymous NES bikes in the alternate palette. No names, no opponent roster | | Tracks | The 5 originals, transcribed from the NES maps, plus 5 new | | Presentation | True 256×240 NES frame at integer ×4, centred in a cabinet bezel | | Overlay | Full `ArcadeCRTOverlay` over bezel and all; `m6x11` for the non-NES chrome | | Slug / category / icon | `excitebike` / `arcade-console-pc` / iconFrame **91** (art still needed) | ## Files | File | Role | State | |---|---|---| | `src/games/excitebike/ExcitebikeNES.js` | NES palette, index grids, 8×8 tiles, screen geometry | done | | `src/games/excitebike/ExcitebikeArt.js` | Tile bank, themes, 5×7 font, parametric bike/rider/dust | done | | `src/games/excitebike/ExcitebikeTrack.js` | Hurdles A–S, terrain compiler, sampling, validation | done | | `src/games/excitebike/ExcitebikeLogic.js` | 60Hz sim: throttle, heat, jumps, crashes, rivals | done | | `src/games/excitebike/ExcitebikeAuto.js` | Reference riders + `probeTrack` | done | | `src/games/excitebike/ExcitebikeRaster.js` | Index grids and terrain → canvases | done | | `src/games/excitebike/ExcitebikeGame.js` | Phaser scene: viewport, screens, HUD, audio | done | | `src/games/excitebike/ExcitebikeDesign.js` | DESIGN mode screen | done | | `src/games/excitebike/ExcitebikeDesignData.js` | Node-safe half of DESIGN, so the harness can pin it | done | | `src/games/excitebike/sprites.md` | Drop-in art spec | done | | `assets/gamedata/excitebike/` | `tracks.json` + `track-01..10.json` | done | | `tools/genExcitebikeTracks.js` | Builds the bank, measures the qualifying times | done | | `tools/readExcitebikeMaps.js` | Transcribes the five originals from their map PNGs | done | | `tools/lib/png.js`, `tools/lib/canvasStub.js` | Zero-dependency PNG reader; headless canvas | done | | `tools/verifyExcitebike.js` | The harness | done | Wiring is in place across the six touchpoints: `gamesRegistry.js`, `main.js` (import + scene array), `GameRoomScene.js` `slugDispatch`, `assetManifest.js` (music only — every pixel is generated), `soundtrack.js` (`excitebike: 'nintendo'`). `PreloadScene` needed no change: there is no artwork JSON because there is no artwork to fetch. --- ## Wave 0 — NES foundation — ✅ DONE 2026-07-29 Palette, tile bank, raster stage, the masked ×4 viewport, CRT overlay, all wiring. - [x] 64-entry NES master palette; everything drawn samples only from it - [x] Art as **index grids** of subpalette slots, not pixels, so Node can assert on it - [x] 8×8 tile bank as string art; 5×7 font in 8×8 cells via Phaser `RetroFont` - [x] One container at NES coordinates, `setScale(4)`, geometry-masked to a 1024×960 window - [x] Cabinet bezel drawn inside the scene so the CRT curve wraps it too ### Wave 0 findings 1. **The art has to be data to be testable.** Grids of slot indices cost nothing and let the harness prove every tile stays inside four colours and every sprite inside its subpalette — with no canvas and no dependency. `ExcitebikeRaster.js` is the only module that touches the DOM, and `tools/lib/canvasStub.js` stubs enough of it (`createImageData`/`putImageData`/`getImageData`) to run that module under Node as well. Both halves are checked. 2. **Draw the bike parametrically, not by hand.** 17 pitch angles × 4 wheel phases is 68 frames of the same machine; hand-authoring them gets you 68 subtly different machines. Drawing into an index grid keeps the NES constraints while giving smooth animation. 3. **Containers ignore child depth.** Draw order inside the viewport container is insertion order. Bikes are sorted per frame with `bikeLayer.sort('y')` so nearer lanes paint last. 4. **Integer scale only.** 1080/240 is 4.5, and half the rows would be five pixels tall while the rest were four. On a scrolling track that shimmer is very visible. ×4 in a bezel is correct. 5. **`RetroFont` is not used anywhere else in this repo** — the config shape was verified against the Phaser 3.90 bundle before committing to it (`ParseRetroFont`: `image`, `width`, `height`, `chars`, `charsPerRow`, `offset`, `spacing`; `xAdvance` is `width`, so text centres on `len * 8`). ## Wave 1 — Ride it — ✅ DONE 2026-07-29 Full bike physics, heat, jumps, crashes, SELECTION A. - [x] Fixed 60Hz accumulator, seeded RNG, deterministic from a seed + input trace - [x] A accelerates and releasing brakes; B is turbo and heats the engine; cool zones dump heat - [x] Landing judged on `|pitch − groundAngle|`: clean, hard, or down - [x] Crash → tumble → run on foot mashing A → remount - [x] `M:SS:hh` clock, `3RD` target, heat meter, wall BEST — the original's HUD ### Wave 1 findings 1. **Feathering turbo was free speed.** With a slow bleed off the top end, tapping B held you at turbo pace while the meter hovered. `turboBleedRate` makes speed above the normal top end fall away fast, so sustained turbo has to be paid for in heat. 2. **Pitch control needs weight.** The first cut integrated pitch velocity eight times too fast and the nose could be swung end to end in a fifth of a second, which made every landing trivial and made the skill dial do nothing. At ~0.3s level-to-full-up, jumps have to be planned on the way up. 3. **Landing tolerance must narrow with impact speed.** A fixed window meant a passive rider never crashed: gravity alone left them inside it. `landSpeedSqueeze` makes the big jumps the ones that punish a lazy attitude, which is what the ramps are for. 4. **A crash cost three seconds and there were six a race.** Recovery dominated everything. Shorter tumble, a shorter slide, and a shove on the remount instead of a dead stop brought it to about two. ## Wave 2 — The five NES courses — ✅ DONE 2026-07-29 All 19 hurdles, and the originals transcribed rather than guessed. - [x] `tools/lib/png.js` — PNG decode on `node:zlib`, no dependency - [x] `tools/readExcitebikeMaps.js` — palette, extent, ramps, holes, straight off the images - [x] Hurdle catalogue reshaped to the heights and footprints the originals actually use - [x] Playfield geometry corrected to the original's own bands ### Wave 2 findings 1. **The maps carry a legend panel.** The first 256–512px of each image is a black/navy panel with the track's stats on it, not track. Reading the image width as the course length is wrong by up to 512px. The real lengths are **5888 / 5393 / 6416 / 6528 / 5752**. 2. **Lanes are 12 pixels.** The playfield splits crowd 0–39, sky and wall 40–63, infield 64–127, four 12px lanes 128–175, apron 176–191. The first build guessed 24px lanes in a 56–152 band and was wrong about every proportion. A lane is a tile and a half deep, so lane dividers land mid-tile — that is the real geometry, not a rounding slip. 3. **Almost nothing in the original is a ramp with a cliff on the end.** They are *hills*. Of Track 1's 22 raised features, 19 rise and come straight back down. Only the short steep ones (h16 w15) end high. The catalogue was rebuilt around that: A/B/C/D/H/R are mounds, E and S are the launchers. 4. **So hills have to launch you by curvature.** You leave a crest when `v² · curvature > g`. That one line is what turns the original's rolling terrain into jumps at speed and leaves it as bumps at a crawl. Without it every course in the bank is a flat road with decoration on it. 5. **A launch edge is two pixels wide and the bike moves four.** Testing the crest at exactly the bike's position stepped straight over every ramp. The sim now tests the stretch it is about to cross, scaled by speed. 6. **Each course has a rare highlight colour** used only on lit ramp faces. A material threshold tuned on the common colours misses it, which loses small ramps — and a missed ramp in front of a hole turns a jump into an unclearable wall. That was the one validation failure in the transcription. ### Known gap, deliberate Cool zones and mud are **not** transcribed. Both are drawn as texture inside the lane band, and on these maps that texture is not separable from the shading on a ramp body — the detector fires on every ramp. Rather than emit hurdles the images do not support, cool zones are placed into the long clear stretches on a rule and mud is left out. This is the one part of the five courses that is not the original's. Worth revisiting if the maps can be read better. ## Wave 3 — Racing — ✅ DONE 2026-07-29 - [x] Rival bikes, gap avoidance, lane preference, traffic awareness - [x] The manual's contact rule: catch a leader from behind and *you* go down - [x] Qualify → main race → next track, best times and progress in localStorage ### Wave 3 findings 1. **Contact needs a closing speed.** Crashing whoever was behind on any overlap wiped the pack out on every straight. Below `contactKnockdownSpeed` the trailing bike is simply held up. 2. **The reference rider has to avoid traffic too**, or SELECTION B measures a bot flaw rather than the race: it rear-ended the pack all afternoon and never won. 3. **Rivals must be as fallible as a player.** Give them exact landing angles and they ride perfectly, the race has no attrition, and nobody holding a controller can win. They now misjudge and react late on the same terms the `human` probe does. ## Wave 4 — DESIGN mode — ✅ DONE 2026-07-29 - [x] `ABCDEFGHIJKLMNOPQRS CL END LP` strip, bike-as-cursor, 50-hurdle cap, laps 1–9 - [x] `PLAY MODE A / PLAY MODE B / DESIGN / SAVE / LOAD / RESET`, one save slot - [x] A designed track is priced by the same reference rider the shipped ones are ### Editor contract, pinned by the verifier `ExcitebikeDesign.js` imports Phaser and cannot run under Node, so the parts the editor rests on live in `ExcitebikeDesignData.js` and §9 of the harness asserts them: the palette strip is the 19 hurdles plus `CL`/`END`/`LP`, a blank design is a legal track, a designed track survives a save/load round-trip and can be ridden to the finish, and the 50-hurdle and 9-lap caps hold. ## Wave 5 — Polish — ⬜ NEXT - [ ] Paint iconFrame **91** into `assets/images/game-icons.png` (44×44, row 6 col 2) - [ ] Parallax backdrops and per-track crowd colour; the themes exist but are plain - [ ] Revisit cool-zone and mud transcription (see the Wave 2 gap) - [ ] Playtest, then retune from what it actually feels like ## How difficulty is decided One place: `tools/genExcitebikeTracks.js`. Qualifying times are **measured, never guessed** — the `human` probe in `ExcitebikeAuto.js` drives each course over five seeds and the target is the median plus 6%. The expert is deliberately *not* the yardstick: with perfect information and no reaction time it is barely slowed by hazards at all, and a target priced off it would be unreachable. The harness asserts both ends and the gradient across ability: | rider | wins | podium | |---|---|---| | expert | 50–100% | 85–100% | | human | 0–35% | 20–70% | | steady | 0–20% | 0–35% | | naive | 0–2% | 0–10% | A change that makes the pack trivial or impossible breaks one of those bands. ## Sources for original-game behaviour - Official Nintendo manual (NES Classic edition), `CLV-P-NAAHE_en.pdf` — controls, HUD, the three modes, cool zones, the rival contact rule, DESIGN mode's menu and caps - The NES instruction manual's hurdle list — the 19 letters A–S and their names - nesmaps.com full-course track maps for Tracks 1–5 — the source for every length and hurdle position