romdevtools 0.28.0 → 0.30.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -1,173 +1,507 @@
1
- /* ── puzzle.c — SMS match-3 falling-block scaffold ─────────────────
1
+ /* ── puzzle.c — SMS falling-gem versus puzzle (complete example game) ─────────
2
2
  *
3
- * Mirrors the NES/Genesis/SNES/GB puzzle scaffolds. 6-wide × 12-tall
4
- * grid drawn via the BG tilemap (three distinct BG tile shapes for
5
- * R/G/B cells). 1×3 active piece, rotate via B1, soft-drop via DOWN,
6
- * START hard-drops, horizontal-triple clear.
3
+ * GEODE GAMBIT a COMPLETE, working game: title screen, 1P MARATHON mode
4
+ * (levels speed the fall as you clear) and 2P SIMULTANEOUS VERSUS mode — two
5
+ * 6x12 wells side by side, P1 on PORT A, P2 on PORT B, both falling at once,
6
+ * where every cascade chain you score lays SIEGE to the other well: garbage
7
+ * rows rise from the bottom of your rival's board. Score + persistent
8
+ * hi-score (Sega-mapper cart RAM — see the honesty note at hiscore_save),
9
+ * PSG music + SFX, and the SMS's signature LINE-INTERRUPT split: a fixed HUD
10
+ * strip over the wells, timed by the VDP's programmable line counter.
11
+ *
12
+ * The game: a falling-trio match-3. A vertical trio of gems drops into a
13
+ * well; LEFT/RIGHT move it, button 1 cycles its three colours, DOWN
14
+ * soft-drops, button 2 hard-drops. When it lands, any straight run of 3+
15
+ * same-coloured gems (horizontal, vertical, or diagonal) clears; survivors
16
+ * fall and cascades chain for multiplied score. First stack to reach the rim
17
+ * loses.
18
+ *
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 SMS footgun; reshape
22
+ * your gameplay around it (see TROUBLESHOOTING before changing).
23
+ * GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
24
+ *
25
+ * What depends on what:
26
+ * sms_hw.h / vdp_init.c / load_palette.c / load_tiles.c / sprite_table.c /
27
+ * joypad_read.c — the bundled VDP + input runtime (this file's externs).
28
+ * sms_sfx.{h,c} + sms_music.{h,c} — SN76489 PSG sound layers.
29
+ * sms_crt0.s — boot + vector table. Its $0038 IM-1 handler is the OTHER
30
+ * HALF of the line-interrupt idiom below: it acks the VDP (one status
31
+ * read clears BOTH the frame and line IRQ flags) and returns with
32
+ * ei/reti. Load-bearing; edit with TROUBLESHOOTING open.
33
+ *
34
+ * Frame budget (NTSC, 60fps) — and a TEACHING POINT vs the NES version of
35
+ * this game (examples/nes/templates/puzzle.c): on the NES, board repaints
36
+ * squeeze through a ~16-entry vblank queue, so a full-board repaint is
37
+ * BUDGETED across 12 frames of dirty-row bitmask tricks. The SMS has no such
38
+ * famine: the BOARD IS A BG TILEMAP, and a whole well (12 rows x 6 cells) is
39
+ * 144 sms_set_tilemap_cell writes — well under a single vblank's VRAM
40
+ * bandwidth. So when a lock dirties a board we just repaint the WHOLE well in
41
+ * the next vblank (board_dirty flag) — no per-row drip, no queue. The only
42
+ * thing we DO budget is the HUD's software 16-bit divisions (see the BUDGET
43
+ * FOOTGUN at the main loop), exactly as the platformer/shmup do. Same genre,
44
+ * two bandwidth worlds — fork accordingly.
45
+ *
46
+ * SDCC FOOTGUN (bites every fork): uint8 loop bounds silently wrap —
47
+ * `for (uint8_t i = 0; i < 12 * 6; i++)` is fine (72 < 255), but a full-board
48
+ * paint `for (uint8_t i = 0; i < 24 * 32; i++)` is an INFINITE loop (768 >
49
+ * 255; SDCC even warns "comparison is always true"). Treat that warning as an
50
+ * error: widen the counter to uint16_t or keep loops nested per-row like the
51
+ * painters below.
7
52
  */
8
53
  #include "sms_hw.h"
9
54
  #include "sms_sfx.h"
10
55
  #include "sms_music.h"
11
56
  #include <stdint.h>
12
57
 
13
- extern void sms_vdp_init(void);
14
- extern void sms_vdp_display_on(void);
15
- extern void sms_vdp_set_addr(uint16_t addr, uint8_t prefix);
16
- extern void sms_load_palette(const uint8_t *palette);
17
- extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
18
- extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
19
- extern void sms_vblank_wait(void);
58
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
59
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
60
+ #define GAME_TITLE "GEODE GAMBIT"
61
+
62
+ extern void sms_vdp_init(void);
63
+ extern void sms_vdp_write_reg(uint8_t reg, uint8_t value);
64
+ extern void sms_vdp_display_on(void);
65
+ extern void sms_vdp_display_off(void);
66
+ extern void sms_vdp_set_addr(uint16_t addr, uint8_t prefix);
67
+ extern void sms_load_palette(const uint8_t *palette);
68
+ extern void sms_load_tiles(uint16_t vram_dest, const uint8_t *src, uint16_t byte_count);
69
+ extern void sms_set_tilemap_cell(uint8_t row, uint8_t col, uint8_t tile_idx, uint8_t attr);
20
70
  extern uint8_t sms_joypad_read(void);
71
+ extern uint8_t sms_joypad_read_p2(void);
72
+ extern void sms_sprite_init(void);
73
+ extern void sms_sprite_set(uint8_t slot, uint8_t x, uint8_t y, uint8_t tile);
74
+ extern void sms_sat_upload(void);
21
75
 
22
- #define COLS 6
23
- #define ROWS 12
76
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
77
+ * Palettes. SMS CRAM is 2-2-2 BGR (--BBGGRR): R bits 0-1, G bits 2-3,
78
+ * B bits 4-5. White = 0x3F. BG colour 0 doubles as the backdrop/border. */
79
+ static const uint8_t palette[32] = {
80
+ /* BG: 0 = cabinet navy, 1 = ruby, 2 = emerald, 3 = white (text + glint),
81
+ * 4 = HUD-bar steel, 5 = sapphire, 6 = well-frame grey, 7 = dim well floor */
82
+ 0x10, 0x03, 0x0C, 0x3F, 0x15, 0x30, 0x2A, 0x05,
83
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
84
+ /* Sprites: 1 = ruby, 2 = emerald, 3 = sapphire (the falling trio's three
85
+ * colours). One shared sprite palette on SMS — per-"sprite" colour means
86
+ * per-TILE colour indices, not per-sprite palettes. */
87
+ 0x00, 0x03, 0x0C, 0x30, 0x00, 0x00, 0x00, 0x00,
88
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
89
+ };
24
90
 
