romdevtools 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,182 +1,649 @@
1
- /* ── sports/main.c — MSX two-player Pong scaffold (screen 2) ─────────
1
+ /* ── sports/main.c — MSX head-to-head court sports (complete example game) ────
2
2
  *
3
- * Mirrors the SMS/GB/etc Pong scaffolds, translated to the MSX VDP via the
4
- * romdev helper lib (msx_hw.h + msx_vdp.c).
3
+ * SPARK SWAT a COMPLETE, working game: title screen, 1P VS a beatable CPU
4
+ * and 2P SIMULTANEOUS VERSUS (P2 on JOYSTICK PORT 2), first-to-5 match flow
5
+ * into a result screen, a longest-win-streak record, music + SFX on the
6
+ * AY-3-8910 PSG, and the MSX's signature SCREEN-2 PER-ROW COLOR: the court
7
+ * floor, the two rails, the centre net and the HUD band all come ENTIRELY
8
+ * from the three independent screen-2 color thirds plus a one-tile vertical
9
+ * "pulse" gradient down the net — costing zero extra tiles.
5
10
  *
6
- * The court (green field + white top/bottom + sidelines + dashed centre
7
- * net) fills the whole 32x24 screen-2 name table. Two paddles (each three
8
- * stacked 8x8 sprites) and a ball are sprites.
11
+ * The game (Pong lineage): a ball rallies between two paddles on a netted
12
+ * court. UP/DOWN move your paddle; where the ball strikes the paddle sets the
13
+ * return angle (centre = flat, edges = steep). Steep edge returns outrun the
14
+ * half-speed CPU — that is exactly how a human beats it. First side to 5 wins.
9
15
  *
10
- * Controls:
11
- * Player 1 joystick PORT 1 UP/DOWN moves the left paddle.
12
- * Player 2joystick PORT 2 UP/DOWN moves the right paddle; when no
13
- * second pad is present (stick 2 reads centre) the right paddle falls
14
- * back to chase-the-ball AI, so the game is playable solo. Plug a
15
- * second pad in mid-session and player 2 just starts working.
16
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
17
+ * very different one. The markers tell you what's what:
18
+ * HARDWARE IDIOM (load-bearing) dodges a documented MSX footgun; reshape
19
+ * your gameplay around it (see TROUBLESHOOTING before changing).
20
+ * GAME LOGIC (clay) court art, ball physics, CPU skill, scoring rules:
21
+ * reshape freely.
16
22
  *
17
- * Cartridge rule: INIT must never return — main() ends in for(;;).
23
+ * What depends on what:
24
+ * msx_hw.h / msx_vdp.c — VDP + PSG + joystick helpers (direct Z80 ports;
25
+ * the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
26
+ * read msx_vdp.c before adding your own PSG pokes).
27
+ * msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
28
+ * INIT must NEVER return, so main() ends in for(;;).
29
+ *
30
+ * A TEACHING POINT vs the Genesis version of this game
31
+ * (examples/genesis/templates/sports.c): the Genesis hangs its HUD on a
32
+ * hardware WINDOW plane (a fixed status bar at zero per-frame cost) and paints
33
+ * the court ONCE into plane B. The MSX has no window plane and no DMA — but
34
+ * screen 2 gives us three independent COLOR thirds for free, so our HUD band,
35
+ * court floor and rails are all one tilemap differentiated purely by which
36
+ * third (row band) they sit in. Same genre, a different "free" hardware gift.
37
+ *
38
+ * Controls: JOYSTICK PORT 1 (or keyboard cursors) UP/DOWN moves the left
39
+ * paddle. In 2P versus, JOYSTICK PORT 2 UP/DOWN moves the right paddle. On
40
+ * the title screen trigger A (or SPACE) starts 1P vs CPU; trigger B starts
41
+ * 2P versus. On the result screen any fire returns to the title.
42
+ *
43
+ * Record honesty: the bundled bluemsx core build exposes NO battery save path
44
+ * (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so BEST (the
45
+ * longest 1P-vs-CPU win streak) lives in plain RAM: it survives title↔match
46
+ * cycles but NOT a power cycle / hardReset. Never fake persistence — if you
47
+ * need real saves, that's a future core round (ASCII8-SRAM mapper carts
48
+ * exist; the core just doesn't surface their RAM yet). The Genesis/NES/SMS
49
+ * versions of this game DO persist the same streak to cartridge SRAM.
18
50
  */
19
51
  #include "msx_hw.h"
20
52
 
21
- /* ── interrupt-free vblank sync (poll VDP status S#0 bit 7) ────────────── */
53
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
54
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
55
+ #define GAME_TITLE "SPARK SWAT"
56
+
57
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
58
+ * Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
59
+ * the port ALSO clears the flag, so one read per frame = one game step per
60
+ * frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
61
+ * works even with interrupts masked, and never depends on the BIOS ISR
62
+ * keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
63
+ * flag first and this loop just waits for the next one; a one-frame hiccup,
64
+ * never a hang.) */
22
65
  __sfr __at 0x99 VDPSTATUS;
23
66
  static void vsync(void) {
24
- (void)VDPSTATUS;
67
+ (void)VDPSTATUS; /* throw away a possibly-stale flag */
25
68
  while (!(VDPSTATUS & 0x80)) {
26
69
  }
27
70
  }
