romdevtools 0.26.0 → 0.28.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 (144) hide show
  1. package/AGENTS.md +5 -3
  2. package/CHANGELOG.md +322 -3
  3. package/README.md +1 -1
  4. package/examples/README.md +1 -1
  5. package/examples/atari2600/templates/platformer.asm +18 -9
  6. package/examples/atari2600/templates/racing.asm +25 -4
  7. package/examples/atari2600/templates/shmup.asm +30 -5
  8. package/examples/atari2600/templates/sports.asm +41 -9
  9. package/examples/atari7800/templates/hello_sprite.c +8 -4
  10. package/examples/atari7800/templates/platformer.c +12 -8
  11. package/examples/atari7800/templates/puzzle.c +7 -4
  12. package/examples/atari7800/templates/racing.c +5 -2
  13. package/examples/atari7800/templates/shmup.c +8 -4
  14. package/examples/atari7800/templates/sports.c +6 -3
  15. package/examples/c64/templates/platformer.c +28 -24
  16. package/examples/c64/templates/puzzle.c +77 -16
  17. package/examples/c64/templates/racing.c +9 -0
  18. package/examples/c64/templates/shmup.c +13 -1
  19. package/examples/c64/templates/sports.c +9 -4
  20. package/examples/gb/templates/platformer.c +6 -2
  21. package/examples/gb/templates/puzzle.c +279 -101
  22. package/examples/gb/templates/racing.c +13 -1
  23. package/examples/gb/templates/shmup.c +13 -1
  24. package/examples/gb/templates/sports.c +9 -3
  25. package/examples/gba/templates/platformer.c +7 -13
  26. package/examples/gba/templates/puzzle.c +93 -15
  27. package/examples/gba/templates/racing.c +13 -1
  28. package/examples/gba/templates/shmup.c +13 -1
  29. package/examples/gba/templates/sports.c +17 -5
  30. package/examples/gbc/templates/platformer.c +6 -2
  31. package/examples/gbc/templates/puzzle.c +878 -178
  32. package/examples/gbc/templates/racing.c +13 -1
  33. package/examples/gbc/templates/shmup.c +13 -1
  34. package/examples/gbc/templates/sports.c +9 -3
  35. package/examples/genesis/templates/puzzle.c +76 -15
  36. package/examples/genesis/templates/racing.c +13 -1
  37. package/examples/genesis/templates/shmup_2p.c +13 -1
  38. package/examples/gg/templates/platformer.c +4 -0
  39. package/examples/gg/templates/puzzle.c +80 -14
  40. package/examples/gg/templates/racing.c +17 -1
  41. package/examples/gg/templates/shmup.c +17 -1
  42. package/examples/gg/templates/sports.c +4 -0
  43. package/examples/lynx/templates/platformer.c +25 -6
  44. package/examples/lynx/templates/puzzle.c +77 -14
  45. package/examples/lynx/templates/shmup.c +13 -1
  46. package/examples/lynx/templates/sports.c +5 -2
  47. package/examples/msx/platformer/main.c +2 -0
  48. package/examples/msx/puzzle/main.c +78 -15
  49. package/examples/msx/racing/main.c +1 -0
  50. package/examples/msx/shmup/main.c +1 -0
  51. package/examples/msx/sports/main.c +3 -2
  52. package/examples/nes/templates/platformer.c +11 -3
  53. package/examples/nes/templates/puzzle.c +81 -21
  54. package/examples/nes/templates/racing.c +15 -1
  55. package/examples/nes/templates/shmup.c +1 -0
  56. package/examples/nes/templates/sports.c +1 -0
  57. package/examples/pce/platformer/main.c +3 -1
  58. package/examples/pce/puzzle/main.c +78 -12
  59. package/examples/pce/racing/main.c +1 -0
  60. package/examples/pce/shmup/main.c +5 -4
  61. package/examples/pce/sports/main.c +4 -3
  62. package/examples/sms/templates/platformer.c +4 -0
  63. package/examples/sms/templates/puzzle.c +80 -14
  64. package/examples/sms/templates/racing.c +17 -1
  65. package/examples/sms/templates/shmup.c +17 -1
  66. package/examples/sms/templates/shmup_2p.c +17 -1
  67. package/examples/sms/templates/sports.c +4 -0
  68. package/examples/snes/templates/platformer.c +32 -15
  69. package/examples/snes/templates/puzzle.c +84 -16
  70. package/examples/snes/templates/racing.c +20 -1
  71. package/examples/snes/templates/shmup.c +20 -2
  72. package/examples/snes/templates/sports.c +7 -0
  73. package/package.json +12 -12
  74. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  75. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  76. package/src/cores/wasm/fceumm_libretro.js +1 -1
  77. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  78. package/src/cores/wasm/gambatte_libretro.js +1 -1
  79. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  80. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  81. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  82. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  83. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  84. package/src/cores/wasm/handy_libretro.js +1 -1
  85. package/src/cores/wasm/handy_libretro.wasm +0 -0
  86. package/src/cores/wasm/mgba_libretro.js +1 -1
  87. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  88. package/src/cores/wasm/prosystem_libretro.js +1 -1
  89. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  90. package/src/cores/wasm/snes9x_libretro.js +1 -1
  91. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  92. package/src/cores/wasm/stella2014_libretro.js +1 -1
  93. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  94. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  95. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  96. package/src/host/LibretroHost.js +245 -10
  97. package/src/mcp/server.js +6 -0
  98. package/src/mcp/tools/disasm-rebuild.js +315 -65
  99. package/src/mcp/tools/disasm.js +149 -28
  100. package/src/mcp/tools/find-references.js +216 -51
  101. package/src/mcp/tools/frame.js +11 -4
  102. package/src/mcp/tools/index.js +15 -1
  103. package/src/mcp/tools/input.js +26 -3
  104. package/src/mcp/tools/memory.js +208 -39
  105. package/src/mcp/tools/playtest.js +56 -4
  106. package/src/mcp/tools/project.js +35 -9
  107. package/src/mcp/tools/toolchain.js +43 -10
  108. package/src/mcp/tools/watch-memory.js +172 -25
  109. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  110. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  111. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  112. package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
  113. package/src/platforms/gb/MENTAL_MODEL.md +16 -1
  114. package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
  115. package/src/platforms/gb/lib/c/patch-header.js +7 -4
  116. package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
  117. package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
  118. package/src/platforms/gbc/lib/c/font.h +43 -0
  119. package/src/platforms/gbc/lib/c/patch-header.js +7 -4
  120. package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
  121. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  122. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  123. package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
  124. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  125. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  126. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  127. package/src/platforms/msx/MENTAL_MODEL.md +6 -0
  128. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  129. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  130. package/src/platforms/msx/lib/c/msx_hw.h +2 -0
  131. package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
  132. package/src/platforms/nes/MENTAL_MODEL.md +10 -3
  133. package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
  134. package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
  135. package/src/platforms/pce/MENTAL_MODEL.md +9 -0
  136. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  137. package/src/platforms/pce/lib/c/pce_hw.h +2 -1
  138. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  139. package/src/platforms/sms/MENTAL_MODEL.md +5 -0
  140. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  141. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  142. package/src/platforms/snes/MENTAL_MODEL.md +5 -0
  143. package/src/playtest/playtest.js +73 -3
  144. package/src/toolchains/index.js +37 -8
