romdevtools 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -1,45 +1,76 @@
1
- /* PUZZLEa falling-jewel matcher for Game Boy Color.
1
+ /* ── puzzle.c CHROMA WELL: Game Boy Color falling-jewel matcher (complete example game) ──
2
2
  *
3
- * A vertical column of 3 jewels falls into an 8-wide x 17-tall well. Move it
4
- * left/right, soft-drop (Down), hard-drop (Start), and CYCLE the three colors
5
- * (A/B). Line up 3+ of one color horizontally, vertically, or diagonally to
6
- * clear them; gravity pulls the survivors down, which can chain. Every 18th
7
- * piece is a MAGIC jewel that clears every gem of the color it lands on.
8
- * SELECT toggles the background music.
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.
9
+ *
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.
9
33
  *
10
34
  * RENDERING — the hard-won architecture (details at each routine below):
11
35
  * - The FALLING column and the NEXT preview are OBJ sprites (OAM), not BG
12
36
  * tiles, so moving them is just an OAM rewrite — no per-frame BG writes.
13
- * - The LOCKED well + HUD are BG tiles, updated through a COLLECT/FLUSH queue:
14
- * redraw_collect() decides what to write (RAM only); redraw_flush() writes a
15
- * few cells to VRAM as the very first thing in vblank. The whole per-frame
16
- * job (OAM DMA + flush) MUST finish inside the ~10-line vblank window —
17
- * overrunning into active display silently DROPS writes on this core, which
18
- * is exactly what made the screen disagree with the board. An idle "scrub"
19
- * continuously repaints the well from the grid so nothing can drift.
20
- * - We NEVER toggle the LCD in-game (this core blanks the whole frame on any
21
- * LCDC bit-7 toggle a strobe). LCD-off is used only for the full-screen
22
- * title <-> game transitions.
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.
23
49
  *
24
50
  * WRAM NOTE: build with dataLoc:0xC200 so our statics sit ABOVE shadow_oam
25
- * ($C100) — else oam_clear() would zero our state (RNG seed / grid).
51
+ * ($C100) — else oam_clear() would zero our state (RNG seed / grid). The
52
+ * project build recipe sets that automatically.
26
53
  */
27
54
  #include "gb_hardware.h"
28
55
  #include "gb_runtime.h"
29
56
  #include "font.h"
30
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 */
31
63
  #define COLS 8
32
- #define ROWS 17
64
+ #define ROWS 15 /* rows 0-14; floor at map row 15; window HUD rows 16-17 */
33
65
  #define NCELL (ROWS * COLS)
34
- #define NCOLORS 6 /* jewel colors 1..6 */
66
+ #define NCOLORS 6 /* jewel colors 1..6 — one CGB palette each */
35
67
 
36
68
  /* BG map cell of interior grid cell (0,0) — the well's top-left corner.
37
- * Open at the top (row 0); walls sit one cell outside on the left/right and
38
- * below the bottom (floor at WELL_MY+ROWS = row 17, the last screen row). */
69
+ * Open at the top (row 0); walls one cell outside left/right, floor below. */
39
70
  #define WELL_MX 1
40
71
  #define WELL_MY 0
41
72
 
42
- /* BG map column where the HUD text starts (right of the well). */
73
+ /* BG map column where the right-hand panel (NEXT preview) starts. */
43
74
  #define HUD_X 12
44
75
 
45
76
  #define G(r,c) grid[((r) * COLS) + (c)]
@@ -60,13 +91,20 @@
60
91
  #define PAL_WELL 6
61
92
  #define PAL_OUT 7
62
93
 
63
- #define ST_PLAY 0
64
- #define ST_OVER 1
65
- #define ST_TITLE 2
94
+ #define ST_TITLE 0
95
+ #define ST_PLAY 1
96
+ #define ST_OVER 2
66
97
 
67
98
  #define VRAM ((volatile uint8_t *)0x9800)
