fertig-classic-games/docs/excitebike-build-plan.md

12 KiB
Raw Blame History

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 04 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.js579 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 AS, 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.

  • 64-entry NES master palette; everything drawn samples only from it
  • Art as index grids of subpalette slots, not pixels, so Node can assert on it
  • 8×8 tile bank as string art; 5×7 font in 8×8 cells via Phaser RetroFont
  • One container at NES coordinates, setScale(4), geometry-masked to a 1024×960 window
  • 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.

  • Fixed 60Hz accumulator, seeded RNG, deterministic from a seed + input trace
  • A accelerates and releasing brakes; B is turbo and heats the engine; cool zones dump heat
  • Landing judged on |pitch groundAngle|: clean, hard, or down
  • Crash → tumble → run on foot mashing A → remount
  • 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.

  • tools/lib/png.js — PNG decode on node:zlib, no dependency
  • tools/readExcitebikeMaps.js — palette, extent, ramps, holes, straight off the images
  • Hurdle catalogue reshaped to the heights and footprints the originals actually use
  • Playfield geometry corrected to the original's own bands

Wave 2 findings

  1. The maps carry a legend panel. The first 256512px 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 039, sky and wall 4063, infield 64127, four 12px lanes 128175, apron 176191. The first build guessed 24px lanes in a 56152 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

  • Rival bikes, gap avoidance, lane preference, traffic awareness
  • The manual's contact rule: catch a leader from behind and you go down
  • 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

  • ABCDEFGHIJKLMNOPQRS CL END LP strip, bike-as-cursor, 50-hurdle cap, laps 19
  • PLAY MODE A / PLAY MODE B / DESIGN / SAVE / LOAD / RESET, one save slot
  • 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 50100% 85100%
human 035% 2070%
steady 020% 035%
naive 02% 010%

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 AS and their names
  • nesmaps.com full-course track maps for Tracks 15 — the source for every length and hurdle position