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,250 +1,815 @@
1
- /* ── puzzle/main.c — MSX match-3 falling-block scaffold (screen 2) ───
1
+ /* ── puzzle/main.c — MSX falling-trio match-3 puzzle (complete example game) ──
2
2
  *
3
- * Mirrors the SMS/GB/etc puzzle scaffolds, translated to the MSX VDP via
4
- * the romdev helper lib (msx_hw.h + msx_vdp.c).
3
+ * STOKE STACK a COMPLETE, working game: title screen, 1P MARATHON mode
4
+ * (levels speed the fall as you clear) and 2P SIMULTANEOUS VERSUS mode —
5
+ * two 6x12 wells side by side, P1 on JOYSTICK PORT 1, P2 on JOYSTICK PORT 2,
6
+ * both falling at once, where every cascade CHAIN you score stokes the heat
7
+ * under your rival: a garbage row rises from the bottom of their well.
8
+ * Score + session hi-score, music + SFX on the AY-3-8910 PSG, and the MSX's
9
+ * signature SCREEN-2 PER-ROW COLOR: the two wells, the HUD band, and a
10
+ * one-tile vertical "ember" gradient seam come ENTIRELY from the three
11
+ * independent color thirds, costing zero extra tiles.
5
12
  *
6
- * A 6-wide x 12-tall well drawn entirely with the BG tilemap: distinct
7
- * R/G/B cell tiles, a grey border frame, and a dim field interior so the
8
- * playfield is visible even when empty (the screen is never blank). A 1x3
9
- * active piece falls; clears happen on a horizontal triple of one colour.
13
+ * The game: a falling-trio match-3. A vertical trio of gems drops into a well;
14
+ * LEFT/RIGHT move it, trigger A cycles its three colours, DOWN soft-drops,
15
+ * trigger B hard-drops. When it lands, any straight run of 3+ same-coloured
16
+ * gems (horizontal, vertical, or diagonal) clears; survivors fall and cascades
17
+ * chain for multiplied score. In versus, the stack that reaches the rim loses.
10
18
  *
11
- * Controls (joystick PORT 1 + triggers):
12
- * LEFT/RIGHT shift the piece (edge-detected)
13
- * trigger A rotate the colour order of the 1x3 piece
14
- * DOWN soft-drop (fast fall)
15
- * trigger B hard-drop + lock
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) — match rules, garbage, scoring, tuning, art: reshape
24
+ * freely.
16
25
  *
17
- * Cartridge rule: INIT must never return — main() ends in for(;;).
26
+ * What depends on what:
27
+ * msx_hw.h / msx_vdp.c — VDP + PSG + joystick helpers (direct Z80 ports;
28
+ * the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
29
+ * read msx_vdp.c before adding your own PSG pokes).
30
+ * msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
31
+ * INIT must NEVER return, so main() ends in for(;;).
32
+ *
33
+ * Frame budget — and a TEACHING POINT vs the Genesis version of this game
34
+ * (examples/genesis/templates/puzzle.c): the Genesis mirrors each well in RAM
35
+ * and repaints it as ONE queued DMA rect in vblank. The MSX has no DMA: every
36
+ * dirty cell is a per-byte VRAM port write (msx_vram_write). But screen-2 VRAM
37
+ * writes are CHEAP at C speed (~29 Z80 cycles between VDP accesses, and SDCC
38
+ * loops are slower than that — see TROUBLESHOOTING), and we only ever repaint
39
+ * the cells that CHANGED (the active trio + a board redraw after a lock), so a
40
+ * worst-case double cascade still lands inside one frame. Same genre, two
41
+ * bandwidth worlds — fork accordingly.
42
+ *
43
+ * Controls: JOYSTICK PORT 1 (or keyboard cursors) plays well 0 — LEFT/RIGHT
44
+ * move, trigger A (or SPACE) cycles colours, DOWN soft-drops, trigger B
45
+ * hard-drops. In 2P versus, JOYSTICK PORT 2 plays well 1 the same way. On
46
+ * the title screen trigger A starts 1P marathon; trigger B starts 2P versus.
47
+ *
48
+ * Hi-score honesty: the bundled bluemsx core build exposes NO battery save
49
+ * path (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so the
50
+ * hi-score lives in plain RAM: it survives title↔game cycles but NOT a
51
+ * power cycle / hardReset. Never fake persistence — if you need real saves,
52
+ * that's a future core round (ASCII8-SRAM mapper carts exist; the core just
53
+ * doesn't surface their RAM yet).
18
54
  */
19
55
  #include "msx_hw.h"
20
56
 
21
- /* ── 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 "STOKE STACK"
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.) */
22
69
  __sfr __at 0x99 VDPSTATUS;
23
70
  static void vsync(void) {
24
- (void)VDPSTATUS;
71
+ (void)VDPSTATUS; /* throw away a possibly-stale flag */
25
72
  while (!(VDPSTATUS & 0x80)) {
26
73
  }
27
74
  }
