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,245 +1,780 @@
1
- /* ── racing.c — Genesis SGDK top-down racing scaffold ──────────────
1
+ /* ── racing.c — Genesis top-down road racer (complete example game) ──────────
2
2
  *
3
- * Endless top-down lane racer. Player car at the bottom of the screen,
4
- * three lanes, obstacles spawn from the top and slide down. Left/Right
5
- * D-pad switches lanes. Survive as long as possible score is the
6
- * frame count since the last collision.
3
+ * MIRAGE MILE 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 CONTROLLER 2), a vertically-scrolling road done the
6
+ * Genesis way (full-plane hardware VSCROLL), streamed roadside scenery
7
+ * through the DMA queue, crash/lives rules, persistent best distance
8
+ * (cartridge SRAM), music + SFX — and a LIVE per-scanline HSCROLL_LINE
9
+ * heat-haze band shimmering across the asphalt, the deluxe scroll variant
10
+ * the platformer template only documents.
7
11
  *
8
- * Game state:
9
- * - Player car (1×1 tile) at fixed Y, X = lane_x[lane]
10
- * - 4 obstacle cars (object pool), each spawning from a random lane
11
- * - Speed grows slightly with score
12
- * - On collision: 60-frame freeze then run resets
12
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
13
+ * very different one. The markers tell you what's what:
14
+ * HARDWARE IDIOM (load-bearing) dodges a documented Genesis footgun;
15
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
16
+ * GAME LOGIC (clay) traffic patterns, speeds, tuning, art: reshape
17
+ * freely.
13
18
  *
14
- * Two-player: pass through the JOY_2 input — port 1 (player 2) shares
15
- * the same lanes (just visually shifted). When no second controller
16
- * is connected, P2 is a "ghost" you can pretend you're racing.
19
+ * What depends on what:
20
+ * genesis_sfx.{h,c} PSG sound wrapper (tones + noise + a background
21
+ * melody loop). For full FM music, see the xgm2_demo template.
22
+ * rom_header.c (SGDK) — the Sega header at $100. Its 'RA' block at $1B0
23
+ * DECLARES the cartridge SRAM that best_load/save below depend on (see
24
+ * the SRAM idiom). The build assembles it automatically.
25
+ *
26
+ * THE DESIGN (read before reshaping):
27
+ * Scrolling — the road is PLANE A, scrolled down by decrementing one
28
+ * vertical-scroll value per frame. Compare the NES version of this
29
+ * game (examples/nes/templates/racing.c): there a nametable is 240 px
30
+ * tall, scroll_y 240-255 fetches attribute bytes as tiles (garbage
31
+ * rows), and every scroll change goes through a wrap helper. The
32
+ * Genesis plane is 256 px tall and the VDP masks the scroll value to
33
+ * the plane IN HARDWARE: `vs -= speed` on a plain u16 is the entire
34
+ * idiom (65536 is a multiple of 256, so overflow is seamless forever).
35
+ * Streamed scenery — rows re-entering at the top get restamped with
36
+ * fresh random roadside through the DMA queue, hidden under the
37
+ * 16-px WINDOW HUD (the same curtain trick the NES game plays with
38
+ * the overscan-cropped top band).
39
+ * Heat haze — HSCROLL_LINE mode: the VDP fetches one hscroll entry PER
40
+ * SCANLINE, so a 32-line band of the road ripples ±2 px in a moving
41
+ * wave while the rest of the screen holds still. 64 bytes/frame of
42
+ * vblank DMA. Sprites are NOT displaced — per-line hscroll bends
43
+ * planes only.
44
+ * HUD — the WINDOW plane: a hardware-fixed status bar that ignores all
45
+ * scrolling (no raster tricks needed — one register).
46
+ * 2P VERSUS — ONE VDP means ONE road scroll, so both players share one
47
+ * road at a fixed speed and only steer (the same constraint the NES
48
+ * version explains): solid center divider, P1 (blue, pad 1) owns the
49
+ * left two lanes, P2 (green, pad 2) the right two. Each starts with 3
50
+ * crashes; first to use them all LOSES.
51
+ * 1P RACE — all four lanes, A/UP accelerates, B/DOWN brakes (speed 1-4);
52
+ * 3 crashes end the run. Persistent stat: best DISTANCE (u16, one
53
+ * unit = 16 scrolled pixels ≈ one car length) via best_load/save.
54
+ *
55
+ * Frame budget (NTSC, 60 fps): 6 traffic × 2 cars of AABB, one 64-cell row
56
+ * restamp at most every other frame (128 B), the 32-entry haze table (64 B)
57
+ * and 8 SAT entries (64 B) queued for vblank — ~300 bytes of the ~7 KB
58
+ * H40 vblank DMA ceiling. The 68000 barely notices.
17
59
  */
18
60
 
19
61
  #include <genesis.h>
20
62
  #include "genesis_sfx.h"
21
63
 
