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,95 +1,328 @@
1
- /* ── racing/main.c — MSX top-down 3-lane racing scaffold (screen 2) ──
1
+ /* ── racing/main.c — MSX top-down road racer (complete example game) ─────────
2
2
  *
3
- * Mirrors the SMS/GB/etc racing scaffolds, translated to the MSX VDP via
4
- * the romdev helper lib (msx_hw.h + msx_vdp.c).
3
+ * TURBO TANGLE a COMPLETE, working game: title screen, 1P endless race with
4
+ * speed control + a best-distance record, 2P SIMULTANEOUS VERSUS (P2 on
5
+ * JOYSTICK PORT 2) on one shared road, crash/lives rules into a result screen,
6
+ * music + SFX on the AY-3-8910 PSG, and the MSX's signature SCREEN-2 PER-ROW
7
+ * COLOR: the asphalt, the grass shoulders, the centre divider and the HUD band
8
+ * are all ONE tile set differentiated purely by which screen-2 color third they
9
+ * sit in — plus a one-tile vertical "shimmer" gradient down the divider — at
10
+ * zero extra tiles.
5
11
  *
6
- * Endless 3-lane top-down racer. Grey road down the centre lanes + green
7
- * grass shoulders fill the whole 32x24 screen-2 name table. The player car
8
- * sits near the bottom; obstacle cars (object pool) spawn at the top and
9
- * slide down. Speed grows with score; an AABB crash triggers a ~60-frame
10
- * freeze then auto-reset. SCORE is drawn as on-screen tiles.
12
+ * The game (top-down vertical racer): a four-lane road scrolls toward you; you
13
+ * steer LEFT/RIGHT between lanes to weave through slower traffic. In 1P,
14
+ * UP/A accelerates and DOWN/B brakes (speed 1-4) and the run banks DISTANCE;
15
+ * 3 crashes end it. In 2P, both cars share one road at a fixed speed — P1 owns
16
+ * the left two lanes, P2 (port 2) the right two — and the first driver to burn
17
+ * all 3 crashes LOSES.
11
18
  *
12
- * Controls: joystick PORT 1 LEFT/RIGHT (edge-detected) switches lanes.
19
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
20
+ * very different one. The markers tell you what's what:
21
+ * HARDWARE IDIOM (load-bearing) — dodges a documented MSX footgun; reshape
22
+ * your gameplay around it (see TROUBLESHOOTING before changing).
23
+ * GAME LOGIC (clay) — traffic patterns, speeds, scoring, art: reshape freely.
13
24
  *
14
- * Cartridge rule: INIT must never return — main() ends in for(;;).
25
+ * What depends on what:
26
+ * msx_hw.h / msx_vdp.c — VDP + PSG + joystick helpers (direct Z80 ports;
27
+ * the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
28
+ * read msx_vdp.c before adding your own PSG pokes).
29
+ * msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
30
+ * INIT must NEVER return, so main() ends in for(;;).
31
+ *
32
+ * A TEACHING POINT vs the NES version of this game
33
+ * (examples/nes/templates/racing.c): the NES scrolls the road as the
34
+ * BACKGROUND — it decrements the PPU's hardware scroll_y every frame and the
35
+ * whole nametable slides for free. The MSX SCREEN 2 has NO HARDWARE SCROLL at
36
+ * all (see the idiom below), so TURBO TANGLE fakes the motion by REDRAWING the
37
+ * road's dashes + shoulder texture one phase further down the name table each
38
+ * frame: a moving-stripe pattern, recomputed from a single scrolling offset.
39
+ * Same genre, the opposite hardware reality — and the honest way to teach it.
40
+ *
41
+ * Controls: JOYSTICK PORT 1 (or keyboard cursors) LEFT/RIGHT steers; UP/trigger
42
+ * A accelerates, DOWN/trigger B brakes (1P only). In 2P versus, JOYSTICK
43
+ * PORT 2 LEFT/RIGHT steers player 2. On the title screen trigger A starts the
44
+ * 1P race; trigger B starts 2P versus. On the result screen any fire returns
45
+ * to the title.
46
+ *
47
+ * Record honesty: the bundled bluemsx core build exposes NO battery save path
48
+ * (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so BEST (the
49
+ * best 1P distance) lives in plain RAM: it survives title↔race cycles but NOT
50
+ * a power cycle / hardReset. Never fake persistence — if you need real saves,
51
+ * that's a future core round (ASCII8-SRAM mapper carts exist; the core just
52
+ * doesn't surface their RAM yet). The Genesis/NES/SMS versions of this game
53
+ * DO persist the same best distance to cartridge SRAM.
15
54
  */
16
55
  #include "msx_hw.h"
17
56
 
18
- /* ── interrupt-free vblank sync (poll VDP status S#0 bit 7) ────────────── */
57
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
58
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
59
+ #define GAME_TITLE "TURBO TANGLE"
60
+
61
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
62
+ * Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
63
+ * the port ALSO clears the flag, so one read per frame = one game step per
64
+ * frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
65
+ * works even with interrupts masked, and never depends on the BIOS ISR
66
+ * keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
67
+ * flag first and this loop just waits for the next one; a one-frame hiccup,
68
+ * never a hang.) */
19
69
  __sfr __at 0x99 VDPSTATUS;
20
70
  static void vsync(void) {
21
- (void)VDPSTATUS;
71
+ (void)VDPSTATUS; /* throw away a possibly-stale flag */
22
72
  while (!(VDPSTATUS & 0x80)) {
23
73
  }
24
74
  }
25
75
 