68
-
69
- /* ── tile pixel data (2bpp) ────────────────────────────────────────── */
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. */
70
108
  static const uint8_t tile_empty[16] = {
71
109
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
72
110
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
@@ -100,7 +138,9 @@ static const uint8_t tile_exp2[16] = {
100
138
  0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x81,
101
139
  };
102
140
 
103
- /* ── palettes (15-bit BGR) ─────────────────────────────────────────── */
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. */
104
144
  #define RGB(r,g,b) ((uint16_t)(((uint16_t)(b)<<10)|((uint16_t)(g)<<5)|(r)))
105
145
  #define C_WELL RGB(4,6,12)
106
146
  #define C_OUT RGB(1,2,4)
@@ -118,7 +158,7 @@ static const uint16_t palettes[8][4] = {
118
158
  /* 7 out/txt*/ { C_OUT, RGB(2,3,7), C_OUT, RGB(31,31,31) },
119
159
  };
120
160
 
121
- /* ── game state ────────────────────────────────────────────────────── */
161
+ /* ── GAME LOGIC (clay — reshape freely) ── game state */
122
162
  static uint8_t grid[NCELL]; /* the well: 0 = empty, 1..NCOLORS = a gem */
123
163
  static uint8_t matched[NCELL]; /* scratch: cells flagged for clearing */
124
164
  static uint8_t shadow[NCELL]; /* color currently on the BG, for diff redraw */
@@ -134,7 +174,8 @@ static uint8_t cur_fall_rate; /* frames per downward step (lower = faster)
134
174
  static uint16_t total_cleared; /* gems cleared this game (drives level) */
135
175
  static uint8_t level;
136
176
  static uint8_t score_d[6]; /* 6-digit BCD score, most significant first */
137
- static uint8_t state; /* ST_PLAY / ST_OVER / ST_TITLE */
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 */
138
179
  static uint8_t chain; /* cascade depth of the current resolve */
139
180
  static uint16_t rng = 0xACE1; /* xorshift PRNG state */
140
181
 
@@ -171,12 +212,79 @@ static void add_score(uint16_t amt) {
171
212
  }
172
213
  }
173
214
 
174
- /* ── sound effects ──────────────────────────────────────────────────
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;
221
+ }
222
+ return 0;
223
+ }
224
+
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 */
266
+ }
267
+
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;
280
+ }
281
+
282
+ /* ── GAME LOGIC (clay — reshape freely) ── sound effects.
175
283
  * A tiny note sequencer driving square channel 2 directly. Each note has
176
284
  * a real volume-decay envelope (NR22) so it fades instead of clicking off
177
- * (the old sound_play_tone hard-cut every note that was the "static").
178
- * sfx_tick() advances one step per frame; multi-note effects become little
179
- * arpeggios. GB period p ⇒ freq = 131072/(2048-p); higher p = higher note. */
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. */
180
288
  #define P_C4 1548
181
289
  #define P_G4 1714
182
290
  #define P_A4 1750
@@ -242,7 +350,7 @@ static void sfx_over(void) { /* slow descending */
242
350
  sfx_go(3);
243
351
  }
244
352
 
245
- /* ── background music ───────────────────────────────────────────────
353
+ /* ── GAME LOGIC (clay — reshape freely) ── background music.
246
354
  * A looping square-wave lead on channel 1 (SFX live on channel 2, so they
247
355
  * mix and the effects cut through the music). music_tick() plays one melody
248
356
  * step every 12 frames, re-triggering ch1 at a steady volume. Toggle on/off
@@ -294,6 +402,8 @@ static void music_toggle(void) {
294
402
  if (!music_on) { NR12 = 0x00; NR14 = 0x80; } /* kill the lead immediately */
295
403
  }
296
404
 
405
+ /* ── GAME LOGIC (clay — reshape freely) ── board mechanics */
406
+
297
407
  /* is grid cell (r,col) off the bottom or already filled? */
