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,43 +1,68 @@
1
- /* ── racing.c — NES top-down racing scaffold ─────────────────────────
1
+ /* ── racing.c — NES top-down road racer (complete example game) ──────────────
2
2
  *
3
- * A simple endless top-down racer. Player car at the bottom; the
4
- * "road" scrolls down as obstacles (other cars) come up from the top.
5
- * Left/Right or A/B switches between three lanes. Survive as long as
6
- * possible score = frames survived.
3
+ * THROTTLE FEUD a COMPLETE, working game: title screen, 1P endless race and
4
+ * 2P simultaneous VERSUS, a vertically-scrolling road (the real thing BG
5
+ * scroll, not falling sprites), streamed roadside scenery through the queued
6
+ * tile path, crash/lives rules, persistent best distance (battery SRAM),
7
+ * music + SFX.
7
8
  *
8
- * Mechanics:
9
- * - 3 lanes (left / centre / right)
10
- * - 4 obstacle slots (object pool) — one spawns from a random lane
11
- * every ~40 frames
12
- * - Obstacles slide down at constant speed
13
- * - AABB collision; on hit, game-over state pauses for 60 frames then
14
- * resets the run
15
- * - Score = frames-survived-in-current-run, rendered with the
16
- * digit tiles
9
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
10
+ * very different one. The markers tell you what's what:
11
+ * HARDWARE IDIOM (load-bearing) — dodges a documented NES footgun; reshape
12
+ * your gameplay around it (see TROUBLESHOOTING before changing).
13
+ * GAME LOGIC (clay) traffic patterns, speeds, tuning, art: reshape freely.
17
14
  *
18
- * Two-player mode: port 1 (player 2) controls a second car in
19
- * the opposite half of the screen. When no second controller is
20
- * present, P2 lane runs an AI that dodges in the same pattern.
15
+ * What depends on what:
16
+ * nes_runtime.{h,c} rendering/input/sound/text/hi-score library.
17
+ * chr-ram-runtime.crt0.s boot + NMI + iNES header (BATTERY bit feeds
18
+ * hiscore_load/save; vertical mirroring makes the Y-wrap seamless).
19
+ * Load-bearing; edit with TROUBLESHOOTING open.
21
20
  *
22
- * Easy extensions: scrolling BG road stripes, scenery sprites,
23
- * acceleration / brake, lap timer, multi-tier obstacle types.
21
+ * THE DESIGN (read before reshaping):
22
+ * Scrolling the road is the BACKGROUND, scrolled down by decrementing
23
+ * scroll_y each frame (the crt0 NMI commits scroll_x AND scroll_y every
24
+ * vblank). Cars/traffic are sprites with their own Y. See the Y-WRAP
25
+ * idiom below: NES vertical scroll wraps at 240, NOT 256.
26
+ * HUD — sprite digits on a fixed scanline. With the whole BG scrolling
27
+ * vertically, a fixed BG HUD would need a mid-frame Y-scroll change:
28
+ * unlike the X-only sprite-0 split in shmup.c, mid-frame Y needs the
29
+ * 4-write $2006/$2005 sequence (the advanced variant — see
30
+ * TROUBLESHOOTING). Sprite HUD is the simple honest option, so that's
31
+ * what this game uses. Budget rule: max 8 sprites per scanline.
32
+ * 2P VERSUS — ONE PPU means ONE road scroll, so both players share one
33
+ * road at a fixed speed and only steer: solid center divider, P1 (blue,
34
+ * port 0) owns the left two lanes, P2 (green, port 1) the right two.
35
+ * Each starts with 3 crashes; first to use them all LOSES.
36
+ * 1P RACE — all four lanes, A/UP accelerates, B/DOWN brakes (speed 1-4);
37
+ * 3 crashes end the run. Persistent stat: best DISTANCE (uint16, one
38
+ * unit = 16 scrolled pixels ≈ one car length) via hiscore_load/save.
39
+ *
40
+ * Frame budget (NTSC, 60fps): 6 traffic × 2 cars AABB = 12 checks, ≤4
41
+ * queued tile writes per row crossing, and HUD digits recomputed only when
42
+ * the distance value changes — comfortably inside one frame.
24
43
  */
25
44
 
26
45
  #include "nes_runtime.h"
27
46
 
28
- /* ── Tile data ────────────────────────────────────────────────────── */
47
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
48
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
49
+ #define GAME_TITLE "THROTTLE FEUD"
50
+
51
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
52
+ * Sprite tile art ($0000 pattern table). Each 8x8 tile = 16 bytes: 8 plane-0
53
+ * rows then 8 plane-1 rows (2bpp — plane0-only = colour 1, both = colour 3). */
29
54
  static const uint8_t tile_blank[16] = { 0 };