28
71
 
29
- #define COURT_TOP 16
30
- #define COURT_BOT 184
31
- #define PADDLE_H 24
32
- #define BALL_SIZE 8
33
- #define PADDLE_X1 16
34
- #define PADDLE_X2 232
72
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
73
+ * Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
74
+ * court tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits draw in
75
+ * the tile's FOREGROUND color, clear bits in its BACKGROUND color (both come
76
+ * from the screen-2 color table — see the per-row-color idiom below). */
77
+ #define T_SPACE 0
78
+ #define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
79
+ #define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
80
+ #define T_DASH 37
81
+ #define T_FLOOR 38 /* the court surface (faint speckle) */
82
+ #define T_RAIL 39 /* solid top/bottom court rail */
83
+ #define T_NET 40 /* dashed centre net (its COLOR carries the pulse) */
84
+ #define NUM_TILES 41
35
85
 
36
- /* tile patterns (8x8) for the court */
37
- #define T_FIELD 0
38
- #define T_LINE 1
39
- #define T_NET 2
86
+ static const uint8_t font[NUM_TILES][8] = {
87
+ /* SPACE */ {0,0,0,0,0,0,0,0},
88
+ /* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
89
+ /* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
90
+ /* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
91
+ /* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
92
+ /* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
93
+ /* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
94
+ /* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
95
+ /* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
96
+ /* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
97
+ /* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
98
+ /* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
99
+ /* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
100
+ /* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
101
+ /* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
102
+ /* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
103
+ /* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
104
+ /* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
105
+ /* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
106
+ /* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
107
+ /* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
108
+ /* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
109
+ /* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
110
+ /* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
111
+ /* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
112
+ /* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
113
+ /* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
114
+ /* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
115
+ /* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
116
+ /* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
117
+ /* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
118
+ /* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
119
+ /* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
120
+ /* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
121
+ /* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
122
+ /* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
123
+ /* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
124
+ /* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
125
+ /* 38 FLOOR (sparse speckle so the arena reads as a court, not a void) */
126
+ {0x00,0x00,0x10,0x00,0x00,0x00,0x01,0x00},
127
+ /* 39 RAIL (solid border) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
128
+ /* 40 NET (dashed bar — solid pixels so the COLOR pulse below shows) */
129
+ {0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18},
130
+ };
40
131
 