298
408
  static uint8_t cell_blocked(uint8_t r, uint8_t col) {
299
409
  if (r >= ROWS) return 1;
@@ -311,6 +421,8 @@ static uint8_t collides(uint8_t col, uint8_t topy) {
311
421
  return 0;
312
422
  }
313
423
 
424
+ static void game_over(void);
425
+
314
426
  /* start a new falling column at the top-center. Every 18th piece is a MAGIC
315
427
  * column; otherwise take the previewed colors and roll the next preview. If
316
428
  * it can't even appear, the well is full → game over. */
@@ -330,10 +442,7 @@ static void spawn(void) {
330
442
  piece_active = 1;
331
443
  fall_timer = 0;
332
444
  next_dirty = 1;
333
- if (collides(piece_x, piece_y)) {
334
- piece_active = 0;
335
- state = ST_OVER;
336
- }
445
+ if (collides(piece_x, piece_y)) game_over();
337
446
  }
338
447
 
339
448
  /* Flag every gem that's part of a run of 3+ same-color cells in any of the 4
@@ -423,7 +532,7 @@ static void explode_matched(void) {
423
532
  uint16_t offs[8];
424
533
  uint8_t cols[8];
425
534
  uint8_t *o = (uint8_t *)0xC100;
426
- for (i = 0; i < 12; i++) *o++ = 0; /* hide the locked piece sprites */
535
+ for (i = 0; i < 12; i++) *o++ = 0; /* hide the falling-piece sprites */
427
536
  ((void (*)(uint8_t))0xFF80)(0xC1);
428
537
  n = 0;
429
538
  for (i = 0; i < NCELL && n < 8; i++) {
@@ -513,10 +622,25 @@ static void upload_tile(uint8_t slot, const uint8_t *src) {
513
622
  memcpy_vram((uint8_t *)(0x8000 + slot * 16), src, 16);
514
623
  }
515
624
 
516
- /* push all 8 BG palettes (4 colors each, 15-bit BGR) via the BCPS/BCPD port */
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. */
517
641
  static void load_palettes(void) {
518
642
  uint8_t p, i;
519
- BCPS = 0x80;
643
+ BCPS = 0x80; /* index 0, auto-increment on */
520
644
  for (p = 0; p < 8; p++)
521
645
  for (i = 0; i < 4; i++) {
522
646
  BCPD = (uint8_t)(palettes[p][i] & 0xFF);
@@ -524,8 +648,9 @@ static void load_palettes(void) {
524
648
  }
525
649
  }
526
650
 
527
- /* OBJ palettes: 0-5 = jewels (light/main/dark), 6 = magic white. Color 0 of
528
- * every OBJ palette is transparent (sprite shows the well behind it). */
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). */
529
654
  static void load_obj_palettes(void) {
530
655
  uint8_t p, i;
531
656
  uint16_t col;
@@ -541,9 +666,11 @@ static void load_obj_palettes(void) {
541
666
  }
542
667
 
543
668
  /* The falling column = sprites 0-2; the NEXT preview = sprites 3-5 (sprites
544
- * so their transparent corners blend with the dark HUD). Then flush OAM.
669
+ * so their transparent corners blend with the panel). Then flush OAM.
545
670
  * MUST be the first VRAM/OAM work after wait_vblank: the OAM DMA has to
546
- * land in vblank, or sprites tear on a fixed scanline near the top. */
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). */
547
674
  static void update_sprites(void) {
548
675
  /* Write shadow_oam ($C100) directly with a walking pointer — calling
549
676
  * oam_set() six times burns ~10 scanlines of vblank (SDCC call overhead),
@@ -570,9 +697,9 @@ static void update_sprites(void) {
570
697
  if (state == ST_TITLE) {
571
698
  for (i = 0; i < 12; i++) *o++ = 0;
572
699
  } else {
573
- sx = (uint8_t)((HUD_X + 2) * 8 + 8);
700
+ sx = (uint8_t)((HUD_X + 1) * 8 + 8);
574
701
  for (i = 0; i < 3; i++) {
575
- *o++ = (uint8_t)((9 + i) * 8 + 16);
702
+ *o++ = (uint8_t)((3 + i) * 8 + 16);
576
703
  *o++ = sx;
577
704
  *o++ = T_GEM;
578
705
  *o++ = (uint8_t)(nextp[i] - 1);
@@ -584,8 +711,28 @@ static void update_sprites(void) {
584
711
  ((void (*)(uint8_t))0xFF80)(0xC1);
585
712
  }
586
713
 
587
- /* write one BG map cell: tile index (VRAM bank 0) + palette/attr (bank 1).
588
- * Direct, unboundedonly safe with the LCD off or in a bounded vblank batch. */
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.) */
589
736
  static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
590
737
  uint16_t off = (uint16_t)my * 32 + mx;
591
738
  VBK = 0;
@@ -595,6 +742,16 @@ static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
595
742
  VBK = 0;
596
743
  }
597
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
+
598
755
  /* map an ASCII char to its font tile slot (digits, then A-Z); blank otherwise */
599
756
  static uint8_t font_slot(char ch) {
600
757
  if (ch >= '0' && ch <= '9') return FONT_BASE + (uint8_t)(ch - '0');
@@ -609,19 +766,69 @@ static void draw_text(uint8_t col, uint8_t row, const char *s) {
609
766
  set_cell((uint8_t)(col + i), row, font_slot(s[i]), PAL_OUT);
610
767
  }
611
768
 
612
- /* Only the DYNAMIC HUD values (~11 cells) the labels are static, drawn
613
- * once in draw_static. Small enough to write inside one vblank. */
614
- static void draw_hud(void) {
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
+ }
775
+
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) {
615
820
  uint8_t i;
616
- for (i = 0; i < 6; i++) set_cell((uint8_t)(HUD_X + i), 2, FONT_BASE + score_d[i], PAL_OUT);
617
- set_cell(HUD_X, 5, FONT_BASE + (uint8_t)(level / 10), PAL_OUT);
618
- set_cell((uint8_t)(HUD_X + 1), 5, FONT_BASE + (uint8_t)(level % 10), PAL_OUT);
619
- /* NEXT gems are sprites now (update_sprites) — nothing to draw here */
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);
620
827
  }