22
- #define LANE_LEFT_X 96
23
- #define LANE_MID_X 156
24
- #define LANE_RIGHT_X 216
25
- #define PLAYER_Y 176
26
- #define MAX_OBSTACLES 4
27
-
28
- #define T_CAR_P1 (TILE_USER_INDEX + 0)
29
- #define T_CAR_EN (TILE_USER_INDEX + 1)
30
- #define T_LANE (TILE_USER_INDEX + 2) /* dashed lane divider (BG_B) */
31
- #define T_EDGE (TILE_USER_INDEX + 3) /* solid road edge (BG_B) */
32
- #define T_GRASS (TILE_USER_INDEX + 4) /* roadside backdrop (BG_A) */
33
- #define T_ASPHALT (TILE_USER_INDEX + 5) /* road surface backdrop(BG_A) */
34
-
35
- static const u32 tile_car_p1[8] = {
36
- 0x01111110, 0x11111111, 0x12222221, 0x11111111,
37
- 0x11111111, 0x12222221, 0x11111111, 0x01100110,
64
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
65
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
66
+ #define GAME_TITLE "MIRAGE MILE"
67
+
68
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
69
+ * CONTROLLER MAPPING — two layers, both bite:
70
+ *
71
+ * On the pad: SGDK's JOY_readJoypad(JOY_1/JOY_2) returns BUTTON_A/B/C/
72
+ * START/UP/DOWN/LEFT/RIGHT as a bitmask. Gas is BUTTON_A or UP, brake is
73
+ * BUTTON_B or DOWN (real Genesis racers double the face buttons onto the
74
+ * d-pad so either thumb works).
75
+ *
76
+ * Driving this game HEADLESSLY through an emulator (libretro/gpgx): the
77
+ * core maps Genesis A/B/C onto libretro Y/B/A. So setInput({y:true})
78
+ * presses GENESIS A (gas/1P start here), setInput({b:true}) presses
79
+ * GENESIS B (brake/2P select), and setInput({a:true}) presses GENESIS C —
80
+ * NOT Genesis A. Getting this wrong looks like "the game ignores input".
81
+ * START is start.
82
+ */
83
+ #define BTN_GAS (BUTTON_A | BUTTON_UP)
84
+ #define BTN_BRAKE (BUTTON_B | BUTTON_DOWN)
85
+
86
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
87
+ * Tile art. Genesis tiles are 4bpp: each u32 row = 8 pixels, one hex nibble
88
+ * per pixel = a colour index into the tile's palette line (0 = transparent).
89
+ * Road plane (A) uses PAL1; the HUD band on plane B uses PAL2; sprites pick
90
+ * their line per-sprite (P1 PAL0, traffic PAL2, P2 PAL3) so ONE car tile
91
+ * serves three liveries. */
92
+ #define T_GRASS (TILE_USER_INDEX + 0) /* plane A: roadside base */
93
+ #define T_TUFT (TILE_USER_INDEX + 1) /* plane A: scenery, common */
94
+ #define T_TREE (TILE_USER_INDEX + 2) /* plane A: scenery, rare */
95
+ #define T_ASPHALT (TILE_USER_INDEX + 3) /* plane A: road surface */
96
+ #define T_SPECK (TILE_USER_INDEX + 4) /* plane A: textured asphalt */
97
+ #define T_EDGE (TILE_USER_INDEX + 5) /* plane A: solid shoulder line */
98
+ #define T_DASH (TILE_USER_INDEX + 6) /* plane A: dashed lane line */
99
+ #define T_DIVIDE (TILE_USER_INDEX + 7) /* plane A: double center line */
100
+ #define T_BAND (TILE_USER_INDEX + 8) /* plane B: flat band behind HUD */
101
+ #define T_CAR (TILE_USER_INDEX + 9) /* sprite: player car, nose up */
102
+ #define T_TRAFFIC (TILE_USER_INDEX + 10) /* sprite: slow traffic, tail up */
103
+
104
+ static const u32 tile_grass[8] = { /* speckles make motion visible */
105
+ 0x11111111, 0x11121111, 0x11111111, 0x21111112,
106
+ 0x11111111, 0x11112111, 0x11111111, 0x12111111,
107
+ };
108
+ static const u32 tile_tuft[8] = {
109
+ 0x11111111, 0x11121111, 0x11222111, 0x12222211,
110
+ 0x11222111, 0x11121111, 0x11111111, 0x12111121,
111
+ };
112
+ static const u32 tile_tree[8] = {
113
+ 0x11166111, 0x11666611, 0x16666661, 0x16666661,
114
+ 0x11666611, 0x11122111, 0x11122111, 0x11111111,
38
115
  };
39
- static const u32 tile_car_enemy[8] = {
40
- 0x03333330, 0x33333333, 0x34444443, 0x33333333,
41
- 0x33333333, 0x34444443, 0x33333333, 0x03300330,
116
+ static const u32 tile_asphalt[8] = { /* a flat colour shifted N px */
117
+ 0x44444444, 0x44445444, 0x44444444, /* looks identical to itself — */
118
+ 0x54444444, 0x44444444, 0x44444454, /* the speckle is what makes the */
119
+ 0x44444444, 0x44544444, /* scroll readable */
42
120
  };
43
- /* Dashed lane-divider segment (colour 2 = grey): a 2px dash in the
44
- * centre columns, on/off vertically so a stacked column reads as a
45
- * dashed road centre-line. */
46
- static const u32 tile_lane[8] = {
47
- 0x00022000, 0x00022000, 0x00022000, 0x00000000,
48
- 0x00000000, 0x00022000, 0x00022000, 0x00022000,
121
+ static const u32 tile_speck[8] = {
122
+ 0x44444444, 0x44544444, 0x44455444, 0x44444444,
123
+ 0x44444454, 0x45444444, 0x44444444, 0x44445444,
49
124
  };
50
- /* Solid 2px road-edge stripe (colour 2 = grey) down the right side of
51
- * the tile — used on the left rail; mirrored (hflip) for the right. */
52
- static const u32 tile_edge[8] = {
53
- 0x00000022, 0x00000022, 0x00000022, 0x00000022,
54
- 0x00000022, 0x00000022, 0x00000022, 0x00000022,
125
+ static const u32 tile_edge[8] = { /* solid white shoulder stripe */
126
+ 0x44334444, 0x44334444, 0x44334444, 0x44334444,
127
+ 0x44334444, 0x44334444, 0x44334444, 0x44334444,
55
128
  };
56
- /* Roadside grass (colour 5) with a couple of darker tufts (colour 6) so
57
- * it isn't a flat fill — tiled down both shoulders on BG_A. */
58
- static const u32 tile_grass[8] = {
59
- 0x55555555, 0x55556555, 0x55555555, 0x65555555,
60
- 0x55555555, 0x55555565, 0x55555555, 0x55655555,
129
+ static const u32 tile_dash[8] = { /* 4 px on, 4 off: stacked tiles */
130
+ 0x44433444, 0x44433444, 0x44433444, /* read as a dashed lane line */
131
+ 0x44433444, 0x44444444, 0x44444444,
132
+ 0x44444444, 0x44444444,
61
133
  };
62
- /* Road asphalt (colour 6) with faint speckle (colour 5) — tiled across
63
- * the driving surface on BG_A, behind the BG_B lane markings + cars. */
64
- static const u32 tile_asphalt[8] = {
65
- 0x66666666, 0x66666566, 0x66666666, 0x56666666,
66
- 0x66666666, 0x66666656, 0x66666666, 0x66566666,
134
+ static const u32 tile_divide[8] = { /* double line = 2P territory */
135
+ 0x43344334, 0x43344334, 0x43344334, /* border */
136
+ 0x43344334, 0x43344334, 0x43344334,
137
+ 0x43344334, 0x43344334,
67
138
  };
139
+ static const u32 tile_band[8] = {
140
+ 0x11111111, 0x11111111, 0x11111111, 0x11111111,
141
+ 0x11111111, 0x11111111, 0x11111111, 0x11111111,
142
+ };
143
+ static const u32 tile_car[8] = { /* nose up; 1 = body, 2 = glass */
144
+ 0x00111100, 0x01111110, 0x11222211, 0x11111111,
145
+ 0x01111110, 0x11222211, 0x11111111, 0x01100110,
146
+ };
147
+ static const u32 tile_traffic[8] = { /* tail up (it's slower traffic */
148
+ 0x03300330, 0x33333333, 0x33444433, /* you overtake, so you see its */
149
+ 0x03333330, 0x33333333, 0x33444433, /* rear). Colours 3/4, NOT 1/2: */
150
+ 0x03333330, 0x00333300, /* it shares PAL2 with the HUD */
151
+ }; /* band, whose dark is index 1. */
68
152
 
69
- /* The road lives on BG_B (8×8 cells). Two dashed dividers sit between the
70
- * three lanes; solid edges frame the outermost lanes. */
71
- #define ROAD_TOP_ROW 1
72
- #define ROAD_BOT_ROW 26
73
- #define LANE_DIV1_COL ((LANE_LEFT_X + 8 + LANE_MID_X) / 16)
74
- #define LANE_DIV2_COL ((LANE_MID_X + 8 + LANE_RIGHT_X) / 16)
75
- #define ROAD_EDGE_L ((LANE_LEFT_X - 12) / 8)
76
- #define ROAD_EDGE_R ((LANE_RIGHT_X + 12) / 8)
77
-
78
- /* Far plane (BG_A): grass shoulders + asphalt driving surface, tiled
79
- * across the whole 40x28 screen so the road no longer floats on black.
80
- * Drawn at low priority; the BG_B markings + sprite cars sit on top. */
81
- static void draw_backdrop(void) {
82
- s16 r, c;
83
- for (r = 0; r < 28; r++)
84
- for (c = 0; c < 40; c++) {
85
- u16 t = (c >= ROAD_EDGE_L && c <= ROAD_EDGE_R) ? T_ASPHALT : T_GRASS;
86
- VDP_setTileMapXY(BG_A, TILE_ATTR_FULL(PAL0, 0, 0, 0, t), c, r);
87
- }
153
+ /* ── GAME LOGIC (clay reshape freely) ──────────────────────────────────────
154
+ * Road geometry. Four 4-cell-wide lanes between shoulders, a double center
155
+ * divider (it's also the 2P territory line). Plane columns (cells):
156
+ * 12 = left shoulder, 16/24 = dashed lane lines, 20 = center divider,
157
+ * 28 = right shoulder; grass outside. The plane is 64 cells wide — paint
158
+ * ALL 64 (the haze wobble slides up to 2 px of the plane's wrap onto the
159
+ * screen edge; bare cells there would flash black). */
160
+ #define COL_EDGE_L 12
161
+ #define COL_DASH_1 16
162
+ #define COL_DIVIDER 20
163
+ #define COL_DASH_2 24
164
+ #define COL_EDGE_R 28
165
+ /* Lane center X for the 8px-wide car sprite (lane i spans 32 px). */
166
+ static const s16 lane_x[4] = { 108, 140, 172, 204 };
167
+
168
+ #define MAX_TRAFFIC 6
169
+ #define CAR_Y 192 /* both players' fixed screen Y */
170
+ #define SPAWN_Y 20 /* traffic entry Y just below the HUD */
171
+ #define DESPAWN_Y 216 /* traffic exits past the player */
172
+ #define START_LIVES 3 /* crashes per run / per player */
173
+ #define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic
174
+ * moves at road speed, so per-meter density
175
+ * stays constant whatever the player does */
176
+ #define SPEED_2P 2 /* fixed road speed in versus (one VDP =
177
+ * one scroll = one shared speed) */
178
+ #define MAX_SPEED 4 /* px/frame — MUST stay under 8: the row
179
+ * streamer restamps one row per crossing
180
+ * and a >8 px step could skip a row */
181
+ #define HUD_ROWS 2 /* window rows reserved for the HUD */
182
+
183
+ /* Players: index 0 = P1 (pad 1), 1 = P2 (pad 2, versus only). */
184
+ static u8 car_lane[2];
185
+ static u8 car_active[2];
186
+ static u8 crashes_left[2];
187
+ static u8 invuln[2]; /* post-crash blink/no-collide frames */
188
+ static u16 prev_pads[2];
189
+ static u8 lane_min[2], lane_max[2]; /* 2P: split territories */
190
+ static u8 two_player;
191
+ static u8 winner; /* versus result: 0 = P1, 1 = P2 */
192
+
193
+ static u8 traffic_alive[MAX_TRAFFIC];
194
+ static u8 traffic_lane[MAX_TRAFFIC];
195
+ static s16 traffic_y[MAX_TRAFFIC];
196
+
197
+ static u8 speed; /* road px/frame, 1..MAX_SPEED */
198
+ static u16 dist; /* 1P distance, 1 unit = 16 scrolled px */
199
+ static u8 dist_frac;
200
+ static u16 best; /* persisted best 1P distance */
201
+ static u8 spawn_timer;
202
+ static u16 vs; /* vertical scroll. NEVER wrapped by hand: *
203
+ * the plane is 256 px tall, the VDP masks *
204
+ * the scroll value to the plane, and 65536 *
205
+ * is a multiple of 256 — plain u16 *
206
+ * overflow keeps the road seamless forever *
207
+ * (the NES needs a 240-wrap helper here). */
208
+ static u8 prev_top_row; /* last restamped plane row */
209
+ static u8 start_pause; /* freeze frames at green light */
210
+ static u16 rng = 0xC0DE;
211
+
212
+ /* Game states — the shell every example shares: title → play → game over. */
213
+ #define ST_TITLE 0
214
+ #define ST_PLAY 1
215
+ #define ST_OVER 2
216
+ static u8 state;
217
+
218
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (a few 68k instructions) ── */
219
+ static u8 random8(void) {
220
+ u16 r = rng;
221
+ r ^= r << 7;
222
+ r ^= r >> 9;
223
+ r ^= r << 8;
224
+ rng = r;
225
+ return (u8)r;
88
226
  }
89
227
 
90
- static void draw_road(void) {
91
- s16 r;
92
- for (r = ROAD_TOP_ROW; r <= ROAD_BOT_ROW; r++) {
93
- /* Left edge (stripe on its right), right edge (hflipped). HIGH
94
- * priority so the markings sit above the BG_A asphalt backdrop. */
95
- VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 1, 0, 0, T_EDGE), ROAD_EDGE_L, r);
96
- VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 1, 0, 1, T_EDGE), ROAD_EDGE_R, r);
97
- /* Two dashed lane dividers. */
98
- VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 1, 0, 0, T_LANE), LANE_DIV1_COL, r);
99
- VDP_setTileMapXY(BG_B, TILE_ATTR_FULL(PAL0, 1, 0, 0, T_LANE), LANE_DIV2_COL, r);
100
- }
228
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
229
+ * CARTRIDGE SRAM — the Genesis battery-save mechanism, three parts:
230
+ *
231
+ * 1. The ROM HEADER declares it: bytes $1B0.. hold 'R','A', a type word
232
+ * ($F820 = battery-backed, byte-wide on ODD addresses the classic
233
+ * cart wiring), then start/end addresses $200000/$20FFFF. SGDK's
234
+ * rom_header.c (assembled into every build) already declares exactly
235
+ * this no linker work needed. Emulators allocate the save RAM by
236
+ * READING THIS HEADER; no 'RA' block = writes to $200000+ go nowhere.
237
+ * 2. The MAPPER GATE: writing 1 to $A130F1 banks SRAM into $200000+,
238
+ * 0 banks the ROM back in. SGDK's SRAM_enable()/SRAM_disable() do
239
+ * this. ALWAYS disable after access — on carts >2 MB the SRAM window
240
+ * shadows ROM, and leaving it enabled corrupts later ROM fetches.
241
+ * 3. ODD-BYTE ADDRESSING: SRAM_readByte/writeByte(offset) access 68k
242
+ * address $200001 + offset*2. Headlessly, the emulator's save_ram
243
+ * region interleaves with dead even bytes: SGDK offset k lives at
244
+ * save_ram[k*2 + 1] (the even bytes read back $FF).
245
+ *
246
+ * Best-distance record layout (SGDK offsets): 0='B' 1='D' 2=lo 3=hi
247
+ * 4=checksum(lo^hi^$A5). Fresh SRAM is all $FF — the magic+checksum
248
+ * rejects it (and any corruption) so first boot shows 0, not 65535.
249
+ *
250
+ * Emulator note (verified against gpgx): the core sizes its save_ram
251
+ * region by scanning for the last non-$FF byte, so the region reads as
252
+ * EMPTY until the first write below lands — that's why best_init runs
253
+ * at the very top of main(). Real hardware and .srm-restoring frontends
254
+ * have no such wrinkle. */
255
+ static u16 best_load(void) {
256
+ u8 m0, m1, lo, hi, ck;
257
+ SRAM_enableRO();
258
+ m0 = SRAM_readByte(0);
259
+ m1 = SRAM_readByte(1);
260
+ lo = SRAM_readByte(2);
261
+ hi = SRAM_readByte(3);
262
+ ck = SRAM_readByte(4);
263
+ SRAM_disable();
264
+ if (m0 == 'B' && m1 == 'D' && ck == (u8)(lo ^ hi ^ 0xA5))
265
+ return ((u16)hi << 8) | lo;
266
+ return 0;
101
267
  }
102
268
 
103
- typedef struct { s16 x, y; bool alive; } Car;
269
+ static void best_save(u16 d) {
270
+ u8 lo = (u8)d, hi = (u8)(d >> 8);
271
+ SRAM_enable();
272
+ SRAM_writeByte(0, 'B');
273
+ SRAM_writeByte(1, 'D');
274
+ SRAM_writeByte(2, lo);
275
+ SRAM_writeByte(3, hi);
276
+ SRAM_writeByte(4, (u8)(lo ^ hi ^ 0xA5));
277
+ SRAM_disable();
278
+ }
104
279
 
105
- static Car player;
106
- static Car obstacles[MAX_OBSTACLES];
107
- static u16 score;
108
- static u16 spawn_timer;
109
- static u16 game_over_timer;
110
- static u16 prev_pad;
111
- static u8 player_lane;
280
+ /* Format-on-first-boot: if the magic is absent (fresh battery), write a
281
+ * valid zero record immediately so the save file exists from frame one. */
282
+ static void best_init(void) {
283
+ best = best_load();
284
+ if (best == 0) best_save(0);
285
+ }
112
286
 
113
- static const s16 lane_x[3] = { LANE_LEFT_X, LANE_MID_X, LANE_RIGHT_X };
287
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
288
+ * FULL-PLANE VERTICAL SCROLL + STREAMED ROWS — the Genesis road. With
289
+ * VSCROLL_PLANE mode, one VSRAM value scrolls the whole plane vertically;
290
+ * the VDP wraps it inside the 256-px plane in hardware. Screen line y shows
291
+ * plane line (y + vs) & 255, so DECREMENTING vs slides the road DOWN — the
292
+ * driving-up illusion — for the cost of one register write per frame.
293
+ * Zero tilemap writes for the motion itself (rewriting tilemaps in the
294
+ * loop is the #1 "choppy movement" bug).
295
+ *
296
+ * The plane's 32 rows recycle as vs shrinks: the row crossing into the top
297
+ * of the screen is plane row (vs >> 3) & 31. The moment it changes we
298
+ * restamp that ONE row with fresh random roadside, so the 256-px loop
299
+ * never shows the same scenery twice. Three hard rules:
300
+ * 1. DMA_QUEUE only — the queued write lands in vblank, never mid-frame
301
+ * (SYS_doVBlankProcess flushes the queue; raw mid-frame VRAM writes
302
+ * tear). The data buffer must be STATIC: the queue reads it AT FLUSH
303
+ * TIME — a stack buffer is gone by then, shipping garbage.
304
+ * 2. The restamped row enters under the 16-px WINDOW HUD, which hides
305
+ * the swap (the NES version uses the overscan-cropped top band as
306
+ * its curtain; the window is ours). Restamp rows anywhere lower and
307
+ * the player sees tiles pop.
308
+ * 3. Road speed stays under 8 px/frame (MAX_SPEED) so a frame never
309
+ * skips past a whole row crossing.
310
+ */
311
+ static u16 rowbuf[64]; /* static — the DMA queue reads it at flush time */
312
+
313
+ static u16 road_cell(u16 c) {
314
+ u8 r;
315
+ if (c == COL_EDGE_L || c == COL_EDGE_R)
316
+ return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_EDGE);
317
+ if (c == COL_DIVIDER)
318
+ return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_DIVIDE);
319
+ if (c == COL_DASH_1 || c == COL_DASH_2)
320
+ return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_DASH);
321
+ r = random8();
322
+ if (c > COL_EDGE_L && c < COL_EDGE_R) /* tarmac */
323
+ return TILE_ATTR_FULL(PAL1, 0, 0, 0, (r & 7) == 0 ? T_SPECK : T_ASPHALT);
324
+ if ((r & 31) == 0) return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_TREE);
325
+ if ((r & 7) == 0) return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_TUFT);
326
+ return TILE_ATTR_FULL(PAL1, 0, 0, 0, T_GRASS); /* roadside */
327
+ }
114
328
 