26
- #define LANE_LEFT_X 96
27
- #define LANE_MID_X 124
28
- #define LANE_RIGHT_X 152
29
- #define PLAYER_Y 160
30
- #define MAX_OBSTACLES 4
31
-
32
- /* ── tile font (digits + S C O R E) + track tiles ─────────────────────── */
33
- #define T_SPACE 0
34
- #define T_S 1
35
- #define T_C 2
36
- #define T_O 3
37
- #define T_R 4
38
- #define T_E 5
39
- #define T_0 6
40
- #define T_GRASS 16
41
- #define T_ROAD 17
42
- #define T_LANE 18 /* dashed lane marker */
43
-
44
- static const uint8_t font[19][8] = {
45
- /* 0 SPACE */ {0,0,0,0,0,0,0,0},
46
- /* 1 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
47
- /* 2 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
48
- /* 3 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
49
- /* 4 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
50
- /* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
51
- /* 6 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
52
- /* 7 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
53
- /* 8 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
54
- /* 9 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
55
- /* 10 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
56
- /* 11 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
57
- /* 12 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
58
- /* 13 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
59
- /* 14 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
60
- /* 15 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
61
- /* 16 GRASS */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
62
- /* 17 ROAD */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
63
- /* 18 LANE */ {0x18,0x18,0x18,0x00,0x00,0x18,0x18,0x18}
76
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
77
+ * NO HARDWARE SCROLL ON SCREEN 2. The TMS9918 GRAPHIC-II mode has no smooth
78
+ * pixel-scroll register at all (the V9938's R23 is a whole-screen vertical
79
+ * LINE shift, not a per-layer camera, and MSX1 lacks even that). So a vertical
80
+ * road racer cannot just "scroll the background down" the way the NES version
81
+ * does — there is no register to turn.
82
+ *
83
+ * TURBO TANGLE fakes the scroll the only cheap way screen 2 allows: it keeps a
84
+ * single 0-7 "road phase" that advances with the car's speed, and each frame it
85
+ * REDRAWS just the road's moving parts — the dashed lane markers and a sparse
86
+ * shoulder speckle — one phase-step further DOWN the name table. The static
87
+ * parts (asphalt fill, solid shoulders, centre divider) are painted ONCE and
88
+ * never touched. Redrawing only ~2 columns of dashes + a speckle column per
89
+ * frame keeps the per-frame VRAM burst tiny, so it never fights the per-row
90
+ * color idiom below (the color tables still upload ONCE). The eye reads the
91
+ * marching dashes as forward motion — exactly the trick fixed-screen arcade
92
+ * racers used before hardware scroll was common.
93
+ *
94
+ * If you want REAL smooth scroll on MSX2, that is an R23 line-shift routine
95
+ * plus re-streaming the name + color tables as rows enter — the single biggest
96
+ * MSX scroller footgun; see TROUBLESHOOTING before attempting it. */
97
+
98
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
99
+ * Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
100
+ * road tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits draw in
101
+ * the tile's FOREGROUND color, clear bits in its BACKGROUND color (both come
102
+ * from the screen-2 color table — see the per-row-color idiom below). */
103
+ #define T_SPACE 0
104
+ #define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
105
+ #define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
106
+ #define T_DASH 37
107
+ #define T_ASPHALT 38 /* plain road surface (faint tarmac speck) */
108
+ #define T_GRASS 39 /* roadside shoulder (hatch texture) */
109
+ #define T_LANE 40 /* the marching dashed lane marker (scrolls) */
110
+ #define T_DIVIDER 41 /* solid centre divider — its COLOR shimmers */
111
+ #define T_TUFT 42 /* roadside scenery tuft (rides the speckle) */
112
+ #define NUM_TILES 43
113
+
114
+ static const uint8_t font[NUM_TILES][8] = {
115
+ /* SPACE */ {0,0,0,0,0,0,0,0},
116
+ /* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
117
+ /* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
118
+ /* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
119
+ /* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
120
+ /* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
121
+ /* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
122
+ /* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
123
+ /* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
124
+ /* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
125
+ /* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
126
+ /* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
127
+ /* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
128
+ /* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
129
+ /* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
130
+ /* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
131
+ /* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
132
+ /* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
133
+ /* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
134
+ /* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
135
+ /* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
136
+ /* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
137
+ /* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
138
+ /* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
139
+ /* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
140
+ /* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
141
+ /* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
142
+ /* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
143
+ /* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
144
+ /* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
145
+ /* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
146
+ /* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
147
+ /* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
148
+ /* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
149
+ /* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
150
+ /* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
151
+ /* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
152
+ /* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
153
+ /* 38 ASPHALT (sparse tarmac speck so the road isn't a flat void) */
154
+ {0x00,0x00,0x00,0x10,0x00,0x00,0x02,0x00},
155
+ /* 39 GRASS (roadside hatch texture) */
156
+ {0xAA,0x55,0xAA,0x55,0xAA,0x55,0xAA,0x55},
157
+ /* 40 LANE (a vertical dash segment — half on, half off; phase-shifted
158
+ * by which name-table row it lands on for the marching look) */
159
+ {0x18,0x18,0x18,0x18,0x00,0x00,0x00,0x00},
160
+ /* 41 DIVIDER(solid bar — its 8 COLOR bytes carry the shimmer gradient) */
161
+ {0x3C,0x3C,0x3C,0x3C,0x3C,0x3C,0x3C,0x3C},
162
+ /* 42 TUFT (a little roadside bush over the grass) */
163
+ {0x00,0x18,0x3C,0x7E,0xFF,0x7E,0x3C,0x00},
64
164
  };
65
165
 