28
- /* triggers use the BIOS GTTRIG wrapper (gttrig) provided by msx_hw.h. */
29
-
30
- #define COLS 6
31
- #define ROWS 12
32
-
33
- #define T_BLANK 0
34
- #define T_R 1
35
- #define T_G 2
36
- #define T_B 3
37
- #define T_WALL 4 /* well border */
38
- #define T_FIELD 5 /* dim well interior */
39
-
40
- /* 8x8 tile patterns: solid fills (the colour comes from the colour table) */
41
- static const uint8_t TILE_SOLID[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
42
- static const uint8_t TILE_BLANK[8] = {0,0,0,0,0,0,0,0};
43
- static const uint8_t TILE_CELL[8] = {0x7E,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0x7E};
44
-
45
- /* colour bytes (hi nibble fg, lo nibble bg). TMS9918 palette:
46
- * 9=red, 12=green(light), 4=blue, 14=grey, 1=black. */
47
- #define COL_R 0x91
48
- #define COL_G 0xC1
49
- #define COL_B 0x41
50
- #define COL_WALL 0xE1
51
- #define COL_FIELD 0x41 /* same blue as B but used for the empty field */
52
-
53
- static uint8_t grid[ROWS][COLS];
54
- static uint8_t piece[3];
55
- static int8_t piece_x;
56
- static int8_t piece_y;
57
- static uint8_t fall_timer;
58
- static uint16_t score;
75
+
76
+ /* ── GAME LOGIC (clay — reshape freely) — board geometry ─────────────────────
77
+ * Two 6x12 wells on the 32x24 screen-2 name table. Cells are ONE 8x8 tile
78
+ * each (the MSX screen is 256x192 — a 6-wide well is 48 px, both wells plus a
79
+ * "VS" gap fit comfortably). Row 0 is the HUD band; the wells live in the air
80
+ * thirds and rest on the ground third. */
81
+ #define GRID_W 6
82
+ #define GRID_H 12
83
+ #define WELL_TOP 6 /* name-table row of the well's TOP interior cell */
84
+ #define WELL_1P_LX 13 /* 1P: single centered well, interior cols 13-18 */
85
+ #define WELL_VS_LX0 4 /* 2P: P1 well interior cols 4-9 ... */
86
+ #define WELL_VS_LX1 22 /* P2 well interior cols 22-27 (split board) */
87
+
88
+ #define EMPTY 0 /* cell colours 1..3 = ruby / emerald / sapphire */
89
+
90
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
91
+ * Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
92
+ * board tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits draw in
93
+ * the tile's FOREGROUND color, clear bits in its BACKGROUND color (both come
94
+ * from the screen-2 color table — see the per-row-color idiom below). */
95
+ #define T_SPACE 0
96
+ #define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
97
+ #define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
98
+ #define T_DASH 37
99
+ #define T_FIELD 38 /* empty well interior cell (recessed, faint speck)*/
100
+ #define T_FRAME 39 /* well border */
101
+ #define T_GEM 40 /* a locked gem cell (its COLOR picks the colour) */
102
+ #define T_EMBER 41 /* the per-8x1-row gradient strip (see below) */
103
+ #define NUM_TILES 42
104
+
105
+ static const uint8_t font[NUM_TILES][8] = {
106
+ /* SPACE */ {0,0,0,0,0,0,0,0},
107
+ /* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
108
+ /* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
109
+ /* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
110
+ /* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
111
+ /* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
112
+ /* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
113
+ /* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
114
+ /* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
115
+ /* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
116
+ /* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
117
+ /* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
118
+ /* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
119
+ /* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
120
+ /* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
121
+ /* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
122
+ /* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
123
+ /* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
124
+ /* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
125
+ /* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
126
+ /* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
127
+ /* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
128
+ /* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
129
+ /* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
130
+ /* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
131
+ /* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
132
+ /* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
133
+ /* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
134
+ /* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
135
+ /* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
136
+ /* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
137
+ /* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
138
+ /* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
139
+ /* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
140
+ /* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
141
+ /* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
142
+ /* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
143
+ /* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
144
+ /* 38 FIELD (recessed cell + faint speck) */
145
+ {0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00},
146
+ /* 39 FRAME (solid border) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
147
+ /* 40 GEM (rounded gem; its COLOR byte picks ruby/emerald/sapphire) */
148
+ {0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C},
149
+ /* 41 EMBER (solid fg — its COLOR bytes paint the gradient) */
150
+ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
151
+ };
152
+
153
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
154
+ * SCREEN-2 PER-ROW COLOR — the MSX's signature background trick, AND the
155
+ * reason a single GEM tile can show THREE colours.
156
+ *
157
+ * Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
158
+ *
159
+ * 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
160
+ * (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
161
+ * pattern table slice and its OWN 2KB color table slice:
162
+ * patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
163
+ * The SAME tile index can look completely different in each third. We
164
+ * exploit exactly that to draw the THREE gem colours from ONE T_GEM tile:
165
+ * the gem pattern is uploaded identically to all three thirds, but each
166
+ * third's color byte for T_GEM is a DIFFERENT foreground (ruby / emerald /
167
+ * sapphire). A gem's colour is therefore chosen by WHICH NAME-TABLE ROW
168
+ * it sits in — so the board is laid out three rows per colour band (see
169
+ * gem_tile_for / well row→third mapping below). One piece of art, three
170
+ * colours, zero extra tiles — the puzzle-genre twin of the shmup's
171
+ * depth-banded starfield.
172
+ *
173
+ * 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
174
+ * row — each packing (foreground<<4)|background from the fixed TMS9918
175
+ * palette. So one tile can carry an 8-color vertical gradient
176
+ * (T_EMBER's whole "forge glow" seam is a single tile, colors only).
177
+ *
178
+ * Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
179
+ * R4=0x03 — the "thirds" configuration), and pattern + color uploads to
180
+ * EVERY third a tile is used in. Tile N's slot is pattern[N*8] / color[N*8].
181
+ *
182
+ * TMS9918 fixed palette used here: 1 black, 4 dark blue, 5 light blue,
183
+ * 6 dark red, 8 cyan, 9 light red, 11 light yellow, 12 green, 14 gray,
184
+ * 15 white (high nibble = fg, low nibble = bg of each row byte). */
185
+ static const uint8_t col_text[3] = { 0xF4, 0xF1, 0xF1 }; /* HUD white-on-blue; title/play white-on-black */
186
+ static const uint8_t col_field[3] = { 0x41, 0x41, 0x41 }; /* recessed cell: dark-blue speck on black */
187
+ static const uint8_t col_frame[3] = { 0xE1, 0xE1, 0xE1 }; /* well border: gray on black, every third */
188
+ /* THE GEM-COLOUR-PER-THIRD trick: T_GEM's foreground in each third is a
189
+ * different gem colour. Board rows are bucketed into colour bands by third so
190
+ * a locked gem renders in its colour with no per-cell palette work:
191
+ * third 0 (top, rows 0-7) → ruby (fg 9 light red)
192
+ * third 1 (middle, rows 8-15)→ emerald (fg 12 green)
193
+ * third 2 (bottom, rows 16-23)→ sapphire(fg 5 light blue)
194
+ * ...so the in-well colour a gem SHOWS depends on its name-table row third.
195
+ * (gem_tile_for() picks the row a colour-c gem must occupy — see below.) */
196
+ static const uint8_t col_gem[3] = { 0x91, 0xC1, 0x51 }; /* ruby / emerald / sapphire, each on black */
197
+ /* T_EMBER: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row forge
198
+ * glow (black → dark red → light red → yellow → white and back down). The
199
+ * pattern is solid 0xFF so only the fg nibbles show. Drawn as the seam row
200
+ * directly under each well. */
201
+ static const uint8_t col_ember[8] = { 0x11,0x61,0x91,0xB1,0xF1,0xB1,0x91,0x61 };
202
+
203
+ static void load_tiles(void) {
204
+ uint8_t third, i;
205
+ uint16_t patbase, colbase;
206
+ for (third = 0; third < 3; third++) {
207
+ patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
208
+ colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
209
+ for (i = 0; i < NUM_TILES; i++) {
210
+ uint8_t col;
211
+ /* pattern bits are the same in every third — only COLOR varies */
212
+ msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
213
+ if (i == T_EMBER) { /* the one per-pixel-row gradient */
214
+ msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_ember, 8);
215
+ continue;
216
+ }
217
+ if (i == T_FIELD) col = col_field[third];
218
+ else if (i == T_FRAME) col = col_frame[third];
219
+ else if (i == T_GEM) col = col_gem[third]; /* colour per third */
220
+ else col = col_text[third];
221
+ msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
222
+ }
223
+ }
224
+ }
225
+
226
+ /* ── GAME LOGIC (clay — reshape freely) — name-table drawing helpers ────────
227
+ * Screen 2 VRAM writes are safe at any point in the frame at C speed: the
228
+ * TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
229
+ * and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
230
+ * are the thing that outruns the VDP — see TROUBLESHOOTING.) */
231
+ static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
232
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
233
+ }
234
+
235
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
236
+ uint8_t buf[32];
237
+ uint8_t n = 0;
238
+ while (*s && n < 32) {
239
+ char c = *s++;
240
+ if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
241
+ else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
242
+ else if (c == '-') buf[n] = T_DASH;
243
+ else buf[n] = T_SPACE;
244
+ n++;
245
+ }
246
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
247
+ }
248
+
249
+ static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
250
+ uint8_t buf[4];
251
+ buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
252
+ buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
253
+ buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
254
+ buf[3] = (uint8_t)(T_0 + v % 10);
255
+ msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
256
+ }
257
+
258
+ /* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────────
259
+ * Boards are plain static arrays. The falling trio is drawn with SPRITES (so
260
+ * it floats over the well tiles without disturbing them); locked gems are
261
+ * name-table tiles. */
262
+ static uint8_t grid[2][GRID_H][GRID_W]; /* the two wells (well 1 unused in 1P) */
263
+ static int8_t piece_x[2]; /* falling trio: column 0..5 */
264
+ static int8_t piece_y[2]; /* row of its TOP cell (<0 above rim) */
265
+ static uint8_t piece_col[2][3]; /* trio colours, top to bottom */
266
+ static uint8_t fall_t[2]; /* frames until next gravity step */
267
+ static uint16_t score[2];
268
+ static uint16_t hiscore; /* SESSION-ONLY: plain RAM. The bundled
269
+ * bluemsx build exposes no SAVE_RAM region,
270
+ * so there is nothing battery-backed to
271
+ * write — survives title↔game cycles, not a
272
+ * power cycle (honest, not faked). */
273
+ static uint8_t level; /* 1P: 1..9, speeds up the fall */
274
+ static uint16_t cleared_total; /* 1P: gems cleared, drives the level */
275
+ static uint8_t well_lx[2]; /* left interior name-table col per well */
276
+ static uint8_t two_player; /* mode chosen on the title screen */
59
277
  static uint16_t rng;
