romdevtools 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -1,313 +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);
67
297
  }
68
- static uint8_t rand_color(void) { return (uint8_t)(1 + (xorshift() % 3)); }
69
298
 
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 */
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;
75
330
  }
76
331
 
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
- }
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); }
96
335
  }
97
336
 
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);
100
367
  }
101
368
 
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));
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);
106
374
  }
107
375
 
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);
117
- }
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);
118
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);
119
392
  }
120
393
 
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]);
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
+ }
125
439
  }
126
440
 
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;
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
+ }
133
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]); }
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)); }
461
+
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} };
134
469
 
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;
139
- 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;
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
+ }
498
+ }
143
499
  }
144
- return 0;
500
+ return cnt;
145
501
  }
146
502
 
147
- /* ── match / clear / gravity core (ported from the GBC reference puzzle).
148
- * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
149
- * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
150
- * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
151
- * 4 directions, clears them, applies per-column gravity, and loops so
152
- * cascades chain (score scales with chain depth). */
153
- static uint8_t matched[ROWS][COLS];
154
- static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
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
+ }
514
+ }
155
515
 
156
- static uint8_t mark_and_count(void) {
157
- uint8_t r, c, d, len, k, cnt;
158
- uint8_t col;
159
- int8_t dr, dc;
160
- int sr, sc;
161
- cnt = 0;
162
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
163
- for (r = 0; r < ROWS; r++) {
164
- for (c = 0; c < COLS; c++) {
165
- col = grid[r][c];
166
- if (col == 0) continue;
167
- for (d = 0; d < 4; d++) {
168
- dr = DIRS4[d][0]; dc = DIRS4[d][1];
169
- sr = (int)r - dr; sc = (int)c - dc;
170
- if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
171
- && grid[sr][sc] == col) continue; /* not the run's start */
172
- len = 1;
173
- sr = (int)r + dr; sc = (int)c + dc;
174
- while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
175
- && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
176
- if (len >= 3) {
177
- sr = r; sc = c;
178
- for (k = 0; k < len; k++) {
179
- if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
180
- sr += dr; sc += dc;
181
- }
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;
182
539
  }
183
- }
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();
184
550
  }
185
- }
186
- return cnt;
551
+ return chain;
187
552
  }
188
553
 
189
- /* collapse each column so survivors rest on the floor (in place: walk
190
- * from the bottom, copying gems down to a write cursor, then zero above) */
191
- static void apply_gravity(void) {
192
- uint8_t c;
193
- int r, w;
194
- for (c = 0; c < COLS; c++) {
195
- w = ROWS - 1;
196
- for (r = ROWS - 1; r >= 0; r--) {
197
- if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
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;
559
+ for (i = 0; i < 3; i++) {
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;
198
564
  }
199
- for (; w >= 0; w--) grid[w][c] = 0;
200
- }
565
+ return 1;
201
566
  }
202
567
 
203
- static void resolve_board(void) {
204
- uint8_t n, r, c, chain;
205
- unsigned int amt;
206
- chain = 0;
207
- while (1) {
208
- n = mark_and_count();
209
- if (n == 0) break;
210
- chain++;
211
- for (r = 0; r < ROWS; r++)
212
- for (c = 0; c < COLS; c++)
213
- if (matched[r][c]) grid[r][c] = 0;
214
- amt = (unsigned int)n * 10u;
215
- if (chain > 1) amt = amt * chain;
216
- if (score < 999) score += n;
217
- msx_psg_tone(0, 0x180, 13); blip = 8; /* clear chime */
218
- apply_gravity();
219
- }
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);
220
576
  }
221
577
 
222
- static void lock_piece(void) {
223
- uint8_t i;
224
- int8_t r;
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;
225
583
  for (i = 0; i < 3; i++) {
226
- r = (int8_t)(piece_y + i);
227
- 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];
586
+ }
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 */
228
595
  }
229
- resolve_board();
230
- draw_grid();
596
+ spawn_piece(p);
231
597
  }
232
598
 
233
- static void draw_piece(uint8_t clear) {
234
- 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;
235
605
  int8_t r;
236
- uint8_t v;
237
- for (i = 0; i < 3; i++) {
238
- r = (int8_t)(piece_y + i);
239
- if (r < 0 || r >= ROWS) continue;
240
- v = clear ? grid[r][piece_x] : piece[i];
241
- 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 */
242
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 */
676
+ }
677
+ }
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 */
243
750
  }
244
751
 
245
752
  void main(void) {
246
- uint8_t r, c, dir, prev_dir, ta, tb, prev_ta, prev_tb, fall_rate, t;
753
+ uint8_t i, t1, t2;
247
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. */
248
760
  msx_set_screen2();
249
761
  msx_clear_sprites();
250
762
  load_tiles();
251
- msx_fill_vram(VRAM_NAME, 32 * 24, T_BLANK);
252
-
253
- for (r = 0; r < ROWS; r++)
254
- for (c = 0; c < COLS; c++) grid[r][c] = 0;
763
+ msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_GEM * 8), spr_gem, 8);
255
764
 
256
- score = 0;
257
- 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) */
258
768
  rng = 0xACE1;
259
- blip = 0;
260
- prev_dir = 0; prev_ta = 0; prev_tb = 0;
261
- new_piece();
262
- draw_well();
263
- 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();
264
776
 
265
777
  for (;;) {
266
778
  vsync();
267
- msx_music_tick();
268
- draw_piece(1);
269
-
270
- dir = msx_read_joystick(1);
271
- if (dir == STICK_CENTER) dir = msx_read_joystick(0);
272
- ta = (uint8_t)(gttrig(1) != 0);
273
- tb = (uint8_t)(gttrig(2) != 0);
274
-
275
- if ((dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL)
276
- && !(prev_dir == STICK_LEFT || prev_dir == STICK_UL || prev_dir == STICK_DL)
277
- && !collides((int8_t)(piece_x - 1), piece_y)) piece_x--;
278
- if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
279
- && !(prev_dir == STICK_RIGHT || prev_dir == STICK_UR || prev_dir == STICK_DR)
280
- && !collides((int8_t)(piece_x + 1), piece_y)) piece_x++;
281
-
282
- if (ta && !prev_ta) {
283
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
284
- msx_psg_tone(1, 0x280, 8); blip = 4;
285
- }
286
-
287
- if (tb && !prev_tb) {
288
- while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
289
- lock_piece();
290
- new_piece();
291
- prev_dir = dir; prev_ta = ta; prev_tb = tb;
292
- if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
779
+ music_tick();
780
+ sfx_tick();
781
+
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();
293
790
  continue;
294
791
  }
295
792
 
296
- prev_dir = dir; prev_ta = ta; prev_tb = tb;
297
-
298
- fall_rate = (dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR) ? 4 : 30;
299
- fall_timer = (uint8_t)(fall_timer + 1);
300
- if (fall_timer >= fall_rate) {
301
- fall_timer = 0;
302
- if (collides(piece_x, (int8_t)(piece_y + 1))) {
303
- lock_piece();
304
- new_piece();
305
- } else {
306
- 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();
307
801
  }
802
+ prev_t1 = t1; prev_t2 = t1;
803
+ push_sprites();
804
+ continue;
308
805
  }
309
- draw_piece(0);
310
806
 
311
- 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();
312
814
  }
313
815
  }