@@ -1,248 +1,948 @@
1
- /* ── puzzle.c Game Boy match-3 falling-block scaffold ─────────────
1
+ /* PUZZLEa falling-jewel matcher for Game Boy Color.
2
2
  *
3
- * Mirrors the NES/Genesis/SNES puzzle scaffolds. 6-wide × 12-tall
4
- * grid drawn via BG tilemap (each cell = 1 BG tile). 1×3 vertical
5
- * active piece; LEFT/RIGHT shifts, A rotates colour order, DOWN
6
- * soft-drops, START hard-drops. Horizontal triples clear and score.
3
+ * A 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.
7
9
  *
8
- * On the Game Boy we don't have a built-in font, so we render the
9
- * grid as coloured tile cells using three BG tile shapes (R/G/B
10
- * stripes). Score is kept in WRAM; rendering a numeric HUD requires
11
- * a digit-tile blob left as an extension.
10
+ * RENDERING the hard-won architecture (details at each routine below):
11
+ * - The FALLING column and the NEXT preview are OBJ sprites (OAM), not BG
12
+ * 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.
23
+ *
24
+ * 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).
12
26
  */
13
-
14
27
  #include "gb_hardware.h"
15
28
  #include "gb_runtime.h"
29
+ #include "font.h"
30
+
31
+ #define COLS 8
32
+ #define ROWS 17
33
+ #define NCELL (ROWS * COLS)
34
+ #define NCOLORS 6 /* jewel colors 1..6 */
35
+
36
+ /* 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). */
39
+ #define WELL_MX 1
40
+ #define WELL_MY 0
41
+
42
+ /* BG map column where the HUD text starts (right of the well). */
43
+ #define HUD_X 12
44
+
45
+ #define G(r,c) grid[((r) * COLS) + (c)]
46
+ #define M(r,c) matched[((r) * COLS) + (c)]
47
+
48
+ #define T_EMPTY 0
49
+ #define T_GEM 1
50
+ #define T_WALL 2
51
+ #define T_BLANK 3
52
+ #define T_MAGIC 4
53
+ #define T_EXP0 5 /* explosion frames: gem bursting apart (its own color) */
54
+ #define T_EXP1 6
55
+ #define T_EXP2 7
56
+ #define FONT_BASE 16
57
+
58
+ #define MAGIC 7
59
+
60
+ #define PAL_WELL 6
61
+ #define PAL_OUT 7
62
+
63
+ #define ST_PLAY 0
64
+ #define ST_OVER 1
65
+ #define ST_TITLE 2
16
66
 
