romdevtools 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,248 +1,1202 @@
1
- /* ── puzzle.c — Game Boy match-3 falling-block scaffold ─────────────
1
+ /* ── puzzle.c — CHROMA WELL: Game Boy Color falling-jewel matcher (complete example game) ──
2
2
  *
3
- * Mirrors the NES/Genesis/SNES puzzle scaffolds. 6-wide × 12-tall
4
- * grid drawn via BG tilemap (each cell = 1 BG tile). 1×3 vertical
5
- * active piece; LEFT/RIGHT shifts, A rotates colour order, DOWN
6
- * soft-drops, START hard-drops. Horizontal triples clear and score.
3
+ * A COMPLETE, working game title screen, persistent battery hi-score
4
+ * (MBC1+RAM+BATTERY SRAM), music + SFX, and the GBC's signature feature:
5
+ * TRUE per-tile color. Six jewel types are six REAL CGB palettes (15-bit
6
+ * BGR, loaded through BCPS/BCPD + OCPS/OCPD), selected per BG cell through
7
+ * the VRAM bank-1 attribute map and per sprite through OAM attribute bits —
8
+ * not a colorized monochrome game.
7
9
  *
8
- * On the Game Boy we don't have a built-in font, so we render the
9
- * grid as coloured tile cells using three BG tile shapes (R/G/B
10
- * stripes). Score is kept in WRAM; rendering a numeric HUD requires
11
- * a digit-tile blob left as an extension.
10
+ * THE GAME: a vertical column of 3 jewels falls into an 8-wide x 15-tall
11
+ * well. Move it left/right, soft-drop (Down), hard-drop (Start), and CYCLE
12
+ * the three colors (A/B). Line up 3+ of one color horizontally, vertically,
13
+ * or diagonally to clear; gravity pulls survivors down, which can chain.
14
+ * Every 18th piece is a MAGIC jewel that clears every gem of the color it
15
+ * lands on. SELECT toggles the music.
16
+ *
17
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
18
+ * very different one. The markers tell you what's what:
19
+ * HARDWARE IDIOM (load-bearing) — dodges a documented GB/GBC footgun;
20
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
21
+ * GAME LOGIC (clay) — board rules, scoring, tuning, art: reshape freely.
22
+ *
23
+ * SINGLE-PLAYER, honestly: the Game Boy's "player 2" is a LINK CABLE, which
24
+ * one emulator instance cannot provide — so handheld examples ship a
25
+ * press-start title and no 2P mode instead of faking one.
26
+ *
27
+ * What depends on what:
28
+ * gb_runtime.{h,c} — vblank/joypad/OAM-DMA/sound library (shared with GB).
29
+ * gb_crt0.s — boot + header window. It DECLARES the cart as
30
+ * MBC1+RAM+BATTERY ($0147=$03, $0149=$02): that header is what makes
31
+ * the SRAM hi-score persist (the GB equivalent of the NES BATTERY bit).
32
+ * font.h — 0-9 A-Z glyphs for all text.
33
+ *
34
+ * RENDERING — the hard-won architecture (details at each routine below):
35
+ * - The FALLING column and the NEXT preview are OBJ sprites (OAM), not BG
36
+ * tiles, so moving them is just an OAM rewrite — no per-frame BG writes.
37
+ * - The LOCKED well is BG tiles, updated through a COLLECT/FLUSH queue:
38
+ * redraw_collect() decides what to write (RAM only); redraw_flush()
39
+ * writes a few cells to VRAM as the very first thing in vblank. The whole
40
+ * per-frame job (OAM DMA + flush) MUST finish inside the ~10-line vblank
41
+ * window — overrunning into active display silently DROPS writes on this
42
+ * core. An idle "scrub" continuously repaints the well from the grid so
43
+ * nothing can drift.
44
+ * - The HUD (score / hi-score / level) lives on the WINDOW layer — a fixed
45
+ * strip at the bottom of the screen, immune to BG scrolling.
46
+ * - We NEVER toggle the LCD in-game (this core blanks the whole frame on
47
+ * any LCDC bit-7 toggle — a strobe). LCD-off is used only for the
48
+ * full-screen title <-> game transitions.
49
+ *
50
+ * WRAM NOTE: build with dataLoc:0xC200 so our statics sit ABOVE shadow_oam
51
+ * ($C100) — else oam_clear() would zero our state (RNG seed / grid). The
52
+ * project build recipe sets that automatically.
12
53
  */
13
-
14
54
  #include "gb_hardware.h"
15
55
  #include "gb_runtime.h"
56
+ #include "font.h"
57
+
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 "CHROMA WELL"
61
+
62
+ /* ── GAME LOGIC (clay — reshape freely) ── board geometry */
63
+ #define COLS 8
64
+ #define ROWS 15 /* rows 0-14; floor at map row 15; window HUD rows 16-17 */
65
+ #define NCELL (ROWS * COLS)
66
+ #define NCOLORS 6 /* jewel colors 1..6 — one CGB palette each */
67
+
68
+ /* BG map cell of interior grid cell (0,0) — the well's top-left corner.
69
+ * Open at the top (row 0); walls one cell outside left/right, floor below. */
70
+ #define WELL_MX 1
71
+ #define WELL_MY 0
72
+
73
+ /* BG map column where the right-hand panel (NEXT preview) starts. */
74
+ #define HUD_X 12
75
+
76
+ #define G(r,c) grid[((r) * COLS) + (c)]
77
+ #define M(r,c) matched[((r) * COLS) + (c)]
78
+
79
+ #define T_EMPTY 0
80
+ #define T_GEM 1
81
+ #define T_WALL 2
82
+ #define T_BLANK 3
83
+ #define T_MAGIC 4
84
+ #define T_EXP0 5 /* explosion frames: gem bursting apart (its own color) */
85
+ #define T_EXP1 6
86
+ #define T_EXP2 7
87
+ #define FONT_BASE 16
88
+
89
+ #define MAGIC 7
16
90
 