25
- #define T_BLANK 0
26
- #define T_R 1
27
- #define T_G 2
28
- #define T_B 3
29
- #define T_WALL 4 /* well border */
30
- #define T_FIELD 5 /* empty well interior */
91
+ /* ── GAME LOGIC (clay) — BG tile inventory (BG bank $0000) ───────────────────
92
+ * tile 0 = blank cabinet (colour 0)
93
+ * tiles 1..37 = font: digits 0-9, A-Z, '-' (uploaded 1bpp→4bpp below)
94
+ * tiles 38..40 = gem colours 1/2/3 (ruby/emerald/sapphire) as BG tiles
95
+ * tile 41 = well frame (steel grey)
96
+ * tile 42 = empty well floor (dim — so the well reads as recessed)
97
+ * tile 43 = solid HUD bar (colour 4) — the split seam hides in it */
98
+ #define FONT_BASE 1
99
+ #define BG_GEM_BASE 38 /* +0/+1/+2 = gem colours 1/2/3 */
100
+ #define BG_FRAME 41
101
+ #define BG_FLOOR 42
102
+ #define BG_HUDBAR 43
31
103
 
32
- static const uint8_t palette[32] = {
33
- /* BG palette: 0 backdrop navy, 1 red, 2 green, 3 blue, 4 wall grey,
34
- * 5 dim field blue */
35
- 0x10,0x03,0x0C,0x30, 0x15,0x14, 0x00,0x00,
36
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
37
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
38
- 0x00,0x00,0x00,0x00, 0x00,0x00,0x00,0x00,
104
+ /* 1bpp font (same glyph set as the NES/GB examples — 0-9, A-Z, '-').
105
+ * Stored 8 bytes/glyph; expanded to the SMS's 32-byte 4bpp tiles at upload
106
+ * (see load_font below), so the ROM carries 296 bytes instead of 1184. */
107
+ static const uint8_t font8[37][8] = {
108
+ /* 0-9 */
109
+ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
110
+ {0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
111
+ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
112
+ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
113
+ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
114
+ /* A-Z */
115
+ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
116
+ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
117
+ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
118
+ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3E,0x00}, {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
119
+ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00}, {0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00},
120
+ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
121
+ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
122
+ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
123
+ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
124
+ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
125
+ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
126
+ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
127
+ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
128
+ /* '-' */
129
+ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
39
130
  };
40
131
 
41
- static const uint8_t bg_tiles[32 * 6] = {
42
- /* T_BLANK */
43
- 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
44
- 0,0,0,0, 0,0,0,0, 0,0,0,0, 0,0,0,0,
45
- /* T_R colour 1 fill */
46
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
47
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
48
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
49
- 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00,
50
- /* T_G colour 2 fill */
51
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
52
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
53
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
54
- 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
55
- /* T_B — colour 3 fill (planes 0+1) */
56
- 0xFF,0xFF,0x00,0x00, 0xFF,0xFF,0x00,0x00,
57
- 0xFF,0xFF,0x00,0x00, 0xFF,0xFF,0x00,0x00,
58
- 0xFF,0xFF,0x00,0x00, 0xFF,0xFF,0x00,0x00,
59
- 0xFF,0xFF,0x00,0x00, 0xFF,0xFF,0x00,0x00,
60
- /* T_WALL colour 4 fill (plane 2 set) */
61
- 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
62
- 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
63
- 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
64
- 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
65
- /* T_FIELD colour 5 fill (planes 0+2 set) = dim field */
66
- 0xFF,0x00,0xFF,0x00, 0xFF,0x00,0xFF,0x00,
67
- 0xFF,0x00,0xFF,0x00, 0xFF,0x00,0xFF,0x00,
68
- 0xFF,0x00,0xFF,0x00, 0xFF,0x00,0xFF,0x00,
69
- 0xFF,0x00,0xFF,0x00, 0xFF,0x00,0xFF,0x00,
132
+ /* Expand 1bpp glyphs into 4bpp SMS tiles as colour 3 (planes 0+1 set).
133
+ * SMS tile rows are 4 bytes: plane0, plane1, plane2, plane3. */
134
+ static void load_font(void) {
135
+ uint8_t g, r, bits;
136
+ sms_vdp_set_addr((uint16_t)(FONT_BASE * 32), VDP_VRAM_WRITE);
137
+ for (g = 0; g < 37; g++) {
138
+ for (r = 0; r < 8; r++) {
139
+ bits = font8[g][r];
140
+ PORT_VDP_DATA = bits; /* plane 0 */
141
+ PORT_VDP_DATA = bits; /* plane 1 colour index 3 */
142
+ PORT_VDP_DATA = 0; /* plane 2 */
143
+ PORT_VDP_DATA = 0; /* plane 3 */
144
+ }
145
+ }
146
+ }
147
+
148
+ /* ── GAME LOGIC (clay) — gem + furniture tiles (4bpp, 32 bytes each).
149
+ * KEY TRICK: the three gem tiles are the SAME rounded shape on different
150
+ * colour planes — a cell changes colour by changing its TILE index, no
151
+ * re-upload. Colour 1 = plane 0 only, colour 2 = plane 1 only, colour 3 =
152
+ * planes 0+1. A bright corner pixel (colour 3 = planes 0+1) gives each gem a
153
+ * glint so they don't read as flat squares. */
154
+ static const uint8_t gem_furniture[32 * 6] = {
155
+ /* BG_GEM_BASE+0 — ruby (colour 1 fill, white glint top-left) */
156
+ 0x3C,0x3C,0x00,0x00, 0x7E,0x42,0x00,0x00, 0xFF,0x81,0x00,0x00, 0xFF,0x00,0x00,0x00,
157
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00, 0x3C,0x00,0x00,0x00,
158
+ /* BG_GEM_BASE+1 — emerald (colour 2 fill = plane 1, glint colour 3) */
159
+ 0x00,0x3C,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x18,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
160
+ 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x00,0x3C,0x00,0x00,
161
+ /* BG_GEM_BASE+2 — sapphire (colour 5 fill = plane 0+2, glint colour 3) */
162
+ 0x3C,0x00,0x3C,0x00, 0x42,0x00,0x7E,0x00, 0x81,0x18,0xFF,0x00, 0x00,0x00,0xFF,0x00,
163
+ 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0x7E,0x00, 0x00,0x00,0x3C,0x00,
164
+ /* BG_FRAME — solid colour 6 (well-frame grey = planes 1+2) */
165
+ 0x00,0xFF,0xFF,0x00, 0x00,0xFF,0xFF,0x00, 0x00,0xFF,0xFF,0x00, 0x00,0xFF,0xFF,0x00,
166
+ 0x00,0xFF,0xFF,0x00, 0x00,0xFF,0xFF,0x00, 0x00,0xFF,0xFF,0x00, 0x00,0xFF,0xFF,0x00,
167
+ /* BG_FLOOR — empty well floor: colour 7 (planes 0+1+2) with a faint speck */
168
+ 0xFF,0xFF,0xFF,0x00, 0xFF,0xFF,0xFF,0x00, 0xFF,0xFF,0xFF,0x00, 0xEF,0xFF,0xFF,0x00,
169
+ 0xFF,0xFF,0xFF,0x00, 0xFF,0xFF,0xFF,0x00, 0xFF,0xFF,0xFF,0x00, 0xFF,0xFF,0xFF,0x00,
170
+ /* BG_HUDBAR — solid colour 4 (binary 100 → plane 2 only); seam hides here */
171
+ 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
172
+ 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00, 0x00,0x00,0xFF,0x00,
70
173
  };
71
174
 
72
- static uint8_t grid[ROWS][COLS];
73
- static uint8_t piece[3];
74
- static int8_t piece_x;
75
- static int8_t piece_y;
76
- static uint8_t fall_timer;
77
- static uint16_t score;
78
- static uint32_t rng = 1;
79
-
80
- static uint32_t xorshift(void) {
81
- rng ^= rng << 13;
82
- rng ^= rng >> 17;
83
- rng ^= rng << 5;
84
- return rng;
175
+ /* Sprite tiles (sprite bank $2000 — vdp_init's R6=0xFF baseline reads
176
+ * sprite patterns from $2000, so upload there, not $0000). The falling trio's
177
+ * cells are SPRITES (they move every frame); locked gems are BG tiles. Same
178
+ * three colours as the BG gems above. */
179
+ static const uint8_t sprite_tiles[32 * 3] = {
180
+ /* T_SPR+0 — ruby (colour 1) */
181
+ 0x3C,0x3C,0x00,0x00, 0x7E,0x42,0x00,0x00, 0xFF,0x81,0x00,0x00, 0xFF,0x00,0x00,0x00,
182
+ 0xFF,0x00,0x00,0x00, 0xFF,0x00,0x00,0x00, 0x7E,0x00,0x00,0x00, 0x3C,0x00,0x00,0x00,
183
+ /* T_SPR+1 — emerald (colour 2 = plane 1) */
184
+ 0x00,0x3C,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x18,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
185
+ 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x00,0x3C,0x00,0x00,
186
+ /* T_SPR+2 sapphire (colour 3 = planes 0+1; sprite palette index 3) */
187
+ 0x3C,0x3C,0x00,0x00, 0x42,0x7E,0x00,0x00, 0x81,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00,
188
+ 0x00,0xFF,0x00,0x00, 0x00,0xFF,0x00,0x00, 0x00,0x7E,0x00,0x00, 0x00,0x3C,0x00,0x00,
189
+ };
190
+ #define T_SPR 0 /* sprite tile of gem colour 1 */
191
+
192
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
193
+ * Board geometry. Cells are 8x8 (one BG tile), 6 wide x 12 tall. The 32-cell
194
+ * name table is 256 px wide; a single well is 8 cells (frame + 6 + frame), so
195
+ * two wells fit side by side with a centre gutter for the 2P split board.
196
+ * Playfield TILE rows 4..15 sit below the 3-row HUD strip. */
197
+ #define GRID_W 6
198
+ #define GRID_H 12
199
+ #define WELL_TY 4 /* top TILE row of the well interior */
200
+ #define WELL_1P_TX 13 /* 1P: single centred well interior */
201
+ #define WELL_VS_P1 3 /* 2P: P1 interior cols 3-8 ... */
202
+ #define WELL_VS_P2 23 /* P2 interior cols 23-28 (split) */
203
+ #define EMPTY 0 /* cell colours 1..3 = ruby/em/sapph */
204
+
205
+ /* HUD layout: row 0 = text, row 1 = blank, row 2 = solid bar. The bar row is
206
+ * both the visual divider AND where the split seam hides. R0's leftmost-column
207
+ * blank (bit 5) makes screen column 0 invisible — HUD text starts at col 1. */
208
+ #define HUD_ROWS 3
209
+ #define HUD_PX (HUD_ROWS * 8)
210
+
211
+ #define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
212
+ #define GARBAGE_CAP 4 /* max garbage rows per attack */
213
+
214
+ /* ── GAME LOGIC (clay) — game state.
215
+ * The hot ones are deliberately NON-static: they then appear in the sdld map
216
+ * (build symbols) at $Cxxx in work RAM, so a headless agent can resolve them
217
+ * by name and read/poke live state (parse the map → system_ram offset =
218
+ * addr-0xC000). The SMS has 8KB of work RAM ($C000-$DFFF), so these plain
219
+ * arrays cost nothing — no NES scratch-page gymnastics. */
220
+ uint8_t grid[2][GRID_H][GRID_W]; /* the two wells (P2's unused in 1P) */
221
+ int8_t piece_x[2]; /* falling trio: column 0..5 */
222
+ int8_t piece_y[2]; /* row of its TOP cell (<0 above rim) */
223
+ uint8_t piece_col[2][3]; /* trio colours, top to bottom */
224
+ uint16_t score[2];
225
+ uint16_t hiscore;
226
+ uint8_t level; /* 1P: 1..9, speeds up the fall */
227
+ uint8_t state; /* ST_TITLE / ST_PLAY / ST_OVER */
228
+ uint8_t two_player;
229
+
230
+ static uint8_t matched[GRID_H][GRID_W];
231
+ static uint8_t well_tx[2]; /* left interior TILE column per well */
232
+ static uint8_t fall_t[2]; /* frames until next gravity step */
233
+ static uint8_t prev_pad[2]; /* for edge-triggered input */
234
+ static uint16_t cleared_total; /* 1P: gems cleared, drives the level */
235
+ static uint8_t board_dirty[2]; /* well needs a full repaint this frame*/
236
+ static uint8_t hud_dirty; /* score/level/layout changed → redraw */
237
+ static uint8_t loser; /* who topped out (2P: 0=P1, 1=P2) */
238
+ static uint8_t over_step; /* results text, one piece per vblank */
239
+ static uint16_t rng = 0xACE1;
240
+
241
+ #define ST_TITLE 0
242
+ #define ST_PLAY 1
243
+ #define ST_OVER 2
244
+
245
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
246
+ static uint8_t random8(void) {
247
+ uint16_t r = rng;
248
+ r ^= r << 7;
249
+ r ^= r >> 9;
250
+ r ^= r << 8;
251
+ rng = r;
252
+ return (uint8_t)r;
253
+ }
254
+
255
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
256
+ * LINE-INTERRUPT SPLIT — the SMS's signature trick (fixed status bar over the
257
+ * playfield, palette splits, water effects). The VDP has ONE scroll register
258
+ * pair for the whole frame; to keep the HUD strip pinned at the top while the
259
+ * playfield renders below it, we DON'T scroll here (a puzzle board doesn't
260
+ * move) — but we still take the line IRQ at the bar so the idiom is wired and
261
+ * ready, and so the per-frame timing (vblank → line IRQ → game logic) matches
262
+ * the platformer/shmup exactly. Where the NES needs the sprite-0-hit HACK
263
+ * (park a sprite, busy-poll a status bit, burn scanlines spinning), the SMS
264
+ * has a real, PROGRAMMABLE line interrupt:
265
+ *
266
+ * R10 = N line counter: a down-counter reloaded with N every line
267
+ * outside the active area; underflow → IRQ at line N.
268
+ * R0 bit 4 (IE1) line-IRQ enable (already set in vdp_init's 0x36 baseline).
269
+ * R1 bit 5 (IE0) frame(vblank)-IRQ enable (set by sms_vdp_display_on's 0xE0).
270
+ *
271
+ * Both IRQs land on the Z80's IM-1 vector at $0038. The crt0's handler does
272
+ * the canonical minimal handshake: push af / in a,($BF) / pop af / ei / reti
273
+ * — reading the status port ACKS the VDP (clears BOTH pending flags; skip the
274
+ * read and the IRQ line stays asserted = interrupt storm), and EI must
275
+ * precede RETI or interrupts stay off forever after the first one.
276
+ *
277
+ * Because the handler does no work, the MAIN loop synchronizes with HALT: the
278
+ * Z80 sleeps until the next interrupt, then reads the V-counter (port $7E) to
279
+ * learn WHICH one woke us — line IRQs fire during the active area (V < 0xC0),
280
+ * the frame IRQ fires at vblank (V ≥ 0xC0).
281
+ *
282
+ * wait_vblank(): sleep until the frame IRQ → do per-frame VRAM work.
283
+ * wait_split(): sleep until the line IRQ at the last bar line → past it,
284
+ * the playfield renders. (If you ADD a scrolling background
285
+ * under the wells, this is where you'd write R8 — see the
286
+ * shmup/platformer templates for the R8 = scroll write.)
287
+ *
288
+ * FOOTGUN — you cannot poll once IRQs are on: a status-port poll spins on the
289
+ * same port the ISR reads. The ISR always wins the race, eats the flag, and
290
+ * the poll loop hangs forever. HALT + V-counter is the IRQ-era replacement.
291
+ *
292
+ * Requires: R10 programmed, IE1 + IE0 enabled, EI executed once after
293
+ * display-on, the crt0's ack-only ISR, and wait_vblank/wait_split called
294
+ * EVERY frame in this order. R10 reloads after each underflow, so the line
295
+ * IRQ re-fires every HUD_PX lines all the way down the frame — the later
296
+ * wakes harmlessly interrupt game logic (the ISR acks them) and we re-halt
297
+ * inside the NEXT wait_vblank(). */
298
+ #define SPLIT_LINE (HUD_PX - 1)
299
+
300
+ static void wait_vblank(void) {
301
+ /* check-first: if game logic overran into vblank, don't sleep a frame */
302
+ while (PORT_V_COUNTER < 0xC0) { __asm__("halt"); }
85
303
  }
86
304
 
87
- static uint8_t rand_color(void) { return (uint8_t)(1 + (xorshift() % 3)); }
305
+ static void wait_split(void) {
306
+ /* halt-first: vblank work always ends inside vblank (V ≥ 0xC0), and the
307
+ * first wake at V < 0xC0 is the line IRQ at SPLIT_LINE */
308
+ do { __asm__("halt"); } while (PORT_V_COUNTER >= 0xC0);
309
+ }
88
310
 
89
- static uint8_t tile_for(uint8_t c) {
90
- if (c == 1) return T_R;
91
- if (c == 2) return T_G;
92
- if (c == 3) return T_B;
93
- return T_FIELD; /* empty cell shows the dim well interior, not backdrop */
311
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
312
+ * hi-score in Sega-mapper cart RAM. The Sega mapper's control register at
313
+ * $FFFC: bit 3 maps the cart's 8KB battery RAM into $8000-$BFFF (bank slot
314
+ * 2). Map → copy → unmap; keep the window short so stray pointer bugs can't
315
+ * shred the save. The block is magic + value + checksum so a never-written
316
+ * cart (all $FF) reads back as "no save" instead of a garbage hi-score.
317
+ *
318
+ * NOTE the $FFFC address: it's IN the WRAM mirror ($C000-$DFFF mirrors at
319
+ * $E000-$FFFF), so this write also lands in WRAM at $DFFC — the mapper just
320
+ * snoops the bus. That's why the crt0 parks SP at $DFF0: the bytes above it
321
+ * ($DFFC-$FFFF) belong to the mapper registers' shadow.
322
+ *
323
+ * HONESTY (verified against the bundled gpgx core): gpgx only instantiates
324
+ * the Sega mapper for ROMs LARGER than 48KB, and this build pipeline emits
325
+ * 32KB ROMs — so in-emulator the $8000 window stays open-bus (reads $FF), the
326
+ * magic check fails, and the game falls back to the WRAM hi-score (in-session
327
+ * only). The code below is still the correct real-hardware idiom and lights up
328
+ * unchanged on a >48KB build or a cart with battery RAM: the load path is
329
+ * self-falsifying, never wrong. (The verify harness pads this ROM to 64KB to
330
+ * exercise the cart-RAM path for real.) */
331
+ #define MAPPER_CTRL (*(volatile uint8_t *)0xFFFC)
332
+ #define CART_RAM ((volatile uint8_t *)0x8000)
333
+
334
+ static void hiscore_save(uint16_t v) {
335
+ uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
336
+ MAPPER_CTRL = 0x08; /* map cart RAM at $8000 */
337
+ CART_RAM[0] = 0x48; /* 'H' */
338
+ CART_RAM[1] = 0x53; /* 'S' */
339
+ CART_RAM[2] = lo;
340
+ CART_RAM[3] = hi;
341
+ CART_RAM[4] = (uint8_t)(lo ^ hi ^ 0xA5);
342
+ MAPPER_CTRL = 0x00; /* back to ROM in slot 2 */
94
343
  }
95
344
 
96
- static void draw_cell(int8_t col, int8_t row, uint8_t cell) {
97
- if (row < 0 || row >= ROWS) return;
98
- /* Centre the 6-col grid; +7 columns left margin, +1 row top margin. */
99
- sms_set_tilemap_cell((uint8_t)(row + 1), (uint8_t)(col + 7), tile_for(cell), 0);
345
+ static uint16_t hiscore_load(void) {
346
+ uint16_t v = 0;
347
+ MAPPER_CTRL = 0x08;
348
+ if (CART_RAM[0] == 0x48 && CART_RAM[1] == 0x53 &&
349
+ CART_RAM[4] == (uint8_t)(CART_RAM[2] ^ CART_RAM[3] ^ 0xA5)) {
350
+ v = (uint16_t)(CART_RAM[2] | ((uint16_t)CART_RAM[3] << 8));
351
+ }
352
+ MAPPER_CTRL = 0x00;
353
+ return v;
100
354
  }
101
355
 
102
- /* Draw the well: a grey border frame around the 6x12 play field with a dim
103
- * field interior, so the playfield is clearly visible even when empty.
104
- * The grid maps cell (col,row) -> tilemap (row+1, col+7), i.e. rows 1..12
105
- * cols 7..12. Frame the perimeter at rows 0..13, cols 6..13. */
106
- static void draw_well(void) {
107
- uint8_t r, c;
108
- for (r = 0; r <= 13; r++) {
109
- for (c = 6; c <= 13; c++) {
110
- uint8_t t = T_FIELD;
111
- if (r == 0 || r == 13 || c == 6 || c == 13) t = T_WALL;
112
- sms_set_tilemap_cell(r, c, t, 0);
113
- }
356
+ /* ── GAME LOGIC (clay) text via the font tiles ─────────────────────────────
357
+ * These write the name table directly, so call them only during vblank (or
358
+ * with the display off): VRAM access during active display races the VDP's
359
+ * own fetches and drops/garbles bytes on real hardware. */
360
+ static uint8_t font_tile(char ch) {
361
+ if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE + (ch - '0'));
362
+ if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE + 10 + (ch - 'A'));
363
+ if (ch == '-') return (uint8_t)(FONT_BASE + 36);
364
+ return 0; /* space → blank tile */
365
+ }
366
+
367
+ static void text_draw(uint8_t row, uint8_t col, const char *s) {
368
+ while (*s) sms_set_tilemap_cell(row, col++, font_tile(*s++), 0);
369
+ }
370
+
371
+ static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
372
+ uint8_t d[5], i;
373
+ for (i = 0; i < 5; i++) { d[i] = (uint8_t)(v % 10); v /= 10; }
374
+ for (i = 0; i < 5; i++)
375
+ sms_set_tilemap_cell(row, (uint8_t)(col + i), (uint8_t)(FONT_BASE + d[4 - i]), 0);
376
+ }
377
+
378
+ /* ── GAME LOGIC (clay) — HUD: row 0 (columns start at 1 — R0 left-column blank
379
+ * hides column 0). 1P: SC sssss HI hhhhh LV n. 2P: P1 sssss HI P2 sssss. */
380
+ static void draw_hud(void) {
381
+ uint8_t c;
382
+ for (c = 1; c < 32; c++) sms_set_tilemap_cell(0, c, 0, 0); /* clear row */
383
+ if (state == ST_TITLE) {
384
+ text_draw(0, 13, "HI");
385
+ draw_u16(0, 16, hiscore);
386
+ return;
387
+ }
388
+ if (two_player) {
389
+ text_draw(0, 1, "P1"); draw_u16(0, 4, score[0]);
390
+ text_draw(0, 13, "HI"); draw_u16(0, 16, hiscore);
391
+ text_draw(0, 24, "P2"); draw_u16(0, 27, score[1]);
392
+ } else {
393
+ text_draw(0, 1, "SC"); draw_u16(0, 4, score[0]);
394
+ text_draw(0, 13, "HI"); draw_u16(0, 16, hiscore);
395
+ text_draw(0, 24, "LV");
396
+ sms_set_tilemap_cell(0, 27, (uint8_t)(FONT_BASE + level), 0);
114
397
  }
115
398
  }
116
399
 
117
- static void draw_grid(void) {
118
- int8_t r, c;
119
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) draw_cell(c, r, grid[r][c]);
400
+ /* ── GAME LOGIC (clay) — cell colour → BG tile (empty shows the dim floor). */
401
+ static uint8_t bg_tile_for(uint8_t col) {
402
+ return col ? (uint8_t)(BG_GEM_BASE - 1 + col) : BG_FLOOR;
120
403
  }
121
404
 
122
- static void new_piece(void) {
123
- piece[0] = rand_color();
124
- piece[1] = rand_color();
125
- piece[2] = rand_color();
126
- piece_x = COLS / 2 - 1;
127
- piece_y = -3;
405
+ /* ── HARDWARE IDIOM (load-bearing) — whole-well repaint. Each dirtied well is
406
+ * repainted ENTIRELY in the next vblank: 12 rows × 6 cells = 72 cell writes,
407
+ * trivially inside a vblank's VRAM budget (the per-cell PERF FOOTGUN the shmup
408
+ * found applies to FULL-SCREEN repaints, not a 72-cell well — and these only
409
+ * fire on a lock/clear, not every frame). Contrast the NES version, which
410
+ * must drip ONE board row per frame through a 16-entry queue. ── */
411
+ static void repaint_well(uint8_t p) {
412
+ uint8_t r, c, tx = well_tx[p];
413
+ for (r = 0; r < GRID_H; r++)
414
+ for (c = 0; c < GRID_W; c++)
415
+ sms_set_tilemap_cell((uint8_t)(WELL_TY + r), (uint8_t)(tx + c),
416
+ bg_tile_for(grid[p][r][c]), 0);
128
417
  }
129
418
 
130
- static uint8_t collides(int8_t col, int8_t row) {
131
- uint8_t i;
132
- int8_t r;
133
- if (col < 0 || col >= COLS) return 1;
134
- for (i = 0; i < 3; i++) {
135
- r = (int8_t)(row + i);
136
- if (r >= ROWS) return 1;
137
- if (r >= 0 && grid[r][col] != 0) return 1;
419
+ /* ── GAME LOGIC (clay) screen painters (DISPLAY OFF: free VRAM access, clean
420
+ * cut). While the display is off the frame IRQ doesn't fire — so no halt-based
421
+ * waits in here, or you hang forever. */
422
+ static void paint_frame_chrome(uint8_t p) {
423
+ uint8_t r, tx = well_tx[p];
424
+ /* top + bottom frame rows */
425
+ for (r = 0; r < GRID_W + 2; r++) {
426
+ sms_set_tilemap_cell((uint8_t)(WELL_TY - 1), (uint8_t)(tx - 1 + r), BG_FRAME, 0);
427
+ sms_set_tilemap_cell((uint8_t)(WELL_TY + GRID_H), (uint8_t)(tx - 1 + r), BG_FRAME, 0);
428
+ }
429
+ /* left + right frame columns */
430
+ for (r = 0; r < GRID_H; r++) {
431
+ sms_set_tilemap_cell((uint8_t)(WELL_TY + r), (uint8_t)(tx - 1), BG_FRAME, 0);
432
+ sms_set_tilemap_cell((uint8_t)(WELL_TY + r), (uint8_t)(tx + GRID_W), BG_FRAME, 0);
433
+ }
434
+ }
435
+
436
+ static void paint_blank_field(void) {
437
+ uint8_t r, c;
438
+ for (r = 0; r < 24; r++) {
439
+ sms_vdp_set_addr((uint16_t)(0x3800 + (uint16_t)r * 64), VDP_VRAM_WRITE);
440
+ for (c = 0; c < 32; c++) {
441
+ uint8_t t = (r == 2) ? BG_HUDBAR : 0; /* row 2 = HUD bar (split seam) */
442
+ PORT_VDP_DATA = t; /* name-table entry low byte */
443
+ PORT_VDP_DATA = 0; /* high byte: flips/pal/priority */
444
+ }
138
445
  }
139
- return 0;
140
446
  }
141
447
 
142
- /* ── match / clear / gravity core (ported from the GBC reference puzzle).
143
- * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
144
- * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
145
- * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
146
- * 4 directions, clears them, applies per-column gravity, and loops so
147
- * cascades chain (score scales with chain depth). */
148
- static uint8_t matched[ROWS][COLS];
448
+ static void paint_title(void) {
449
+ sms_vdp_display_off();
450
+ paint_blank_field();
451
+ text_draw(7, (uint8_t)((32 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
452
+ text_draw(12, 10, "1P START - 1");
453
+ text_draw(14, 10, "2P VERSUS - 2");
454
+ text_draw(17, 7, "1 ROTATE 2 DROP");
455
+ text_draw(19, 4, "CHAINS BESIEGE YOUR RIVAL");
456
+ draw_hud();
457
+ sms_sprite_init(); /* park every sprite off-screen */
458
+ sms_sat_upload();
459
+ sms_vdp_display_on(); /* re-enables the frame IRQ too */
460
+ }
461
+
462
+ static void paint_play(void) {
463
+ sms_vdp_display_off();
464
+ paint_blank_field();
465
+ paint_frame_chrome(0);
466
+ repaint_well(0);
467
+ if (two_player) {
468
+ paint_frame_chrome(1);
469
+ repaint_well(1);
470
+ text_draw(11, 15, "VS");
471
+ }
472
+ draw_hud();
473
+ sms_sprite_init();
474
+ sms_sat_upload();
475
+ sms_vdp_display_on();
476
+ }
477
+
478
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
479
+ * Match scan: mark every straight run of 3+ same-coloured gems in all 4
480
+ * directions (a cell can belong to several runs — the mask de-dupes), and
481
+ * return how many cells matched. Runs flat-out on the Z80 over 72 cells — no
482
+ * need to smear it across frames like the cc65 NES version. */
149
483
  static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
150
484
 
151
- static uint8_t mark_and_count(void) {
152
- uint8_t r, c, d, len, k, cnt;
153
- uint8_t col;
485
+ static uint8_t mark_and_count(uint8_t p) {
486
+ uint8_t r, c, d, len, k, cnt, col;
154
487
  int8_t dr, dc;
155
488
  int sr, sc;
156
489
  cnt = 0;
157
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
158
- for (r = 0; r < ROWS; r++) {
159
- for (c = 0; c < COLS; c++) {
160
- col = grid[r][c];
161
- if (col == 0) continue;
490
+ for (r = 0; r < GRID_H; r++)
491
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
492
+ for (r = 0; r < GRID_H; r++) {
493
+ for (c = 0; c < GRID_W; c++) {
494
+ col = grid[p][r][c];
495
+ if (col == EMPTY) continue;
162
496
  for (d = 0; d < 4; d++) {
163
497
  dr = DIRS4[d][0]; dc = DIRS4[d][1];
164
498
  sr = (int)r - dr; sc = (int)c - dc;
165
- if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
166
- && grid[sr][sc] == col) continue; /* not the run's start */
499
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
500
+ && grid[p][sr][sc] == col) continue; /* not the run's start */
167
501
  len = 1;
168
502
  sr = (int)r + dr; sc = (int)c + dc;
169
- while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
170
- && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
503
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
504
+ && grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
171
505
  if (len >= 3) {
172
506
  sr = r; sc = c;
173
507
  for (k = 0; k < len; k++) {
@@ -181,119 +515,325 @@ static uint8_t mark_and_count(void) {
181
515
  return cnt;
182
516
  }
183
517
 
184
- /* collapse each column so survivors rest on the floor (in place: walk
185
- * from the bottom, copying gems down to a write cursor, then zero above) */
186
- static void apply_gravity(void) {
518
+ /* Collapse each column so survivors rest on the floor (walk from the bottom,
519
+ * copying gems down to a write cursor, then zero everything above it). */
520
+ static void apply_gravity(uint8_t p) {
187
521
  uint8_t c;
188
- int r, w;
189
- for (c = 0; c < COLS; c++) {
190
- w = ROWS - 1;
191
- for (r = ROWS - 1; r >= 0; r--) {
192
- if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
522
+ int8_t r, w;
523
+ for (c = 0; c < GRID_W; c++) {
524
+ w = GRID_H - 1;
525
+ for (r = GRID_H - 1; r >= 0; r--) {
526
+ if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
193
527
  }
194
- for (; w >= 0; w--) grid[w][c] = 0;
528
+ for (; w >= 0; w--) grid[p][w][c] = EMPTY;
195
529
  }
196
530
  }
197
531
 
198
- static void resolve_board(void) {
532
+ /* Forward decls — game_over/garbage_insert/spawn_piece reference each other. */
533
+ static void game_over(void);
534
+
535
+ /* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
536
+ * Returns the chain depth (0 = the lock matched nothing). */
537
+ static uint8_t resolve_board(uint8_t p) {
199
538
  uint8_t n, r, c, chain;
200
- unsigned int amt;
539
+ uint16_t amt;
201
540
  chain = 0;
202
- while (1) {
203
- n = mark_and_count();
541
+ for (;;) {
542
+ n = mark_and_count(p);
204
543
  if (n == 0) break;
205
- chain++;
206
- for (r = 0; r < ROWS; r++)
207
- for (c = 0; c < COLS; c++)
208
- if (matched[r][c]) grid[r][c] = 0;
209
- amt = (unsigned int)n * 10u;
210
- if (chain > 1) amt = amt * chain;
211
- if (score < 65500u) score = (uint16_t)(score + amt);
212
- sfx_tone(0, 200, 10); /* clear chime */
213
- apply_gravity();
544
+ ++chain;
545
+ for (r = 0; r < GRID_H; r++)
546
+ for (c = 0; c < GRID_W; c++)
547
+ if (matched[r][c]) grid[p][r][c] = EMPTY;
548
+ amt = (uint16_t)n * 10;
549
+ if (chain > 1) amt *= chain; /* cascades pay multiplied */
550
+ if (score[p] < 65000) score[p] += amt;
551
+ /* clear chime — pitch rises with chain depth (smaller divider = higher
552
+ * note on the PSG). Voice 0 doubles as an sfx voice over the music. */
553
+ sfx_tone(0, (uint16_t)(360 - ((uint16_t)chain << 5)), 10);
554
+ apply_gravity(p);
555
+ board_dirty[p] = 1;
556
+ hud_dirty = 1;
557
+ if (!two_player) {
558
+ cleared_total += n;
559
+ while (level < 9 && cleared_total >= (uint16_t)level * 10) ++level;
560
+ }
214
561
  }
562
+ return chain;
215
563
  }
216
564
 
217
- static void lock_piece(void) {
218
- uint8_t i;
565
+ /* ── GAME LOGIC (clay) — VERSUS attack: garbage rows rise from the bottom of
566
+ * the victim's well (random gems with one gap — matchable, so a skilled
567
+ * victim digs out). The victim's stack rising means the falling trio shifts
568
+ * up one to stay board-aligned; if the top row is already occupied, the
569
+ * victim tops out and loses. ── */
570
+ static void garbage_insert(uint8_t v, uint8_t nrows) {
571
+ uint8_t k, c, gap;
219
572
  int8_t r;
573
+ sfx_noise(8); /* incoming-garbage thud */
574
+ for (k = 0; k < nrows; k++) {
575
+ for (c = 0; c < GRID_W; c++) {
576
+ if (grid[v][0][c] != EMPTY) { loser = v; game_over(); return; }
577
+ }
578
+ for (r = 0; r < GRID_H - 1; r++)
579
+ for (c = 0; c < GRID_W; c++)
580
+ grid[v][r][c] = grid[v][r + 1][c];
581
+ gap = random8() % GRID_W;
582
+ for (c = 0; c < GRID_W; c++)
583
+ grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (uint8_t)(1 + random8() % 3);
584
+ if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
585
+ }
586
+ board_dirty[v] = 1;
587
+ }
588
+
589
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
590
+ * (pieces enter from above); below the floor or on a gem is not. */
591
+ static uint8_t can_place(uint8_t p, int8_t x, int8_t y) {
592
+ int8_t i, cy;
593
+ if (x < 0 || x >= GRID_W) return 0;
220
594
  for (i = 0; i < 3; i++) {
221
- r = (int8_t)(piece_y + i);
222
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
595
+ cy = (int8_t)(y + i);
596
+ if (cy < 0) continue;
597
+ if (cy >= GRID_H) return 0;
598
+ if (grid[p][cy][x] != EMPTY) return 0;
223
599
  }
224
- resolve_board();
225
- draw_grid();
600
+ return 1;
601
+ }
602
+
603
+ static void spawn_piece(uint8_t p) {
604
+ piece_x[p] = GRID_W / 2;
605
+ piece_y[p] = -2;
606
+ piece_col[p][0] = (uint8_t)(1 + random8() % 3);
607
+ piece_col[p][1] = (uint8_t)(1 + random8() % 3);
608
+ piece_col[p][2] = (uint8_t)(1 + random8() % 3);
609
+ if (!can_place(p, piece_x[p], piece_y[p])) { loser = p; game_over(); }
226
610
  }
227
611
 
228
- static void draw_piece(uint8_t clear) {
229
- uint8_t i;
612
+ /* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
613
+ static void lock_piece(uint8_t p) {
614
+ int8_t i, y;
615
+ uint8_t chain;
230
616
  for (i = 0; i < 3; i++) {
231
- int8_t r = (int8_t)(piece_y + i);
232
- uint8_t v;
233
- if (r < 0 || r >= ROWS) continue;
234
- v = clear ? grid[r][piece_x] : piece[i];
235
- draw_cell(piece_x, r, v);
617
+ y = (int8_t)(piece_y[p] + i);
618
+ if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
619
+ }
620
+ board_dirty[p] = 1;
621
+ sfx_tone(1, 200, 4); /* lock thunk (low note) */
622
+ if (piece_y[p] < 0) { loser = p; game_over(); return; } /* locked above rim */
623
+ chain = resolve_board(p);
624
+ if (state != ST_PLAY) return;
625
+ if (chain && two_player) {
626
+ garbage_insert(p ^ 1, chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
627
+ if (state != ST_PLAY) return; /* garbage topped them out */
236
628
  }
629
+ spawn_piece(p);
237
630
  }
238
631
 
239
- void main(void) {
240
- uint8_t prev = 0;
241
- uint8_t r, c;
632
+ /* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
633
+ * (one cell per press), held DOWN soft-drops, button 1 cycles the trio's
634
+ * colours (the classic trio "rotate"), button 2 hard-drops. P2 reads PORT B. ── */
635
+ static void update_player(uint8_t p) {
636
+ uint8_t pad, fresh, t, fd;
637
+ pad = p ? sms_joypad_read_p2() : sms_joypad_read();
638
+ fresh = (uint8_t)(pad & ~prev_pad[p]);
639
+ prev_pad[p] = pad;
640
+ if ((fresh & JOY_LEFT) && can_place(p, (int8_t)(piece_x[p] - 1), piece_y[p]))
641
+ --piece_x[p];
642
+ if ((fresh & JOY_RIGHT) && can_place(p, (int8_t)(piece_x[p] + 1), piece_y[p]))
643
+ ++piece_x[p];
644
+ if (fresh & JOY_B1) { /* cycle colours downward */
645
+ t = piece_col[p][2];
646
+ piece_col[p][2] = piece_col[p][1];
647
+ piece_col[p][1] = piece_col[p][0];
648
+ piece_col[p][0] = t;
649
+ sfx_tone(1, 320, 3);
650
+ }
651
+ if (fresh & JOY_B2) { /* hard drop */
652
+ while (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1))) ++piece_y[p];
653
+ lock_piece(p); /* may end the game */
654
+ return;
655
+ }
656
+ if (pad & JOY_DOWN) fall_t[p] += 4; /* soft drop */
657
+ ++fall_t[p];
658
+ fd = two_player ? VS_FALL_DELAY
659
+ : (uint8_t)(32 - ((level << 1) + level)); /* 29..5 */
660
+ if (fall_t[p] >= fd) {
661
+ fall_t[p] = 0;
662
+ if (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1)))
663
+ ++piece_y[p];
664
+ else
665
+ lock_piece(p); /* may end the game */
666
+ }
667
+ }
242
668
 
243
- sms_vdp_init();
244
- sms_load_palette(palette);
245
- sms_load_tiles(0x0000, bg_tiles, 32 * 6);
669
+ /* ── GAME LOGIC (clay) — stage this frame's sprites. Only the falling trios
670
+ * are sprites (locked gems are BG tiles): 3 SAT slots per player. Cells above
671
+ * the rim aren't drawn — they'd poke out from under the HUD strip.
672
+ * Slot map: 0-2 = P1 trio, 3-5 = P2 trio. Inactive slots park at Y=$E0
673
+ * (below the 192-line area). NEVER park at Y=$D0 — that's the SAT terminator:
674
+ * the VDP stops scanning at the first $D0 and every later slot vanishes. ── */
675
+ static void stage_sprites(void) {
676
+ uint8_t p, i, slot;
677
+ for (p = 0; p < 2; p++) {
678
+ uint8_t active = (state == ST_PLAY) && (p == 0 || two_player);
679
+ for (i = 0; i < 3; i++) {
680
+ int8_t r = (int8_t)(piece_y[p] + (int8_t)i);
681
+ uint8_t col = piece_col[p][i] ? piece_col[p][i] : 1;
682
+ slot = (uint8_t)(p * 3 + i);
683
+ if (active && r >= 0)
684
+ sms_sprite_set(slot,
685
+ (uint8_t)((well_tx[p] + piece_x[p]) << 3),
686
+ (uint8_t)((WELL_TY + r) << 3),
687
+ (uint8_t)(T_SPR + col - 1));
688
+ else
689
+ sms_sprite_set(slot, 0, 0xE0, T_SPR); /* parked below the screen */
690
+ }
691
+ }
692
+ }
246
693
 
247
- for (r = 0; r < 24; r++) for (c = 0; c < 32; c++) sms_set_tilemap_cell(r, c, T_BLANK, 0);
248
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) grid[r][c] = 0;
694
+ /* ── GAME LOGIC (clay) end of game (top-out). `loser` topped out. ── */
695
+ static void game_over(void) {
696
+ uint16_t best = score[0];
697
+ if (two_player && score[1] > best) best = score[1];
698
+ if (best > hiscore) {
699
+ hiscore = best;
700
+ hiscore_save(hiscore); /* cart RAM (real hardware); WRAM copy is live */
701
+ }
702
+ sfx_noise(20); /* game-over rumble */
703
+ state = ST_OVER;
704
+ board_dirty[0] = board_dirty[1] = 0; /* play field is frozen now */
705
+ prev_pad[0] = prev_pad[1] = 0xFF; /* require a fresh press */
706
+ over_step = 4; /* results text, one per vblank
707
+ * (each draw_u16 is 5 software
708
+ * divisions — see the BUDGET
709
+ * FOOTGUN at the main loop) */
710
+ }
249
711
 
250
- score = 0;
251
- fall_timer = 0;
252
- new_piece();
253
- draw_well();
254
- draw_grid();
712
+ /* ── GAME LOGIC (clay) — start a run ── */
713
+ static void start_game(uint8_t versus) {
714
+ uint8_t p, r, c;
715
+ two_player = versus;
716
+ well_tx[0] = versus ? WELL_VS_P1 : WELL_1P_TX;
717
+ well_tx[1] = WELL_VS_P2;
718
+ /* Stir the PRNG with time-spent-on-title so runs differ. */
719
+ rng ^= (uint16_t)((uint16_t)PORT_V_COUNTER << 3);
720
+ if (rng == 0) rng = 0xACE1;
721
+ for (p = 0; p < 2; p++) {
722
+ for (r = 0; r < GRID_H; r++)
723
+ for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
724
+ fall_t[p] = 0;
725
+ score[p] = 0;
726
+ prev_pad[p] = 0xFF; /* the button that started the game
727
+ * shouldn't also rotate the first trio */
728
+ }
729
+ cleared_total = 0;
730
+ level = 1;
731
+ state = ST_PLAY;
732
+ paint_play();
733
+ spawn_piece(0);
734
+ if (versus) spawn_piece(1);
735
+ sfx_tone(0, 200, 10); /* start jingle */
736
+ }
255
737
 
738
+ void main(void) {
739
+ uint8_t pad, fresh;
740
+
741
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
742
+ * Init order: VDP regs (display off) → palette → tiles → name table → SAT →
743
+ * R10 → display on (which also enables the frame IRQ) → EI. The one hard
744
+ * rule: EI comes LAST, after every register is in place — the crt0 boots
745
+ * with DI and the FIRST halt would hang forever if interrupts were never
746
+ * enabled. */
747
+ sms_vdp_init(); /* R0=0x36 already has IE1 (line IRQ) set */
748
+ sms_load_palette(palette);
749
+ load_font();
750
+ sms_load_tiles((uint16_t)(BG_GEM_BASE * 32), gem_furniture, 32 * 6);
751
+ sms_load_tiles(0x2000, sprite_tiles, 32 * 3);
752
+ sms_sprite_init();
256
753
  sfx_init();
257
754
  music_init();
258
- music_play(0); /* continuous background music ("no sound" was the playtest verdict) */
259
- sms_vdp_display_on();
755
+ music_play(0);
260
756
 
261
- do {
262
- uint8_t pad, fall_rate, t;
263
- sms_vblank_wait();
264
- sfx_update();
265
- music_update();
266
- draw_piece(1);
267
-
268
- pad = sms_joypad_read();
269
- if ((pad & JOY_LEFT) && !(prev & JOY_LEFT)
270
- && !collides((int8_t)(piece_x - 1), piece_y)) piece_x--;
271
- if ((pad & JOY_RIGHT) && !(prev & JOY_RIGHT)
272
- && !collides((int8_t)(piece_x + 1), piece_y)) piece_x++;
273
- if ((pad & JOY_B1) && !(prev & JOY_B1)) {
274
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
275
- sfx_tone(1, 350, 2);
276
- }
277
- if ((pad & JOY_B2) && !(prev & JOY_B2)) {
278
- while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
279
- lock_piece();
280
- new_piece();
281
- prev = pad;
757
+ /* R10 = SPLIT_LINE arms the line counter: IRQ at the last bar line. Set
758
+ * once it reloads itself every underflow. */
759
+ sms_vdp_write_reg(10, SPLIT_LINE);
760
+
761
+ hiscore = hiscore_load(); /* cart RAM if present — else 0 */
762
+ state = ST_TITLE;
763
+ prev_pad[0] = prev_pad[1] = 0xFF;
764
+ paint_title();
765
+ __asm__("ei"); /* interrupts live from here on */
766
+
767
+ for (;;) {
768
+ if (state == ST_TITLE) {
769
+ /* ── GAME LOGIC (clay) title: button 1 = 1P, button 2 = 2P versus ── */
770
+ wait_vblank();
771
+ sfx_update();
772
+ music_update();
773
+ wait_split();
774
+ pad = sms_joypad_read();
775
+ fresh = (uint8_t)(pad & ~prev_pad[0]);
776
+ prev_pad[0] = pad;
777
+ if (fresh & JOY_B1) start_game(0);
778
+ else if (fresh & JOY_B2) start_game(1);
282
779
  continue;
283
780
  }
284
- prev = pad;
285
-
286
- fall_rate = (pad & JOY_DOWN) ? 4 : 30;
287
- fall_timer = (uint8_t)(fall_timer + 1);
288
- if (fall_timer >= fall_rate) {
289
- fall_timer = 0;
290
- if (collides(piece_x, (int8_t)(piece_y + 1))) {
291
- lock_piece();
292
- new_piece();
293
- } else {
294
- piece_y++;
781
+
782
+ if (state == ST_OVER) {
783
+ /* Freeze the boards; button 1 or 2 returns to the title. */
784
+ wait_vblank();
785
+ if (over_step) { /* deferred draws — one per vblank */
786
+ if (over_step == 4)
787
+ text_draw(8, 11, two_player ? (loser ? "P1 WINS" : "P2 WINS") : "GAME OVER");
788
+ else if (over_step == 3) { text_draw(11, 9, "P1"); draw_u16(11, 13, score[0]); }
789
+ else if (over_step == 2) { if (two_player) { text_draw(13, 9, "P2"); draw_u16(13, 13, score[1]); } }
790
+ else { text_draw(16, 9, "HI"); draw_u16(16, 13, hiscore); }
791
+ over_step--;
792
+ }
793
+ wait_split();
794
+ sfx_update();
795
+ music_update();
796
+ pad = sms_joypad_read();
797
+ fresh = (uint8_t)(pad & ~prev_pad[0]);
798
+ prev_pad[0] = pad;
799
+ if (fresh & (JOY_B1 | JOY_B2)) {
800
+ state = ST_TITLE;
801
+ prev_pad[0] = prev_pad[1] = 0xFF;
802
+ paint_title();
295
803
  }
804
+ continue;
296
805
  }
297
- draw_piece(0);
298
- } while (1);
806
+
807
+ /* ── ST_PLAY ─────────────────────────────────────────────────────────
808
+ * Frame shape: [vblank: SAT + dirty-well repaints + HUD writes] → [line
809
+ * IRQ at the bar] → [rest of frame: game logic]. VRAM traffic stays
810
+ * inside vblank; logic runs while the VDP draws the field.
811
+ *
812
+ * BUDGET FOOTGUN (inherited from the shmup, which found it the hard way):
813
+ * everything between wait_vblank() and wait_split() must finish before the
814
+ * line IRQ at line 23 — vblank (70 lines) + the HUD strip (23) ≈ 21k
815
+ * cycles. The SAT upload eats ~7k of that. An unconditional HUD redraw (10
816
+ * software 16-bit divisions for the digits) blows the budget when it lands
817
+ * the SAME frame as a 72-cell well repaint. So we GATE both behind dirty
818
+ * flags — HUD redraws only when the score/level changed, wells repaint
819
+ * only when a lock/clear dirtied them — and they rarely coincide. */
820
+ wait_vblank();
821
+ sms_sat_upload(); /* shadow SAT staged at end of last frame */
822
+ if (board_dirty[0]) { board_dirty[0] = 0; repaint_well(0); }
823
+ if (two_player && board_dirty[1]) { board_dirty[1] = 0; repaint_well(1); }
824
+ if (hud_dirty) { hud_dirty = 0; draw_hud(); }
825
+ sfx_update();
826
+ music_update();
827
+ wait_split(); /* the line-interrupt split — every frame */
828
+
829
+ /* ── GAME LOGIC (clay — reshape freely) — both players update EVERY frame
830
+ * (simultaneous versus, not alternating turns). Any update can end the
831
+ * game, so re-check state between them. */
832
+ update_player(0);
833
+ if (two_player && state == ST_PLAY) update_player(1);
834
+
835
+ /* Stage the SAT shadow NOW (RAM only — cheap, any time); the actual VRAM
836
+ * upload waits for the next vblank at the top of the loop. */
837
+ stage_sprites();
838
+ }
299
839
  }