115
- static bool aabb(Car* a, Car* b) {
116
- return a->x < b->x + 8 && a->x + 8 > b->x
117
- && a->y < b->y + 8 && a->y + 8 > b->y;
329
+ static void build_road_row(void) {
330
+ u16 c;
331
+ for (c = 0; c < 64; c++) rowbuf[c] = road_cell(c);
332
+ }
333
+
334
+ /* Initial paint: all 32 plane rows, immediate CPU writes (init-time only —
335
+ * inside the frame loop everything goes through the DMA queue). */
336
+ static void paint_road(void) {
337
+ u16 r;
338
+ for (r = 0; r < 32; r++) {
339
+ build_road_row();
340
+ VDP_setTileMapData(VDP_BG_A, rowbuf, r * 64, 64, 2, CPU);
341
+ }
118
342
  }
119
343
 
120
- static void reset_run(void) {
344
+ /* Advance the road by px pixels: one VSRAM write + at most one queued row
345
+ * restamp. Called every frame the road moves (play AND the title drift). */
346
+ static void advance_road(u8 px) {
347
+ u8 top_row;
348
+ vs -= px; /* hardware wraps — see idiom */
349
+ VDP_setVerticalScroll(BG_A, (s16)vs);
350
+ top_row = (u8)((vs >> 3) & 31);
351
+ if (top_row != prev_top_row) {
352
+ prev_top_row = top_row;
353
+ build_road_row();
354
+ VDP_setTileMapData(VDP_BG_A, rowbuf, (u16)top_row * 64, 64, 2, DMA_QUEUE);
355
+ }
356
+ }
357
+
358
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
359
+ * PER-SCANLINE HSCROLL — the heat-haze band, LIVE. This game runs in
360
+ * HSCROLL_LINE mode: the VDP fetches one hscroll entry per SCANLINE from
361
+ * the hscroll table in VRAM (interleaved words: plane A's value for the
362
+ * line, then plane B's). The platformer template runs the cheaper
363
+ * HSCROLL_TILE (one entry per 8-line strip) and only documents this
364
+ * variant — here it earns its keep: a traveling ±2 px sine wave across a
365
+ * 32-line band of the road reads as heat shimmer rising off the asphalt.
366
+ *
367
+ * Requires: HSCROLL_LINE set BEFORE any scroll-table write (the mode
368
+ * decides the table layout the VDP reads); the value array STATIC (the
369
+ * DMA queue reads it at flush time); and only the band's lines need
370
+ * updating each frame — the other 192 entries stay 0 in VRAM (SGDK's
371
+ * boot cleared VRAM, and a console reset re-runs that boot), so the
372
+ * cost is 32 words = 64 bytes/frame of the ~7 KB vblank budget. The
373
+ * FULL table at one entry per line per plane would be ~1.8 KB/frame —
374
+ * budget it before scaling this up.
375
+ * Sprites are not displaced — per-line hscroll bends PLANES only. The
376
+ * cars drive through the shimmer untouched, which is exactly how real
377
+ * carts looked (and why effect bands avoid gameplay-critical rows). */
378
+ #define HAZE_TOP 96 /* first shimmering scanline */
379
+ #define HAZE_LINES 32
380
+ static s16 haze[HAZE_LINES]; /* static — DMA queue reads at flush time */
381
+ static const s16 haze_wave[8] = { 0, 1, 2, 1, 0, -1, -2, -1 };
382
+ static u16 haze_phase;
383
+
384
+ static void update_haze(void) {
121
385
  u16 i;
122
- player_lane = 1;
123
- player.x = lane_x[1];
124
- player.y = PLAYER_Y;
125
- player.alive = TRUE;
126
- for (i = 0; i < MAX_OBSTACLES; i++) obstacles[i].alive = FALSE;
127
- score = 0;
128
- spawn_timer = 0;
129
- game_over_timer = 0;
386
+ haze_phase++;
387
+ for (i = 0; i < HAZE_LINES; i++)
388
+ haze[i] = haze_wave[(i + (haze_phase >> 1)) & 7];
389
+ VDP_setHorizontalScrollLine(BG_A, HAZE_TOP, haze, HAZE_LINES, DMA_QUEUE);
390
+ }
391
+
392
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
393
+ * WINDOW-PLANE HUD — the fixed status bar. The window is a third tilemap
394
+ * that REPLACES plane A wherever it's shown and IGNORES ALL SCROLLING —
395
+ * a hardware-fixed HUD with zero per-frame cost over a road that never
396
+ * stops moving (the NES version needs sprite digits for this; on Genesis
397
+ * it's one register). Two footguns:
398
+ * - The window only lives at screen edges (top/bottom N rows or left/
399
+ * right N columns) — it cannot float mid-screen.
400
+ * - It replaces plane A ONLY: plane B and sprites still render behind/
401
+ * over it. Plane B's top rows are painted with a flat dark band so
402
+ * HUD text always reads, and traffic spawns BELOW y=16.
403
+ * Bonus idiom on display here: the title/results text lives on PLANE B
404
+ * with the text priority bit SET, floating over the LOW-priority road on
405
+ * plane A — priority trumps plane order on the Genesis (high-pri B draws
406
+ * above low-pri A), which is how text sits on a busy foreground plane
407
+ * without repainting it. */
408
+ static void hud_init(void) {
409
+ VDP_setWindowOnTop(HUD_ROWS);
410
+ VDP_setTextPriority(1); /* window + plane-B text above the road */
411
+ }
412
+
413
+ /* ── GAME LOGIC (clay) — HUD text (window plane, redrawn only on change) ── */
414
+ static void draw_u16(VDPPlane plane, u16 v, u16 x, u16 y) {
415
+ char buf[8];
416
+ uintToStr(v, buf, 5);
417
+ VDP_drawTextBG(plane, buf, x, y);
418
+ }
419
+
420
+ static void draw_hud(void) {
421
+ char b[4];
422
+ VDP_clearTextAreaBG(WINDOW, 0, 0, 40, 1);
423
+ if (two_player) {
424
+ b[0] = 'P'; b[1] = '1'; b[2] = 0;
425
+ VDP_drawTextBG(WINDOW, b, 1, 0);
426
+ b[0] = 'x'; b[1] = '0' + crashes_left[0]; b[2] = 0;
427
+ VDP_drawTextBG(WINDOW, b, 4, 0);
428
+ b[0] = 'P'; b[1] = '2'; b[2] = 0;
429
+ VDP_drawTextBG(WINDOW, b, 34, 0);
430
+ b[0] = 'x'; b[1] = '0' + crashes_left[1]; b[2] = 0;
431
+ VDP_drawTextBG(WINDOW, b, 37, 0);
432
+ return;
433
+ }
434
+ b[0] = 'x'; b[1] = '0' + crashes_left[0]; b[2] = 0;
435
+ VDP_drawTextBG(WINDOW, b, 1, 0);
436
+ VDP_drawTextBG(WINDOW, "SPD", 5, 0);
437
+ b[0] = '0' + speed; b[1] = 0;
438
+ VDP_drawTextBG(WINDOW, b, 9, 0);
439
+ VDP_drawTextBG(WINDOW, "DIST", 12, 0);
440
+ draw_u16(WINDOW, dist, 17, 0);
441
+ VDP_drawTextBG(WINDOW, "BEST", 24, 0);
442
+ draw_u16(WINDOW, best, 29, 0);
443
+ }
444
+
445
+ static void draw_hud_title(void) {
446
+ VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
447
+ VDP_drawTextBG(WINDOW, "BEST", 24, 0);
448
+ draw_u16(WINDOW, best, 29, 0);
449
+ }
450
+
451
+ /* ── GAME LOGIC (clay) — plane B cards (title / results) ────────────────────
452
+ * Plane B never scrolls: rows 0-1 hold the dark band behind the window HUD,
453
+ * the rest holds high-priority text floating over the live road. Repainted
454
+ * on state changes only. */
455
+ static void paint_band(void) {
456
+ VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_BAND),
457
+ 0, 0, 64, HUD_ROWS);
458
+ }
459
+
460
+ static void paint_title(void) {
461
+ VDP_clearPlane(BG_B, TRUE);
462
+ paint_band();
463
+ VDP_drawTextBG(BG_B, GAME_TITLE, (40 - (sizeof(GAME_TITLE) - 1)) / 2, 6);
464
+ VDP_drawTextBG(BG_B, "1P RACE - A", 14, 12);
465
+ VDP_drawTextBG(BG_B, "2P VERSUS - B", 13, 14);
466
+ VDP_drawTextBG(BG_B, "STEER L R - GAS A - BRAKE B", 6, 20);
467
+ draw_hud_title();
468
+ }
469
+
470
+ static void paint_over(void) {
471
+ VDP_clearPlane(BG_B, TRUE);
472
+ paint_band();
473
+ if (two_player) {
474
+ VDP_drawTextBG(BG_B, winner ? "P2 WINS" : "P1 WINS", 16, 8);
475
+ VDP_drawTextBG(BG_B, "RIVAL WRECKED", 13, 12);
476
+ } else {
477
+ VDP_drawTextBG(BG_B, "WRECKED", 16, 8);
478
+ VDP_drawTextBG(BG_B, "DIST", 13, 12);
479
+ draw_u16(BG_B, dist, 18, 12);
480
+ VDP_drawTextBG(BG_B, "BEST", 13, 14);
481
+ draw_u16(BG_B, best, 18, 14);
482
+ }
483
+ VDP_drawTextBG(BG_B, "START - TITLE", 13, 20);
130
484
  }
131
485
 
132
- static void spawn_obstacle(void) {
486
+ /* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
487
+ static void spawn_traffic(void) {
133
488
  u16 i;
134
- for (i = 0; i < MAX_OBSTACLES; i++) {
135
- if (!obstacles[i].alive) {
136
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
137
- obstacles[i].y = 0;
138
- obstacles[i].alive = TRUE;
489
+ for (i = 0; i < MAX_TRAFFIC; i++) {
490
+ if (!traffic_alive[i]) {
491
+ traffic_alive[i] = 1;
492
+ traffic_lane[i] = random8() & 3;
493
+ traffic_y[i] = SPAWN_Y;
139
494
  return;
140
495
  }
141
496
  }
142
497
  }
143
498
 
144
- static void render_score(void) {
145
- char buf[6] = "00000";
146
- u16 v = score;
147
- s16 i;
148
- for (i = 4; i >= 0; i--) { buf[i] = '0' + (v % 10); v /= 10; }
149
- VDP_drawText(buf, 33, 2);
499
+ /* AABB, both boxes 8x8 (s16 math — sprite coords go negative off-screen). */
500
+ static u8 hits(s16 ax, s16 ay, s16 bx, s16 by) {
501
+ return ax < bx + 8 && ax + 8 > bx && ay < by + 8 && ay + 8 > by;
502
+ }
503
+
504
+ /* ── GAME LOGIC (clay) start a run ── */
505
+ static void start_game(u8 versus) {
506
+ u16 i;
507
+ two_player = versus;
508
+ for (i = 0; i < MAX_TRAFFIC; i++) traffic_alive[i] = 0;
509
+ for (i = 0; i < 2; i++) {
510
+ crashes_left[i] = START_LIVES;
511
+ invuln[i] = 0;
512
+ prev_pads[i] = 0xFFFF; /* swallow buttons held across the change */
513
+ }
514
+ if (versus) {
515
+ car_active[0] = 1; car_active[1] = 1;
516
+ lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
517
+ lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
518
+ speed = SPEED_2P; /* shared road, fixed speed (see header) */
519
+ } else {
520
+ car_active[0] = 1; car_active[1] = 0;
521
+ lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
522
+ speed = 1;
523
+ }
524
+ dist = 0; dist_frac = 0;
525
+ spawn_timer = 0;
526
+ start_pause = 30; /* green-light breather */
527
+ VDP_clearPlane(BG_B, TRUE); /* drop the title card — road shows clear */
528
+ paint_band();
529
+ draw_hud();
530
+ sfx_tone(0, 523, 10); /* start jingle (C5) */
531
+ state = ST_PLAY;
532
+ }
533
+
534
+ static void game_over(void) {
535
+ if (!two_player && dist > best) {
536
+ best = dist;
537
+ best_save(best); /* battery SRAM — see the SRAM idiom */
538
+ }
539
+ state = ST_OVER;
540
+ paint_over();
541
+ draw_hud_title(); /* window shows BEST — may have changed */
542
+ sfx_noise(20);
543
+ }
544
+
545
+ /* ── GAME LOGIC (clay) — crash rules ── */
546
+ static void crash(u8 p) {
547
+ sfx_noise(14);
548
+ invuln[p] = 60; /* blink + no-collide grace */
549
+ if (!two_player) speed = 1; /* a wreck kills your momentum */
550
+ if (crashes_left[p] > 0) --crashes_left[p];
551
+ if (crashes_left[p] == 0) {
552
+ winner = (u8)(1 - p); /* versus: the OTHER player wins */
553
+ game_over();
554
+ return;
555
+ }
556
+ draw_hud();
557
+ }
558
+
559
+ /* ── GAME LOGIC (clay) — per-player input ───────────────────────────────────
560
+ * LEFT/RIGHT steer between lanes (edge-detected — held d-pad shouldn't
561
+ * machine-gun across the road). 1P only: A/UP accelerate, B/DOWN brake
562
+ * (speed is shared in versus — see the design note). */
563
+ static void update_player(u8 p) {
564
+ u16 pad = JOY_readJoypad(p ? JOY_2 : JOY_1);
565
+ u16 pressed = pad & ~prev_pads[p];
566
+ prev_pads[p] = pad;
567
+ if (!car_active[p]) return;
568
+ if ((pressed & BUTTON_LEFT) && car_lane[p] > lane_min[p]) {
569
+ --car_lane[p];
570
+ sfx_tone(0, 880, 3); /* lane tick */
571
+ }
572
+ if ((pressed & BUTTON_RIGHT) && car_lane[p] < lane_max[p]) {
573
+ ++car_lane[p];
574
+ sfx_tone(0, 880, 3);
575
+ }
576
+ if (!two_player) {
577
+ if ((pressed & BTN_GAS) && speed < MAX_SPEED) {
578
+ ++speed;
579
+ sfx_tone(1, (u16)(700 - speed * 120), 8); /* engine rev */
580
+ draw_hud();
581
+ }
582
+ if ((pressed & BTN_BRAKE) && speed > 1) {
583
+ --speed;
584
+ sfx_tone(1, 220, 5); /* brake blip */
585
+ draw_hud();
586
+ }
587
+ }
588
+ if (invuln[p] > 0) --invuln[p];
589
+ }
590
+
591
+ /* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
592
+ * Fixed SAT slots: 0 = P1, 1 = P2, 2-7 = traffic. Hidden sprites park at
593
+ * y = -16 (above the screen). NEVER hide with x = -128..0 — a SAT x of 0
594
+ * is the VDP's sprite-masking trigger and silently blanks every lower-
595
+ * priority sprite on those scanlines. */
596
+ #define HIDE_Y (-16)
597
+ static void stage_sprites(void) {
598
+ u16 i;
599
+ u8 p;
600
+ for (p = 0; p < 2; p++) {
601
+ u8 vis = (state == ST_PLAY) && car_active[p] && !(invuln[p] & 2);
602
+ VDP_setSprite(p, lane_x[car_lane[p]], vis ? (s16)CAR_Y : (s16)HIDE_Y,
603
+ SPRITE_SIZE(1, 1),
604
+ TILE_ATTR_FULL(p ? PAL3 : PAL0, 1, 0, 0, T_CAR));
605
+ }
606
+ for (i = 0; i < MAX_TRAFFIC; i++) {
607
+ u8 vis = (state == ST_PLAY) && traffic_alive[i];
608
+ VDP_setSprite(2 + i, lane_x[traffic_lane[i]],
609
+ vis ? traffic_y[i] : (s16)HIDE_Y,
610
+ SPRITE_SIZE(1, 1),
611
+ TILE_ATTR_FULL(PAL2, 1, 0, 0, T_TRAFFIC));
612
+ }
613
+ /* ── HARDWARE IDIOM (load-bearing) — CHAIN the sprite list before
614
+ * uploading. VDP_setSprite does NOT set the SAT link byte, and link 0
615
+ * means "end of list": skip this and the VDP draws sprite 0 only.
616
+ * VDP_linkSprites(0, 8) links slots 0..7; the queued DMA flushes the
617
+ * 8 SAT entries during vblank. ── */
618
+ VDP_linkSprites(0, 8);
619
+ VDP_updateSprites(8, DMA_QUEUE);
150
620
  }
151
621
 
152
622
  int main(bool hard) {
623
+ u16 i, pad, fresh;
624
+ u8 p;
153
625
  (void)hard;
154
626
 
155
- /* PAL0 = player (white + blue trim) + road backdrop. PAL1 = enemy. */
156
- PAL_setColor(0 + 1, 0x0EEE); /* white body */
157
- PAL_setColor(0 + 2, 0x0AAA); /* roof grey */
158
- PAL_setColor(0 + 5, 0x0260); /* roadside grass */
159
- PAL_setColor(0 + 6, 0x0222); /* asphalt grey */
160
- PAL_setColor(16 + 3, 0x000E); /* enemy red */
161
- PAL_setColor(16 + 4, 0x0666); /* enemy roof */
162
-
163
- VDP_loadTileData(tile_car_p1, T_CAR_P1, 1, DMA);
164
- VDP_loadTileData(tile_car_enemy, T_CAR_EN, 1, DMA);
165
- VDP_loadTileData(tile_lane, T_LANE, 1, DMA);
166
- VDP_loadTileData(tile_edge, T_EDGE, 1, DMA);
167
- VDP_loadTileData(tile_grass, T_GRASS, 1, DMA);
168
- VDP_loadTileData(tile_asphalt, T_ASPHALT, 1, DMA);
169
-
170
- /* Draw the grass+asphalt backdrop (BG_A) then the road markings on
171
- * BG_B over it. */
172
- draw_backdrop();
173
- draw_road();
174
-
175
- VDP_drawText("SCORE", 28, 2);
176
- VDP_drawText("L/R MOVES LANE", 13, 27);
177
-
178
- sfx_init();
179
- reset_run();
180
- prev_pad = 0;
627
+ /* SRAM first before any VDP work. The save file then exists within
628
+ * the game's first frames of life, which is what lets a frontend (or
629
+ * a headless host) see a non-empty save_ram region as early as
630
+ * possible (see the SRAM idiom note on gpgx's size scan). */
631
+ best_init();
632
+
633
+ /* ── HARDWARE IDIOM (load-bearing see TROUBLESHOOTING) ──
634
+ * Init order: scrolling MODE before scroll VALUES (the mode decides
635
+ * the hscroll-table layout the VDP reads — see the haze idiom), tiles
636
+ * + palettes before tilemaps that reference them, window size before
637
+ * window text. SGDK's boot already did the dangerous part (VDP regs,
638
+ * Z80, vblank int, VRAM clear). */
639
+ VDP_setScrollingMode(HSCROLL_LINE, VSCROLL_PLANE);
640
+ hud_init();
641
+
642
+ /* Palettes: PAL0 P1 car + font, PAL1 road plane, PAL2 traffic + HUD
643
+ * band, PAL3 P2 car. Colours are BGR, 3 bits per channel: 0x0BGR with
644
+ * E = full. */
645
+ PAL_setColor( 1, 0x0E44); /* P1 body electric blue */
646
+ PAL_setColor( 2, 0x0420); /* P1 glass dark */
647
+ PAL_setColor(15, 0x0EEE); /* font white (index 15 = SGDK font) */
648
+ PAL_setColor(16 + 1, 0x0292); /* grass green */
649
+ PAL_setColor(16 + 2, 0x0161); /* tuft dark green */
650
+ PAL_setColor(16 + 3, 0x0EEE); /* road markings white */
651
+ PAL_setColor(16 + 4, 0x0444); /* asphalt grey */
652
+ PAL_setColor(16 + 5, 0x0666); /* asphalt speck */
653
+ PAL_setColor(16 + 6, 0x0040); /* tree foliage deep green*/
654
+ PAL_setColor(32 + 1, 0x0202); /* HUD band near-black */
655
+ PAL_setColor(32 + 3, 0x022E); /* traffic body red */
656
+ PAL_setColor(32 + 4, 0x0CCC); /* traffic glass light */
657
+ PAL_setColor(48 + 1, 0x04C4); /* P2 body green */
658
+ PAL_setColor(48 + 2, 0x0420); /* P2 glass dark */
659
+
660
+ VDP_loadTileData(tile_grass, T_GRASS, 1, DMA);
661
+ VDP_loadTileData(tile_tuft, T_TUFT, 1, DMA);
662
+ VDP_loadTileData(tile_tree, T_TREE, 1, DMA);
663
+ VDP_loadTileData(tile_asphalt, T_ASPHALT, 1, DMA);
664
+ VDP_loadTileData(tile_speck, T_SPECK, 1, DMA);
665
+ VDP_loadTileData(tile_edge, T_EDGE, 1, DMA);
666
+ VDP_loadTileData(tile_dash, T_DASH, 1, DMA);
667
+ VDP_loadTileData(tile_divide, T_DIVIDE, 1, DMA);
668
+ VDP_loadTileData(tile_band, T_BAND, 1, DMA);
669
+ VDP_loadTileData(tile_car, T_CAR, 1, DMA);
670
+ VDP_loadTileData(tile_traffic, T_TRAFFIC, 1, DMA);
671
+
672
+ paint_road(); /* plane A: 32 rows, then streamed forever */
673
+ sfx_init(); /* PSG: sfx channels + background melody */
674
+
675
+ vs = 0;
676
+ prev_top_row = 0;
677
+ state = ST_TITLE;
678
+ paint_title();
679
+ prev_pads[0] = 0xFFFF;
181
680
 
182
681
  while (TRUE) {
183
- u16 pad = JOY_readJoypad(JOY_1);
184
- u16 slot = 0;
185
-
186
- if (game_over_timer > 0) {
187
- game_over_timer--;
188
- VDP_drawText("CRASH! TRY AGAIN", 12, 14);
189
- if (game_over_timer == 0) {
190
- VDP_clearTextLineBG(BG_A, 14);
191
- reset_run();
192
- }
193
- } else {
194
- if ((pad & BUTTON_LEFT) && !(prev_pad & BUTTON_LEFT) && player_lane > 0) {
195
- player_lane--;
196
- sfx_tone(2, 380, 2); /* lane switch */
197
- }
198
- if ((pad & BUTTON_RIGHT) && !(prev_pad & BUTTON_RIGHT) && player_lane < 2) {
199
- player_lane++;
200
- sfx_tone(2, 380, 2);
201
- }
202
- player.x = lane_x[player_lane];
203
- prev_pad = pad;
204
-
205
- /* Obstacle speed = 2 + score/500 (caps low). */
206
- s16 step = 2 + (s16)(score / 500);
207
- if (step > 4) step = 4;
208
-
209
- u16 i;
210
- for (i = 0; i < MAX_OBSTACLES; i++) {
211
- if (!obstacles[i].alive) continue;
212
- obstacles[i].y += step;
213
- if (obstacles[i].y > 224) obstacles[i].alive = FALSE;
682
+ if (state == ST_TITLE) {
683
+ /* ── GAME LOGIC (clay) — title: A = 1P race, B = 2P versus ──
684
+ * The road idles under the title card so the screen sells the
685
+ * scroll + the heat haze before anyone presses a button. */
686
+ advance_road(1);
687
+ update_haze();
688
+ stage_sprites();
689
+ pad = JOY_readJoypad(JOY_1);
690
+ fresh = pad & ~prev_pads[0];
691
+ if (fresh & (BUTTON_A | BUTTON_C | BUTTON_START)) start_game(0);
692
+ else if (fresh & BUTTON_B) start_game(1);
693
+ else prev_pads[0] = pad;
694
+ sfx_update();
695
+ SYS_doVBlankProcess();
696
+ continue;
697
+ }
698
+
699
+ if (state == ST_OVER) {
700
+ /* Results card; the road freezes, the haze keeps shimmering.
701
+ * START or A returns to the title. */
702
+ update_haze();
703
+ stage_sprites();
704
+ pad = JOY_readJoypad(JOY_1);
705
+ fresh = pad & ~prev_pads[0];
706
+ if (fresh & (BUTTON_START | BUTTON_A | BUTTON_C)) {
707
+ state = ST_TITLE;
708
+ prev_pads[0] = 0xFFFF; /* swallow the held START */
709
+ paint_title();
710
+ } else {
711
+ prev_pads[0] = pad;
214
712
  }
713
+ sfx_update();
714
+ SYS_doVBlankProcess();
715
+ continue;
716
+ }
215
717
 
216
- if (++spawn_timer >= 32) { spawn_timer = 0; spawn_obstacle(); }
718
+ /* ── ST_PLAY ──────────────────────────────────────────────────── */
719
+ stage_sprites();
720
+ update_haze();
217
721
 
218
- for (i = 0; i < MAX_OBSTACLES; i++) {
219
- if (obstacles[i].alive && aabb(&player, &obstacles[i])) {
220
- game_over_timer = 60;
221
- sfx_noise(40); /* crash */
222
- break;
223
- }
722
+ if (start_pause) { /* green light: freeze gameplay, */
723
+ --start_pause; /* keep frames honest (sprites + */
724
+ sfx_update(); /* haze staged) */
725
+ SYS_doVBlankProcess();
726
+ continue;
727
+ }
728
+
729
+ advance_road(speed);
730
+
731
+ /* ── GAME LOGIC (clay) from here down ── */
732
+ update_player(0);
733
+ if (two_player) update_player(1);
734
+
735
+ /* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every
736
+ * 256 units marks a checkpoint. */
737
+ if (!two_player) {
738
+ dist_frac = (u8)(dist_frac + speed);
739
+ if (dist_frac >= 16) {
740
+ dist_frac -= 16;
741
+ if (dist < 65535u) ++dist;
742
+ draw_u16(WINDOW, dist, 17, 0);
743
+ if (dist != 0 && (dist & 0xFF) == 0)
744
+ sfx_tone(0, 1047, 8); /* checkpoint chime (C6) */
224
745
  }
746
+ }
225
747
 
226
- if (score < 65500u) score++;
748
+ /* Traffic flows down at road speed (it reads as slower cars you're
749
+ * overtaking); despawn past the player with a little pass tick. */
750
+ for (i = 0; i < MAX_TRAFFIC; i++) {
751
+ if (!traffic_alive[i]) continue;
752
+ traffic_y[i] += speed;
753
+ if (traffic_y[i] > DESPAWN_Y) {
754
+ traffic_alive[i] = 0;
755
+ sfx_tone(1, 660, 2);
756
+ }
757
+ }
758
+ if (++spawn_timer >= SPAWN_PERIOD) {
759
+ spawn_timer = 0;
760
+ spawn_traffic();
227
761
  }
228
762
 
229
- /* SAT update player + up to 4 obstacles = 5 sprites. */
230
- VDP_setSprite(slot++, player.x, player.y, SPRITE_SIZE(1, 1),
231
- TILE_ATTR_FULL(PAL0, 1, 0, 0, T_CAR_P1));
232
- for (u16 i = 0; i < MAX_OBSTACLES; i++) {
233
- s16 ey = obstacles[i].alive ? obstacles[i].y : -16;
234
- VDP_setSprite(slot++, obstacles[i].x, ey, SPRITE_SIZE(1, 1),
235
- TILE_ATTR_FULL(PAL1, 1, 0, 0, T_CAR_EN));
763
+ /* Traffic cars. Crash grace: a just-wrecked car blinks and can't
764
+ * collide for 60 frames. */
765
+ for (i = 0; i < MAX_TRAFFIC && state == ST_PLAY; i++) {
766
+ if (!traffic_alive[i]) continue;
767
+ for (p = 0; p < 2; p++) {
768
+ if (!car_active[p] || invuln[p]) continue;
769
+ if (hits(lane_x[traffic_lane[i]], traffic_y[i],
770
+ lane_x[car_lane[p]], CAR_Y)) {
771
+ traffic_alive[i] = 0;
772
+ crash(p);
773
+ break;
774
+ }
775
+ }
236
776
  }
237
- /* Link slots 0..slot-1 so the VDP's SAT walk draws all of them — without
238
- * this the link bytes stay 0 (= end-of-list) and only slot 0 renders. */
239
- VDP_linkSprites(0, slot);
240
- VDP_updateSprites(slot, DMA);
241
777
 
242
- render_score();
243
778
  sfx_update();
244
779
  SYS_doVBlankProcess();
245
780
  }