66
- /* colour bytes. 3=green(dark), 12=green(light), 14=grey, 15=white, 1=black */
67
- #define COL_TEXT 0xF1 /* white text on black */
68
- #define COL_GRASS 0xC1 /* light green grass on black */
69
- #define COL_ROAD 0xE1 /* grey road on black */
70
- #define COL_LANE 0xAE /* yellow dashes on grey road */
71
-
72
- /* sprite patterns (8x8): player car + enemy car */
73
- static const uint8_t spr_player[8] = {0x18,0x3C,0x24,0x3C,0x7E,0x24,0x7E,0x66};
74
- static const uint8_t spr_enemy[8] = {0x66,0x7E,0x24,0x7E,0x3C,0x24,0x3C,0x18};
75
- #define PAT_PLAYER 0
76
- #define PAT_ENEMY 1
77
- #define COL_PLAYER 15 /* white */
78
- #define COL_ENEMY 9 /* red */
79
-
80
- typedef struct { uint8_t x, y, alive; } Car;
81
-
82
- static Car player;
83
- static Car obstacles[MAX_OBSTACLES];
84
- static uint16_t score;
166
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
167
+ * SCREEN-2 PER-ROW COLOR — the MSX's signature background trick.
168
+ *
169
+ * Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
170
+ *
171
+ * 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
172
+ * (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
173
+ * pattern table slice and its OWN 2KB color table slice:
174
+ * patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
175
+ * The SAME tile index can look completely different in each third. We
176
+ * exploit exactly that to make ONE road tile set read as a depth-shaded
177
+ * track: the HUD band (third 0) gets bright text colors; the asphalt cools
178
+ * from a hazy distance grey at the top toward a darker near-road grey at
179
+ * the bottom, and the grass deepens the same way — one tile set, three
180
+ * bands, zero extra tiles (the racing twin of the shmup's depth starfield).
181
+ *
182
+ * 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
183
+ * row — each packing (foreground<<4)|background from the fixed TMS9918
184
+ * palette. So one tile can carry an 8-color vertical gradient: T_DIVIDER's
185
+ * whole "shimmer" running down the centre divider is a single tile,
186
+ * colors only.
187
+ *
188
+ * Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
189
+ * R4=0x03 — the "thirds" configuration), and pattern + color uploads to
190
+ * EVERY third a tile is used in. Tile N's slot is pattern[N*8] / color[N*8].
191
+ *
192
+ * TMS9918 fixed palette used here: 1 black, 4 dark blue, 6 dark red, 8 medium
193
+ * red, 12 dark green, 13 light green, 14 gray, 15 white, 10 dark yellow,
194
+ * 11 light yellow (high nibble = fg, low nibble = bg of each row byte). */
195
+ static const uint8_t col_text[3] = { 0xF1, 0xF1, 0xF1 }; /* white-on-black text everywhere */
196
+ /* The asphalt speck, banded by third: hazy light-grey far off, mid grey, dark
197
+ * near-road grey close — pure per-third recolor of one tile (bg = the road). */
198
+ static const uint8_t col_asphalt[3] = { 0xE4, 0xE1, 0x1E };
199
+ /* The grass shoulders, banded so distant grass reads cooler/darker and near
200
+ * grass brightens — same hatch tile, three colors. */
201
+ static const uint8_t col_grass[3] = { 0xC1, 0xD1, 0xD1 };
202
+ /* The marching lane dashes: bright yellow on the road bg, banded subtly. */
203
+ static const uint8_t col_lane[3] = { 0xB1, 0xB4, 0xB1 };
204
+ /* Roadside scenery tuft: green bush over the grass band. */
205
+ static const uint8_t col_tuft[3] = { 0xC1, 0xC1, 0xD1 };
206
+ /* T_DIVIDER: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row shimmer
207
+ * down the centre divider (black → grey → white and back). The divider pattern
208
+ * is a solid 4px bar so the fg nibbles show. Recolored again per third free. */
209
+ static const uint8_t col_div[8] = { 0x11,0xE1,0xF1,0xF1,0xF1,0xF1,0xE1,0x11 };
210
+
211
+ static void load_tiles(void) {
212
+ uint8_t third, i;
213
+ uint16_t patbase, colbase;
214
+ for (third = 0; third < 3; third++) {
215
+ patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
216
+ colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
217
+ for (i = 0; i < NUM_TILES; i++) {
218
+ uint8_t col;
219
+ /* pattern bits are the same in every third — only COLOR varies */
220
+ msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
221
+ if (i == T_DIVIDER) { /* the one per-pixel-row gradient */
222
+ msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_div, 8);
223
+ continue;
224
+ }
225
+ if (i == T_ASPHALT) col = col_asphalt[third];
226
+ else if (i == T_GRASS) col = col_grass[third];
227
+ else if (i == T_LANE) col = col_lane[third];
228
+ else if (i == T_TUFT) col = col_tuft[third];
229
+ else col = col_text[third];
230
+ msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
231
+ }
232
+ }
233
+ }
234
+
235
+ /* ── GAME LOGIC (clay — reshape freely) — name-table drawing helpers ────────
236
+ * Screen 2 VRAM writes are safe at any point in the frame at C speed: the
237
+ * TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
238
+ * and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
239
+ * are the thing that outruns the VDP — see TROUBLESHOOTING.) */
240
+ static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
241
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
242
+ }
243
+
244
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
245
+ uint8_t buf[32];
246
+ uint8_t n = 0;
247
+ while (*s && n < 32) {
248
+ char c = *s++;
249
+ if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
250
+ else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
251
+ else if (c == '-') buf[n] = T_DASH;
252
+ else buf[n] = T_SPACE;
253
+ n++;
254
+ }
255
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
256
+ }
257
+
258
+ static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
259
+ uint8_t buf[4];
260
+ buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
261
+ buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
262
+ buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
263
+ buf[3] = (uint8_t)(T_0 + v % 10);
264
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
265
+ }
266
+
267
+ /* ── GAME LOGIC (clay — reshape freely) — road geometry + race rules ─────────
268
+ * The road fills the 32x24 screen-2 name table. Four 2-cell lanes sit between
269
+ * grass shoulders, with a solid divider down the middle (it is also the 2P
270
+ * territory line). Tile columns:
271
+ * COL_EDGE_L/R — solid grass-edge shoulders bounding the asphalt
272
+ * COL_LANE_1/2 — the two dashed inner lane lines
273
+ * COL_DIVIDER — the solid centre divider
274
+ * Row 0 is the HUD band (third 0's text colors make it a distinct strip). */
275
+ #define COL_EDGE_L 8
276
+ #define COL_LANE_1 12
277
+ #define COL_DIVIDER 16
278
+ #define COL_LANE_2 20
279
+ #define COL_EDGE_R 24
280
+ /* Lane centre X for the 8px-wide car sprite (4 lanes across the asphalt). */
281
+ static const uint8_t lane_x[4] = { 80, 112, 136, 168 };
282
+
283
+ #define MAX_TRAFFIC 5
284
+ #define CAR_Y 168 /* both cars' fixed screen Y (near the bottom) */
285
+ #define SPAWN_Y 16 /* traffic entry Y (just under the HUD band) */
286
+ #define DESPAWN_Y 184 /* traffic leaves the road past here */
287
+ #define START_LIVES 3 /* crashes per run / per player */
288
+ #define SPAWN_PERIOD 40 /* frames between traffic spawns */
289
+ #define SPEED_2P 2 /* fixed shared road speed in versus */
290
+
291
+ /* Players: index 0 = P1 (port 1 + keyboard), 1 = P2 (port 2, versus only). */
292
+ static uint8_t car_lane[2]; /* 0..3 */
293
+ static uint8_t car_active[2];
294
+ static uint8_t crashes_left[2];
295
+ static uint8_t invuln[2]; /* post-crash blink / no-collide frames */
296
+ static uint8_t lane_min[2], lane_max[2]; /* 2P: split territories */
297
+ static uint8_t prev_dir[2]; /* per-player steer edge detection */
298
+ static uint8_t prev_acc; /* 1P accel/brake edge detection */
299
+ static uint8_t two_player;
300
+ static uint8_t winner; /* versus result: 0 = P1 wins, 1 = P2 wins */
301
+
302
+ static uint8_t traffic_alive[MAX_TRAFFIC];
303
+ static uint8_t traffic_lane[MAX_TRAFFIC];
304
+ static uint8_t traffic_y[MAX_TRAFFIC];
305
+
306
+ static uint8_t speed; /* road px/frame, 1-4 */
307
+ static uint16_t dist; /* 1P distance, 1 unit = 16 scrolled px */
308
+ static uint8_t dist_frac;
309
+ static uint16_t best; /* SESSION-ONLY best 1P distance — see header.
310
+ * No SAVE_RAM on this core, so it lives in
311
+ * plain RAM: survives title↔race cycles, NOT
312
+ * a power cycle (honest, not faked). */
85
313
  static uint8_t spawn_timer;
