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