romdevtools 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,198 +1,877 @@
1
- /* ── racing.c — SMS top-down racing scaffold ────────────────────────
1
+ /* ── racing.c — SMS top-down road racer (complete example game) ──────────────
2
2
  *
3
- * Endless 3-lane top-down racer. Player car at the bottom of the
4
- * screen, obstacles spawn from the top and slide down. LEFT/RIGHT
5
- * (edge-detected) switches lanes. Speed grows with score; collision
6
- * triggers a 60-frame freeze then auto-resets.
3
+ * FENDER FURY a COMPLETE, working game: title screen, 1P endless race with
4
+ * speed control, 2P simultaneous SPLIT-LANE VERSUS (both cars on screen at
5
+ * once — player 2 on PORT B), a vertically-scrolling road done the SMS way
6
+ * (whole-plane R9 vertical scroll, latched once per frame), streamed roadside
7
+ * scenery rows, crash/lives rules, persistent best DISTANCE (Sega-mapper cart
8
+ * RAM — see the honesty note at best_save), PSG music + SFX, and the SMS's
9
+ * signature LINE-INTERRUPT split holding a fixed HUD strip over the road.
7
10
  *
8
- * Hardware bits used:
9
- * - VDP sprite SAT for player car + 4 obstacle cars
10
- * - VDP_drawText (via tile fill) for the SCORE HUD
11
- * - sms_joypad_read for player 1 input
11
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
12
+ * very different one. The markers tell you what's what:
13
+ * HARDWARE IDIOM (load-bearing) dodges a documented SMS footgun; reshape
14
+ * your gameplay around it (see TROUBLESHOOTING before changing).
15
+ * GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
16
+ *
17
+ * What depends on what:
18
+ * sms_hw.h / vdp_init.c / load_tiles.c / load_palette.c / sprite_table.c /
19
+ * joypad_read.c — the bundled VDP + input runtime (this file's externs).
20
+ * sms_sfx.{h,c} + sms_music.{h,c} — SN76489 PSG sound layers.
21
+ * sms_crt0.s — boot + vector table. Its $0038 IM-1 handler is the OTHER
22
+ * HALF of the line-interrupt idiom below: one status-port read acks BOTH
23
+ * the frame and line IRQ flags, then ei/reti. Load-bearing; edit with
24
+ * TROUBLESHOOTING open.
25
+ *
26
+ * THE DESIGN (read before reshaping):
27
+ * Scrolling — the road is the BACKGROUND, scrolled DOWN by DECREMENTING the
28
+ * vertical-scroll register R9 each frame (the driving-up illusion). Cars
29
+ * and traffic are sprites with their own Y. Compare the Genesis version of
30
+ * this game (examples/genesis/templates/racing.c): there a single VSRAM
31
+ * value scrolls the whole plane and the VDP wraps it in hardware at 256.
32
+ * The SMS is the SAME idea — ONE register, whole-plane, single-plane — with
33
+ * two twists this file is built around: (1) R9 is LATCHED ONCE PER FRAME,
34
+ * not per scanline (so the road scrolls per-frame, never mid-frame — see
35
+ * the R9 idiom); (2) in 192-line mode the name table is 32x28 = 224 px
36
+ * tall, so R9 WRAPS AT 224, not 256 (the SMS analog of the NES's 240-wrap;
37
+ * plain uint8 math overruns it — see scroll_road_down).
38
+ * Streamed scenery — as the road scrolls, name-table rows re-enter at the
39
+ * TOP; the moment a row becomes the top road row we restamp its roadside
40
+ * cells with fresh random scenery, so the 224-px loop never shows the same
41
+ * scenery twice. The restamp lands UNDER the HUD strip, which hides it.
42
+ * HUD — the line-IRQ split. The road scrolls vertically (R9, whole plane),
43
+ * so the HUD strip's name-table rows scroll with it — a BG HUD over a
44
+ * vertical road would crawl. So HUD GLYPHS ARE SPRITES on the fixed top
45
+ * scanlines (immune to R9, exactly as the NES version uses sprite digits
46
+ * for the same reason). The line-IRQ split still earns its keep: it holds
47
+ * the top HUD band at horizontal scroll 0 while the road BELOW it SWAYS
48
+ * left/right per-strip (R8 — the one scroll axis you CAN change mid-frame),
49
+ * a gentle curve that reads as the road bending ahead. Fixed un-swayed HUD
50
+ * band on top, curving road below: a real line-IRQ fixed-HUD split.
51
+ * 2P VERSUS — ONE VDP means ONE road scroll, so both players share one road
52
+ * at a fixed speed and only steer (the same constraint the NES/Genesis
53
+ * versions explain): solid center divider, P1 (white, port A) owns the left
54
+ * two lanes, P2 (red, port B) the right two. Each starts with 3 crashes;
55
+ * first to use them all LOSES.
56
+ * 1P RACE — all four lanes, button 1/UP accelerates, button 2/DOWN brakes
57
+ * (speed 1-4); 3 crashes end the run. Persistent stat: best DISTANCE
58
+ * (uint16, one unit = 16 scrolled pixels ≈ one car length) via best_save.
59
+ *
60
+ * Frame budget (NTSC, 60fps): SAT upload (192 OUTs) + the HUD sprite stage fit
61
+ * in vblank + the 24-line HUD strip; 6 traffic × 2 cars of AABB and one row
62
+ * restamp at most every other frame run in the active frame with room to
63
+ * spare. The sway table is 21 R8 writes scheduled off the line IRQ.
64
+ *
65
+ * SDCC FOOTGUN (bites every fork): uint8 loop bounds silently wrap —
66
+ * `for (uint8_t i = 0; i < 24 * 32; i++)` is an INFINITE loop (768 > 255;
67
+ * SDCC even warns "comparison is always true"). Treat that warning as an
68
+ * error: widen the counter to uint16_t or keep loops nested per-row like the
69
+ * painters below.
12
70
  */
13
71
  #include "sms_hw.h"
14
72
  #include "sms_sfx.h"
73
+ #include "sms_music.h"
15
74
  #include <stdint.h>
16
75
 
76
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
77
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
78
+ #define GAME_TITLE "FENDER FURY"
79
+
17
80
  extern void sms_vdp_init(void);
81
+ extern void sms_vdp_write_reg(uint8_t reg, uint8_t value);
18
82
  extern void sms_vdp_display_on(void);
83
+ extern void sms_vdp_display_off(void);
84
+ extern void sms_vdp_set_addr(uint16_t addr, uint8_t prefix);
19
85
  extern void sms_load_palette(const uint8_t *palette);
20
86
  extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
21
87
  extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
22
- extern void sms_vblank_wait(void);
23
88
  extern uint8_t sms_joypad_read(void);
89
+ extern uint8_t sms_joypad_read_p2(void);
24
90
  extern void sms_sprite_init(void);
25
91
  extern void sms_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
26
92
  extern void sms_sat_upload(void);
27
93
 