621
828
 
622
- /* Lay down the unchanging screen: clear the whole map, draw the well's left/
623
- * right/bottom walls, and the static HUD labels. Only called with the LCD off
624
- * (it writes the entire map at once). */
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). */
625
832
  static void draw_static(void) {
626
833
  uint8_t x, y;
627
834
  uint16_t off;
@@ -638,15 +845,12 @@ static void draw_static(void) {
638
845
  }
639
846
  for (x = (uint8_t)(WELL_MX - 1); x <= (uint8_t)(WELL_MX + COLS); x++)
640
847
  set_cell(x, (uint8_t)(WELL_MY + ROWS), T_WALL, PAL_WELL);
641
- /* static HUD labels (drawn once — the values come from draw_hud) */
642
- draw_text(HUD_X, 1, "SCORE");
643
- draw_text(HUD_X, 4, "LEVEL");
644
- draw_text(HUD_X, 7, "NEXT");
848
+ draw_window_static();
645
849
  }
646
850
 
647
- /* Full LOCKED-well redraw (no piece — that's a sprite). Used only with the
648
- * LCD OFF (boot / title↔game transitions), where writing all changed cells
649
- * at once is safe. */
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. */
650
854
  static void redraw_all(void) {
651
855
  uint8_t r, c, col;
652
856
  uint8_t i = 0;
@@ -667,19 +871,26 @@ static void redraw_all(void) {
667
871
  }
668
872
  }
669
873
 
670
- /* ── deferred well/HUD rendering (NO LCD toggling in-game) ───────────
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
+ *
671
880
  * This core blanks the whole frame on ANY LCDC bit-7 toggle (a strobe we
672
881
  * must never do), AND it occasionally drops a VRAM write even at the start
673
882
  * of vblank. So in-game we never touch the LCD; instead:
674
- * COLLECT — queue work (RAM only): changed cells after a lock, the HUD,
675
- * and — when idle — a rolling SCRUB of the whole well.
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.
676
885
  * FLUSH — write the queue to VRAM as the FIRST thing after wait_vblank.
677
886
  * The scrub re-writes every well cell from the grid every ~0.2s, so any
678
887
  * dropped write self-corrects instead of becoming a permanent wrong color
679
- * (the "3 oranges that won't clear" bug). Idempotent ⇒ invisible. */
680
- /* Batches are kept small so the whole flush fits in vblank AFTER the OAM DMA
681
- * — overrunning into active display drops writes (a garbage "burst" on lock
682
- * frames before the scrub heals them). */
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). */
683
894
  #define REDRAW_BUDGET 4 /* changed well cells per frame (responsive) */
684
895
  #define SCRUB_N 4 /* idle cells re-written per frame (self-heal) */
685
896
  #define WQ_MAX 6 /* queue capacity (≤4 pushed per frame) */
@@ -711,9 +922,14 @@ static void wq_text(uint8_t col, uint8_t row, const char *s) {
711
922
  wq_push((uint16_t)row * 32 + col + i, font_slot(s[i]), PAL_OUT);
712
923
  }