30
- /* Player car 8×8 small box with a roof */
31
- static const uint8_t tile_car_p1[16] = {
32
- 0x3C, 0x7E, 0x42, 0x7E, 0x7E, 0x7E, 0x42, 0x66,
55
+ static const uint8_t tile_car[16] = { /* player car, nose up */
56
+ 0x18, 0x7E, 0x5A, 0x7E, 0x3C, 0x7E, 0x5A, 0x66,
33
57
  0, 0, 0, 0, 0, 0, 0, 0,
34
58
  };
35
- /* Enemy car inverted */
36
- static const uint8_t tile_car_enemy[16] = {
37
- 0x66, 0x42, 0x7E, 0x7E, 0x7E, 0x42, 0x7E, 0x3C,
59
+ static const uint8_t tile_traffic[16] = { /* slow traffic, tail up */
60
+ 0x66, 0x5A, 0x7E, 0x3C, 0x7E, 0x5A, 0x7E, 0x18,
38
61
  0, 0, 0, 0, 0, 0, 0, 0,
39
62
  };
40
- /* Digit tiles for the HUD (same 3×5-padded shape as sports.c). */
63
+ /* Compact 3x5 digits for the sprite HUD. font_upload() only serves the
64
+ * BACKGROUND pattern table, and sprites read from $0000 — so the HUD gets
65
+ * its own digit tiles on the sprite side. */
41
66
  static const uint8_t tile_digits[10 * 16] = {
42
67
  /* 0 */ 0xE0,0xA0,0xA0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
43
68
  /* 1 */ 0x40,0xC0,0x40,0x40,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
@@ -50,235 +75,518 @@ static const uint8_t tile_digits[10 * 16] = {
50
75
  /* 8 */ 0xE0,0xA0,0xE0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
51
76
  /* 9 */ 0xE0,0xA0,0xE0,0x20,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
52
77
  };
78
+ #define TILE_CAR 1
79
+ #define TILE_TRAFFIC 2
80
+ #define TILE_DIGIT0 3 /* sprite tiles 3-12 */
53
81
 
54
- #define T_BLANK 0
55
- #define T_CAR_P1 1
56
- #define T_CAR_ENEMY 2
57
- #define T_DIGIT0 3
58
-
59
- /* ── Background road tiles ───────────────────────────────────────────
60
- * Default PPUCTRL ($90) reads BG patterns from pattern table 1 ($1000),
61
- * so these go to CHR $1000+ and are indexed independently of the sprite
62
- * tiles above. The grey backdrop (colour 0) is the road surface; colour 1
63
- * (white) draws the markings, colour 2 (green) the grass, colour 3 the
64
- * dark seam in the tarmac.
65
- *
66
- * BG_T_EDGE: a solid 2px vertical stripe — the road shoulder line.
67
- * BG_T_LANE: a 2px vertical dash (on 4 rows / off 4) — the dashed centre
68
- * lane marking when stacked down a column.
69
- * BG_T_GRASS: a textured green roadside (colour 2 hatch) so the area
70
- * outside the shoulders isn't flat — fills the screen sides.
71
- * BG_T_ROAD: a faint tarmac texture (a couple of colour-3 specks) tiled
72
- * across the driving surface so the road doesn't read as one
73
- * solid grey block. */
74
- #define BG_T_EDGE 1
75
- #define BG_T_LANE 2
76
- #define BG_T_GRASS 3
77
- #define BG_T_ROAD 4
78
- static const uint8_t bg_tile_edge[16] = {
79
- 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, /* plane 0 (colour bit 0) */
82
+ /* ── GAME LOGIC (clay) — road BG tiles (BACKGROUND pattern table $1000 —
83
+ * separate from the sprite table at $0000; the runtime's PPUCTRL setup makes
84
+ * that split). Colour 0 = the grey backdrop = the asphalt itself. */
85
+ static const uint8_t bg_edge[16] = { /* solid shoulder/divider line */
86
+ 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18, 0x18,
87
+ 0, 0, 0, 0, 0, 0, 0, 0,
88
+ };
89
+ static const uint8_t bg_dash[16] = { /* lane dash: 4 px on, 4 off */
90
+ 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00,
80
91
  0, 0, 0, 0, 0, 0, 0, 0,
81
92
  };
82
- static const uint8_t bg_tile_lane[16] = {
83
- 0x18, 0x18, 0x18, 0x18, 0x00, 0x00, 0x00, 0x00, /* dash: 4 on, 4 off */
93
+ static const uint8_t bg_grass[16] = { /* roadside hatch (colour 2) */
84
94
  0, 0, 0, 0, 0, 0, 0, 0,
95
+ 0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB,
85
96
  };
86
- static const uint8_t bg_tile_grass[16] = {
87
- 0, 0, 0, 0, 0, 0, 0, 0, /* plane 0 clear */
88
- 0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB, 0xEE, 0xBB, /* plane 1 → colour 2 hatch */
97
+ static const uint8_t bg_tuft[16] = { /* scenery: grass tuft */
98
+ 0x00, 0x00, 0x00, 0x24, 0x5A, 0x00, 0x00, 0x00,
99
+ 0xEE, 0xBB, 0xEE, 0x9B, 0xA4, 0xBB, 0xEE, 0xBB,
89
100
  };
90
- static const uint8_t bg_tile_road[16] = {
91
- 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, /* plane 0 specks */
92
- 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00, /* plane 1 too → colour 3 */
101
+ static const uint8_t bg_tree[16] = { /* scenery: bush/tree */
102
+ 0x18, 0x3C, 0x7E, 0x7E, 0x3C, 0x18, 0x18, 0x00,
103
+ 0x18, 0x3C, 0x7E, 0x7E, 0x3C, 0x18, 0x18, 0xBB,
93
104
  };
105
+ static const uint8_t bg_speck[16] = { /* tarmac texture speck */
106
+ 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00,
107
+ 0x00, 0x00, 0x10, 0x00, 0x00, 0x08, 0x00, 0x00,
108
+ };
109
+ #define BG_EDGE 1
110
+ #define BG_DASH 2
111
+ #define BG_GRASS 3
112
+ #define BG_TUFT 4
113
+ #define BG_TREE 5
114
+ #define BG_SPECK 6
94
115
 
95
116
  static const uint8_t palette[32] = {
96
- /* BG palettes light grey backdrop simulates road; idx2 = grass green */
97
- 0x10, 0x30, 0x1A, 0x00,
98
- 0x10, 0x30, 0x1A, 0x00,
99
- 0x10, 0x30, 0x1A, 0x00,
100
- 0x10, 0x30, 0x1A, 0x00,
101
- /* Sprite palettes */
102
- 0x10, 0x21, 0x16, 0x30, /* sp0: blue car (P1) */
103
- 0x10, 0x16, 0x21, 0x30, /* sp1: red enemy */
104
- 0x10, 0x2A, 0x21, 0x30,
105
- 0x10, 0x2A, 0x21, 0x30,
117
+ /* BG: dark-grey asphalt backdrop, white markings, green grass,
118
+ * light-grey specks. One palette everywhere = no attribute scrolling
119
+ * headaches (attribute bytes cover 16x16 zones and scroll WITH the BG). */
120
+ 0x00, 0x30, 0x1A, 0x10,
121
+ 0x00, 0x30, 0x1A, 0x10,
122
+ 0x00, 0x30, 0x1A, 0x10,
123
+ 0x00, 0x30, 0x1A, 0x10,
124
+ /* Sprites: P1 blue, P2 green, traffic red, HUD white */
125
+ 0x00, 0x21, 0x11, 0x30,
126
+ 0x00, 0x2A, 0x1A, 0x30,
127
+ 0x00, 0x16, 0x06, 0x30,
128
+ 0x00, 0x30, 0x10, 0x00,
106
129
  };
130
+ #define PAL_P1 0
131
+ #define PAL_P2 1
132
+ #define PAL_TRAFFIC 2
133
+ #define PAL_HUD 3
134
+
135
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
136
+ * Road geometry. Four 4-tile-wide lanes between shoulders, solid divider in
137
+ * the middle (it's also the 2P territory line). Tile columns:
138
+ * 7 = left shoulder, 12/20 = dashed lane lines, 16 = solid center divider,
139
+ * 24 = right shoulder; grass outside. */
140
+ #define COL_EDGE_L 7
141
+ #define COL_DASH_1 12
142
+ #define COL_DIVIDER 16
143
+ #define COL_DASH_2 20
144
+ #define COL_EDGE_R 24
145
+ /* Lane center X for the 8px-wide car sprite (lane i spans 32 px). */
146
+ static const uint8_t lane_x[4] = { 76, 108, 140, 172 };
107
147
 
108
- #define LANE_LEFT_X 88
109
- #define LANE_MID_X 120
110
- #define LANE_RIGHT_X 152
111
- #define PLAYER_Y 192
148
+ #define MAX_TRAFFIC 6
149
+ #define CAR_Y 200 /* both players' fixed screen Y */
150
+ #define HUD_Y 9 /* sprite HUD scanline (top 8 are overscan-cropped) */
151
+ #define SPAWN_Y 18 /* traffic entry Y — BELOW the HUD scanlines so
152
+ * traffic never shares them (8 sprites/scanline
153
+ * is a hard PPU limit; the 1P HUD already puts
154
+ * 6 there) */
155
+ #define START_LIVES 3 /* crashes per run/per player */
156
+ #define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic moves
157
+ * at road speed, so per-meter density stays
158
+ * constant whatever the player's speed is */
159
+ #define SPEED_2P 2 /* fixed road speed in versus (one PPU = one
160
+ * scroll = one shared speed; see header) */
112
161
 
113
- #define MAX_OBSTACLES 4
162
+ /* Players: index 0 = P1 (port 0), 1 = P2 (port 1, versus only). */
163
+ static uint8_t car_lane[2];
164
+ static uint8_t car_active[2];
165
+ static uint8_t crashes_left[2];
166
+ static uint8_t invuln[2]; /* post-crash blink/no-collide frames */
167
+ static uint8_t prev_pad[2];
168
+ static uint8_t lane_min[2], lane_max[2]; /* 2P: split territories */
169
+ static uint8_t two_player;
170
+ static uint8_t winner; /* versus result: 0 = P1, 1 = P2 */
114
171
 
115
- typedef struct { uint8_t x, y, alive; } Car;
172
+ static uint8_t traffic_alive[MAX_TRAFFIC];
173
+ static uint8_t traffic_lane[MAX_TRAFFIC];
174
+ static uint8_t traffic_y[MAX_TRAFFIC];
116
175
 
117
- static Car player; /* y is fixed at PLAYER_Y; x switches lanes */
118
- static Car obstacles[MAX_OBSTACLES];
119
- static uint16_t score;
176
+ static uint8_t speed; /* road px/frame, 1-4 */
177
+ static uint16_t dist; /* 1P distance, 1 unit = 16 scrolled px */
178
+ static uint8_t dist_frac;
179
+ static uint16_t best; /* persisted best 1P distance */
120
180
  static uint8_t spawn_timer;
121
- static uint8_t game_over_timer;
122
- static uint8_t prev_p1;
181
+ static uint8_t road_scroll; /* BG scroll_y, ALWAYS kept in 0..239 */
182
+ static uint8_t prev_top_row; /* last streamed nametable row */
183
+ static uint16_t rng = 0xC0DE;
184
+
185
+ /* HUD digit cache — cc65's 16-bit div/mod helpers cost hundreds of cycles
186
+ * each; recompute the 5 digits only when dist actually changes. */
187
+ static uint8_t hud_digits[5];
188
+ static uint16_t hud_cached = 0xFFFF;
189
+
190
+ /* Game states — the shell every example shares: title → play → game over. */
191
+ #define ST_TITLE 0
192
+ #define ST_PLAY 1
193
+ #define ST_OVER 2
194
+ static uint8_t state;
123
195
 
124
- static const uint8_t lane_x[3] = { LANE_LEFT_X, LANE_MID_X, LANE_RIGHT_X };
125
- static uint8_t player_lane;
196
+ /* ── GAME LOGIC (clay) xorshift16 PRNG (~tens of cycles per call) ── */
197
+ static uint8_t random8(void) {
198
+ uint16_t r = rng;
199
+ r ^= r << 7;
200
+ r ^= r >> 9;
201
+ r ^= r << 8;
202
+ rng = r;
203
+ return (uint8_t)r;
204
+ }
205
+
206
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
207
+ * Vertical scroll Y-WRAP. A nametable is 32x30 tiles = 240 pixels tall, so
208
+ * vertical scroll wraps at 240, NOT 256. scroll_y values 240-255 make the
209
+ * PPU fetch ATTRIBUTE-table bytes as tile indices — rows of garbage tiles.
210
+ * Plain uint8_t arithmetic happily produces 240-255, so every change to
211
+ * road_scroll goes through this helper. (Scrolling DOWN = the road slides
212
+ * toward the player = scroll_y DECREASES.) The crt0's iNES header sets
213
+ * vertical mirroring, so the nametable below $2000 mirrors $2000 and the
214
+ * wrap is seamless. */
215
+ static void scroll_road_down(uint8_t px) {
216
+ if (road_scroll >= px) road_scroll -= px;
217
+ else road_scroll = (uint8_t)(road_scroll + 240 - px);
218
+ ppu_scroll(0, road_scroll); /* NMI commits scroll_x AND scroll_y */
219
+ }
220
+
221
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
222
+ * Streaming-row scenery through the QUEUED tile path. As the road scrolls
223
+ * down, nametable rows re-enter at the top of the screen; the moment row R
224
+ * becomes the top row we restamp its roadside scenery cells with fresh
225
+ * random tiles, so the wrap never shows the same 240px loop twice. Classic
226
+ * streaming-row technique — same trick big scrollers use, just downward.
227
+ * Two hard rules:
228
+ * 1. QUEUED writes only (tile_set) — raw $2007 traffic while rendering
229
+ * corrupts the scroll/address latch. The NMI drains 16 queue entries
230
+ * per vblank; we stamp 4 cells per row crossing, and at max speed (4
231
+ * px/frame) a crossing happens at most every other frame. Stay under
232
+ * the 16/vblank budget when adding cells.
233
+ * 2. The restamped row sits in the overscan-cropped top band (most NTSC
234
+ * displays/cores hide the top 8 scanlines) when the queue commits, so
235
+ * the swap is invisible. Restamp rows anywhere lower and the player
236
+ * sees tiles pop. */
237
+ static void stream_road_row(uint8_t row) {
238
+ uint8_t r;
239
+ r = random8(); tile_set(0, 2, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
240
+ r = random8(); tile_set(0, 5, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
241
+ r = random8(); tile_set(0, 26, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
242
+ r = random8(); tile_set(0, 29, row, (r & 7) == 0 ? BG_TREE : ((r & 3) == 0 ? BG_TUFT : BG_GRASS));
243
+ }
244
+
245
+ /* AABB, both boxes 8x8. */
246
+ static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
247
+ uint8_t dx = (ax > bx) ? (ax - bx) : (bx - ax);
248
+ uint8_t dy = (ay > by) ? (ay - by) : (by - ay);
249
+ return (dx < 8) && (dy < 8);
250
+ }
251
+
252
+ /* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
253
+ static void spawn_traffic(void) {
254
+ uint8_t i;
255
+ for (i = 0; i < MAX_TRAFFIC; i++) {
256
+ if (!traffic_alive[i]) {
257
+ traffic_alive[i] = 1;
258
+ traffic_lane[i] = random8() & 3;
259
+ traffic_y[i] = SPAWN_Y;
260
+ return;
261
+ }
262
+ }
263
+ }
126
264
 
127
- static uint8_t aabb(Car *a, Car *b) {
128
- return a->x < b->x + 8 && a->x + 8 > b->x
129
- && a->y < b->y + 8 && a->y + 8 > b->y;
265
+ /* ── GAME LOGIC (clay) sprite HUD ─────────────────────────────────────────
266
+ * All HUD glyphs are SPRITES on one fixed scanline (see header for why not
267
+ * a BG HUD). 1P: lives digit left + 5-digit distance right = 6 sprites on
268
+ * the line; 2P: one crashes-left digit per player = 2. Traffic spawns below
269
+ * this scanline, so the 8-sprites-per-scanline PPU limit is never hit. */
270
+ static void stage_hud(void) {
271
+ uint8_t i;
272
+ if (two_player) {
273
+ oam_spr(8, HUD_Y, (uint8_t)(TILE_DIGIT0 + crashes_left[0]), PAL_P1);
274
+ oam_spr(240, HUD_Y, (uint8_t)(TILE_DIGIT0 + crashes_left[1]), PAL_P2);
275
+ return;
276
+ }
277
+ oam_spr(8, HUD_Y, (uint8_t)(TILE_DIGIT0 + crashes_left[0]), PAL_HUD);
278
+ if (dist != hud_cached) { /* recompute digits only on change */
279
+ uint16_t v = dist;
280
+ for (i = 0; i < 5; i++) { hud_digits[4 - i] = (uint8_t)(v % 10); v /= 10; }
281
+ hud_cached = dist;
282
+ }
283
+ for (i = 0; i < 5; i++)
284
+ oam_spr((uint8_t)(192 + i * 8), HUD_Y, (uint8_t)(TILE_DIGIT0 + hud_digits[i]), PAL_HUD);
130
285
  }
131
286
 
132
- /* Draw the static road into nametable 0 ($2000): solid shoulder lines on
133
- * the outside of the outer lanes and dashed dividers between the three
134
- * lanes. PPU must be OFF call from init (uses vram_unsafe_set). Tile
135
- * columns: lanes sit at 11/15/19, so dividers go at 13/17 and shoulders
136
- * just outside at 9/21. */
137
- #define ROAD_TOP_ROW 2
138
- #define ROAD_BOT_ROW 27
139
- #define ROAD_EDGE_L 9
140
- #define ROAD_EDGE_R 21
141
- #define ROAD_DIV_1 13
142
- #define ROAD_DIV_2 17
143
- static void draw_road(void) {
144
- uint8_t row, col;
287
+ /* ── GAME LOGIC (clay) — paint the road into nametable 0 ───────────────────
288
+ * Whole-screen paint with the PPU OFF (vram_unsafe_set the queued path
289
+ * would deadlock with rendering disabled; see TROUBLESHOOTING). The dashed
290
+ * lane lines are painted ONCE and never touched again: they live in the BG,
291
+ * so the scroll moves them with the road for free. */
292
+ static void paint_road(void) {
293
+ uint8_t row, col, tile;
145
294
  uint16_t base;
146
- /* Fill the WHOLE nametable so nothing reads as flat colour: grass on the
147
- * roadside (outside the shoulders) and a faint tarmac texture on the
148
- * driving surface. Then stamp the shoulder + lane markings on top. */
149
295
  for (row = 0; row < 30; row++) {
150
296
  base = (uint16_t)(0x2000 + (uint16_t)row * 32);
151
297
  for (col = 0; col < 32; col++) {
152
- if (col < ROAD_EDGE_L || col > ROAD_EDGE_R) {
153
- vram_unsafe_set((uint16_t)(base + col), BG_T_GRASS); /* roadside */
154
- } else if (((row + col) & 3) == 0) {
155
- vram_unsafe_set((uint16_t)(base + col), BG_T_ROAD); /* tarmac speck */
298
+ if (col < COL_EDGE_L || col > COL_EDGE_R) {
299
+ tile = BG_GRASS; /* roadside */
300
+ if (((row * 7 + col * 13) % 31) == 0) tile = BG_TREE;
301
+ else if (((row * 5 + col * 3) % 11) == 0) tile = BG_TUFT;
302
+ } else if (col == COL_EDGE_L || col == COL_EDGE_R) {
303
+ tile = BG_EDGE; /* shoulders */
304
+ } else if (col == COL_DIVIDER) {
305
+ tile = BG_EDGE; /* solid center line */
306
+ } else if (col == COL_DASH_1 || col == COL_DASH_2) {
307
+ tile = BG_DASH; /* dashed lane lines */
308
+ } else {
309
+ tile = (((row * 5 + col * 3) % 13) == 0) ? BG_SPECK : 0; /* tarmac */
156
310
  }
157
- /* else leave colour-0 grey road surface */
311
+ vram_unsafe_set((uint16_t)(base + col), tile);
158
312
  }
159
313
  }
160
- for (row = ROAD_TOP_ROW; row <= ROAD_BOT_ROW; row++) {
161
- base = (uint16_t)(0x2000 + (uint16_t)row * 32);
162
- vram_unsafe_set((uint16_t)(base + ROAD_EDGE_L), BG_T_EDGE);
163
- vram_unsafe_set((uint16_t)(base + ROAD_EDGE_R), BG_T_EDGE);
164
- vram_unsafe_set((uint16_t)(base + ROAD_DIV_1), BG_T_LANE);
165
- vram_unsafe_set((uint16_t)(base + ROAD_DIV_2), BG_T_LANE);
314
+ }
315
+
316
+ /* ── GAME LOGIC (clay) the title screen ──────────────────────────────────
317
+ * Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes). The road
318
+ * itself is the backdrop; text cells overwrite road cells (font pixels are
319
+ * colour 1 = white over the colour-0 asphalt backdrop). */
320
+ static void paint_title(void) {
321
+ uint8_t i;
322
+ uint16_t v;
323
+ uint8_t d[5];
324
+ ppu_off();
325
+ paint_road();
326
+ text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
327
+ text_draw_unsafe(0x2000 + 13 * 32 + 10, "1P RACE - A");
328
+ text_draw_unsafe(0x2000 + 15 * 32 + 9, "2P VERSUS - B");
329
+ /* Persistent best line — hand-painted digits (queued text needs rendering
330
+ * on; we're PPU-off here). */
331
+ text_draw_unsafe(0x2000 + 20 * 32 + 10, "BEST");
332
+ v = best;
333
+ for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
334
+ for (i = 0; i < 5; i++)
335
+ vram_unsafe_set((uint16_t)(0x2000 + 20 * 32 + 15 + i), (uint8_t)(0x40 + d[4 - i]));
336
+ road_scroll = 0;
337
+ ppu_scroll(0, 0);
338
+ oam_clear();
339
+ ppu_on_all();
340
+ }
341
+
342
+ /* ── GAME LOGIC (clay) — the result screen ── */
343
+ static void paint_over(void) {
344
+ ppu_off();
345
+ /* Same road backdrop as the title — a bare single-colour card looks like a
346
+ * render failure (the verify tool flags >92% one-colour frames). */
347
+ paint_road();
348
+ if (two_player) {
349
+ text_draw_unsafe(0x2000 + 10 * 32 + 12, winner ? "P2 WINS" : "P1 WINS");
350
+ text_draw_unsafe(0x2000 + 14 * 32 + 8, "RIVAL CRASHED OUT");
351
+ } else {
352
+ uint8_t i; uint16_t v; uint8_t d[5];
353
+ text_draw_unsafe(0x2000 + 9 * 32 + 12, "WRECKED");
354
+ text_draw_unsafe(0x2000 + 13 * 32 + 9, "DIST");
355
+ text_draw_unsafe(0x2000 + 15 * 32 + 9, "BEST");
356
+ v = dist;
357
+ for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
358
+ for (i = 0; i < 5; i++)
359
+ vram_unsafe_set((uint16_t)(0x2000 + 13 * 32 + 14 + i), (uint8_t)(0x40 + d[4 - i]));
360
+ v = best;
361
+ for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
362
+ for (i = 0; i < 5; i++)
363
+ vram_unsafe_set((uint16_t)(0x2000 + 15 * 32 + 14 + i), (uint8_t)(0x40 + d[4 - i]));
166
364
  }
365
+ text_draw_unsafe(0x2000 + 20 * 32 + 9, "START - TITLE");
366
+ road_scroll = 0;
367
+ ppu_scroll(0, 0);
368
+ oam_clear();
369
+ ppu_on_all();
167
370
  }
168
371
 
169
- static void reset_run(void) {
372
+ /* ── GAME LOGIC (clay) — start a run ── */
373
+ static void start_game(uint8_t versus) {
170
374
  uint8_t i;
171
- player_lane = 1;
172
- player.x = lane_x[1]; player.y = PLAYER_Y; player.alive = 1;
173
- for (i = 0; i < MAX_OBSTACLES; i++) obstacles[i].alive = 0;
174
- score = 0;
375
+ two_player = versus;
376
+ for (i = 0; i < MAX_TRAFFIC; i++) traffic_alive[i] = 0;
377
+ for (i = 0; i < 2; i++) {
378
+ crashes_left[i] = START_LIVES;
379
+ invuln[i] = 0;
380
+ prev_pad[i] = 0;
381
+ }
382
+ if (versus) {
383
+ car_active[0] = 1; car_active[1] = 1;
384
+ lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
385
+ lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
386
+ speed = SPEED_2P; /* shared road, fixed speed (see header) */
387
+ } else {
388
+ car_active[0] = 1; car_active[1] = 0;
389
+ lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
390
+ speed = 1;
391
+ }
392
+ dist = 0; dist_frac = 0;
393
+ hud_cached = 0xFFFF;
175
394
  spawn_timer = 0;
176
- game_over_timer = 0;
395
+ ppu_off();
396
+ paint_road();
397
+ road_scroll = 0;
398
+ prev_top_row = 0;
399
+ ppu_scroll(0, 0);
400
+ oam_clear();
401
+ ppu_on_all();
402
+ state = ST_PLAY;
177
403
  }
178
404
 
179
- static void spawn_obstacle(void) {
180
- uint8_t i;
181
- for (i = 0; i < MAX_OBSTACLES; i++) {
182
- if (!obstacles[i].alive) {
183
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
184
- obstacles[i].y = 0;
185
- obstacles[i].alive = 1;
186
- return;
405
+ static void game_over(void) {
406
+ if (!two_player && dist > best) {
407
+ best = dist;
408
+ /* ── HARDWARE IDIOM (load-bearing) — persists via battery PRG-RAM at
409
+ * $6000; works because the crt0's iNES header sets the BATTERY bit.
410
+ * See nes_runtime.c for the magic+checksum layout. ── */
411
+ hiscore_save(best);
412
+ }
413
+ state = ST_OVER;
414
+ paint_over();
415
+ }
416
+
417
+ /* ── GAME LOGIC (clay) — per-player input ───────────────────────────────────
418
+ * LEFT/RIGHT steer between lanes (edge-detected — held d-pad shouldn't
419
+ * machine-gun across the road). 1P only: A/UP accelerate, B/DOWN brake. */
420
+ static void update_player(uint8_t p) {
421
+ uint8_t pad = pad_poll(p);
422
+ uint8_t pressed = (uint8_t)(pad & ~prev_pad[p]);
423
+ prev_pad[p] = pad;
424
+ if (!car_active[p]) return;
425
+ if ((pressed & PAD_LEFT) && car_lane[p] > lane_min[p]) {
426
+ --car_lane[p];
427
+ sound_play_tone(0, 0x120, 5, 2); /* lane tick */
428
+ }
429
+ if ((pressed & PAD_RIGHT) && car_lane[p] < lane_max[p]) {
430
+ ++car_lane[p];
431
+ sound_play_tone(0, 0x120, 5, 2);
432
+ }
433
+ if (!two_player) { /* speed is shared — only 1P gets it */
434
+ if ((pressed & (PAD_A | PAD_UP)) && speed < 4) {
435
+ ++speed;
436
+ sound_play_tone(1, (uint16_t)(0x140 - speed * 0x30), 7, 4); /* engine */
437
+ }
438
+ if ((pressed & (PAD_B | PAD_DOWN)) && speed > 1) {
439
+ --speed;
440
+ sound_play_tone(1, 0x1C0, 4, 3); /* brake blip */
187
441
  }
188
442
  }
443
+ if (invuln[p] > 0) --invuln[p];
189
444
  }
190
445
 
191
- static void render_score(void) {
192
- /* Render a 5-digit decimal score as 5 OAM sprites in the top-right. */
193
- uint16_t v = score;
194
- uint8_t digits[5];
195
- int8_t i;
196
- for (i = 4; i >= 0; i--) { digits[i] = v % 10; v /= 10; }
197
- for (i = 0; i < 5; i++) {
198
- oam_spr((uint8_t)(192 + i * 8), 4,
199
- (uint8_t)(T_DIGIT0 + digits[i]), 0);
446
+ static void crash(uint8_t p) {
447
+ sound_play_noise(10, 12, 14);
448
+ invuln[p] = 60; /* blink + no-collide grace */
449
+ if (!two_player) speed = 1; /* a wreck kills your momentum */
450
+ if (crashes_left[p] > 0) --crashes_left[p];
451
+ if (crashes_left[p] == 0) {
452
+ winner = (uint8_t)(1 - p); /* versus: the OTHER player wins */
453
+ game_over();
200
454
  }
201
455
  }
202
456
 
203
457
  void main(void) {
204
- uint8_t i;
205
- uint8_t p1;
458
+ uint8_t i, p, pad;
459
+ uint8_t top_row;
206
460
 
461
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
462
+ * Init order: PPU off → CHR upload → palette → nametable (raw writes) →
463
+ * OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
464
+ * off (raw $2007 traffic during rendering corrupts the address latch
465
+ * mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
466
+ * PPUMASK bits — don't poke those registers directly alongside it. */
207
467
  ppu_off();
208
-
209
- chr_ram_upload(T_BLANK * 16, tile_blank, 16);
210
- chr_ram_upload(T_CAR_P1 * 16, tile_car_p1, 16);
211
- chr_ram_upload(T_CAR_ENEMY * 16, tile_car_enemy, 16);
212
- chr_ram_upload(T_DIGIT0 * 16, tile_digits, sizeof(tile_digits));
213
-
214
- /* BG road tiles live in pattern table 1 ($1000) — that's where the
215
- * default PPUCTRL ($90) tells the PPU to read background patterns. */
216
- chr_ram_upload((uint16_t)(0x1000 + BG_T_EDGE * 16), bg_tile_edge, 16);
217
- chr_ram_upload((uint16_t)(0x1000 + BG_T_LANE * 16), bg_tile_lane, 16);
218
- chr_ram_upload((uint16_t)(0x1000 + BG_T_GRASS * 16), bg_tile_grass, 16);
219
- chr_ram_upload((uint16_t)(0x1000 + BG_T_ROAD * 16), bg_tile_road, 16);
220
-
468
+ chr_ram_upload(0x0000, tile_blank, 16);
469
+ chr_ram_upload(TILE_CAR * 16, tile_car, 16);
470
+ chr_ram_upload(TILE_TRAFFIC * 16, tile_traffic, 16);
471
+ chr_ram_upload(TILE_DIGIT0 * 16, tile_digits, sizeof(tile_digits));
472
+ chr_ram_upload((uint16_t)(0x1000 + BG_EDGE * 16), bg_edge, 16);
473
+ chr_ram_upload((uint16_t)(0x1000 + BG_DASH * 16), bg_dash, 16);
474
+ chr_ram_upload((uint16_t)(0x1000 + BG_GRASS * 16), bg_grass, 16);
475
+ chr_ram_upload((uint16_t)(0x1000 + BG_TUFT * 16), bg_tuft, 16);
476
+ chr_ram_upload((uint16_t)(0x1000 + BG_TREE * 16), bg_tree, 16);
477
+ chr_ram_upload((uint16_t)(0x1000 + BG_SPECK * 16), bg_speck, 16);
478
+ font_upload();
221
479
  palette_load(palette);
222
- draw_road(); /* paint the static road while the PPU is off */
223
- oam_clear();
224
- ppu_on_all();
225
480
  sound_init();
226
481
 
227
- reset_run();
228
- prev_p1 = 0;
482
+ best = hiscore_load(); /* battery SRAM — 0 on first boot */
483
+ state = ST_TITLE;
484
+ paint_title();
229
485
 
230
486
  for (;;) {
487
+ if (state == ST_TITLE) {
488
+ /* ── GAME LOGIC (clay) — title: A = 1P race, B = 2P versus ── */
489
+ oam_clear();
490
+ ppu_wait_nmi();
491
+ sound_music_tick();
492
+ pad = pad_poll(0);
493
+ if ((pad & PAD_A) && !(prev_pad[0] & PAD_A)) { prev_pad[0] = pad; start_game(0); continue; }
494
+ if ((pad & PAD_B) && !(prev_pad[0] & PAD_B)) { prev_pad[0] = pad; start_game(1); continue; }
495
+ if ((pad & PAD_START) && !(prev_pad[0] & PAD_START)) { prev_pad[0] = pad; start_game(0); continue; }
496
+ prev_pad[0] = pad;
497
+ continue;
498
+ }
499
+
500
+ if (state == ST_OVER) {
501
+ /* Result card; START or A returns to the title. */
502
+ oam_clear();
503
+ ppu_wait_nmi();
504
+ sound_music_tick();
505
+ pad = pad_poll(0);
506
+ if ((pad & (PAD_START | PAD_A)) && !(prev_pad[0] & (PAD_START | PAD_A))) {
507
+ state = ST_TITLE;
508
+ paint_title();
509
+ }
510
+ prev_pad[0] = pad;
511
+ continue;
512
+ }
513
+
514
+ /* ── ST_PLAY ─────────────────────────────────────────────────────── */
515
+
516
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
517
+ * Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
518
+ * real OAM at the START of vblank, copying whatever shadow OAM holds AT
519
+ * THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites.
520
+ * (No sprite-0 split here — the HUD is sprites — so order past that is
521
+ * free; we stage cars first purely so they win sprite-priority ties.) */
231
522
  oam_clear();
232
- /* Player car */
233
- oam_spr(player.x, player.y, T_CAR_P1, 0);
234
- /* Obstacles */
235
- for (i = 0; i < MAX_OBSTACLES; i++) {
236
- if (obstacles[i].alive)
237
- oam_spr(obstacles[i].x, obstacles[i].y, T_CAR_ENEMY, 1);
523
+ for (p = 0; p < 2; p++) {
524
+ if (!car_active[p]) continue;
525
+ if (invuln[p] & 2) continue; /* crash blink: skip odd pairs */
526
+ oam_spr(lane_x[car_lane[p]], CAR_Y, TILE_CAR, p ? PAL_P2 : PAL_P1);
238
527
  }
239
- render_score();
528
+ for (i = 0; i < MAX_TRAFFIC; i++)
529
+ if (traffic_alive[i]) oam_spr(lane_x[traffic_lane[i]], traffic_y[i], TILE_TRAFFIC, PAL_TRAFFIC);
530
+ stage_hud();
240
531
 
241
532
  ppu_wait_nmi();
533
+ sound_music_tick();
242
534
 
243
- p1 = pad_poll(0);
244
-
245
- if (game_over_timer > 0) {
246
- game_over_timer--;
247
- if (game_over_timer == 0) reset_run();
248
- prev_p1 = p1;
249
- continue;
535
+ /* Scroll the road, then stream scenery into the row that just wrapped
536
+ * into the (overscan-hidden) top band. Both idioms documented above. */
537
+ scroll_road_down(speed);
538
+ top_row = (uint8_t)(road_scroll >> 3);
539
+ if (top_row != prev_top_row) {
540
+ prev_top_row = top_row;
541
+ stream_road_row(top_row);
250
542
  }
251
543
 
252
- /* ── Input switch lanes on left/right press (edge-detected). */
253
- if ((p1 & PAD_LEFT) && !(prev_p1 & PAD_LEFT) && player_lane > 0) {
254
- player_lane--;
255
- sound_play_tone(0, 0x180, 6, 3);
544
+ /* ── GAME LOGIC (clay) from here down ── */
545
+ update_player(0);
546
+ if (two_player) update_player(1);
547
+ if (state != ST_PLAY) continue; /* a crash may have ended the game */
548
+
549
+ /* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every 256
550
+ * units marks a checkpoint. */
551
+ if (!two_player) {
552
+ dist_frac = (uint8_t)(dist_frac + speed);
553
+ if (dist_frac >= 16) {
554
+ dist_frac -= 16;
555
+ if (dist < 65535u) ++dist;
556
+ if (dist != 0 && (dist & 0xFF) == 0)
557
+ sound_play_tone(0, 0x0D6, 8, 10); /* checkpoint chime (C6) */
558
+ }
256
559
  }
257
- if ((p1 & PAD_RIGHT) && !(prev_p1 & PAD_RIGHT) && player_lane < 2) {
258
- player_lane++;
259
- sound_play_tone(0, 0x180, 6, 3);
560
+
561
+ /* Traffic flows down at road speed (it reads as slower cars you're
562
+ * overtaking); despawn past the bottom with a little pass tick. */
563
+ for (i = 0; i < MAX_TRAFFIC; i++) {
564
+ if (!traffic_alive[i]) continue;
565
+ if (traffic_y[i] >= (uint8_t)(224 - speed)) {
566
+ traffic_alive[i] = 0;
567
+ sound_play_tone(1, 0x0C0, 2, 2);
568
+ } else {
569
+ traffic_y[i] = (uint8_t)(traffic_y[i] + speed);
570
+ }
260
571
  }
261
- player.x = lane_x[player_lane];
262
- prev_p1 = p1;
263
-
264
- /* ── Obstacles slide down ─────────────────────────────────── */
265
- for (i = 0; i < MAX_OBSTACLES; i++) {
266
- if (!obstacles[i].alive) continue;
267
- if (obstacles[i].y < 232) obstacles[i].y += 2;
268
- else obstacles[i].alive = 0;
572
+ if (++spawn_timer >= SPAWN_PERIOD) {
573
+ spawn_timer = 0;
574
+ spawn_traffic();
269
575
  }
270
- if (++spawn_timer >= 36) { spawn_timer = 0; spawn_obstacle(); }
271
-
272
- /* Collisions */
273
- for (i = 0; i < MAX_OBSTACLES; i++) {
274
- if (!obstacles[i].alive) continue;
275
- if (aabb(&player, &obstacles[i])) {
276
- sound_play_noise(8, 8, 16);
277
- game_over_timer = 60;
278
- break;
576
+
577
+ /* Traffic ↔ cars. Crash grace: a just-wrecked car blinks and can't
578
+ * collide for 60 frames. */
579
+ for (i = 0; i < MAX_TRAFFIC; i++) {
580
+ if (!traffic_alive[i]) continue;
581
+ for (p = 0; p < 2; p++) {
582
+ if (!car_active[p] || invuln[p]) continue;
583
+ if (hits(lane_x[traffic_lane[i]], traffic_y[i], lane_x[car_lane[p]], CAR_Y)) {
584
+ traffic_alive[i] = 0;
585
+ crash(p);
586
+ if (state != ST_PLAY) break;
587
+ }
279
588
  }
589
+ if (state != ST_PLAY) break;
280
590
  }
281
-
282
- if (score < 65500u) score++;
283
591
  }
284
592
  }