romdevtools 0.28.0 → 0.30.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 (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -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 +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -1,305 +1,880 @@
1
- /*
2
- * PC Engine "racing" — a top-down lane-racer scaffold.
1
+ /* ── main.c — PC Engine top-down road racer (complete example game) ───────────
3
2
  *
4
- * Drive a car at the bottom of the screen up a 3-lane road. LEFT/RIGHT switch
5
- * lanes; obstacle cars spawn at the top and slide down toward you. Dodge them —
6
- * a collision freezes the game for a beat, then auto-resets. The road scrolls
7
- * (dashed lane stripes animate via the BG Y-scroll register) and speed grows
8
- * with your distance score. Mirrors the NES/Genesis/SNES/GB/SMS racing
9
- * scaffolds, translated to the PCE helper API.
3
+ * PINION PURSUIT a COMPLETE, working game: title screen, 1P endless race with
4
+ * speed control, 2P simultaneous SPLIT-LANE VERSUS (both cars on screen at once,
5
+ * P2 on the TurboTap's second pad), a vertically-scrolling road done the PC
6
+ * Engine way (hardware BG Y-scroll via the VDC's BYR register), streamed
7
+ * roadside scenery as the road wraps, crash/lives rules, in-session best
8
+ * distance (a bare HuCard can't save — see the best-distance note), PSG music + SFX.
10
9
  *
11
- * Cars are hardware sprites; the road (grey lanes between green shoulders with
12
- * animated dashed lane lines) is the BG tilemap, so the screen is clearly a
13
- * road scene (clears the verify gate).
10
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
11
+ * very different one. The markers tell you what's what:
12
+ * HARDWARE IDIOM (load-bearing) dodges a documented PCE footgun; reshape
13
+ * your gameplay around it (see TROUBLESHOOTING before changing).
14
+ * GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
14
15
  *
15
- * PCE notes (see pce_hw.h / MENTAL_MODEL.md):
16
- * - disp_enable() turns on BG + sprites + the VBlank IRQ (waitvsync needs it).
17
- * - the road scroll is BG Y-scroll: vdc_set_reg(VDC_BYR, scroll).
18
- * - .bss must be non-empty (pce_video.c's _pce_keep[] covers it).
16
+ * What depends on what:
17
+ * pce_hw.h / pce_video.c / pce_input.c / pce_sound.c the helper lib
18
+ * (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
19
+ * pce_video.c say which parts are load-bearing.
20
+ * cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
21
+ * (applied automatically to example projects) gives a 32KB HuCard.
19
22
  *
20
- * cc65 is C89 declare locals at the top of a block.
23
+ * THE DESIGN (read before reshaping):
24
+ * Scrolling — the road is the BACKGROUND, scrolled DOWN by INCREMENTING the
25
+ * VDC's BYR register each frame (driving up = the road slides toward you).
26
+ * The PCE wins here: its BAT is a 32x32 (256px-tall) virtual map and the
27
+ * VDC masks BYR to the plane IN HARDWARE, so `road_scroll += speed` on a
28
+ * plain u8 is the whole idiom — 256 wraps seamlessly forever. Compare the
29
+ * NES racing template (examples/nes/templates/racing.c): there a nametable
30
+ * is only 240px tall, scroll_y 240-255 fetches attribute bytes as garbage
31
+ * tiles, and EVERY scroll change must run through a 240-wrap helper. The
32
+ * SMS (examples/sms/templates/racing.c) wraps at 224. On the PCE there is
33
+ * no wrap math at all. Cars/traffic are hardware sprites with their own Y.
34
+ * Streamed scenery — see the BYR idiom below: as the road wraps, the BAT row
35
+ * re-entering at the top gets restamped with fresh random roadside so the
36
+ * 256-px loop never shows the same scenery twice. The swap hides under the
37
+ * HUD band (the PCE's curtain — same trick the Genesis window HUD plays).
38
+ * HUD — the PCE has no hardware window plane and this minimal lib does no
39
+ * raster split, so (like the platformer template's painted-band HUD) the
40
+ * status row is BAT tiles at the top. Because BYR scrolls the WHOLE BG, we
41
+ * keep the HUD readable by parking it in BAT rows the scroll never exposes:
42
+ * the road only ever occupies the play band, and the top 2 BAT rows hold a
43
+ * fixed HUD band repainted with each scenery stream so it reads continuous.
44
+ * 2P VERSUS — ONE VDC means ONE road scroll, so both players share one road
45
+ * at a fixed speed and only STEER (the same constraint the NES/Genesis
46
+ * versions explain): solid center divider, P1 (cyan, port 0) owns the left
47
+ * two lanes, P2 (amber, TurboTap port 1) the right two. Each starts with 3
48
+ * crashes; first to use them all LOSES.
49
+ * 1P RACE — all four lanes, UP/I accelerates, DOWN/II brakes (speed 1-4);
50
+ * 3 crashes end the run. Persistent stat: best DISTANCE (u16, one unit =
51
+ * 16 scrolled pixels ≈ one car length); in-session only (see the note below).
52
+ *
53
+ * 2P, honestly: the stock PC Engine has ONE controller port; 2P needs a
54
+ * TurboTap. The geargrafx core implements the TurboTap and the romdev host
55
+ * force-ENABLES it (PLATFORM_CORE_OPTIONS pce: geargrafx_turbotap), so a second
56
+ * pad's input reaches the game on pad slot 2 — verified by driving port-1 input
57
+ * and seeing car 2 move. So this game ships REAL simultaneous 2P versus. (On
58
+ * real hardware the player plugs a TurboTap and a second pad.)
59
+ *
60
+ * Frame budget (NTSC, 60fps, 7.16MHz 65C02-class CPU): 4 traffic + 2 cars AABB,
61
+ * one BAT row restamp at most every other frame, an 8-entry SATB copy in
62
+ * vblank — a tiny fraction of a frame. Hardware BYR scroll is one register.
21
63
  */
22
64
  #include <pce.h>
23
- #include <stdint.h> /* int16_t for the per-frame speed step */
65
+ #include <stdint.h> /* int16_t for the per-frame speed step */
66
+ #include <joystick.h> /* JOY_2 + joy_read for the 2nd pad (TurboTap port 1) */
24
67
  #include "pce_hw.h"
25
68
 
26
- /* ---- VRAM layout (word addresses) --------------------------------------- */
27
- #define BAT_VRAM 0x0000
28
- #define FONT_VRAM 0x1000
29
- #define GRASS_VRAM 0x1400 /* shoulder grass (colour 1) */
30
- #define ROAD_VRAM 0x1410 /* plain road (colour 2) */
31
- #define DASH_VRAM 0x1420 /* road with a lane dash (colour 3) */
32
- #define PLAYER_VRAM 0x1800 /* 16x16 player car */
33
- #define ENEMY_VRAM 0x1840 /* 16x16 enemy car */
69
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
70
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
71
+ #define GAME_TITLE "PINION PURSUIT"
72
+
73
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
74
+ * VRAM map (WORD addresses — the VDC is a 16-bit-word machine; an 8x8 tile is
75
+ * 16 words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
76
+ * VRAM, so lay it out ONCE and keep the SATB out of pattern space:
77
+ * $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
78
+ * $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash) — BG text only
79
+ * $1400 road furniture tiles (grass, asphalt, dash, edge, divider, band)
80
+ * $1800 16x16 sprite cells: player car, traffic car
81
+ * $1900 16x16 sprite DIGIT cells (0-9) for the SPRITE HUD (see HUD idiom) */
82
+ #define BAT_VRAM 0x0000
83
+ #define FONT_VRAM 0x1000
84
+ #define GRASS_VRAM 0x1400 /* roadside grass (BG colour 1) */
85
+ #define ROAD_VRAM 0x1410 /* asphalt (BG colour 2) */
86
+ #define DASH_VRAM 0x1420 /* asphalt + a colour-3 lane dash */
87
+ #define EDGE_VRAM 0x1430 /* solid colour-3 shoulder / centre divider */
88
+ #define BAND_VRAM 0x1440 /* flat band behind the title/result text */
89
+ #define PLAYER_VRAM 0x1800 /* 16x16 player car */
90
+ #define ENEMY_VRAM 0x1840 /* 16x16 traffic car */
91
+ #define SDIGIT_VRAM 0x1900 /* 10 consecutive 16x16 digit cells (0..9) */
34
92
 
35
93
  #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
36
94
 
37
- #define LANE_L_X 76
38
- #define LANE_M_X 120
39
- #define LANE_R_X 164
40
- #define PLAYER_Y 176
41
- #define MAX_OBST 4
95
+ /* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
96
+ #define PLAYER_PAT (PLAYER_VRAM >> 6)
97
+ #define ENEMY_PAT (ENEMY_VRAM >> 6)
98
+ #define SDIGIT_PAT (SDIGIT_VRAM >> 6) /* digit d → SDIGIT_PAT + d (cells are *4 words apart = +1 pattern code) */
99
+
100
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
101
+ * Road geometry. Four 4-cell-wide lanes between shoulders, a solid centre
102
+ * divider (it's also the 2P territory line). BAT columns (cells):
103
+ * 8 = left shoulder, 12/20 = dashed lane lines, 16 = centre divider,
104
+ * 24 = right shoulder; grass outside. The BAT is 32 cells (256px) wide. */
105
+ #define COL_EDGE_L 8
106
+ #define COL_DASH_1 12
107
+ #define COL_DIVIDER 16
108
+ #define COL_DASH_2 20
109
+ #define COL_EDGE_R 24
110
+ /* Lane centre X for the 16px-wide car sprite (lane i spans 32 px). */
111
+ static const u16 lane_x[4] = { 80, 112, 144, 176 };
112
+
113
+ #define MAX_TRAFFIC 4 /* sprite slots 2-5 (0=P1, 1=P2) */
114
+ #define CAR_Y 176 /* both players' fixed screen Y */
115
+ #define SPAWN_Y 28 /* traffic entry Y — BELOW the sprite HUD line */
116
+ #define HUD_Y 8 /* sprite HUD scanline (digits live here) */
117
+ #define DESPAWN_Y 216 /* traffic exits past the player */
118
+ #define START_LIVES 3 /* crashes per run / per player */
119
+ #define SPAWN_PERIOD 40 /* frames between traffic spawns — traffic moves
120
+ * at road speed, so per-meter density stays
121
+ * constant whatever the player does */
122
+ #define SPEED_2P 2 /* fixed road speed in versus (one VDC = one
123
+ * scroll = one shared speed; see the design) */
124
+ #define MAX_SPEED 4 /* px/frame — MUST stay under 8: the row
125
+ * streamer restamps one row per 8px crossing
126
+ * and a >8px step could skip a row */
127
+
128
+ /* SATB slot plan (slot order = priority): 0 = P1, 1 = P2, 2-5 = traffic,
129
+ * 6-11 = the 6 sprite-HUD digits (see the HUD idiom). PAL plan: cars on their
130
+ * own sprite sub-palettes so P1/P2/traffic read as three liveries; digits on
131
+ * the HUD palette. */
132
+ #define SLOT_P1 0
133
+ #define SLOT_P2 1
134
+ #define SLOT_TRAFFIC 2
135
+ #define SLOT_HUD 6 /* slots 6..11: crash digit + 5 distance digits */
136
+ #define PAL_P1 0
137
+ #define PAL_P2 1
138
+ #define PAL_TRAFFIC 2
139
+ #define PAL_HUD 3
140
+ #define OFFSCREEN_Y 0x1F0 /* park hidden sprites below the display */
141
+
142
+ /* ── GAME LOGIC (clay — reshape freely) ── game state ── */
143
+ /* Players: index 0 = P1 (port 0), 1 = P2 (TurboTap port 1, versus only). */
144
+ static u8 car_lane[2];
145
+ static u8 car_active[2];
146
+ static u8 crashes_left[2];
147
+ static u8 invuln[2]; /* post-crash blink/no-collide frames */
148
+ static u8 lane_cd[2]; /* steer cooldown frames (latency-robust) */
149
+ static u8 prev_pads[2];
150
+ static u8 lane_min[2], lane_max[2]; /* 2P: split territories */
151
+ static u8 two_player;
152
+ static u8 winner; /* versus result: 0 = P1, 1 = P2 */
42
153
 
43
- /* ---- font (digits only) ------------------------------------------------- */
44
- #define NUM_GLYPHS 10
45
- static const u8 FONT5x7[NUM_GLYPHS][7] = {
46
- {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E},
47
- {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
48
- {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F},
49
- {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
50
- {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02},
51
- {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
52
- {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E},
53
- {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
54
- {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E},
55
- {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C}
56
- };
57
-
58
- /* ---- state -------------------------------------------------------------- */
59
154
  typedef struct { u16 x, y; u8 alive; } Car;
155
+ static Car traffic[MAX_TRAFFIC];
60
156
 
61
- static Car player;
62
- static Car obst[MAX_OBST];
63
- static u16 score;
157
+ static u8 speed; /* road px/frame, 1..MAX_SPEED */
158
+ static u16 dist; /* 1P distance, 1 unit = 16 scrolled px */
159
+ static u8 dist_frac;
160
+ static u16 best; /* persisted best 1P distance */
64
161
  static u8 spawn_timer;
65
- static u8 crash_timer;
66
- static u8 player_lane;
67
- static u8 road_scroll;
68
- static u16 rng;
69
- static u8 pad, prev_pad;
162
+ static u8 road_scroll; /* BG Y scroll. NEVER wrapped by hand: the BAT
163
+ * is 256px tall, the VDC masks BYR to the
164
+ * plane, and 256 wrapping a u8 is seamless —
165
+ * see the BYR idiom (the NES needs a 240-wrap
166
+ * helper here, the SMS a 224-wrap). */
167
+ static u8 prev_top_row; /* last restamped BAT row */
168
+ static u8 start_pause; /* green-light freeze frames */
70
169
  static u8 sfx_timer;
71
- static u16 tile_buf[16];
72
- static u16 spr_buf[64];
170
+ static u8 state; /* ST_TITLE / ST_PLAY / ST_OVER */
171
+
172
+ /* Game states — the shell every example shares: title → play → game over. */
173
+ #define ST_TITLE 0
174
+ #define ST_PLAY 1
175
+ #define ST_OVER 2
176
+
177
+ static u16 tile_buf[16]; /* scratch for one 8x8 tile */
178
+ static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
73
179
 
74
- static const u16 lane_x[3] = { LANE_L_X, LANE_M_X, LANE_R_X };
180
+ /* ── GAME LOGIC (clay) 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
181
+ * Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
182
+ * them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
183
+ #define G_BLANK 0
184
+ #define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
185
+ #define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
186
+ #define G_DASH 37
187
+ #define NUM_GLYPHS 38
75
188
 
189
+ static const u8 FONT5x7[NUM_GLYPHS][7] = {
190
+ {0,0,0,0,0,0,0},
191
+ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
192
+ {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
193
+ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
194
+ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
195
+ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
196
+ {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
197
+ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
198
+ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
199
+ {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
200
+ {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
201
+ {0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
202
+ {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
203
+ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
204
+ {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
205
+ {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
206
+ {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
207
+ {0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
208
+ {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
209
+ {0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
210
+ };
211
+
212
+ /* ── GAME LOGIC (clay) — 16x16 car sprite mask (16 rows × 16 bits, bit15 left).
213
+ * A blocky top-down car: cabin, windows, wheels. Colour is the PALETTE, not the
214
+ * bits (one shape, three sub-palettes → P1 cyan, P2 amber, traffic red). */
215
+ static const u16 car_mask[16] = {
216
+ 0x0660, 0x0660, 0x3FFC, 0x7FFE, 0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6,
217
+ 0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6, 0x7FFE, 0x3FFC, 0x6006, 0x6006
218
+ };
219
+
220
+ /* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
76
221
  static void make_solid_tile(u16 *t, u8 ci) {
77
222
  u8 r;
78
223
  u8 p0 = (ci & 1) ? 0xFF : 0x00;
79
224
  u8 p1 = (ci & 2) ? 0xFF : 0x00;
80
- u8 p2 = (ci & 4) ? 0xFF : 0x00;
81
- u8 p3 = (ci & 8) ? 0xFF : 0x00;
82
225
  for (r = 0; r < 8; ++r) {
83
226
  t[r] = (u16)(p0 | (p1 << 8));
84
- t[r + 8] = (u16)(p2 | (p3 << 8));
227
+ t[r + 8] = 0;
85
228
  }
86
229
  }
87
230
 
88
- /* road tile with a centred lane dash in colour 3 (top half of the tile) */
231
+ /* speckled grass: colour-1 body with a few colour-2 specks so the vertical
232
+ * scroll reads (a flat colour shifted N px looks identical to itself). */
233
+ static void make_grass_tile(u16 *t) {
234
+ make_solid_tile(t, 1); /* body = colour 1 (plane0) */
235
+ t[1] |= 0x1000; /* row 1: one plane1 speck → colour 3 */
236
+ t[5] |= 0x0400; /* row 5: another speck */
237
+ }
238
+
239
+ /* asphalt: colour-2 body with a sparse colour-3 speck for the same reason. */
240
+ static void make_road_tile(u16 *t) {
241
+ make_solid_tile(t, 2); /* body = colour 2 (plane1) */
242
+ t[3] |= 0x0008; /* a single colour-3 speck */
243
+ }
244
+
245
+ /* road tile with a centred colour-3 lane dash on the top 4 rows. */
89
246
  static void make_dash_tile(u16 *t) {
90
247
  u8 r;
91
- make_solid_tile(t, 2); /* base road (colour 2) */
92
- for (r = 0; r < 4; ++r) {
93
- /* centre 4px (mask 0x18) -> colour 3 (planes 0+1): add plane0 bits */
94
- t[r] = (u16)((t[r] & 0xFF00) | 0x18 | (t[r] & 0x00FF));
95
- }
248
+ make_road_tile(t);
249
+ for (r = 0; r < 4; ++r) t[r] |= 0x0018; /* centre 2px → colour 3 (dash) */
96
250
  }
97
251
 
98
- static void make_car_sprite(u16 vram, u8 ci) {
99
- static const u16 car[16] = {
100
- 0x0660, 0x0660, 0x3FFC, 0x7FFE, 0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6,
101
- 0x7FFE, 0x7FFE, 0x6FF6, 0x6FF6, 0x7FFE, 0x3FFC, 0x6006, 0x6006
102
- };
252
+ /* one-colour 16x16 sprite cell from a 16-row mask (colour = plane0 → index 1) */
253
+ static void make_sprite16(u16 vram, const u16 *mask) {
103
254
  u8 r;
104
255
  for (r = 0; r < 64; ++r) spr_buf[r] = 0;
105
- for (r = 0; r < 16; ++r) {
106
- if (ci & 1) spr_buf[r] = car[r];
107
- if (ci & 2) spr_buf[r + 16] = car[r];
108
- if (ci & 4) spr_buf[r + 32] = car[r];
109
- }
256
+ for (r = 0; r < 16; ++r) spr_buf[r] = mask[r]; /* plane 0 → colour 1 */
110
257
  load_tiles(vram, spr_buf, 64);
111
258
  }
112
259
 
113
260
  static void upload_font(void) {
114
- u8 g, row, bits, plane0;
261
+ u8 g, row, bits, px;
115
262
  for (g = 0; g < NUM_GLYPHS; ++g) {
116
263
  for (row = 0; row < 16; ++row) tile_buf[row] = 0;
117
264
  for (row = 0; row < 7; ++row) {
118
265
  bits = FONT5x7[g][row];
119
- plane0 = 0;
120
- if (bits & 0x10) plane0 |= 0x40;
121
- if (bits & 0x08) plane0 |= 0x20;
122
- if (bits & 0x04) plane0 |= 0x10;
123
- if (bits & 0x02) plane0 |= 0x08;
124
- if (bits & 0x01) plane0 |= 0x04;
125
- tile_buf[row] = (u16)plane0;
266
+ px = 0;
267
+ if (bits & 0x10) px |= 0x40;
268
+ if (bits & 0x08) px |= 0x20;
269
+ if (bits & 0x04) px |= 0x10;
270
+ if (bits & 0x02) px |= 0x08;
271
+ if (bits & 0x01) px |= 0x04;
272
+ tile_buf[row] = (u16)px;
126
273
  }
127
274
  load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
128
275
  }
129
276
  }
130
277
 
131
- /* Paint the road: grass shoulders, grey road in the middle, dashed lane lines
132
- * between the three lanes. Player X spans ~76..164 -> BAT cols ~9..22. */
133
- static void draw_road(void) {
134
- u8 r, c;
135
- u16 grass = BAT_ENTRY(0, GRASS_VRAM);
136
- u16 road = BAT_ENTRY(0, ROAD_VRAM);
137
- u16 dash = BAT_ENTRY(0, DASH_VRAM);
138
- u16 e;
139
- for (r = 0; r < 32; ++r) {
140
- vram_set_write_addr((u16)(BAT_VRAM + r * 32));
141
- for (c = 0; c < 32; ++c) {
142
- if (c < 8 || c > 23) {
143
- e = grass;
144
- } else if ((c == 12 || c == 17) && (r & 1)) {
145
- e = dash; /* dashed lane dividers, every other row */
146
- } else {
147
- e = road;
148
- }
149
- VDC_DATA_LO = (u8)(e & 0xFF);
150
- VDC_DATA_HI = (u8)(e >> 8);
278
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
279
+ * SPRITE HUD digits. The PCE has NO hardware window plane and this minimal lib
280
+ * does no raster split, so a BAT-tile HUD would scroll WITH the road under BYR
281
+ * (and the road-row STREAMER below restamps every BAT row in turn, wiping any
282
+ * tile HUD outright). The honest fix — the same one the NES racing template
283
+ * uses — is a SPRITE HUD: sprites are positioned in SCREEN space and never move
284
+ * with a BG scroll. We build 10 digit cells here and stage them at HUD_Y every
285
+ * frame. Traffic spawns BELOW HUD_Y so the HuC6270's 16-sprites-per-scanline
286
+ * limit is never hit (6 HUD digits + 0 traffic share the line).
287
+ * requires: digit cells consecutive from SDIGIT_VRAM; stage_hud() each frame. */
288
+ static void upload_sprite_digits(void) {
289
+ u8 d, row, bits, px;
290
+ for (d = 0; d < 10; ++d) {
291
+ for (row = 0; row < 64; ++row) spr_buf[row] = 0;
292
+ /* reuse the 5x7 glyph for digit d (G_DIGIT + d), centred in the cell */
293
+ for (row = 0; row < 7; ++row) {
294
+ bits = FONT5x7[G_DIGIT + d][row];
295
+ px = 0;
296
+ if (bits & 0x10) px |= 0x40;
297
+ if (bits & 0x08) px |= 0x20;
298
+ if (bits & 0x04) px |= 0x10;
299
+ if (bits & 0x02) px |= 0x08;
300
+ if (bits & 0x01) px |= 0x04;
301
+ spr_buf[row] = (u16)px; /* plane 0 → colour 1 (white) */
151
302
  }
303
+ load_tiles((u16)(SDIGIT_VRAM + d * 64), spr_buf, 64);
152
304
  }
153
305
  }
154
306
 
155
- static void put_glyph(u8 col, u8 row, u8 digit) {
156
- u16 e = BAT_ENTRY(0, (u16)(FONT_VRAM + digit * 16));
307
+ static void upload_art(void) {
308
+ upload_font();
309
+ make_grass_tile(tile_buf); load_tiles(GRASS_VRAM, tile_buf, 16);
310
+ make_road_tile(tile_buf); load_tiles(ROAD_VRAM, tile_buf, 16);
311
+ make_dash_tile(tile_buf); load_tiles(DASH_VRAM, tile_buf, 16);
312
+ make_solid_tile(tile_buf, 3); load_tiles(EDGE_VRAM, tile_buf, 16);
313
+ make_solid_tile(tile_buf, 2); load_tiles(BAND_VRAM, tile_buf, 16);
314
+ make_sprite16(PLAYER_VRAM, car_mask);
315
+ make_sprite16(ENEMY_VRAM, car_mask);
316
+ upload_sprite_digits();
317
+ }
318
+
319
+ /* ── GAME LOGIC (clay) — BAT text helpers ────────────────────────────────── */
320
+ static void put_glyph(u8 col, u8 row, u8 glyph) {
321
+ u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
157
322
  vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
158
323
  VDC_DATA_LO = (u8)(e & 0xFF);
159
324
  VDC_DATA_HI = (u8)(e >> 8);
160
325
  }
161
326
 
162
- static void draw_score(void) {
163
- u16 v = score;
164
- u8 d0, d1, d2, d3;
165
- d3 = (u8)(v % 10); v /= 10;
166
- d2 = (u8)(v % 10); v /= 10;
167
- d1 = (u8)(v % 10); v /= 10;
168
- d0 = (u8)(v % 10);
169
- put_glyph(1, 1, d0);
170
- put_glyph(2, 1, d1);
171
- put_glyph(3, 1, d2);
172
- put_glyph(4, 1, d3);
327
+ static void put_tile(u8 col, u8 row, u16 e) {
328
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
329
+ VDC_DATA_LO = (u8)(e & 0xFF);
330
+ VDC_DATA_HI = (u8)(e >> 8);
173
331
  }
174
332
 
175
- static u8 aabb(Car *a, Car *b) {
176
- return (u8)(a->x < b->x + 14 && a->x + 14 > b->x &&
177
- a->y < b->y + 14 && a->y + 14 > b->y);
333
+ static void draw_text(u8 col, u8 row, const char *s) {
334
+ u8 c;
335
+ while ((c = (u8)*s++) != 0) {
336
+ u8 g = G_BLANK;
337
+ if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
338
+ else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
339
+ else if (c == '-') g = G_DASH;
340
+ put_glyph(col++, row, g);
341
+ }
178
342
  }
179
343
 
180
- static u16 next_rand(void) {
181
- rng = (u16)(rng * 25173u + 13849u);
182
- return rng;
344
+ static void draw_num5(u8 col, u8 row, u16 v) {
345
+ u8 i, d[5];
346
+ for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
347
+ for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
183
348
  }
184
349
 
185
- static void reset_run(void) {
186
- u8 i;
187
- player_lane = 1;
188
- player.x = lane_x[1];
189
- player.y = PLAYER_Y;
190
- player.alive = 1;
191
- for (i = 0; i < MAX_OBST; ++i) obst[i].alive = 0;
192
- score = 0;
193
- spawn_timer = 0;
194
- crash_timer = 0;
350
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG ───────────────────────────────────── */
351
+ static u16 rng = 0xBEEF;
352
+ static u8 random8(void) {
353
+ u16 r = rng;
354
+ r ^= r << 7;
355
+ r ^= r >> 9;
356
+ r ^= r << 8;
357
+ rng = r;
358
+ return (u8)r;
359
+ }
360
+
361
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
362
+ * HARDWARE BG Y-SCROLL via BYR + STREAMED ROWS — the PCE's road. The BAT is a
363
+ * 32x32 (256px-tall) virtual map and the VDC's R8 (BYR) shifts the whole
364
+ * background vertically with ZERO CPU per pixel. Screen line y shows plane line
365
+ * (BYR + y) & 255, so DECREMENTING road_scroll slides the road DOWN — the
366
+ * driving-up illusion — for one register write per frame. The BAT is 256px tall
367
+ * and the VDC masks BYR to it IN HARDWARE, so a plain u8 wraps at 256 seamlessly
368
+ * forever — the NES racing template (examples/nes/templates/racing.c) needs a
369
+ * 240-wrap helper here (a nametable is 240px tall; scroll_y 240-255 fetches
370
+ * attribute bytes as garbage tiles), and the SMS a 224-wrap. On the PCE there
371
+ * is no wrap math at all.
372
+ *
373
+ * The 32 BAT rows recycle as road_scroll moves: the row crossing into the top
374
+ * of the screen is BAT row (road_scroll >> 3) & 31. The moment it changes we
375
+ * restamp that ONE row with fresh random roadside, so the 256-px loop never
376
+ * shows the same scenery twice. Two rules:
377
+ * 1. Restamp with the address latch armed by vram_set_write_addr() — a row
378
+ * is 32 contiguous BAT words, so one latch + 32 word writes does it.
379
+ * 2. Road speed stays under 8 px/frame (MAX_SPEED) so a frame never skips a
380
+ * whole row crossing (the streamer restamps one row per crossing).
381
+ * The HUD is SPRITES (see upload_sprite_digits), so unlike the NES (overscan
382
+ * band) or Genesis (window plane) there's no BG "curtain" to hide a restamp —
383
+ * the restamp lands at the very top edge and the dashes/edges are identical
384
+ * tiles row to row, so the only thing that changes is the random grass speckle,
385
+ * which reads as roadside texture, not a pop.
386
+ *
387
+ * requires: BYR written every frame (we do, in the loop); each BAT row painted
388
+ * when it enters; the BAT 32x32 (vdc_init's MWR). */
389
+ static u16 road_cell(u8 c) {
390
+ if (c == COL_EDGE_L || c == COL_EDGE_R || c == COL_DIVIDER)
391
+ return BAT_ENTRY(0, EDGE_VRAM); /* shoulders + divider */
392
+ if (c == COL_DASH_1 || c == COL_DASH_2)
393
+ return BAT_ENTRY(0, DASH_VRAM); /* dashed lane lines */
394
+ if (c > COL_EDGE_L && c < COL_EDGE_R)
395
+ return BAT_ENTRY(0, ROAD_VRAM); /* asphalt */
396
+ return BAT_ENTRY(0, GRASS_VRAM); /* roadside grass */
397
+ }
398
+
399
+ /* Restamp one BAT row with fresh roadside (the dashes/edges are fixed; only the
400
+ * grass speckle phase changes per row via the road_cell tiles themselves). */
401
+ static void paint_road_row(u8 row) {
402
+ u8 c;
403
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32));
404
+ for (c = 0; c < 32; ++c) {
405
+ u16 e = road_cell(c);
406
+ VDC_DATA_LO = (u8)(e & 0xFF);
407
+ VDC_DATA_HI = (u8)(e >> 8);
408
+ }
409
+ }
410
+
411
+ /* Initial full road paint (all 32 rows) — used on (re)entering the race. */
412
+ static void paint_road(void) {
413
+ u8 r;
414
+ for (r = 0; r < 32; ++r) paint_road_row(r);
415
+ }
416
+
417
+ /* Advance the road by `px` pixels: one BYR write + at most one row restamp.
418
+ * DECREMENT so the road slides DOWN (driving up); the u8 wraps at 256 — idiom. */
419
+ static void advance_road(u8 px) {
420
+ u8 top_row;
421
+ road_scroll = (u8)(road_scroll - px); /* hardware wraps at 256 — idiom */
422
+ vdc_set_reg(VDC_BYR, (u16)road_scroll);
423
+ top_row = (u8)((road_scroll >> 3) & 31);
424
+ if (top_row != prev_top_row) {
425
+ prev_top_row = top_row;
426
+ paint_road_row(top_row);
427
+ }
428
+ }
429
+
430
+ /* ── HARDWARE TRUTH: a bare HuCard CANNOT save the best distance (in-session) ──
431
+ * This was researched and corrected: earlier versions wrote the best distance
432
+ * to BRAM ("backup RAM", bank $F7) and claimed it persisted across power
433
+ * cycles. That is NOT honest for a HuCard game. On REAL hardware a plain HuCard
434
+ * plugged into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM
435
+ * exists ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
436
+ * supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
437
+ * commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
438
+ * "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
439
+ * emulator like geargrafx exposes BRAM unconditionally, so the old code
440
+ * "worked" in emulation in a way the real machine never would.
441
+ *
442
+ * So this game keeps an IN-SESSION best only (like the honest 2600/Lynx
443
+ * examples) — it survives across runs within a power-on, resets to 0 on a cold
444
+ * boot. To ACTUALLY persist on real hardware you would target a peripheral
445
+ * (BRAM behind a detect, or a CD-ROM² build) — a real-hardware feature, not a
446
+ * property of the cartridge. */
447
+ static u16 best_load(void) {
448
+ return 0; /* cold boot: no persistence on a bare HuCard */
449
+ }
450
+
451
+ static void best_save(u16 v) {
452
+ (void)v; /* in-session only — nowhere to persist on real HW */
453
+ }
454
+
455
+ static void best_init(void) {
456
+ best = best_load(); /* always 0 — in-session best starts fresh each boot */
457
+ }
458
+
459
+ /* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
460
+ * PSG channel plan: 5 = melody, 4 = bass, 0-3 = SFX (tones cut by sfx_timer).
461
+ * PCE frequency regs are DIVIDERS: pitch ≈ 3.58MHz / (32 × value), so a
462
+ * BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
463
+ enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
464
+ static const u16 NOTE_DIV[17] = {
465
+ 0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
466
+ };
467
+ /* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
468
+ static const u8 MEL_TITLE[16] = { C4,E4,G4,C5, B4,G4,E4,G4, A4,C5,E5,C5, D5,B4,G4,E4 };
469
+ static const u8 BAS_TITLE[8] = { C3,C3, G3,G3, A2N,A2N, G3,G3 };
470
+ static const u8 MEL_PLAY[16] = { E4,G4,A4,G4, E4,D4,E4,G4, C5,B4,A4,G4, A4,G4,E4,R };
471
+ static const u8 BAS_PLAY[8] = { A2N,A2N, C3,C3, G3,G3, F3,F3 };
472
+ static const u8 MEL_OVER[16] = { C5,R,G4,R, E4,R,D4,R, C4,R,A2N,R, A2N,R,R,R };
473
+
474
+ static u8 music_song; /* reuses the ST_* ids */
475
+ static u8 music_step, music_timer, music_done;
476
+
477
+ static void music_set(u8 song) {
478
+ music_song = song;
479
+ music_step = 0;
480
+ music_timer = 0;
481
+ music_done = 0;
482
+ psg_off(4);
483
+ psg_off(5);
484
+ }
485
+
486
+ static void music_tick(void) {
487
+ const u8 *mel;
488
+ u8 n;
489
+ if (music_done) return;
490
+ if (music_timer == 0) {
491
+ mel = (music_song == ST_PLAY) ? MEL_PLAY
492
+ : (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
493
+ n = mel[music_step & 15];
494
+ if (n != R) psg_tone(5, NOTE_DIV[n], 26);
495
+ else psg_off(5);
496
+ if (music_song != ST_OVER) { /* the wreck jingle has no bass */
497
+ n = ((music_step & 1) == 0)
498
+ ? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
499
+ : BAS_TITLE[(music_step >> 1) & 7])
500
+ : R;
501
+ if (n != R) psg_tone(4, NOTE_DIV[n], 20);
502
+ }
503
+ ++music_step;
504
+ if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
505
+ music_done = 1;
506
+ psg_off(4);
507
+ psg_off(5);
508
+ }
509
+ }
510
+ ++music_timer;
511
+ if (music_timer >= 9) music_timer = 0;
512
+ }
513
+
514
+ /* short SFX on channels 0-3, auto-cut by sfx_timer */
515
+ static void sfx(u8 chan, u16 freq, u8 frames) {
516
+ psg_tone(chan, freq, 31);
517
+ if (frames > sfx_timer) sfx_timer = frames;
195
518
  }
196
519
 
197
- static void spawn_obst(void) {
520
+ /* ── GAME LOGIC (clay) — AABB, both boxes 14x14 (16px cars, slight slack). ── */
521
+ static u8 hits(u16 ax, u16 ay, u16 bx, u16 by) {
522
+ return (u8)(ax < bx + 14 && ax + 14 > bx && ay < by + 14 && ay + 14 > by);
523
+ }
524
+
525
+ /* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation) ── */
526
+ static void spawn_traffic(void) {
198
527
  u8 i;
199
- for (i = 0; i < MAX_OBST; ++i) {
200
- if (!obst[i].alive) {
201
- obst[i].x = lane_x[(next_rand() >> 9) % 3];
202
- obst[i].y = 0;
203
- obst[i].alive = 1;
528
+ for (i = 0; i < MAX_TRAFFIC; ++i) {
529
+ if (!traffic[i].alive) {
530
+ traffic[i].x = lane_x[random8() & 3];
531
+ traffic[i].y = SPAWN_Y;
532
+ traffic[i].alive = 1;
204
533
  return;
205
534
  }
206
535
  }
207
536
  }
208
537
 
209
- void main(void) {
538
+ /* ── GAME LOGIC (clay) — stage the SPRITE HUD digits at HUD_Y ────────────────
539
+ * 1P: crashes-left digit (left) + 5-digit distance (right) = 6 sprites on the
540
+ * HUD scanline. 2P: one crashes-left digit per player = 2 sprites. Unused HUD
541
+ * slots park off-screen. Sprites are SCREEN-space, so the HUD holds steady over
542
+ * the scrolling road (see the SPRITE-HUD idiom on upload_sprite_digits). */
543
+ static void put_digit(u8 slot, u16 x, u8 d) {
544
+ set_sprite(slot, x, (u16)HUD_Y, (u16)(SDIGIT_PAT + d), PAL_HUD);
545
+ }
546
+ static void hide_hud_slot(u8 slot) {
547
+ set_sprite(slot, 0, OFFSCREEN_Y, SDIGIT_PAT, PAL_HUD);
548
+ }
549
+ static void stage_hud(void) {
210
550
  u8 i;
551
+ if (state != ST_PLAY) { for (i = 0; i < 6; ++i) hide_hud_slot((u8)(SLOT_HUD + i)); return; }
552
+ if (two_player) {
553
+ put_digit((u8)(SLOT_HUD + 0), 24, crashes_left[0]); /* P1 left */
554
+ put_digit((u8)(SLOT_HUD + 1), 216, crashes_left[1]); /* P2 right */
555
+ for (i = 2; i < 6; ++i) hide_hud_slot((u8)(SLOT_HUD + i));
556
+ return;
557
+ }
558
+ put_digit((u8)(SLOT_HUD + 0), 16, crashes_left[0]); /* crashes */
559
+ { /* distance */
560
+ u16 v = dist;
561
+ u8 d[5];
562
+ for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
563
+ for (i = 0; i < 5; ++i) put_digit((u8)(SLOT_HUD + 1 + i), (u16)(176 + i * 14), d[4 - i]);
564
+ }
565
+ }
211
566
 
212
- _pce_keep[0] = 0;
567
+ /* ── GAME LOGIC (clay) — flat band behind title/result text (BAT tiles) ──────
568
+ * The title and result screens DON'T scroll (BYR held at 0, no streaming), so
569
+ * their text safely lives in the BAT. A dark band sits behind the text rows. */
570
+ static void paint_band_rows(u8 r0, u8 r1) {
571
+ u8 c, r;
572
+ for (r = r0; r <= r1; ++r)
573
+ for (c = 0; c < 32; ++c) put_tile(c, r, BAT_ENTRY(0, BAND_VRAM));
574
+ }
213
575
 
214
- /* palette */
215
- vce_set_color(0, PCE_RGB(0, 1, 0)); /* backdrop dark green */
216
- vce_set_color(1, PCE_RGB(1, 5, 1)); /* BG c1: grass */
217
- vce_set_color(2, PCE_RGB(2, 2, 2)); /* BG c2: road grey */
218
- vce_set_color(3, PCE_RGB(7, 7, 1)); /* BG c3: yellow lane dash */
219
- vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr pal0 transparent */
220
- vce_set_color(257, PCE_RGB(2, 5, 7)); /* spr pal0 c1: cyan player */
221
- vce_set_color(272, PCE_RGB(0, 0, 0)); /* spr pal1 transparent */
222
- vce_set_color(273, PCE_RGB(7, 1, 1)); /* spr pal1 c1: red enemy */
576
+ /* ── GAME LOGIC (clay) — screen painters (full BAT repaint per state change) ──
577
+ * Title/result paint the road as a STATIC backdrop (so the scene reads as a
578
+ * road, not a blank card) then lay text over a dark band. Only ST_PLAY scrolls. */
579
+ static void paint_title(void) {
580
+ paint_road();
581
+ paint_band_rows(6, 23);
582
+ draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 8, GAME_TITLE);
583
+ draw_text(10, 13, "1P RACE - I");
584
+ draw_text(10, 15, "2P VERSUS - II");
585
+ draw_text(11, 18, "BEST");
586
+ draw_num5(16, 18, best);
587
+ draw_text(4, 22, "STEER L R - GAS I - BRAKE II");
588
+ }
223
589
 
224
- upload_font();
225
- make_solid_tile(tile_buf, 1); load_tiles(GRASS_VRAM, tile_buf, 16);
226
- make_solid_tile(tile_buf, 2); load_tiles(ROAD_VRAM, tile_buf, 16);
227
- make_dash_tile(tile_buf); load_tiles(DASH_VRAM, tile_buf, 16);
228
- make_car_sprite(PLAYER_VRAM, 1); /* colour 1 */
229
- make_car_sprite(ENEMY_VRAM, 1); /* colour 1 (sub-pal 1 = red) */
590
+ static void paint_play(void) {
591
+ paint_road(); /* fresh 32-row road; the sprite HUD floats */
592
+ }
593
+
594
+ static void paint_over(void) {
595
+ paint_road();
596
+ paint_band_rows(7, 22);
597
+ if (two_player) {
598
+ draw_text(13, 8, winner ? "P2 WINS" : "P1 WINS");
599
+ draw_text(10, 11, "RIVAL WRECKED");
600
+ } else {
601
+ draw_text(13, 8, "WRECKED");
602
+ draw_text(11, 11, "DIST");
603
+ draw_num5(16, 11, dist);
604
+ draw_text(11, 13, "BEST");
605
+ draw_num5(16, 13, best);
606
+ }
607
+ draw_text(9, 21, "RUN - TITLE");
608
+ }
230
609
 
231
- draw_road();
610
+ /* ── GAME LOGIC (clay) — start a run ── */
611
+ static void start_game(u8 versus) {
612
+ u8 i;
613
+ two_player = versus;
614
+ for (i = 0; i < MAX_TRAFFIC; ++i) traffic[i].alive = 0;
615
+ for (i = 0; i < 2; ++i) {
616
+ crashes_left[i] = START_LIVES;
617
+ invuln[i] = 0;
618
+ lane_cd[i] = 0;
619
+ /* prev_pads = the CURRENTLY-held pad so a button held across the
620
+ * title→play transition (the start press) isn't read as a fresh edge,
621
+ * WITHOUT swallowing the player's first deliberate steer. (0xFF here
622
+ * would mask the first press of every direction until released once.) */
623
+ prev_pads[i] = 0;
624
+ }
625
+ prev_pads[0] = pce_joy_read(); /* swallow the held start button (pad 1) */
626
+ if (versus) {
627
+ car_active[0] = 1; car_active[1] = 1;
628
+ lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
629
+ lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
630
+ speed = SPEED_2P; /* shared road, fixed speed (see design) */
631
+ } else {
632
+ car_active[0] = 1; car_active[1] = 0;
633
+ lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
634
+ speed = 1;
635
+ }
636
+ dist = 0; dist_frac = 0;
637
+ spawn_timer = 0;
638
+ start_pause = 30; /* green-light breather */
639
+ road_scroll = 0;
640
+ prev_top_row = 0;
641
+ state = ST_PLAY;
642
+ paint_play();
643
+ vdc_set_reg(VDC_BYR, 0);
644
+ music_set(ST_PLAY);
645
+ sfx(2, 0x180, 6); /* start blip */
646
+ }
232
647
 
233
- rng = 0xBEEF;
648
+ static void game_over(void) {
649
+ if (!two_player && dist > best) {
650
+ best = dist;
651
+ best_save(best); /* in-session only (no save on a bare HuCard) */
652
+ }
653
+ state = ST_OVER;
654
+ prev_pads[0] = pce_joy_read(); /* swallow only the held button, not the
655
+ * player's next deliberate press */
234
656
  road_scroll = 0;
235
- prev_pad = 0;
236
- sfx_timer = 0;
237
- reset_run();
238
- draw_score();
657
+ vdc_set_reg(VDC_BYR, 0);
658
+ paint_over();
659
+ music_set(ST_OVER);
660
+ sfx(3, 0x500, 16); /* wreck rumble */
661
+ }
662
+
663
+ /* ── GAME LOGIC (clay) — crash rules ── */
664
+ static void crash(u8 p) {
665
+ sfx(3, 0x080, 16); /* crash buzz */
666
+ invuln[p] = 60; /* blink + no-collide grace */
667
+ if (!two_player) speed = 1; /* a wreck kills your momentum */
668
+ if (crashes_left[p] > 0) --crashes_left[p];
669
+ if (crashes_left[p] == 0) {
670
+ winner = (u8)(1 - p); /* versus: the OTHER player wins */
671
+ game_over();
672
+ }
673
+ }
674
+
675
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
676
+ * 2P INPUT via the TurboTap. pce_joy_read() reads pad 1 (slot 0). For pad 2 we
677
+ * read cc65's JOY_2 directly and translate it to the same clean PCE bitmask
678
+ * pce_input.c builds for pad 1. The host force-enables the TurboTap core
679
+ * option, so JOY_2 carries real port-1 input; without that override port 1 is
680
+ * dead and this would silently fall back to 1P. ── */
681
+ static u8 read_pad2(void) {
682
+ u8 raw = joy_read(JOY_2);
683
+ u8 m = 0;
684
+ if (JOY_UP(raw)) m |= PCE_JOY_UP;
685
+ if (JOY_DOWN(raw)) m |= PCE_JOY_DOWN;
686
+ if (JOY_LEFT(raw)) m |= PCE_JOY_LEFT;
687
+ if (JOY_RIGHT(raw)) m |= PCE_JOY_RIGHT;
688
+ if (JOY_BTN_1(raw)) m |= PCE_JOY_I;
689
+ if (JOY_BTN_2(raw)) m |= PCE_JOY_II;
690
+ if (JOY_BTN_3(raw)) m |= PCE_JOY_SELECT;
691
+ if (JOY_BTN_4(raw)) m |= PCE_JOY_RUN;
692
+ return m;
693
+ }
694
+
695
+ /* ── GAME LOGIC (clay) — per-player input ───────────────────────────────────
696
+ * LEFT/RIGHT steer between lanes; UP/I accelerate, DOWN/II brake (1P only).
697
+ *
698
+ * Steering uses a short COOLDOWN (lane_cd) rather than pure rising-edge: a held
699
+ * direction steps one lane, then can't step again until lane_cd reaches 0
700
+ * (~9 frames). This still prevents machine-gun lane spam from a held d-pad, but
701
+ * unlike strict `pad & ~prev` edge detection it does NOT depend on catching the
702
+ * exact frame the button transitions — robust against input sampling latency
703
+ * (a tap that spans only a couple of frames still lands). Speed changes stay
704
+ * rising-edge (a held gas shouldn't ramp to max in 4 frames). */
705
+ static void update_player(u8 p, u8 pad) {
706
+ u8 pressed = (u8)(pad & ~prev_pads[p]);
707
+ prev_pads[p] = pad;
708
+ if (!car_active[p]) return;
709
+ if (lane_cd[p]) --lane_cd[p];
710
+ if (!lane_cd[p]) {
711
+ if ((pad & PCE_JOY_LEFT) && car_lane[p] > lane_min[p]) {
712
+ --car_lane[p]; lane_cd[p] = 9; sfx(2, 0x2C0, 4); /* lane tick */
713
+ } else if ((pad & PCE_JOY_RIGHT) && car_lane[p] < lane_max[p]) {
714
+ ++car_lane[p]; lane_cd[p] = 9; sfx(2, 0x2C0, 4);
715
+ }
716
+ }
717
+ if (!two_player) {
718
+ if ((pressed & (PCE_JOY_UP | PCE_JOY_I)) && speed < MAX_SPEED) {
719
+ ++speed;
720
+ sfx(1, (u16)(0x300 - speed * 0x60), 6); /* engine rev */
721
+ }
722
+ if ((pressed & (PCE_JOY_DOWN | PCE_JOY_II)) && speed > 1) {
723
+ --speed;
724
+ sfx(1, 0x3C0, 5); /* brake blip */
725
+ }
726
+ }
727
+ if (invuln[p] > 0) --invuln[p];
728
+ }
729
+
730
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
731
+ * SPRITE STAGING + THE SATB DMA. The VDC never reads your RAM: sprites live in
732
+ * its INTERNAL sprite attribute table, refreshed by a DMA you schedule by
733
+ * writing R19 (satb_dma() does the copy + the R19 write; the transfer happens
734
+ * at the next vblank). So the per-frame contract is:
735
+ * waitvsync() → restage EVERY slot → satb_dma()
736
+ * Stage during vblank — satb_dma() also streams words through the VWR port, and
737
+ * doing that mid-display tears sprite pattern fetches. Hidden slots park below
738
+ * the display at OFFSCREEN_Y. ── */
739
+ static void stage_sprites(void) {
740
+ u8 i, p;
741
+ for (p = 0; p < 2; ++p) {
742
+ u8 vis = (state == ST_PLAY) && car_active[p] && !(invuln[p] & 2);
743
+ set_sprite((u8)(SLOT_P1 + p), lane_x[car_lane[p]],
744
+ vis ? (u16)CAR_Y : OFFSCREEN_Y, PLAYER_PAT, p ? PAL_P2 : PAL_P1);
745
+ }
746
+ for (i = 0; i < MAX_TRAFFIC; ++i) {
747
+ u8 vis = (state == ST_PLAY) && traffic[i].alive;
748
+ set_sprite((u8)(SLOT_TRAFFIC + i), traffic[i].x,
749
+ vis ? traffic[i].y : OFFSCREEN_Y, ENEMY_PAT, PAL_TRAFFIC);
750
+ }
751
+ }
752
+
753
+ void main(void) {
754
+ u8 pad1, pad2, newpad;
755
+
756
+ _pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
757
+
758
+ /* BRAM first — before any VDC work, so the save file exists within the
759
+ * game's first frames (a headless host sees a non-empty save_ram region
760
+ * as early as possible; see the BRAM idiom). */
761
+ best_init();
762
+
763
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
764
+ * Init order: palette → VRAM uploads → BAT paint → joypad → display ON.
765
+ * disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
766
+ * never returns and the game freezes on its first frame. */
767
+ /* BG sub-pal 0: road scene. BG sub-pal 1: HUD/text white. */
768
+ vce_set_color(0, PCE_RGB(0, 1, 0)); /* backdrop: dark green */
769
+ vce_set_color(1, PCE_RGB(1, 5, 1)); /* BG c1: roadside grass green */
770
+ vce_set_color(2, PCE_RGB(2, 2, 2)); /* BG c2: asphalt grey */
771
+ vce_set_color(3, PCE_RGB(7, 7, 1)); /* BG c3: yellow markings/specks */
772
+ vce_set_color(17, PCE_RGB(7, 7, 7)); /* pal1 text: white */
773
+ /* sprite sub-palettes (256 + pal*16 + index) — P1 cyan, P2 amber, traffic
774
+ * red, each on its own sub-palette so the cars read as three liveries. */
775
+ vce_set_color(256 + 0 * 16 + 1, PCE_RGB(2, 6, 7)); /* spr pal0 c1: P1 cyan */
776
+ vce_set_color(256 + 1 * 16 + 1, PCE_RGB(7, 5, 0)); /* spr pal1 c1: P2 amber */
777
+ vce_set_color(256 + 2 * 16 + 1, PCE_RGB(7, 1, 1)); /* spr pal2 c1: traffic red*/
778
+
779
+ upload_art();
780
+
781
+ state = ST_TITLE;
782
+ paint_title();
783
+ music_set(ST_TITLE);
239
784
 
240
785
  pce_joy_init();
241
786
  disp_enable();
242
787
 
243
788
  for (;;) {
244
- u8 slot;
245
- int16_t step;
246
789
  waitvsync();
247
- psg_music_tick();
248
-
249
- /* stage sprites: player + obstacles */
250
- slot = 0;
251
- set_sprite(slot++, player.x, player.y, PLAYER_VRAM >> 6, 0);
252
- for (i = 0; i < MAX_OBST; ++i) {
253
- u16 ey = obst[i].alive ? obst[i].y : 0x1F0;
254
- set_sprite(slot++, obst[i].x, ey, ENEMY_VRAM >> 6, 1);
255
- }
790
+
791
+ /* ── vblank work first: cars + sprite HUD + SATB DMA ── */
792
+ stage_sprites();
793
+ stage_hud();
256
794
  satb_dma();
257
795
 
258
- pad = pce_joy_read();
796
+ music_tick();
797
+ if (sfx_timer) {
798
+ --sfx_timer;
799
+ if (sfx_timer == 0) { psg_off(0); psg_off(1); psg_off(2); psg_off(3); }
800
+ }
259
801
 
260
- if (crash_timer > 0) {
261
- crash_timer--;
262
- if (crash_timer == 0) reset_run();
263
- prev_pad = pad;
264
- if (sfx_timer) { --sfx_timer; if (sfx_timer == 0) psg_off(0); }
802
+ /* ── input: pad 1 always; pad 2 only in 2P play (TurboTap port 1). ── */
803
+ pad1 = pce_joy_read();
804
+ pad2 = (state == ST_PLAY && two_player) ? read_pad2() : 0;
805
+
806
+ if (state == ST_TITLE) {
807
+ /* The title road is a STATIC backdrop: with no hardware window and
808
+ * no raster split, BYR would scroll the BG title text off-screen
809
+ * (and the row-streamer would wipe it). So the title doesn't scroll
810
+ * — the play state is where the road comes alive. */
811
+ newpad = (u8)(pad1 & ~prev_pads[0]);
812
+ prev_pads[0] = pad1;
813
+ if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_game(0);
814
+ else if (newpad & PCE_JOY_II) start_game(1);
815
+ continue;
816
+ }
817
+ if (state == ST_OVER) {
818
+ newpad = (u8)(pad1 & ~prev_pads[0]);
819
+ prev_pads[0] = pad1;
820
+ if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
821
+ state = ST_TITLE;
822
+ road_scroll = 0;
823
+ vdc_set_reg(VDC_BYR, 0);
824
+ paint_title();
825
+ music_set(ST_TITLE);
826
+ }
265
827
  continue;
266
828
  }
267
829
 
268
- /* lane switch (edge-triggered) */
269
- if ((pad & PCE_JOY_LEFT) && !(prev_pad & PCE_JOY_LEFT) && player_lane > 0) { player_lane--; psg_tone(1, 0x2C0, 16); sfx_timer = 3; }
270
- if ((pad & PCE_JOY_RIGHT) && !(prev_pad & PCE_JOY_RIGHT) && player_lane < 2) { player_lane++; psg_tone(1, 0x2C0, 16); sfx_timer = 3; }
271
- player.x = lane_x[player_lane];
272
- prev_pad = pad;
830
+ /* ── ST_PLAY ──────────────────────────────────────────────────────── */
831
+ if (start_pause) { --start_pause; continue; } /* green-light freeze */
273
832
 
274
- /* speed grows with score */
275
- step = (int16_t)(2 + (score / 400));
276
- if (step > 5) step = 5;
833
+ advance_road(speed);
277
834
 
278
- /* scroll the road to sell motion */
279
- road_scroll = (u8)(road_scroll + step);
280
- vdc_set_reg(VDC_BYR, (u16)road_scroll);
835
+ update_player(0, pad1);
836
+ if (two_player) update_player(1, pad2);
837
+ if (state != ST_PLAY) continue; /* a crash may have ended the game */
281
838
 
282
- for (i = 0; i < MAX_OBST; ++i) {
283
- if (!obst[i].alive) continue;
284
- obst[i].y = (u16)(obst[i].y + step);
285
- if (obst[i].y >= 216) obst[i].alive = 0;
839
+ /* Distance (1P stat): 1 unit per 16 scrolled pixels. A chime every 256
840
+ * units marks a checkpoint. */
841
+ if (!two_player) {
842
+ dist_frac = (u8)(dist_frac + speed);
843
+ if (dist_frac >= 16) {
844
+ dist_frac = (u8)(dist_frac - 16);
845
+ if (dist < 65535u) ++dist;
846
+ if (dist != 0 && (dist & 0xFF) == 0)
847
+ sfx(0, 0x0D6, 8); /* checkpoint chime (C6) */
848
+ }
286
849
  }
287
850
 
288
- spawn_timer++;
289
- if (spawn_timer >= 40) { spawn_timer = 0; spawn_obst(); }
290
-
291
- for (i = 0; i < MAX_OBST; ++i) {
292
- if (obst[i].alive && aabb(&player, &obst[i])) {
293
- crash_timer = 70;
294
- psg_tone(0, 0x080, 28); /* crash buzz */
295
- sfx_timer = 16;
296
- break;
851
+ /* Traffic flows down at road speed (it reads as slower cars you're
852
+ * overtaking); despawn past the player with a little pass tick. */
853
+ {
854
+ u8 i, p;
855
+ for (i = 0; i < MAX_TRAFFIC; ++i) {
856
+ if (!traffic[i].alive) continue;
857
+ traffic[i].y = (u16)(traffic[i].y + speed);
858
+ if (traffic[i].y > DESPAWN_Y) {
859
+ traffic[i].alive = 0;
860
+ sfx(1, 0x0C0, 2);
861
+ }
862
+ }
863
+ if (++spawn_timer >= SPAWN_PERIOD) { spawn_timer = 0; spawn_traffic(); }
864
+
865
+ /* Traffic ↔ cars. Crash grace: a just-wrecked car blinks and can't
866
+ * collide for 60 frames. */
867
+ for (i = 0; i < MAX_TRAFFIC && state == ST_PLAY; ++i) {
868
+ if (!traffic[i].alive) continue;
869
+ for (p = 0; p < 2; ++p) {
870
+ if (!car_active[p] || invuln[p]) continue;
871
+ if (hits(traffic[i].x, traffic[i].y, lane_x[car_lane[p]], CAR_Y)) {
872
+ traffic[i].alive = 0;
873
+ crash(p);
874
+ break;
875
+ }
876
+ }
297
877
  }
298
878
  }
299
-
300
- if (score < 9999) score++;
301
- if ((score & 7) == 0) draw_score();
302
-
303
- if (sfx_timer) { --sfx_timer; if (sfx_timer == 0) { psg_off(0); psg_off(1); } }
304
879
  }
305
880
  }