28
- #define LANE_LEFT_X 72
29
- #define LANE_MID_X 124
30
- #define LANE_RIGHT_X 176
31
- #define PLAYER_Y 160
32
- #define MAX_OBSTACLES 4
33
-
94
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
95
+ * Palettes. SMS CRAM is 2-2-2 BGR (--BBGGRR): R bits 0-1, G bits 2-3,
96
+ * B bits 4-5. White = 0x3F. BG colour 0 doubles as the backdrop/border. */
34
97
  static const uint8_t palette[32] = {
35
- /* BG: 0 = dark navy backdrop, 1 = grass green, 2 = road grey */
36
- 0x10, 0x08, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00,
98
+ /* BG: 0 = asphalt grey (backdrop = the road itself), 1 = grass green,
99
+ * 2 = white (markings + clouds + text), 3 = dark speck, 4 = HUD navy */
100
+ 0x15, 0x08, 0x3F, 0x0A, 0x20, 0x00, 0x00, 0x00,
37
101
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
38
- /* Sprite palette: white (1), red (2) */
39
- 0x00, 0x3F, 0x03, 0x00, 0x00, 0x00, 0x00, 0x00,
102
+ /* Sprites: 1 = white (P1 car + HUD digits), 2 = red (P2 car + traffic),
103
+ * 3 = gold (checkpoint flash, unused-by-default). One shared sprite palette
104
+ * on SMS — per-"car" colour means per-TILE colour indices. */
105
+ 0x00, 0x3F, 0x03, 0x0F, 0x00, 0x00, 0x00, 0x00,
40
106
  0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
41
107
  };
42
108
 