41
- static const uint8_t TILE_FIELD[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
42
- static const uint8_t TILE_LINE[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
43
- static const uint8_t TILE_NET[8] = {0x18,0x18,0x00,0x00,0x18,0x18,0x00,0x00};
132
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
133
+ * SCREEN-2 PER-ROW COLOR — the MSX's signature background trick.
134
+ *
135
+ * Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
136
+ *
137
+ * 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
138
+ * (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
139
+ * pattern table slice and its OWN 2KB color table slice:
140
+ * patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
141
+ * The SAME tile index can look completely different in each third. We
142
+ * exploit exactly that to make a single FLOOR/RAIL/NET tile set read as a
143
+ * depth-shaded court: third 0 (the HUD band + top rail) gets its own
144
+ * bright text colors; the play thirds get a cooler court palette; the
145
+ * bottom third deepens toward the foreground. One tile set, three bands,
146
+ * zero extra tiles — the sports-genre twin of the shmup's depth starfield.
147
+ *
148
+ * 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
149
+ * row — each packing (foreground<<4)|background from the fixed TMS9918
150
+ * palette. So one tile can carry an 8-color vertical gradient: T_NET's
151
+ * whole "energy pulse" running down the centre net is a single tile,
152
+ * colors only.
153
+ *
154
+ * Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
155
+ * R4=0x03 — the "thirds" configuration), and pattern + color uploads to
156
+ * EVERY third a tile is used in. Tile N's slot is pattern[N*8] / color[N*8].
157
+ *
158
+ * TMS9918 fixed palette used here: 1 black, 4 dark blue, 5 light blue,
159
+ * 6 dark red, 7 cyan, 8 medium red, 11 light yellow, 12 green, 13 light green,
160
+ * 14 gray, 15 white (high nibble = fg, low nibble = bg of each row byte). */
161
+ static const uint8_t col_text[3] = { 0xF4, 0xF1, 0xF1 }; /* HUD white-on-blue; play/title white-on-black */
162
+ /* The court FLOOR speckle, banded by third: cyan-ish near the HUD, deeper blue
163
+ * mid-court, light-blue close — pure per-third recolor of one tile. */
164
+ static const uint8_t col_floor[3] = { 0x71, 0x41, 0x51 };
165
+ /* The court RAILS, banded so the top rail (third 0) reads bright and the
166
+ * bottom rail (third 2) reads cooler — same solid tile, three colors. */
167
+ static const uint8_t col_rail[3] = { 0xF1, 0xE1, 0xD1 };
168
+ /* T_NET: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row "energy
169
+ * pulse" down the net (black → dark blue → cyan → white and back). The net
170
+ * pattern is a solid 2px bar so only the fg nibbles show. Drawn down the
171
+ * centre column; recolored again per third for free. */
172
+ static const uint8_t col_net[8] = { 0x11,0x41,0x71,0xF1,0xF1,0x71,0x41,0x11 };
44
173
 
45
- /* colour bytes (hi fg, lo bg). 3=green(dark), 12=green(light), 15=white */
46
- #define COL_FIELD 0x21 /* dark green field on black */
47
- #define COL_LINE 0xF1 /* white line on black */
48
- #define COL_NET 0xF2 /* white net dashes on dark-green field */
174
+ static void load_tiles(void) {
175
+ uint8_t third, i;
176
+ uint16_t patbase, colbase;
177
+ for (third = 0; third < 3; third++) {
178
+ patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
179
+ colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
180
+ for (i = 0; i < NUM_TILES; i++) {
181
+ uint8_t col;
182
+ /* pattern bits are the same in every third — only COLOR varies */
183
+ msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
184
+ if (i == T_NET) { /* the one per-pixel-row gradient */
185
+ msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_net, 8);
186
+ continue;
187
+ }
188
+ if (i == T_FLOOR) col = col_floor[third];
189
+ else if (i == T_RAIL) col = col_rail[third];
190
+ else col = col_text[third];
191
+ msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
192
+ }
193
+ }
194
+ }
49
195
 
50
- /* paddle/ball sprite pattern (8x8 solid block) */
51
- static const uint8_t SPR_BLOCK[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
52
- #define COL_SPR 15 /* white */
196
+ /* ── GAME LOGIC (clay reshape freely) — name-table drawing helpers ────────
197
+ * Screen 2 VRAM writes are safe at any point in the frame at C speed: the
198
+ * TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
199
+ * and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
200
+ * are the thing that outruns the VDP — see TROUBLESHOOTING.) */
201
+ static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
202
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
203
+ }
53
204
 
54
- static int16_t p1y, p2y, bx, by;
55
- static int8_t bdx, bdy;
205
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
206
+ uint8_t buf[32];
207
+ uint8_t n = 0;
208
+ while (*s && n < 32) {
209
+ char c = *s++;
210
+ if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
211
+ else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
212
+ else if (c == '-') buf[n] = T_DASH;
213
+ else buf[n] = T_SPACE;
214
+ n++;
215
+ }
216
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
217
+ }
218
+
219
+ static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
220
+ uint8_t buf[4];
221
+ buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
222
+ buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
223
+ buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
224
+ buf[3] = (uint8_t)(T_0 + v % 10);
225
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
226
+ }
227
+
228
+ /* ── GAME LOGIC (clay — reshape freely) — court geometry + match rules ───────
229
+ * The court fills the 32x24 screen-2 name table. Rails on name-table rows 2
230
+ * and 22; COURT_TOP/BOT keep the ball between them (pixels). Net down column
231
+ * 16. Row 0 is the HUD band (third 0's text colors make it a distinct strip). */
232
+ #define NET_COL 16
233
+ #define RAIL_TOP_ROW 2
234
+ #define RAIL_BOT_ROW 22
235
+ #define COURT_TOP 24 /* first pixel row below the top rail */
236
+ #define COURT_BOT 176 /* first pixel row of the bottom rail */
237
+ #define PADDLE_H 24 /* 3 stacked 8x8 sprites */
238
+ #define PADDLE_X1 16 /* P1 — left side */
239
+ #define PADDLE_X2 232 /* P2/CPU — right side */
240
+ #define BALL_W 8
241
+ #define BALL_H 8
242
+ #define WIN_SCORE 5 /* first to 5 takes the match */
243
+ #define P_SPEED 3 /* px/frame — both humans move at this */
244
+ #define CPU_SPEED 1 /* px/frame — HALF the ball's 2px/frame *
245
+ * horizontal speed: it cannot always reach *
246
+ * a steep edge return, so a human who aims *
247
+ * edge hits beats it (verified). Raise this *
248
+ * toward P_SPEED to make the CPU tougher. */
249
+
250
+ static int16_t p1y, p2y; /* paddle top Y (pixels) */
251
+ static int16_t bx, by; /* ball top-left (pixels) */
252
+ static int8_t bdx, bdy; /* ball velocity (px/frame) */
56
253
  static uint8_t score_p1, score_p2;
57
- static uint8_t serve_timer;
58
- static uint8_t blip;
254
+ static uint8_t serve_timer; /* freeze frames between points */
255
+ static uint8_t two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
256
+ static uint8_t streak; /* current 1P-vs-CPU win streak (RAM) */
257
+ static uint16_t best_streak; /* SESSION-ONLY record — see end_match + the
258
+ * record-honesty note at the top of file.
259
+ * No SAVE_RAM on this core, so it lives in
260
+ * plain RAM: survives title↔match cycles,
261
+ * NOT a power cycle (honest, not faked). */
262
+ static uint8_t new_record; /* result screen shows NEW RECORD */
59
263
 