60
- static uint8_t blip;
61
278
 
62
- static uint16_t xorshift(void) {
279
+ #define ST_TITLE 0
280
+ #define ST_PLAY 1
281
+ #define ST_OVER 2
282
+ static uint8_t state;
283
+ static uint8_t over_loser; /* 2P: which well topped out (P that LOST) */
284
+ static uint8_t prev_t1, prev_t2; /* title/over trigger edge detection */
285
+ /* per-player edge memory for in-play input (move/rotate/drop) */
286
+ static uint8_t prev_dir[2], prev_a[2], prev_b[2];
287
+
288
+ #define FALL_VS 24 /* 2P: fixed gravity (frames per row) */
289
+ #define GARBAGE_CAP 4 /* max garbage rows per attack */
290
+
291
+ /* xorshift16 PRNG — a few dozen cycles, no tables. */
292
+ static uint8_t next_rand(void) {
63
293
  rng ^= (uint16_t)(rng << 7);
64
294
  rng ^= (uint16_t)(rng >> 9);
65
295
  rng ^= (uint16_t)(rng << 8);
66
- return rng;
296
+ return (uint8_t)(rng & 0xFF);
297
+ }
298
+
299
+ /* ── GAME LOGIC (clay — reshape freely) — music + SFX on the AY-3-8910 ──────
300
+ * Channel plan: A = move/rotate/clear blips, B = lock/garbage noise, C =
301
+ * music. The PSG has 3 tone channels + ONE shared noise generator, mixed
302
+ * per-channel in reg 7. All register traffic goes through msx_psg_tone/noise/
303
+ * off — they wrap the PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT
304
+ * ISR clobbers the PSG address latch every frame (the bug that once silenced
305
+ * every MSX scaffold — see msx_vdp.c).
306
+ *
307
+ * The tune: one period entry per half-beat, 0 = rest. AY period =
308
+ * 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame; a
309
+ * note advances every 8 frames. The lib's built-in demo loop (msx_music_tick)
310
+ * also uses channel C, so we switch it OFF in main() and run THIS table
311
+ * instead — edit this table to rescore. */
312
+ static const uint16_t tune[32] = {
313
+ 339, 0, 285, 339, 427, 0, 339, 285, /* E4 G4 E4 C4 E4 G4 (steady groove) */
314
+ 254, 0, 285, 339, 285, 0, 0, 0, /* A4 G4 E4 G4 rest */
315
+ 380, 0, 339, 285, 254, 0, 285, 339, /* D4 E4 G4 A4 G4 E4 */
316
+ 427, 0, 339, 285, 339, 0, 0, 0, /* C4 E4 G4 E4 rest */
317
+ };
318
+ static uint8_t music_step, music_timer;
319
+ static uint8_t sfx_a_t, sfx_b_t; /* frames left on the A/B SFX channels */
320
+
321
+ static void music_tick(void) {
322
+ if (music_timer == 0) {
323
+ uint16_t p = tune[music_step & 31];
324
+ if (p) msx_psg_tone(2, p, 9);
325
+ else msx_psg_off(2);
326
+ music_step++;
327
+ }
328
+ music_timer++;
329
+ if (music_timer >= 8) music_timer = 0;
67
330
  }
68
- static uint8_t rand_color(void) { return (uint8_t)(1 + (xorshift() % 3)); }
69
331
 
70
- static uint8_t tile_for(uint8_t c) {
71
- if (c == 1) return T_R;
72
- if (c == 2) return T_G;
73
- if (c == 3) return T_B;
74
- return T_FIELD; /* empty cell shows the dim field, not the backdrop */
332
+ static void sfx_tick(void) {
333
+ if (sfx_a_t) { sfx_a_t--; if (!sfx_a_t) msx_psg_off(0); }
334
+ if (sfx_b_t) { sfx_b_t--; if (!sfx_b_t) msx_psg_noise(1, 0, 0); }
75
335
  }
76
336
 
77
- static void load_tiles(void) {
78
- uint8_t third;
79
- uint16_t pat, col;
80
- for (third = 0; third < 3; third++) {
81
- pat = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
82
- col = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
83
- msx_vram_write((uint16_t)(pat + T_BLANK * 8), TILE_BLANK, 8);
84
- msx_vram_write((uint16_t)(pat + T_R * 8), TILE_CELL, 8);
85
- msx_vram_write((uint16_t)(pat + T_G * 8), TILE_CELL, 8);
86
- msx_vram_write((uint16_t)(pat + T_B * 8), TILE_CELL, 8);
87
- msx_vram_write((uint16_t)(pat + T_WALL * 8), TILE_SOLID, 8);
88
- msx_vram_write((uint16_t)(pat + T_FIELD * 8), TILE_SOLID, 8);
89
- msx_fill_vram((uint16_t)(col + T_BLANK * 8), 8, 0x11);
90
- msx_fill_vram((uint16_t)(col + T_R * 8), 8, COL_R);
91
- msx_fill_vram((uint16_t)(col + T_G * 8), 8, COL_G);
92
- msx_fill_vram((uint16_t)(col + T_B * 8), 8, COL_B);
93
- msx_fill_vram((uint16_t)(col + T_WALL * 8), 8, COL_WALL);
94
- msx_fill_vram((uint16_t)(col + T_FIELD * 8), 8, COL_FIELD);
95
- }
96
- }
97
-
98
- static void set_cell_tile(uint8_t row, uint8_t col, uint8_t tile) {
99
- msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
337
+ static void sfx_move(void) { msx_psg_tone(0, 0x300, 8); sfx_a_t = 2; }
338
+ static void sfx_rotate(void){ msx_psg_tone(0, 0x200, 9); sfx_a_t = 3; }
339
+ static void sfx_clear(uint8_t chain) {
340
+ /* pitch rises with chain depth (smaller period = higher note) */
341
+ uint16_t p = (uint16_t)(0x180 - ((uint16_t)chain << 5));
342
+ msx_psg_tone(0, p, 12); sfx_a_t = 6;
343
+ }
344
+ static void sfx_lock(void) { msx_psg_tone(1, 0x040, 10); sfx_b_t = 3; }
345
+ static void sfx_garbage(void){ msx_psg_noise(1, 16, 13); sfx_b_t = 8; }
346
+ static void sfx_over(void) { msx_psg_noise(1, 28, 14); sfx_b_t = 22; }
347
+
348
+ /* ── GAME LOGIC (clay — reshape freely) well row→third gem colour ──────────
349
+ * The screen-2 thirds idiom means a gem's COLOUR is decided by which screen
350
+ * third (name-table row band) its cell lands in. The well spans name-table
351
+ * rows WELL_TOP..WELL_TOP+11 (6..17), which straddles two thirds:
352
+ * rows 6,7 → third 0 → ruby
353
+ * rows 8..15 → third 1 → emerald
354
+ * rows 16,17 → third 2 → sapphire
355
+ * That is a fixed visual banding of the SHARED T_GEM tile, NOT the logical
356
+ * gem colour (the logical colour lives in grid[]). For a forked game that
357
+ * wants logical colour == shown colour at every cell, give each colour its own
358
+ * tile index and upload three patterns instead — costs 2 extra tiles. Here we
359
+ * keep the one-tile trick and accept the painterly banding; matches still run
360
+ * on the LOGICAL grid[] colours, so play is unaffected. */
361
+ static uint8_t well_row(uint8_t r) { return (uint8_t)(WELL_TOP + r); }
362
+
363
+ /* draw one well interior cell (logical grid[p][r][c]) into the name table */
364
+ static void draw_cell(uint8_t p, uint8_t r, uint8_t c) {
365
+ uint8_t tile = grid[p][r][c] ? T_GEM : T_FIELD;
366
+ put_tile((uint8_t)(well_lx[p] + c), well_row(r), tile);
367
+ }
368
+
369
+ /* repaint a full well interior from grid[] (used after a lock/cascade) */
370
+ static void draw_well(uint8_t p) {
371
+ uint8_t r, c;
372
+ for (r = 0; r < GRID_H; r++)
373
+ for (c = 0; c < GRID_W; c++) draw_cell(p, r, c);
374
+ }
375
+
376
+ /* paint a well's gray frame (top/bottom/sides) + the ember seam under it */
377
+ static void paint_frame(uint8_t p) {
378
+ uint8_t r, lx = well_lx[p];
379
+ uint8_t top = (uint8_t)(WELL_TOP - 1), bot = (uint8_t)(WELL_TOP + GRID_H);
380
+ uint8_t c;
381
+ for (c = 0; c < GRID_W + 2; c++) {
382
+ put_tile((uint8_t)(lx - 1 + c), top, T_FRAME);
383
+ put_tile((uint8_t)(lx - 1 + c), bot, T_FRAME);
384
+ }
385
+ for (r = 0; r < GRID_H; r++) {
386
+ put_tile((uint8_t)(lx - 1), well_row(r), T_FRAME);
387
+ put_tile((uint8_t)(lx + GRID_W), well_row(r), T_FRAME);
388
+ }
389
+ /* ember seam: the gradient tile row directly beneath the well frame */
390
+ for (c = 0; c < GRID_W + 2; c++)
391
+ put_tile((uint8_t)(lx - 1 + c), (uint8_t)(bot + 1), T_EMBER);
392
+ }
393
+
394
+ /* ── GAME LOGIC (clay — reshape freely) — sprites: the falling trio ──────────
395
+ * 8x8 one-color hardware sprites — 3 per player (the three gems of the active
396
+ * trio). Plane layout: 0-2 = P1 trio, 3-5 = P2 trio. Locked gems are tiles,
397
+ * not sprites, so the well never needs more than 6 sprite planes. */
398
+ static const uint8_t spr_gem[8] = {0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C};
399
+ #define PAT_GEM 0
400
+ /* sprite colour per logical gem colour 1..3 (matches the tile banding intent) */
401
+ static const uint8_t spr_col[4] = { 15, 9, 12, 5 }; /* [0] unused; ruby/emerald/sapphire */
402
+
403
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
404
+ * Sprite limits + the Y=208 terminator:
405
+ * - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
406
+ * attribute table — every higher-numbered plane vanishes, not just that
407
+ * one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
408
+ * END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
409
+ * PARK_Y (192 = first line below the display) — never at 0xD0.
410
+ * (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
411
+ * is "just offscreen" — code that leans on that breaks on MSX1.)
412
+ * - Per scanline the TMS9918 draws only 4 sprites (V9938: 8). The two trios
413
+ * never share a scanline (the wells are side by side), and one trio is 3
414
+ * vertically-stacked gems = 3 sprites on different rows, so a row pileup
415
+ * can't exceed 1-2 here. */
416
+ #define PARK_Y 192
417
+
418
+ /* cell (col,row in a well) → screen pixel position of its sprite */
419
+ static uint8_t cell_px(uint8_t p, int8_t c) { return (uint8_t)((well_lx[p] + c) * 8); }
420
+ static uint8_t cell_py(int8_t r) { return (uint8_t)((WELL_TOP + r) * 8); }
421
+
422
+ /* Push the two trios to their sprite planes. A gem above the rim (row < 0) or
423
+ * an inactive well parks offscreen at PARK_Y, NEVER 0xD0 — see the idiom. */
424
+ static void push_sprites(void) {
425
+ uint8_t p, i, plane;
426
+ for (p = 0; p < 2; p++) {
427
+ uint8_t active = (state == ST_PLAY) && (p == 0 || two_player);
428
+ for (i = 0; i < 3; i++) {
429
+ int8_t r = (int8_t)(piece_y[p] + (int8_t)i);
430
+ plane = (uint8_t)(p * 3 + i);
431
+ if (active && r >= 0 && r < GRID_H)
432
+ msx_set_sprite(plane, cell_px(p, piece_x[p]), cell_py(r),
433
+ PAT_GEM, spr_col[piece_col[p][i]]);
434
+ else
435
+ msx_set_sprite(plane, cell_px(p, piece_x[p]), PARK_Y,
436
+ PAT_GEM, spr_col[piece_col[p][i]]);
437
+ }
438
+ }
100
439
  }
101
440
 
102
- /* grid cell (col,row) -> name-table (row+1, col+13): centres the well */
103
- static void draw_cell(int8_t col, int8_t row, uint8_t cell) {
104
- if (row < 0 || row >= ROWS) return;
105
- set_cell_tile((uint8_t)(row + 1), (uint8_t)(col + 13), tile_for(cell));
441
+ /* ── GAME LOGIC (clay reshape freely) HUD ──────────────────────────────
442
+ * Row 0 = the HUD band (third 0's text colors make it a distinct strip).
443
+ * 1P: SC=score, HI=hi-score, LV=level. 2P: P1 score, HI, P2 score. */
444
+ static void draw_hud_labels(void) {
445
+ if (two_player) {
446
+ draw_text(1, 0, "P1");
447
+ draw_text(13, 0, "HI");
448
+ draw_text(25, 0, "P2");
449
+ } else {
450
+ draw_text(1, 0, "SC");
451
+ draw_text(13, 0, "HI");
452
+ draw_text(25, 0, "LV");
453
+ }
454
+ }
455
+ static void draw_scores(void) {
456
+ if (two_player) { draw_num4(4, 0, score[0]); draw_num4(28, 0, score[1]); }
457
+ else { draw_num4(4, 0, score[0]); }
106
458
  }
459
+ static void draw_hi(void) { draw_num4(16, 0, hiscore); }
460
+ static void draw_level(void) { if (!two_player) put_tile(28, 0, (uint8_t)(T_0 + level)); }
107
461
 
108
- /* grey frame around the 6x12 field + dim interior so it is always visible.
109
- * field cells are rows 1..12, cols 13..18; frame rows 0..13, cols 12..19. */
110
- static void draw_well(void) {
111
- uint8_t r, c, t;
112
- for (r = 0; r <= 13; r++) {
113
- for (c = 12; c <= 19; c++) {
114
- t = T_FIELD;
115
- if (r == 0 || r == 13 || c == 12 || c == 19) t = T_WALL;
116
- set_cell_tile(r, c, t);
462
+ /* ── GAME LOGIC (clay reshape freely) match scan ────────────────────────
463
+ * Mark every straight run of 3+ same-coloured gems in all 4 directions (a
464
+ * cell can belong to several runs — the mask de-dupes), and return how many
465
+ * cells matched. Runs on the LOGICAL grid[] colours (independent of the
466
+ * thirds tile banding). */
467
+ static uint8_t matched[GRID_H][GRID_W];
468
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
469
+
470
+ static uint8_t mark_and_count(uint8_t p) {
471
+ uint8_t r, c, d, len, k, cnt, col;
472
+ int8_t dr, dc;
473
+ int16_t sr, sc;
474
+ cnt = 0;
475
+ for (r = 0; r < GRID_H; r++)
476
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
477
+ for (r = 0; r < GRID_H; r++) {
478
+ for (c = 0; c < GRID_W; c++) {
479
+ col = grid[p][r][c];
480
+ if (col == EMPTY) continue;
481
+ for (d = 0; d < 4; d++) {
482
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
483
+ sr = (int16_t)r - dr; sc = (int16_t)c - dc;
484
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
485
+ && grid[p][sr][sc] == col) continue; /* not the run's start */
486
+ len = 1;
487
+ sr = (int16_t)r + dr; sc = (int16_t)c + dc;
488
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
489
+ && grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
490
+ if (len >= 3) {
491
+ sr = r; sc = c;
492
+ for (k = 0; k < len; k++) {
493
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
494
+ sr += dr; sc += dc;
495
+ }
496
+ }
497
+ }
117
498
  }
118
499
  }
500
+ return cnt;
119
501
  }
120
502
 
121
- static void draw_grid(void) {
122
- int8_t r, c;
123
- for (r = 0; r < ROWS; r++)
124
- for (c = 0; c < COLS; c++) draw_cell(c, r, grid[r][c]);
503
+ /* Collapse each column so survivors rest on the floor. */
504
+ static void apply_gravity(uint8_t p) {
505
+ uint8_t c;
506
+ int8_t r, w;
507
+ for (c = 0; c < GRID_W; c++) {
508
+ w = GRID_H - 1;
509
+ for (r = GRID_H - 1; r >= 0; r--) {
510
+ if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
511
+ }
512
+ for (; w >= 0; w--) grid[p][w][c] = EMPTY;
513
+ }
125
514
  }
126
515
 
127
- static void new_piece(void) {
128
- piece[0] = rand_color();
129
- piece[1] = rand_color();
130
- piece[2] = rand_color();
131
- piece_x = COLS / 2 - 1;
132
- piece_y = -3;
516
+ /* forward decls for the clear→attack→end chain */
517
+ static void game_over(uint8_t loser);
518
+ static void garbage_insert(uint8_t v, uint8_t nrows);
519
+ static void spawn_piece(uint8_t p);
520
+
521
+ /* ── GAME LOGIC (clay — reshape freely) — clear matches, drop survivors,
522
+ * chain cascades. Returns the chain depth (0 = the lock matched nothing). ── */
523
+ static uint8_t resolve_board(uint8_t p) {
524
+ uint8_t n, r, c, chain;
525
+ uint16_t amt;
526
+ chain = 0;
527
+ for (;;) {
528
+ n = mark_and_count(p);
529
+ if (n == 0) break;
530
+ ++chain;
531
+ for (r = 0; r < GRID_H; r++)
532
+ for (c = 0; c < GRID_W; c++)
533
+ if (matched[r][c]) grid[p][r][c] = EMPTY;
534
+ amt = (uint16_t)n * 10;
535
+ if (chain > 1) amt = (uint16_t)(amt * chain); /* cascades pay more */
536
+ if (score[p] < 9999) {
537
+ score[p] = (uint16_t)(score[p] + amt);
538
+ if (score[p] > 9999) score[p] = 9999;
539
+ }
540
+ sfx_clear(chain);
541
+ apply_gravity(p);
542
+ if (!two_player) {
543
+ cleared_total += n;
544
+ while (level < 9 && cleared_total >= (uint16_t)level * 10) {
545
+ ++level;
546
+ draw_level();
547
+ }
548
+ }
549
+ draw_scores();
550
+ }
551
+ return chain;
133
552
  }
134
553
 
135
- static uint8_t collides(int8_t col, int8_t row) {
136
- uint8_t i;
137
- int8_t r;
138
- if (col < 0 || col >= COLS) return 1;
554
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
555
+ * (pieces enter from above); below the floor or on a gem is not. */
556
+ static uint8_t can_place(uint8_t p, int8_t x, int8_t y) {
557
+ int8_t i, cy;
558
+ if (x < 0 || x >= GRID_W) return 0;
139
559
  for (i = 0; i < 3; i++) {
140
- r = (int8_t)(row + i);
141
- if (r >= ROWS) return 1;
142
- if (r >= 0 && grid[r][col] != 0) return 1;
560
+ cy = (int8_t)(y + i);
561
+ if (cy < 0) continue;
562
+ if (cy >= GRID_H) return 0;
563
+ if (grid[p][cy][x] != EMPTY) return 0;
143
564
  }
144
- return 0;
565
+ return 1;
566
+ }
567
+
568
+ static void spawn_piece(uint8_t p) {
569
+ piece_x[p] = GRID_W / 2;
570
+ piece_y[p] = -2;
571
+ piece_col[p][0] = (uint8_t)(1 + next_rand() % 3);
572
+ piece_col[p][1] = (uint8_t)(1 + next_rand() % 3);
573
+ piece_col[p][2] = (uint8_t)(1 + next_rand() % 3);
574
+ prev_a[p] = prev_b[p] = 1; /* swallow the drop button that just locked */
575
+ if (!can_place(p, piece_x[p], piece_y[p])) game_over(p);
145
576
  }
146
577
 
147
- static void lock_piece(void) {
148
- uint8_t i;
149
- int8_t r, c;
150
- uint8_t a, b, d;
578
+ /* ── GAME LOGIC (clay — reshape freely) — land the trio, resolve, attack,
579
+ * respawn. ── */
580
+ static void lock_piece(uint8_t p) {
581
+ int8_t i, y;
582
+ uint8_t chain;
151
583
  for (i = 0; i < 3; i++) {
152
- r = (int8_t)(piece_y + i);
153
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
584
+ y = (int8_t)(piece_y[p] + i);
585
+ if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
154
586
  }
155
- for (i = 0; i < 3; i++) {
156
- r = (int8_t)(piece_y + i);
157
- if (r < 0 || r >= ROWS) continue;
158
- for (c = 0; c <= COLS - 3; c++) {
159
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
160
- if (a != 0 && a == b && b == d) {
161
- grid[r][c] = 0; grid[r][c + 1] = 0; grid[r][c + 2] = 0;
162
- if (score < 999) score += 3;
163
- msx_psg_tone(0, 0x180, 13);
164
- blip = 8;
165
- }
166
- }
587
+ sfx_lock();
588
+ if (piece_y[p] < 0) { draw_well(p); game_over(p); return; } /* locked above rim */
589
+ chain = resolve_board(p);
590
+ draw_well(p);
591
+ if (state != ST_PLAY) return;
592
+ if (chain && two_player) {
593
+ garbage_insert((uint8_t)(p ^ 1), chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
594
+ if (state != ST_PLAY) return; /* garbage topped them out */
167
595
  }
168
- draw_grid();
596
+ spawn_piece(p);
169
597
  }
170
598
 
171
- static void draw_piece(uint8_t clear) {
172
- uint8_t i;
599
+ /* ── GAME LOGIC (clay — reshape freely) — VERSUS attack: garbage rows rise
600
+ * from the bottom of the victim's well (random gems with one gap — matchable,
601
+ * so a skilled victim digs out). If the top row is already occupied the victim
602
+ * tops out and loses. ── */
603
+ static void garbage_insert(uint8_t v, uint8_t nrows) {
604
+ uint8_t k, c, gap;
173
605
  int8_t r;
174
- uint8_t v;
175
- for (i = 0; i < 3; i++) {
176
- r = (int8_t)(piece_y + i);
177
- if (r < 0 || r >= ROWS) continue;
178
- v = clear ? grid[r][piece_x] : piece[i];
179
- draw_cell(piece_x, r, v);
606
+ sfx_garbage();
607
+ for (k = 0; k < nrows; k++) {
608
+ for (c = 0; c < GRID_W; c++)
609
+ if (grid[v][0][c] != EMPTY) { draw_well(v); game_over(v); return; }
610
+ for (r = 0; r < GRID_H - 1; r++)
611
+ for (c = 0; c < GRID_W; c++)
612
+ grid[v][r][c] = grid[v][r + 1][c];
613
+ gap = (uint8_t)(next_rand() % GRID_W);
614
+ for (c = 0; c < GRID_W; c++)
615
+ grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (uint8_t)(1 + next_rand() % 3);
616
+ if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
617
+ }
618
+ draw_well(v);
619
+ }
620
+
621
+ /* ── GAME LOGIC (clay — reshape freely) — per-player input + gravity ─────────
622
+ * P0 reads JOYSTICK PORT 1 (keyboard cursors fall back); P1 reads PORT 2.
623
+ * Edge-triggered moves (one cell per press), held DOWN soft-drops, trigger A
624
+ * cycles the trio's colours, trigger B hard-drops. */
625
+ static void update_player(uint8_t p) {
626
+ uint8_t dir, a, b, fd, soft, moved_l, moved_r, t;
627
+ if (p == 0) {
628
+ dir = msx_read_joystick(1);
629
+ if (dir == STICK_CENTER) dir = msx_read_joystick(0);
630
+ a = (uint8_t)(gttrig(1) || gttrig(0)); /* trig A or SPACE */
631
+ b = gttrig(3); /* port-1 button 2 */
632
+ } else {
633
+ dir = msx_read_joystick(2);
634
+ a = gttrig(2); /* port-2 trigger A */
635
+ b = gttrig(4); /* port-2 button 2 */
636
+ }
637
+
638
+ moved_l = (uint8_t)(dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL);
639
+ moved_r = (uint8_t)(dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR);
640
+ soft = (uint8_t)(dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR);
641
+
642
+ if (moved_l && !(prev_dir[p] & 1) && can_place(p, (int8_t)(piece_x[p] - 1), piece_y[p])) {
643
+ --piece_x[p]; sfx_move();
644
+ }
645
+ if (moved_r && !(prev_dir[p] & 2) && can_place(p, (int8_t)(piece_x[p] + 1), piece_y[p])) {
646
+ ++piece_x[p]; sfx_move();
647
+ }
648
+ prev_dir[p] = (uint8_t)((moved_l ? 1 : 0) | (moved_r ? 2 : 0));
649
+
650
+ if (a && !prev_a[p]) { /* cycle colours downward */
651
+ t = piece_col[p][2];
652
+ piece_col[p][2] = piece_col[p][1];
653
+ piece_col[p][1] = piece_col[p][0];
654
+ piece_col[p][0] = t;
655
+ sfx_rotate();
656
+ }
657
+ prev_a[p] = a;
658
+
659
+ if (b && !prev_b[p]) { /* hard drop */
660
+ prev_b[p] = b;
661
+ while (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1))) ++piece_y[p];
662
+ lock_piece(p); /* may end the game */
663
+ return;
664
+ }
665
+ prev_b[p] = b;
666
+
667
+ /* gravity: soft-drop adds extra ticks; level/mode set the base rate */
668
+ fd = two_player ? FALL_VS : (uint8_t)(32 - ((level << 1) + level)); /* 29..5 */
669
+ fall_t[p] = (uint8_t)(fall_t[p] + (soft ? 4 : 1));
670
+ if (fall_t[p] >= fd) {
671
+ fall_t[p] = 0;
672
+ if (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1)))
673
+ ++piece_y[p];
674
+ else
675
+ lock_piece(p); /* may end the game */
180
676
  }
181
677
  }
182
678
 
679
+ /* ── GAME LOGIC (clay — reshape freely) — screens ──────────────────────────
680
+ * Title rows land in third 1 / third 2 — recolored for free by the thirds
681
+ * idiom. A clean name table behind the text. */
682
+ static void clear_field(void) { msx_fill_vram(VRAM_NAME, 32u * 24u, T_SPACE); }
683
+
684
+ static void paint_title(void) {
685
+ uint8_t len = 0, col;
686
+ const char *p = GAME_TITLE;
687
+ while (*p++) len++;
688
+ col = (uint8_t)((32 - len) / 2);
689
+ clear_field();
690
+ draw_text(col, 6, GAME_TITLE);
691
+ draw_text(7, 11, "1P START - FIRE A");
692
+ draw_text(7, 13, "2P VERSUS - FIRE B");
693
+ draw_text(12, 18, "HI 0000"); /* the space blanks the cell between */
694
+ draw_num4(15, 18, hiscore);
695
+ }
696
+
697
+ static void paint_play(void) {
698
+ clear_field();
699
+ paint_frame(0);
700
+ draw_well(0);
701
+ if (two_player) {
702
+ paint_frame(1);
703
+ draw_well(1);
704
+ draw_text(15, 12, "VS");
705
+ }
706
+ draw_hud_labels();
707
+ draw_scores();
708
+ draw_hi();
709
+ draw_level();
710
+ }
711
+
712
+ static void start_game(uint8_t versus) {
713
+ uint8_t p, r, c;
714
+ two_player = versus;
715
+ well_lx[0] = versus ? WELL_VS_LX0 : WELL_1P_LX;
716
+ well_lx[1] = WELL_VS_LX1;
717
+ for (p = 0; p < 2; p++) {
718
+ for (r = 0; r < GRID_H; r++)
719
+ for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
720
+ fall_t[p] = 0;
721
+ score[p] = 0;
722
+ piece_x[p] = GRID_W / 2;
723
+ piece_y[p] = -2;
724
+ prev_dir[p] = 0;
725
+ prev_a[p] = prev_b[p] = 1; /* swallow the button that started us */
726
+ }
727
+ cleared_total = 0;
728
+ level = 1;
729
+ state = ST_PLAY;
730
+ paint_play();
731
+ spawn_piece(0);
732
+ if (versus) spawn_piece(1);
733
+ }
734
+
735
+ static void game_over(uint8_t loser) {
736
+ uint16_t best = score[0];
737
+ if (two_player && score[1] > best) best = score[1];
738
+ if (best > hiscore) { hiscore = best; }
739
+ over_loser = loser;
740
+ sfx_over();
741
+ state = ST_OVER;
742
+ clear_field();
743
+ if (two_player) draw_text(11, 7, loser ? "P1 WINS" : "P2 WINS");
744
+ else draw_text(11, 7, "GAME OVER");
745
+ draw_text(9, 10, "P1"); draw_num4(13, 10, score[0]);
746
+ if (two_player) { draw_text(9, 12, "P2"); draw_num4(13, 12, score[1]); }
747
+ draw_text(11, 14, "HI"); draw_num4(15, 14, hiscore);
748
+ draw_text(8, 17, "FIRE FOR TITLE");
749
+ prev_t1 = prev_t2 = 1; /* swallow a fire still held from play */
750
+ }
751
+
183
752
  void main(void) {
184
- uint8_t r, c, dir, prev_dir, ta, tb, prev_ta, prev_tb, fall_rate, t;
753
+ uint8_t i, t1, t2;
185
754
 
755
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
756
+ * Init order: set the video mode FIRST (INIGRP also clears VRAM — any
757
+ * upload done before it is wiped), then tiles, then sprites. The crt0's
758
+ * INIT contract means main() must NEVER return — the BIOS has nothing
759
+ * sane to fall back to — hence the for(;;) below. */
186
760
  msx_set_screen2();
187
761
  msx_clear_sprites();
188
762
  load_tiles();
189
- msx_fill_vram(VRAM_NAME, 32 * 24, T_BLANK);
190
-
191
- for (r = 0; r < ROWS; r++)
192
- for (c = 0; c < COLS; c++) grid[r][c] = 0;
763
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_GEM * 8), spr_gem, 8);
193
764
 
194
- score = 0;
195
- fall_timer = 0;
765
+ msx_music(0); /* the lib's demo loop also owns channel C —
766
+ * hand the channel to OUR tune table instead */
767
+ hiscore = 0; /* session hi-score (no SAVE_RAM on this core) */
196
768
  rng = 0xACE1;
197
- blip = 0;
198
- prev_dir = 0; prev_ta = 0; prev_tb = 0;
199
- new_piece();
200
- draw_well();
201
- draw_grid();
769
+ music_step = music_timer = 0;
770
+ sfx_a_t = sfx_b_t = 0;
771
+ for (i = 0; i < 2; i++) { prev_dir[i] = 0; prev_a[i] = prev_b[i] = 1; }
772
+ prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
773
+ two_player = 0;
774
+ state = ST_TITLE;
775
+ paint_title();
202
776
 
203
777
  for (;;) {
204
778
  vsync();
205
- draw_piece(1);
779
+ music_tick();
780
+ sfx_tick();
206
781
 
207
- dir = msx_read_joystick(1);
208
- if (dir == STICK_CENTER) dir = msx_read_joystick(0);
209
- ta = (uint8_t)(gttrig(1) != 0);
210
- tb = (uint8_t)(gttrig(2) != 0);
211
-
212
- if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
213
- && !(prev_dir == STICK_LEFT || prev_dir == STICK_UL || prev_dir == STICK_DL)
214
- && !collides((int8_t)(piece_x - 1), piece_y)) piece_x--;
215
- if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
216
- && !(prev_dir == STICK_RIGHT || prev_dir == STICK_UR || prev_dir == STICK_DR)
217
- && !collides((int8_t)(piece_x + 1), piece_y)) piece_x++;
218
-
219
- if (ta && !prev_ta) {
220
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
221
- msx_psg_tone(1, 0x280, 8); blip = 4;
222
- }
223
-
224
- if (tb && !prev_tb) {
225
- while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
226
- lock_piece();
227
- new_piece();
228
- prev_dir = dir; prev_ta = ta; prev_tb = tb;
229
- if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
782
+ if (state == ST_TITLE) {
783
+ /* ── GAME LOGIC (clay) title: trig A = 1P; trig B = 2P versus. */
784
+ t1 = (uint8_t)(gttrig(1) || gttrig(0));
785
+ t2 = (uint8_t)(gttrig(3) || gttrig(2));
786
+ if (t2 && !prev_t2) start_game(1);
787
+ else if (t1 && !prev_t1) start_game(0);
788
+ prev_t1 = t1; prev_t2 = t2;
789
+ push_sprites();
230
790
  continue;
231
791
  }
232
792
 
233
- prev_dir = dir; prev_ta = ta; prev_tb = tb;
234
-
235
- fall_rate = (dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR) ? 4 : 30;
236
- fall_timer = (uint8_t)(fall_timer + 1);
237
- if (fall_timer >= fall_rate) {
238
- fall_timer = 0;
239
- if (collides(piece_x, (int8_t)(piece_y + 1))) {
240
- lock_piece();
241
- new_piece();
242
- } else {
243
- piece_y++;
793
+ if (state == ST_OVER) {
794
+ /* Freeze the final frame; any fire button returns to the title. */
795
+ t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
796
+ if (t1 && !prev_t1) {
797
+ state = ST_TITLE;
798
+ msx_clear_sprites();
799
+ two_player = 0;
800
+ paint_title();
244
801
  }
802
+ prev_t1 = t1; prev_t2 = t1;
803
+ push_sprites();
804
+ continue;
245
805
  }
246
- draw_piece(0);
247
806
 
248
- if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
807
+ /* ── ST_PLAY GAME LOGIC (clay) both players update EVERY frame
808
+ * (simultaneous versus, not alternating turns). Any update can end
809
+ * the game, so re-check state between them. */
810
+ update_player(0);
811
+ if (two_player && state == ST_PLAY) update_player(1);
812
+
813
+ push_sprites();
249
814
  }
250
815
  }