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,223 +1,815 @@
1
- /* ── racing.c — Game Boy top-down racing scaffold ──────────────────
1
+ /* ── racing.c — Game Boy top-down road racer (complete example game) ─────────
2
2
  *
3
- * Endless 3-lane top-down dodge for the Game Boy. LEFT/RIGHT switches
4
- * lanes (edge-detected), obstacles slide down at speed = 2 + score/500
5
- * (capped). Collision triggers a 60-frame freeze + auto-reset.
3
+ * TARMAC TILT a COMPLETE, working game: title screen, a vertically-
4
+ * scrolling road (the real thing BG scroll via SCY, not falling sprites),
5
+ * streamed roadside scenery through the vblank queue, lane-steered car,
6
+ * overtaking traffic, crash/lives rules, persistent best DISTANCE (battery
7
+ * cart RAM), GB APU music + SFX, and the Game Boy's signature WINDOW-LAYER
8
+ * HUD: a fixed best/dist/lives strip the scrolling road slides beneath, with
9
+ * zero mid-frame raster tricks.
6
10
  *
7
- * Game Boy screen is 160×144 3 lanes centred around x = {40, 76, 112}.
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 GB footgun; reshape
14
+ * your gameplay around it (see TROUBLESHOOTING before changing).
15
+ * GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
8
16
  *
9
- * The road is a real background: grass shoulders down each side, dark
10
- * asphalt across the playfield, and dashed white lane lines between the
11
- * lanes (LCDC bit 0 = BG ON drop it and the screen is a flat colour,
12
- * the #1 GB "why is it blank" footgun). Cars are sprites on top.
17
+ * SINGLE-PLAYER BY DESIGN (the honest handheld story): the Game Boy has ONE
18
+ * controller. Multiplayer on real hardware means the LINK CABLE, and a
19
+ * single emulator instance cannot emulate the second Game Boy on the other
20
+ * end of that cable so this game ships 1P only instead of faking a 2P mode
21
+ * the platform can't deliver. (The console racing examples have real
22
+ * split-lane 2P; the handheld is an honest 1P endless run.)
23
+ *
24
+ * What depends on what:
25
+ * gb_hardware.h — register names (LCDC/WX/WY/SCY/NRxx/...) + LCDC bit masks.
26
+ * gb_runtime.{h,c} — vblank wait (HALT-driven), joypad, shadow OAM +
27
+ * the OAM-DMA-from-HRAM routine, VRAM-safe memcpy, APU helpers.
28
+ * gb_crt0.s — boot + interrupt vectors + the cartridge header window.
29
+ * It DECLARES the cart as MBC1+RAM+BATTERY ($0147=$03, $0149=$02) —
30
+ * that declaration is what makes best_save() below persist (the
31
+ * emulator sizes battery SAVE_RAM from those two header bytes).
32
+ * Load-bearing; edit with TROUBLESHOOTING open.
33
+ *
34
+ * THE DESIGN (read before reshaping):
35
+ * Scrolling — the road is the BACKGROUND, scrolled down by INCREASING SCY
36
+ * each frame (raising SCY slides the BG map up under the screen = the
37
+ * road rushes DOWN toward the player). Cars/traffic are sprites with
38
+ * their own screen Y. See the SCY-WRAP idiom below: the GB BG map is 256
39
+ * px tall and SCY is a plain uint8 that wraps at 256 — which lines up
40
+ * EXACTLY with one 32-row map loop, so the road tiles a seamless ribbon
41
+ * with no wrap helper at all. (Contrast: NES vertical scroll wraps at
42
+ * 240 not 256 — values 240-255 fetch attribute bytes as garbage tiles,
43
+ * so the NES racing game needs a wrap helper; SMS wraps at 224 the same
44
+ * way; the Genesis plane is a full 256 and masks in hardware. The GB's
45
+ * uint8-SCY-into-256px-map is the friendliest of the four.)
46
+ * HUD — the WINDOW layer: a fixed strip the scrolling road can't move (see
47
+ * the window idiom). The platformer/shmup templates scroll the world
48
+ * under this same HUD on the OTHER axis (SCX); this game scrolls SCY.
49
+ * 1P RACE — four lanes, A/UP accelerate, B/DOWN brake (speed 1-4); LEFT/
50
+ * RIGHT tilt the car between lanes. 3 crashes end the run. Persistent
51
+ * stat: best DISTANCE (uint16, 1 unit = 16 scrolled px ≈ one car length)
52
+ * via best_load/save to battery SRAM.
53
+ *
54
+ * Frame budget (59.7 fps, ~17 556 machine cycles/frame, vblank = 10 of 154
55
+ * lines ≈ 1 140 cycles): everything VRAM/OAM-touching below happens in the
56
+ * vblank slice (OAM DMA ~165 cycles + ≤ 11 queued HUD/roadside bytes + one
57
+ * SCY write); game logic (1 car × 6 traffic AABB + staging ≤7 OAM slots)
58
+ * runs in the other 144 lines. Comfortable.
13
59
  */
14
60
 
15
61
  #include "gb_hardware.h"
16
62
  #include "gb_runtime.h"
17
63
 
18
- #define LANE_LEFT_X 40
19
- #define LANE_MID_X 76
20
- #define LANE_RIGHT_X 112
21
- #define PLAYER_Y 120
22
- #define MAX_OBSTACLES 4
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 "TARMAC TILT"
23
67
 