713
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
+
714
930
  /* Fill the queue with the next batch of pending changes (RAM only).
715
931
  * Each branch pushes at most REDRAW_BUDGET cells, so the flush always fits
716
- * in vblank; the HUD and game-over text are split across two frames. */
932
+ * in vblank; the HUD digits and game-over text are split across frames. */
717
933
  static void redraw_collect(void) {
718
934
  uint8_t col, k, r, c, i;
719
935
  wq_n = 0;
@@ -733,14 +949,20 @@ static void redraw_collect(void) {
733
949
  if (scan_i >= NCELL) { scanning = 0; hud_pending = 1; hud_phase = 0; }
734
950
  } else if (hud_pending) {
735
951
  if (hud_phase == 0) { /* score digits 0-3 */
736
- for (i = 0; i < 4; i++)
737
- wq_push((uint16_t)2 * 32 + HUD_X + i, FONT_BASE + score_d[i], PAL_OUT);
952
+ for (i = 0; i < 4; i++) wq_wdigit((uint8_t)(HUD_SC_X + i), 0, score_d[i]);
738
953
  hud_phase = 1;
739
- } else { /* score digits 4-5 + level */
740
- wq_push((uint16_t)2 * 32 + HUD_X + 4, FONT_BASE + score_d[4], PAL_OUT);
741
- wq_push((uint16_t)2 * 32 + HUD_X + 5, FONT_BASE + score_d[5], PAL_OUT);
742
- wq_push((uint16_t)5 * 32 + HUD_X, FONT_BASE + (uint8_t)(level / 10), PAL_OUT);
743
- wq_push((uint16_t)5 * 32 + HUD_X + 1, FONT_BASE + (uint8_t)(level % 10), PAL_OUT);
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]);
744
966
  hud_pending = 0;
745
967
  if (state == ST_OVER) { over_pending = 1; over_phase = 0; }
746
968
  }
@@ -762,9 +984,11 @@ static void redraw_collect(void) {
762
984
  }
763
985
  }
764
986
 
765
- /* Write the queued cells to VRAM. MUST run first after wait_vblank, and
766
- * MUST finish inside the ~10-line vblank window or writes drop. Pointer-walk
767
- * (not array indexing) — SDCC sm83 generates far tighter code for *p++. */
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. */
768
992
  static void redraw_flush(void) {
769
993
  uint8_t k = wq_n;
770
994
  uint16_t *op;
@@ -782,15 +1006,17 @@ static void redraw_flush(void) {
782
1006
  wq_n = 0;
783
1007
  }
784
1008
 
785
- /* a jagged, colorful gem pile to dress up the empty well on the title */
786
- static const uint8_t title_heights[COLS] = { 5, 7, 4, 8, 6, 7, 5, 6 };
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 };
787
1013
 
788
1014
  static void draw_title(void) {
789
1015
  uint8_t x, y, c, k, color;
790
- /* clear the right panel (overwrites the HUD labels from draw_static) */
791
- for (y = 0; y <= 17; y++)
1016
+ /* clear the right panel (NEXT label from a previous game) */
1017
+ for (y = 0; y <= 15; y++)
792
1018
  for (x = 10; x <= 19; x++) set_cell(x, y, T_EMPTY, PAL_OUT);
793
- /* decorative gems piled at the bottom of the well */
1019
+ /* decorative gems piled at the bottom of the well, cycling palettes */
794
1020
  color = 1;
795
1021
  for (c = 0; c < COLS; c++) {
796
1022
  for (k = 0; k < title_heights[c]; k++) {
@@ -800,20 +1026,23 @@ static void draw_title(void) {
800
1026
  color++; if (color > NCOLORS) color = 1;
801
1027
  }
802
1028
  }
803
- /* title text, aligned to the in-game HUD column */
804
- draw_text(HUD_X, 2, "PUZZLE");
805
- draw_text(HUD_X, 9, "PRESS");
806
- draw_text(HUD_X, 10, "START");
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");
807
1032
  }
808
1033
 
809
- /* LCD off / on — only used to bracket the full-screen rebuilds at the title and
810
- * game-start transitions. NEVER call these from the in-game loop (the off-frame
811
- * blanks the whole screen — a flash/strobe). */
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. */
812
1038
  static void blit_off(void) { wait_vblank(); LCDC = 0; }
813
- static void blit_on(void) { LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO; }
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
+ }
814
1043
 