17
- #define COLS 6
18
- #define ROWS 12
19
-
20
- #define T_BLANK 0
21
- #define T_R 1
22
- #define T_G 2
23
- #define T_B 3
24
- #define T_WALL 4
25
-
26
- /* tile_blank is the EMPTY-cell / backdrop tile. It is NOT all-zero: a
27
- * subtle dither (colour 0 + faint colour 1) so the empty playfield and the
28
- * area around the well read as a textured surface, never one flat colour
29
- * (the #1 GB "why is it blank" footgun). Locked blocks / the active piece
30
- * overdraw it with the R/G/B shape tiles. */
31
- static const uint8_t tile_blank[16] = {
91
+ #define PAL_WELL 6
92
+ #define PAL_OUT 7
93
+
94
+ #define ST_TITLE 0
95
+ #define ST_PLAY 1
96
+ #define ST_OVER 2
97
+
98
+ #define VRAM ((volatile uint8_t *)0x9800)
99
+ /* The window layer fetches from the $9C00 map — offset $400 past $9800 in
100
+ * the same VRAM pointer (see the WINDOW HUD idiom below). */
101
+ #define WIN_OFF 0x400
102
+
103
+ /* ── GAME LOGIC (clay reshape freely) ── tile pixel data (2bpp).
104
+ * Each 8x8 tile = 16 bytes, 2 bytes per row (plane 0 then plane 1); a pixel's
105
+ * 2-bit color value indexes into whichever CGB palette the cell's attribute
106
+ * byte (BG) or the sprite's OAM attr (OBJ) selects. ONE gem tile becomes six
107
+ * different-colored gems purely through palette selection. */
108
+ static const uint8_t tile_empty[16] = {
32
109
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
33
110
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
34
111
  };
35
- /* Well frame: a solid colour-2 border drawn around the play area. */
112
+ static const uint8_t tile_gem[16] = {
113
+ 0x00,0x3C, 0x30,0x4E, 0x60,0x9F, 0x40,0xBF,
114
+ 0x02,0xFF, 0x06,0xFF, 0x1C,0x7E, 0x00,0x3C,
115
+ };
36
116
  static const uint8_t tile_wall[16] = {
37
117
  0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
38
118
  0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
39
119
  };
40
- /* Three distinct tile shapes (since GB BG is 2bpp, we differentiate
41
- * by *shape*, not colour-on-CGB). The CGB palette path could give us
42
- * real colours; for DMG-compatibility we use shape. */
43
- static const uint8_t tile_r[16] = {
44
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
45
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
120
+ static const uint8_t tile_blank[16] = { 0 };
121
+ static const uint8_t tile_magic[16] = {
122
+ 0x18,0x18, 0x3C,0x3C, 0x7E,0x7E, 0xFF,0xFF,
123
+ 0x7E,0x7E, 0x3C,0x3C, 0x18,0x18, 0x00,0x00,
124
+ };
125
+ /* explosion frames (value 2, drawn in the gem's own colour so they blend with
126
+ * the well — value 0 = C_WELL). The gem bursts into a star, fragments fly
127
+ * outward, then sparks, then gone. Shown ONCE, expanding — no blinking. */
128
+ static const uint8_t tile_exp0[16] = {
129
+ 0x00,0x99, 0x00,0x5A, 0x00,0x3C, 0x00,0xFF,
130
+ 0x00,0xFF, 0x00,0x3C, 0x00,0x5A, 0x00,0x99,
131
+ };
132
+ static const uint8_t tile_exp1[16] = {
133
+ 0x00,0x81, 0x00,0x42, 0x00,0x24, 0x00,0x18,
134
+ 0x00,0x18, 0x00,0x24, 0x00,0x42, 0x00,0x81,
46
135
  };
47
- static const uint8_t tile_g[16] = {
48
- 0xAA,0x55, 0xAA,0x55, 0xAA,0x55, 0xAA,0x55,
49
- 0xAA,0x55, 0xAA,0x55, 0xAA,0x55, 0xAA,0x55,
136
+ static const uint8_t tile_exp2[16] = {
137
+ 0x00,0x81, 0x00,0x00, 0x00,0x00, 0x00,0x00,
138
+ 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x81,
50
139
  };
51
- static const uint8_t tile_b[16] = {
52
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
53
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
140
+
141
+ /* ── GAME LOGIC (clay — reshape freely) ── the palette TABLE (the colors
142
+ * themselves are art; the LOADER below is the hardware idiom).
143
+ * 15-bit BGR: 5 bits each, blue in the high bits — RGB() packs it. */
144
+ #define RGB(r,g,b) ((uint16_t)(((uint16_t)(b)<<10)|((uint16_t)(g)<<5)|(r)))
145
+ #define C_WELL RGB(4,6,12)
146
+ #define C_OUT RGB(1,2,4)
147
+ #define C_FAINT RGB(7,9,15)
148
+ #define C_FRAME RGB(16,20,28)
149
+
150
+ static const uint16_t palettes[8][4] = {
151
+ /* 0 red */ { C_WELL, RGB(31,16,16), RGB(31,3,3), RGB(17,1,1) },
152
+ /* 1 orange */ { C_WELL, RGB(31,24,12), RGB(31,16,2), RGB(20,9,0) },
153
+ /* 2 yellow */ { C_WELL, RGB(31,31,18), RGB(30,28,4), RGB(22,18,0) },
154
+ /* 3 green */ { C_WELL, RGB(16,31,16), RGB(6,26,8), RGB(1,16,4) },
155
+ /* 4 blue */ { C_WELL, RGB(14,22,31), RGB(5,12,31), RGB(2,5,20) },
156
+ /* 5 purple */ { C_WELL, RGB(28,16,31), RGB(20,5,30), RGB(12,1,20) },
157
+ /* 6 well */ { C_WELL, C_FAINT, RGB(8,11,18), C_FRAME },
158
+ /* 7 out/txt*/ { C_OUT, RGB(2,3,7), C_OUT, RGB(31,31,31) },
54
159
  };
55
160
 
56
- static const uint16_t bg_palette[4] = { 0x7FFF, 0x5294, 0x294A, 0x0000 };
161
+ /* ── GAME LOGIC (clay reshape freely) ── game state */
162
+ static uint8_t grid[NCELL]; /* the well: 0 = empty, 1..NCOLORS = a gem */
163
+ static uint8_t matched[NCELL]; /* scratch: cells flagged for clearing */
164
+ static uint8_t shadow[NCELL]; /* color currently on the BG, for diff redraw */
165
+ static uint8_t piece[3]; /* the 3 falling colors, top→bottom */
166
+ static uint8_t nextp[3]; /* the previewed next column */
167
+ static uint8_t piece_x, piece_y; /* well coords of the falling column's top */
168
+ static uint8_t piece_active; /* a column is currently falling */
169
+ static uint8_t piece_magic; /* the falling column is a MAGIC piece */
170
+ static uint8_t next_dirty; /* NEXT-preview sprites need re-writing */
171
+ static uint8_t piece_counter; /* pieces since last magic (→ magic every 18) */
172
+ static uint8_t fall_timer; /* frames since the column last stepped down */
173
+ static uint8_t cur_fall_rate; /* frames per downward step (lower = faster) */
174
+ static uint16_t total_cleared; /* gems cleared this game (drives level) */
175
+ static uint8_t level;
176
+ static uint8_t score_d[6]; /* 6-digit BCD score, most significant first */
177
+ static uint8_t hi_d[6]; /* 6-digit BCD hi-score (battery SRAM) */
178
+ static uint8_t state; /* ST_TITLE / ST_PLAY / ST_OVER */
179
+ static uint8_t chain; /* cascade depth of the current resolve */
180
+ static uint16_t rng = 0xACE1; /* xorshift PRNG state */
57
181
 
58
- static uint8_t grid[ROWS][COLS];
59
- static uint8_t piece[3];
60
- static int16_t piece_x, piece_y;
61
- static uint8_t fall_timer;
62
- static uint16_t score;
63
- static uint32_t rng = 1;
182
+ /* the 4 line directions we scan for matches: horizontal, vertical, and the
183
+ * two diagonals (we only walk each line once, from its lowest cell). */
184
+ static const int8_t DIRS[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
64
185
 
65
- static uint32_t xorshift(void) {
66
- rng ^= rng << 13;
67
- rng ^= rng >> 17;
68
- rng ^= rng << 5;
69
- return rng;
186
+ /* 16-bit xorshift PRNG — kept 16-bit on purpose (sm83 has no fast 32-bit
187
+ * shifts; a wider generator there degenerates toward one value). */
188
+ static uint8_t xorshift(void) {
189
+ rng ^= rng << 7;
190
+ rng ^= rng >> 9;
191
+ rng ^= rng << 8;
192
+ return (uint8_t)(rng >> 8);
70
193
  }
71
194
 
72
- static uint8_t random_colour(void) { return 1 + (xorshift() % 3); }
195
+ /* fill a 3-jewel column with random colors 1..NCOLORS */
196
+ static void roll(uint8_t *p) {
197
+ p[0] = 1 + (uint8_t)(xorshift() % NCOLORS);
198
+ p[1] = 1 + (uint8_t)(xorshift() % NCOLORS);
199
+ p[2] = 1 + (uint8_t)(xorshift() % NCOLORS);
200
+ }
73
201
 
74
- static void new_piece(void) {
75
- piece[0] = random_colour();
76
- piece[1] = random_colour();
77
- piece[2] = random_colour();
78
- piece_x = COLS / 2 - 1;
79
- piece_y = -3;
202
+ /* add to the 6-digit BCD score (score_d[0] = most significant), with carry */
203
+ static void add_score(uint16_t amt) {
204
+ uint8_t k, idx;
205
+ uint16_t carry = amt;
206
+ for (k = 0; k < 6; k++) {
207
+ if (carry == 0) break;
208
+ idx = 5 - k;
209
+ carry += score_d[idx];
210
+ score_d[idx] = (uint8_t)(carry % 10);
211
+ carry = carry / 10;
212
+ }
80
213
  }
81
214
 
82
- static uint8_t tile_for(uint8_t c) {
83
- switch (c) {
84
- case 1: return T_R;
85
- case 2: return T_G;
86
- case 3: return T_B;
87
- default: return T_BLANK;
215
+ /* most-significant-digit-first BCD compare: did this run beat the record? */
216
+ static uint8_t score_beats_hi(void) {
217
+ uint8_t i;
218
+ for (i = 0; i < 6; i++) {
219
+ if (score_d[i] > hi_d[i]) return 1;
220
+ if (score_d[i] < hi_d[i]) return 0;
88
221
  }
222
+ return 0;
89
223
  }
90
224
 
91
- static void draw_cell(int16_t col, int16_t row, uint8_t cell) {
92
- /* Map base $9800, 32 cells wide. Centre the 6-col grid offset +7. */
93
- uint8_t *map = (uint8_t *)0x9800;
94
- if (row < 0 || row >= ROWS) return;
95
- map[(row + 1) * 32 + (col + 7)] = tile_for(cell);
225
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
226
+ * BATTERY SRAM hi-score persistent saves on a Game Boy cart.
227
+ * requires: gb_crt0.s declaring MBC1+RAM+BATTERY in the cartridge header
228
+ * ($0147=$03, $0149=$02 8KB at $A000-$BFFF). With a ROM-only header the
229
+ * $A000 region is OPEN BUS: writes vanish, reads return garbage, and
230
+ * nothing tells you why. The header is the save system.
231
+ *
232
+ * The MBC powers up with cart RAM DISABLED (protection against corrupting
233
+ * the battery RAM with stray bus traffic while power rails settle). The
234
+ * $0A-enable dance:
235
+ * 1. write $0A to anywhere in $0000-$1FFF → RAM enabled
236
+ * 2. read/write $A000-$BFFF → real battery RAM
237
+ * 3. write $00 to $0000-$1FFF → RAM disabled again
238
+ * ALWAYS re-disable after access — that's what makes a yanked cartridge /
239
+ * dying battery corrupt at most the bytes mid-write, not the whole save.
240
+ *
241
+ * First boot is GARBAGE, not zeros: battery RAM holds whatever the silicon
242
+ * woke up with. The magic bytes + XOR checksum below are how the load path
243
+ * tells "my save" from "factory noise" — without them a fresh cart shows a
244
+ * junk hi-score like 974382.
245
+ *
246
+ * Save block at $A000: 'H' 'S' d0 d1 d2 d3 d4 d5 ck
247
+ * (6 BCD digits, most significant first; ck = d0^..^d5^$A5)
248
+ * No timing constraints — SRAM is not VRAM; access it any time. */
249
+ #define SRAM_BASE ((volatile uint8_t *)0xA000)
250
+ #define MBC_RAMG (*(volatile uint8_t *)0x0000) /* MBC1 RAM-gate register */
251
+
252
+ static void hiscore_load(void) {
253
+ uint8_t i, ck;
254
+ MBC_RAMG = 0x0A; /* enable cart RAM */
255
+ ck = 0xA5;
256
+ for (i = 0; i < 6; i++) ck ^= SRAM_BASE[2 + i];
257
+ if (SRAM_BASE[0] == 'H' && SRAM_BASE[1] == 'S' && SRAM_BASE[8] == ck) {
258
+ for (i = 0; i < 6; i++) {
259
+ hi_d[i] = SRAM_BASE[2 + i];
260
+ if (hi_d[i] > 9) hi_d[i] = 9; /* belt + braces on a bad digit */
261
+ }
262
+ } else {
263
+ for (i = 0; i < 6; i++) hi_d[i] = 0; /* first boot / corrupt → 0 */
264
+ }
265
+ MBC_RAMG = 0x00; /* ALWAYS re-disable */
96
266
  }
97
267
 
98
- static void draw_grid(void) {
99
- int16_t r, c;
100
- for (r = 0; r < ROWS; r++)
101
- for (c = 0; c < COLS; c++)
102
- draw_cell(c, r, grid[r][c]);
268
+ static void hiscore_save(void) {
269
+ uint8_t i, ck;
270
+ MBC_RAMG = 0x0A;
271
+ SRAM_BASE[0] = 'H';
272
+ SRAM_BASE[1] = 'S';
273
+ ck = 0xA5;
274
+ for (i = 0; i < 6; i++) {
275
+ SRAM_BASE[2 + i] = hi_d[i];
276
+ ck ^= hi_d[i];
277
+ }
278
+ SRAM_BASE[8] = ck;
279
+ MBC_RAMG = 0x00;
103
280
  }
104
281
 
105
- static uint8_t collides(int16_t col, int16_t row) {
106
- uint8_t i;
107
- int16_t r;
108
- if (col < 0 || col >= COLS) return 1;
109
- for (i = 0; i < 3; i++) {
110
- r = row + i;
111
- if (r >= ROWS) return 1;
112
- if (r >= 0 && grid[r][col] != 0) return 1;
282
+ /* ── GAME LOGIC (clay reshape freely) ── sound effects.
283
+ * A tiny note sequencer driving square channel 2 directly. Each note has
284
+ * a real volume-decay envelope (NR22) so it fades instead of clicking off
285
+ * (a hard NRx2=0 cut every note sounds like static). sfx_tick() advances
286
+ * one step per frame; multi-note effects become little arpeggios.
287
+ * GB period p ⇒ freq = 131072/(2048-p); higher p = higher note. */
288
+ #define P_C4 1548
289
+ #define P_G4 1714
290
+ #define P_A4 1750
291
+ #define P_C5 1797
292
+ #define P_E5 1849
293
+ #define P_G5 1881
294
+ #define P_A5 1899
295
+ #define P_C6 1923
296
+
297
+ /* NR21 duty: 0x40 = 25% (soft), 0x80 = 50% (full). NR22 vol/env byte:
298
+ * (volume<<4)|(0=decay)|envPace — bigger pace = slower fade. */
299
+ #define SFX_STEPS 4
300
+ static uint16_t sfx_p[SFX_STEPS];
301
+ static uint8_t sfx_v[SFX_STEPS];
302
+ static uint8_t sfx_d[SFX_STEPS];
303
+ static uint8_t sfx_f[SFX_STEPS];
304
+ static uint8_t sfx_n, sfx_i, sfx_t;
305
+
306
+ static void sfx_tick(void) {
307
+ if (sfx_i >= sfx_n) return;
308
+ if (sfx_t != 0) { sfx_t--; return; }
309
+ NR21 = sfx_d[sfx_i];
310
+ NR22 = sfx_v[sfx_i];
311
+ NR23 = (uint8_t)(sfx_p[sfx_i] & 0xFF);
312
+ NR24 = (uint8_t)(0x80 | (sfx_p[sfx_i] >> 8)); /* trigger (let envelope end it) */
313
+ sfx_t = sfx_f[sfx_i];
314
+ sfx_i++;
315
+ }
316
+
317
+ static void sfx_go(uint8_t n) { sfx_n = n; sfx_i = 0; sfx_t = 0; sfx_tick(); }
318
+
319
+ static void sfx_move(void) {
320
+ sfx_p[0] = P_A5; sfx_v[0] = 0x81; sfx_d[0] = 0x40; sfx_f[0] = 4;
321
+ sfx_go(1);
322
+ }
323
+ static void sfx_rotate(void) {
324
+ sfx_p[0] = P_C6; sfx_v[0] = 0x81; sfx_d[0] = 0x40; sfx_f[0] = 4;
325
+ sfx_go(1);
326
+ }
327
+ static void sfx_drop(void) {
328
+ sfx_p[0] = P_C5; sfx_v[0] = 0xC2; sfx_d[0] = 0x80; sfx_f[0] = 3;
329
+ sfx_p[1] = P_C4; sfx_v[1] = 0xC3; sfx_d[1] = 0x80; sfx_f[1] = 8;
330
+ sfx_go(2);
331
+ }
332
+ static void sfx_clear(void) { /* bright ascending C-E-G */
333
+ sfx_p[0] = P_C5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 4;
334
+ sfx_p[1] = P_E5; sfx_v[1] = 0xD2; sfx_d[1] = 0x80; sfx_f[1] = 4;
335
+ sfx_p[2] = P_G5; sfx_v[2] = 0xD3; sfx_d[2] = 0x80; sfx_f[2] = 8;
336
+ sfx_go(3);
337
+ }
338
+ static void sfx_chain(uint8_t n) { /* arpeggio whose top note rises per chain */
339
+ uint16_t top = (uint16_t)(P_C6 + (uint16_t)n * 6);
340
+ if (top > 1980) top = 1980;
341
+ sfx_p[0] = P_E5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 3;
342
+ sfx_p[1] = P_G5; sfx_v[1] = 0xD2; sfx_d[1] = 0x80; sfx_f[1] = 3;
343
+ sfx_p[2] = top; sfx_v[2] = 0xD3; sfx_d[2] = 0x80; sfx_f[2] = 8;
344
+ sfx_go(3);
345
+ }
346
+ static void sfx_over(void) { /* slow descending */
347
+ sfx_p[0] = P_A4; sfx_v[0] = 0xC3; sfx_d[0] = 0x80; sfx_f[0] = 10;
348
+ sfx_p[1] = P_G4; sfx_v[1] = 0xC3; sfx_d[1] = 0x80; sfx_f[1] = 10;
349
+ sfx_p[2] = P_C4; sfx_v[2] = 0xC5; sfx_d[2] = 0x80; sfx_f[2] = 24;
350
+ sfx_go(3);
351
+ }
352
+
353
+ /* ── GAME LOGIC (clay — reshape freely) ── background music.
354
+ * A looping square-wave lead on channel 1 (SFX live on channel 2, so they
355
+ * mix and the effects cut through the music). music_tick() plays one melody
356
+ * step every 12 frames, re-triggering ch1 at a steady volume. Toggle on/off
357
+ * with SELECT — defaults ON.
358
+ *
359
+ * The melody is the GB 11-bit period split into low/high BYTE arrays (NR13 +
360
+ * NR14 low 3 bits) — period p ⇒ freq 131072/(2048-p). hi == 0xFF marks a
361
+ * rest. Arpeggios over a C - Am - F - G chord loop, 8 steps each. */
362
+ static const uint8_t mel_lo[32] = {
363
+ 0x06,0x39,0x59,0x83, 0x59,0x39,0x06,0x00, /* C E G C6 G E C - */
364
+ 0xD6,0x06,0x39,0x6B, 0x39,0x06,0xD6,0x00, /* A C E A5 E C A - */
365
+ 0x88,0xD6,0x06,0x44, 0x06,0xD6,0x88,0x00, /* F A C F5 C A F - */
366
+ 0xB2,0xF7,0x21,0x59, 0x21,0xF7,0xB2,0x00, /* G B D G5 D B G - */
367
+ };
368
+ static const uint8_t mel_hi[32] = { /* high 3 bits; 0xFF = rest */
369
+ 0x07,0x07,0x07,0x07, 0x07,0x07,0x07,0xFF,
370
+ 0x06,0x07,0x07,0x07, 0x07,0x07,0x06,0xFF,
371
+ 0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
372
+ 0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
373
+ };
374
+ static uint8_t music_on;
375
+ static uint8_t music_idx;
376
+ static uint8_t music_timer;
377
+
378
+ static void music_note(uint8_t idx) {
379
+ uint8_t hi = mel_hi[idx];
380
+ if (hi == 0xFF) { NR12 = 0x00; NR14 = 0x80; return; } /* rest: silence ch1 */
381
+ NR10 = 0x00; /* no sweep */
382
+ NR11 = 0x80; /* 50% duty, no length counter */
383
+ NR12 = 0x90; /* volume 9, no envelope (steady lead) */
384
+ NR13 = mel_lo[idx];
385
+ NR14 = (uint8_t)(0x80 | hi); /* trigger + freq high bits */
386
+ }
387
+
388
+ static void music_tick(void) {
389
+ if (!music_on) return;
390
+ if (music_timer == 0) {
391
+ music_note(music_idx);
392
+ music_timer = 12;
393
+ if (++music_idx >= 32) music_idx = 0;
113
394
  }
395
+ music_timer--;
396
+ }
397
+
398
+ static void music_toggle(void) {
399
+ music_on = (uint8_t)(!music_on);
400
+ music_idx = 0;
401
+ music_timer = 0;
402
+ if (!music_on) { NR12 = 0x00; NR14 = 0x80; } /* kill the lead immediately */
403
+ }
404
+
405
+ /* ── GAME LOGIC (clay — reshape freely) ── board mechanics */
406
+
407
+ /* is grid cell (r,col) off the bottom or already filled? */
408
+ static uint8_t cell_blocked(uint8_t r, uint8_t col) {
409
+ if (r >= ROWS) return 1;
410
+ return grid[(uint8_t)(r * COLS + col)] ? 1 : 0;
411
+ }
412
+
413
+ /* would the 3-tall falling column collide if its top cell were at (col,topy)?
414
+ * Checks are unrolled (not a loop) — short indexed-read loops can miscompile on
415
+ * sm83, and this is the hottest correctness check in the game. */
416
+ static uint8_t collides(uint8_t col, uint8_t topy) {
417
+ if (col >= COLS) return 1;
418
+ if (cell_blocked(topy, col)) return 1;
419
+ if (cell_blocked((uint8_t)(topy + 1), col)) return 1;
420
+ if (cell_blocked((uint8_t)(topy + 2), col)) return 1;
114
421
  return 0;
115
422
  }
116
423
 
117
- static void lock_piece(void) {
424
+ static void game_over(void);
425
+
426
+ /* start a new falling column at the top-center. Every 18th piece is a MAGIC
427
+ * column; otherwise take the previewed colors and roll the next preview. If
428
+ * it can't even appear, the well is full → game over. */
429
+ static void spawn(void) {
430
+ rng ^= DIV;
431
+ if (++piece_counter >= 18) {
432
+ piece_counter = 0;
433
+ piece_magic = 1;
434
+ piece[0] = MAGIC; piece[1] = MAGIC; piece[2] = MAGIC;
435
+ } else {
436
+ piece_magic = 0;
437
+ piece[0] = nextp[0]; piece[1] = nextp[1]; piece[2] = nextp[2];
438
+ roll(nextp);
439
+ }
440
+ piece_x = COLS / 2 - 1;
441
+ piece_y = 0;
442
+ piece_active = 1;
443
+ fall_timer = 0;
444
+ next_dirty = 1;
445
+ if (collides(piece_x, piece_y)) game_over();
446
+ }
447
+
448
+ /* Flag every gem that's part of a run of 3+ same-color cells in any of the 4
449
+ * directions, into matched[]; return how many cells were flagged. Each line
450
+ * is counted from its lowest end only (we skip a cell if its predecessor in
451
+ * that direction is the same color), so runs aren't double-walked. */
452
+ static uint8_t mark_and_count(void) {
453
+ uint8_t r, c, d, len, cnt, col, k;
454
+ int8_t dr, dc;
455
+ int16_t sr, sc;
456
+
457
+ for (r = 0; r < NCELL; r++) matched[r] = 0;
458
+
459
+ for (r = 0; r < ROWS; r++) {
460
+ for (c = 0; c < COLS; c++) {
461
+ col = G(r, c);
462
+ if (col == 0) continue;
463
+ for (d = 0; d < 4; d++) {
464
+ dr = DIRS[d][0];
465
+ dc = DIRS[d][1];
466
+ sr = (int16_t)r - dr;
467
+ sc = (int16_t)c - dc;
468
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
469
+ && G(sr, sc) == col) continue;
470
+ len = 1;
471
+ sr = (int16_t)r + dr;
472
+ sc = (int16_t)c + dc;
473
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
474
+ && G(sr, sc) == col) {
475
+ len++;
476
+ sr += dr;
477
+ sc += dc;
478
+ }
479
+ if (len >= 3) {
480
+ sr = (int16_t)r;
481
+ sc = (int16_t)c;
482
+ for (k = 0; k < len; k++) {
483
+ M(sr, sc) = 1;
484
+ sr += dr;
485
+ sc += dc;
486
+ }
487
+ }
488
+ }
489
+ }
490
+ }
491
+
492
+ cnt = 0;
493
+ for (r = 0; r < NCELL; r++) if (matched[r]) cnt++;
494
+ return cnt;
495
+ }
496
+
497
+ /* empty every flagged cell */
498
+ static void clear_marked(void) {
118
499
  uint8_t i;
119
- int16_t r;
120
- int16_t c;
121
- uint8_t a, b, d;
122
- for (i = 0; i < 3; i++) {
123
- r = piece_y + i;
124
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
500
+ for (i = 0; i < NCELL; i++) if (matched[i]) grid[i] = 0;
501
+ }
502
+
503
+ /* collapse each column so all gems rest on the floor with no gaps */
504
+ static void apply_gravity(void) {
505
+ uint8_t c, r, n, w;
506
+ uint8_t buf[ROWS];
507
+ for (c = 0; c < COLS; c++) {
508
+ n = 0;
509
+ for (r = 0; r < ROWS; r++)
510
+ if (G(r, c)) { buf[n] = G(r, c); n++; }
511
+ for (r = 0; r < (uint8_t)(ROWS - n); r++) G(r, c) = 0;
512
+ w = 0;
513
+ for (r = (uint8_t)(ROWS - n); r < ROWS; r++) { G(r, c) = buf[w]; w++; }
125
514
  }
126
- for (i = 0; i < 3; i++) {
127
- r = piece_y + i;
128
- if (r < 0 || r >= ROWS) continue;
129
- for (c = 0; c <= COLS - 3; c++) {
130
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
131
- if (a != 0 && a == b && b == d) {
132
- grid[r][c] = 0;
133
- grid[r][c + 1] = 0;
134
- grid[r][c + 2] = 0;
135
- if (score < 65500u) score += 30;
515
+ }
516
+
517
+ /* level rises every 15 cleared gems (capped at 13); each level shortens the
518
+ * frames-per-row fall interval, so the column drops faster. */
519
+ static void update_level(void) {
520
+ level = (uint8_t)(total_cleared / 15);
521
+ if (level > 13) level = 13;
522
+ cur_fall_rate = 32 - level * 2;
523
+ if (cur_fall_rate < 4) cur_fall_rate = 4;
524
+ }
525
+
526
+ /* Matched gems burst apart before they clear — a one-shot expanding star in
527
+ * each gem's own colour (no blinking, no LCD-off). Only ever runs on a real
528
+ * match. Direct vblank writes (no OAM DMA contends here, so plenty of room);
529
+ * blocks ~6 frames, which is the satisfying beat. */
530
+ static void explode_matched(void) {
531
+ uint8_t i, j, n, tile;
532
+ uint16_t offs[8];
533
+ uint8_t cols[8];
534
+ uint8_t *o = (uint8_t *)0xC100;
535
+ for (i = 0; i < 12; i++) *o++ = 0; /* hide the falling-piece sprites */
536
+ ((void (*)(uint8_t))0xFF80)(0xC1);
537
+ n = 0;
538
+ for (i = 0; i < NCELL && n < 8; i++) {
539
+ if (matched[i]) {
540
+ offs[n] = (uint16_t)(WELL_MY + (i >> 3)) * 32 + WELL_MX + (i & 7);
541
+ cols[n] = (uint8_t)(grid[i] - 1);
542
+ n++;
543
+ }
544
+ }
545
+ for (j = 0; j < 9; j++) {
546
+ tile = (j < 3) ? T_EXP0 : (j < 6) ? T_EXP1 : T_EXP2;
547
+ wait_vblank();
548
+ sfx_tick();
549
+ music_tick();
550
+ VBK = 0;
551
+ for (i = 0; i < n; i++) VRAM[offs[i]] = tile;
552
+ VBK = 1;
553
+ for (i = 0; i < n; i++) VRAM[offs[i]] = cols[i];
554
+ VBK = 0;
555
+ }
556
+ }
557
+
558
+ /* Settle the board after a lock: repeatedly find matches, burst+clear them,
559
+ * score, and apply gravity — looping so cascades chain. Score per clear scales
560
+ * with level and (for 2nd+ cascades) the chain depth. */
561
+ static void resolve_board(void) {
562
+ uint8_t n;
563
+ uint16_t amt, mult;
564
+ chain = 0;
565
+ while (1) {
566
+ n = mark_and_count();
567
+ if (n == 0) break;
568
+ chain++;
569
+ sfx_chain(chain);
570
+ explode_matched();
571
+ clear_marked();
572
+ mult = (uint16_t)(10 + level * 2);
573
+ amt = (uint16_t)n * mult;
574
+ if (chain > 1) amt = amt * chain;
575
+ if (amt > 60000) amt = 60000;
576
+ add_score(amt);
577
+ total_cleared += n;
578
+ apply_gravity();
579
+ }
580
+ update_level();
581
+ }
582
+
583
+ /* MAGIC column: clears every gem sharing the color of whatever it landed on,
584
+ * then resolves any resulting cascades. */
585
+ static void magic_clear(void) {
586
+ uint8_t below = (uint8_t)(piece_y + 3);
587
+ uint8_t target, i;
588
+ uint16_t cleared = 0;
589
+ piece_active = 0;
590
+ if (below < ROWS) {
591
+ target = G(below, piece_x);
592
+ if (target != 0 && target != MAGIC) {
593
+ for (i = 0; i < NCELL; i++)
594
+ if (grid[i] == target) { grid[i] = 0; cleared++; }
595
+ if (cleared) {
596
+ add_score((uint16_t)cleared * 20u);
597
+ total_cleared += cleared;
598
+ sfx_clear();
136
599
  }
600
+ apply_gravity();
137
601
  }
138
602
  }
139
- draw_grid();
603
+ resolve_board();
604
+ }
605
+
606
+ /* Stamp the falling column into the grid where it came to rest, then resolve.
607
+ * A magic column takes its own path. */
608
+ static void lock_and_resolve(void) {
609
+ uint8_t i, r;
610
+ if (piece_magic) { magic_clear(); return; }
611
+ for (i = 0; i < 3; i++) {
612
+ r = (uint8_t)(piece_y + i);
613
+ if (r < ROWS) G(r, piece_x) = piece[i];
614
+ }
615
+ piece_active = 0;
616
+ resolve_board();
140
617
  }
141
618
 
619
+ /* ── rendering ─────────────────────────────────────────────────────── */
620
+ /* copy one 16-byte 2bpp tile into VRAM tile slot `slot` ($8000 + slot*16) */
142
621
  static void upload_tile(uint8_t slot, const uint8_t *src) {
143
- uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
144
- /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
145
- * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
146
- memcpy_vram(dst, src, 16);
622
+ memcpy_vram((uint8_t *)(0x8000 + slot * 16), src, 16);
147
623
  }
148
624
 
149
- /* Draw the well frame around the 6×12 play area. Grid cells live at
150
- * map[(row+1)*32 + (col+7)] (rows 1..12, cols 7..12), so the frame is the
151
- * column to each side (6 and 13) and the floor row just below (row 13). */
152
- static void draw_well(void) {
153
- uint8_t *map = (uint8_t *)0x9800;
154
- uint8_t r;
155
- for (r = 1; r <= 12; r++) {
156
- map[r * 32 + 6] = T_WALL; /* left wall */
157
- map[r * 32 + 13] = T_WALL; /* right wall */
625
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
626
+ * CGB palette RAM the BCPS/BCPD (BG) and OCPS/OCPD (OBJ) port pairs.
627
+ * requires: a .gbc build (CGB flag $0143 set the build pipeline does it);
628
+ * on a DMG build these registers are dead and you get 4-shade green.
629
+ *
630
+ * Palette RAM is NOT memory-mapped: it's 64 bytes (8 palettes × 4 colors ×
631
+ * 2 bytes, little-endian 15-bit BGR) behind an index/data port pair.
632
+ * BCPS = 0x80 | index set write index; bit 7 = AUTO-INCREMENT, so a
633
+ * burst of BCPD writes walks the whole 64 bytes.
634
+ * BCPD = low byte; BCPD = high byte; ... 32 times = all 8 palettes.
635
+ *
636
+ * TIMING FOOTGUN: palette RAM belongs to the PPU. Writes during active
637
+ * display (mode 3) are IGNORED on real hardware — same constraint as VRAM.
638
+ * Load palettes with the LCD OFF (boot / transitions, as here) or inside
639
+ * vblank. A palette "fade" effect = a few BCPD writes per vblank, never a
640
+ * mid-frame burst. */
641
+ static void load_palettes(void) {
642
+ uint8_t p, i;
643
+ BCPS = 0x80; /* index 0, auto-increment on */
644
+ for (p = 0; p < 8; p++)
645
+ for (i = 0; i < 4; i++) {
646
+ BCPD = (uint8_t)(palettes[p][i] & 0xFF);
647
+ BCPD = (uint8_t)((palettes[p][i] >> 8) & 0xFF);
648
+ }
649
+ }
650
+
651
+ /* OBJ palettes 0-5 = the six jewel colors (same table as the BG, so a
652
+ * falling gem matches its locked twin exactly), 6 = magic white. Color 0
653
+ * of every OBJ palette is transparent (the well shows through). */
654
+ static void load_obj_palettes(void) {
655
+ uint8_t p, i;
656
+ uint16_t col;
657
+ OCPS = 0x80;
658
+ for (p = 0; p < 8; p++)
659
+ for (i = 0; i < 4; i++) {
660
+ if (p < 6) col = palettes[p][i];
661
+ else if (p == 6) col = (i == 3) ? RGB(31,31,31) : C_OUT;
662
+ else col = 0;
663
+ OCPD = (uint8_t)(col & 0xFF);
664
+ OCPD = (uint8_t)((col >> 8) & 0xFF);
665
+ }
666
+ }
667
+
668
+ /* The falling column = sprites 0-2; the NEXT preview = sprites 3-5 (sprites
669
+ * so their transparent corners blend with the panel). Then flush OAM.
670
+ * MUST be the first VRAM/OAM work after wait_vblank: the OAM DMA has to
671
+ * land in vblank, or sprites tear on a fixed scanline near the top.
672
+ * A sprite's CGB palette = OAM attr bits 0-2 — that's the whole "color this
673
+ * sprite" story (we write piece[i]-1 straight into the attr byte). */
674
+ static void update_sprites(void) {
675
+ /* Write shadow_oam ($C100) directly with a walking pointer — calling
676
+ * oam_set() six times burns ~10 scanlines of vblank (SDCC call overhead),
677
+ * starving the BG flush. Inlined it's ~2 lines. */
678
+ uint8_t *o = (uint8_t *)0xC100;
679
+ uint8_t i, tile, sx, sy, pal0, pal1, pal2;
680
+ if (piece_active) {
681
+ tile = piece_magic ? T_MAGIC : T_GEM;
682
+ sx = (uint8_t)((WELL_MX + piece_x) * 8 + 8);
683
+ sy = (uint8_t)((WELL_MY + piece_y) * 8 + 16);
684
+ if (piece_magic) { pal0 = pal1 = pal2 = 6; }
685
+ else { pal0 = piece[0] - 1; pal1 = piece[1] - 1; pal2 = piece[2] - 1; }
686
+ *o++ = sy; *o++ = sx; *o++ = tile; *o++ = pal0;
687
+ *o++ = (uint8_t)(sy + 8); *o++ = sx; *o++ = tile; *o++ = pal1;
688
+ *o++ = (uint8_t)(sy + 16); *o++ = sx; *o++ = tile; *o++ = pal2;
689
+ } else {
690
+ for (i = 0; i < 12; i++) *o++ = 0;
691
+ }
692
+ /* NEXT preview (sprites 3-5) only changes on a spawn — skip it most
693
+ * frames to keep the OAM build short enough to leave the BG flush vblank. */
694
+ if (next_dirty) {
695
+ next_dirty = 0;
696
+ o = (uint8_t *)0xC10C; /* sprite slot 3 */
697
+ if (state == ST_TITLE) {
698
+ for (i = 0; i < 12; i++) *o++ = 0;
699
+ } else {
700
+ sx = (uint8_t)((HUD_X + 1) * 8 + 8);
701
+ for (i = 0; i < 3; i++) {
702
+ *o++ = (uint8_t)((3 + i) * 8 + 16);
703
+ *o++ = sx;
704
+ *o++ = T_GEM;
705
+ *o++ = (uint8_t)(nextp[i] - 1);
706
+ }
707
+ }
158
708
  }
159
- for (r = 6; r <= 13; r++)
160
- map[13 * 32 + r] = T_WALL; /* floor */
709
+ /* Trigger the OAM DMA via the HRAM stub directly (skip the oam_dma_flush
710
+ * / oam_dma_copy wrappers). A = high byte of shadow_oam ($C100). */
711
+ ((void (*)(uint8_t))0xFF80)(0xC1);
161
712
  }
162
713
 
163
- void main(void) {
164
- uint8_t pad, prev = 0, fall_rate, t;
165
- int16_t r, c;
714
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
715
+ * Per-tile color the VRAM bank-1 attribute map (VBK register).
716
+ * requires: CGB mode (see the palette idiom above); writes in a VRAM-safe
717
+ * window (LCD off, or a bounded vblank batch like redraw_flush).
718
+ *
719
+ * VRAM is TWO 8KB banks behind the same $8000-$9FFF window; VBK ($FF4F)
720
+ * selects which one the CPU sees. Bank 0 holds what the DMG had: tile
721
+ * pixels + the tile-index maps. Bank 1 at the SAME map address holds one
722
+ * ATTRIBUTE byte per cell:
723
+ * bits 0-2 palette 0-7 ← this game's whole color system
724
+ * bit 3 tile VRAM bank
725
+ * bit 5/6 H/V flip
726
+ * bit 7 BG-over-OBJ priority
727
+ * So coloring a cell is a write PAIR: tile index with VBK=0, attribute with
728
+ * VBK=1, at the SAME offset.
729
+ *
730
+ * FOOTGUN: VBK is global state. Forget to restore VBK=0 and every later
731
+ * "tile" write lands in the attribute map — the screen turns into garbage
732
+ * colors while the tile data you wrote is simply gone. Always end VBK=0
733
+ * (every routine here does).
734
+ * (Direct, unbounded — only safe with the LCD off or in a bounded vblank
735
+ * batch; the in-game path queues instead — see redraw_collect/flush.) */
736
+ static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
737
+ uint16_t off = (uint16_t)my * 32 + mx;
738
+ VBK = 0;
739
+ VRAM[off] = tile;
740
+ VBK = 1;
741
+ VRAM[off] = pal;
742
+ VBK = 0;
743
+ }
744
+
745
+ /* same write-pair, into the WINDOW's map at $9C00 (see the window idiom) */
746
+ static void set_wcell(uint8_t wx, uint8_t wy, uint8_t tile, uint8_t pal) {
747
+ uint16_t off = WIN_OFF + (uint16_t)wy * 32 + wx;
748
+ VBK = 0;
749
+ VRAM[off] = tile;
750
+ VBK = 1;
751
+ VRAM[off] = pal;
752
+ VBK = 0;
753
+ }
754
+
755
+ /* map an ASCII char to its font tile slot (digits, then A-Z); blank otherwise */
756
+ static uint8_t font_slot(char ch) {
757
+ if (ch >= '0' && ch <= '9') return FONT_BASE + (uint8_t)(ch - '0');
758
+ if (ch >= 'A' && ch <= 'Z') return FONT_BASE + 10 + (uint8_t)(ch - 'A');
759
+ return T_BLANK;
760
+ }
761
+
762
+ /* draw a NUL-terminated string into the BG map starting at (col,row) */
763
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
166
764
  uint8_t i;
167
- uint8_t *map;
168
- int16_t pr;
765
+ for (i = 0; s[i] != 0; i++)
766
+ set_cell((uint8_t)(col + i), row, font_slot(s[i]), PAL_OUT);
767
+ }
169
768
 
170
- lcd_init_default();
171
- LCDC = 0;
769
+ /* draw a NUL-terminated string into the WINDOW map starting at (col,row) */
770
+ static void draw_wtext(uint8_t col, uint8_t row, const char *s) {
771
+ uint8_t i;
772
+ for (i = 0; s[i] != 0; i++)
773
+ set_wcell((uint8_t)(col + i), row, font_slot(s[i]), PAL_OUT);
774
+ }
172
775
 
173
- upload_tile(T_BLANK, tile_blank);
174
- upload_tile(T_R, tile_r);
175
- upload_tile(T_G, tile_g);
176
- upload_tile(T_B, tile_b);
177
- upload_tile(T_WALL, tile_wall);
776
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
777
+ * WINDOW-layer HUD — a fixed strip the BG scroll can never move.
778
+ * requires: LCDC bits 5 (window on) + 6 (window map = $9C00), WX/WY set,
779
+ * and HUD text written to the $9C00 map (set_wcell), not the $9800 one.
780
+ *
781
+ * The window is the GB's second BG plane: same tile data, its OWN 32x32
782
+ * map, drawn OVER the BG starting at screen position (WX-7, WY) and
783
+ * extending to the bottom-right. It ignores SCX/SCY completely — that's
784
+ * the point: scroll the playfield all you want, the HUD strip stays put.
785
+ * Classic placements: a bottom status bar (this game: WY=128 → the last
786
+ * 16 pixel rows) or a full-width top bar. It CANNOT be a floating box —
787
+ * the window always runs to the screen's bottom-right corner.
788
+ *
789
+ * Gotchas:
790
+ * - WX is offset by 7: WX=7 is the left edge. WX<7 glitches on hardware.
791
+ * - The window has its OWN line counter: it renders ITS map from window
792
+ * row 0 downward, regardless of WY — our HUD lives at $9C00 rows 0-1.
793
+ * - On CGB the window cells take bank-1 attributes exactly like the BG
794
+ * (set_wcell writes both banks).
795
+ * - This block is DMG-era hardware — it transplants to plain GB examples
796
+ * unchanged; only the bank-1 attribute half is CGB-specific.
797
+ *
798
+ * Window HUD layout (window map rows 0-1):
799
+ * row 0: SC dddddd HI dddddd row 1: LV dd
800
+ * Static labels drawn once at transitions; the digits go through the
801
+ * vblank queue (see redraw_collect) so in-game updates never tear. */
802
+ #define WINY 128 /* screen y where the strip starts */
803
+ #define HUD_SC_X 3 /* score digits, window row 0 */
804
+ #define HUD_HI_X 13 /* hi-score digits, window row 0 */
805
+ #define HUD_LV_X 3 /* level digits, window row 1 */
806
+
807
+ /* paint the whole window strip: dark backdrop + labels (LCD off only) */
808
+ static void draw_window_static(void) {
809
+ uint8_t x, y;
810
+ for (y = 0; y < 2; y++)
811
+ for (x = 0; x < 20; x++) set_wcell(x, y, T_BLANK, PAL_OUT);
812
+ draw_wtext(0, 0, "SC");
813
+ draw_wtext(10, 0, "HI");
814
+ draw_wtext(0, 1, "LV");
815
+ }
816
+
817
+ /* draw every dynamic HUD value directly (LCD off / transitions only —
818
+ * in-game updates go through the queue, 4 cells per vblank) */
819
+ static void draw_hud_now(void) {
820
+ uint8_t i;
821
+ for (i = 0; i < 6; i++) {
822
+ set_wcell((uint8_t)(HUD_SC_X + i), 0, FONT_BASE + score_d[i], PAL_OUT);
823
+ set_wcell((uint8_t)(HUD_HI_X + i), 0, FONT_BASE + hi_d[i], PAL_OUT);
824
+ }
825
+ set_wcell(HUD_LV_X, 1, FONT_BASE + (uint8_t)(level / 10), PAL_OUT);
826
+ set_wcell((uint8_t)(HUD_LV_X + 1), 1, FONT_BASE + (uint8_t)(level % 10), PAL_OUT);
827
+ }
828
+
829
+ /* Lay down the unchanging screen: clear the whole BG map, draw the well's
830
+ * walls + floor, the right panel, and the window HUD. Only called with the
831
+ * LCD off (it writes entire maps at once). */
832
+ static void draw_static(void) {
833
+ uint8_t x, y;
834
+ uint16_t off;
835
+ VBK = 0;
836
+ for (y = 0; y < 18; y++)
837
+ for (x = 0; x < 20; x++) { off = (uint16_t)y * 32 + x; VRAM[off] = T_EMPTY; }
838
+ VBK = 1;
839
+ for (y = 0; y < 18; y++)
840
+ for (x = 0; x < 20; x++) { off = (uint16_t)y * 32 + x; VRAM[off] = PAL_OUT; }
841
+ VBK = 0;
842
+ for (y = WELL_MY; y < (uint8_t)(WELL_MY + ROWS); y++) {
843
+ set_cell((uint8_t)(WELL_MX - 1), y, T_WALL, PAL_WELL);
844
+ set_cell((uint8_t)(WELL_MX + COLS), y, T_WALL, PAL_WELL);
845
+ }
846
+ for (x = (uint8_t)(WELL_MX - 1); x <= (uint8_t)(WELL_MX + COLS); x++)
847
+ set_cell(x, (uint8_t)(WELL_MY + ROWS), T_WALL, PAL_WELL);
848
+ draw_window_static();
849
+ }
850
+
851
+ /* Full LOCKED-well repaint from the grid (no piece — that's a sprite). Used
852
+ * only with the LCD OFF (boot / title↔game transitions), where writing all
853
+ * changed cells at once is safe. */
854
+ static void redraw_all(void) {
855
+ uint8_t r, c, col;
856
+ uint8_t i = 0;
857
+ uint16_t rowoff, off;
858
+ for (r = 0; r < ROWS; r++) {
859
+ rowoff = (uint16_t)(WELL_MY + r) * 32 + WELL_MX;
860
+ for (c = 0; c < COLS; c++) {
861
+ col = grid[i];
862
+ if (col != shadow[i]) {
863
+ shadow[i] = col;
864
+ off = rowoff + c;
865
+ VBK = 0; VRAM[off] = col ? T_GEM : T_EMPTY;
866
+ VBK = 1; VRAM[off] = col ? (uint8_t)(col - 1) : PAL_WELL;
867
+ VBK = 0;
868
+ }
869
+ i++;
870
+ }
871
+ }
872
+ }
873
+
874
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
875
+ * Deferred well/HUD rendering — the vblank COLLECT/FLUSH queue.
876
+ * requires: update_sprites + redraw_flush as the FIRST two things after
877
+ * wait_vblank (in that order), batches capped at REDRAW_BUDGET, and no
878
+ * LCDC bit-7 toggling in-game.
879
+ *
880
+ * This core blanks the whole frame on ANY LCDC bit-7 toggle (a strobe we
881
+ * must never do), AND it occasionally drops a VRAM write even at the start
882
+ * of vblank. So in-game we never touch the LCD; instead:
883
+ * COLLECT — queue work (RAM only): changed cells after a lock, the HUD
884
+ * digits, and — when idle — a rolling SCRUB of the whole well.
885
+ * FLUSH — write the queue to VRAM as the FIRST thing after wait_vblank.
886
+ * The scrub re-writes every well cell from the grid every ~0.2s, so any
887
+ * dropped write self-corrects instead of becoming a permanent wrong color
888
+ * (the "3 oranges that won't clear" bug). Idempotent ⇒ invisible.
889
+ * Batches are kept small so the whole flush fits in vblank AFTER the OAM
890
+ * DMA — overrunning into active display drops writes (a garbage "burst" on
891
+ * lock frames before the scrub heals them).
892
+ * Queue offsets are plain offsets from $9800, so the same queue serves the
893
+ * BG map (well) and the window map at $9800+$400 (HUD digits). */
894
+ #define REDRAW_BUDGET 4 /* changed well cells per frame (responsive) */
895
+ #define SCRUB_N 4 /* idle cells re-written per frame (self-heal) */
896
+ #define WQ_MAX 6 /* queue capacity (≤4 pushed per frame) */
897
+ static uint8_t scanning, hud_pending, over_pending;
898
+ static uint8_t hud_phase, over_phase; /* split big HUD/text writes across frames */
899
+ static uint8_t scan_i, scan_c, scrub_i;
900
+ static uint16_t scan_rowoff;
901
+
902
+ static uint8_t wq_n;
903
+ static uint16_t wq_off[WQ_MAX];
904
+ static uint8_t wq_tile[WQ_MAX];
905
+ static uint8_t wq_attr[WQ_MAX];
906
+
907
+ static void start_redraw(void) {
908
+ scanning = 1;
909
+ scan_i = 0; scan_c = 0;
910
+ scan_rowoff = (uint16_t)WELL_MY * 32 + WELL_MX;
911
+ }
178
912
 
179
- BCPS = 0x80;
180
- for (i = 0; i < 4; i++) {
181
- BCPD = (uint8_t)(bg_palette[i] & 0xFF);
182
- BCPD = (uint8_t)((bg_palette[i] >> 8) & 0xFF);
913
+ static void wq_push(uint16_t off, uint8_t tile, uint8_t attr) {
914
+ if (wq_n < WQ_MAX) {
915
+ wq_off[wq_n] = off; wq_tile[wq_n] = tile; wq_attr[wq_n] = attr; wq_n++;
183
916
  }
917
+ }
918
+
919
+ static void wq_text(uint8_t col, uint8_t row, const char *s) {
920
+ uint8_t i;
921
+ for (i = 0; s[i] != 0; i++)
922
+ wq_push((uint16_t)row * 32 + col + i, font_slot(s[i]), PAL_OUT);
923
+ }
924
+
925
+ /* queue one window-HUD digit cell (window map = offset $400) */
926
+ static void wq_wdigit(uint8_t col, uint8_t row, uint8_t digit) {
927
+ wq_push(WIN_OFF + (uint16_t)row * 32 + col, FONT_BASE + digit, PAL_OUT);
928
+ }
929
+
930
+ /* Fill the queue with the next batch of pending changes (RAM only).
931
+ * Each branch pushes at most REDRAW_BUDGET cells, so the flush always fits
932
+ * in vblank; the HUD digits and game-over text are split across frames. */
933
+ static void redraw_collect(void) {
934
+ uint8_t col, k, r, c, i;
935
+ wq_n = 0;
936
+ if (scanning) {
937
+ while (scan_i < NCELL && wq_n < REDRAW_BUDGET) {
938
+ col = grid[scan_i];
939
+ if (col != shadow[scan_i]) {
940
+ shadow[scan_i] = col;
941
+ wq_off[wq_n] = scan_rowoff + scan_c;
942
+ wq_tile[wq_n] = col ? T_GEM : T_EMPTY;
943
+ wq_attr[wq_n] = col ? (uint8_t)(col - 1) : PAL_WELL;
944
+ wq_n++;
945
+ }
946
+ scan_i++; scan_c++;
947
+ if (scan_c >= COLS) { scan_c = 0; scan_rowoff += 32; }
948
+ }
949
+ if (scan_i >= NCELL) { scanning = 0; hud_pending = 1; hud_phase = 0; }
950
+ } else if (hud_pending) {
951
+ if (hud_phase == 0) { /* score digits 0-3 */
952
+ for (i = 0; i < 4; i++) wq_wdigit((uint8_t)(HUD_SC_X + i), 0, score_d[i]);
953
+ hud_phase = 1;
954
+ } else if (hud_phase == 1) { /* score 4-5 + level */
955
+ wq_wdigit(HUD_SC_X + 4, 0, score_d[4]);
956
+ wq_wdigit(HUD_SC_X + 5, 0, score_d[5]);
957
+ wq_wdigit(HUD_LV_X, 1, (uint8_t)(level / 10));
958
+ wq_wdigit(HUD_LV_X + 1, 1, (uint8_t)(level % 10));
959
+ hud_phase = 2;
960
+ } else if (hud_phase == 2) { /* hi-score digits 0-3 */
961
+ for (i = 0; i < 4; i++) wq_wdigit((uint8_t)(HUD_HI_X + i), 0, hi_d[i]);
962
+ hud_phase = 3;
963
+ } else { /* hi-score digits 4-5 */
964
+ wq_wdigit(HUD_HI_X + 4, 0, hi_d[4]);
965
+ wq_wdigit(HUD_HI_X + 5, 0, hi_d[5]);
966
+ hud_pending = 0;
967
+ if (state == ST_OVER) { over_pending = 1; over_phase = 0; }
968
+ }
969
+ } else if (over_pending) {
970
+ if (over_phase == 0) { wq_text(3, 6, "GAME"); over_phase = 1; }
971
+ else { wq_text(3, 7, "OVER"); over_pending = 0; }
972
+ } else if (state == ST_PLAY) {
973
+ /* idle: rolling scrub of the well so any dropped write heals itself.
974
+ * (COLS is a power of two, so >>3 / &7 split index → row,col cheaply.)
975
+ * Only during play — would erase the title gems / game-over text. */
976
+ for (k = 0; k < SCRUB_N; k++) {
977
+ r = scrub_i >> 3; c = scrub_i & 7;
978
+ col = grid[scrub_i];
979
+ wq_push((uint16_t)(WELL_MY + r) * 32 + WELL_MX + c,
980
+ col ? T_GEM : T_EMPTY, col ? (uint8_t)(col - 1) : PAL_WELL);
981
+ scrub_i++;
982
+ if (scrub_i >= NCELL) scrub_i = 0;
983
+ }
984
+ }
985
+ }
986
+
987
+ /* Write the queued cells to VRAM. MUST run first after wait_vblank (right
988
+ * after the OAM DMA), and MUST finish inside the ~10-line vblank window or
989
+ * writes drop. Pointer-walk (not array indexing) — SDCC sm83 generates far
990
+ * tighter code for *p++. Each cell is the bank pair: tile (VBK=0) then
991
+ * attribute (VBK=1) at the same offset — see the per-tile color idiom. */
992
+ static void redraw_flush(void) {
993
+ uint8_t k = wq_n;
994
+ uint16_t *op;
995
+ uint8_t *tp, *ap;
996
+ uint16_t off;
997
+ if (k == 0) return;
998
+ op = wq_off; tp = wq_tile; ap = wq_attr;
999
+ while (k != 0) {
1000
+ off = *op++;
1001
+ VBK = 0; VRAM[off] = *tp++;
1002
+ VBK = 1; VRAM[off] = *ap++;
1003
+ k--;
1004
+ }
1005
+ VBK = 0;
1006
+ wq_n = 0;
1007
+ }
1008
+
1009
+ /* ── GAME LOGIC (clay — reshape freely) ── title screen.
1010
+ * A jagged pile of all six gem colors dresses the well — it doubles as the
1011
+ * "this cart is COLOR" proof the moment the title appears. */
1012
+ static const uint8_t title_heights[COLS] = { 4, 6, 3, 7, 5, 6, 4, 5 };
184
1013
 
185
- map = (uint8_t *)0x9800;
186
- for (i = 0; i < 32; i++) {
187
- c = 0;
188
- while (c < 32) { map[i * 32 + c] = T_BLANK; c++; }
1014
+ static void draw_title(void) {
1015
+ uint8_t x, y, c, k, color;
1016
+ /* clear the right panel (NEXT label from a previous game) */
1017
+ for (y = 0; y <= 15; y++)
1018
+ for (x = 10; x <= 19; x++) set_cell(x, y, T_EMPTY, PAL_OUT);
1019
+ /* decorative gems piled at the bottom of the well, cycling palettes */
1020
+ color = 1;
1021
+ for (c = 0; c < COLS; c++) {
1022
+ for (k = 0; k < title_heights[c]; k++) {
1023
+ y = (uint8_t)(ROWS - 1 - k);
1024
+ set_cell((uint8_t)(WELL_MX + c), (uint8_t)(WELL_MY + y),
1025
+ T_GEM, (uint8_t)(color - 1));
1026
+ color++; if (color > NCOLORS) color = 1;
1027
+ }
189
1028
  }
1029
+ /* game name + prompt, centered across the full 20-column screen */
1030
+ draw_text((uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), 2, GAME_TITLE);
1031
+ draw_text(4, 4, "PRESS START");
1032
+ }
190
1033
 
191
- for (r = 0; r < ROWS; r++)
192
- for (c = 0; c < COLS; c++)
193
- grid[r][c] = 0;
1034
+ /* LCD off / on only used to bracket the full-screen rebuilds at the title
1035
+ * and game-start transitions. NEVER call these from the in-game loop (the
1036
+ * off-frame blanks the whole screen — a flash/strobe). blit_on enables BG +
1037
+ * OBJ + the WINDOW (map $9C00) — see the window idiom for the LCDC bits. */
1038
+ static void blit_off(void) { wait_vblank(); LCDC = 0; }
1039
+ static void blit_on(void) {
1040
+ LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO
1041
+ | LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI;
1042
+ }
194
1043
 
195
- score = 0;
1044
+ /* zero the board and all run stats for a fresh game (shadow set to 0xFF so the
1045
+ * first redraw repaints every cell). Does not touch music_on or hi_d. */
1046
+ static void reset_state(void) {
1047
+ uint8_t i;
1048
+ for (i = 0; i < NCELL; i++) grid[i] = 0;
1049
+ for (i = 0; i < NCELL; i++) shadow[i] = 0xFF;
1050
+ for (i = 0; i < 6; i++) score_d[i] = 0;
1051
+ total_cleared = 0;
1052
+ level = 0;
1053
+ cur_fall_rate = 32;
196
1054
  fall_timer = 0;
197
- new_piece();
198
- draw_well();
199
- draw_grid();
1055
+ piece_counter = 0;
1056
+ piece_magic = 0;
1057
+ }
1058
+
1059
+ /* leave the title and begin play: reset, seed the first piece + preview, and
1060
+ * rebuild the screen with the LCD off. */
1061
+ static void start_game(void) {
1062
+ reset_state();
1063
+ state = ST_PLAY;
1064
+ roll(nextp);
1065
+ spawn();
1066
+ blit_off();
1067
+ draw_static();
1068
+ redraw_all();
1069
+ draw_text(HUD_X, 1, "NEXT");
1070
+ draw_hud_now();
1071
+ blit_on();
1072
+ update_sprites();
1073
+ scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
1074
+ }
200
1075
 
201
- LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_TILE_DATA_LO;
1076
+ /* show the title screen (gem pile + name + PRESS START + persisted HI) */
1077
+ static void go_title(void) {
1078
+ reset_state();
1079
+ piece_active = 0;
1080
+ state = ST_TITLE;
1081
+ blit_off();
1082
+ draw_static();
1083
+ redraw_all();
1084
+ draw_title();
1085
+ draw_hud_now();
1086
+ next_dirty = 1;
1087
+ blit_on();
1088
+ update_sprites();
1089
+ scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
1090
+ }
1091
+
1092
+ /* the run is over: persist a new record, then let the queue paint GAME OVER
1093
+ * + the updated HI digits (hud_pending → over_pending chain). */
1094
+ static void game_over(void) {
1095
+ piece_active = 0;
1096
+ state = ST_OVER;
1097
+ sfx_over();
1098
+ if (score_beats_hi()) {
1099
+ uint8_t i;
1100
+ for (i = 0; i < 6; i++) hi_d[i] = score_d[i];
1101
+ hiscore_save(); /* battery SRAM — survives power-off */
1102
+ }
1103
+ }
1104
+
1105
+ void main(void) {
1106
+ uint8_t pad, prev = 0, t, rate, g;
1107
+
1108
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
1109
+ * Boot order: LCD defaults (installs the OAM-DMA HRAM stub) → vblank IRQ
1110
+ * (so wait_vblank HALTs instead of busy-polling LY — the poll runs at
1111
+ * ~1/30 speed on this core) → APU on → LCD OFF → then all the bulk VRAM
1112
+ * work (tiles, palettes, maps). Tile/palette/map uploads REQUIRE a
1113
+ * VRAM-safe window and boot does them all at once, so LCD-off is the
1114
+ * only sane choice here. The window position registers are plain I/O —
1115
+ * set once, they hold. */
1116
+ lcd_init_default();
1117
+ enable_vblank_irq();
1118
+ sound_init();
1119
+ music_on = 1; /* background music on by default (SELECT toggles) */
1120
+ LCDC = 0;
1121
+ WY = WINY; /* window HUD strip: bottom 16 pixel rows */
1122
+ WX = 7; /* WX is offset by 7 — this is the left edge */
1123
+
1124
+ upload_tile(T_EMPTY, tile_empty);
1125
+ upload_tile(T_GEM, tile_gem);
1126
+ upload_tile(T_WALL, tile_wall);
1127
+ upload_tile(T_BLANK, tile_blank);
1128
+ upload_tile(T_MAGIC, tile_magic);
1129
+ upload_tile(T_EXP0, tile_exp0);
1130
+ upload_tile(T_EXP1, tile_exp1);
1131
+ upload_tile(T_EXP2, tile_exp2);
1132
+ for (g = 0; g < FONT_GLYPHS; g++)
1133
+ memcpy_vram((uint8_t *)(0x8000 + (FONT_BASE + g) * 16), &font_data[g * 16], 16);
1134
+ load_palettes();
1135
+ load_obj_palettes();
1136
+ oam_clear();
202
1137
 
1138
+ hiscore_load(); /* battery SRAM — 0 on a fresh cart */
1139
+ go_title();
1140
+
1141
+ /* Main loop, one pass per frame. The order is deliberate: the two VRAM/OAM
1142
+ * writers (sprites, then the bounded BG flush) run FIRST so they land inside
1143
+ * vblank; audio and game logic follow; the next frame's BG writes are queued
1144
+ * last (RAM only) for the following frame's flush. */
203
1145
  while (1) {
204
1146
  wait_vblank();
205
-
206
- /* Erase current piece visual (overwrite with what's underneath). */
207
- for (i = 0; i < 3; i++) {
208
- pr = piece_y + i;
209
- if (pr >= 0 && pr < ROWS)
210
- draw_cell(piece_x, pr, grid[pr][piece_x]);
211
- }
1147
+ update_sprites(); /* OAM DMA FIRST — must land in vblank (no tear) */
1148
+ redraw_flush(); /* then drain queued BG writes (≤4, fits vblank) */
1149
+ sfx_tick();
1150
+ music_tick();
212
1151
 
213
1152
  pad = joypad_read();
214
1153
 
215
- if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
216
- && !collides(piece_x - 1, piece_y)) piece_x--;
217
- if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
218
- && !collides(piece_x + 1, piece_y)) piece_x++;
219
- if ((pad & PAD_A) && !(prev & PAD_A)) {
220
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
221
- }
222
- if ((pad & PAD_START) && !(prev & PAD_START)) {
223
- while (!collides(piece_x, piece_y + 1)) piece_y++;
224
- lock_piece();
225
- new_piece();
226
- prev = pad;
227
- continue;
228
- }
229
- prev = pad;
1154
+ /* SELECT toggles the background music, in any state */
1155
+ if ((pad & PAD_SELECT) && !(prev & PAD_SELECT)) music_toggle();
230
1156
 
231
- fall_rate = (pad & PAD_DOWN) ? 4 : 30;
232
- if (++fall_timer >= fall_rate) {
233
- fall_timer = 0;
234
- if (collides(piece_x, piece_y + 1)) {
235
- lock_piece();
236
- new_piece();
237
- } else {
238
- piece_y++;
1157
+ if (state == ST_TITLE) {
1158
+ /* ── GAME LOGIC (clay reshape freely) ── press-start title
1159
+ * (handheld: no 2P mode select — see the header note) */
1160
+ if ((pad & PAD_START) && !(prev & PAD_START)) start_game();
1161
+ } else if (state == ST_PLAY) {
1162
+ /* ── GAME LOGIC (clay — reshape freely) ── one frame of play */
1163
+ if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
1164
+ && !collides((uint8_t)(piece_x - 1), piece_y)) { piece_x--; sfx_move(); }
1165
+ if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
1166
+ && !collides((uint8_t)(piece_x + 1), piece_y)) { piece_x++; sfx_move(); }
1167
+ if ((pad & PAD_A) && !(prev & PAD_A) && !piece_magic) {
1168
+ t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
1169
+ sfx_rotate();
1170
+ }
1171
+ if ((pad & PAD_B) && !(prev & PAD_B) && !piece_magic) {
1172
+ t = piece[2]; piece[2] = piece[1]; piece[1] = piece[0]; piece[0] = t;
1173
+ sfx_rotate();
1174
+ }
1175
+ if ((pad & PAD_START) && !(prev & PAD_START)) {
1176
+ while (!collides(piece_x, (uint8_t)(piece_y + 1))) piece_y++;
1177
+ sfx_drop();
1178
+ lock_and_resolve();
1179
+ spawn();
1180
+ start_redraw();
239
1181
  }
240
- }
241
1182
 
242
- /* Re-draw piece in new position. */
243
- for (i = 0; i < 3; i++) {
244
- pr = piece_y + i;
245
- if (pr >= 0 && pr < ROWS) draw_cell(piece_x, pr, piece[i]);
1183
+ rate = (pad & PAD_DOWN) ? 3 : cur_fall_rate;
1184
+ if (state == ST_PLAY && ++fall_timer >= rate) {
1185
+ fall_timer = 0;
1186
+ if (collides(piece_x, (uint8_t)(piece_y + 1))) {
1187
+ sfx_drop();
1188
+ lock_and_resolve();
1189
+ spawn();
1190
+ start_redraw();
1191
+ } else {
1192
+ piece_y++;
1193
+ }
1194
+ }
1195
+ } else { /* ST_OVER — START returns to the title (shows the new HI) */
1196
+ if ((pad & PAD_START) && !(prev & PAD_START)) go_title();
246
1197
  }
1198
+
1199
+ redraw_collect(); /* queue next frame's VRAM writes (RAM only) */
1200
+ prev = pad;
247
1201
  }
248
1202
  }