24
- /* ── Sprite tiles (cars) ──────────────────────────────────────────── */
25
- static const uint8_t tile_car_p1[16] = {
26
- 0x3C,0x00, 0x7E,0x00, 0x42,0x00, 0x7E,0x00,
27
- 0x7E,0x00, 0x42,0x00, 0x7E,0x00, 0x66,0x00,
68
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
69
+ * Tile inventory. GB tiles are 16 bytes: 8 rows × [low-plane byte,
70
+ * high-plane byte]. Pixel colour index = (hi_bit << 1) | lo_bit.
71
+ * lo only = colour 1 hi only = colour 2 both = colour 3
72
+ * With BGP = $E4 below the BG reads 0 = white, 1 = light grey, 2 = dark grey
73
+ * (asphalt), 3 = black (markings/text). */
74
+ static const uint8_t tile_blank[16] = { 0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0 };
75
+ static const uint8_t tile_car[16] = { /* player car, nose up */
76
+ 0x18,0x00, 0x7E,0x18, 0xFF,0x3C, 0xDB,0x00,
77
+ 0xFF,0x3C, 0xFF,0x00, 0x7E,0x18, 0x66,0x00,
28
78
  };
29
- static const uint8_t tile_car_en[16] = {
30
- 0x00,0x3C, 0x00,0x7E, 0x00,0x42, 0x00,0x7E,
31
- 0x00,0x7E, 0x00,0x42, 0x00,0x7E, 0x00,0x66,
79
+ static const uint8_t tile_traffic[16] = { /* rival car, tail up */
80
+ 0x66,0x00, 0x7E,0x18, 0xFF,0x00, 0xFF,0x3C,
81
+ 0xDB,0x00, 0xFF,0x3C, 0x7E,0x18, 0x18,0x00,
32
82
  };
33
-
34
- /* ── BG tiles (road) ──────────────────────────────────────────────── */
35
- /* 2bpp: row N = byte 2N (low plane) + byte 2N+1 (high plane).
36
- * asphalt — all colour 2 (mid-dark)
37
- * grass all colour 1 (light, the shoulders)
38
- * laneA/B — dashed white (colour 3) lane line, two phases for the dashes */
39
- static const uint8_t tile_asphalt[16] = {
40
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
41
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
83
+ /* Road-surface tiles. tile_road carries two colour-1 specks so even bare
84
+ * asphalt is never one flat colour (the render-health floor every example
85
+ * keeps) and the specks make the vertical scroll motion visible everywhere. */
86
+ static const uint8_t tile_road[16] = { /* dark asphalt + specks */
87
+ 0x00,0xFF, 0x00,0xFB, 0x00,0xFF, 0x00,0xFF,
88
+ 0x00,0xFF, 0x00,0xDF, 0x00,0xFF, 0x00,0xFF,
89
+ };
90
+ static const uint8_t tile_edge[16] = { /* solid shoulder line c3 */
91
+ 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18,
92
+ 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18,
42
93
  };
43
- static const uint8_t tile_grass[16] = {
44
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
45
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
94
+ static const uint8_t tile_dash[16] = { /* lane dash: 4 on 4 off */
95
+ 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18,
96
+ 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x00,
46
97
  };
47
- /* lane line = a 2px-wide colour-3 stripe down the centre of the cell;
48
- * phase A draws the top half, phase B the bottom half → dashes. */
49
- static const uint8_t tile_laneA[16] = {
50
- 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18,
51
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
98
+ static const uint8_t tile_grass[16] = { /* roadside hatch c2 */
99
+ 0xEE,0xEE, 0xBB,0xBB, 0xEE,0xEE, 0xBB,0xBB,
100
+ 0xEE,0xEE, 0xBB,0xBB, 0xEE,0xEE, 0xBB,0xBB,
52
101
  };
53
- static const uint8_t tile_laneB[16] = {
54
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
55
- 0x18,0x18, 0x18,0x18, 0x18,0x18, 0x18,0x18,
102
+ static const uint8_t tile_tree[16] = { /* roadside bush c3 over c2*/
103
+ 0x18,0xFE, 0x3C,0xFE, 0x7E,0xBA, 0x7E,0xFE,
104
+ 0x3C,0xFE, 0x18,0xBA, 0x18,0xFE, 0x00,0xEE,
105
+ };
106
+ static const uint8_t tile_hudbar[16] = { /* solid colour 2 */
107
+ 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
108
+ 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
56
109
  };
57
110
 
58
- /* OBJ palette: 0 transparent, 1 white (player), 2 red, 3 green. */
59
- static const uint16_t obj_palette[4] = { 0x7FFF, 0x7FFF, 0x001F, 0x03E0 };
111
+ /* Tile indices ($8000 unsigned addressing LCDC bit 4 set below). Sprites
112
+ * and BG share the $8000 table in this layout, so one upload serves both. */
113
+ #define T_CAR 1
114
+ #define T_TRAFFIC 2
115
+ #define T_ROAD 3
116
+ #define T_EDGE 4
117
+ #define T_DASH 5
118
+ #define T_GRASS 6
119
+ #define T_TREE 7
120
+ #define T_HUDBAR 8
121
+ /* Font: '0'-'9' → 16..25, 'A'-'Z' → 26..51, '-' → 52 (see char_tile). */
122
+ #define T_DIGIT0 16
123
+ #define T_ALPHA 26
124
+ #define T_DASHCH 52
60
125
 
61
- /* Tile indices in VRAM. Sprites and BG share the $8000 table here. */
62
- #define T_CAR_P1 1
63
- #define T_CAR_EN 2
64
- #define T_ASPHALT 3
65
- #define T_GRASS 4
66
- #define T_LANE_A 5
67
- #define T_LANE_B 6
126
+ /* 1bpp font (same glyph set as the NES/SMS examples 0-9, A-Z, '-').
127
+ * Stored 8 bytes/glyph and expanded to 2bpp colour 3 at upload time, so
128
+ * the ROM carries 296 bytes of font instead of 592. */
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},
152
+ };
68
153
 
69
- typedef struct { int16_t x, y; uint8_t alive; } Car;
154
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
155
+ * THE WINDOW-LAYER HUD — the Game Boy's signature "fixed HUD over a
156
+ * scrolling world" technique. The window is a second BG plane with its own
157
+ * 32×32 tile map and NO scroll registers: it always draws its map from
158
+ * (0,0), pinned to the screen, on top of the BG. So the HUD lives in the
159
+ * window and the road lives in the BG — SCY/SCX scroll the world all they
160
+ * like and the HUD never moves. No raster splits, no IRQ timing (the NES
161
+ * needs a sprite-0 polling dance for this exact effect; on GB it's three
162
+ * register writes). This game scrolls SCY (vertical road); the platformer
163
+ * scrolls SCX — same idiom, either axis.
164
+ *
165
+ * The three registers, and their two famous footguns:
166
+ * WY ($FF4A) — first screen LINE the window covers. We use 128: lines
167
+ * 0-127 are road, 128-143 (two tile rows) are HUD.
168
+ * WX ($FF4B) — screen column PLUS SEVEN. WX=7 means "left edge". The
169
+ * -7 offset is hardware fact, not a library quirk: WX=0..6 glitches
170
+ * (real DMG pixel pipeline artifacts), WX≥167 pushes it off-screen.
171
+ * LCDC bit 5 — window enable; bit 6 — which map it reads ($9800/$9C00).
172
+ *
173
+ * FOOTGUN 1 — "the window ate the bottom of my screen": once the window
174
+ * starts on a line it covers EVERY line from there DOWN, full width from
175
+ * WX to the right edge. There is no window height register. That is why
176
+ * GB HUDs sit at the BOTTOM of the screen (this game, and most of the
177
+ * classic library). A TOP HUD needs a mid-frame trick — STAT-interrupt on
178
+ * LYC, flip LCDC bit 5 off after the HUD rows — which is a different,
179
+ * fragile idiom; don't drift into it by accident by setting WY=0.
180
+ *
181
+ * FOOTGUN 2 — sprites are NOT clipped by the window. OBJs draw on top of
182
+ * it (priority bits notwithstanding), so a sprite that wanders below
183
+ * line 128 sits ON the HUD. The car sits at fixed CAR_Y and traffic
184
+ * despawns before PLAY_H, so no object ever touches the HUD rows.
185
+ *
186
+ * Requires: window map at $9C00 (LCDC bit 6 set — keeps it separate from
187
+ * the BG's $9800 map), tile data at $8000 (LCDC bit 4), WX=7, WY=PLAY_H,
188
+ * LCDC bit 5 set during play (title turns the window off — LCDC bit
189
+ * discipline lives in the two LCDC_* values below, poke those, not LCDC). */
190
+ #define PLAY_H 128 /* first HUD line = window top */
191
+ #define WIN_MAP ((uint8_t *)0x9C00) /* window's 32×32 tile map */
192
+ #define LCDC_TITLE (LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO)
193
+ #define LCDC_PLAY (LCDC_TITLE | LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI)
70
194
 
71
- static Car player;
72
- static Car obstacles[MAX_OBSTACLES];
73
- static uint16_t score;
74
- static uint8_t spawn_timer;
75
- static uint8_t game_over_timer;
76
- static uint8_t prev_pad;
77
- static uint8_t player_lane;
195
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
196
+ * BATTERY SRAM — persistent best distance. MBC1 cart RAM is 8KB at
197
+ * $A000-$BFFF, but it boots DISABLED and writes to a disabled bank are
198
+ * silently discarded (reads float). The gate is the MBC's RAM-enable
199
+ * register: any WRITE to ROM space $0000-$1FFF with $0A in the low nibble
200
+ * enables the RAM; writing $00 disables it again. (Writing "into ROM" feels
201
+ * wrong the first time — ROM-area writes never touch ROM, they're how you
202
+ * talk to the mapper chip.) Leaving RAM enabled all the time "works" in
203
+ * emulators but on real hardware risks corruption at power-off — battery
204
+ * carts since forever do enable → touch → disable, so we do too.
205
+ *
206
+ * The record is magic 'B','D' + dist lo,hi + a checksum byte, so a
207
+ * first-boot cart full of $FF garbage reads as "no record" instead of a
208
+ * 65535 best.
209
+ *
210
+ * Requires: gb_crt0.s declaring $0147=$03 (MBC1+RAM+BATTERY) + $0149=$02
211
+ * (8KB) — those header bytes are how the emulator knows to allocate and
212
+ * persist SAVE_RAM. Verify headlessly: race, crash out, then
213
+ * memory({op:'read', region:'save_ram'}) shows the block, and the best
214
+ * survives host.hardReset(). */
215
+ #define MBC_RAM_ENABLE (*(volatile uint8_t *)0x0000)
216
+ #define SRAM ((volatile uint8_t *)0xA000)
78
217
 
79
- static const int16_t lane_x[3] = { LANE_LEFT_X, LANE_MID_X, LANE_RIGHT_X };
218
+ static uint16_t best_load(void) {
219
+ uint16_t v = 0;
220
+ MBC_RAM_ENABLE = 0x0A; /* unlock cart RAM */
221
+ if (SRAM[0] == 'B' && SRAM[1] == 'D' &&
222
+ SRAM[4] == (uint8_t)(SRAM[2] ^ SRAM[3] ^ 0xA5)) {
223
+ v = (uint16_t)(SRAM[2] | ((uint16_t)SRAM[3] << 8));
224
+ }
225
+ MBC_RAM_ENABLE = 0x00; /* re-lock (battery hygiene) */
226
+ return v;
227
+ }
80
228
 
81
- static uint8_t aabb(Car *a, Car *b) {
82
- return a->x < b->x + 8 && a->x + 8 > b->x
83
- && a->y < b->y + 8 && a->y + 8 > b->y;
229
+ static void best_save(uint16_t v) {
230
+ uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
231
+ MBC_RAM_ENABLE = 0x0A;
232
+ SRAM[0] = 'B'; SRAM[1] = 'D';
233
+ SRAM[2] = lo; SRAM[3] = hi;
234
+ SRAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
235
+ MBC_RAM_ENABLE = 0x00;
84
236
  }
85
237
 
86
- static void reset_run(void) {
87
- uint8_t i;
88
- player_lane = 1;
89
- player.x = lane_x[1];
90
- player.y = PLAYER_Y;
91
- player.alive = 1;
92
- for (i = 0; i < MAX_OBSTACLES; i++) obstacles[i].alive = 0;
93
- score = 0;
94
- spawn_timer = 0;
95
- game_over_timer = 0;
238
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
239
+ * Road geometry. Four 4-px-wide lanes between shoulders, painted ONCE into
240
+ * the BG map (cols below); the scroll moves them with the road for free. The
241
+ * BG map is 32 cols; the visible 20 sit at cols 0..19 (SCX stays 0).
242
+ * col 1 = left shoulder, 6/14 = dashed lane lines, 10 = center divider,
243
+ * 19 = right shoulder; grass/trees outside cols 1..19. */
244
+ #define COL_EDGE_L 1
245
+ #define COL_DASH_1 6
246
+ #define COL_DIVIDER 10
247
+ #define COL_DASH_2 14
248
+ #define COL_EDGE_R 19
249
+ /* Lane centre screen X for the 8px car sprite (each lane spans ~32 px). */
250
+ static const uint8_t lane_x[4] = { 28, 60, 92, 124 };
251
+
252
+ #define NUM_TRAFFIC 6
253
+ #define CAR_Y 96 /* player's fixed screen Y (well above PLAY_H) */
254
+ #define SPAWN_Y 8 /* traffic entry Y */
255
+ #define DESPAWN_Y 120 /* traffic gone before it reaches the HUD */
256
+ #define START_LIVES 3 /* crashes per run */
257
+ #define SPAWN_PERIOD 36 /* frames between traffic spawns */
258
+
259
+ static uint8_t car_lane; /* 0..3 */
260
+ static uint8_t speed; /* road px/frame, 1..4 */
261
+ static uint8_t scy; /* BG scroll Y — uint8 wraps at 256 = *
262
+ * exactly one 32-row map loop (seamless) */
263
+ static uint16_t dist; /* 1 unit = 16 scrolled px ≈ one car len */
264
+ static uint8_t dist_frac;
265
+ static uint16_t best; /* live HUD readout: max(dist, record) */
266
+ static uint16_t record; /* what the battery SRAM actually holds */
267
+ static uint8_t lives;
268
+ static uint8_t invuln; /* post-crash blink + no-collide frames */
269
+ static uint8_t prev_pad;
270
+ static uint8_t spawn_timer;
271
+ static uint8_t prev_top_row; /* last streamed BG-map row */
272
+ static uint8_t hud_dirty; /* queue VRAM writes; vblank commits them */
273
+ static uint8_t msg_stage; /* game-over text: 2 = line 1, 1 = line 2 */
274
+
275
+ static uint8_t traffic_active[NUM_TRAFFIC];
276
+ static uint8_t traffic_lane[NUM_TRAFFIC];
277
+ static uint8_t traffic_y[NUM_TRAFFIC];
278
+
279
+ /* Game states — the shell every example shares: title → play → game over.
280
+ * (Handheld adaptation: title is press-start; consoles add a 1P/2P pick.) */
281
+ #define ST_TITLE 0
282
+ #define ST_PLAY 1
283
+ #define ST_OVER 2
284
+ static uint8_t state;
285
+
286
+ /* ── GAME LOGIC (clay) — Galois LFSR (taps $B8), period 255 ── */
287
+ static uint8_t rng_state = 0xA5;
288
+ static uint8_t rand8(void) {
289
+ uint8_t lsb = (uint8_t)(rng_state & 1);
290
+ rng_state >>= 1;
291
+ if (lsb) rng_state ^= 0xB8;
292
+ return rng_state;
96
293
  }
97
294
 
98
- static void spawn_obstacle(void) {
99
- uint8_t i;
100
- for (i = 0; i < MAX_OBSTACLES; i++) {
101
- if (!obstacles[i].alive) {
102
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
103
- obstacles[i].y = 0;
104
- obstacles[i].alive = 1;
105
- return;
295
+ static uint8_t dist8(uint8_t a, uint8_t b) {
296
+ return (a > b) ? (uint8_t)(a - b) : (uint8_t)(b - a);
297
+ }
298
+
299
+ /* ── GAME LOGIC (clay) VRAM upload + text helpers ──────────────────────────
300
+ * All of these write VRAM, so they run with the LCD OFF (boot/repaints) or
301
+ * inside vblank (the HUD digit commits). Note every loop walks a pointer
302
+ * (*dst++ = v) instead of indexing dst[i] — SDCC's sm83 port miscompiles
303
+ * indexed stores through VRAM-pointing pointers (the documented
304
+ * memcpy_vram footgun; see gb_runtime.c). */
305
+ static void upload_tile(uint8_t slot, const uint8_t *src) {
306
+ memcpy_vram((uint8_t *)(0x8000 + (uint16_t)slot * 16), src, 16);
307
+ }
308
+
309
+ static void upload_font(void) {
310
+ uint8_t *dst = (uint8_t *)(0x8000 + (uint16_t)T_DIGIT0 * 16);
311
+ uint8_t g, r, bits;
312
+ for (g = 0; g < 37; g++) {
313
+ for (r = 0; r < 8; r++) {
314
+ bits = font8[g][r];
315
+ *dst++ = bits; /* low plane ─┐ both set → colour 3 (black) */
316
+ *dst++ = bits; /* high plane ─┘ */
106
317
  }
107
318
  }
108
319
  }
109
320
 
110
- static void upload_tile(uint8_t slot, const uint8_t *src) {
111
- uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
112
- /* memcpy_vram (pointer-walk) NOT an indexed dst[i]=src[i] loop, which
113
- * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
114
- memcpy_vram(dst, src, 16);
115
- }
116
-
117
- /* Paint the road into BG map 0 ($9800). 20×18 visible cells:
118
- * col 0 = grass shoulder (left)
119
- * col 19 = grass shoulder (right)
120
- * cols 1..18 = asphalt, with dashed lane lines at the two lane
121
- * boundaries (cols 6 and 12). Dashes alternate per row. */
122
- static void draw_road(void) {
123
- uint8_t *bg = BG_MAP_0;
321
+ static uint8_t char_tile(char ch) {
322
+ if (ch >= '0' && ch <= '9') return (uint8_t)(T_DIGIT0 + (ch - '0'));
323
+ if (ch >= 'A' && ch <= 'Z') return (uint8_t)(T_ALPHA + (ch - 'A'));
324
+ if (ch == '-') return T_DASHCH;
325
+ return 0; /* space → blank tile */
326
+ }
327
+
328
+ /* Both 32×32 maps (BG $9800, window $9C00) take the same row/col math. */
329
+ static void draw_text(uint8_t *map, uint8_t row, uint8_t col, const char *s) {
330
+ uint8_t *p = map + (uint16_t)row * 32 + col;
331
+ while (*s) *p++ = char_tile(*s++);
332
+ }
333
+
334
+ /* Decimal digits WITHOUT divide/modulo (the sm83 has neither — SDCC's
335
+ * software % costs ~700 cycles a call). Repeated power-of-ten subtraction
336
+ * caps at 36 SUBs for any u16. */
337
+ static void u16_to_tiles(uint16_t v, uint8_t *out5) {
338
+ static const uint16_t pow10[4] = { 10000, 1000, 100, 10 };
339
+ uint8_t i, d;
340
+ for (i = 0; i < 4; i++) {
341
+ d = 0;
342
+ while (v >= pow10[i]) { v -= pow10[i]; ++d; }
343
+ *out5++ = (uint8_t)(T_DIGIT0 + d);
344
+ }
345
+ *out5 = (uint8_t)(T_DIGIT0 + (uint8_t)v);
346
+ }
347
+
348
+ static void draw_u16(uint8_t *map, uint8_t row, uint8_t col, uint16_t v) {
349
+ uint8_t d[5];
350
+ uint8_t i, *p = map + (uint16_t)row * 32 + col;
351
+ u16_to_tiles(v, d);
352
+ for (i = 0; i < 5; i++) *p++ = d[i];
353
+ }
354
+
355
+ /* Pre-convert a string to tile indices (full-frame time) so the vblank
356
+ * commit is a dumb byte copy. char_tile's compare chain per character is
357
+ * exactly the kind of work that blows the ~1140-cycle vblank budget. */
358
+ static uint8_t msg_q[20]; /* 9 "GAME OVER" + 11 "PRESS START" */
359
+ static void stage_text(const char *s, uint8_t *out) {
360
+ while (*s) *out++ = char_tile(*s++);
361
+ }
362
+
363
+ /* ── GAME LOGIC (clay) — screen painters (LCD off = free VRAM access) ────────
364
+ * Paints the road into the FULL 32-row BG map. Because SCY wraps at 256 (one
365
+ * full map height), the visible window can sit anywhere in the map and always
366
+ * shows a valid road ribbon — so we paint all 32 rows, not just the visible
367
+ * 18, and the scroll loops forever with no wrap helper. The dashed lane lines
368
+ * alternate per row (drawn on even rows only); the scroll animates them for
369
+ * free. Roadside trees use a divide-free running pattern counter (the sm83
370
+ * has no divide — treat every / and % in a loop as a red flag, ~700 cycles
371
+ * each). */
372
+ static void paint_road(uint8_t *map) {
373
+ uint8_t *p = map;
124
374
  uint8_t r, c, t;
125
- for (r = 0; r < 18; r++) {
126
- for (c = 0; c < 20; c++) {
127
- if (c == 0 || c == 19) t = T_GRASS;
128
- else if (c == 6 || c == 12) t = (r & 1) ? T_LANE_A : T_LANE_B;
129
- else t = T_ASPHALT;
130
- bg[r * 32 + c] = t;
375
+ uint8_t tc = 0; /* (r*7 + c*5) mod 13, incremental */
376
+ uint8_t tr = 0;
377
+ for (r = 0; r < 32; r++) {
378
+ tc = tr;
379
+ for (c = 0; c < 32; c++) {
380
+ if (c < COL_EDGE_L || c > COL_EDGE_R) {
381
+ t = T_GRASS;
382
+ if (tc == 0) t = T_TREE; /* sparse roadside trees */
383
+ } else if (c == COL_EDGE_L || c == COL_EDGE_R || c == COL_DIVIDER) {
384
+ t = T_EDGE; /* shoulders + solid centre line */
385
+ } else if (c == COL_DASH_1 || c == COL_DASH_2) {
386
+ t = (r & 1) ? T_ROAD : T_DASH; /* dashed lane lines */
387
+ } else {
388
+ t = T_ROAD; /* asphalt */
389
+ }
390
+ *p++ = t;
391
+ tc += 5; if (tc >= 13) tc -= 13;
131
392
  }
393
+ tr += 7; if (tr >= 13) tr -= 13;
132
394
  }
133
395
  }
134
396
 
135
- void main(void) {
136
- uint8_t pad;
137
- uint8_t i;
138
- int16_t step;
397
+ static void paint_title(void) {
398
+ paint_road(BG_MAP_0); /* road backdrop — text owns lanes */
399
+ draw_text(BG_MAP_0, 3, (uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
400
+ draw_text(BG_MAP_0, 6, 5, "PRESS START");
401
+ draw_text(BG_MAP_0, 9, 6, "BEST");
402
+ draw_u16(BG_MAP_0, 9, 11, best);
403
+ draw_text(BG_MAP_0, 12, 6, "1P ONLY"); /* see header: no link 2P */
404
+ SCX = 0; SCY = 0;
405
+ scy = 0;
406
+ }
139
407
 
140
- lcd_init_default();
408
+ /* HUD strip = window rows 0-1: a solid divider bar, then the text row.
409
+ * Columns 0-19 are the visible 20 (WX=7 pins map col 0 to screen x 0). */
410
+ static void paint_hud(void) {
411
+ uint8_t *p = WIN_MAP;
412
+ uint8_t c;
413
+ for (c = 0; c < 20; c++) *p++ = T_HUDBAR;
414
+ draw_text(WIN_MAP, 1, 0, "DS");
415
+ draw_u16(WIN_MAP, 1, 3, dist);
416
+ draw_text(WIN_MAP, 1, 9, "BS");
417
+ draw_u16(WIN_MAP, 1, 12, best);
418
+ draw_text(WIN_MAP, 1, 18, "L");
419
+ *(WIN_MAP + 32 + 19) = (uint8_t)(T_DIGIT0 + lives);
420
+ }
421
+
422
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
423
+ * LCD-off repaints. Bulk VRAM rewrites (full title/road repaints) happen
424
+ * with the LCD OFF — free access, no per-byte timing worries. The rule:
425
+ * only flip LCDC bit 7 to 0 DURING VBLANK. Killing the LCD mid-scanline
426
+ * is the classic "damages real DMG hardware" move; emulators shrug, real
427
+ * units can be permanently marked. wait_vblank() first, always.
428
+ * Requires: enable_vblank_irq() already called (wait_vblank HALT path);
429
+ * lcd_off-safe runtime (gb_runtime's wait_vblank bails if LCD is off). */
430
+ static void repaint_with_lcd_off(uint8_t to_title) {
431
+ msg_stage = 0; /* a queued game-over line must not land on
432
+ * the freshly painted screen a frame later */
433
+ wait_vblank(); /* never cut the LCD outside vblank */
141
434
  LCDC = 0;
435
+ if (to_title) {
436
+ paint_title();
437
+ oam_clear(); /* hide every sprite slot before re-enable */
438
+ LCDC = LCDC_TITLE; /* window OFF on the title */
439
+ } else {
440
+ paint_road(BG_MAP_0);
441
+ paint_hud();
442
+ LCDC = LCDC_PLAY; /* window ON below WY — the HUD appears */
443
+ }
444
+ }
142
445
 
143
- upload_tile(T_CAR_P1, tile_car_p1);
144
- upload_tile(T_CAR_EN, tile_car_en);
145
- upload_tile(T_ASPHALT, tile_asphalt);
146
- upload_tile(T_GRASS, tile_grass);
147
- upload_tile(T_LANE_A, tile_laneA);
148
- upload_tile(T_LANE_B, tile_laneB);
446
+ /* ── GAME LOGIC (clay) — sound: frame-ticked tune + steer/crash SFX ──────────
447
+ * Channel plan keeps SFX from cutting the music: ch2 = music (one
448
+ * sound_play_tone trigger per note, the APU sustains it), ch1 = engine/
449
+ * steer/checkpoint blips, ch4 = noise for crashes. music_tick() runs once
450
+ * per frame from the main loop; the APU needs no other upkeep. Periods are
451
+ * the 11-bit GB frequency code: 2048 - (131072 / Hz). 0 = rest. */
452
+ static const uint16_t tune[16] = {
453
+ 1602, 0, 1602, 1714, 1750, 0, 1714, 0, /* an engine-y ostinato */
454
+ 1602, 0, 1602, 1798, 1750, 0, 1602, 0,
455
+ };
456
+ static uint8_t music_pos, music_timer;
457
+ static void music_tick(void) {
458
+ uint16_t n;
459
+ if (++music_timer < 12) return;
460
+ music_timer = 0;
461
+ n = tune[music_pos];
462
+ music_pos = (uint8_t)((music_pos + 1) & 15);
463
+ if (n) sound_play_tone(2, n, 10);
464
+ }
149
465
 
150
- /* DMG BG palette: 0 white, 1 light, 2 dark, 3 black. The road reads
151
- * as asphalt(dark) + grass(light) + white dashes. */
152
- BGP = 0xE4;
153
- OCPS = 0x80;
154
- for (i = 0; i < 4; i++) {
155
- OCPD = (uint8_t)(obj_palette[i] & 0xFF);
156
- OCPD = (uint8_t)((obj_palette[i] >> 8) & 0xFF);
466
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
467
+ * Streamed roadside through the VRAM commit QUEUE. As the road scrolls down
468
+ * (SCY rising), BG-map rows re-enter at the TOP of the screen. The moment a
469
+ * new top row appears we restamp its two roadside columns (grass/tree) with
470
+ * fresh random tiles, so the wrap never shows the same 256-px loop twice.
471
+ * Classic streaming-row technique, downward.
472
+ *
473
+ * Two hard rules, mirroring the platformer/shmup VRAM discipline:
474
+ * 1. QUEUED through commit_vram() (one item per vblank) — a raw mid-frame
475
+ * write that slides past vblank into mode 3 is silently dropped by the
476
+ * core (the shmup harness caught exactly that as half-missing text).
477
+ * 2. The restamped row is two rows ABOVE the visible band (about to scroll
478
+ * on), so the swap lands before the player sees it.
479
+ * road_dirty carries the row index to restamp; commit_vram() drains it. */
480
+ static uint8_t road_dirty; /* 1 = a roadside row restamp is queued */
481
+ static uint8_t road_row; /* BG-map row to restamp */
482
+ static uint8_t road_l, road_r; /* the two roadside tiles (left, right col) */
483
+
484
+ static uint8_t roadside_tile(void) {
485
+ uint8_t r = rand8();
486
+ return ((r & 7) == 0) ? T_TREE : T_GRASS;
487
+ }
488
+
489
+ static void queue_roadside(uint8_t top_row) {
490
+ /* Restamp the row two above the screen top (about to scroll in). */
491
+ road_row = (uint8_t)((top_row + 30) & 31);
492
+ road_l = roadside_tile();
493
+ road_r = roadside_tile();
494
+ road_dirty = 1;
495
+ }
496
+
497
+ /* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
498
+ static void spawn_traffic(void) {
499
+ uint8_t i;
500
+ for (i = 0; i < NUM_TRAFFIC; i++) {
501
+ if (!traffic_active[i]) {
502
+ traffic_active[i] = 1;
503
+ traffic_lane[i] = (uint8_t)(rand8() & 3);
504
+ traffic_y[i] = SPAWN_Y;
505
+ return;
506
+ }
507
+ }
508
+ }
509
+
510
+ /* ── GAME LOGIC (clay) — state transitions ── */
511
+ static void begin_run(void) {
512
+ uint8_t i;
513
+ car_lane = 1;
514
+ speed = 1;
515
+ scy = 0;
516
+ dist = 0;
517
+ dist_frac = 0;
518
+ prev_top_row = 0;
519
+ spawn_timer = 0;
520
+ invuln = 48; /* ready breather — car blinks */
521
+ prev_pad = 0xFF; /* swallow held buttons across the reset */
522
+ for (i = 0; i < NUM_TRAFFIC; i++) traffic_active[i] = 0;
523
+ }
524
+
525
+ static void start_game(void) {
526
+ lives = START_LIVES;
527
+ begin_run();
528
+ hud_dirty = 1; /* restage hud_q — a stale game-over stage queued
529
+ * before the repaint would overwrite the fresh
530
+ * zeros next vblank otherwise */
531
+ state = ST_PLAY;
532
+ repaint_with_lcd_off(0);
533
+ sound_play_tone(1, 1602, 8); /* start rev */
534
+ }
535
+
536
+ static void game_over(void) {
537
+ /* Compare against the SAVED record, not the live `best` readout — the
538
+ * scoring path already raised `best` to track the run, so testing
539
+ * `dist > best` here would never fire (the shmup example shipped exactly
540
+ * that bug for an hour; verified-by-harness is the cure). */
541
+ if (dist > record) {
542
+ record = dist;
543
+ best_save(record); /* battery write — survives power-off */
157
544
  }
545
+ state = ST_OVER;
546
+ /* The BG scrolled vertically, but the game-over text is painted into fixed
547
+ * BG rows (columns don't shift on a vertical scroll), so a plain row/col
548
+ * anchor lands mid-screen. Convert the strings to tile indices HERE
549
+ * (full-frame time) and queue them — commit_vram() copies one line per
550
+ * vblank. */
551
+ stage_text("GAME OVER", msg_q);
552
+ stage_text("PRESS START", msg_q + 9);
553
+ msg_stage = 2;
554
+ }
158
555
 
159
- draw_road();
556
+ static void crash(void) {
557
+ sound_play_noise(20);
558
+ invuln = 60; /* blink + no-collide grace */
559
+ speed = 1; /* a wreck kills your momentum */
560
+ if (lives) --lives;
561
+ hud_dirty = 1;
562
+ if (lives == 0) game_over();
563
+ }
160
564
 
161
- oam_clear();
162
- LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO;
163
- sound_init();
565
+ /* ── GAME LOGIC (clay) — per-state update (runs OUTSIDE vblank) ── */
566
+ static void update_play(uint8_t pad) {
567
+ uint8_t i, pressed, ty;
164
568
 
165
- reset_run();
166
- prev_pad = 0;
569
+ pressed = (uint8_t)(pad & ~prev_pad);
167
570
 
168
- while (1) {
169
- wait_vblank();
571
+ /* Steer: LEFT/RIGHT tilt one lane (edge-detected — a held d-pad must not
572
+ * machine-gun across the road). */
573
+ if ((pressed & PAD_LEFT) && car_lane > 0) {
574
+ --car_lane;
575
+ sound_play_tone(1, 1750, 3); /* tilt tick */
576
+ }
577
+ if ((pressed & PAD_RIGHT) && car_lane < 3) {
578
+ ++car_lane;
579
+ sound_play_tone(1, 1750, 3);
580
+ }
581
+ /* Throttle: A/UP accelerate, B/DOWN brake. */
582
+ if ((pressed & (PAD_A | PAD_UP)) && speed < 4) {
583
+ ++speed;
584
+ sound_play_tone(1, (uint16_t)(1500 + speed * 40), 6); /* engine up */
585
+ }
586
+ if ((pressed & (PAD_B | PAD_DOWN)) && speed > 1) {
587
+ --speed;
588
+ sound_play_tone(1, 1850, 4); /* brake blip */
589
+ }
590
+ if (invuln) --invuln;
170
591
 
171
- /* Stage OAM player + obstacles. */
172
- for (i = 0; i < 40; i++) oam_set(i, 0, 0, 0, 0);
173
- oam_set(0, (uint8_t)(player.y + 16), (uint8_t)(player.x + 8), T_CAR_P1, 0);
174
- for (i = 0; i < MAX_OBSTACLES; i++) {
175
- if (obstacles[i].alive) {
176
- oam_set((uint8_t)(1 + i),
177
- (uint8_t)(obstacles[i].y + 16),
178
- (uint8_t)(obstacles[i].x + 8),
179
- T_CAR_EN, 0);
592
+ /* Scroll the road down: SCY increases (BG slides up under the screen).
593
+ * Plain uint8 wraps at 256 = one full map loop, seamless (see idiom). */
594
+ scy = (uint8_t)(scy + speed);
595
+
596
+ /* Distance: 1 unit per 16 scrolled px. A chime every 256 units. */
597
+ dist_frac = (uint8_t)(dist_frac + speed);
598
+ if (dist_frac >= 16) {
599
+ dist_frac -= 16;
600
+ if (dist < 65535u) ++dist;
601
+ if (dist > best) best = dist; /* live HUD readout */
602
+ hud_dirty = 1;
603
+ if (dist != 0 && (dist & 0xFF) == 0)
604
+ sound_play_tone(1, 1923, 8); /* checkpoint chime */
605
+ }
606
+
607
+ /* Traffic flows DOWN at road speed (reads as slower cars you overtake);
608
+ * despawn before the HUD band, spawn on a timer. */
609
+ for (i = 0; i < NUM_TRAFFIC; i++) {
610
+ if (!traffic_active[i]) continue;
611
+ ty = (uint8_t)(traffic_y[i] + speed);
612
+ if (ty >= DESPAWN_Y) { traffic_active[i] = 0; continue; }
613
+ traffic_y[i] = ty;
614
+ }
615
+ if (++spawn_timer >= SPAWN_PERIOD) {
616
+ spawn_timer = 0;
617
+ spawn_traffic();
618
+ }
619
+
620
+ /* Traffic ↔ car (AABB, both 8x8). Crash grace: a just-wrecked car blinks
621
+ * and can't collide for 60 frames. */
622
+ if (!invuln) {
623
+ for (i = 0; i < NUM_TRAFFIC; i++) {
624
+ if (!traffic_active[i]) continue;
625
+ if (dist8(lane_x[traffic_lane[i]], lane_x[car_lane]) < 8 &&
626
+ dist8(traffic_y[i], CAR_Y) < 8) {
627
+ traffic_active[i] = 0;
628
+ crash();
629
+ return;
180
630
  }
181
631
  }
182
- oam_dma_flush();
632
+ }
633
+ }
183
634
 
184
- pad = joypad_read();
185
- if (game_over_timer > 0) {
186
- game_over_timer--;
187
- if (game_over_timer == 0) reset_run();
188
- prev_pad = pad;
189
- continue;
190
- }
635
+ /* ── GAME LOGIC (clay) — stage the shadow OAM for THIS frame ─────────────────
636
+ * Pure WRAM writes (shadow_oam at $C100) — safe any time; only the DMA
637
+ * flush is vblank-sensitive. OAM coords are hardware coords: +16 on Y,
638
+ * +8 on X (Y=0/X=0 park a sprite off-screen, which is what oam_clear's
639
+ * zero-fill does for every unused slot). Slot plan (40 hardware slots, we
640
+ * use 7): 0 = player car, 1-6 traffic — well under the 10-OBJ/line drop. */
641
+ static void stage_sprites(void) {
642
+ uint8_t i;
643
+ oam_clear();
644
+ if (state == ST_TITLE) {
645
+ /* Guaranteed-visible sprite from the first title frame — proof the
646
+ * whole OAM pipeline (shadow → HRAM DMA stub → OAM) is alive before
647
+ * any gameplay complicates the picture. */
648
+ oam_set(0, 96 + 16, 76 + 8, T_CAR, 0);
649
+ return;
650
+ }
651
+ if (invuln == 0 || (invuln & 4)) /* crash/ready blink */
652
+ oam_set(0, CAR_Y + 16, (uint8_t)(lane_x[car_lane] + 8), T_CAR, 0);
653
+ for (i = 0; i < NUM_TRAFFIC; i++)
654
+ if (traffic_active[i])
655
+ oam_set((uint8_t)(1 + i), (uint8_t)(traffic_y[i] + 16),
656
+ (uint8_t)(lane_x[traffic_lane[i]] + 8), T_TRAFFIC, 0x10); /* OBP1 */
657
+ }
191
658
 
192
- if ((pad & PAD_LEFT) && !(prev_pad & PAD_LEFT) && player_lane > 0) {
193
- player_lane--;
194
- sound_play_tone(2, 1700, 3);
195
- }
196
- if ((pad & PAD_RIGHT) && !(prev_pad & PAD_RIGHT) && player_lane < 2) {
197
- player_lane++;
198
- sound_play_tone(2, 1700, 3);
199
- }
200
- player.x = lane_x[player_lane];
201
- prev_pad = pad;
659
+ /* ── GAME LOGIC (clay) queued VRAM commits ─────────────────────────────────
660
+ * Two-phase update, mirroring the shadow-OAM discipline: game logic only
661
+ * sets the dirty flags (hud_dirty / road_dirty / msg_stage). stage_hud()
662
+ * (full-frame time) does the digit math into hud_q; commit_vram() (vblank
663
+ * time) copies bytes and commits AT MOST ONE queued item per vblank. The
664
+ * budget after the OAM DMA (~165 cycles of the ~1140) fits one item
665
+ * comfortably; committing everything at once on a busy frame overruns into
666
+ * mode 3, where the PPU locks VRAM and the writes are silently discarded
667
+ * (the shmup harness caught exactly that as half-missing GAME OVER text).
668
+ * One item per frame = zero dropped bytes, and a frame of HUD latency
669
+ * nobody can see. */
670
+ static uint8_t hud_q[11]; /* 5 dist digits, 5 best digits, lives tile */
671
+ static uint8_t hud_ready;
202
672
 
203
- step = (int16_t)(2 + (score / 500));
204
- if (step > 4) step = 4;
673
+ static void stage_hud(void) {
674
+ if (!hud_dirty) return;
675
+ hud_dirty = 0;
676
+ u16_to_tiles(dist, hud_q);
677
+ u16_to_tiles(best, hud_q + 5);
678
+ hud_q[10] = (uint8_t)(T_DIGIT0 + lives);
679
+ hud_ready = 1;
680
+ }
205
681
 
206
- for (i = 0; i < MAX_OBSTACLES; i++) {
207
- if (!obstacles[i].alive) continue;
208
- obstacles[i].y = (int16_t)(obstacles[i].y + step);
209
- if (obstacles[i].y >= 144) obstacles[i].alive = 0;
210
- }
211
- spawn_timer++;
212
- if (spawn_timer >= 36) { spawn_timer = 0; spawn_obstacle(); }
213
-
214
- for (i = 0; i < MAX_OBSTACLES; i++) {
215
- if (obstacles[i].alive && aabb(&player, &obstacles[i])) {
216
- sound_play_noise(8);
217
- game_over_timer = 60;
218
- break;
682
+ static void commit_vram(void) {
683
+ uint8_t i;
684
+ uint8_t *p;
685
+ const uint8_t *q;
686
+ if (hud_ready) { /* item 1: HUD digits */
687
+ hud_ready = 0;
688
+ p = WIN_MAP + 32 + 3; q = hud_q; for (i = 0; i < 5; i++) *p++ = *q++;
689
+ p = WIN_MAP + 32 + 12; q = hud_q + 5; for (i = 0; i < 5; i++) *p++ = *q++;
690
+ *(WIN_MAP + 32 + 19) = hud_q[10];
691
+ return;
692
+ }
693
+ if (road_dirty) { /* item 2: roadside row restamp */
694
+ road_dirty = 0;
695
+ p = BG_MAP_0 + (uint16_t)road_row * 32;
696
+ *p = road_l; /* col 0 (left roadside) */
697
+ *(p + 20) = road_r; /* col 20 (right roadside) */
698
+ return;
699
+ }
700
+ if (msg_stage == 2) { /* item 3: GAME OVER line */
701
+ msg_stage = 1;
702
+ p = BG_MAP_0 + (uint16_t)5 * 32 + 5; q = msg_q;
703
+ for (i = 0; i < 9; i++) *p++ = *q++;
704
+ return;
705
+ }
706
+ if (msg_stage == 1) { /* item 4: PRESS START line */
707
+ msg_stage = 0;
708
+ p = BG_MAP_0 + (uint16_t)7 * 32 + 4; q = msg_q + 9;
709
+ for (i = 0; i < 11; i++) *p++ = *q++;
710
+ }
711
+ }
712
+
713
+ void main(void) {
714
+ uint8_t pad, top_row;
715
+
716
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
717
+ * Boot order. Three load-bearing calls, in this order:
718
+ * 1. lcd_init_default() — sane LCD state AND it installs the OAM-DMA
719
+ * stub into HRAM ($FF80). During OAM DMA the CPU can only fetch
720
+ * from HRAM; the broken alternative (spinning in ROM) fetches $FF
721
+ * = rst $38 and corrupts the stack — the classic "sprites never
722
+ * show / game dies after a while" GB death. Every oam_dma_flush()
723
+ * below depends on this stub existing.
724
+ * 2. enable_vblank_irq() — flips wait_vblank() from LY-polling to
725
+ * HALT-until-vblank-IRQ. The polling fallback runs at ~1/30 speed
726
+ * on the WASM emulator; the HALT path is full speed everywhere.
727
+ * 3. LCD off (inside vblank) for the bulk VRAM uploads — tiles, font,
728
+ * first screen — then back on. VRAM is only freely writable with
729
+ * the LCD off or during vblank/hblank windows. */
730
+ lcd_init_default();
731
+ enable_vblank_irq();
732
+ sound_init();
733
+
734
+ wait_vblank();
735
+ LCDC = 0; /* LCD off — free VRAM access from here */
736
+
737
+ upload_tile(0, tile_blank);
738
+ upload_tile(T_CAR, tile_car);
739
+ upload_tile(T_TRAFFIC, tile_traffic);
740
+ upload_tile(T_ROAD, tile_road);
741
+ upload_tile(T_EDGE, tile_edge);
742
+ upload_tile(T_DASH, tile_dash);
743
+ upload_tile(T_GRASS, tile_grass);
744
+ upload_tile(T_TREE, tile_tree);
745
+ upload_tile(T_HUDBAR, tile_hudbar);
746
+ upload_font();
747
+
748
+ /* DMG palettes (2 bits per colour index, low bits = index 0):
749
+ * BGP $E4 → 0=white 1=light 2=dark (asphalt) 3=black (markings/text).
750
+ * OBP0 $1C → player car: body black, cockpit white.
751
+ * OBP1 $C4 → traffic: light grey body, black outline. */
752
+ BGP = 0xE4;
753
+ OBP0 = 0x1C;
754
+ OBP1 = 0xC4;
755
+
756
+ /* Window position — set once; LCDC bit 5 decides if it shows. */
757
+ WX = 7; /* the +7 quirk: 7 = screen left edge */
758
+ WY = PLAY_H; /* HUD owns lines 128-143 */
759
+
760
+ record = best_load(); /* battery SRAM — 0 on first boot */
761
+ best = record;
762
+ state = ST_TITLE;
763
+ paint_title();
764
+ oam_clear();
765
+ LCDC = LCDC_TITLE;
766
+
767
+ for (;;) {
768
+ /* ── full-frame work: input, game state, shadow-OAM staging ── */
769
+ pad = joypad_read();
770
+
771
+ if (state == ST_TITLE) {
772
+ if ((pad & PAD_START) && !(prev_pad & PAD_START)) start_game();
773
+ else if ((pad & PAD_A) && !(prev_pad & PAD_A)) start_game();
774
+ prev_pad = pad;
775
+ } else if (state == ST_PLAY) {
776
+ update_play(pad);
777
+ prev_pad = pad;
778
+ /* Stream a fresh roadside row as each new row scrolls on at the top. */
779
+ top_row = (uint8_t)(scy >> 3);
780
+ if (top_row != prev_top_row) {
781
+ prev_top_row = top_row;
782
+ if (!road_dirty) queue_roadside(top_row);
783
+ }
784
+ } else { /* ST_OVER — freeze the field; START/A returns to title */
785
+ if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
786
+ state = ST_TITLE;
787
+ repaint_with_lcd_off(1);
219
788
  }
789
+ prev_pad = pad;
220
790
  }
221
- if (score < 65500u) score++;
791
+ stage_sprites();
792
+ stage_hud(); /* digit math out here, not in vblank */
793
+
794
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
795
+ * The vblank slice. wait_vblank() wakes at the START of vblank
796
+ * (~1140 cycles of safe OAM/VRAM access). Order is everything:
797
+ * oam_dma_flush() FIRST — the DMA takes ~165 cycles and MUST finish
798
+ * inside vblank; pushing it later (after VRAM writes that grow
799
+ * over time) slides it into active display, where the PPU is
800
+ * reading OAM = one frame of torn/invisible sprites, intermittent
801
+ * and miserable to debug.
802
+ * commit_vram() second — the few queued HUD/roadside/text bytes.
803
+ * SCY last — scroll latches per-scanline, so writing it during
804
+ * vblank (before line 0 renders) moves the WHOLE next frame
805
+ * consistently; the window ignores it by design (the HUD idiom).
806
+ * Game logic above NEVER touches VRAM directly — it sets the dirty
807
+ * flags and shadow OAM, and this slice commits them. Keep that split
808
+ * when you reshape the game. */
809
+ wait_vblank();
810
+ oam_dma_flush();
811
+ commit_vram();
812
+ SCY = scy; /* title resets scy to 0; over freezes it */
813
+ music_tick();
222
814
  }
223
815
  }