60
- static void load_tiles(void) {
61
- uint8_t third;
62
- uint16_t pat, col;
63
- for (third = 0; third < 3; third++) {
64
- pat = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
65
- col = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
66
- msx_vram_write((uint16_t)(pat + T_FIELD * 8), TILE_FIELD, 8);
67
- msx_vram_write((uint16_t)(pat + T_LINE * 8), TILE_LINE, 8);
68
- msx_vram_write((uint16_t)(pat + T_NET * 8), TILE_NET, 8);
69
- msx_fill_vram((uint16_t)(col + T_FIELD * 8), 8, COL_FIELD);
70
- msx_fill_vram((uint16_t)(col + T_LINE * 8), 8, COL_LINE);
71
- msx_fill_vram((uint16_t)(col + T_NET * 8), 8, COL_NET);
264
+ #define ST_TITLE 0
265
+ #define ST_PLAY 1
266
+ #define ST_OVER 2
267
+ static uint8_t state;
268
+ static uint8_t prev_t1, prev_t2; /* title/over trigger edge detection */
269
+
270
+ /* ── GAME LOGIC (clay reshape freely) xorshift16 PRNG.
271
+ * A versus game NEEDS this: the MSX is fully deterministic, so without a noise
272
+ * source two fixed strategies lock into an infinite rally loop (the exact same
273
+ * cycle, forever — a match that NEVER ends). next_rand() is ticked once per
274
+ * play frame so identical game states a few seconds apart still diverge, and
275
+ * every paddle return adds a ±1 "spin" — so an idle 1P-vs-CPU match always
276
+ * reaches 5 in bounded time. */
277
+ static uint16_t rng;
278
+ static uint8_t next_rand(void) {
279
+ rng ^= (uint16_t)(rng << 7);
280
+ rng ^= (uint16_t)(rng >> 9);
281
+ rng ^= (uint16_t)(rng << 8);
282
+ return (uint8_t)(rng & 0xFF);
283
+ }
284
+
285
+ /* ── GAME LOGIC (clay — reshape freely) — music + SFX on the AY-3-8910 ──────
286
+ * Channel plan: A = paddle/score blips, B = rail bonk + whistle noise, C =
287
+ * music. The PSG has 3 tone channels + ONE shared noise generator, mixed
288
+ * per-channel in reg 7. All register traffic goes through msx_psg_tone/noise/
289
+ * off — they wrap the PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT
290
+ * ISR clobbers the PSG address latch every frame (the bug that once silenced
291
+ * every MSX scaffold — see msx_vdp.c).
292
+ *
293
+ * The tune: one period entry per half-beat, 0 = rest. AY period =
294
+ * 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame; a
295
+ * note advances every 8 frames. The lib's built-in demo loop (msx_music_tick)
296
+ * also uses channel C, so we switch it OFF in main() and run THIS table
297
+ * instead — edit this table to rescore. */
298
+ static const uint16_t tune[32] = {
299
+ 285, 0, 339, 285, 254, 0, 285, 339, /* G4 E4 G4 A4 G4 E4 (bright march) */
300
+ 427, 0, 339, 254, 339, 0, 0, 0, /* C4 E4 A4 E4 rest */
301
+ 320, 0, 285, 254, 214, 0, 254, 285, /* F4 G4 A4 C5 A4 G4 */
302
+ 339, 0, 285, 339, 427, 0, 0, 0, /* E4 G4 E4 C4 rest */
303
+ };
304
+ static uint8_t music_step, music_timer;
305
+ static uint8_t sfx_a_t, sfx_b_t; /* frames left on the A/B SFX channels */
306
+
307
+ static void music_tick(void) {
308
+ if (music_timer == 0) {
309
+ uint16_t p = tune[music_step & 31];
310
+ if (p) msx_psg_tone(2, p, 9);
311
+ else msx_psg_off(2);
312
+ music_step++;
72
313
  }
314
+ music_timer++;
315
+ if (music_timer >= 8) music_timer = 0;
73
316
  }
74
317
 
75
- static void set_cell(uint8_t row, uint8_t col, uint8_t tile) {
76
- msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
318
+ static void sfx_tick(void) {
319
+ if (sfx_a_t) { sfx_a_t--; if (!sfx_a_t) msx_psg_off(0); }
320
+ if (sfx_b_t) { sfx_b_t--; if (!sfx_b_t) msx_psg_noise(1, 0, 0); }
321
+ }
322
+
323
+ static void sfx_hit(void) { msx_psg_tone(0, 0x200, 11); sfx_a_t = 4; }
324
+ static void sfx_rail(void) { msx_psg_tone(1, 0x300, 9); sfx_b_t = 3; }
325
+ static void sfx_point(void) { msx_psg_noise(1, 14, 13); sfx_b_t = 8; }
326
+ static void sfx_over(void) { msx_psg_noise(1, 28, 14); sfx_b_t = 22; }
327
+ static void sfx_start(void) { msx_psg_tone(0, 0x130, 12); sfx_a_t = 6; }
328
+
329
+ /* ── GAME LOGIC (clay — reshape freely) — HUD ──────────────────────────────
330
+ * Row 0 = the HUD band (third 0's text colors make it a distinct strip).
331
+ * P1 score | BEST (longest streak) | P2/CPU score. */
332
+ static void draw_hud_labels(void) {
333
+ draw_text(1, 0, "P1");
334
+ draw_text(12, 0, "BEST");
335
+ draw_text(25, 0, two_player ? "P2" : "CPU");
336
+ }
337
+ static void draw_scores(void) {
338
+ put_tile(4, 0, (uint8_t)(T_0 + score_p1));
339
+ put_tile(29, 0, (uint8_t)(T_0 + score_p2));
77
340
  }
341
+ static void draw_best(void) { draw_num4(17, 0, best_streak); }
78
342
 
79
- static void draw_court(void) {
343
+ /* ── GAME LOGIC (clay — reshape freely) — paint the court (name table) ──────
344
+ * The whole 32x24 name table: HUD band on row 0, rails on rows 2 and 22, net
345
+ * down column 16, floor everywhere else. The per-third color idiom shades it
346
+ * into bands for free — this routine writes only TILE INDICES. */
347
+ static void clear_field(void) { msx_fill_vram(VRAM_NAME, 32u * 24u, T_SPACE); }
348
+
349
+ static void paint_court(void) {
80
350
  uint8_t row, col, t;
81
351
  for (row = 0; row < 24; row++) {
82
352
  for (col = 0; col < 32; col++) {
83
- t = T_FIELD;
84
- if (row <= 1 || row >= 22) t = T_LINE;
85
- else if (col == 1 || col == 30) t = T_LINE;
86
- else if (col == 16) t = T_NET;
87
- set_cell(row, col, t);
353
+ if (row == 0) t = T_SPACE; /* HUD band */
354
+ else if (row == RAIL_TOP_ROW
355
+ || row == RAIL_BOT_ROW) t = T_RAIL;
356
+ else if (row > RAIL_TOP_ROW
357
+ && row < RAIL_BOT_ROW
358
+ && col == NET_COL) t = T_NET;
359
+ else if (row > RAIL_TOP_ROW
360
+ && row < RAIL_BOT_ROW) t = T_FLOOR;
361
+ else t = T_SPACE;
362
+ put_tile(col, row, t);
88
363
  }
89
364
  }
365
+ draw_hud_labels();
366
+ draw_scores();
367
+ draw_best();
90
368
  }
91
369
 
370
+ /* ── GAME LOGIC (clay — reshape freely) — sprites: paddles + ball ───────────
371
+ * 8x8 one-color hardware sprites. Plane layout: 0-2 = P1 paddle (3 stacked
372
+ * cells), 3-5 = P2 paddle, 6 = ball. Locked court art is tiles, not sprites,
373
+ * so the list never needs more than 7 planes. */
374
+ static const uint8_t spr_block[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
375
+ static const uint8_t spr_ball[8] = {0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C};
376
+ #define PAT_PADDLE 0
377
+ #define PAT_BALL 1
378
+ #define COL_P1 15 /* white */
379
+ #define COL_P2 8 /* medium red */
380
+ #define COL_BALL 11 /* light yellow — distinct from the white paddles */
381
+
382
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
383
+ * Sprite limits + the Y=208 terminator:
384
+ * - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
385
+ * attribute table — every higher-numbered plane vanishes, not just that
386
+ * one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
387
+ * END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
388
+ * PARK_Y (192 = first line below the display) — never at 0xD0.
389
+ * (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
390
+ * is "just offscreen" — code that leans on that breaks on MSX1.)
391
+ * - Per scanline the TMS9918 draws only 4 sprites (V9938: 8). The two
392
+ * paddles sit at opposite screen edges and the ball rallies between them,
393
+ * so a single scanline never carries more than 2 of our 7 planes. */
394
+ #define PARK_Y 192
395
+
396
+ /* Push the two paddles + ball to their planes. Paddles freeze (but stay
397
+ * visible) on the result screen; the ball parks offscreen there and on the
398
+ * title. Never park at 0xD0 mid-list — see the idiom. */
399
+ static void push_sprites(void) {
400
+ uint8_t i;
401
+ uint8_t actors = (state != ST_TITLE); /* paddles show in play + result */
402
+ uint8_t ball_on = (state == ST_PLAY); /* ball only lives during a rally*/
403
+ for (i = 0; i < PADDLE_H / 8; i++) {
404
+ msx_set_sprite((uint8_t)(0 + i), PADDLE_X1,
405
+ actors ? (uint8_t)(p1y + i * 8) : PARK_Y, PAT_PADDLE, COL_P1);
406
+ msx_set_sprite((uint8_t)(3 + i), PADDLE_X2,
407
+ actors ? (uint8_t)(p2y + i * 8) : PARK_Y, PAT_PADDLE, COL_P2);
408
+ }
409
+ msx_set_sprite(6, (uint8_t)bx, ball_on ? (uint8_t)by : PARK_Y, PAT_BALL, COL_BALL);
410
+ }
411
+
412
+ /* ── GAME LOGIC (clay — reshape freely) — serve: ball to centre, toward the
413
+ * chosen side. The serve angle takes a PRNG bit (not a fixed alternation) —
414
+ * one more place determinism is broken so idle matches can't settle. */
92
415
  static void serve_ball(uint8_t to_left) {
93
416
  bx = 124;
94
- by = 90;
417
+ by = (COURT_TOP + COURT_BOT) / 2;
95
418
  bdx = to_left ? -2 : 2;
96
- bdy = ((score_p1 + score_p2) & 1) ? -1 : 1;
97
- serve_timer = 30;
419
+ bdy = (next_rand() & 1) ? -1 : 1;
420
+ serve_timer = 30; /* half-second breather */
421
+ }
422
+
423
+ /* ── GAME LOGIC (clay — reshape freely) — the screens ──────────────────────
424
+ * Title rows land across the play thirds — recolored for free by the thirds
425
+ * idiom. A clean name table behind the text. */
426
+ static void paint_title(void) {
427
+ uint8_t len = 0, col;
428
+ const char *p = GAME_TITLE;
429
+ while (*p++) len++;
430
+ col = (uint8_t)((32 - len) / 2);
431
+ clear_field();
432
+ draw_text(col, 6, GAME_TITLE);
433
+ draw_text(7, 11, "1P VS CPU - FIRE A");
434
+ draw_text(7, 13, "2P VERSUS - FIRE B");
435
+ draw_text(11, 16, "FIRST TO 5");
436
+ draw_text(11, 19, "BEST 0000"); /* the space blanks the cell between */
437
+ draw_num4(16, 19, best_streak);
438
+ }
439
+
440
+ static void paint_over(void) {
441
+ clear_field();
442
+ if (score_p1 >= WIN_SCORE)
443
+ draw_text(11, 7, two_player ? "P1 WINS" : "YOU WIN");
444
+ else
445
+ draw_text(11, 7, two_player ? "P2 WINS" : "CPU WINS");
446
+ draw_text(13, 10, "P1"); put_tile(16, 10, (uint8_t)(T_0 + score_p1));
447
+ put_tile(17, 10, T_DASH);
448
+ put_tile(18, 10, (uint8_t)(T_0 + score_p2)); draw_text(20, 10, two_player ? "P2" : "CPU");
449
+ if (new_record) draw_text(11, 13, "NEW RECORD");
450
+ draw_text(11, 14, "BEST"); draw_num4(16, 14, best_streak);
451
+ draw_text(8, 17, "FIRE FOR TITLE");
452
+ prev_t1 = prev_t2 = 1; /* swallow a fire still held from play */
98
453
  }
99
454
 
100
- static void reset_match(void) {
101
- p1y = 84; p2y = 84;
102
- score_p1 = 0; score_p2 = 0;
455
+ /* ── GAME LOGIC (clay — reshape freely) — start a match ── */
456
+ static void start_match(uint8_t versus) {
457
+ two_player = versus;
458
+ p1y = (COURT_TOP + COURT_BOT) / 2 - PADDLE_H / 2;
459
+ p2y = p1y;
460
+ score_p1 = 0;
461
+ score_p2 = 0;
462
+ new_record = 0;
103
463
  serve_ball(0);
464
+ paint_court();
465
+ sfx_start();
466
+ state = ST_PLAY;
104
467
  }
105
468
 
106
- void main(void) {
107
- uint8_t i, slot, p1, p2;
469
+ /* ── GAME LOGIC (clay — reshape freely) — match over: result + record.
470
+ * Persistence choice: for a VERSUS sports game a raw hi-score is meaningless
471
+ * (every match ends 5-x), so we track the longest 1P win streak against the
472
+ * CPU — the stat a returning player actually chases. 2P matches never touch it
473
+ * (humans beating each other isn't a record). On THIS core the record is
474
+ * session-only RAM (no SAVE_RAM — see the file header); the Genesis/NES/SMS
475
+ * builds of this game persist the identical streak to cartridge SRAM. ── */
476
+ static void end_match(void) {
477
+ if (score_p1 >= WIN_SCORE && !two_player) {
478
+ ++streak;
479
+ if (streak > best_streak) { best_streak = streak; new_record = 1; }
480
+ } else if (!two_player) {
481
+ streak = 0; /* the streak dies with the loss */
482
+ }
483
+ sfx_over();
484
+ paint_over();
485
+ state = ST_OVER;
486
+ }
487
+
488
+ /* ── GAME LOGIC (clay — reshape freely) — one point scored ── */
489
+ static void score_point(uint8_t for_p1) {
490
+ if (for_p1) ++score_p1; else ++score_p2;
491
+ sfx_point();
492
+ draw_scores();
493
+ if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
494
+ else serve_ball(for_p1); /* winner of the point receives */
495
+ }
496
+
497
+ /* ── GAME LOGIC (clay — reshape freely) — paddle hit: deflect by where the
498
+ * ball struck. Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU
499
+ * moves at 1 (half the ball's horizontal speed), so a steep edge return slips
500
+ * past it: that is exactly how a human beats the CPU. A ±1 random "spin" on
501
+ * every return keeps rallies from repeating (see the PRNG note above). */
502
+ static void deflect(int16_t paddle_y) {
503
+ int16_t rel = (by + BALL_H / 2) - (paddle_y + PADDLE_H / 2);
504
+ bdy = (int8_t)(rel >> 3);
505
+ bdy += (int8_t)((next_rand() & 2) - 1); /* spin: -1 or +1 */
506
+ if (bdy > 2) bdy = 2;
507
+ if (bdy < -2) bdy = -2;
508
+ if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
509
+ sfx_hit();
510
+ }
511
+
512
+ /* ── GAME LOGIC (clay — reshape freely) — per-player paddle input ───────────
513
+ * P0 reads JOYSTICK PORT 1 (keyboard cursors fall back); P1 reads PORT 2. */
514
+ static void update_player(uint8_t p) {
515
+ uint8_t dir;
516
+ int16_t *py = (p == 0) ? &p1y : &p2y;
517
+ if (p == 0) {
518
+ dir = msx_read_joystick(1);
519
+ if (dir == STICK_CENTER) dir = msx_read_joystick(0);
520
+ } else {
521
+ dir = msx_read_joystick(2);
522
+ }
523
+ if ((dir == STICK_UP || dir == STICK_UL || dir == STICK_UR)
524
+ && *py > COURT_TOP) *py -= P_SPEED;
525
+ if ((dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR)
526
+ && *py < COURT_BOT - PADDLE_H) *py += P_SPEED;
527
+ }
528
+
529
+ /* ── GAME LOGIC (clay — reshape freely) — CPU paddle: chase the ball centre at
530
+ * CPU_SPEED, but ONLY while the ball is heading toward it (bdx > 0), and with a
531
+ * generous DEAD ZONE. Beatable by design, three ways stacked:
532
+ * - it does not start tracking back until the ball turns toward it, so a
533
+ * steep return aimed at the far rail clears the paddle before it reacts;
534
+ * - CPU_SPEED (1) is half the ball's horizontal speed (2), so on a steep
535
+ * return it simply can't cover the vertical distance in time;
536
+ * - the ±CPU_DEAD dead zone leaves a gap at the paddle edges.
537
+ * Raise CPU_SPEED toward P_SPEED, shrink CPU_DEAD, or drop the bdx>0 gate to
538
+ * make the CPU tougher. ── */
539
+ #define CPU_DEAD 6
540
+ static void update_cpu(void) {
108
541
  int16_t target;
542
+ if (bdx <= 0) return; /* ball moving away — CPU rests */
543
+ target = by + BALL_H / 2 - PADDLE_H / 2;
544
+ if (p2y + CPU_DEAD < target && p2y < COURT_BOT - PADDLE_H) p2y += CPU_SPEED;
545
+ else if (p2y > target + CPU_DEAD && p2y > COURT_TOP) p2y -= CPU_SPEED;
546
+ }
109
547
 
548
+ void main(void) {
549
+ uint8_t t1, t2;
550
+
551
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
552
+ * Init order: set the video mode FIRST (INIGRP also clears VRAM — any
553
+ * upload done before it is wiped), then tiles, then sprites. The crt0's
554
+ * INIT contract means main() must NEVER return — the BIOS has nothing
555
+ * sane to fall back to — hence the for(;;) below. */
110
556
  msx_set_screen2();
111
557
  msx_clear_sprites();
112
558
  load_tiles();
113
- draw_court();
114
- msx_vram_write((uint16_t)(VRAM_SPRPAT + 0), SPR_BLOCK, 8);
559
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_PADDLE * 8), spr_block, 8);
560
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_BALL * 8), spr_ball, 8);
115
561
 
116
- blip = 0;
117
- reset_match();
562
+ msx_music(0); /* the lib's demo loop also owns channel C —
563
+ * hand the channel to OUR tune table instead */
564
+ best_streak = 0; /* session record (no SAVE_RAM on this core) */
565
+ streak = 0;
566
+ rng = 0xACE1;
567
+ music_step = music_timer = 0;
568
+ sfx_a_t = sfx_b_t = 0;
569
+ prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
570
+ two_player = 0;
571
+ bx = 124; by = (COURT_TOP + COURT_BOT) / 2;
572
+ state = ST_TITLE;
573
+ paint_title();
118
574
 
119
575
  for (;;) {
120
576
  vsync();
577
+ music_tick();
578
+ sfx_tick();
121
579
 
122
- /* push sprites: left paddle (3 cells), right paddle (3), ball */
123
- slot = 0;
124
- for (i = 0; i < PADDLE_H / 8; i++)
125
- msx_set_sprite(slot++, PADDLE_X1, (uint8_t)(p1y + i * 8), 0, COL_SPR);
126
- for (i = 0; i < PADDLE_H / 8; i++)
127
- msx_set_sprite(slot++, PADDLE_X2, (uint8_t)(p2y + i * 8), 0, COL_SPR);
128
- msx_set_sprite(slot++, (uint8_t)bx, (uint8_t)by, 0, COL_SPR);
129
-
130
- p1 = msx_read_joystick(1);
131
- p2 = msx_read_joystick(2);
132
-
133
- if ((p1 == STICK_UP || p1 == STICK_UL || p1 == STICK_UR)
134
- && p1y > COURT_TOP) p1y -= 3;
135
- if ((p1 == STICK_DOWN || p1 == STICK_DL || p1 == STICK_DR)
136
- && p1y < COURT_BOT - PADDLE_H) p1y += 3;
137
-
138
- if (p2 != STICK_CENTER) {
139
- if ((p2 == STICK_UP || p2 == STICK_UL || p2 == STICK_UR)
140
- && p2y > COURT_TOP) p2y -= 3;
141
- if ((p2 == STICK_DOWN || p2 == STICK_DL || p2 == STICK_DR)
142
- && p2y < COURT_BOT - PADDLE_H) p2y += 3;
143
- } else {
144
- target = (int16_t)(by - PADDLE_H / 2);
145
- if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 2;
146
- else if (p2y > target && p2y > COURT_TOP) p2y -= 2;
580
+ if (state == ST_TITLE) {
581
+ /* ── GAME LOGIC (clay) — title: trig A = 1P vs CPU; trig B = 2P. */
582
+ t1 = (uint8_t)(gttrig(1) || gttrig(0));
583
+ t2 = (uint8_t)(gttrig(3) || gttrig(2));
584
+ if (t2 && !prev_t2) start_match(1);
585
+ else if (t1 && !prev_t1) start_match(0);
586
+ prev_t1 = t1; prev_t2 = t2;
587
+ push_sprites();
588
+ continue;
147
589
  }
148
590
 
149
- if (serve_timer > 0) {
150
- serve_timer--;
151
- } else {
152
- bx = (int16_t)(bx + bdx);
153
- by = (int16_t)(by + bdy);
154
- if (by < COURT_TOP) { by = COURT_TOP; bdy = (int8_t)(-bdy); msx_psg_tone(1, 0x300, 8); blip = 3; }
155
- if (by + BALL_SIZE > COURT_BOT) { by = (int16_t)(COURT_BOT - BALL_SIZE); bdy = (int8_t)(-bdy); msx_psg_tone(1, 0x300, 8); blip = 3; }
156
-
157
- if (bdx < 0
158
- && bx <= PADDLE_X1 + 8
159
- && bx + BALL_SIZE >= PADDLE_X1
160
- && by + BALL_SIZE > p1y
161
- && by < p1y + PADDLE_H) {
162
- bdx = (int8_t)(-bdx);
163
- bx = PADDLE_X1 + 8;
164
- msx_psg_tone(0, 0x200, 10); blip = 4;
165
- }
166
- if (bdx > 0
167
- && bx + BALL_SIZE >= PADDLE_X2
168
- && bx <= PADDLE_X2 + 8
169
- && by + BALL_SIZE > p2y
170
- && by < p2y + PADDLE_H) {
171
- bdx = (int8_t)(-bdx);
172
- bx = (int16_t)(PADDLE_X2 - BALL_SIZE);
173
- msx_psg_tone(0, 0x200, 10); blip = 4;
591
+ if (state == ST_OVER) {
592
+ /* Freeze the final frame; any fire button returns to the title. */
593
+ t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
594
+ if (t1 && !prev_t1) {
595
+ state = ST_TITLE;
596
+ msx_clear_sprites();
597
+ two_player = 0;
598
+ paint_title();
174
599
  }
600
+ prev_t1 = t1; prev_t2 = t1;
601
+ push_sprites();
602
+ continue;
603
+ }
175
604
 
176
- if (bx < 4) { if (score_p2 < 9) score_p2++; msx_psg_tone(0, 0x500, 14); blip = 8; serve_ball(0); }
177
- if (bx > 252) { if (score_p1 < 9) score_p1++; msx_psg_tone(0, 0x180, 14); blip = 8; serve_ball(1); }
605
+ /* ── ST_PLAY GAME LOGIC (clay) ────────────────────────────────────
606
+ * Both players (or P1 + CPU) update EVERY frame a simultaneous
607
+ * versus match, not alternating turns. */
608
+ next_rand(); /* tick the noise source every play frame */
609
+
610
+ update_player(0);
611
+ if (two_player) update_player(1);
612
+ else update_cpu();
613
+
614
+ /* Ball update (frozen during the post-point serve pause). */
615
+ if (serve_timer > 0) {
616
+ --serve_timer;
617
+ push_sprites();
618
+ continue;
619
+ }
620
+ bx = (int16_t)(bx + bdx);
621
+ by = (int16_t)(by + bdy);
622
+
623
+ /* Rail bounce. */
624
+ if (by < COURT_TOP) { by = COURT_TOP; bdy = (int8_t)(-bdy); sfx_rail(); }
625
+ if (by + BALL_H > COURT_BOT) { by = COURT_BOT - BALL_H; bdy = (int8_t)(-bdy); sfx_rail(); }
626
+
627
+ /* Paddle collisions (direction-gated so the ball can't double-hit). */
628
+ if (bdx < 0
629
+ && bx <= PADDLE_X1 + 8 && bx + BALL_W >= PADDLE_X1
630
+ && by + BALL_H > p1y && by < p1y + PADDLE_H) {
631
+ bdx = (int8_t)(-bdx);
632
+ bx = PADDLE_X1 + 8;
633
+ deflect(p1y);
178
634
  }
635
+ if (bdx > 0
636
+ && bx + BALL_W >= PADDLE_X2 && bx <= PADDLE_X2 + 8
637
+ && by + BALL_H > p2y && by < p2y + PADDLE_H) {
638
+ bdx = (int8_t)(-bdx);
639
+ bx = (int16_t)(PADDLE_X2 - BALL_W);
640
+ deflect(p2y);
641
+ }
642
+
643
+ /* Off either side → point. */
644
+ if (bx < 4) score_point(0); /* past P1 → right side (P2/CPU) scores */
645
+ if (bx > 252) score_point(1); /* past P2/CPU → P1 scores */
179
646
 
180
- if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
647
+ push_sprites();
181
648
  }
182
649
  }