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