86
- static uint8_t game_over_timer;
87
- static uint8_t player_lane;
88
- static uint16_t rng;
89
- static uint8_t blip;
314
+ static uint8_t road_phase; /* 0..7 — the faked-scroll offset (idiom) */
90
315
 
91
- static const uint8_t lane_x[3] = { LANE_LEFT_X, LANE_MID_X, LANE_RIGHT_X };
316
+ #define ST_TITLE 0
317
+ #define ST_PLAY 1
318
+ #define ST_OVER 2
319
+ static uint8_t state;
320
+ static uint8_t prev_t1, prev_t2; /* title/over trigger edge detection */
92
321
 
322
+ /* ── GAME LOGIC (clay — reshape freely) — xorshift16 PRNG.
323
+ * Traffic lanes + spawn timing read from this so two runs never play the same;
324
+ * ticked once per play frame so identical states a few seconds apart diverge. */
325
+ static uint16_t rng;
93
326
  static uint8_t next_rand(void) {
94
327
  rng ^= (uint16_t)(rng << 7);
95
328
  rng ^= (uint16_t)(rng >> 9);
@@ -97,154 +330,412 @@ static uint8_t next_rand(void) {
97
330
  return (uint8_t)(rng & 0xFF);
98
331
  }
99
332
 
100
- static uint8_t aabb(Car *a, Car *b) {
101
- return a->x < b->x + 8 && a->x + 8 > b->x
102
- && a->y < b->y + 8 && a->y + 8 > b->y;
333
+ /* ── GAME LOGIC (clay reshape freely) — music + SFX on the AY-3-8910 ──────
334
+ * Channel plan: A = lane-tick / engine blips, B = crash + brake noise, C =
335
+ * music. The PSG has 3 tone channels + ONE shared noise generator, mixed
336
+ * per-channel in reg 7. All register traffic goes through msx_psg_tone/noise/
337
+ * off — they wrap the PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT
338
+ * ISR clobbers the PSG address latch every frame (the bug that once silenced
339
+ * every MSX scaffold — see msx_vdp.c).
340
+ *
341
+ * The tune: one period entry per half-beat, 0 = rest. AY period =
342
+ * 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame; a
343
+ * note advances every 8 frames. The lib's built-in demo loop (msx_music_tick)
344
+ * also uses channel C, so we switch it OFF in main() and run THIS table
345
+ * instead — edit this table to rescore. */
346
+ static const uint16_t tune[32] = {
347
+ 254, 0, 254, 214, 254, 0, 285, 254, /* A4 A4 C5 A4 G4 A4 (driving riff) */
348
+ 214, 0, 254, 285, 254, 0, 0, 0, /* C5 A4 G4 A4 rest */
349
+ 190, 0, 214, 254, 190, 0, 214, 190, /* D5 C5 A4 D5 C5 D5 */
350
+ 254, 0, 285, 254, 214, 0, 0, 0, /* A4 G4 A4 C5 rest */
351
+ };
352
+ static uint8_t music_step, music_timer;
353
+ static uint8_t sfx_a_t, sfx_b_t; /* frames left on the A/B SFX channels */
354
+
355
+ static void music_tick(void) {
356
+ if (music_timer == 0) {
357
+ uint16_t p = tune[music_step & 31];
358
+ if (p) msx_psg_tone(2, p, 9);
359
+ else msx_psg_off(2);
360
+ music_step++;
361
+ }
362
+ music_timer++;
363
+ if (music_timer >= 8) music_timer = 0;
103
364
  }
104
365
 
105
- static void load_tiles(void) {
106
- uint8_t third, i;
107
- uint16_t pat, col;
108
- for (third = 0; third < 3; third++) {
109
- pat = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
110
- col = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
111
- for (i = 0; i < 19; i++) {
112
- uint8_t cc = COL_TEXT;
113
- if (i == T_GRASS) cc = COL_GRASS;
114
- else if (i == T_ROAD) cc = COL_ROAD;
115
- else if (i == T_LANE) cc = COL_LANE;
116
- msx_vram_write((uint16_t)(pat + ((uint16_t)i << 3)), font[i], 8);
117
- msx_fill_vram((uint16_t)(col + ((uint16_t)i << 3)), 8, cc);
118
- }
119
- }
366
+ static void sfx_tick(void) {
367
+ if (sfx_a_t) { sfx_a_t--; if (!sfx_a_t) msx_psg_off(0); }
368
+ if (sfx_b_t) { sfx_b_t--; if (!sfx_b_t) msx_psg_noise(1, 0, 0); }
120
369
  }
121
370
 
122
- static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
123
- msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
371
+ static void sfx_lane(void) { msx_psg_tone(0, 0x180, 10); sfx_a_t = 3; }
372
+ static void sfx_accel(void) { msx_psg_tone(0, 0x110, 11); sfx_a_t = 5; }
373
+ static void sfx_brake(void) { msx_psg_noise(1, 18, 9); sfx_b_t = 5; }
374
+ static void sfx_pass(void) { msx_psg_tone(0, 0x0C0, 8); sfx_a_t = 2; }
375
+ static void sfx_crash(void) { msx_psg_noise(1, 28, 15); sfx_b_t = 20; }
376
+ static void sfx_start(void) { msx_psg_tone(0, 0x130, 12); sfx_a_t = 6; }
377
+
378
+ /* ── GAME LOGIC (clay — reshape freely) — HUD ──────────────────────────────
379
+ * Row 0 = the HUD band (third 0's text colors make it a distinct strip).
380
+ * 1P: LIVES left, DIST right. 2P: P1 crashes-left | VS | P2 crashes-left. */
381
+ static void draw_hud_labels(void) {
382
+ if (two_player) {
383
+ draw_text(1, 0, "P1");
384
+ draw_text(14, 0, "VS");
385
+ draw_text(26, 0, "P2");
386
+ } else {
387
+ draw_text(1, 0, "LIVES");
388
+ draw_text(20, 0, "DIST");
389
+ }
124
390
  }
391
+ static void draw_lives(void) {
392
+ if (two_player) {
393
+ put_tile(4, 0, (uint8_t)(T_0 + crashes_left[0]));
394
+ put_tile(29, 0, (uint8_t)(T_0 + crashes_left[1]));
395
+ } else {
396
+ put_tile(7, 0, (uint8_t)(T_0 + crashes_left[0]));
397
+ }
398
+ }
399
+ static void draw_dist(void) { if (!two_player) draw_num4(25, 0, dist); }
400
+
401
+ /* ── GAME LOGIC (clay — reshape freely) — paint the road (name table) ───────
402
+ * The whole 32x24 name table: HUD band on row 0, grass shoulders outside the
403
+ * asphalt, solid edges + centre divider, asphalt fill between. The marching
404
+ * lane dashes and roadside speckle are NOT written here — restripe_road()
405
+ * redraws those each frame to fake the scroll (see the no-hw-scroll idiom).
406
+ * The per-third color idiom shades the whole thing into depth bands for free. */
407
+ static void clear_field(void) { msx_fill_vram(VRAM_NAME, 32u * 24u, T_SPACE); }
125
408
 
126
- /* road spans cols ~11..20 (player X 96..152); grass elsewhere; dashed lane
127
- * markers between the three lanes (cols 14 and 17) on alternating rows. */
128
- static void draw_track(void) {
409
+ static void paint_road(void) {
129
410
  uint8_t row, col, t;
130
411
  for (row = 0; row < 24; row++) {
131
412
  for (col = 0; col < 32; col++) {
132
- t = (col >= 11 && col <= 20) ? T_ROAD : T_GRASS;
133
- if ((col == 14 || col == 17) && (row & 1)) t = T_LANE;
413
+ if (row == 0) t = T_SPACE; /* HUD band */
414
+ else if (col < COL_EDGE_L || col > COL_EDGE_R) t = T_GRASS;
415
+ else if (col == COL_EDGE_L || col == COL_EDGE_R) t = T_GRASS; /* shoulder */
416
+ else if (col == COL_DIVIDER) t = T_DIVIDER;
417
+ else t = T_ASPHALT;
134
418
  put_tile(col, row, t);
135
419
  }
136
420
  }
137
421
  }
138
422
 
139
- static void draw_label(void) {
140
- put_tile(1, 0, T_S); put_tile(2, 0, T_C); put_tile(3, 0, T_O);
141
- put_tile(4, 0, T_R); put_tile(5, 0, T_E);
423
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
424
+ * The faked vertical scroll. Because screen 2 has no scroll register (see the
425
+ * idiom above), we redraw only the MOVING cells each frame from road_phase:
426
+ * - The two dashed lane lines: a cell shows a dash when (row+phase) is in the
427
+ * "on" half of an 8-row cycle, asphalt otherwise — so the dash pattern
428
+ * marches DOWN one row per phase step, reading as forward motion.
429
+ * - One roadside speckle column: a tuft drops down the grass with the phase,
430
+ * giving the shoulder a sense of speed too.
431
+ * Only ~3 columns × 23 rows are touched (well inside the frame's VRAM budget),
432
+ * and the static asphalt/edges/divider painted by paint_road() are left alone. */
433
+ static void restripe_road(uint8_t phase) {
434
+ uint8_t row, on, t;
435
+ for (row = 1; row < 24; row++) {
436
+ on = (uint8_t)(((row + phase) & 7) < 4); /* dash on/off cycle */
437
+ t = on ? T_LANE : T_ASPHALT;
438
+ put_tile(COL_LANE_1, row, t);
439
+ put_tile(COL_LANE_2, row, t);
440
+ /* a single roadside tuft riding down the left grass band */
441
+ put_tile(2, row, (((row + phase) & 7) == 0) ? T_TUFT : T_GRASS);
442
+ put_tile(29, row, (((row + phase + 4) & 7) == 0) ? T_TUFT : T_GRASS);
443
+ }
142
444
  }
143
445
 
144
- static void draw_score(void) {
145
- uint16_t s = score;
146
- put_tile(7, 0, (uint8_t)(T_0 + (s / 100) % 10));
147
- put_tile(8, 0, (uint8_t)(T_0 + (s / 10) % 10));
148
- put_tile(9, 0, (uint8_t)(T_0 + s % 10));
446
+ /* ── GAME LOGIC (clay — reshape freely) — sprites: cars + traffic ───────────
447
+ * 8x8 one-color hardware sprites. Plane layout: 0 = P1 car, 1 = P2 car,
448
+ * 2..2+MAX_TRAFFIC-1 = traffic. Road art is tiles, not sprites, so the list
449
+ * never exceeds 2 + MAX_TRAFFIC planes. */
450
+ static const uint8_t spr_car[8] = {0x18,0x3C,0x5A,0x7E,0x3C,0x7E,0x5A,0x66};
451
+ static const uint8_t spr_traffic[8] = {0x66,0x5A,0x7E,0x3C,0x7E,0x5A,0x3C,0x18};
452
+ #define PAT_CAR 0
453
+ #define PAT_TRAFFIC 1
454
+ #define COL_P1 15 /* white */
455
+ #define COL_P2 13 /* light green */
456
+ #define COL_TRAFFIC 8 /* medium red */
457
+
458
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
459
+ * Sprite limits + the Y=208 terminator:
460
+ * - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
461
+ * attribute table — every higher-numbered plane vanishes, not just that
462
+ * one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
463
+ * END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
464
+ * PARK_Y (192 = first line below the display) — never at 0xD0.
465
+ * (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
466
+ * is "just offscreen" — code that leans on that breaks on MSX1.)
467
+ * - Per scanline the TMS9918 draws only 4 sprites (V9938: 8). Traffic is
468
+ * spread across 4 lanes and four screen-Y bands, so a single scanline
469
+ * almost never carries more than 2-3 of our planes; if you raise
470
+ * MAX_TRAFFIC, watch for 4-on-a-line flicker. */
471
+ #define PARK_Y 192
472
+
473
+ static void push_sprites(void) {
474
+ uint8_t i, plane = 0;
475
+ uint8_t actors = (state == ST_PLAY);
476
+ /* P1 car — blink while invulnerable after a crash (skip on odd frames). */
477
+ msx_set_sprite(plane++, lane_x[car_lane[0]],
478
+ (actors && car_active[0] && !(invuln[0] & 2)) ? CAR_Y : PARK_Y,
479
+ PAT_CAR, COL_P1);
480
+ /* P2 car. */
481
+ msx_set_sprite(plane++, lane_x[car_lane[1]],
482
+ (actors && car_active[1] && !(invuln[1] & 2)) ? CAR_Y : PARK_Y,
483
+ PAT_CAR, COL_P2);
484
+ for (i = 0; i < MAX_TRAFFIC; i++)
485
+ msx_set_sprite(plane++, lane_x[traffic_lane[i]],
486
+ (actors && traffic_alive[i]) ? traffic_y[i] : PARK_Y,
487
+ PAT_TRAFFIC, COL_TRAFFIC);
149
488
  }
150
489
 
151
- static void reset_run(void) {
490
+ /* ── GAME LOGIC (clay — reshape freely) — traffic pool (fixed slots) ── */
491
+ static void spawn_traffic(void) {
152
492
  uint8_t i;
153
- player_lane = 1;
154
- player.x = lane_x[1];
155
- player.y = PLAYER_Y;
156
- player.alive = 1;
157
- for (i = 0; i < MAX_OBSTACLES; i++) obstacles[i].alive = 0;
158
- score = 0;
159
- spawn_timer = 0;
160
- game_over_timer = 0;
161
- draw_score();
493
+ for (i = 0; i < MAX_TRAFFIC; i++) {
494
+ if (!traffic_alive[i]) {
495
+ traffic_alive[i] = 1;
496
+ traffic_lane[i] = (uint8_t)(next_rand() & 3);
497
+ traffic_y[i] = SPAWN_Y;
498
+ return;
499
+ }
500
+ }
501
+ }
502
+
503
+ /* AABB, both boxes 8x8. */
504
+ static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
505
+ uint8_t dx = (ax > bx) ? (uint8_t)(ax - bx) : (uint8_t)(bx - ax);
506
+ uint8_t dy = (ay > by) ? (uint8_t)(ay - by) : (uint8_t)(by - ay);
507
+ return (dx < 8) && (dy < 8);
508
+ }
509
+
510
+ /* ── GAME LOGIC (clay — reshape freely) — the screens ──────────────────────
511
+ * Title rows land across the play thirds — recolored for free by the thirds
512
+ * idiom. A clean name table behind the text. */
513
+ static void paint_title(void) {
514
+ uint8_t len = 0, col;
515
+ const char *p = GAME_TITLE;
516
+ while (*p++) len++;
517
+ col = (uint8_t)((32 - len) / 2);
518
+ clear_field();
519
+ draw_text(col, 6, GAME_TITLE);
520
+ draw_text(7, 11, "1P RACE - FIRE A");
521
+ draw_text(7, 13, "2P VERSUS - FIRE B");
522
+ draw_text(11, 16, "STEER L-R");
523
+ draw_text(11, 19, "BEST 0000"); /* the space blanks the cell between */
524
+ draw_num4(16, 19, best);
162
525
  }
163
526
 
164
- static void spawn_obstacle(void) {
527
+ static void paint_over(void) {
528
+ clear_field();
529
+ if (two_player) {
530
+ draw_text(11, 7, winner ? "P2 WINS" : "P1 WINS");
531
+ draw_text(8, 10, "RIVAL CRASHED OUT");
532
+ } else {
533
+ draw_text(11, 7, "WRECKED");
534
+ draw_text(11, 10, "DIST"); draw_num4(16, 10, dist);
535
+ draw_text(11, 13, "BEST"); draw_num4(16, 13, best);
536
+ }
537
+ draw_text(8, 17, "FIRE FOR TITLE");
538
+ prev_t1 = prev_t2 = 1; /* swallow a fire still held from play */
539
+ }
540
+
541
+ /* ── GAME LOGIC (clay — reshape freely) — start a run ── */
542
+ static void start_game(uint8_t versus) {
165
543
  uint8_t i;
166
- for (i = 0; i < MAX_OBSTACLES; i++) {
167
- if (!obstacles[i].alive) {
168
- obstacles[i].x = lane_x[next_rand() % 3];
169
- obstacles[i].y = 16;
170
- obstacles[i].alive = 1;
171
- return;
544
+ two_player = versus;
545
+ for (i = 0; i < MAX_TRAFFIC; i++) traffic_alive[i] = 0;
546
+ for (i = 0; i < 2; i++) { crashes_left[i] = START_LIVES; invuln[i] = 0; prev_dir[i] = 0; }
547
+ if (versus) {
548
+ car_active[0] = 1; car_active[1] = 1;
549
+ lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
550
+ lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
551
+ speed = SPEED_2P; /* one road, fixed shared speed */
552
+ } else {
553
+ car_active[0] = 1; car_active[1] = 0;
554
+ lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
555
+ speed = 1;
556
+ }
557
+ dist = 0; dist_frac = 0;
558
+ spawn_timer = 0;
559
+ road_phase = 0;
560
+ prev_acc = 1;
561
+ paint_road();
562
+ restripe_road(road_phase);
563
+ draw_hud_labels();
564
+ draw_lives();
565
+ draw_dist();
566
+ sfx_start();
567
+ state = ST_PLAY;
568
+ }
569
+
570
+ /* ── GAME LOGIC (clay — reshape freely) — run over: result + record.
571
+ * Persistence choice: a 1P run banks its DISTANCE; the best is the stat a
572
+ * returning player chases. 2P matches never touch it (humans beating each
573
+ * other isn't a record). On THIS core the best is session-only RAM (no
574
+ * SAVE_RAM — see the file header); the Genesis/NES/SMS builds persist the
575
+ * identical best distance to cartridge SRAM. ── */
576
+ static void end_run(void) {
577
+ if (!two_player && dist > best) best = dist;
578
+ sfx_crash();
579
+ paint_over();
580
+ state = ST_OVER;
581
+ }
582
+
583
+ /* ── GAME LOGIC (clay — reshape freely) — a crash ── */
584
+ static void crash(uint8_t p) {
585
+ sfx_crash();
586
+ invuln[p] = 60; /* blink + no-collide grace */
587
+ if (!two_player) speed = 1; /* a wreck kills your momentum */
588
+ if (crashes_left[p] > 0) --crashes_left[p];
589
+ draw_lives();
590
+ if (crashes_left[p] == 0) {
591
+ winner = (uint8_t)(1 - p); /* versus: the OTHER player wins */
592
+ end_run();
593
+ }
594
+ }
595
+
596
+ /* ── GAME LOGIC (clay — reshape freely) — per-player input ───────────────────
597
+ * P0 reads JOYSTICK PORT 1 (keyboard cursors fall back); P1 reads PORT 2.
598
+ * LEFT/RIGHT steer between lanes (edge-detected — a held stick must NOT
599
+ * machine-gun across the road). 1P only: UP/A accelerate, DOWN/B brake. */
600
+ static void update_player(uint8_t p) {
601
+ uint8_t dir, left, right;
602
+ if (p == 0) {
603
+ dir = msx_read_joystick(1);
604
+ if (dir == STICK_CENTER) dir = msx_read_joystick(0);
605
+ } else {
606
+ dir = msx_read_joystick(2);
607
+ }
608
+ left = (dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL);
609
+ right = (dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR);
610
+ {
611
+ uint8_t pl = prev_dir[p];
612
+ uint8_t prev_left = (pl == STICK_LEFT || pl == STICK_UL || pl == STICK_DL);
613
+ uint8_t prev_right = (pl == STICK_RIGHT || pl == STICK_UR || pl == STICK_DR);
614
+ if (left && !prev_left && car_lane[p] > lane_min[p]) { --car_lane[p]; sfx_lane(); }
615
+ if (right && !prev_right && car_lane[p] < lane_max[p]) { ++car_lane[p]; sfx_lane(); }
616
+ }
617
+ prev_dir[p] = dir;
618
+
619
+ if (!two_player) { /* speed is shared — only 1P gets it */
620
+ uint8_t up = (dir == STICK_UP || dir == STICK_UL || dir == STICK_UR) || gttrig(1) || gttrig(0);
621
+ uint8_t down = (dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR);
622
+ uint8_t acc = (uint8_t)(up ? 1 : (down ? 2 : 0));
623
+ if (acc && acc != prev_acc) {
624
+ if (up && speed < 4) { ++speed; sfx_accel(); }
625
+ if (down && speed > 1) { --speed; sfx_brake(); }
172
626
  }
627
+ prev_acc = acc;
173
628
  }
629
+ if (invuln[p] > 0) --invuln[p];
174
630
  }
175
631
 
176
632
  void main(void) {
177
- uint8_t i, slot, dir, prev_dir;
178
- int16_t step;
633
+ uint8_t i, p, t1, t2;
179
634
 
635
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
636
+ * Init order: set the video mode FIRST (INIGRP also clears VRAM — any
637
+ * upload done before it is wiped), then tiles, then sprites. The crt0's
638
+ * INIT contract means main() must NEVER return — the BIOS has nothing
639
+ * sane to fall back to — hence the for(;;) below. */
180
640
  msx_set_screen2();
181
641
  msx_clear_sprites();
182
642
  load_tiles();
183
- draw_track();
184
- draw_label();
185
-
186
- msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_PLAYER * 8), spr_player, 8);
187
- msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_ENEMY * 8), spr_enemy, 8);
643
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_CAR * 8), spr_car, 8);
644
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_TRAFFIC * 8), spr_traffic, 8);
188
645
 
646
+ msx_music(0); /* the lib's demo loop also owns channel C —
647
+ * hand the channel to OUR tune table instead */
648
+ best = 0; /* session record (no SAVE_RAM on this core) */
189
649
  rng = 0xACE1;
190
- blip = 0;
191
- prev_dir = 0;
192
- reset_run();
650
+ music_step = music_timer = 0;
651
+ sfx_a_t = sfx_b_t = 0;
652
+ prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
653
+ two_player = 0;
654
+ car_lane[0] = car_lane[1] = 1;
655
+ state = ST_TITLE;
656
+ paint_title();
193
657
 
194
658
  for (;;) {
195
659
  vsync();
196
- msx_music_tick();
197
-
198
- /* push sprites */
199
- slot = 0;
200
- msx_set_sprite(slot++, player.x, player.y, PAT_PLAYER, COL_PLAYER);
201
- for (i = 0; i < MAX_OBSTACLES; i++)
202
- msx_set_sprite(slot++, obstacles[i].x,
203
- obstacles[i].alive ? obstacles[i].y : SPRITE_END_Y,
204
- PAT_ENEMY, COL_ENEMY);
205
-
206
- dir = msx_read_joystick(1);
207
- if (dir == STICK_CENTER) dir = msx_read_joystick(0);
208
-
209
- if (game_over_timer > 0) {
210
- game_over_timer--;
211
- if (game_over_timer == 0) reset_run();
212
- prev_dir = dir;
213
- if (blip) { blip--; if (!blip) msx_psg_off(0); }
660
+ music_tick();
661
+ sfx_tick();
662
+
663
+ if (state == ST_TITLE) {
664
+ /* ── GAME LOGIC (clay) title: trig A = 1P race; trig B = 2P. */
665
+ t1 = (uint8_t)(gttrig(1) || gttrig(0));
666
+ t2 = (uint8_t)(gttrig(3) || gttrig(2));
667
+ if (t2 && !prev_t2) start_game(1);
668
+ else if (t1 && !prev_t1) start_game(0);
669
+ prev_t1 = t1; prev_t2 = t2;
670
+ push_sprites();
214
671
  continue;
215
672
  }
216
673
 
217
- if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
218
- && !(prev_dir == STICK_LEFT || prev_dir == STICK_UL || prev_dir == STICK_DL)
219
- && player_lane > 0) { player_lane--; msx_psg_tone(1, 0x280, 6); blip = 3; }
220
- if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
221
- && !(prev_dir == STICK_RIGHT || prev_dir == STICK_UR || prev_dir == STICK_DR)
222
- && player_lane < 2) { player_lane++; msx_psg_tone(1, 0x280, 6); blip = 3; }
223
- player.x = lane_x[player_lane];
224
- prev_dir = dir;
225
-
226
- step = (int16_t)(2 + (score / 200));
227
- if (step > 4) step = 4;
228
-
229
- for (i = 0; i < MAX_OBSTACLES; i++) {
230
- if (!obstacles[i].alive) continue;
231
- obstacles[i].y = (uint8_t)(obstacles[i].y + step);
232
- if (obstacles[i].y >= 184) obstacles[i].alive = 0;
674
+ if (state == ST_OVER) {
675
+ /* Freeze the final frame; any fire button returns to the title. */
676
+ t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
677
+ if (t1 && !prev_t1) {
678
+ state = ST_TITLE;
679
+ msx_clear_sprites();
680
+ two_player = 0;
681
+ paint_title();
682
+ }
683
+ prev_t1 = t1; prev_t2 = t1;
684
+ push_sprites();
685
+ continue;
233
686
  }
234
687
 
235
- spawn_timer = (uint8_t)(spawn_timer + 1);
236
- if (spawn_timer >= 36) { spawn_timer = 0; spawn_obstacle(); }
237
-
238
- for (i = 0; i < MAX_OBSTACLES; i++) {
239
- if (obstacles[i].alive && aabb(&player, &obstacles[i])) {
240
- game_over_timer = 60;
241
- msx_psg_tone(0, 0x600, 15); blip = 12;
242
- break;
688
+ /* ── ST_PLAY — GAME LOGIC (clay) ────────────────────────────────────
689
+ * Both players (or just P1) update EVERY frame a simultaneous
690
+ * versus race, not alternating turns. */
691
+ next_rand(); /* tick the noise source every play frame */
692
+
693
+ update_player(0);
694
+ if (two_player) update_player(1);
695
+
696
+ /* Advance the faked scroll by the current speed, then restripe only
697
+ * the moving cells (see the no-hw-scroll idiom). */
698
+ road_phase = (uint8_t)((road_phase + speed) & 7);
699
+ restripe_road(road_phase);
700
+
701
+ /* 1P distance: 1 unit per 16 scrolled pixels. */
702
+ if (!two_player) {
703
+ dist_frac = (uint8_t)(dist_frac + speed);
704
+ if (dist_frac >= 16) {
705
+ dist_frac = (uint8_t)(dist_frac - 16);
706
+ if (dist < 9999u) ++dist;
707
+ draw_dist();
243
708
  }
244
709
  }
245
710
 
246
- if (score < 999) { score++; draw_score(); }
711
+ /* Traffic flows DOWN the road at road speed (reads as slower cars you
712
+ * overtake); despawn past the bottom with a little pass tick. */
713
+ for (i = 0; i < MAX_TRAFFIC; i++) {
714
+ if (!traffic_alive[i]) continue;
715
+ if (traffic_y[i] >= (uint8_t)(DESPAWN_Y - speed)) {
716
+ traffic_alive[i] = 0;
717
+ sfx_pass();
718
+ } else {
719
+ traffic_y[i] = (uint8_t)(traffic_y[i] + speed);
720
+ }
721
+ }
722
+ if (++spawn_timer >= SPAWN_PERIOD) { spawn_timer = 0; spawn_traffic(); }
723
+
724
+ /* Traffic ↔ cars. A just-wrecked car blinks + can't collide for 60f. */
725
+ for (i = 0; i < MAX_TRAFFIC; i++) {
726
+ if (!traffic_alive[i]) continue;
727
+ for (p = 0; p < 2; p++) {
728
+ if (!car_active[p] || invuln[p]) continue;
729
+ if (hits(lane_x[traffic_lane[i]], traffic_y[i], lane_x[car_lane[p]], CAR_Y)) {
730
+ traffic_alive[i] = 0;
731
+ crash(p);
732
+ break;
733
+ }
734
+ }
735
+ if (state != ST_PLAY) break; /* a crash may have ended the run */
736
+ }
737
+ if (state != ST_PLAY) continue;
247
738
 
248
- if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
739
+ push_sprites();
249
740
  }
250
741
  }