815
1044
  /* zero the board and all run stats for a fresh game (shadow set to 0xFF so the
816
- * first redraw repaints every cell). Does not touch music_on. */
1045
+ * first redraw repaints every cell). Does not touch music_on or hi_d. */
817
1046
  static void reset_state(void) {
818
1047
  uint8_t i;
819
1048
  for (i = 0; i < NCELL; i++) grid[i] = 0;
@@ -837,13 +1066,14 @@ static void start_game(void) {
837
1066
  blit_off();
838
1067
  draw_static();
839
1068
  redraw_all();
840
- draw_hud();
1069
+ draw_text(HUD_X, 1, "NEXT");
1070
+ draw_hud_now();
841
1071
  blit_on();
842
1072
  update_sprites();
843
1073
  scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
844
1074
  }
845
1075
 
846
- /* show the title screen (decorative gem pile + PUZZLE / PRESS START) */
1076
+ /* show the title screen (gem pile + name + PRESS START + persisted HI) */
847
1077
  static void go_title(void) {
848
1078
  reset_state();
849
1079
  piece_active = 0;
@@ -852,22 +1082,44 @@ static void go_title(void) {
852
1082
  draw_static();
853
1083
  redraw_all();
854
1084
  draw_title();
1085
+ draw_hud_now();
855
1086
  next_dirty = 1;
856
1087
  blit_on();
857
1088
  update_sprites();
858
1089
  scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
859
1090
  }
860
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
+
861
1105
  void main(void) {
862
1106
  uint8_t pad, prev = 0, t, rate, g;
863
1107
 
864
- /* one-time hardware setup: LCD defaults, vblank IRQ (so wait_vblank HALTs),
865
- * the APU, then LCD off while we populate VRAM. */
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. */
866
1116
  lcd_init_default();
867
1117
  enable_vblank_irq();
868
1118
  sound_init();
869
1119
  music_on = 1; /* background music on by default (SELECT toggles) */
870
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 */
871
1123
 
872
1124
  upload_tile(T_EMPTY, tile_empty);
873
1125
  upload_tile(T_GEM, tile_gem);
@@ -883,6 +1135,7 @@ void main(void) {
883
1135
  load_obj_palettes();
884
1136
  oam_clear();
885
1137
 
1138
+ hiscore_load(); /* battery SRAM — 0 on a fresh cart */
886
1139
  go_title();
887
1140
 
888
1141
  /* Main loop, one pass per frame. The order is deliberate: the two VRAM/OAM
@@ -902,8 +1155,11 @@ void main(void) {
902
1155
  if ((pad & PAD_SELECT) && !(prev & PAD_SELECT)) music_toggle();
903
1156
 
904
1157
  if (state == ST_TITLE) {
1158
+ /* ── GAME LOGIC (clay — reshape freely) ── press-start title
1159
+ * (handheld: no 2P mode select — see the header note) */
905
1160
  if ((pad & PAD_START) && !(prev & PAD_START)) start_game();
906
1161
  } else if (state == ST_PLAY) {
1162
+ /* ── GAME LOGIC (clay — reshape freely) ── one frame of play */
907
1163
  if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
908
1164
  && !collides((uint8_t)(piece_x - 1), piece_y)) { piece_x--; sfx_move(); }
909
1165
  if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
@@ -921,25 +1177,23 @@ void main(void) {
921
1177
  sfx_drop();
922
1178
  lock_and_resolve();
923
1179
  spawn();
924
- if (state == ST_OVER) sfx_over();
925
1180
  start_redraw();
926
1181
  }
927
1182
 
928
1183
  rate = (pad & PAD_DOWN) ? 3 : cur_fall_rate;
929
- if (++fall_timer >= rate) {
1184
+ if (state == ST_PLAY && ++fall_timer >= rate) {
930
1185
  fall_timer = 0;
931
1186
  if (collides(piece_x, (uint8_t)(piece_y + 1))) {
932
1187
  sfx_drop();
933
1188
  lock_and_resolve();
934
1189
  spawn();
935
- if (state == ST_OVER) sfx_over();
936
1190
  start_redraw();
937
1191
  } else {
938
1192
  piece_y++;
939
1193
  }
940
1194
  }
941
- } else { /* ST_OVER — START restarts immediately */
942
- if ((pad & PAD_START) && !(prev & PAD_START)) start_game();
1195
+ } else { /* ST_OVER — START returns to the title (shows the new HI) */
1196
+ if ((pad & PAD_START) && !(prev & PAD_START)) go_title();
943
1197
  }
944
1198
 
945
1199
  redraw_collect(); /* queue next frame's VRAM writes (RAM only) */