17
- #define COLS 6
18
- #define ROWS 12
19
-
20
- #define T_BLANK 0
21
- #define T_R 1
22
- #define T_G 2
23
- #define T_B 3
24
- #define T_WALL 4
25
-
26
- /* tile_blank is the EMPTY-cell / backdrop tile. It is NOT all-zero: a
27
- * subtle dither (colour 0 + faint colour 1) so the empty playfield and the
28
- * area around the well read as a textured surface, never one flat colour
29
- * (the #1 GB "why is it blank" footgun). Locked blocks / the active piece
30
- * overdraw it with the R/G/B shape tiles. */
31
- static const uint8_t tile_blank[16] = {
67
+ #define VRAM ((volatile uint8_t *)0x9800)
68
+
69
+ /* ── tile pixel data (2bpp) ────────────────────────────────────────── */
70
+ static const uint8_t tile_empty[16] = {
32
71
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
33
72
  0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
34
73
  };
35
- /* Well frame: a solid colour-2 border drawn around the play area. */
74
+ static const uint8_t tile_gem[16] = {
75
+ 0x00,0x3C, 0x30,0x4E, 0x60,0x9F, 0x40,0xBF,
76
+ 0x02,0xFF, 0x06,0xFF, 0x1C,0x7E, 0x00,0x3C,
77
+ };
36
78
  static const uint8_t tile_wall[16] = {
37
79
  0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
38
80
  0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
39
81
  };
40
- /* Three distinct tile shapes (since GB BG is 2bpp, we differentiate
41
- * by *shape*, not colour-on-CGB). The CGB palette path could give us
42
- * real colours; for DMG-compatibility we use shape. */
43
- static const uint8_t tile_r[16] = {
44
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
45
- 0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
82
+ static const uint8_t tile_blank[16] = { 0 };
83
+ static const uint8_t tile_magic[16] = {
84
+ 0x18,0x18, 0x3C,0x3C, 0x7E,0x7E, 0xFF,0xFF,
85
+ 0x7E,0x7E, 0x3C,0x3C, 0x18,0x18, 0x00,0x00,
86
+ };
87
+ /* explosion frames (value 2, drawn in the gem's own colour so they blend with
88
+ * the well — value 0 = C_WELL). The gem bursts into a star, fragments fly
89
+ * outward, then sparks, then gone. Shown ONCE, expanding — no blinking. */
90
+ static const uint8_t tile_exp0[16] = {
91
+ 0x00,0x99, 0x00,0x5A, 0x00,0x3C, 0x00,0xFF,
92
+ 0x00,0xFF, 0x00,0x3C, 0x00,0x5A, 0x00,0x99,
46
93
  };
47
- static const uint8_t tile_g[16] = {
48
- 0xAA,0x55, 0xAA,0x55, 0xAA,0x55, 0xAA,0x55,
49
- 0xAA,0x55, 0xAA,0x55, 0xAA,0x55, 0xAA,0x55,
94
+ static const uint8_t tile_exp1[16] = {
95
+ 0x00,0x81, 0x00,0x42, 0x00,0x24, 0x00,0x18,
96
+ 0x00,0x18, 0x00,0x24, 0x00,0x42, 0x00,0x81,
50
97
  };
51
- static const uint8_t tile_b[16] = {
52
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
53
- 0x00,0xFF, 0x00,0xFF, 0x00,0xFF, 0x00,0xFF,
98
+ static const uint8_t tile_exp2[16] = {
99
+ 0x00,0x81, 0x00,0x00, 0x00,0x00, 0x00,0x00,
100
+ 0x00,0x00, 0x00,0x00, 0x00,0x00, 0x00,0x81,
54
101
  };
55
102
 
56
- static const uint16_t bg_palette[4] = { 0x7FFF, 0x5294, 0x294A, 0x0000 };
103
+ /* ── palettes (15-bit BGR) ─────────────────────────────────────────── */
104
+ #define RGB(r,g,b) ((uint16_t)(((uint16_t)(b)<<10)|((uint16_t)(g)<<5)|(r)))
105
+ #define C_WELL RGB(4,6,12)
106
+ #define C_OUT RGB(1,2,4)
107
+ #define C_FAINT RGB(7,9,15)
108
+ #define C_FRAME RGB(16,20,28)
57
109
 
58
- static uint8_t grid[ROWS][COLS];
59
- static uint8_t piece[3];
60
- static int16_t piece_x, piece_y;
61
- static uint8_t fall_timer;
62
- static uint16_t score;
63
- static uint32_t rng = 1;
110
+ static const uint16_t palettes[8][4] = {
111
+ /* 0 red */ { C_WELL, RGB(31,16,16), RGB(31,3,3), RGB(17,1,1) },
112
+ /* 1 orange */ { C_WELL, RGB(31,24,12), RGB(31,16,2), RGB(20,9,0) },
113
+ /* 2 yellow */ { C_WELL, RGB(31,31,18), RGB(30,28,4), RGB(22,18,0) },
114
+ /* 3 green */ { C_WELL, RGB(16,31,16), RGB(6,26,8), RGB(1,16,4) },
115
+ /* 4 blue */ { C_WELL, RGB(14,22,31), RGB(5,12,31), RGB(2,5,20) },
116
+ /* 5 purple */ { C_WELL, RGB(28,16,31), RGB(20,5,30), RGB(12,1,20) },
117
+ /* 6 well */ { C_WELL, C_FAINT, RGB(8,11,18), C_FRAME },
118
+ /* 7 out/txt*/ { C_OUT, RGB(2,3,7), C_OUT, RGB(31,31,31) },
119
+ };
64
120
 
65
- static uint32_t xorshift(void) {
66
- rng ^= rng << 13;
67
- rng ^= rng >> 17;
68
- rng ^= rng << 5;
69
- return rng;
70
- }
121
+ /* ── game state ────────────────────────────────────────────────────── */
122
+ static uint8_t grid[NCELL]; /* the well: 0 = empty, 1..NCOLORS = a gem */
123
+ static uint8_t matched[NCELL]; /* scratch: cells flagged for clearing */
124
+ static uint8_t shadow[NCELL]; /* color currently on the BG, for diff redraw */
125
+ static uint8_t piece[3]; /* the 3 falling colors, top→bottom */
126
+ static uint8_t nextp[3]; /* the previewed next column */
127
+ static uint8_t piece_x, piece_y; /* well coords of the falling column's top */
128
+ static uint8_t piece_active; /* a column is currently falling */
129
+ static uint8_t piece_magic; /* the falling column is a MAGIC piece */
130
+ static uint8_t next_dirty; /* NEXT-preview sprites need re-writing */
131
+ static uint8_t piece_counter; /* pieces since last magic (→ magic every 18) */
132
+ static uint8_t fall_timer; /* frames since the column last stepped down */
133
+ static uint8_t cur_fall_rate; /* frames per downward step (lower = faster) */
134
+ static uint16_t total_cleared; /* gems cleared this game (drives level) */
135
+ static uint8_t level;
136
+ static uint8_t score_d[6]; /* 6-digit BCD score, most significant first */
137
+ static uint8_t state; /* ST_PLAY / ST_OVER / ST_TITLE */
138
+ static uint8_t chain; /* cascade depth of the current resolve */
139
+ static uint16_t rng = 0xACE1; /* xorshift PRNG state */
71
140
 
72
- static uint8_t random_colour(void) { return 1 + (xorshift() % 3); }
141
+ /* the 4 line directions we scan for matches: horizontal, vertical, and the
142
+ * two diagonals (we only walk each line once, from its lowest cell). */
143
+ static const int8_t DIRS[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
73
144
 
74
- static void new_piece(void) {
75
- piece[0] = random_colour();
76
- piece[1] = random_colour();
77
- piece[2] = random_colour();
78
- piece_x = COLS / 2 - 1;
79
- piece_y = -3;
145
+ /* 16-bit xorshift PRNG — kept 16-bit on purpose (sm83 has no fast 32-bit
146
+ * shifts; a wider generator there degenerates toward one value). */
147
+ static uint8_t xorshift(void) {
148
+ rng ^= rng << 7;
149
+ rng ^= rng >> 9;
150
+ rng ^= rng << 8;
151
+ return (uint8_t)(rng >> 8);
152
+ }
153
+
154
+ /* fill a 3-jewel column with random colors 1..NCOLORS */
155
+ static void roll(uint8_t *p) {
156
+ p[0] = 1 + (uint8_t)(xorshift() % NCOLORS);
157
+ p[1] = 1 + (uint8_t)(xorshift() % NCOLORS);
158
+ p[2] = 1 + (uint8_t)(xorshift() % NCOLORS);
80
159
  }
81
160
 
82
- static uint8_t tile_for(uint8_t c) {
83
- switch (c) {
84
- case 1: return T_R;
85
- case 2: return T_G;
86
- case 3: return T_B;
87
- default: return T_BLANK;
161
+ /* add to the 6-digit BCD score (score_d[0] = most significant), with carry */
162
+ static void add_score(uint16_t amt) {
163
+ uint8_t k, idx;
164
+ uint16_t carry = amt;
165
+ for (k = 0; k < 6; k++) {
166
+ if (carry == 0) break;
167
+ idx = 5 - k;
168
+ carry += score_d[idx];
169
+ score_d[idx] = (uint8_t)(carry % 10);
170
+ carry = carry / 10;
88
171
  }
89
172
  }
90
173
 
91
- static void draw_cell(int16_t col, int16_t row, uint8_t cell) {
92
- /* Map base $9800, 32 cells wide. Centre the 6-col grid offset +7. */
93
- uint8_t *map = (uint8_t *)0x9800;
94
- if (row < 0 || row >= ROWS) return;
95
- map[(row + 1) * 32 + (col + 7)] = tile_for(cell);
174
+ /* ── sound effects ──────────────────────────────────────────────────
175
+ * A tiny note sequencer driving square channel 2 directly. Each note has
176
+ * 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. */
180
+ #define P_C4 1548
181
+ #define P_G4 1714
182
+ #define P_A4 1750
183
+ #define P_C5 1797
184
+ #define P_E5 1849
185
+ #define P_G5 1881
186
+ #define P_A5 1899
187
+ #define P_C6 1923
188
+
189
+ /* NR21 duty: 0x40 = 25% (soft), 0x80 = 50% (full). NR22 vol/env byte:
190
+ * (volume<<4)|(0=decay)|envPace — bigger pace = slower fade. */
191
+ #define SFX_STEPS 4
192
+ static uint16_t sfx_p[SFX_STEPS];
193
+ static uint8_t sfx_v[SFX_STEPS];
194
+ static uint8_t sfx_d[SFX_STEPS];
195
+ static uint8_t sfx_f[SFX_STEPS];
196
+ static uint8_t sfx_n, sfx_i, sfx_t;
197
+
198
+ static void sfx_tick(void) {
199
+ if (sfx_i >= sfx_n) return;
200
+ if (sfx_t != 0) { sfx_t--; return; }
201
+ NR21 = sfx_d[sfx_i];
202
+ NR22 = sfx_v[sfx_i];
203
+ NR23 = (uint8_t)(sfx_p[sfx_i] & 0xFF);
204
+ NR24 = (uint8_t)(0x80 | (sfx_p[sfx_i] >> 8)); /* trigger (let envelope end it) */
205
+ sfx_t = sfx_f[sfx_i];
206
+ sfx_i++;
207
+ }
208
+
209
+ static void sfx_go(uint8_t n) { sfx_n = n; sfx_i = 0; sfx_t = 0; sfx_tick(); }
210
+
211
+ static void sfx_move(void) {
212
+ sfx_p[0] = P_A5; sfx_v[0] = 0x81; sfx_d[0] = 0x40; sfx_f[0] = 4;
213
+ sfx_go(1);
214
+ }
215
+ static void sfx_rotate(void) {
216
+ sfx_p[0] = P_C6; sfx_v[0] = 0x81; sfx_d[0] = 0x40; sfx_f[0] = 4;
217
+ sfx_go(1);
218
+ }
219
+ static void sfx_drop(void) {
220
+ sfx_p[0] = P_C5; sfx_v[0] = 0xC2; sfx_d[0] = 0x80; sfx_f[0] = 3;
221
+ sfx_p[1] = P_C4; sfx_v[1] = 0xC3; sfx_d[1] = 0x80; sfx_f[1] = 8;
222
+ sfx_go(2);
223
+ }
224
+ static void sfx_clear(void) { /* bright ascending C-E-G */
225
+ sfx_p[0] = P_C5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 4;
226
+ sfx_p[1] = P_E5; sfx_v[1] = 0xD2; sfx_d[1] = 0x80; sfx_f[1] = 4;
227
+ sfx_p[2] = P_G5; sfx_v[2] = 0xD3; sfx_d[2] = 0x80; sfx_f[2] = 8;
228
+ sfx_go(3);
229
+ }
230
+ static void sfx_chain(uint8_t n) { /* arpeggio whose top note rises per chain */
231
+ uint16_t top = (uint16_t)(P_C6 + (uint16_t)n * 6);
232
+ if (top > 1980) top = 1980;
233
+ sfx_p[0] = P_E5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 3;
234
+ sfx_p[1] = P_G5; sfx_v[1] = 0xD2; sfx_d[1] = 0x80; sfx_f[1] = 3;
235
+ sfx_p[2] = top; sfx_v[2] = 0xD3; sfx_d[2] = 0x80; sfx_f[2] = 8;
236
+ sfx_go(3);
237
+ }
238
+ static void sfx_over(void) { /* slow descending */
239
+ sfx_p[0] = P_A4; sfx_v[0] = 0xC3; sfx_d[0] = 0x80; sfx_f[0] = 10;
240
+ sfx_p[1] = P_G4; sfx_v[1] = 0xC3; sfx_d[1] = 0x80; sfx_f[1] = 10;
241
+ sfx_p[2] = P_C4; sfx_v[2] = 0xC5; sfx_d[2] = 0x80; sfx_f[2] = 24;
242
+ sfx_go(3);
96
243
  }
97
244
 
98
- static void draw_grid(void) {
99
- int16_t r, c;
100
- for (r = 0; r < ROWS; r++)
101
- for (c = 0; c < COLS; c++)
102
- draw_cell(c, r, grid[r][c]);
245
+ /* ── background music ───────────────────────────────────────────────
246
+ * A looping square-wave lead on channel 1 (SFX live on channel 2, so they
247
+ * mix and the effects cut through the music). music_tick() plays one melody
248
+ * step every 12 frames, re-triggering ch1 at a steady volume. Toggle on/off
249
+ * with SELECT — defaults ON.
250
+ *
251
+ * The melody is the GB 11-bit period split into low/high BYTE arrays (NR13 +
252
+ * NR14 low 3 bits) — period p ⇒ freq 131072/(2048-p). hi == 0xFF marks a
253
+ * rest. Arpeggios over a C - Am - F - G chord loop, 8 steps each. */
254
+ static const uint8_t mel_lo[32] = {
255
+ 0x06,0x39,0x59,0x83, 0x59,0x39,0x06,0x00, /* C E G C6 G E C - */
256
+ 0xD6,0x06,0x39,0x6B, 0x39,0x06,0xD6,0x00, /* A C E A5 E C A - */
257
+ 0x88,0xD6,0x06,0x44, 0x06,0xD6,0x88,0x00, /* F A C F5 C A F - */
258
+ 0xB2,0xF7,0x21,0x59, 0x21,0xF7,0xB2,0x00, /* G B D G5 D B G - */
259
+ };
260
+ static const uint8_t mel_hi[32] = { /* high 3 bits; 0xFF = rest */
261
+ 0x07,0x07,0x07,0x07, 0x07,0x07,0x07,0xFF,
262
+ 0x06,0x07,0x07,0x07, 0x07,0x07,0x06,0xFF,
263
+ 0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
264
+ 0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
265
+ };
266
+ static uint8_t music_on;
267
+ static uint8_t music_idx;
268
+ static uint8_t music_timer;
269
+
270
+ static void music_note(uint8_t idx) {
271
+ uint8_t hi = mel_hi[idx];
272
+ if (hi == 0xFF) { NR12 = 0x00; NR14 = 0x80; return; } /* rest: silence ch1 */
273
+ NR10 = 0x00; /* no sweep */
274
+ NR11 = 0x80; /* 50% duty, no length counter */
275
+ NR12 = 0x90; /* volume 9, no envelope (steady lead) */
276
+ NR13 = mel_lo[idx];
277
+ NR14 = (uint8_t)(0x80 | hi); /* trigger + freq high bits */
103
278
  }
104
279
 
105
- static uint8_t collides(int16_t col, int16_t row) {
106
- uint8_t i;
107
- int16_t r;
108
- if (col < 0 || col >= COLS) return 1;
109
- for (i = 0; i < 3; i++) {
110
- r = row + i;
111
- if (r >= ROWS) return 1;
112
- if (r >= 0 && grid[r][col] != 0) return 1;
280
+ static void music_tick(void) {
281
+ if (!music_on) return;
282
+ if (music_timer == 0) {
283
+ music_note(music_idx);
284
+ music_timer = 12;
285
+ if (++music_idx >= 32) music_idx = 0;
113
286
  }
287
+ music_timer--;
288
+ }
289
+
290
+ static void music_toggle(void) {
291
+ music_on = (uint8_t)(!music_on);
292
+ music_idx = 0;
293
+ music_timer = 0;
294
+ if (!music_on) { NR12 = 0x00; NR14 = 0x80; } /* kill the lead immediately */
295
+ }
296
+
297
+ /* is grid cell (r,col) off the bottom or already filled? */
298
+ static uint8_t cell_blocked(uint8_t r, uint8_t col) {
299
+ if (r >= ROWS) return 1;
300
+ return grid[(uint8_t)(r * COLS + col)] ? 1 : 0;
301
+ }
302
+
303
+ /* would the 3-tall falling column collide if its top cell were at (col,topy)?
304
+ * Checks are unrolled (not a loop) — short indexed-read loops can miscompile on
305
+ * sm83, and this is the hottest correctness check in the game. */
306
+ static uint8_t collides(uint8_t col, uint8_t topy) {
307
+ if (col >= COLS) return 1;
308
+ if (cell_blocked(topy, col)) return 1;
309
+ if (cell_blocked((uint8_t)(topy + 1), col)) return 1;
310
+ if (cell_blocked((uint8_t)(topy + 2), col)) return 1;
114
311
  return 0;
115
312
  }
116
313
 
117
- static void lock_piece(void) {
314
+ /* start a new falling column at the top-center. Every 18th piece is a MAGIC
315
+ * column; otherwise take the previewed colors and roll the next preview. If
316
+ * it can't even appear, the well is full → game over. */
317
+ static void spawn(void) {
318
+ rng ^= DIV;
319
+ if (++piece_counter >= 18) {
320
+ piece_counter = 0;
321
+ piece_magic = 1;
322
+ piece[0] = MAGIC; piece[1] = MAGIC; piece[2] = MAGIC;
323
+ } else {
324
+ piece_magic = 0;
325
+ piece[0] = nextp[0]; piece[1] = nextp[1]; piece[2] = nextp[2];
326
+ roll(nextp);
327
+ }
328
+ piece_x = COLS / 2 - 1;
329
+ piece_y = 0;
330
+ piece_active = 1;
331
+ fall_timer = 0;
332
+ next_dirty = 1;
333
+ if (collides(piece_x, piece_y)) {
334
+ piece_active = 0;
335
+ state = ST_OVER;
336
+ }
337
+ }
338
+
339
+ /* Flag every gem that's part of a run of 3+ same-color cells in any of the 4
340
+ * directions, into matched[]; return how many cells were flagged. Each line
341
+ * is counted from its lowest end only (we skip a cell if its predecessor in
342
+ * that direction is the same color), so runs aren't double-walked. */
343
+ static uint8_t mark_and_count(void) {
344
+ uint8_t r, c, d, len, cnt, col, k;
345
+ int8_t dr, dc;
346
+ int16_t sr, sc;
347
+
348
+ for (r = 0; r < NCELL; r++) matched[r] = 0;
349
+
350
+ for (r = 0; r < ROWS; r++) {
351
+ for (c = 0; c < COLS; c++) {
352
+ col = G(r, c);
353
+ if (col == 0) continue;
354
+ for (d = 0; d < 4; d++) {
355
+ dr = DIRS[d][0];
356
+ dc = DIRS[d][1];
357
+ sr = (int16_t)r - dr;
358
+ sc = (int16_t)c - dc;
359
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
360
+ && G(sr, sc) == col) continue;
361
+ len = 1;
362
+ sr = (int16_t)r + dr;
363
+ sc = (int16_t)c + dc;
364
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
365
+ && G(sr, sc) == col) {
366
+ len++;
367
+ sr += dr;
368
+ sc += dc;
369
+ }
370
+ if (len >= 3) {
371
+ sr = (int16_t)r;
372
+ sc = (int16_t)c;
373
+ for (k = 0; k < len; k++) {
374
+ M(sr, sc) = 1;
375
+ sr += dr;
376
+ sc += dc;
377
+ }
378
+ }
379
+ }
380
+ }
381
+ }
382
+
383
+ cnt = 0;
384
+ for (r = 0; r < NCELL; r++) if (matched[r]) cnt++;
385
+ return cnt;
386
+ }
387
+
388
+ /* empty every flagged cell */
389
+ static void clear_marked(void) {
118
390
  uint8_t i;
119
- int16_t r;
120
- int16_t c;
121
- uint8_t a, b, d;
122
- for (i = 0; i < 3; i++) {
123
- r = piece_y + i;
124
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
391
+ for (i = 0; i < NCELL; i++) if (matched[i]) grid[i] = 0;
392
+ }
393
+
394
+ /* collapse each column so all gems rest on the floor with no gaps */
395
+ static void apply_gravity(void) {
396
+ uint8_t c, r, n, w;
397
+ uint8_t buf[ROWS];
398
+ for (c = 0; c < COLS; c++) {
399
+ n = 0;
400
+ for (r = 0; r < ROWS; r++)
401
+ if (G(r, c)) { buf[n] = G(r, c); n++; }
402
+ for (r = 0; r < (uint8_t)(ROWS - n); r++) G(r, c) = 0;
403
+ w = 0;
404
+ for (r = (uint8_t)(ROWS - n); r < ROWS; r++) { G(r, c) = buf[w]; w++; }
125
405
  }
126
- for (i = 0; i < 3; i++) {
127
- r = piece_y + i;
128
- if (r < 0 || r >= ROWS) continue;
129
- for (c = 0; c <= COLS - 3; c++) {
130
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
131
- if (a != 0 && a == b && b == d) {
132
- grid[r][c] = 0;
133
- grid[r][c + 1] = 0;
134
- grid[r][c + 2] = 0;
135
- if (score < 65500u) score += 30;
406
+ }
407
+
408
+ /* level rises every 15 cleared gems (capped at 13); each level shortens the
409
+ * frames-per-row fall interval, so the column drops faster. */
410
+ static void update_level(void) {
411
+ level = (uint8_t)(total_cleared / 15);
412
+ if (level > 13) level = 13;
413
+ cur_fall_rate = 32 - level * 2;
414
+ if (cur_fall_rate < 4) cur_fall_rate = 4;
415
+ }
416
+
417
+ /* Matched gems burst apart before they clear — a one-shot expanding star in
418
+ * each gem's own colour (no blinking, no LCD-off). Only ever runs on a real
419
+ * match. Direct vblank writes (no OAM DMA contends here, so plenty of room);
420
+ * blocks ~6 frames, which is the satisfying beat. */
421
+ static void explode_matched(void) {
422
+ uint8_t i, j, n, tile;
423
+ uint16_t offs[8];
424
+ uint8_t cols[8];
425
+ uint8_t *o = (uint8_t *)0xC100;
426
+ for (i = 0; i < 12; i++) *o++ = 0; /* hide the locked piece sprites */
427
+ ((void (*)(uint8_t))0xFF80)(0xC1);
428
+ n = 0;
429
+ for (i = 0; i < NCELL && n < 8; i++) {
430
+ if (matched[i]) {
431
+ offs[n] = (uint16_t)(WELL_MY + (i >> 3)) * 32 + WELL_MX + (i & 7);
432
+ cols[n] = (uint8_t)(grid[i] - 1);
433
+ n++;
434
+ }
435
+ }
436
+ for (j = 0; j < 9; j++) {
437
+ tile = (j < 3) ? T_EXP0 : (j < 6) ? T_EXP1 : T_EXP2;
438
+ wait_vblank();
439
+ sfx_tick();
440
+ music_tick();
441
+ VBK = 0;
442
+ for (i = 0; i < n; i++) VRAM[offs[i]] = tile;
443
+ VBK = 1;
444
+ for (i = 0; i < n; i++) VRAM[offs[i]] = cols[i];
445
+ VBK = 0;
446
+ }
447
+ }
448
+
449
+ /* Settle the board after a lock: repeatedly find matches, burst+clear them,
450
+ * score, and apply gravity — looping so cascades chain. Score per clear scales
451
+ * with level and (for 2nd+ cascades) the chain depth. */
452
+ static void resolve_board(void) {
453
+ uint8_t n;
454
+ uint16_t amt, mult;
455
+ chain = 0;
456
+ while (1) {
457
+ n = mark_and_count();
458
+ if (n == 0) break;
459
+ chain++;
460
+ sfx_chain(chain);
461
+ explode_matched();
462
+ clear_marked();
463
+ mult = (uint16_t)(10 + level * 2);
464
+ amt = (uint16_t)n * mult;
465
+ if (chain > 1) amt = amt * chain;
466
+ if (amt > 60000) amt = 60000;
467
+ add_score(amt);
468
+ total_cleared += n;
469
+ apply_gravity();
470
+ }
471
+ update_level();
472
+ }
473
+
474
+ /* MAGIC column: clears every gem sharing the color of whatever it landed on,
475
+ * then resolves any resulting cascades. */
476
+ static void magic_clear(void) {
477
+ uint8_t below = (uint8_t)(piece_y + 3);
478
+ uint8_t target, i;
479
+ uint16_t cleared = 0;
480
+ piece_active = 0;
481
+ if (below < ROWS) {
482
+ target = G(below, piece_x);
483
+ if (target != 0 && target != MAGIC) {
484
+ for (i = 0; i < NCELL; i++)
485
+ if (grid[i] == target) { grid[i] = 0; cleared++; }
486
+ if (cleared) {
487
+ add_score((uint16_t)cleared * 20u);
488
+ total_cleared += cleared;
489
+ sfx_clear();
136
490
  }
491
+ apply_gravity();
137
492
  }
138
493
  }
139
- draw_grid();
494
+ resolve_board();
495
+ }
496
+
497
+ /* Stamp the falling column into the grid where it came to rest, then resolve.
498
+ * A magic column takes its own path. */
499
+ static void lock_and_resolve(void) {
500
+ uint8_t i, r;
501
+ if (piece_magic) { magic_clear(); return; }
502
+ for (i = 0; i < 3; i++) {
503
+ r = (uint8_t)(piece_y + i);
504
+ if (r < ROWS) G(r, piece_x) = piece[i];
505
+ }
506
+ piece_active = 0;
507
+ resolve_board();
140
508
  }
141
509
 
510
+ /* ── rendering ─────────────────────────────────────────────────────── */
511
+ /* copy one 16-byte 2bpp tile into VRAM tile slot `slot` ($8000 + slot*16) */
142
512
  static void upload_tile(uint8_t slot, const uint8_t *src) {
143
- uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
144
- /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
145
- * SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
146
- memcpy_vram(dst, src, 16);
513
+ memcpy_vram((uint8_t *)(0x8000 + slot * 16), src, 16);
147
514
  }
148
515
 
149
- /* Draw the well frame around the 6×12 play area. Grid cells live at
150
- * map[(row+1)*32 + (col+7)] (rows 1..12, cols 7..12), so the frame is the
151
- * column to each side (6 and 13) and the floor row just below (row 13). */
152
- static void draw_well(void) {
153
- uint8_t *map = (uint8_t *)0x9800;
154
- uint8_t r;
155
- for (r = 1; r <= 12; r++) {
156
- map[r * 32 + 6] = T_WALL; /* left wall */
157
- map[r * 32 + 13] = T_WALL; /* right wall */
516
+ /* push all 8 BG palettes (4 colors each, 15-bit BGR) via the BCPS/BCPD port */
517
+ static void load_palettes(void) {
518
+ uint8_t p, i;
519
+ BCPS = 0x80;
520
+ for (p = 0; p < 8; p++)
521
+ for (i = 0; i < 4; i++) {
522
+ BCPD = (uint8_t)(palettes[p][i] & 0xFF);
523
+ BCPD = (uint8_t)((palettes[p][i] >> 8) & 0xFF);
524
+ }
525
+ }
526
+
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). */
529
+ static void load_obj_palettes(void) {
530
+ uint8_t p, i;
531
+ uint16_t col;
532
+ OCPS = 0x80;
533
+ for (p = 0; p < 8; p++)
534
+ for (i = 0; i < 4; i++) {
535
+ if (p < 6) col = palettes[p][i];
536
+ else if (p == 6) col = (i == 3) ? RGB(31,31,31) : C_OUT;
537
+ else col = 0;
538
+ OCPD = (uint8_t)(col & 0xFF);
539
+ OCPD = (uint8_t)((col >> 8) & 0xFF);
540
+ }
541
+ }
542
+
543
+ /* 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.
545
+ * 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. */
547
+ static void update_sprites(void) {
548
+ /* Write shadow_oam ($C100) directly with a walking pointer — calling
549
+ * oam_set() six times burns ~10 scanlines of vblank (SDCC call overhead),
550
+ * starving the BG flush. Inlined it's ~2 lines. */
551
+ uint8_t *o = (uint8_t *)0xC100;
552
+ uint8_t i, tile, sx, sy, pal0, pal1, pal2;
553
+ if (piece_active) {
554
+ tile = piece_magic ? T_MAGIC : T_GEM;
555
+ sx = (uint8_t)((WELL_MX + piece_x) * 8 + 8);
556
+ sy = (uint8_t)((WELL_MY + piece_y) * 8 + 16);
557
+ if (piece_magic) { pal0 = pal1 = pal2 = 6; }
558
+ else { pal0 = piece[0] - 1; pal1 = piece[1] - 1; pal2 = piece[2] - 1; }
559
+ *o++ = sy; *o++ = sx; *o++ = tile; *o++ = pal0;
560
+ *o++ = (uint8_t)(sy + 8); *o++ = sx; *o++ = tile; *o++ = pal1;
561
+ *o++ = (uint8_t)(sy + 16); *o++ = sx; *o++ = tile; *o++ = pal2;
562
+ } else {
563
+ for (i = 0; i < 12; i++) *o++ = 0;
564
+ }
565
+ /* NEXT preview (sprites 3-5) only changes on a spawn — skip it most
566
+ * frames to keep the OAM build short enough to leave the BG flush vblank. */
567
+ if (next_dirty) {
568
+ next_dirty = 0;
569
+ o = (uint8_t *)0xC10C; /* sprite slot 3 */
570
+ if (state == ST_TITLE) {
571
+ for (i = 0; i < 12; i++) *o++ = 0;
572
+ } else {
573
+ sx = (uint8_t)((HUD_X + 2) * 8 + 8);
574
+ for (i = 0; i < 3; i++) {
575
+ *o++ = (uint8_t)((9 + i) * 8 + 16);
576
+ *o++ = sx;
577
+ *o++ = T_GEM;
578
+ *o++ = (uint8_t)(nextp[i] - 1);
579
+ }
580
+ }
158
581
  }
159
- for (r = 6; r <= 13; r++)
160
- map[13 * 32 + r] = T_WALL; /* floor */
582
+ /* Trigger the OAM DMA via the HRAM stub directly (skip the oam_dma_flush
583
+ * / oam_dma_copy wrappers). A = high byte of shadow_oam ($C100). */
584
+ ((void (*)(uint8_t))0xFF80)(0xC1);
161
585
  }
162
586
 
163
- void main(void) {
164
- uint8_t pad, prev = 0, fall_rate, t;
165
- int16_t r, c;
587
+ /* write one BG map cell: tile index (VRAM bank 0) + palette/attr (bank 1).
588
+ * Direct, unbounded only safe with the LCD off or in a bounded vblank batch. */
589
+ static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
590
+ uint16_t off = (uint16_t)my * 32 + mx;
591
+ VBK = 0;
592
+ VRAM[off] = tile;
593
+ VBK = 1;
594
+ VRAM[off] = pal;
595
+ VBK = 0;
596
+ }
597
+
598
+ /* map an ASCII char to its font tile slot (digits, then A-Z); blank otherwise */
599
+ static uint8_t font_slot(char ch) {
600
+ if (ch >= '0' && ch <= '9') return FONT_BASE + (uint8_t)(ch - '0');
601
+ if (ch >= 'A' && ch <= 'Z') return FONT_BASE + 10 + (uint8_t)(ch - 'A');
602
+ return T_BLANK;
603
+ }
604
+
605
+ /* draw a NUL-terminated string into the BG map starting at (col,row) */
606
+ static void draw_text(uint8_t col, uint8_t row, const char *s) {
166
607
  uint8_t i;
167
- uint8_t *map;
168
- int16_t pr;
608
+ for (i = 0; s[i] != 0; i++)
609
+ set_cell((uint8_t)(col + i), row, font_slot(s[i]), PAL_OUT);
610
+ }
169
611
 
170
- lcd_init_default();
171
- LCDC = 0;
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) {
615
+ 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 */
620
+ }
172
621
 
173
- upload_tile(T_BLANK, tile_blank);
174
- upload_tile(T_R, tile_r);
175
- upload_tile(T_G, tile_g);
176
- upload_tile(T_B, tile_b);
177
- upload_tile(T_WALL, tile_wall);
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). */
625
+ static void draw_static(void) {
626
+ uint8_t x, y;
627
+ uint16_t off;
628
+ VBK = 0;
629
+ for (y = 0; y < 18; y++)
630
+ for (x = 0; x < 20; x++) { off = (uint16_t)y * 32 + x; VRAM[off] = T_EMPTY; }
631
+ VBK = 1;
632
+ for (y = 0; y < 18; y++)
633
+ for (x = 0; x < 20; x++) { off = (uint16_t)y * 32 + x; VRAM[off] = PAL_OUT; }
634
+ VBK = 0;
635
+ for (y = WELL_MY; y < (uint8_t)(WELL_MY + ROWS); y++) {
636
+ set_cell((uint8_t)(WELL_MX - 1), y, T_WALL, PAL_WELL);
637
+ set_cell((uint8_t)(WELL_MX + COLS), y, T_WALL, PAL_WELL);
638
+ }
639
+ for (x = (uint8_t)(WELL_MX - 1); x <= (uint8_t)(WELL_MX + COLS); x++)
640
+ 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");
645
+ }
178
646
 
179
- BCPS = 0x80;
180
- for (i = 0; i < 4; i++) {
181
- BCPD = (uint8_t)(bg_palette[i] & 0xFF);
182
- BCPD = (uint8_t)((bg_palette[i] >> 8) & 0xFF);
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. */
650
+ static void redraw_all(void) {
651
+ uint8_t r, c, col;
652
+ uint8_t i = 0;
653
+ uint16_t rowoff, off;
654
+ for (r = 0; r < ROWS; r++) {
655
+ rowoff = (uint16_t)(WELL_MY + r) * 32 + WELL_MX;
656
+ for (c = 0; c < COLS; c++) {
657
+ col = grid[i];
658
+ if (col != shadow[i]) {
659
+ shadow[i] = col;
660
+ off = rowoff + c;
661
+ VBK = 0; VRAM[off] = col ? T_GEM : T_EMPTY;
662
+ VBK = 1; VRAM[off] = col ? (uint8_t)(col - 1) : PAL_WELL;
663
+ VBK = 0;
664
+ }
665
+ i++;
666
+ }
667
+ }
668
+ }
669
+
670
+ /* ── deferred well/HUD rendering (NO LCD toggling in-game) ───────────
671
+ * This core blanks the whole frame on ANY LCDC bit-7 toggle (a strobe we
672
+ * must never do), AND it occasionally drops a VRAM write even at the start
673
+ * 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.
676
+ * FLUSH — write the queue to VRAM as the FIRST thing after wait_vblank.
677
+ * The scrub re-writes every well cell from the grid every ~0.2s, so any
678
+ * 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). */
683
+ #define REDRAW_BUDGET 4 /* changed well cells per frame (responsive) */
684
+ #define SCRUB_N 4 /* idle cells re-written per frame (self-heal) */
685
+ #define WQ_MAX 6 /* queue capacity (≤4 pushed per frame) */
686
+ static uint8_t scanning, hud_pending, over_pending;
687
+ static uint8_t hud_phase, over_phase; /* split big HUD/text writes across frames */
688
+ static uint8_t scan_i, scan_c, scrub_i;
689
+ static uint16_t scan_rowoff;
690
+
691
+ static uint8_t wq_n;
692
+ static uint16_t wq_off[WQ_MAX];
693
+ static uint8_t wq_tile[WQ_MAX];
694
+ static uint8_t wq_attr[WQ_MAX];
695
+
696
+ static void start_redraw(void) {
697
+ scanning = 1;
698
+ scan_i = 0; scan_c = 0;
699
+ scan_rowoff = (uint16_t)WELL_MY * 32 + WELL_MX;
700
+ }
701
+
702
+ static void wq_push(uint16_t off, uint8_t tile, uint8_t attr) {
703
+ if (wq_n < WQ_MAX) {
704
+ wq_off[wq_n] = off; wq_tile[wq_n] = tile; wq_attr[wq_n] = attr; wq_n++;
705
+ }
706
+ }
707
+
708
+ static void wq_text(uint8_t col, uint8_t row, const char *s) {
709
+ uint8_t i;
710
+ for (i = 0; s[i] != 0; i++)
711
+ wq_push((uint16_t)row * 32 + col + i, font_slot(s[i]), PAL_OUT);
712
+ }
713
+
714
+ /* Fill the queue with the next batch of pending changes (RAM only).
715
+ * 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. */
717
+ static void redraw_collect(void) {
718
+ uint8_t col, k, r, c, i;
719
+ wq_n = 0;
720
+ if (scanning) {
721
+ while (scan_i < NCELL && wq_n < REDRAW_BUDGET) {
722
+ col = grid[scan_i];
723
+ if (col != shadow[scan_i]) {
724
+ shadow[scan_i] = col;
725
+ wq_off[wq_n] = scan_rowoff + scan_c;
726
+ wq_tile[wq_n] = col ? T_GEM : T_EMPTY;
727
+ wq_attr[wq_n] = col ? (uint8_t)(col - 1) : PAL_WELL;
728
+ wq_n++;
729
+ }
730
+ scan_i++; scan_c++;
731
+ if (scan_c >= COLS) { scan_c = 0; scan_rowoff += 32; }
732
+ }
733
+ if (scan_i >= NCELL) { scanning = 0; hud_pending = 1; hud_phase = 0; }
734
+ } else if (hud_pending) {
735
+ 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);
738
+ 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);
744
+ hud_pending = 0;
745
+ if (state == ST_OVER) { over_pending = 1; over_phase = 0; }
746
+ }
747
+ } else if (over_pending) {
748
+ if (over_phase == 0) { wq_text(3, 6, "GAME"); over_phase = 1; }
749
+ else { wq_text(3, 7, "OVER"); over_pending = 0; }
750
+ } else if (state == ST_PLAY) {
751
+ /* idle: rolling scrub of the well so any dropped write heals itself.
752
+ * (COLS is a power of two, so >>3 / &7 split index → row,col cheaply.)
753
+ * Only during play — would erase the title gems / game-over text. */
754
+ for (k = 0; k < SCRUB_N; k++) {
755
+ r = scrub_i >> 3; c = scrub_i & 7;
756
+ col = grid[scrub_i];
757
+ wq_push((uint16_t)(WELL_MY + r) * 32 + WELL_MX + c,
758
+ col ? T_GEM : T_EMPTY, col ? (uint8_t)(col - 1) : PAL_WELL);
759
+ scrub_i++;
760
+ if (scrub_i >= NCELL) scrub_i = 0;
761
+ }
183
762
  }
763
+ }
184
764
 
185
- map = (uint8_t *)0x9800;
186
- for (i = 0; i < 32; i++) {
187
- c = 0;
188
- while (c < 32) { map[i * 32 + c] = T_BLANK; c++; }
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++. */
768
+ static void redraw_flush(void) {
769
+ uint8_t k = wq_n;
770
+ uint16_t *op;
771
+ uint8_t *tp, *ap;
772
+ uint16_t off;
773
+ if (k == 0) return;
774
+ op = wq_off; tp = wq_tile; ap = wq_attr;
775
+ while (k != 0) {
776
+ off = *op++;
777
+ VBK = 0; VRAM[off] = *tp++;
778
+ VBK = 1; VRAM[off] = *ap++;
779
+ k--;
189
780
  }
781
+ VBK = 0;
782
+ wq_n = 0;
783
+ }
190
784
 
191
- for (r = 0; r < ROWS; r++)
192
- for (c = 0; c < COLS; c++)
193
- grid[r][c] = 0;
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 };
194
787
 
195
- score = 0;
788
+ static void draw_title(void) {
789
+ 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++)
792
+ for (x = 10; x <= 19; x++) set_cell(x, y, T_EMPTY, PAL_OUT);
793
+ /* decorative gems piled at the bottom of the well */
794
+ color = 1;
795
+ for (c = 0; c < COLS; c++) {
796
+ for (k = 0; k < title_heights[c]; k++) {
797
+ y = (uint8_t)(ROWS - 1 - k);
798
+ set_cell((uint8_t)(WELL_MX + c), (uint8_t)(WELL_MY + y),
799
+ T_GEM, (uint8_t)(color - 1));
800
+ color++; if (color > NCOLORS) color = 1;
801
+ }
802
+ }
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");
807
+ }
808
+
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). */
812
+ 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; }
814
+
815
+ /* 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. */
817
+ static void reset_state(void) {
818
+ uint8_t i;
819
+ for (i = 0; i < NCELL; i++) grid[i] = 0;
820
+ for (i = 0; i < NCELL; i++) shadow[i] = 0xFF;
821
+ for (i = 0; i < 6; i++) score_d[i] = 0;
822
+ total_cleared = 0;
823
+ level = 0;
824
+ cur_fall_rate = 32;
196
825
  fall_timer = 0;
197
- new_piece();
198
- draw_well();
199
- draw_grid();
826
+ piece_counter = 0;
827
+ piece_magic = 0;
828
+ }
829
+
830
+ /* leave the title and begin play: reset, seed the first piece + preview, and
831
+ * rebuild the screen with the LCD off. */
832
+ static void start_game(void) {
833
+ reset_state();
834
+ state = ST_PLAY;
835
+ roll(nextp);
836
+ spawn();
837
+ blit_off();
838
+ draw_static();
839
+ redraw_all();
840
+ draw_hud();
841
+ blit_on();
842
+ update_sprites();
843
+ scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
844
+ }
845
+
846
+ /* show the title screen (decorative gem pile + PUZZLE / PRESS START) */
847
+ static void go_title(void) {
848
+ reset_state();
849
+ piece_active = 0;
850
+ state = ST_TITLE;
851
+ blit_off();
852
+ draw_static();
853
+ redraw_all();
854
+ draw_title();
855
+ next_dirty = 1;
856
+ blit_on();
857
+ update_sprites();
858
+ scanning = 0; hud_pending = 0; over_pending = 0; wq_n = 0;
859
+ }
200
860
 
201
- LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_TILE_DATA_LO;
861
+ void main(void) {
862
+ uint8_t pad, prev = 0, t, rate, g;
863
+
864
+ /* one-time hardware setup: LCD defaults, vblank IRQ (so wait_vblank HALTs),
865
+ * the APU, then LCD off while we populate VRAM. */
866
+ lcd_init_default();
867
+ enable_vblank_irq();
868
+ sound_init();
869
+ music_on = 1; /* background music on by default (SELECT toggles) */
870
+ LCDC = 0;
871
+
872
+ upload_tile(T_EMPTY, tile_empty);
873
+ upload_tile(T_GEM, tile_gem);
874
+ upload_tile(T_WALL, tile_wall);
875
+ upload_tile(T_BLANK, tile_blank);
876
+ upload_tile(T_MAGIC, tile_magic);
877
+ upload_tile(T_EXP0, tile_exp0);
878
+ upload_tile(T_EXP1, tile_exp1);
879
+ upload_tile(T_EXP2, tile_exp2);
880
+ for (g = 0; g < FONT_GLYPHS; g++)
881
+ memcpy_vram((uint8_t *)(0x8000 + (FONT_BASE + g) * 16), &font_data[g * 16], 16);
882
+ load_palettes();
883
+ load_obj_palettes();
884
+ oam_clear();
885
+
886
+ go_title();
202
887
 
888
+ /* Main loop, one pass per frame. The order is deliberate: the two VRAM/OAM
889
+ * writers (sprites, then the bounded BG flush) run FIRST so they land inside
890
+ * vblank; audio and game logic follow; the next frame's BG writes are queued
891
+ * last (RAM only) for the following frame's flush. */
203
892
  while (1) {
204
893
  wait_vblank();
205
-
206
- /* Erase current piece visual (overwrite with what's underneath). */
207
- for (i = 0; i < 3; i++) {
208
- pr = piece_y + i;
209
- if (pr >= 0 && pr < ROWS)
210
- draw_cell(piece_x, pr, grid[pr][piece_x]);
211
- }
894
+ update_sprites(); /* OAM DMA FIRST — must land in vblank (no tear) */
895
+ redraw_flush(); /* then drain queued BG writes (≤4, fits vblank) */
896
+ sfx_tick();
897
+ music_tick();
212
898
 
213
899
  pad = joypad_read();
214
900
 
215
- if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
216
- && !collides(piece_x - 1, piece_y)) piece_x--;
217
- if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
218
- && !collides(piece_x + 1, piece_y)) piece_x++;
219
- if ((pad & PAD_A) && !(prev & PAD_A)) {
220
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
221
- }
222
- if ((pad & PAD_START) && !(prev & PAD_START)) {
223
- while (!collides(piece_x, piece_y + 1)) piece_y++;
224
- lock_piece();
225
- new_piece();
226
- prev = pad;
227
- continue;
228
- }
229
- prev = pad;
901
+ /* SELECT toggles the background music, in any state */
902
+ if ((pad & PAD_SELECT) && !(prev & PAD_SELECT)) music_toggle();
230
903
 
231
- fall_rate = (pad & PAD_DOWN) ? 4 : 30;
232
- if (++fall_timer >= fall_rate) {
233
- fall_timer = 0;
234
- if (collides(piece_x, piece_y + 1)) {
235
- lock_piece();
236
- new_piece();
237
- } else {
238
- piece_y++;
904
+ if (state == ST_TITLE) {
905
+ if ((pad & PAD_START) && !(prev & PAD_START)) start_game();
906
+ } else if (state == ST_PLAY) {
907
+ if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
908
+ && !collides((uint8_t)(piece_x - 1), piece_y)) { piece_x--; sfx_move(); }
909
+ if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
910
+ && !collides((uint8_t)(piece_x + 1), piece_y)) { piece_x++; sfx_move(); }
911
+ if ((pad & PAD_A) && !(prev & PAD_A) && !piece_magic) {
912
+ t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
913
+ sfx_rotate();
914
+ }
915
+ if ((pad & PAD_B) && !(prev & PAD_B) && !piece_magic) {
916
+ t = piece[2]; piece[2] = piece[1]; piece[1] = piece[0]; piece[0] = t;
917
+ sfx_rotate();
918
+ }
919
+ if ((pad & PAD_START) && !(prev & PAD_START)) {
920
+ while (!collides(piece_x, (uint8_t)(piece_y + 1))) piece_y++;
921
+ sfx_drop();
922
+ lock_and_resolve();
923
+ spawn();
924
+ if (state == ST_OVER) sfx_over();
925
+ start_redraw();
239
926
  }
240
- }
241
927
 
242
- /* Re-draw piece in new position. */
243
- for (i = 0; i < 3; i++) {
244
- pr = piece_y + i;
245
- if (pr >= 0 && pr < ROWS) draw_cell(piece_x, pr, piece[i]);
928
+ rate = (pad & PAD_DOWN) ? 3 : cur_fall_rate;
929
+ if (++fall_timer >= rate) {
930
+ fall_timer = 0;
931
+ if (collides(piece_x, (uint8_t)(piece_y + 1))) {
932
+ sfx_drop();
933
+ lock_and_resolve();
934
+ spawn();
935
+ if (state == ST_OVER) sfx_over();
936
+ start_redraw();
937
+ } else {
938
+ piece_y++;
939
+ }
940
+ }
941
+ } else { /* ST_OVER — START restarts immediately */
942
+ if ((pad & PAD_START) && !(prev & PAD_START)) start_game();
246
943
  }
944
+
945
+ redraw_collect(); /* queue next frame's VRAM writes (RAM only) */
946
+ prev = pad;
247
947
  }
248
948
  }