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,94 +1,243 @@
1
- /* ── puzzle.c — Game Boy match-3 falling-block scaffold ─────────────
1
+ /* ── puzzle.c — SHALE WELL: Game Boy falling-stone matcher (complete example game) ──
2
2
  *
3
- * 8-wide × 14-tall well drawn via BG tilemap (each cell = 1 BG tile).
4
- * 1×3 vertical active piece; LEFT/RIGHT shifts, A/B cycles the colour
5
- * order, DOWN soft-drops, START hard-drops. Matches of 3+ in a row
6
- * horizontal, vertical, or either diagonal clear, survivors fall
7
- * (gravity), and cascades chain with rising score.
3
+ * A COMPLETE, working game title screen, persistent battery hi-score
4
+ * (MBC1+RAM+BATTERY SRAM), APU music + SFX, level progression, cascades,
5
+ * and the Game Boy's signature WINDOW-LAYER HUD: a fixed score/hi/level
6
+ * strip pinned to the bottom of the screen.
8
7
  *
9
- * On DMG we differentiate the three block kinds by SHAPE (2bpp stripe
10
- * patterns), not colour. The GBC template is the full-colour version.
8
+ * THE GAME: a vertical column of 3 stones falls into an 8-wide x 15-tall
9
+ * well. Move it left/right (D-pad), soft-drop (Down), hard-drop (Start),
10
+ * and CYCLE the three stones (A rolls up, B rolls down). Line up 3+ of one
11
+ * KIND in a row — horizontally, vertically, or diagonally — to clear them;
12
+ * gravity pulls survivors down, which can CHAIN into cascades for bonus
13
+ * score. Every 18th piece is a MAGIC stone that clears every stone of the
14
+ * kind it lands on. SELECT toggles the music. Levels rise as you clear,
15
+ * and each level drops the column faster. This is a 1P marathon: survive,
16
+ * climb the levels, beat the battery-backed record.
11
17
  *
12
- * RENDERING CONTRACT (the "pieces flash / don't render" fix): this
13
- * core silently DROPS VRAM writes during active display and can
14
- * even drop one early in vblank. So (mirroring the GBC reference
15
- * puzzle):
16
- * - The FALLING piece is OAM sprites 0-2 (one OAM DMA per frame —
17
- * no BG writes at all to move it, no erase artifacts).
18
- * - The LOCKED well is BG tiles, written ONLY right after
19
- * wait_vblank(): a budgeted diff (grid vs shadow) plus a rolling
20
- * SCRUB that continuously repaints the well from grid[], so any
21
- * dropped write self-heals within ~half a second.
22
- * - enable_vblank_irq() at boot wait_vblank HALTs to the real
23
- * vblank leading edge (also ~30x faster on the WASM core than
24
- * the LY-polling fallback).
18
+ * MONOCHROME, on purpose: the DMG has FOUR shades of grey, no color. The
19
+ * five stone KINDS are five distinct 2bpp TILE SHAPES (a stripe, a checker,
20
+ * a ring, a brick, a diamond) read through the one DMG background palette —
21
+ * the honest handheld take on the GBC's six-color version. You tell stones
22
+ * apart by their PATTERN, the way the great DMG puzzlers did.
23
+ *
24
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
25
+ * very different one. The markers tell you what's what:
26
+ * HARDWARE IDIOM (load-bearing) dodges a documented GB footgun; reshape
27
+ * your gameplay around it (see TROUBLESHOOTING before changing).
28
+ * GAME LOGIC (clay) board rules, scoring, tuning, art: reshape freely.
29
+ *
30
+ * SINGLE-PLAYER, honestly: the Game Boy's "player 2" is a LINK CABLE, which
31
+ * one emulator instance cannot provide — a single instance cannot emulate
32
+ * the second Game Boy on the other end of that cable. So handheld examples
33
+ * ship a press-start title and a 1P marathon instead of faking a 2P mode
34
+ * the platform cannot deliver. (Consoles' examples have real 2P.)
35
+ *
36
+ * What depends on what:
37
+ * gb_hardware.h — register names (LCDC/WX/WY/BGP/OBP/NRxx/...) + bit masks.
38
+ * gb_runtime.{h,c} — vblank wait (HALT-driven), joypad, shadow OAM + the
39
+ * OAM-DMA-from-HRAM routine, VRAM-safe memcpy, APU helpers.
40
+ * gb_crt0.s — boot + interrupt vectors + the cartridge header window. It
41
+ * DECLARES the cart as MBC1+RAM+BATTERY ($0147=$03, $0149=$02): that
42
+ * header is what makes the SRAM hi-score persist (the GB equivalent of
43
+ * the NES iNES BATTERY bit).
44
+ * (No font.h — the 1bpp glyphs are embedded below, so this template
45
+ * builds with exactly the same includes as the platformer/shmup.)
46
+ *
47
+ * RENDERING — the hard-won architecture (details at each routine below):
48
+ * - The FALLING column and the NEXT preview are OBJ sprites (OAM), not BG
49
+ * tiles, so moving them is just an OAM rewrite — no per-frame BG writes.
50
+ * - The LOCKED well is BG tiles, updated through a COLLECT/FLUSH queue:
51
+ * collect_well() decides what to write (RAM only); flush_well() writes a
52
+ * few cells to VRAM as the very first thing in vblank. The whole
53
+ * per-frame job (OAM DMA + flush) MUST finish inside the ~10-line vblank
54
+ * window — overrunning into active display silently DROPS writes on this
55
+ * core. An idle "scrub" continuously repaints the well from the grid so
56
+ * nothing can drift (the "3 stones that won't clear" bug heals itself).
57
+ * - The HUD (score / hi-score / level) lives on the WINDOW layer — a fixed
58
+ * strip at the bottom of the screen, immune to BG scrolling.
59
+ * - We NEVER toggle the LCD in-game. LCD-off is used only for the
60
+ * full-screen title <-> game transitions.
25
61
  */
26
-
27
62
  #include "gb_hardware.h"
28
63
  #include "gb_runtime.h"
29
64
 