43
- /* Three BG tiles for the track, loaded into the BG tile bank at $0000:
44
- * tile 0 = blank (backdrop), tile 1 = grass (colour 1), tile 2 = road
45
- * (colour 2). The track fills the whole 32x24 SMS screen so the display
46
- * is a clear road scene, not a flat backdrop. */
47
- static const uint8_t bg_tiles[96] = {
48
- /* BG tile 0 = blank */
49
- 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
50
- 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
51
- /* BG tile 1 = grass (colour 1 -> plane 0 set) */
52
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
53
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
54
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
55
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
56
- /* BG tile 2 = road (colour 2 -> plane 1 set) */
57
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
58
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
59
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
60
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
109
+ /* ── GAME LOGIC (clay) BG tile inventory (BG bank $0000) ───────────────────
110
+ * tile 0 = blank (shows the colour-0 asphalt backdrop = the road)
111
+ * tiles 1..37 = font: digits 0-9, A-Z, '-' (uploaded 1bpp→4bpp below)
112
+ * tile 38 = grass (colour 1)
113
+ * tile 39 = tuft (grass + speck, colour 1/3)
114
+ * tile 40 = solid HUD bar (colour 4) — the split seam hides in it
115
+ * tile 41 = tarmac speck (colour 3 dot on asphalt)
116
+ * tile 42 = solid shoulder/divider line (colour 2 = white)
117
+ * tile 43 = dashed lane line (colour 2, 4 px on / 4 off) */
118
+ #define FONT_BASE 1
119
+ #define BG_GRASS 38
120
+ #define BG_TUFT 39
121
+ #define BG_HUDBAR 40
122
+ #define BG_SPECK 41
123
+ #define BG_EDGE 42
124
+ #define BG_DASH 43
125
+
126
+ /* 1bpp font (same glyph set as the platformer/shmup examples — 0-9, A-Z, '-').
127
+ * Expanded to the SMS's 32-byte 4bpp tiles at upload (see load_font), so the
128
+ * ROM carries 296 bytes instead of 1184. */
129
+ static const uint8_t font8[37][8] = {
130
+ /* 0-9 */
131
+ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
132
+ {0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
133
+ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
134
+ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
135
+ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
136
+ /* A-Z */
137
+ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
138
+ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
139
+ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
140
+ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3E,0x00}, {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
141
+ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00}, {0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00},
142
+ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
143
+ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
144
+ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
145
+ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
146
+ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
147
+ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
148
+ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
149
+ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
150
+ /* '-' */
151
+ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
61
152
  };
62
153
 
63
- /* Paint the whole 32x24 SMS screen: grey road down the centre lanes,
64
- * green grass on the shoulders. BG tile bank is $0000. The road spans the
65
- * three lanes (player X 72..184 -> roughly cols 8..23). */
66
- static void draw_track(void) {
67
- uint8_t row, col;
68
- for (row = 0; row < 24; row++) {
69
- for (col = 0; col < 32; col++) {
70
- uint8_t road = (col >= 8 && col <= 23);
71
- sms_set_tilemap_cell(row, col, road ? 2 : 1, 0);
154
+ /* Expand 1bpp glyphs into 4bpp SMS tiles as colour 2 (plane 1 set).
155
+ * SMS tile rows are 4 bytes: plane0, plane1, plane2, plane3. */
156
+ static void load_font(void) {
157
+ uint8_t g, r, bits;
158
+ sms_vdp_set_addr((uint16_t)(FONT_BASE * 32), VDP_VRAM_WRITE);
159
+ for (g = 0; g < 37; g++) {
160
+ for (r = 0; r < 8; r++) {
161
+ bits = font8[g][r];
162
+ PORT_VDP_DATA = 0; /* plane 0 */
163
+ PORT_VDP_DATA = bits; /* plane 1 → colour index 2 (white) */
164
+ PORT_VDP_DATA = 0; /* plane 2 */
165
+ PORT_VDP_DATA = 0; /* plane 3 */
72
166
  }
73
167
  }
74
168
  }
75
169
 
76
- /* Two sprite tiles player (colour 1) + enemy (colour 2). */
77
- static const uint8_t tiles[64] = {
78
- /* Tile 0 = player car (colour 1 plane 0 set) */
79
- 0x3C,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00,
80
- 0x42,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00,
81
- 0x7E,0x00,0x00,0x00, 0x42,0x00,0x00,0x00,
82
- 0x7E,0x00,0x00,0x00, 0x66,0x00,0x00,0x00,
83
- /* Tile 1 = enemy car (colour 2 → plane 1 set) */
84
- 0x00,0x3C,0x00,0x00, 0x00,0x7E,0x00,0x00,
85
- 0x00,0x42,0x00,0x00, 0x00,0x7E,0x00,0x00,
86
- 0x00,0x7E,0x00,0x00, 0x00,0x42,0x00,0x00,
87
- 0x00,0x7E,0x00,0x00, 0x00,0x66,0x00,0x00,
170
+ /* Road/roadside/HUD-bar tiles (4bpp, 32 bytes each rows of plane0..3). */
171
+ static const uint8_t deco_tiles[192] = {
172
+ /* BG_GRASS: solid colour 1 (plane 0) */
173
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
174
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
175
+ /* BG_TUFT: grass (colour 1) with a couple speck dots (colour 3 = planes 0+1) */
176
+ 0xFF,0x00,0x00,0x00, 0xFF,0x18,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x42,0x00,0x00,
177
+ 0xFF,0x00,0x00,0x00, 0xFF,0x18,0x00,0x00, 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
178
+ /* BG_HUDBAR: solid colour 4 (binary 100 → plane 2 only) — the split seam
179
+ * lands inside this row */
180
+ 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
181
+ 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
182
+ /* BG_SPECK: asphalt (colour 0) with a few colour-3 specks so the scroll is
183
+ * readable on the otherwise-flat road (plane 0+1 dots) */
184
+ 0x00,0x00,0x00,0x00, 0x10,0x10,0x00,0x00, 0x00,0x00,0x00,0x00, 0x02,0x02,0x00,0x00,
185
+ 0x00,0x00,0x00,0x00, 0x08,0x08,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
186
+ /* BG_EDGE: solid white vertical stripe (colour 2 = plane 1), 2 px wide */
187
+ 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00,
188
+ 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00,
189
+ /* BG_DASH: dashed lane line — 4 px white (colour 2) on, 4 off, stacked */
190
+ 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00, 0x00,0x18,0x00,0x00,
191
+ 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
88
192
  };
89
193
 
90
- typedef struct { uint8_t x, y, alive; } Car;
194
+ /* Sprite tiles (sprite bank $2000 vdp_init's R6=0xFF baseline reads sprite
195
+ * patterns from $2000, so upload there, not $0000).
196
+ * T_CAR — player car, nose up, colour 1 (white)
197
+ * T_TRAFFIC — slow traffic, tail up, colour 2 (red)
198
+ * T_DIGIT0 — 3x5 HUD digits 0-9 (sprites, colour 1) on the fixed top line */
199
+ static const uint8_t sprite_tiles[(2 + 10) * 32] = {
200
+ /* T_CAR (white, plane 0) */
201
+ 0x18,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00, 0x5A,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00,
202
+ 0x3C,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00, 0x5A,0x00,0x00,0x00, 0x66,0x00,0x00,0x00,
203
+ /* T_TRAFFIC (red, plane 1) */
204
+ 0x00,0x66,0x00,0x00, 0x00,0x5A,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x00,0x3C,0x00,0x00,
205
+ 0x00,0x7E,0x00,0x00, 0x00,0x5A,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x00,0x18,0x00,0x00,
206
+ /* T_DIGIT0..9 — compact 3x5 white digits (plane 0) on a fixed HUD scanline */
207
+ /* 0 */ 0xE0,0,0,0, 0xA0,0,0,0, 0xA0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
208
+ /* 1 */ 0x40,0,0,0, 0xC0,0,0,0, 0x40,0,0,0, 0x40,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
209
+ /* 2 */ 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0x80,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
210
+ /* 3 */ 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
211
+ /* 4 */ 0xA0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0x20,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
212
+ /* 5 */ 0xE0,0,0,0, 0x80,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
213
+ /* 6 */ 0xE0,0,0,0, 0x80,0,0,0, 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
214
+ /* 7 */ 0xE0,0,0,0, 0x20,0,0,0, 0x20,0,0,0, 0x40,0,0,0, 0x40,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
215
+ /* 8 */ 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
216
+ /* 9 */ 0xE0,0,0,0, 0xA0,0,0,0, 0xE0,0,0,0, 0x20,0,0,0, 0xE0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
217
+ };
218
+ #define T_CAR 0
219
+ #define T_TRAFFIC 1
220
+ #define T_DIGIT0 2 /* sprite tiles 2..11 = digits 0..9 */
221
+
222
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
223
+ * Road geometry. Four 4-cell-wide lanes between shoulders, a solid center
224
+ * divider (also the 2P territory line). Name-table columns (cells):
225
+ * 8 = left shoulder, 12/20 = dashed lane lines, 16 = center divider,
226
+ * 24 = right shoulder; grass outside. */
227
+ #define COL_EDGE_L 8
228
+ #define COL_DASH_1 12
229
+ #define COL_DIVIDER 16
230
+ #define COL_DASH_2 20
231
+ #define COL_EDGE_R 24
232
+ /* Lane center X for the 8px-wide car sprite (lane i spans 32 px). */
233
+ static const uint8_t lane_x[4] = { 76, 108, 140, 172 };
234
+
235
+ #define MAX_TRAFFIC 6
236
+ #define CAR_Y 160 /* both players' fixed screen Y */
237
+ #define SPAWN_Y 32 /* traffic entry Y — below the HUD strip */
238
+ #define DESPAWN_Y 184 /* traffic exits past the player */
239
+ #define START_LIVES 3 /* crashes per run / per player */
240
+ #define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic moves
241
+ * at road speed, so per-meter density stays
242
+ * constant whatever the player does */
243
+ #define SPEED_2P 2 /* fixed road speed in versus (one VDP =
244
+ * one scroll = one shared speed) */
245
+ #define MAX_SPEED 4 /* px/frame — MUST stay under 8: the row
246
+ * streamer restamps one row per crossing and a
247
+ * >8 px step could skip a row */
248
+
249
+ /* HUD strip: rows 0-2 of the screen. Row 0 holds nothing in the BG (the
250
+ * sprite digits ride there); row 2 is the solid bar where the split seam
251
+ * hides. The 3-row strip is held un-swayed by the line-IRQ split. */
252
+ #define HUD_ROWS 3
253
+ #define HUD_PX (HUD_ROWS * 8)
254
+ #define HUD_Y 1 /* sprite-HUD scanline (top of the fixed band) */
255
+
256
+ /* Players: index 0 = P1 (port A), 1 = P2 (port B — versus only). */
257
+ static uint8_t car_lane[2];
258
+ static uint8_t car_active[2];
259
+ static uint8_t crashes_left[2];
260
+ static uint8_t invuln[2]; /* post-crash blink/no-collide frames */
261
+ static uint8_t prev_pad[2];
262
+ static uint8_t lane_min[2], lane_max[2]; /* 2P: split territories */
263
+ static uint8_t two_player;
264
+ static uint8_t winner; /* versus result: 0 = P1, 1 = P2 */
265
+
266
+ static uint8_t traffic_alive[MAX_TRAFFIC];
267
+ static uint8_t traffic_lane[MAX_TRAFFIC];
268
+ static uint8_t traffic_y[MAX_TRAFFIC];
269
+
270
+ static uint8_t speed; /* road px/frame, 1..MAX_SPEED */
271
+ static uint16_t dist; /* 1P distance, 1 unit = 16 scrolled px */
272
+ static uint8_t dist_frac;
273
+ static uint16_t best; /* persisted best 1P distance */
274
+ static uint8_t spawn_timer;
275
+ static uint8_t road_scroll; /* R9 vertical scroll, ALWAYS kept 0..223 */
276
+ static uint8_t prev_top_row; /* last restamped name-table row */
277
+ static uint8_t start_pause; /* freeze frames at green light */
278
+ static uint8_t hud_dirty; /* lives/speed changed → restage sprite HUD */
279
+ static uint8_t over_step; /* result text, one piece per vblank */
280
+ static uint16_t rng = 0xC0DE;
91
281
 
92
- static Car player;
93
- static Car obstacles[MAX_OBSTACLES];
94
- static uint16_t score;
95
- static uint8_t spawn_timer;
96
- static uint8_t game_over_timer;
97
- static uint8_t prev_pad;
98
- static uint8_t player_lane;
282
+ /* HUD digit cache — SDCC's 16-bit div/mod helpers cost hundreds of cycles
283
+ * each; recompute the 5 distance digits only when dist actually changes. */
284
+ static uint8_t hud_digits[5];
285
+ static uint16_t hud_cached = 0xFFFF;
99
286
 
100
- static const uint8_t lane_x[3] = { LANE_LEFT_X, LANE_MID_X, LANE_RIGHT_X };
287
+ /* Game states the shell every example shares: title → play → game over. */
288
+ #define ST_TITLE 0
289
+ #define ST_PLAY 1
290
+ #define ST_OVER 2
291
+ static uint8_t state;
101
292
 
102
- static uint8_t aabb(Car *a, Car *b) {
103
- return a->x < b->x + 8 && a->x + 8 > b->x
104
- && a->y < b->y + 8 && a->y + 8 > b->y;
293
+ /* ── GAME LOGIC (clay) xorshift16 PRNG (~tens of cycles per call) ── */
294
+ static uint8_t random8(void) {
295
+ uint16_t r = rng;
296
+ r ^= r << 7;
297
+ r ^= r >> 9;
298
+ r ^= r << 8;
299
+ rng = r;
300
+ return (uint8_t)r;
105
301
  }
106
302
 
107
- static void reset_run(void) {
108
- uint8_t i;
109
- player_lane = 1;
110
- player.x = lane_x[1];
111
- player.y = PLAYER_Y;
112
- player.alive = 1;
113
- for (i = 0; i < MAX_OBSTACLES; i++) obstacles[i].alive = 0;
114
- score = 0;
115
- spawn_timer = 0;
116
- game_over_timer = 0;
303
+ static uint8_t dist8(uint8_t a, uint8_t b) {
304
+ return (a > b) ? (uint8_t)(a - b) : (uint8_t)(b - a);
305
+ }
306
+
307
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
308
+ * WHOLE-PLANE VERTICAL ROAD SCROLL — R9, the SMS road. The vertical-scroll
309
+ * register R9 scrolls the ENTIRE name-table plane up/down; screen line y shows
310
+ * plane line (y + R9) mod (plane height), so DECREMENTING R9 slides the road
311
+ * DOWN — the driving-up illusion — for the cost of ONE register write per
312
+ * frame. Zero tilemap writes for the motion itself (rewriting the tilemap in
313
+ * the loop is the #1 "choppy movement" bug).
314
+ *
315
+ * TWO twists this game is built around (vs the Genesis donor's plain u16):
316
+ * 1. R9 IS LATCHED ONCE PER FRAME. The VDP samples R9 at the start of the
317
+ * active display and ignores mid-frame writes until the next frame. So a
318
+ * vertical scroll is a per-FRAME whole-plane move — you cannot split it
319
+ * mid-screen the way you split X-scroll (R8). (That's why the fixed HUD
320
+ * below uses SPRITE glyphs + an R8 sway split, not a mid-frame R9 swap.)
321
+ * Always write R9 in vblank.
322
+ * 2. R9 WRAPS AT 224, NOT 256. In 192-line mode the name table is 32x28 =
323
+ * 224 px tall; R9 values 224-255 make the VDP fetch the unused rows 28-31
324
+ * (garbage). Plain uint8 math happily produces 224-255, so EVERY change
325
+ * to road_scroll goes through this helper. (The NES analog wraps at 240;
326
+ * the Genesis plane is 32x32 = 256 and wraps in hardware for free.)
327
+ * Scrolling DOWN = the road slides toward the player = R9 DECREASES. */
328
+ #define PLANE_H 224
329
+ static void scroll_road_down(uint8_t px) {
330
+ if (road_scroll >= px) road_scroll = (uint8_t)(road_scroll - px);
331
+ else road_scroll = (uint8_t)(road_scroll + PLANE_H - px);
332
+ sms_vdp_write_reg(9, road_scroll); /* commit vertical scroll (vblank only) */
333
+ }
334
+
335
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
336
+ * STREAMED ROADSIDE ROWS. As R9 shrinks, name-table rows recycle into the top
337
+ * of the screen; the road row entering at the top is plane row (R9 >> 3) mod
338
+ * 28. The moment it changes we restamp THAT ONE row's roadside cells with
339
+ * fresh random scenery, so the 224-px loop never shows the same grass twice.
340
+ * Three rules:
341
+ * 1. Restamp in VBLANK only (this game's main loop calls it right after the
342
+ * vblank wait): raw VRAM writes during active display race the VDP's own
343
+ * fetches and drop/garble bytes on real hardware.
344
+ * 2. The restamped row enters UNDER the HUD strip, which hides the swap.
345
+ * Restamp rows lower and the player sees tiles pop.
346
+ * 3. Road speed stays under 8 px/frame (MAX_SPEED) so a frame never skips a
347
+ * whole row crossing. */
348
+ static void stream_road_row(uint8_t row) {
349
+ uint8_t r;
350
+ /* Left shoulder grass band (cols 2,5) + right shoulder (cols 27,30). */
351
+ r = random8(); sms_set_tilemap_cell(row, 2, (r & 7) == 0 ? BG_TUFT : BG_GRASS, 0);
352
+ r = random8(); sms_set_tilemap_cell(row, 5, (r & 7) == 0 ? BG_TUFT : BG_GRASS, 0);
353
+ r = random8(); sms_set_tilemap_cell(row, 27, (r & 7) == 0 ? BG_TUFT : BG_GRASS, 0);
354
+ r = random8(); sms_set_tilemap_cell(row, 30, (r & 7) == 0 ? BG_TUFT : BG_GRASS, 0);
355
+ }
356
+
357
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
358
+ * LINE-INTERRUPT FIXED-HUD SPLIT + per-strip road SWAY. The VDP has ONE scroll
359
+ * register pair for the whole frame; the line interrupt lets you change the X
360
+ * scroll (R8) MID-FRAME (R8 is sampled per line — R9 is NOT, see above). Where
361
+ * the NES needs the sprite-0-hit HACK (park a sprite, busy-poll, burn
362
+ * scanlines), the SMS has a real, PROGRAMMABLE line counter:
363
+ *
364
+ * R10 = N line counter: down-counter reloaded with N every line
365
+ * outside the active area; underflow → IRQ at line N.
366
+ * R0 bit 4 (IE1) line-IRQ enable (already set in vdp_init's 0x36 baseline).
367
+ * R1 bit 5 (IE0) frame(vblank)-IRQ enable (set by sms_vdp_display_on's 0xE0).
368
+ *
369
+ * Both IRQs land on the Z80 IM-1 vector at $0038. The crt0 handler does the
370
+ * minimal handshake: push af / in a,($BF) / pop af / ei / reti — the status
371
+ * read ACKS the VDP (clears BOTH flags; skip it and the IRQ line stays
372
+ * asserted = interrupt storm), and EI must precede RETI.
373
+ *
374
+ * The handler does no work, so the MAIN loop syncs with HALT: sleep until an
375
+ * interrupt, then read the V-counter (port $7E) to learn WHICH one woke us —
376
+ * line IRQs fire in the active area (V < 0xC0), the frame IRQ at vblank
377
+ * (V ≥ 0xC0). Here the road scrolls VERTICALLY (R9, whole plane), so we cannot
378
+ * keep the HUD's name-table rows still by splitting R9. Instead:
379
+ * - HUD GLYPHS ARE SPRITES on the fixed top scanline (immune to R9).
380
+ * - The split holds the top HUD band at R8 = 0 (un-swayed), then below the
381
+ * bar applies a per-strip horizontal SWAY so the road bends left/right
382
+ * ahead of the player — a real raster road-curve effect. Fixed HUD band
383
+ * on top, curving road below.
384
+ *
385
+ * wait_vblank(): sleep to the frame IRQ → R8 = 0 (HUD band un-swayed) and do
386
+ * per-frame VRAM work.
387
+ * wait_split(): sleep to the line IRQ at the bottom of the HUD bar (R10 =
388
+ * HUD_PX-1) → from here down, the active loop pushes the sway
389
+ * value into R8 as the road draws. (We set a single sway value
390
+ * per frame here; reshape into a per-line table for a deeper
391
+ * curve, budgeting OUTs against the line time.)
392
+ *
393
+ * FOOTGUN — you cannot poll once IRQs are on: a status-port poll races the
394
+ * ISR, which always wins and eats the flag, hanging the poll forever. HALT +
395
+ * V-counter is the IRQ-era replacement.
396
+ *
397
+ * Requires: R10 programmed, IE1 + IE0 enabled, EI executed once after
398
+ * display-on, the crt0 ack-only ISR, and wait_vblank/wait_split called EVERY
399
+ * frame in this order. R10 reloads after each underflow, so the line IRQ
400
+ * re-fires every HUD_PX lines down the frame — the later wakes harmlessly
401
+ * interrupt game logic (the ISR acks them) and we re-halt in the NEXT
402
+ * wait_vblank(). */
403
+ #define SPLIT_LINE (HUD_PX - 1)
404
+ static int8_t sway; /* current road horizontal sway, ±a few px */
405
+ static uint8_t sway_phase;
406
+ static const int8_t sway_wave[8] = { 0, 1, 2, 2, 0, -2, -2, -1 };
407
+
408
+ static void wait_vblank(void) {
409
+ /* check-first: if game logic overran into vblank, don't sleep a frame */
410
+ while (PORT_V_COUNTER < 0xC0) { __asm__("halt"); }
411
+ sms_vdp_write_reg(8, 0); /* HUD band renders with X scroll 0 */
412
+ }
413
+
414
+ static void wait_split(void) {
415
+ /* halt-first: vblank work always ends inside vblank (V ≥ 0xC0), and the
416
+ * first wake at V < 0xC0 is the line IRQ at SPLIT_LINE */
417
+ do { __asm__("halt"); } while (PORT_V_COUNTER >= 0xC0);
418
+ sms_vdp_write_reg(8, (uint8_t)sway); /* road below the bar sways */
419
+ }
420
+
421
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
422
+ * BEST-DISTANCE in Sega-mapper cart RAM. The Sega mapper's control register at
423
+ * $FFFC: bit 3 maps the cart's 8KB battery RAM into $8000-$BFFF (bank slot 2).
424
+ * Map → copy → unmap; keep the window short so stray pointer bugs can't shred
425
+ * the save. The block is magic + value + checksum so a never-written cart (all
426
+ * $FF) reads back as "no save" instead of a garbage best.
427
+ *
428
+ * NOTE the $FFFC address: it's IN the WRAM mirror ($C000-$DFFF mirrors at
429
+ * $E000-$FFFF), so this write also lands in WRAM at $DFFC — the mapper just
430
+ * snoops the bus. That's why the crt0 parks SP at $DFF0.
431
+ *
432
+ * HONESTY (verified against the bundled gpgx core): gpgx only instantiates the
433
+ * Sega mapper for ROMs LARGER than 48KB; this build pipeline emits 32KB ROMs,
434
+ * so in-emulator the $8000 window stays open-bus (reads $FF), the magic check
435
+ * fails, and the game falls back to the WRAM best (in-session only). The code
436
+ * is the correct real-hardware idiom and lights up unchanged on a >48KB build
437
+ * or a cart with battery RAM: the load path is self-falsifying, never wrong. */
438
+ #define MAPPER_CTRL (*(volatile uint8_t *)0xFFFC)
439
+ #define CART_RAM ((volatile uint8_t *)0x8000)
440
+
441
+ static void best_save(uint16_t v) {
442
+ uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
443
+ MAPPER_CTRL = 0x08; /* map cart RAM at $8000 */
444
+ CART_RAM[0] = 0x42; /* 'B' */
445
+ CART_RAM[1] = 0x44; /* 'D' */
446
+ CART_RAM[2] = lo;
447
+ CART_RAM[3] = hi;
448
+ CART_RAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
449
+ MAPPER_CTRL = 0x00; /* back to ROM in slot 2 */
450
+ }
451
+
452
+ static uint16_t best_load(void) {
453
+ uint16_t v = 0;
454
+ MAPPER_CTRL = 0x08;
455
+ if (CART_RAM[0] == 0x42 && CART_RAM[1] == 0x44 &&
456
+ CART_RAM[4] == (uint8_t)(CART_RAM[2] ^ CART_RAM[3] ^ 0xA5)) {
457
+ v = (uint16_t)(CART_RAM[2] | ((uint16_t)CART_RAM[3] << 8));
458
+ }
459
+ MAPPER_CTRL = 0x00;
460
+ return v;
461
+ }
462
+
463
+ /* ── GAME LOGIC (clay) — text via the font tiles (BG name table) ─────────────
464
+ * These write the name table directly, so call them only during vblank (or
465
+ * with the display off): VRAM access during active display races the VDP's
466
+ * own fetches and drops/garbles bytes on real hardware. */
467
+ static uint8_t font_tile(char ch) {
468
+ if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE + (ch - '0'));
469
+ if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE + 10 + (ch - 'A'));
470
+ if (ch == '-') return (uint8_t)(FONT_BASE + 36);
471
+ return 0; /* space → blank tile */
472
+ }
473
+
474
+ static void text_draw(uint8_t row, uint8_t col, const char *s) {
475
+ while (*s) sms_set_tilemap_cell(row, col++, font_tile(*s++), 0);
476
+ }
477
+
478
+ static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
479
+ uint8_t d[5], i;
480
+ for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
481
+ for (i = 0; i < 5; i++)
482
+ sms_set_tilemap_cell(row, (uint8_t)(col + i), (uint8_t)(FONT_BASE + d[4 - i]), 0);
483
+ }
484
+
485
+ /* ── GAME LOGIC (clay) — sprite HUD (digits ride the fixed top scanline) ─────
486
+ * HUD glyphs are SPRITES (immune to the road's R9 vertical scroll), staged
487
+ * into the SAT shadow each frame the HUD changes. Slot map (after the cars +
488
+ * traffic): see stage_sprites. 1P: lives digit + 5-digit distance = 6 sprites
489
+ * on the line; 2P: one crashes-left digit per player = 2. Mind the 8-sprites-
490
+ * PER-SCANLINE limit — traffic spawns BELOW the HUD line so it never shares. */
491
+ static uint8_t hud_slot; /* first SAT slot the HUD digits use */
492
+ static void stage_hud_sprites(void) {
493
+ uint8_t i, s = hud_slot;
494
+ if (two_player) {
495
+ sms_sprite_set(s++, 16, HUD_Y, (uint8_t)(T_DIGIT0 + crashes_left[0]));
496
+ sms_sprite_set(s++, 232, HUD_Y, (uint8_t)(T_DIGIT0 + crashes_left[1]));
497
+ return;
498
+ }
499
+ sms_sprite_set(s++, 16, HUD_Y, (uint8_t)(T_DIGIT0 + crashes_left[0]));
500
+ if (dist != hud_cached) { /* recompute digits only on change */
501
+ uint16_t v = dist;
502
+ for (i = 0; i < 5; i++) { hud_digits[4 - i] = (uint8_t)(v % 10); v /= 10; }
503
+ hud_cached = dist;
504
+ }
505
+ for (i = 0; i < 5; i++)
506
+ sms_sprite_set(s++, (uint8_t)(200 + i * 8), HUD_Y, (uint8_t)(T_DIGIT0 + hud_digits[i]));
507
+ }
508
+
509
+ /* ── GAME LOGIC (clay) — paint the road into the name table ──────────────────
510
+ * Whole-screen repaint with the DISPLAY OFF (free VRAM access, clean cut).
511
+ * The dashed lane lines + shoulders are painted ONCE and never touched again:
512
+ * they live in the BG, so R9 moves them with the road for free.
513
+ * PERF FOOTGUN (inherited from the shmup): per-cell sms_set_tilemap_cell
514
+ * redoes the 4-OUT address setup for every cell — over a full screen that's
515
+ * seconds of black. Set the VRAM address ONCE per row (the data port
516
+ * autoincrements) and stream. */
517
+ static uint8_t road_cell(uint8_t r, uint8_t c) {
518
+ if (c == COL_EDGE_L || c == COL_EDGE_R || c == COL_DIVIDER) return BG_EDGE;
519
+ if (c == COL_DASH_1 || c == COL_DASH_2) return BG_DASH;
520
+ if (c > COL_EDGE_L && c < COL_EDGE_R) { /* tarmac */
521
+ return (((uint8_t)(r * 5 + c * 3) % 13) == 0) ? BG_SPECK : 0;
522
+ }
523
+ /* roadside grass + sparse tufts */
524
+ if (((uint8_t)(r * 7 + c * 5) & 7) == 0) return BG_TUFT;
525
+ return BG_GRASS;
526
+ }
527
+
528
+ static void paint_road(void) {
529
+ uint8_t r, c;
530
+ for (r = 0; r < 28; r++) { /* all 28 plane rows (224 px) */
531
+ sms_vdp_set_addr((uint16_t)(0x3800 + (uint16_t)r * 64), VDP_VRAM_WRITE);
532
+ for (c = 0; c < 32; c++) {
533
+ PORT_VDP_DATA = road_cell(r, c); /* name-table entry low byte */
534
+ PORT_VDP_DATA = 0; /* high byte: flips/palette/priority */
535
+ }
536
+ }
537
+ }
538
+
539
+ static void paint_title(void) {
540
+ sms_vdp_display_off();
541
+ paint_road(); /* the road itself is the backdrop */
542
+ text_draw(6, (uint8_t)((32 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
543
+ text_draw(11, 10, "1P RACE - 1");
544
+ text_draw(13, 10, "2P VERSUS - 2");
545
+ text_draw(20, 12, "BEST");
546
+ draw_u16(20, 17, best);
547
+ sms_sprite_init(); /* park every sprite off-screen */
548
+ sms_sat_upload();
549
+ road_scroll = 0;
550
+ sms_vdp_write_reg(8, 0);
551
+ sms_vdp_write_reg(9, 0);
552
+ sms_vdp_display_on(); /* re-enables the frame IRQ too */
553
+ }
554
+
555
+ static void paint_field(void) {
556
+ uint8_t c;
557
+ sms_vdp_display_off();
558
+ paint_road();
559
+ /* HUD strip rows 0-2: clear the BG under the sprite HUD, lay the solid bar
560
+ * on row 2 where the split seam hides. (These rows scroll with the road via
561
+ * R9 — they're a curtain the streamed restamp hides behind, and the sprite
562
+ * digits ride above them.) */
563
+ for (c = 0; c < 32; c++) {
564
+ sms_set_tilemap_cell(0, c, 0, 0);
565
+ sms_set_tilemap_cell(1, c, 0, 0);
566
+ sms_set_tilemap_cell(2, c, BG_HUDBAR, 0);
567
+ }
568
+ sms_sprite_init();
569
+ road_scroll = 0;
570
+ prev_top_row = 0;
571
+ hud_cached = 0xFFFF;
572
+ sms_vdp_write_reg(8, 0);
573
+ sms_vdp_write_reg(9, 0);
574
+ sms_vdp_display_on();
117
575
  }
118
576
 
119
- static void spawn_obstacle(void) {
577
+ /* The result card reuses the live road as its backdrop (a bare single-colour
578
+ * card reads as a render failure — the verify tool flags >92% one-colour
579
+ * frames) and draws its text via deferred over_step pieces in the ST_OVER
580
+ * loop, one per vblank, so each vblank→split budget stays honest. The road
581
+ * keeps scrolling under it. */
582
+
583
+ /* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
584
+ static void spawn_traffic(void) {
120
585
  uint8_t i;
121
- for (i = 0; i < MAX_OBSTACLES; i++) {
122
- if (!obstacles[i].alive) {
123
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
124
- obstacles[i].y = 0;
125
- obstacles[i].alive = 1;
586
+ for (i = 0; i < MAX_TRAFFIC; i++) {
587
+ if (!traffic_alive[i]) {
588
+ traffic_alive[i] = 1;
589
+ traffic_lane[i] = random8() & 3;
590
+ traffic_y[i] = SPAWN_Y;
126
591
  return;
127
592
  }
128
593
  }
129
594
  }
130
595
 
131
- void main(void) {
596
+ /* AABB, both boxes 8x8. */
597
+ static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
598
+ return dist8(ax, bx) < 8 && dist8(ay, by) < 8;
599
+ }
600
+
601
+ /* ── GAME LOGIC (clay) — start a run / end a run ── */
602
+ static void start_game(uint8_t versus) {
132
603
  uint8_t i;
133
- sms_vdp_init();
604
+ two_player = versus;
605
+ for (i = 0; i < MAX_TRAFFIC; i++) traffic_alive[i] = 0;
606
+ for (i = 0; i < 2; i++) {
607
+ crashes_left[i] = START_LIVES;
608
+ invuln[i] = 0;
609
+ prev_pad[i] = 0xFF; /* swallow buttons held across the change */
610
+ }
611
+ if (versus) {
612
+ car_active[0] = 1; car_active[1] = 1;
613
+ lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
614
+ lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
615
+ speed = SPEED_2P; /* shared road, fixed speed (see header) */
616
+ } else {
617
+ car_active[0] = 1; car_active[1] = 0;
618
+ lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
619
+ speed = 1;
620
+ }
621
+ dist = 0; dist_frac = 0;
622
+ spawn_timer = 0;
623
+ sway = 0; sway_phase = 0;
624
+ start_pause = 30; /* green-light breather */
625
+ paint_field(); /* display-off repaint — safe */
626
+ hud_slot = (uint8_t)(2 + MAX_TRAFFIC); /* cars=0,1; traffic=2..7; HUD=8.. */
627
+ hud_dirty = 1;
628
+ sfx_tone(0, 214, 8); /* start jingle (C5) */
629
+ state = ST_PLAY;
630
+ }
631
+
632
+ static void game_over(void) {
633
+ if (!two_player && dist > best) {
634
+ best = dist;
635
+ best_save(best); /* cart RAM (real hardware); WRAM copy live */
636
+ }
637
+ sfx_noise(20);
638
+ state = ST_OVER;
639
+ over_step = 5; /* result text, one piece per vblank */
640
+ }
641
+
642
+ /* ── GAME LOGIC (clay) — crash rules ── */
643
+ static void crash(uint8_t p) {
644
+ sfx_noise(14);
645
+ invuln[p] = 60; /* blink + no-collide grace */
646
+ if (!two_player) speed = 1; /* a wreck kills your momentum */
647
+ if (crashes_left[p] > 0) --crashes_left[p];
648
+ hud_dirty = 1;
649
+ if (crashes_left[p] == 0) {
650
+ winner = (uint8_t)(1 - p); /* versus: the OTHER player wins */
651
+ game_over();
652
+ }
653
+ }
654
+
655
+ /* ── GAME LOGIC (clay) — per-player input ────────────────────────────────────
656
+ * LEFT/RIGHT steer between lanes (edge-detected — held d-pad shouldn't
657
+ * machine-gun across the road). 1P only: button 1/UP accelerate, button 2/DOWN
658
+ * brake (speed is shared in versus — see the design note). P2 is on PORT B. */
659
+ static void update_player(uint8_t p) {
660
+ uint8_t pad = p ? sms_joypad_read_p2() : sms_joypad_read();
661
+ uint8_t pressed = (uint8_t)(pad & ~prev_pad[p]);
662
+ prev_pad[p] = pad;
663
+ if (!car_active[p]) return;
664
+ if ((pressed & JOY_LEFT) && car_lane[p] > lane_min[p]) {
665
+ --car_lane[p];
666
+ sfx_tone(1, 330, 3); /* lane tick */
667
+ }
668
+ if ((pressed & JOY_RIGHT) && car_lane[p] < lane_max[p]) {
669
+ ++car_lane[p];
670
+ sfx_tone(1, 330, 3);
671
+ }
672
+ if (!two_player) { /* speed is shared — only 1P gets it */
673
+ if ((pressed & (JOY_B1 | JOY_UP)) && speed < MAX_SPEED) {
674
+ ++speed;
675
+ sfx_tone(2, (uint16_t)(280 - speed * 30), 8); /* engine rev */
676
+ hud_dirty = 1;
677
+ }
678
+ if ((pressed & (JOY_B2 | JOY_DOWN)) && speed > 1) {
679
+ --speed;
680
+ sfx_tone(2, 500, 5); /* brake blip */
681
+ hud_dirty = 1;
682
+ }
683
+ }
684
+ if (invuln[p] > 0) --invuln[p];
685
+ }
686
+
687
+ /* ── GAME LOGIC (clay) — stage this frame's sprites ──────────────────────────
688
+ * Fixed SAT slots: 0 = P1, 1 = P2, 2..7 = traffic, 8.. = HUD digits. Hidden
689
+ * slots park at Y=$E0 (off-screen, NOT the $D0 terminator — that stops the
690
+ * VDP scanning and blanks every later slot). */
691
+ static void stage_sprites(void) {
692
+ uint8_t i;
693
+ for (i = 0; i < 2; i++) {
694
+ uint8_t vis = (state == ST_PLAY) && car_active[i] && !(invuln[i] & 2);
695
+ sms_sprite_set(i, lane_x[car_lane[i]], vis ? CAR_Y : 0xE0, T_CAR);
696
+ }
697
+ for (i = 0; i < MAX_TRAFFIC; i++) {
698
+ uint8_t vis = (state == ST_PLAY) && traffic_alive[i];
699
+ sms_sprite_set((uint8_t)(2 + i), lane_x[traffic_lane[i]],
700
+ vis ? traffic_y[i] : 0xE0, T_TRAFFIC);
701
+ }
702
+ stage_hud_sprites();
703
+ }
704
+
705
+ void main(void) {
706
+ uint8_t i, p;
707
+ uint8_t top_row;
708
+
709
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
710
+ * Init order: VDP regs (display off) → palette → tiles → name table → SAT →
711
+ * R10 → display on (which also enables the frame IRQ) → EI. The one hard
712
+ * rule: EI comes LAST, after every register is in place — the crt0 boots
713
+ * with DI and the FIRST halt would hang forever if interrupts were never
714
+ * enabled. */
715
+ sms_vdp_init(); /* R0=0x36 already has IE1 (line IRQ) set */
134
716
  sms_load_palette(palette);
135
- sms_load_tiles(0x0000, bg_tiles, 96); /* BG tiles -> BG bank $0000 */
136
- sms_load_tiles(0x2000, tiles, 64); /* sprite tiles -> sprite bank $2000 */
137
- draw_track();
717
+ load_font();
718
+ sms_load_tiles((uint16_t)(BG_GRASS * 32), deco_tiles, sizeof(deco_tiles));
719
+ sms_load_tiles(0x2000, sprite_tiles, sizeof(sprite_tiles));
138
720
  sms_sprite_init();
139
721
  sfx_init();
140
- sms_vdp_display_on();
722
+ music_init();
723
+ music_play(0);
141
724
 
142
- reset_run();
143
- prev_pad = 0;
725
+ /* R10 = SPLIT_LINE arms the line counter: IRQ at the last bar line. Set
726
+ * once — it reloads itself every underflow. */
727
+ sms_vdp_write_reg(10, SPLIT_LINE);
144
728
 
145
- do {
146
- uint8_t pad;
147
- uint8_t slot;
148
- int16_t step;
149
- sms_vblank_wait();
150
- sfx_update();
729
+ best = best_load(); /* cart RAM if present — else 0 */
730
+ state = ST_TITLE;
731
+ hud_slot = (uint8_t)(2 + MAX_TRAFFIC);
732
+ paint_title();
733
+ __asm__("ei"); /* interrupts live from here on */
734
+
735
+ for (;;) {
736
+ /* Advance the per-frame sway wave (used below the HUD split). */
737
+ sway_phase++;
738
+ sway = sway_wave[(uint8_t)((sway_phase >> 2) & 7)];
151
739
 
152
- /* Stage SAT. */
153
- slot = 0;
154
- sms_sprite_set(slot++, player.x, player.y, 0 /* player tile */);
155
- for (i = 0; i < MAX_OBSTACLES; i++) {
156
- uint8_t ey = obstacles[i].alive ? obstacles[i].y : 0xE0;
157
- sms_sprite_set(slot++, obstacles[i].x, ey, 1 /* enemy tile */);
740
+ if (state == ST_TITLE) {
741
+ /* ── GAME LOGIC (clay) — title: button 1 = 1P race, button 2 = 2P versus.
742
+ * The road idles under the title card so the screen sells the scroll
743
+ * before anyone presses a button. */
744
+ wait_vblank();
745
+ scroll_road_down(1);
746
+ top_row = (uint8_t)((road_scroll >> 3) % 28);
747
+ if (top_row != prev_top_row) { prev_top_row = top_row; stream_road_row(top_row); }
748
+ sfx_update();
749
+ music_update();
750
+ wait_split();
751
+ {
752
+ uint8_t pad = sms_joypad_read();
753
+ if ((pad & JOY_B1) && !(prev_pad[0] & JOY_B1)) start_game(0);
754
+ else if ((pad & JOY_B2) && !(prev_pad[0] & JOY_B2)) start_game(1);
755
+ prev_pad[0] = pad;
756
+ }
757
+ continue;
158
758
  }
159
- sms_sat_upload();
160
759
 
161
- pad = sms_joypad_read();
760
+ if (state == ST_OVER) {
761
+ /* Freeze the road; deferred result text, one piece per vblank. Button 1
762
+ * or 2 returns to the title. */
763
+ wait_vblank();
764
+ if (over_step) {
765
+ if (over_step == 5) {
766
+ if (two_player) text_draw(8, 12, winner ? "P2 WINS" : "P1 WINS");
767
+ else text_draw(8, 12, "WRECKED");
768
+ } else if (over_step == 4) {
769
+ if (two_player) text_draw(11, 9, "RIVAL WRECKED");
770
+ else text_draw(11, 12, "DIST");
771
+ } else if (over_step == 3) {
772
+ if (!two_player) draw_u16(11, 17, dist);
773
+ } else if (over_step == 2) {
774
+ if (!two_player) text_draw(13, 12, "BEST");
775
+ } else {
776
+ if (!two_player) draw_u16(13, 17, best);
777
+ }
778
+ over_step--;
779
+ if (over_step == 0) text_draw(20, 10, "PRESS - 1 OR 2");
780
+ }
781
+ sfx_update();
782
+ music_update();
783
+ wait_split();
784
+ {
785
+ uint8_t pad = sms_joypad_read();
786
+ if ((pad & (JOY_B1 | JOY_B2)) && !(prev_pad[0] & (JOY_B1 | JOY_B2))) {
787
+ state = ST_TITLE;
788
+ paint_title();
789
+ }
790
+ prev_pad[0] = pad;
791
+ }
792
+ stage_sprites(); /* park cars/traffic off-screen */
793
+ continue;
794
+ }
795
+
796
+ /* ── ST_PLAY ──────────────────────────────────────────────────────────
797
+ * Frame shape: [vblank: SAT + scroll + streamed row, R8=0] → [line IRQ at
798
+ * the bar: R8=sway] → [rest of frame: game logic]. VRAM traffic stays
799
+ * inside vblank; logic runs while the VDP draws the road.
800
+ *
801
+ * BUDGET FOOTGUN (inherited from the shmup): everything between
802
+ * wait_vblank() and wait_split() must finish before the line IRQ at line
803
+ * 23 — vblank (70 lines) + the HUD strip (23) ≈ 21k cycles, and the SAT
804
+ * upload eats ~7k. The HUD digits are SPRITES (staged into the shadow SAT,
805
+ * uploaded once), and dist digits recompute only when dist changes (see
806
+ * stage_hud_sprites) — so we never blow the budget with division here. */
807
+ wait_vblank();
808
+ sms_sat_upload(); /* shadow SAT staged at end of last frame */
162
809
 
163
- if (game_over_timer > 0) {
164
- game_over_timer--;
165
- if (game_over_timer == 0) reset_run();
166
- prev_pad = pad;
810
+ if (start_pause) { /* green light: freeze gameplay, keep
811
+ * frames honest (scroll idles, sprites
812
+ * staged) */
813
+ --start_pause;
814
+ scroll_road_down(0); /* re-commit R9 (no motion) */
815
+ sfx_update();
816
+ music_update();
817
+ wait_split();
818
+ stage_sprites();
167
819
  continue;
168
820
  }
169
821
 
170
- if ((pad & JOY_LEFT) && !(prev_pad & JOY_LEFT) && player_lane > 0) { player_lane--; sfx_tone(1, 330, 2); }
171
- if ((pad & JOY_RIGHT) && !(prev_pad & JOY_RIGHT) && player_lane < 2) { player_lane++; sfx_tone(1, 330, 2); }
172
- player.x = lane_x[player_lane];
173
- prev_pad = pad;
822
+ scroll_road_down(speed);
823
+ top_row = (uint8_t)((road_scroll >> 3) % 28);
824
+ if (top_row != prev_top_row) { prev_top_row = top_row; stream_road_row(top_row); }
174
825
 
175
- /* Obstacle speed grows with score (cap at 4). */
176
- step = (int16_t)(2 + (score / 500));
177
- if (step > 4) step = 4;
826
+ sfx_update();
827
+ music_update();
828
+ wait_split(); /* the line-interrupt split every frame */
178
829
 
179
- for (i = 0; i < MAX_OBSTACLES; i++) {
180
- if (!obstacles[i].alive) continue;
181
- obstacles[i].y = (uint8_t)(obstacles[i].y + step);
182
- if (obstacles[i].y >= 184) obstacles[i].alive = 0;
830
+ /* ── GAME LOGIC (clay) from here down ── */
831
+ update_player(0);
832
+ if (two_player) update_player(1);
833
+ if (state != ST_PLAY) { stage_sprites(); continue; } /* a crash ended it */
834
+
835
+ /* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every 256
836
+ * units marks a checkpoint. */
837
+ if (!two_player) {
838
+ dist_frac = (uint8_t)(dist_frac + speed);
839
+ if (dist_frac >= 16) {
840
+ dist_frac -= 16;
841
+ if (dist < 65535u) ++dist;
842
+ if (dist != 0 && (dist & 0xFF) == 0) sfx_tone(0, 107, 8); /* C6 chime */
843
+ }
183
844
  }
184
845
 
185
- spawn_timer = (uint8_t)(spawn_timer + 1);
186
- if (spawn_timer >= 36) { spawn_timer = 0; spawn_obstacle(); }
846
+ /* Traffic flows down at road speed (slower cars you overtake); despawn
847
+ * past the player with a little pass tick. */
848
+ for (i = 0; i < MAX_TRAFFIC; i++) {
849
+ if (!traffic_alive[i]) continue;
850
+ traffic_y[i] = (uint8_t)(traffic_y[i] + speed);
851
+ if (traffic_y[i] >= DESPAWN_Y) {
852
+ traffic_alive[i] = 0;
853
+ sfx_tone(1, 660, 2);
854
+ }
855
+ }
856
+ if (++spawn_timer >= SPAWN_PERIOD) {
857
+ spawn_timer = 0;
858
+ spawn_traffic();
859
+ }
187
860
 
188
- for (i = 0; i < MAX_OBSTACLES; i++) {
189
- if (obstacles[i].alive && aabb(&player, &obstacles[i])) {
190
- game_over_timer = 60;
191
- sfx_noise(30); /* crash */
192
- break;
861
+ /* Traffic cars. Crash grace: a just-wrecked car blinks and can't collide
862
+ * for 60 frames. */
863
+ for (i = 0; i < MAX_TRAFFIC && state == ST_PLAY; i++) {
864
+ if (!traffic_alive[i]) continue;
865
+ for (p = 0; p < 2; p++) {
866
+ if (!car_active[p] || invuln[p]) continue;
867
+ if (hits(lane_x[traffic_lane[i]], traffic_y[i], lane_x[car_lane[p]], CAR_Y)) {
868
+ traffic_alive[i] = 0;
869
+ crash(p);
870
+ break;
871
+ }
193
872
  }
194
873
  }
195
874
 
196
- if (score < 65500u) score = (uint16_t)(score + 1);
197
- } while (1);
875
+ stage_sprites(); /* stage the SAT shadow for next vblank */
876
+ }
198
877
  }