30
- #define COLS 8
31
- #define ROWS 14
32
-
33
- #define T_BLANK 0
34
- #define T_R 1
35
- #define T_G 2
36
- #define T_B 3
37
- #define T_WALL 4
38
-
39
- /* Map placement: centre the 8-col well → BG col offset +6, row offset +1. */
40
- #define WELL_MX 6
41
- #define WELL_MY 1
42
-
43
- /* tile_blank is the EMPTY-cell / backdrop tile. It is NOT all-zero: a
44
- * subtle dither (colour 0 + faint colour 1) so the empty playfield and the
45
- * area around the well read as a textured surface, never one flat colour
46
- * (the #1 GB "why is it blank" footgun). Locked blocks / the active piece
47
- * overdraw it with the R/G/B shape tiles. */
48
- static const uint8_t tile_blank[16] = {
65
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
66
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
67
+ #define GAME_TITLE "SHALE WELL"
68
+
69
+ /* ── GAME LOGIC (clay — reshape freely) ── board geometry */
70
+ #define COLS 8
71
+ #define ROWS 15 /* rows 0-14; floor at map row 15; window HUD rows 16-17 */
72
+ #define NCELL (ROWS * COLS)
73
+ #define NKINDS 5 /* stone kinds 1..5 — one tile SHAPE each */
74
+
75
+ /* BG map cell of interior grid cell (0,0) — the well's top-left corner.
76
+ * Open at the top (row 0); walls one cell outside left/right, floor below. */
77
+ #define WELL_MX 1
78
+ #define WELL_MY 0
79
+
80
+ /* BG map column where the right-hand panel (NEXT preview label) starts. */
81
+ #define HUD_X 12
82
+
83
+ #define G(r,c) grid[((r) * COLS) + (c)]
84
+ #define M(r,c) matched[((r) * COLS) + (c)]
85
+
86
+ /* Tile slots in the $8000 table. Stones 1..5 are tiles 1..5; T_WALL/T_EMPTY
87
+ * frame the well; T_MAGIC is the magic stone; explosions burst on a clear;
88
+ * FONT_BASE..FONT_BASE+36 are the 0-9 A-Z '-' glyphs (uploaded at boot). */
89
+ #define T_EMPTY 0
90
+ #define T_S1 1 /* stripe */
91
+ #define T_S2 2 /* checker */
92
+ #define T_S3 3 /* ring */
93
+ #define T_S4 4 /* brick */
94
+ #define T_S5 5 /* diamond */
95
+ #define T_WALL 6
96
+ #define T_MAGIC 7
97
+ #define T_EXP0 8 /* explosion frames: a stone bursting apart */
98
+ #define T_EXP1 9
99
+ #define T_EXP2 10
100
+ #define FONT_BASE 16 /* 0-9 → 16..25, A-Z → 26..51, '-' → 52 */
101
+
102
+ #define MAGIC 6 /* grid value of a magic stone in the falling column */
103
+
104
+ #define ST_TITLE 0
105
+ #define ST_PLAY 1
106
+ #define ST_OVER 2
107
+
108
+ /* VRAM tile maps. BG playfield = $9800; the window HUD = $9C00 (offset
109
+ * $400 in the same VRAM pointer — see the WINDOW HUD idiom below). */
110
+ #define VRAM ((volatile uint8_t *)0x9800)
111
+ #define WIN_OFF 0x400
112
+
113
+ /* ── GAME LOGIC (clay — reshape freely) ── tile pixel data (2bpp).
114
+ * Each 8x8 tile = 16 bytes, 2 bytes per row (low plane then high plane); a
115
+ * pixel's 2-bit value = (hi<<1)|lo indexes the DMG palette BGP (BG) or
116
+ * OBP0/OBP1 (OBJ). With BGP=$E4 below: 0=white, 1=light grey, 2=dark grey,
117
+ * 3=black. The five stone KINDS are five distinct SHAPES — that's how a
118
+ * 4-shade screen carries five readable "colors". */
119
+ static const uint8_t tile_empty[16] = { /* faint dither (never flat) */
49
120
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
50
121
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
51
122
  };
52
- /* Well frame: a solid colour-2 border drawn around the play area. */
53
- static const uint8_t tile_wall[16] = {
123
+ static const uint8_t tile_s1[16] = { /* stripe bold horizontal bars */
124
+ 0xFF,0xFF, 0xFF,0xFF, 0x00,0x00, 0x00,0x00,
125
+ 0xFF,0xFF, 0xFF,0xFF, 0x00,0x00, 0x00,0x00,
126
+ };
127
+ static const uint8_t tile_s2[16] = { /* checker — alternating dark blocks */
128
+ 0xCC,0xCC, 0xCC,0xCC, 0x33,0x33, 0x33,0x33,
129
+ 0xCC,0xCC, 0xCC,0xCC, 0x33,0x33, 0x33,0x33,
130
+ };
131
+ static const uint8_t tile_s3[16] = { /* ring — hollow circle, light fill */
132
+ 0x3C,0x3C, 0x42,0x7E, 0x42,0x7E, 0x42,0x7E,
133
+ 0x42,0x7E, 0x42,0x7E, 0x42,0x7E, 0x3C,0x3C,
134
+ };
135
+ static const uint8_t tile_s4[16] = { /* brick — mortar grid */
136
+ 0xFF,0xFF, 0x88,0x88, 0x88,0x88, 0xFF,0xFF,
137
+ 0x22,0x22, 0x22,0x22, 0xFF,0xFF, 0x88,0x88,
138
+ };
139
+ static const uint8_t tile_s5[16] = { /* diamond — solid lozenge */
140
+ 0x18,0x18, 0x3C,0x3C, 0x7E,0x7E, 0xFF,0xFF,
141
+ 0xFF,0xFF, 0x7E,0x7E, 0x3C,0x3C, 0x18,0x18,
142
+ };
143
+ static const uint8_t tile_wall[16] = { /* solid frame */
54
144
  0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
55
145
  0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
56
146
  };
57
- /* Three distinct tile shapes (since GB BG is 2bpp, we differentiate
58
- * by *shape*, not colour-on-CGB). */
59
- static const uint8_t tile_r[16] = {
60
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
61
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
147
+ static const uint8_t tile_magic[16] = { /* star clears its target kind */
148
+ 0x18,0x18, 0x18,0x3C, 0xDB,0xFF, 0x7E,0x7E,
149
+ 0x3C,0x3C, 0x7E,0x66, 0xC3,0xC3, 0x81,0x81,
150
+ };
151
+ /* explosion frames: the stone bursts into a star, fragments fly outward,
152
+ * then sparks, then gone. Shown ONCE, expanding — no blinking. */
153
+ static const uint8_t tile_exp0[16] = {
154
+ 0x99,0x99, 0x5A,0x5A, 0x3C,0x3C, 0xFF,0xFF,
155
+ 0xFF,0xFF, 0x3C,0x3C, 0x5A,0x5A, 0x99,0x99,
62
156
  };
63
- static const uint8_t tile_g[16] = {
64
- 0xAA,0x55, 0xAA,0x55, 0xAA,0x55, 0xAA,0x55,
65
- 0xAA,0x55, 0xAA,0x55, 0xAA,0x55, 0xAA,0x55,
157
+ static const uint8_t tile_exp1[16] = {
158
+ 0x81,0x81, 0x42,0x42, 0x24,0x24, 0x18,0x18,
159
+ 0x18,0x18, 0x24,0x24, 0x42,0x42, 0x81,0x81,
66
160
  };
67
- static const uint8_t tile_b[16] = {
68
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
69
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
161
+ static const uint8_t tile_exp2[16] = {
162
+ 0x81,0x81, 0x00,0x00, 0x00,0x00, 0x00,0x00,
163
+ 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x81,0x81,
70
164
  };
71
165
 
72
- static const uint16_t bg_palette[4] = { 0x7FFF, 0x5294, 0x294A, 0x0000 };
166
+ /* ── GAME LOGIC (clay reshape freely) ── 1bpp font (same glyph set as the
167
+ * platformer/shmup — 0-9, A-Z, '-'). Stored 8 bytes/glyph and expanded to
168
+ * 2bpp shade 3 (black) at upload time, so the ROM carries 296 bytes of font
169
+ * instead of 592. */
170
+ static const uint8_t font8[37][8] = {
171
+ /* 0-9 */
172
+ {0x3C,0x66,0x6E,0x76,0x66,0x66,0x3C,0x00}, {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
173
+ {0x3C,0x66,0x06,0x0C,0x18,0x30,0x7E,0x00}, {0x3C,0x66,0x06,0x1C,0x06,0x66,0x3C,0x00},
174
+ {0x0C,0x1C,0x3C,0x6C,0x7E,0x0C,0x0C,0x00}, {0x7E,0x60,0x7C,0x06,0x06,0x66,0x3C,0x00},
175
+ {0x1C,0x30,0x60,0x7C,0x66,0x66,0x3C,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
176
+ {0x3C,0x66,0x66,0x3C,0x66,0x66,0x3C,0x00}, {0x3C,0x66,0x66,0x3E,0x06,0x0C,0x38,0x00},
177
+ /* A-Z */
178
+ {0x18,0x3C,0x66,0x66,0x7E,0x66,0x66,0x00}, {0x7C,0x66,0x66,0x7C,0x66,0x66,0x7C,0x00},
179
+ {0x3C,0x66,0x60,0x60,0x60,0x66,0x3C,0x00}, {0x78,0x6C,0x66,0x66,0x66,0x6C,0x78,0x00},
180
+ {0x7E,0x60,0x60,0x7C,0x60,0x60,0x7E,0x00}, {0x7E,0x60,0x60,0x7C,0x60,0x60,0x60,0x00},
181
+ {0x3C,0x66,0x60,0x6E,0x66,0x66,0x3E,0x00}, {0x66,0x66,0x66,0x7E,0x66,0x66,0x66,0x00},
182
+ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00}, {0x06,0x06,0x06,0x06,0x66,0x66,0x3C,0x00},
183
+ {0x66,0x6C,0x78,0x70,0x78,0x6C,0x66,0x00}, {0x60,0x60,0x60,0x60,0x60,0x60,0x7E,0x00},
184
+ {0x63,0x77,0x7F,0x6B,0x63,0x63,0x63,0x00}, {0x66,0x76,0x7E,0x7E,0x6E,0x66,0x66,0x00},
185
+ {0x3C,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x7C,0x66,0x66,0x7C,0x60,0x60,0x60,0x00},
186
+ {0x3C,0x66,0x66,0x66,0x6A,0x6C,0x36,0x00}, {0x7C,0x66,0x66,0x7C,0x6C,0x66,0x66,0x00},
187
+ {0x3C,0x66,0x60,0x3C,0x06,0x66,0x3C,0x00}, {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
188
+ {0x66,0x66,0x66,0x66,0x66,0x66,0x3C,0x00}, {0x66,0x66,0x66,0x66,0x66,0x3C,0x18,0x00},
189
+ {0x63,0x63,0x63,0x6B,0x7F,0x77,0x63,0x00}, {0x66,0x66,0x3C,0x18,0x3C,0x66,0x66,0x00},
190
+ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00}, {0x7E,0x06,0x0C,0x18,0x30,0x60,0x7E,0x00},
191
+ /* '-' */
192
+ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
193
+ };
73
194
 
74
- #define NCELL (ROWS * COLS)
75
- static uint8_t grid[NCELL]; /* 0 = empty, 1..3 = block colour */
76
- static uint8_t shadow[NCELL]; /* what's currently on the BG (diff redraw) */
77
- static uint8_t matched[NCELL]; /* scratch: cells flagged to clear */
78
- static uint8_t piece[3];
79
- static int16_t piece_x, piece_y;
80
- static uint8_t fall_timer;
81
- static uint16_t score;
82
- static uint16_t rng = 0xACE1;
195
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
196
+ * WRAM layout keep big board state ABOVE the shadow-OAM page.
197
+ * The OAM-DMA shadow buffer is pinned by the runtime at $C100 (one page,
198
+ * $C100-$C19F). oam_clear() zeros that whole page every... no — oam_clear
199
+ * zeros the 160-byte shadow_oam — but more to the point, the DMA source
200
+ * lives there. SDCC allocates ordinary statics upward from $C000; with the
201
+ * board's three 120-byte arrays that segment would run straight THROUGH
202
+ * $C100 and collide with shadow_oam — the build links fine and then the
203
+ * grid and the sprite table silently corrupt each other at runtime.
204
+ *
205
+ * The fix used here: pin the three big arrays at FIXED addresses ABOVE the
206
+ * shadow-OAM page with `__at`, so the auto-allocated _DATA segment stays a
207
+ * handful of bytes at $C000 and never reaches $C100. (The GBC sister
208
+ * example instead passes dataLoc:0xC200 to its build recipe — same goal,
209
+ * pushing statics above the page. `__at` keeps the choice IN the source so
210
+ * a fork can't lose it to a forgotten build flag, and so this template
211
+ * builds with the plain default-dataLoc recipe the test harness uses.)
212
+ * If you ADD large arrays, place them at $C2xx+ too, or you'll re-introduce
213
+ * the collision. $C200-$DFFF is free work RAM. */
214
+ static __at(0xC200) uint8_t grid[NCELL]; /* the well: 0=empty, 1..NKINDS=stone */
215
+ static __at(0xC280) uint8_t shadow[NCELL]; /* what's on the BG now (diff redraw) */
216
+ static __at(0xC300) uint8_t matched[NCELL]; /* scratch: cells flagged for clearing */
83
217
 
84
- #define G(r,c) grid[(uint8_t)((r) * COLS + (c))]
85
- #define M(r,c) matched[(uint8_t)((r) * COLS + (c))]
218
+ /* ── GAME LOGIC (clay — reshape freely) ── game state (small — auto _DATA) */
219
+ static uint8_t piece[3]; /* the 3 falling kinds, top→bottom */
220
+ static uint8_t nextp[3]; /* the previewed next column */
221
+ static uint8_t piece_x, piece_y; /* well coords of the falling column's top */
222
+ static uint8_t piece_active; /* a column is currently falling */
223
+ static uint8_t piece_magic; /* the falling column is a MAGIC piece */
224
+ static uint8_t next_dirty; /* NEXT-preview sprites need re-writing */
225
+ static uint8_t piece_counter; /* pieces since last magic (→ magic every 18) */
226
+ static uint8_t fall_timer; /* frames since the column last stepped down */
227
+ static uint8_t cur_fall_rate; /* frames per downward step (lower = faster) */
228
+ static uint16_t total_cleared; /* stones cleared this game (drives level) */
229
+ static uint8_t level;
230
+ static uint8_t score_d[6]; /* 6-digit BCD score, most significant first */
231
+ static uint8_t hi_d[6]; /* 6-digit BCD hi-score (battery SRAM) */
232
+ static uint8_t state; /* ST_TITLE / ST_PLAY / ST_OVER */
233
+ static uint8_t chain; /* cascade depth of the current resolve */
234
+ static uint16_t rng = 0xACE1; /* xorshift PRNG state */
86
235
 
87
- /* the 4 line directions scanned for matches: horizontal, vertical, and
88
- * both diagonals; each line is only walked from its lowest cell. */
236
+ /* the 4 line directions we scan for matches: horizontal, vertical, and the
237
+ * two diagonals (we only walk each line once, from its lowest cell). */
89
238
  static const int8_t DIRS[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
90
239
 
91
- /* 16-bit xorshift — kept 16-bit on purpose (sm83 has no fast 32-bit
240
+ /* 16-bit xorshift PRNG — kept 16-bit on purpose (sm83 has no fast 32-bit
92
241
  * shifts; a wider generator there degenerates toward one value). */
93
242
  static uint8_t xorshift(void) {
94
243
  rng ^= rng << 7;
@@ -97,41 +246,270 @@ static uint8_t xorshift(void) {
97
246
  return (uint8_t)(rng >> 8);
98
247
  }
99
248
 
100
- static uint8_t random_colour(void) { return 1 + (xorshift() % 3); }
249
+ /* fill a 3-stone column with random kinds 1..NKINDS */
250
+ static void roll(uint8_t *p) {
251
+ p[0] = 1 + (uint8_t)(xorshift() % NKINDS);
252
+ p[1] = 1 + (uint8_t)(xorshift() % NKINDS);
253
+ p[2] = 1 + (uint8_t)(xorshift() % NKINDS);
254
+ }
101
255
 
102
- static void new_piece(void) {
103
- piece[0] = random_colour();
104
- piece[1] = random_colour();
105
- piece[2] = random_colour();
106
- piece_x = COLS / 2 - 1;
107
- piece_y = -3;
256
+ /* map a stone kind (1..NKINDS, or MAGIC) to its tile slot */
257
+ static uint8_t tile_for(uint8_t kind) {
258
+ if (kind == 0) return T_EMPTY;
259
+ if (kind == MAGIC) return T_MAGIC;
260
+ return (uint8_t)(T_S1 + (kind - 1)); /* 1→T_S1 .. 5→T_S5 */
108
261
  }
109
262
 
110
- static uint8_t tile_for(uint8_t c) {
111
- switch (c) {
112
- case 1: return T_R;
113
- case 2: return T_G;
114
- case 3: return T_B;
115
- default: return T_BLANK;
263
+ /* add to the 6-digit BCD score (score_d[0] = most significant), with carry */
264
+ static void add_score(uint16_t amt) {
265
+ uint8_t k, idx;
266
+ uint16_t carry = amt;
267
+ for (k = 0; k < 6; k++) {
268
+ if (carry == 0) break;
269
+ idx = 5 - k;
270
+ carry += score_d[idx];
271
+ score_d[idx] = (uint8_t)(carry % 10);
272
+ carry = carry / 10;
116
273
  }
117
274
  }
118
275
 
119
- static uint8_t collides(int16_t col, int16_t row) {
276
+ /* most-significant-digit-first BCD compare: did this run beat the record? */
277
+ static uint8_t score_beats_hi(void) {
120
278
  uint8_t i;
121
- int16_t r;
122
- if (col < 0 || col >= COLS) return 1;
123
- for (i = 0; i < 3; i++) {
124
- r = row + i;
125
- if (r >= ROWS) return 1;
126
- if (r >= 0 && G(r, col) != 0) return 1;
279
+ for (i = 0; i < 6; i++) {
280
+ if (score_d[i] > hi_d[i]) return 1;
281
+ if (score_d[i] < hi_d[i]) return 0;
127
282
  }
128
283
  return 0;
129
284
  }
130
285
 
131
- /* ── match / clear / gravity core (mirrors the GBC reference) ─────── */
286
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
287
+ * BATTERY SRAM hi-score — persistent saves on a Game Boy cart.
288
+ * requires: gb_crt0.s declaring MBC1+RAM+BATTERY in the cartridge header
289
+ * ($0147=$03, $0149=$02 → 8KB at $A000-$BFFF). With a ROM-only header the
290
+ * $A000 region is OPEN BUS: writes vanish, reads return garbage, and
291
+ * nothing tells you why. The header is the save system.
292
+ *
293
+ * The MBC powers up with cart RAM DISABLED (protection against corrupting
294
+ * the battery RAM with stray bus traffic while power rails settle). The
295
+ * $0A-enable dance:
296
+ * 1. write $0A to anywhere in $0000-$1FFF → RAM enabled
297
+ * 2. read/write $A000-$BFFF → real battery RAM
298
+ * 3. write $00 to $0000-$1FFF → RAM disabled again
299
+ * ALWAYS re-disable after access — that's what makes a yanked cartridge /
300
+ * dying battery corrupt at most the bytes mid-write, not the whole save.
301
+ *
302
+ * First boot is GARBAGE, not zeros: battery RAM holds whatever the silicon
303
+ * woke up with. The magic bytes + XOR checksum below are how the load path
304
+ * tells "my save" from "factory noise" — without them a fresh cart shows a
305
+ * junk hi-score like 974382.
306
+ *
307
+ * Save block at $A000: 'H' 'S' d0 d1 d2 d3 d4 d5 ck
308
+ * (6 BCD digits, most significant first; ck = d0^..^d5^$A5)
309
+ * No timing constraints — SRAM is not VRAM; access it any time. */
310
+ #define SRAM_BASE ((volatile uint8_t *)0xA000)
311
+ #define MBC_RAMG (*(volatile uint8_t *)0x0000) /* MBC1 RAM-gate register */
132
312
 
133
- /* Flag every cell in a 3+ run (any of the 4 directions) into matched[];
134
- * return the count. A run is walked once, from its lowest end only. */
313
+ static void hiscore_load(void) {
314
+ uint8_t i, ck;
315
+ MBC_RAMG = 0x0A; /* enable cart RAM */
316
+ ck = 0xA5;
317
+ for (i = 0; i < 6; i++) ck ^= SRAM_BASE[2 + i];
318
+ if (SRAM_BASE[0] == 'H' && SRAM_BASE[1] == 'S' && SRAM_BASE[8] == ck) {
319
+ for (i = 0; i < 6; i++) {
320
+ hi_d[i] = SRAM_BASE[2 + i];
321
+ if (hi_d[i] > 9) hi_d[i] = 9; /* belt + braces on a bad digit */
322
+ }
323
+ } else {
324
+ for (i = 0; i < 6; i++) hi_d[i] = 0; /* first boot / corrupt → 0 */
325
+ }
326
+ MBC_RAMG = 0x00; /* ALWAYS re-disable */
327
+ }
328
+
329
+ static void hiscore_save(void) {
330
+ uint8_t i, ck;
331
+ MBC_RAMG = 0x0A;
332
+ SRAM_BASE[0] = 'H';
333
+ SRAM_BASE[1] = 'S';
334
+ ck = 0xA5;
335
+ for (i = 0; i < 6; i++) {
336
+ SRAM_BASE[2 + i] = hi_d[i];
337
+ ck ^= hi_d[i];
338
+ }
339
+ SRAM_BASE[8] = ck;
340
+ MBC_RAMG = 0x00;
341
+ }
342
+
343
+ /* ── GAME LOGIC (clay — reshape freely) ── sound effects.
344
+ * A tiny note sequencer driving square channel 2 directly. Each note has a
345
+ * real volume-decay envelope (NR22) so it fades instead of clicking off (a
346
+ * hard NRx2=0 cut every note sounds like static). sfx_tick() advances one
347
+ * step per frame; multi-note effects become little arpeggios. GB period
348
+ * p ⇒ freq = 131072/(2048-p); higher p = higher note. */
349
+ #define P_C4 1548
350
+ #define P_G4 1714
351
+ #define P_A4 1750
352
+ #define P_C5 1797
353
+ #define P_E5 1849
354
+ #define P_G5 1881
355
+ #define P_A5 1899
356
+ #define P_C6 1923
357
+
358
+ /* NR21 duty: 0x40 = 25% (soft), 0x80 = 50% (full). NR22 vol/env byte:
359
+ * (volume<<4)|(0=decay)|envPace — bigger pace = slower fade. */
360
+ #define SFX_STEPS 4
361
+ static uint16_t sfx_p[SFX_STEPS];
362
+ static uint8_t sfx_v[SFX_STEPS];
363
+ static uint8_t sfx_d[SFX_STEPS];
364
+ static uint8_t sfx_f[SFX_STEPS];
365
+ static uint8_t sfx_n, sfx_i, sfx_t;
366
+
367
+ static void sfx_tick(void) {
368
+ if (sfx_i >= sfx_n) return;
369
+ if (sfx_t != 0) { sfx_t--; return; }
370
+ NR21 = sfx_d[sfx_i];
371
+ NR22 = sfx_v[sfx_i];
372
+ NR23 = (uint8_t)(sfx_p[sfx_i] & 0xFF);
373
+ NR24 = (uint8_t)(0x80 | (sfx_p[sfx_i] >> 8)); /* trigger (let envelope end it) */
374
+ sfx_t = sfx_f[sfx_i];
375
+ sfx_i++;
376
+ }
377
+
378
+ static void sfx_go(uint8_t n) { sfx_n = n; sfx_i = 0; sfx_t = 0; sfx_tick(); }
379
+
380
+ static void sfx_move(void) {
381
+ sfx_p[0] = P_A5; sfx_v[0] = 0x81; sfx_d[0] = 0x40; sfx_f[0] = 4;
382
+ sfx_go(1);
383
+ }
384
+ static void sfx_rotate(void) {
385
+ sfx_p[0] = P_C6; sfx_v[0] = 0x81; sfx_d[0] = 0x40; sfx_f[0] = 4;
386
+ sfx_go(1);
387
+ }
388
+ static void sfx_drop(void) {
389
+ sfx_p[0] = P_C5; sfx_v[0] = 0xC2; sfx_d[0] = 0x80; sfx_f[0] = 3;
390
+ sfx_p[1] = P_C4; sfx_v[1] = 0xC3; sfx_d[1] = 0x80; sfx_f[1] = 8;
391
+ sfx_go(2);
392
+ }
393
+ static void sfx_clear(void) { /* bright ascending C-E-G */
394
+ sfx_p[0] = P_C5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 4;
395
+ sfx_p[1] = P_E5; sfx_v[1] = 0xD2; sfx_d[1] = 0x80; sfx_f[1] = 4;
396
+ sfx_p[2] = P_G5; sfx_v[2] = 0xD3; sfx_d[2] = 0x80; sfx_f[2] = 8;
397
+ sfx_go(3);
398
+ }
399
+ static void sfx_chain(uint8_t n) { /* arpeggio whose top note rises per chain */
400
+ uint16_t top = (uint16_t)(P_C6 + (uint16_t)n * 6);
401
+ if (top > 1980) top = 1980;
402
+ sfx_p[0] = P_E5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 3;
403
+ sfx_p[1] = P_G5; sfx_v[1] = 0xD2; sfx_d[1] = 0x80; sfx_f[1] = 3;
404
+ sfx_p[2] = top; sfx_v[2] = 0xD3; sfx_d[2] = 0x80; sfx_f[2] = 8;
405
+ sfx_go(3);
406
+ }
407
+ static void sfx_over(void) { /* slow descending */
408
+ sfx_p[0] = P_A4; sfx_v[0] = 0xC3; sfx_d[0] = 0x80; sfx_f[0] = 10;
409
+ sfx_p[1] = P_G4; sfx_v[1] = 0xC3; sfx_d[1] = 0x80; sfx_f[1] = 10;
410
+ sfx_p[2] = P_C4; sfx_v[2] = 0xC5; sfx_d[2] = 0x80; sfx_f[2] = 24;
411
+ sfx_go(3);
412
+ }
413
+
414
+ /* ── GAME LOGIC (clay — reshape freely) ── background music.
415
+ * A looping square-wave lead on channel 1 (SFX live on channel 2, so they
416
+ * mix and the effects cut through the music). music_tick() plays one melody
417
+ * step every 12 frames, re-triggering ch1 at a steady volume. Toggle on/off
418
+ * with SELECT — defaults ON.
419
+ *
420
+ * The melody is the GB 11-bit period split into low/high BYTE arrays (NR13 +
421
+ * NR14 low 3 bits) — period p ⇒ freq 131072/(2048-p). hi == 0xFF marks a
422
+ * rest. Arpeggios over a C - Am - F - G chord loop, 8 steps each. */
423
+ static const uint8_t mel_lo[32] = {
424
+ 0x06,0x39,0x59,0x83, 0x59,0x39,0x06,0x00, /* C E G C6 G E C - */
425
+ 0xD6,0x06,0x39,0x6B, 0x39,0x06,0xD6,0x00, /* A C E A5 E C A - */
426
+ 0x88,0xD6,0x06,0x44, 0x06,0xD6,0x88,0x00, /* F A C F5 C A F - */
427
+ 0xB2,0xF7,0x21,0x59, 0x21,0xF7,0xB2,0x00, /* G B D G5 D B G - */
428
+ };
429
+ static const uint8_t mel_hi[32] = { /* high 3 bits; 0xFF = rest */
430
+ 0x07,0x07,0x07,0x07, 0x07,0x07,0x07,0xFF,
431
+ 0x06,0x07,0x07,0x07, 0x07,0x07,0x06,0xFF,
432
+ 0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
433
+ 0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
434
+ };
435
+ static uint8_t music_on;
436
+ static uint8_t music_idx;
437
+ static uint8_t music_timer;
438
+
439
+ static void music_note(uint8_t idx) {
440
+ uint8_t hi = mel_hi[idx];
441
+ if (hi == 0xFF) { NR12 = 0x00; NR14 = 0x80; return; } /* rest: silence ch1 */
442
+ NR10 = 0x00; /* no sweep */
443
+ NR11 = 0x80; /* 50% duty, no length counter */
444
+ NR12 = 0x90; /* volume 9, no envelope (steady lead) */
445
+ NR13 = mel_lo[idx];
446
+ NR14 = (uint8_t)(0x80 | hi); /* trigger + freq high bits */
447
+ }
448
+
449
+ static void music_tick(void) {
450
+ if (!music_on) return;
451
+ if (music_timer == 0) {
452
+ music_note(music_idx);
453
+ music_timer = 12;
454
+ if (++music_idx >= 32) music_idx = 0;
455
+ }
456
+ music_timer--;
457
+ }
458
+
459
+ static void music_toggle(void) {
460
+ music_on = (uint8_t)(!music_on);
461
+ music_idx = 0;
462
+ music_timer = 0;
463
+ if (!music_on) { NR12 = 0x00; NR14 = 0x80; } /* kill the lead immediately */
464
+ }
465
+
466
+ /* ── GAME LOGIC (clay — reshape freely) ── board mechanics */
467
+
468
+ /* is grid cell (r,col) off the bottom or already filled? */
469
+ static uint8_t cell_blocked(uint8_t r, uint8_t col) {
470
+ if (r >= ROWS) return 1;
471
+ return grid[(uint8_t)(r * COLS + col)] ? 1 : 0;
472
+ }
473
+
474
+ /* would the 3-tall falling column collide if its top cell were at (col,topy)?
475
+ * Checks are unrolled (not a loop) — short indexed-read loops can miscompile on
476
+ * sm83, and this is the hottest correctness check in the game. */
477
+ static uint8_t collides(uint8_t col, uint8_t topy) {
478
+ if (col >= COLS) return 1;
479
+ if (cell_blocked(topy, col)) return 1;
480
+ if (cell_blocked((uint8_t)(topy + 1), col)) return 1;
481
+ if (cell_blocked((uint8_t)(topy + 2), col)) return 1;
482
+ return 0;
483
+ }
484
+
485
+ static void game_over(void);
486
+
487
+ /* start a new falling column at the top-center. Every 18th piece is a MAGIC
488
+ * column; otherwise take the previewed kinds and roll the next preview. If
489
+ * it can't even appear, the well is full → game over. */
490
+ static void spawn(void) {
491
+ rng ^= DIV;
492
+ if (++piece_counter >= 18) {
493
+ piece_counter = 0;
494
+ piece_magic = 1;
495
+ piece[0] = MAGIC; piece[1] = MAGIC; piece[2] = MAGIC;
496
+ } else {
497
+ piece_magic = 0;
498
+ piece[0] = nextp[0]; piece[1] = nextp[1]; piece[2] = nextp[2];
499
+ roll(nextp);
500
+ }
501
+ piece_x = COLS / 2 - 1;
502
+ piece_y = 0;
503
+ piece_active = 1;
504
+ fall_timer = 0;
505
+ next_dirty = 1;
506
+ if (collides(piece_x, piece_y)) game_over();
507
+ }
508
+
509
+ /* Flag every stone that's part of a run of 3+ same-kind cells in any of the
510
+ * 4 directions, into matched[]; return how many cells were flagged. Each line
511
+ * is counted from its lowest end only (we skip a cell if its predecessor in
512
+ * that direction is the same kind), so runs aren't double-walked. */
135
513
  static uint8_t mark_and_count(void) {
136
514
  uint8_t r, c, d, len, cnt, col, k;
137
515
  int8_t dr, dc;
@@ -149,7 +527,7 @@ static uint8_t mark_and_count(void) {
149
527
  sr = (int16_t)r - dr;
150
528
  sc = (int16_t)c - dc;
151
529
  if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
152
- && G(sr, sc) == col) continue; /* not the run's start */
530
+ && G(sr, sc) == col) continue;
153
531
  len = 1;
154
532
  sr = (int16_t)r + dr;
155
533
  sc = (int16_t)c + dc;
@@ -177,13 +555,13 @@ static uint8_t mark_and_count(void) {
177
555
  return cnt;
178
556
  }
179
557
 
558
+ /* empty every flagged cell */
180
559
  static void clear_marked(void) {
181
560
  uint8_t i;
182
561
  for (i = 0; i < NCELL; i++) if (matched[i]) grid[i] = 0;
183
562
  }
184
563
 
185
- /* collapse each column so survivors rest on the floor the "rows move
186
- * down after a clear" the old template was missing. */
564
+ /* collapse each column so all stones rest on the floor with no gaps */
187
565
  static void apply_gravity(void) {
188
566
  uint8_t c, r, n, w;
189
567
  uint8_t buf[ROWS];
@@ -197,230 +575,592 @@ static void apply_gravity(void) {
197
575
  }
198
576
  }
199
577
 
200
- /* clear chime one short square blip per cascade step, pitch rises with
201
- * the chain so combos audibly escalate. */
202
- static void sfx_clear(uint8_t chain) {
203
- uint16_t p = 1797 + (uint16_t)chain * 26; /* ~C5 rising */
204
- if (p > 1980) p = 1980;
205
- sound_play_tone(1, p, 6);
578
+ /* level rises every 15 cleared stones (capped at 13); each level shortens
579
+ * the frames-per-row fall interval, so the column drops faster. */
580
+ static void update_level(void) {
581
+ level = (uint8_t)(total_cleared / 15);
582
+ if (level > 13) level = 13;
583
+ cur_fall_rate = 32 - level * 2;
584
+ if (cur_fall_rate < 4) cur_fall_rate = 4;
206
585
  }
207
586
 
208
- /* settle the board after a lock: match clear gravity, looping so
209
- * cascades chain; score scales with the chain depth. */
587
+ /* Matched stones burst apart before they clear a one-shot expanding star
588
+ * (no blinking, no LCD-off). Only ever runs on a real match. Direct vblank
589
+ * writes (no contending OAM DMA, so plenty of room); blocks ~6 frames, which
590
+ * is the satisfying beat. */
591
+ static void explode_matched(void) {
592
+ uint8_t i, j, n, tile;
593
+ uint16_t offs[8];
594
+ uint8_t *o = (uint8_t *)0xC100;
595
+ for (i = 0; i < 12; i++) *o++ = 0; /* hide the falling-piece sprites */
596
+ ((void (*)(uint8_t))0xFF80)(0xC1);
597
+ n = 0;
598
+ for (i = 0; i < NCELL && n < 8; i++) {
599
+ if (matched[i]) {
600
+ offs[n] = (uint16_t)(WELL_MY + (i >> 3)) * 32 + WELL_MX + (i & 7);
601
+ n++;
602
+ }
603
+ }
604
+ for (j = 0; j < 9; j++) {
605
+ tile = (j < 3) ? T_EXP0 : (j < 6) ? T_EXP1 : T_EXP2;
606
+ wait_vblank();
607
+ sfx_tick();
608
+ music_tick();
609
+ for (i = 0; i < n; i++) VRAM[offs[i]] = tile;
610
+ }
611
+ }
612
+
613
+ /* Settle the board after a lock: repeatedly find matches, burst+clear them,
614
+ * score, and apply gravity — looping so cascades chain. Score per clear
615
+ * scales with level and (for 2nd+ cascades) the chain depth. */
210
616
  static void resolve_board(void) {
211
- uint8_t n, chain = 0;
212
- uint16_t amt;
617
+ uint8_t n;
618
+ uint16_t amt, mult;
619
+ chain = 0;
213
620
  while (1) {
214
621
  n = mark_and_count();
215
622
  if (n == 0) break;
216
623
  chain++;
217
- sfx_clear(chain);
624
+ sfx_chain(chain);
625
+ explode_matched();
218
626
  clear_marked();
219
- amt = (uint16_t)n * 10;
627
+ mult = (uint16_t)(10 + level * 2);
628
+ amt = (uint16_t)n * mult;
220
629
  if (chain > 1) amt = amt * chain;
221
- if (score < (uint16_t)(65500u - amt)) score += amt;
630
+ if (amt > 60000) amt = 60000;
631
+ add_score(amt);
632
+ total_cleared += n;
222
633
  apply_gravity();
223
634
  }
635
+ update_level();
224
636
  }
225
637
 
226
- static void lock_piece(void) {
227
- uint8_t i, written = 0;
228
- int16_t r;
229
- for (i = 0; i < 3; i++) {
230
- r = piece_y + i;
231
- if (r >= 0 && r < ROWS) { G(r, piece_x) = piece[i]; written++; }
638
+ /* MAGIC column: clears every stone sharing the kind of whatever it landed
639
+ * on, then resolves any resulting cascades. */
640
+ static void magic_clear(void) {
641
+ uint8_t below = (uint8_t)(piece_y + 3);
642
+ uint8_t target, i;
643
+ uint16_t cleared = 0;
644
+ piece_active = 0;
645
+ if (below < ROWS) {
646
+ target = G(below, piece_x);
647
+ if (target != 0 && target != MAGIC) {
648
+ for (i = 0; i < NCELL; i++)
649
+ if (grid[i] == target) { grid[i] = 0; cleared++; }
650
+ if (cleared) {
651
+ add_score((uint16_t)cleared * 20u);
652
+ total_cleared += cleared;
653
+ sfx_clear();
654
+ }
655
+ apply_gravity();
656
+ }
232
657
  }
233
- sound_play_noise(3);
234
658
  resolve_board();
235
- if (written == 0) {
236
- /* The piece locked entirely ABOVE the well — the stack reached the
237
- * top. Without this the game silently softlocks (invisible pieces
238
- * locking off-screen forever). Scaffold behavior: low game-over
239
- * tone, clear the board, restart the run. */
240
- sound_play_tone(1, 1548, 30);
241
- for (i = 0; i < NCELL; i++) grid[i] = 0;
242
- score = 0;
243
- }
244
659
  }
245
660
 
246
- /* ── rendering (vblank-budgeted; gameplay code never touches VRAM) ── */
661
+ /* Stamp the falling column into the grid where it came to rest, then
662
+ * resolve. A magic column takes its own path. */
663
+ static void lock_and_resolve(void) {
664
+ uint8_t i, r;
665
+ if (piece_magic) { magic_clear(); return; }
666
+ for (i = 0; i < 3; i++) {
667
+ r = (uint8_t)(piece_y + i);
668
+ if (r < ROWS) G(r, piece_x) = piece[i];
669
+ }
670
+ piece_active = 0;
671
+ resolve_board();
672
+ }
247
673
 
674
+ /* ── rendering ─────────────────────────────────────────────────────── */
675
+ /* copy one 16-byte 2bpp tile into VRAM tile slot `slot` ($8000 + slot*16) */
248
676
  static void upload_tile(uint8_t slot, const uint8_t *src) {
249
- uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
250
677
  /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
251
678
  * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
252
- memcpy_vram(dst, src, 16);
679
+ memcpy_vram((uint8_t *)(0x8000 + (uint16_t)slot * 16), src, 16);
680
+ }
681
+
682
+ /* expand the 1bpp font into VRAM as 2bpp shade-3 glyphs (both planes set) */
683
+ static void upload_font(void) {
684
+ uint8_t *dst = (uint8_t *)(0x8000 + (uint16_t)FONT_BASE * 16);
685
+ uint8_t g, r, bits;
686
+ for (g = 0; g < 37; g++) {
687
+ for (r = 0; r < 8; r++) {
688
+ bits = font8[g][r];
689
+ *dst++ = bits; /* low plane ─┐ both set → shade 3 (black) */
690
+ *dst++ = bits; /* high plane ─┘ */
691
+ }
692
+ }
693
+ }
694
+
695
+ /* The falling column = sprites 0-2; the NEXT preview = sprites 3-5. Then
696
+ * flush OAM. MUST be the first VRAM/OAM work after wait_vblank: the OAM DMA
697
+ * has to land in vblank, or sprites tear on a fixed scanline near the top. */
698
+ static void update_sprites(void) {
699
+ /* Write shadow_oam ($C100) directly with a walking pointer — calling
700
+ * oam_set() six times burns ~10 scanlines of vblank (SDCC call
701
+ * overhead), starving the BG flush. Inlined it's ~2 lines. */
702
+ uint8_t *o = (uint8_t *)0xC100;
703
+ uint8_t i, t0, t1, t2, sx, sy;
704
+ if (piece_active) {
705
+ if (piece_magic) { t0 = t1 = t2 = T_MAGIC; }
706
+ else { t0 = tile_for(piece[0]); t1 = tile_for(piece[1]); t2 = tile_for(piece[2]); }
707
+ sx = (uint8_t)((WELL_MX + piece_x) * 8 + 8);
708
+ sy = (uint8_t)((WELL_MY + piece_y) * 8 + 16);
709
+ *o++ = sy; *o++ = sx; *o++ = t0; *o++ = 0;
710
+ *o++ = (uint8_t)(sy + 8); *o++ = sx; *o++ = t1; *o++ = 0;
711
+ *o++ = (uint8_t)(sy + 16); *o++ = sx; *o++ = t2; *o++ = 0;
712
+ } else {
713
+ for (i = 0; i < 12; i++) *o++ = 0;
714
+ }
715
+ /* NEXT preview (sprites 3-5) only changes on a spawn — skip it most
716
+ * frames to keep the OAM build short enough to leave the BG flush vblank. */
717
+ if (next_dirty) {
718
+ next_dirty = 0;
719
+ o = (uint8_t *)0xC10C; /* sprite slot 3 */
720
+ if (state == ST_TITLE) {
721
+ for (i = 0; i < 12; i++) *o++ = 0;
722
+ } else {
723
+ sx = (uint8_t)((HUD_X + 1) * 8 + 8);
724
+ for (i = 0; i < 3; i++) {
725
+ *o++ = (uint8_t)((3 + i) * 8 + 16);
726
+ *o++ = sx;
727
+ *o++ = tile_for(nextp[i]);
728
+ *o++ = 0;
729
+ }
730
+ }
731
+ }
732
+ /* Trigger the OAM DMA via the HRAM stub directly (skip the oam_dma_flush
733
+ * / oam_dma_copy wrappers). A = high byte of shadow_oam ($C100). */
734
+ ((void (*)(uint8_t))0xFF80)(0xC1);
735
+ }
736
+
737
+ /* direct BG-map cell write — ONLY safe with the LCD off or in a bounded
738
+ * vblank batch (the in-game path queues instead — see collect/flush). */
739
+ static void set_cell(uint8_t mx, uint8_t my, uint8_t tile) {
740
+ VRAM[(uint16_t)my * 32 + mx] = tile;
741
+ }
742
+
743
+ /* same write into the WINDOW's map at $9C00 (see the window idiom) */
744
+ static void set_wcell(uint8_t wx, uint8_t wy, uint8_t tile) {
745
+ VRAM[WIN_OFF + (uint16_t)wy * 32 + wx] = tile;
253
746
  }
254
747
 
255
- #define VRAM_MAP ((volatile uint8_t *)0x9800)
748
+ /* map an ASCII char to its font tile slot (digits, then A-Z, then '-') */
749
+ static uint8_t font_slot(char ch) {
750
+ if (ch >= '0' && ch <= '9') return FONT_BASE + (uint8_t)(ch - '0');
751
+ if (ch >= 'A' && ch <= 'Z') return FONT_BASE + 10 + (uint8_t)(ch - 'A');
752
+ if (ch == '-') return FONT_BASE + 36;
753
+ return T_EMPTY;
754
+ }
256
755
 
257
- /* Direct cell write ONLY safe with the LCD off or just after vblank. */
258
- static void set_cell(uint8_t c, uint8_t r, uint8_t tile) {
259
- VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX + c] = tile;
756
+ /* draw a NUL-terminated string into the BG map starting at (col,row) */
757
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
758
+ uint8_t i;
759
+ for (i = 0; s[i] != 0; i++)
760
+ set_cell((uint8_t)(col + i), row, font_slot(s[i]));
260
761
  }
261
762
 
262
- /* COLLECT/FLUSH split (the reference puzzle's architecture, and the part
263
- * that actually fixes "pieces flash / don't render"):
264
- * - collect_well() runs OUTSIDE vblank: scans for grid-vs-shadow diffs
265
- * (bounded), or queues rolling SCRUB cells when nothing changed, into a
266
- * tiny queue of precomputed (map offset, tile) pairs. RAM only.
267
- * - flush_well() runs FIRST thing after wait_vblank: pure pointer writes,
268
- * no scanning, no multiplies — the whole batch lands inside the ~10-line
269
- * vblank window every frame. The scrub means even a write the core drops
270
- * anyway heals itself on the next pass instead of sticking forever. */
271
- #define WQ_MAX 4
763
+ /* draw a NUL-terminated string into the WINDOW map starting at (col,row) */
764
+ static void draw_wtext(uint8_t col, uint8_t row, const char *s) {
765
+ uint8_t i;
766
+ for (i = 0; s[i] != 0; i++)
767
+ set_wcell((uint8_t)(col + i), row, font_slot(s[i]));
768
+ }
769
+
770
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
771
+ * WINDOW-layer HUD a fixed strip the BG scroll can never move.
772
+ * requires: LCDC bits 5 (window on) + 6 (window map = $9C00), WX/WY set,
773
+ * and HUD text written to the $9C00 map (set_wcell), not the $9800 one.
774
+ *
775
+ * The window is the GB's second BG plane: same tile data, its OWN 32x32
776
+ * map, drawn OVER the BG starting at screen position (WX-7, WY) and
777
+ * extending to the bottom-right. It ignores SCX/SCY completely — that's
778
+ * the point: scroll the playfield all you want, the HUD strip stays put.
779
+ * Classic placements: a bottom status bar (this game: WY=128 → the last
780
+ * 16 pixel rows) or a full-width top bar. It CANNOT be a floating box —
781
+ * the window always runs to the screen's bottom-right corner.
782
+ *
783
+ * Gotchas:
784
+ * - WX is offset by 7: WX=7 is the left edge. WX<7 glitches on hardware.
785
+ * - The window has its OWN line counter: it renders ITS map from window
786
+ * row 0 downward, regardless of WY — our HUD lives at $9C00 rows 0-1.
787
+ * - This is DMG-era hardware — it transplants to the GBC example unchanged.
788
+ *
789
+ * Window HUD layout (window map rows 0-1):
790
+ * row 0: SC dddddd HI dddddd row 1: LV dd
791
+ * Static labels drawn once at transitions; the digits go through the
792
+ * vblank queue (see collect_well) so in-game updates never tear. */
793
+ #define WINY 128 /* screen y where the strip starts */
794
+ #define HUD_SC_X 3 /* score digits, window row 0 */
795
+ #define HUD_HI_X 13 /* hi-score digits, window row 0 */
796
+ #define HUD_LV_X 3 /* level digits, window row 1 */
797
+
798
+ /* paint the whole window strip: blank backdrop + labels (LCD off only) */
799
+ static void draw_window_static(void) {
800
+ uint8_t x, y;
801
+ for (y = 0; y < 2; y++)
802
+ for (x = 0; x < 20; x++) set_wcell(x, y, T_EMPTY);
803
+ draw_wtext(0, 0, "SC");
804
+ draw_wtext(10, 0, "HI");
805
+ draw_wtext(0, 1, "LV");
806
+ }
807
+
808
+ /* draw every dynamic HUD value directly (LCD off / transitions only —
809
+ * in-game updates go through the queue) */
810
+ static void draw_hud_now(void) {
811
+ uint8_t i;
812
+ for (i = 0; i < 6; i++) {
813
+ set_wcell((uint8_t)(HUD_SC_X + i), 0, FONT_BASE + score_d[i]);
814
+ set_wcell((uint8_t)(HUD_HI_X + i), 0, FONT_BASE + hi_d[i]);
815
+ }
816
+ set_wcell(HUD_LV_X, 1, FONT_BASE + (uint8_t)(level / 10));
817
+ set_wcell((uint8_t)(HUD_LV_X + 1), 1, FONT_BASE + (uint8_t)(level % 10));
818
+ }
819
+
820
+ /* Lay down the unchanging screen: clear the whole BG map, draw the well's
821
+ * walls + floor, and the window HUD. Only called with the LCD off (it
822
+ * writes entire maps at once). */
823
+ static void draw_static(void) {
824
+ uint8_t x, y;
825
+ for (y = 0; y < 18; y++)
826
+ for (x = 0; x < 20; x++) set_cell(x, y, T_EMPTY);
827
+ for (y = WELL_MY; y < (uint8_t)(WELL_MY + ROWS); y++) {
828
+ set_cell((uint8_t)(WELL_MX - 1), y, T_WALL);
829
+ set_cell((uint8_t)(WELL_MX + COLS), y, T_WALL);
830
+ }
831
+ for (x = (uint8_t)(WELL_MX - 1); x <= (uint8_t)(WELL_MX + COLS); x++)
832
+ set_cell(x, (uint8_t)(WELL_MY + ROWS), T_WALL);
833
+ draw_window_static();
834
+ }
835
+
836
+ /* Full LOCKED-well repaint from the grid (no piece — that's a sprite). Used
837
+ * only with the LCD OFF (boot / title↔game transitions), where writing all
838
+ * changed cells at once is safe. */
839
+ static void redraw_all(void) {
840
+ uint8_t r, c, col;
841
+ uint8_t i = 0;
842
+ for (r = 0; r < ROWS; r++) {
843
+ for (c = 0; c < COLS; c++) {
844
+ col = grid[i];
845
+ shadow[i] = col;
846
+ set_cell((uint8_t)(WELL_MX + c), (uint8_t)(WELL_MY + r), tile_for(col));
847
+ i++;
848
+ }
849
+ }
850
+ }
851
+
852
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
853
+ * Deferred well/HUD rendering — the vblank COLLECT/FLUSH queue.
854
+ * requires: update_sprites + flush_well as the FIRST two things after
855
+ * wait_vblank (in that order), batches capped at WQ_MAX, and no LCDC
856
+ * bit-7 toggling in-game.
857
+ *
858
+ * This core silently DROPS a VRAM write that lands during active display —
859
+ * and occasionally drops one even at the very start of vblank. So in-game
860
+ * we never touch the LCD; instead:
861
+ * COLLECT — queue work (RAM only): changed cells after a lock, the HUD
862
+ * digits, and — when idle — a rolling SCRUB of the whole well.
863
+ * FLUSH — write the queue to VRAM as the FIRST thing after wait_vblank.
864
+ * The scrub re-writes well cells from the grid continuously, so any dropped
865
+ * write self-corrects instead of becoming a permanent wrong shape (the "3
866
+ * stones that won't clear" bug). Idempotent ⇒ invisible.
867
+ * Batches are kept small so the whole flush fits in vblank AFTER the OAM
868
+ * DMA — overrunning into active display drops writes.
869
+ * Queue offsets are plain offsets from $9800, so the same queue serves the
870
+ * BG map (well) and the window map at $9800+$400 (HUD digits). */
871
+ #define WQ_MAX 6 /* queue capacity (≤4 pushed per frame) */
872
+ #define REDRAW_BUDGET 4 /* changed well cells per frame (responsive) */
873
+ #define SCRUB_N 4 /* idle cells re-written per frame (self-heal) */
874
+ static uint8_t scanning, hud_pending, over_pending;
875
+ static uint8_t hud_phase, over_phase; /* split big HUD/text writes across frames */
876
+ static uint8_t scan_i, scrub_i;
877
+
272
878
  static uint8_t wq_n;
273
879
  static uint16_t wq_off[WQ_MAX];
274
880
  static uint8_t wq_tile[WQ_MAX];
275
- static uint8_t diff_cursor, scrub_cursor;
881
+
882
+ static void start_redraw(void) { scanning = 1; scan_i = 0; }
883
+
884
+ static void wq_push(uint16_t off, uint8_t tile) {
885
+ if (wq_n < WQ_MAX) { wq_off[wq_n] = off; wq_tile[wq_n] = tile; wq_n++; }
886
+ }
887
+
888
+ static void wq_text(uint8_t col, uint8_t row, const char *s) {
889
+ uint8_t i;
890
+ for (i = 0; s[i] != 0; i++)
891
+ wq_push((uint16_t)row * 32 + col + i, font_slot(s[i]));
892
+ }
893
+
894
+ /* queue one window-HUD digit cell (window map = offset $400) */
895
+ static void wq_wdigit(uint8_t col, uint8_t row, uint8_t digit) {
896
+ wq_push(WIN_OFF + (uint16_t)row * 32 + col, FONT_BASE + digit);
897
+ }
276
898
 
277
899
  static uint16_t cell_off(uint8_t i) {
278
- return (uint16_t)(WELL_MY + i / COLS) * 32 + WELL_MX + (i % COLS);
900
+ return (uint16_t)(WELL_MY + (i >> 3)) * 32 + WELL_MX + (i & 7);
279
901
  }
280
902
 
903
+ /* Fill the queue with the next batch of pending changes (RAM only).
904
+ * Each branch pushes at most REDRAW_BUDGET cells, so the flush always fits
905
+ * in vblank; the HUD digits and game-over text are split across frames. */
281
906
  static void collect_well(void) {
282
- uint8_t scanned = 0, i, k;
907
+ uint8_t col, k, i;
283
908
  wq_n = 0;
284
- i = diff_cursor;
285
- while (scanned < NCELL && wq_n < WQ_MAX) {
286
- if (grid[i] != shadow[i]) {
287
- shadow[i] = grid[i];
288
- wq_off[wq_n] = cell_off(i);
289
- wq_tile[wq_n] = tile_for(grid[i]);
290
- wq_n++;
909
+ if (scanning) {
910
+ while (scan_i < NCELL && wq_n < REDRAW_BUDGET) {
911
+ col = grid[scan_i];
912
+ if (col != shadow[scan_i]) {
913
+ shadow[scan_i] = col;
914
+ wq_push(cell_off(scan_i), tile_for(col));
915
+ }
916
+ scan_i++;
291
917
  }
292
- i++;
293
- if (i >= NCELL) i = 0;
294
- scanned++;
295
- }
296
- diff_cursor = i;
297
- if (wq_n == 0) {
298
- /* idle: queue scrub cells so dropped writes self-heal */
299
- for (k = 0; k < 2; k++) {
300
- wq_off[wq_n] = cell_off(scrub_cursor);
301
- wq_tile[wq_n] = tile_for(grid[scrub_cursor]);
302
- wq_n++;
303
- scrub_cursor++;
304
- if (scrub_cursor >= NCELL) scrub_cursor = 0;
918
+ if (scan_i >= NCELL) { scanning = 0; hud_pending = 1; hud_phase = 0; }
919
+ } else if (hud_pending) {
920
+ if (hud_phase == 0) { /* score digits 0-3 */
921
+ for (i = 0; i < 4; i++) wq_wdigit((uint8_t)(HUD_SC_X + i), 0, score_d[i]);
922
+ hud_phase = 1;
923
+ } else if (hud_phase == 1) { /* score 4-5 + level */
924
+ wq_wdigit(HUD_SC_X + 4, 0, score_d[4]);
925
+ wq_wdigit(HUD_SC_X + 5, 0, score_d[5]);
926
+ wq_wdigit(HUD_LV_X, 1, (uint8_t)(level / 10));
927
+ wq_wdigit(HUD_LV_X + 1, 1, (uint8_t)(level % 10));
928
+ hud_phase = 2;
929
+ } else if (hud_phase == 2) { /* hi-score digits 0-3 */
930
+ for (i = 0; i < 4; i++) wq_wdigit((uint8_t)(HUD_HI_X + i), 0, hi_d[i]);
931
+ hud_phase = 3;
932
+ } else { /* hi-score digits 4-5 */
933
+ wq_wdigit(HUD_HI_X + 4, 0, hi_d[4]);
934
+ wq_wdigit(HUD_HI_X + 5, 0, hi_d[5]);
935
+ hud_pending = 0;
936
+ if (state == ST_OVER) { over_pending = 1; over_phase = 0; }
937
+ }
938
+ } else if (over_pending) {
939
+ if (over_phase == 0) { wq_text(3, 6, "GAME"); over_phase = 1; }
940
+ else { wq_text(3, 7, "OVER"); over_pending = 0; }
941
+ } else if (state == ST_PLAY) {
942
+ /* idle: rolling scrub of the well so any dropped write heals itself.
943
+ * Only during play — would erase the title pile / game-over text. */
944
+ for (k = 0; k < SCRUB_N; k++) {
945
+ wq_push(cell_off(scrub_i), tile_for(grid[scrub_i]));
946
+ scrub_i++;
947
+ if (scrub_i >= NCELL) scrub_i = 0;
305
948
  }
306
949
  }
307
950
  }
308
951
 
952
+ /* Write the queued cells to VRAM. MUST run first after wait_vblank (right
953
+ * after the OAM DMA), and MUST finish inside the ~10-line vblank window or
954
+ * writes drop. Pointer-walk (SDCC sm83 generates tighter code for *p++). */
309
955
  static void flush_well(void) {
310
- uint8_t k;
311
- for (k = 0; k < wq_n; k++) VRAM_MAP[wq_off[k]] = wq_tile[k];
956
+ uint8_t k = wq_n;
957
+ uint16_t *op = wq_off;
958
+ uint8_t *tp = wq_tile;
959
+ while (k != 0) { VRAM[*op++] = *tp++; k--; }
312
960
  wq_n = 0;
313
961
  }
314
962
 
315
- /* The falling piece = OAM sprites 0-2 (written to shadow_oam, flushed by
316
- * one OAM DMA right after vblank starts). Rows above the well top (r < 0)
317
- * park the sprite at Y=0 (offscreen). */
318
- static void update_piece_sprites(void) {
319
- uint8_t i, sy, sx;
320
- int16_t r;
321
- for (i = 0; i < 3; i++) {
322
- r = piece_y + i;
323
- if (r >= 0 && r < ROWS) {
324
- sy = (uint8_t)((WELL_MY + r) * 8 + 16);
325
- sx = (uint8_t)((WELL_MX + piece_x) * 8 + 8);
326
- oam_set(i, sy, sx, tile_for(piece[i]), 0);
327
- } else {
328
- oam_set(i, 0, 0, 0, 0);
963
+ /* ── GAME LOGIC (clay reshape freely) ── title screen.
964
+ * A jagged pile of all five stone kinds dresses the well it doubles as
965
+ * the "here are the five shapes you'll match" legend. */
966
+ static const uint8_t title_heights[COLS] = { 4, 6, 3, 7, 5, 6, 4, 5 };
967
+
968
+ static void draw_title(void) {
969
+ uint8_t x, y, c, k, kind;
970
+ /* clear the right panel (NEXT label from a previous game) */
971
+ for (y = 0; y <= 15; y++)
972
+ for (x = 10; x <= 19; x++) set_cell(x, y, T_EMPTY);
973
+ /* decorative pile at the bottom of the well, cycling the five kinds */
974
+ kind = 1;
975
+ for (c = 0; c < COLS; c++) {
976
+ for (k = 0; k < title_heights[c]; k++) {
977
+ y = (uint8_t)(ROWS - 1 - k);
978
+ set_cell((uint8_t)(WELL_MX + c), (uint8_t)(WELL_MY + y), tile_for(kind));
979
+ kind++; if (kind > NKINDS) kind = 1;
329
980
  }
330
981
  }
982
+ /* game name + prompt, centered across the full 20-column screen */
983
+ draw_text((uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), 2, GAME_TITLE);
984
+ draw_text(4, 4, "PRESS START");
331
985
  }
332
986
 
333
- static void draw_well_frame(void) {
334
- uint8_t r;
335
- for (r = 0; r < ROWS; r++) {
336
- VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX - 1] = T_WALL;
337
- VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX + COLS] = T_WALL;
987
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
988
+ * LCD-off transitions. Only flip LCDC bit 7 to 0 DURING VBLANK. Killing the
989
+ * LCD mid-scanline is the classic "damages real DMG hardware" move;
990
+ * emulators shrug, real units can be permanently marked. wait_vblank()
991
+ * first, always. blit_on enables BG + OBJ + the WINDOW (map $9C00). NEVER
992
+ * call these from the in-game loop (the off-frame blanks the whole screen —
993
+ * a flash/strobe). */
994
+ static void blit_off(void) { wait_vblank(); LCDC = 0; }
995
+ static void blit_on(void) {
996
+ LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO
997
+ | LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI;
998
+ }
999
+
1000
+ /* zero the board and all run stats for a fresh game (shadow set to 0xFF so
1001
+ * the first redraw repaints every cell). Does not touch music_on or hi_d. */
1002
+ static void reset_state(void) {
1003
+ uint8_t i;
1004
+ for (i = 0; i < NCELL; i++) grid[i] = 0;
1005
+ for (i = 0; i < NCELL; i++) shadow[i] = 0xFF;
1006
+ for (i = 0; i < 6; i++) score_d[i] = 0;
1007
+ total_cleared = 0;
1008
+ level = 0;
1009
+ cur_fall_rate = 32;
1010
+ fall_timer = 0;
1011
+ piece_counter = 0;
1012
+ piece_magic = 0;
1013
+ }
1014
+
1015
+ /* leave the title and begin play: reset, seed the first piece + preview, and
1016
+ * rebuild the screen with the LCD off. */
1017
+ static void start_game(void) {
1018
+ reset_state();
1019
+ state = ST_PLAY;
1020
+ roll(nextp);
1021
+ spawn();
1022
+ blit_off();
1023
+ draw_static();
1024
+ redraw_all();
1025
+ draw_text(HUD_X, 1, "NEXT");
1026
+ draw_hud_now();
1027
+ blit_on();
1028
+ update_sprites();
1029
+ scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
1030
+ }
1031
+
1032
+ /* show the title screen (stone pile + name + PRESS START + persisted HI) */
1033
+ static void go_title(void) {
1034
+ reset_state();
1035
+ piece_active = 0;
1036
+ state = ST_TITLE;
1037
+ blit_off();
1038
+ draw_static();
1039
+ redraw_all();
1040
+ draw_title();
1041
+ draw_hud_now();
1042
+ next_dirty = 1;
1043
+ blit_on();
1044
+ update_sprites();
1045
+ scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
1046
+ }
1047
+
1048
+ /* the run is over: persist a new record, then let the queue paint GAME OVER
1049
+ * + the updated HI digits (hud_pending → over_pending chain). */
1050
+ static void game_over(void) {
1051
+ piece_active = 0;
1052
+ state = ST_OVER;
1053
+ sfx_over();
1054
+ if (score_beats_hi()) {
1055
+ uint8_t i;
1056
+ for (i = 0; i < 6; i++) hi_d[i] = score_d[i];
1057
+ hiscore_save(); /* battery SRAM — survives power-off */
338
1058
  }
339
- for (r = 0; r < (uint8_t)(COLS + 2); r++)
340
- VRAM_MAP[(uint16_t)(WELL_MY + ROWS) * 32 + WELL_MX - 1 + r] = T_WALL;
341
1059
  }
342
1060
 
343
1061
  void main(void) {
344
- uint8_t pad, prev = 0, fall_rate, t, i;
345
- int16_t c;
346
- uint8_t *map;
1062
+ uint8_t pad, prev = 0, t, rate;
347
1063
 
1064
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
1065
+ * Boot order: LCD defaults (installs the OAM-DMA HRAM stub) → vblank IRQ
1066
+ * (so wait_vblank HALTs instead of busy-polling LY — the poll runs at
1067
+ * ~1/30 speed on this core) → APU on → LCD OFF → then all the bulk VRAM
1068
+ * work (tiles, font, maps). Tile/font/map uploads REQUIRE a VRAM-safe
1069
+ * window and boot does them all at once, so LCD-off is the only sane
1070
+ * choice here. The window position registers are plain I/O — set once,
1071
+ * they hold. */
348
1072
  lcd_init_default();
349
1073
  enable_vblank_irq();
350
1074
  sound_init();
351
1075
  oam_dma_init_hram();
352
1076
  oam_clear();
1077
+ music_on = 1; /* background music on by default (SELECT toggles) */
353
1078
  LCDC = 0;
354
- OBP0 = 0xE4; /* DMG sprite palette: 3=black .. 0=white */
1079
+ WY = WINY; /* window HUD strip: bottom 16 pixel rows */
1080
+ WX = 7; /* WX is offset by 7 — this is the left edge */
355
1081
 
356
- upload_tile(T_BLANK, tile_blank);
357
- upload_tile(T_R, tile_r);
358
- upload_tile(T_G, tile_g);
359
- upload_tile(T_B, tile_b);
360
- upload_tile(T_WALL, tile_wall);
1082
+ /* DMG palettes (2 bits/shade, low bits = index 0):
1083
+ * BGP $E4 → 0=white 1=light 2=dark 3=black (stones + walls + text).
1084
+ * OBP0 $E4 → falling/preview stones match their locked twins exactly. */
1085
+ BGP = 0xE4;
1086
+ OBP0 = 0xE4;
1087
+ OBP1 = 0xE4;
361
1088
 
362
- BCPS = 0x80;
363
- for (i = 0; i < 4; i++) {
364
- BCPD = (uint8_t)(bg_palette[i] & 0xFF);
365
- BCPD = (uint8_t)((bg_palette[i] >> 8) & 0xFF);
366
- }
1089
+ upload_tile(T_EMPTY, tile_empty);
1090
+ upload_tile(T_S1, tile_s1);
1091
+ upload_tile(T_S2, tile_s2);
1092
+ upload_tile(T_S3, tile_s3);
1093
+ upload_tile(T_S4, tile_s4);
1094
+ upload_tile(T_S5, tile_s5);
1095
+ upload_tile(T_WALL, tile_wall);
1096
+ upload_tile(T_MAGIC, tile_magic);
1097
+ upload_tile(T_EXP0, tile_exp0);
1098
+ upload_tile(T_EXP1, tile_exp1);
1099
+ upload_tile(T_EXP2, tile_exp2);
1100
+ upload_font();
367
1101
 
368
- map = (uint8_t *)0x9800;
369
- for (i = 0; i < 32; i++) {
370
- c = 0;
371
- while (c < 32) { map[(uint16_t)i * 32 + c] = T_BLANK; c++; }
372
- }
1102
+ hiscore_load(); /* battery SRAM — 0 on a fresh cart */
1103
+ go_title();
373
1104
 
374
- for (i = 0; i < NCELL; i++) { grid[i] = 0; shadow[i] = 0; }
1105
+ /* Main loop, one pass per frame. The order is deliberate: the two VRAM/
1106
+ * OAM writers (sprites, then the bounded BG flush) run FIRST so they land
1107
+ * inside vblank; audio and game logic follow; the next frame's BG writes
1108
+ * are queued last (RAM only) for the following frame's flush. */
1109
+ while (1) {
1110
+ wait_vblank();
1111
+ update_sprites(); /* OAM DMA FIRST — must land in vblank (no tear) */
1112
+ flush_well(); /* then drain queued BG writes (≤4, fits vblank) */
1113
+ sfx_tick();
1114
+ music_tick();
375
1115
 
376
- score = 0;
377
- fall_timer = 0;
378
- rng ^= DIV; /* a dash of boot-time entropy */
379
- new_piece();
380
- draw_well_frame();
1116
+ pad = joypad_read();
381
1117
 
382
- LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO;
1118
+ /* SELECT toggles the background music, in any state */
1119
+ if ((pad & PAD_SELECT) && !(prev & PAD_SELECT)) music_toggle();
383
1120
 
384
- while (1) {
385
- pad = joypad_read();
1121
+ if (state == ST_TITLE) {
1122
+ /* ── GAME LOGIC (clay — reshape freely) ── press-start title
1123
+ * (handheld: no 2P mode select — see the header note) */
1124
+ if ((pad & PAD_START) && !(prev & PAD_START)) start_game();
1125
+ } else if (state == ST_PLAY) {
1126
+ /* ── GAME LOGIC (clay — reshape freely) ── one frame of play */
1127
+ if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
1128
+ && !collides((uint8_t)(piece_x - 1), piece_y)) { piece_x--; sfx_move(); }
1129
+ if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
1130
+ && !collides((uint8_t)(piece_x + 1), piece_y)) { piece_x++; sfx_move(); }
1131
+ if ((pad & PAD_A) && !(prev & PAD_A) && !piece_magic) {
1132
+ t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
1133
+ sfx_rotate();
1134
+ }
1135
+ if ((pad & PAD_B) && !(prev & PAD_B) && !piece_magic) {
1136
+ t = piece[2]; piece[2] = piece[1]; piece[1] = piece[0]; piece[0] = t;
1137
+ sfx_rotate();
1138
+ }
1139
+ if ((pad & PAD_START) && !(prev & PAD_START)) {
1140
+ while (!collides(piece_x, (uint8_t)(piece_y + 1))) piece_y++;
1141
+ sfx_drop();
1142
+ lock_and_resolve();
1143
+ spawn();
1144
+ start_redraw();
1145
+ }
386
1146
 
387
- if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
388
- && !collides(piece_x - 1, piece_y)) { piece_x--; sound_play_tone(1, 1899, 2); }
389
- if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
390
- && !collides(piece_x + 1, piece_y)) { piece_x++; sound_play_tone(1, 1899, 2); }
391
- if ((pad & PAD_A) && !(prev & PAD_A)) {
392
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
393
- sound_play_tone(1, 1923, 2);
394
- }
395
- if ((pad & PAD_B) && !(prev & PAD_B)) {
396
- t = piece[2]; piece[2] = piece[1]; piece[1] = piece[0]; piece[0] = t;
397
- sound_play_tone(1, 1923, 2);
398
- }
399
- if ((pad & PAD_START) && !(prev & PAD_START)) {
400
- while (!collides(piece_x, piece_y + 1)) piece_y++;
401
- lock_piece();
402
- new_piece();
403
- } else {
404
- fall_rate = (pad & PAD_DOWN) ? 4 : 30;
405
- if (++fall_timer >= fall_rate) {
1147
+ rate = (pad & PAD_DOWN) ? 3 : cur_fall_rate;
1148
+ if (state == ST_PLAY && ++fall_timer >= rate) {
406
1149
  fall_timer = 0;
407
- if (collides(piece_x, piece_y + 1)) {
408
- lock_piece();
409
- new_piece();
1150
+ if (collides(piece_x, (uint8_t)(piece_y + 1))) {
1151
+ sfx_drop();
1152
+ lock_and_resolve();
1153
+ spawn();
1154
+ start_redraw();
410
1155
  } else {
411
1156
  piece_y++;
412
1157
  }
413
1158
  }
1159
+ } else { /* ST_OVER — START returns to the title (shows the new HI) */
1160
+ if ((pad & PAD_START) && !(prev & PAD_START)) go_title();
414
1161
  }
415
- prev = pad;
416
1162
 
417
- /* COLLECT (RAM only, runs in active display) */
418
- update_piece_sprites();
419
- collect_well();
420
- /* … then FLUSH right after vblank starts: OAM DMA first (sprites
421
- * tear if it slips out of vblank), then the queued BG writes. */
422
- wait_vblank();
423
- oam_dma_flush();
424
- flush_well();
1163
+ collect_well(); /* queue next frame's VRAM writes (RAM only) */
1164
+ prev = pad;
425
1165
  }
426
1166
  }