romdevtools 0.27.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 +309 -0
  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 +141 -24
  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,21 +1,34 @@
1
1
  /* ── puzzle.c — Game Boy match-3 falling-block scaffold ─────────────
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
+ * 8-wide × 14-tall well drawn via BG tilemap (each cell = 1 BG tile).
4
+ * 1×3 vertical active piece; LEFT/RIGHT shifts, A/B cycles the colour
5
+ * order, DOWN soft-drops, START hard-drops. Matches of 3+ in a row —
6
+ * horizontal, vertical, or either diagonal clear, survivors fall
7
+ * (gravity), and cascades chain with rising score.
7
8
  *
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.
9
+ * On DMG we differentiate the three block kinds by SHAPE (2bpp stripe
10
+ * patterns), not colour. The GBC template is the full-colour version.
11
+ *
12
+ * RENDERING CONTRACT (the "pieces flash / don't render" fix): this
13
+ * core silently DROPS VRAM writes during active display — and can
14
+ * even drop one early in vblank. So (mirroring the GBC reference
15
+ * puzzle):
16
+ * - The FALLING piece is OAM sprites 0-2 (one OAM DMA per frame —
17
+ * no BG writes at all to move it, no erase artifacts).
18
+ * - The LOCKED well is BG tiles, written ONLY right after
19
+ * wait_vblank(): a budgeted diff (grid vs shadow) plus a rolling
20
+ * SCRUB that continuously repaints the well from grid[], so any
21
+ * dropped write self-heals within ~half a second.
22
+ * - enable_vblank_irq() at boot → wait_vblank HALTs to the real
23
+ * vblank leading edge (also ~30x faster on the WASM core than
24
+ * the LY-polling fallback).
12
25
  */
13
26
 
14
27
  #include "gb_hardware.h"
15
28
  #include "gb_runtime.h"
16
29
 
17
- #define COLS 6
18
- #define ROWS 12
30
+ #define COLS 8
31
+ #define ROWS 14
19
32
 
20
33
  #define T_BLANK 0
21
34
  #define T_R 1
@@ -23,6 +36,10 @@
23
36
  #define T_B 3
24
37
  #define T_WALL 4
25
38
 
39
+ /* Map placement: centre the 8-col well → BG col offset +6, row offset +1. */
40
+ #define WELL_MX 6
41
+ #define WELL_MY 1
42
+
26
43
  /* tile_blank is the EMPTY-cell / backdrop tile. It is NOT all-zero: a
27
44
  * subtle dither (colour 0 + faint colour 1) so the empty playfield and the
28
45
  * area around the well read as a textured surface, never one flat colour
@@ -38,8 +55,7 @@ static const uint8_t tile_wall[16] = {
38
55
  0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
39
56
  };
40
57
  /* 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. */
58
+ * by *shape*, not colour-on-CGB). */
43
59
  static const uint8_t tile_r[16] = {
44
60
  0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
45
61
  0xFF,0x00, 0xFF,0x00, 0xFF,0x00, 0xFF,0x00,
@@ -55,18 +71,30 @@ static const uint8_t tile_b[16] = {
55
71
 
56
72
  static const uint16_t bg_palette[4] = { 0x7FFF, 0x5294, 0x294A, 0x0000 };
57
73
 
58
- static uint8_t grid[ROWS][COLS];
74
+ #define NCELL (ROWS * COLS)
75
+ static uint8_t grid[NCELL]; /* 0 = empty, 1..3 = block colour */
76
+ static uint8_t shadow[NCELL]; /* what's currently on the BG (diff redraw) */
77
+ static uint8_t matched[NCELL]; /* scratch: cells flagged to clear */
59
78
  static uint8_t piece[3];
60
79
  static int16_t piece_x, piece_y;
61
80
  static uint8_t fall_timer;
62
81
  static uint16_t score;
63
- static uint32_t rng = 1;
82
+ static uint16_t rng = 0xACE1;
83
+
84
+ #define G(r,c) grid[(uint8_t)((r) * COLS + (c))]
85
+ #define M(r,c) matched[(uint8_t)((r) * COLS + (c))]
64
86
 
65
- static uint32_t xorshift(void) {
66
- rng ^= rng << 13;
67
- rng ^= rng >> 17;
68
- rng ^= rng << 5;
69
- return rng;
87
+ /* the 4 line directions scanned for matches: horizontal, vertical, and
88
+ * both diagonals; each line is only walked from its lowest cell. */
89
+ static const int8_t DIRS[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
90
+
91
+ /* 16-bit xorshift — kept 16-bit on purpose (sm83 has no fast 32-bit
92
+ * shifts; a wider generator there degenerates toward one value). */
93
+ static uint8_t xorshift(void) {
94
+ rng ^= rng << 7;
95
+ rng ^= rng >> 9;
96
+ rng ^= rng << 8;
97
+ return (uint8_t)(rng >> 8);
70
98
  }
71
99
 
72
100
  static uint8_t random_colour(void) { return 1 + (xorshift() % 3); }
@@ -88,20 +116,6 @@ static uint8_t tile_for(uint8_t c) {
88
116
  }
89
117
  }
90
118
 
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);
96
- }
97
-
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]);
103
- }
104
-
105
119
  static uint8_t collides(int16_t col, int16_t row) {
106
120
  uint8_t i;
107
121
  int16_t r;
@@ -109,36 +123,128 @@ static uint8_t collides(int16_t col, int16_t row) {
109
123
  for (i = 0; i < 3; i++) {
110
124
  r = row + i;
111
125
  if (r >= ROWS) return 1;
112
- if (r >= 0 && grid[r][col] != 0) return 1;
126
+ if (r >= 0 && G(r, col) != 0) return 1;
113
127
  }
114
128
  return 0;
115
129
  }
116
130
 
117
- static void lock_piece(void) {
131
+ /* ── match / clear / gravity core (mirrors the GBC reference) ─────── */
132
+
133
+ /* Flag every cell in a 3+ run (any of the 4 directions) into matched[];
134
+ * return the count. A run is walked once, from its lowest end only. */
135
+ static uint8_t mark_and_count(void) {
136
+ uint8_t r, c, d, len, cnt, col, k;
137
+ int8_t dr, dc;
138
+ int16_t sr, sc;
139
+
140
+ for (r = 0; r < NCELL; r++) matched[r] = 0;
141
+
142
+ for (r = 0; r < ROWS; r++) {
143
+ for (c = 0; c < COLS; c++) {
144
+ col = G(r, c);
145
+ if (col == 0) continue;
146
+ for (d = 0; d < 4; d++) {
147
+ dr = DIRS[d][0];
148
+ dc = DIRS[d][1];
149
+ sr = (int16_t)r - dr;
150
+ sc = (int16_t)c - dc;
151
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
152
+ && G(sr, sc) == col) continue; /* not the run's start */
153
+ len = 1;
154
+ sr = (int16_t)r + dr;
155
+ sc = (int16_t)c + dc;
156
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
157
+ && G(sr, sc) == col) {
158
+ len++;
159
+ sr += dr;
160
+ sc += dc;
161
+ }
162
+ if (len >= 3) {
163
+ sr = (int16_t)r;
164
+ sc = (int16_t)c;
165
+ for (k = 0; k < len; k++) {
166
+ M(sr, sc) = 1;
167
+ sr += dr;
168
+ sc += dc;
169
+ }
170
+ }
171
+ }
172
+ }
173
+ }
174
+
175
+ cnt = 0;
176
+ for (r = 0; r < NCELL; r++) if (matched[r]) cnt++;
177
+ return cnt;
178
+ }
179
+
180
+ static void clear_marked(void) {
118
181
  uint8_t i;
182
+ for (i = 0; i < NCELL; i++) if (matched[i]) grid[i] = 0;
183
+ }
184
+
185
+ /* collapse each column so survivors rest on the floor — the "rows move
186
+ * down after a clear" the old template was missing. */
187
+ static void apply_gravity(void) {
188
+ uint8_t c, r, n, w;
189
+ uint8_t buf[ROWS];
190
+ for (c = 0; c < COLS; c++) {
191
+ n = 0;
192
+ for (r = 0; r < ROWS; r++)
193
+ if (G(r, c)) { buf[n] = G(r, c); n++; }
194
+ for (r = 0; r < (uint8_t)(ROWS - n); r++) G(r, c) = 0;
195
+ w = 0;
196
+ for (r = (uint8_t)(ROWS - n); r < ROWS; r++) { G(r, c) = buf[w]; w++; }
197
+ }
198
+ }
199
+
200
+ /* clear chime — one short square blip per cascade step, pitch rises with
201
+ * the chain so combos audibly escalate. */
202
+ static void sfx_clear(uint8_t chain) {
203
+ uint16_t p = 1797 + (uint16_t)chain * 26; /* ~C5 rising */
204
+ if (p > 1980) p = 1980;
205
+ sound_play_tone(1, p, 6);
206
+ }
207
+
208
+ /* settle the board after a lock: match → clear → gravity, looping so
209
+ * cascades chain; score scales with the chain depth. */
210
+ static void resolve_board(void) {
211
+ uint8_t n, chain = 0;
212
+ uint16_t amt;
213
+ while (1) {
214
+ n = mark_and_count();
215
+ if (n == 0) break;
216
+ chain++;
217
+ sfx_clear(chain);
218
+ clear_marked();
219
+ amt = (uint16_t)n * 10;
220
+ if (chain > 1) amt = amt * chain;
221
+ if (score < (uint16_t)(65500u - amt)) score += amt;
222
+ apply_gravity();
223
+ }
224
+ }
225
+
226
+ static void lock_piece(void) {
227
+ uint8_t i, written = 0;
119
228
  int16_t r;
120
- int16_t c;
121
- uint8_t a, b, d;
122
229
  for (i = 0; i < 3; i++) {
123
230
  r = piece_y + i;
124
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
231
+ if (r >= 0 && r < ROWS) { G(r, piece_x) = piece[i]; written++; }
125
232
  }
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;
136
- }
137
- }
233
+ sound_play_noise(3);
234
+ resolve_board();
235
+ if (written == 0) {
236
+ /* The piece locked entirely ABOVE the well the stack reached the
237
+ * top. Without this the game silently softlocks (invisible pieces
238
+ * locking off-screen forever). Scaffold behavior: low game-over
239
+ * tone, clear the board, restart the run. */
240
+ sound_play_tone(1, 1548, 30);
241
+ for (i = 0; i < NCELL; i++) grid[i] = 0;
242
+ score = 0;
138
243
  }
139
- draw_grid();
140
244
  }
141
245
 
246
+ /* ── rendering (vblank-budgeted; gameplay code never touches VRAM) ── */
247
+
142
248
  static void upload_tile(uint8_t slot, const uint8_t *src) {
143
249
  uint8_t *dst = (uint8_t *)(0x8000 + slot * 16);
144
250
  /* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
@@ -146,29 +252,106 @@ static void upload_tile(uint8_t slot, const uint8_t *src) {
146
252
  memcpy_vram(dst, src, 16);
147
253
  }
148
254
 
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;
255
+ #define VRAM_MAP ((volatile uint8_t *)0x9800)
256
+
257
+ /* Direct cell write ONLY safe with the LCD off or just after vblank. */
258
+ static void set_cell(uint8_t c, uint8_t r, uint8_t tile) {
259
+ VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX + c] = tile;
260
+ }
261
+
262
+ /* COLLECT/FLUSH split (the reference puzzle's architecture, and the part
263
+ * that actually fixes "pieces flash / don't render"):
264
+ * - collect_well() runs OUTSIDE vblank: scans for grid-vs-shadow diffs
265
+ * (bounded), or queues rolling SCRUB cells when nothing changed, into a
266
+ * tiny queue of precomputed (map offset, tile) pairs. RAM only.
267
+ * - flush_well() runs FIRST thing after wait_vblank: pure pointer writes,
268
+ * no scanning, no multiplies — the whole batch lands inside the ~10-line
269
+ * vblank window every frame. The scrub means even a write the core drops
270
+ * anyway heals itself on the next pass instead of sticking forever. */
271
+ #define WQ_MAX 4
272
+ static uint8_t wq_n;
273
+ static uint16_t wq_off[WQ_MAX];
274
+ static uint8_t wq_tile[WQ_MAX];
275
+ static uint8_t diff_cursor, scrub_cursor;
276
+
277
+ static uint16_t cell_off(uint8_t i) {
278
+ return (uint16_t)(WELL_MY + i / COLS) * 32 + WELL_MX + (i % COLS);
279
+ }
280
+
281
+ static void collect_well(void) {
282
+ uint8_t scanned = 0, i, k;
283
+ wq_n = 0;
284
+ i = diff_cursor;
285
+ while (scanned < NCELL && wq_n < WQ_MAX) {
286
+ if (grid[i] != shadow[i]) {
287
+ shadow[i] = grid[i];
288
+ wq_off[wq_n] = cell_off(i);
289
+ wq_tile[wq_n] = tile_for(grid[i]);
290
+ wq_n++;
291
+ }
292
+ i++;
293
+ if (i >= NCELL) i = 0;
294
+ scanned++;
295
+ }
296
+ diff_cursor = i;
297
+ if (wq_n == 0) {
298
+ /* idle: queue scrub cells so dropped writes self-heal */
299
+ for (k = 0; k < 2; k++) {
300
+ wq_off[wq_n] = cell_off(scrub_cursor);
301
+ wq_tile[wq_n] = tile_for(grid[scrub_cursor]);
302
+ wq_n++;
303
+ scrub_cursor++;
304
+ if (scrub_cursor >= NCELL) scrub_cursor = 0;
305
+ }
306
+ }
307
+ }
308
+
309
+ static void flush_well(void) {
310
+ uint8_t k;
311
+ for (k = 0; k < wq_n; k++) VRAM_MAP[wq_off[k]] = wq_tile[k];
312
+ wq_n = 0;
313
+ }
314
+
315
+ /* The falling piece = OAM sprites 0-2 (written to shadow_oam, flushed by
316
+ * one OAM DMA right after vblank starts). Rows above the well top (r < 0)
317
+ * park the sprite at Y=0 (offscreen). */
318
+ static void update_piece_sprites(void) {
319
+ uint8_t i, sy, sx;
320
+ int16_t r;
321
+ for (i = 0; i < 3; i++) {
322
+ r = piece_y + i;
323
+ if (r >= 0 && r < ROWS) {
324
+ sy = (uint8_t)((WELL_MY + r) * 8 + 16);
325
+ sx = (uint8_t)((WELL_MX + piece_x) * 8 + 8);
326
+ oam_set(i, sy, sx, tile_for(piece[i]), 0);
327
+ } else {
328
+ oam_set(i, 0, 0, 0, 0);
329
+ }
330
+ }
331
+ }
332
+
333
+ static void draw_well_frame(void) {
154
334
  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 */
335
+ for (r = 0; r < ROWS; r++) {
336
+ VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX - 1] = T_WALL;
337
+ VRAM_MAP[(uint16_t)(WELL_MY + r) * 32 + WELL_MX + COLS] = T_WALL;
158
338
  }
159
- for (r = 6; r <= 13; r++)
160
- map[13 * 32 + r] = T_WALL; /* floor */
339
+ for (r = 0; r < (uint8_t)(COLS + 2); r++)
340
+ VRAM_MAP[(uint16_t)(WELL_MY + ROWS) * 32 + WELL_MX - 1 + r] = T_WALL;
161
341
  }
162
342
 
163
343
  void main(void) {
164
- uint8_t pad, prev = 0, fall_rate, t;
165
- int16_t r, c;
166
- uint8_t i;
344
+ uint8_t pad, prev = 0, fall_rate, t, i;
345
+ int16_t c;
167
346
  uint8_t *map;
168
- int16_t pr;
169
347
 
170
348
  lcd_init_default();
349
+ enable_vblank_irq();
350
+ sound_init();
351
+ oam_dma_init_hram();
352
+ oam_clear();
171
353
  LCDC = 0;
354
+ OBP0 = 0xE4; /* DMG sprite palette: 3=black .. 0=white */
172
355
 
173
356
  upload_tile(T_BLANK, tile_blank);
174
357
  upload_tile(T_R, tile_r);
@@ -185,64 +368,59 @@ void main(void) {
185
368
  map = (uint8_t *)0x9800;
186
369
  for (i = 0; i < 32; i++) {
187
370
  c = 0;
188
- while (c < 32) { map[i * 32 + c] = T_BLANK; c++; }
371
+ while (c < 32) { map[(uint16_t)i * 32 + c] = T_BLANK; c++; }
189
372
  }
190
373
 
191
- for (r = 0; r < ROWS; r++)
192
- for (c = 0; c < COLS; c++)
193
- grid[r][c] = 0;
374
+ for (i = 0; i < NCELL; i++) { grid[i] = 0; shadow[i] = 0; }
194
375
 
195
376
  score = 0;
196
377
  fall_timer = 0;
378
+ rng ^= DIV; /* a dash of boot-time entropy */
197
379
  new_piece();
198
- draw_well();
199
- draw_grid();
380
+ draw_well_frame();
200
381
 
201
- LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_TILE_DATA_LO;
382
+ LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO;
202
383
 
203
384
  while (1) {
204
- 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
- }
212
-
213
385
  pad = joypad_read();
214
386
 
215
387
  if ((pad & PAD_LEFT) && !(prev & PAD_LEFT)
216
- && !collides(piece_x - 1, piece_y)) piece_x--;
388
+ && !collides(piece_x - 1, piece_y)) { piece_x--; sound_play_tone(1, 1899, 2); }
217
389
  if ((pad & PAD_RIGHT) && !(prev & PAD_RIGHT)
218
- && !collides(piece_x + 1, piece_y)) piece_x++;
390
+ && !collides(piece_x + 1, piece_y)) { piece_x++; sound_play_tone(1, 1899, 2); }
219
391
  if ((pad & PAD_A) && !(prev & PAD_A)) {
220
392
  t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
393
+ sound_play_tone(1, 1923, 2);
394
+ }
395
+ if ((pad & PAD_B) && !(prev & PAD_B)) {
396
+ t = piece[2]; piece[2] = piece[1]; piece[1] = piece[0]; piece[0] = t;
397
+ sound_play_tone(1, 1923, 2);
221
398
  }
222
399
  if ((pad & PAD_START) && !(prev & PAD_START)) {
223
400
  while (!collides(piece_x, piece_y + 1)) piece_y++;
224
401
  lock_piece();
225
402
  new_piece();
226
- prev = pad;
227
- continue;
228
- }
229
- prev = pad;
230
-
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++;
403
+ } else {
404
+ fall_rate = (pad & PAD_DOWN) ? 4 : 30;
405
+ if (++fall_timer >= fall_rate) {
406
+ fall_timer = 0;
407
+ if (collides(piece_x, piece_y + 1)) {
408
+ lock_piece();
409
+ new_piece();
410
+ } else {
411
+ piece_y++;
412
+ }
239
413
  }
240
414
  }
415
+ prev = pad;
241
416
 
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]);
246
- }
417
+ /* COLLECT (RAM only, runs in active display) */
418
+ update_piece_sprites();
419
+ collect_well();
420
+ /* then FLUSH right after vblank starts: OAM DMA first (sprites
421
+ * tear if it slips out of vblank), then the queued BG writes. */
422
+ wait_vblank();
423
+ oam_dma_flush();
424
+ flush_well();
247
425
  }
248
426
  }
@@ -95,11 +95,23 @@ static void reset_run(void) {
95
95
  game_over_timer = 0;
96
96
  }
97
97
 
98
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
99
+ * The old code derived the spawn column from spawn_timer, but the caller
100
+ * resets spawn_timer just before calling here, so it was CONSTANT and
101
+ * every enemy spawned in the same left column/lane. */
102
+ static uint8_t rng_state = 0xA5;
103
+ static uint8_t rand8(void) {
104
+ uint8_t lsb = (uint8_t)(rng_state & 1);
105
+ rng_state >>= 1;
106
+ if (lsb) rng_state ^= 0xB8;
107
+ return rng_state;
108
+ }
109
+
98
110
  static void spawn_obstacle(void) {
99
111
  uint8_t i;
100
112
  for (i = 0; i < MAX_OBSTACLES; i++) {
101
113
  if (!obstacles[i].alive) {
102
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
114
+ obstacles[i].x = lane_x[rand8() % 3];
103
115
  obstacles[i].y = 0;
104
116
  obstacles[i].alive = 1;
105
117
  return;
@@ -86,11 +86,23 @@ static void fire(void) {
86
86
  }
87
87
  }
88
88
 
89
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
90
+ * The old code derived the spawn column from spawn_timer, but the caller
91
+ * resets spawn_timer just before calling here, so it was CONSTANT and
92
+ * every enemy spawned in the same left column/lane. */
93
+ static uint8_t rng_state = 0xA5;
94
+ static uint8_t rand8(void) {
95
+ uint8_t lsb = (uint8_t)(rng_state & 1);
96
+ rng_state >>= 1;
97
+ if (lsb) rng_state ^= 0xB8;
98
+ return rng_state;
99
+ }
100
+
89
101
  static void spawn(void) {
90
102
  uint8_t i;
91
103
  for (i = 0; i < MAX_ENEMIES; i++) {
92
104
  if (!enemies[i].alive) {
93
- enemies[i].x = ((spawn_timer * 37) & 0x7F) % (160 - 16) + 8;
105
+ enemies[i].x = rand8() % (160 - 16) + 8;
94
106
  enemies[i].y = 0;
95
107
  enemies[i].alive = 1;
96
108
  return;
@@ -130,15 +130,21 @@ void main(void) {
130
130
 
131
131
  while (1) {
132
132
  wait_vblank();
133
+ /* OAM DMA FIRST — at the leading edge of vblank. The old order staged
134
+ * 45 oam_set CALLS before the DMA; the SDCC call overhead pushed the
135
+ * DMA ~a third of the frame into ACTIVE display, so the sprites tore
136
+ * on one fixed scanline ("horizontal line a 3rd of the way down").
137
+ * Sprites now display the state staged LAST frame (1 frame of latency,
138
+ * imperceptible in Pong). */
139
+ oam_dma_flush();
133
140
 
134
- /* Stage OAM: left paddle (2 stacked), right paddle (2 stacked), ball. */
135
- for (i = 0; i < 40; i++) oam_set(i, 0, 0, 0, 0);
141
+ /* Stage next frame's OAM (RAM only safe any time). Slots 5-39 were
142
+ * zeroed once by oam_clear() at boot and never change. */
136
143
  oam_set(0, (uint8_t)(p1y + 16), (uint8_t)(PADDLE_X1 + 8), 1, 0);
137
144
  oam_set(1, (uint8_t)(p1y + 16 + 8), (uint8_t)(PADDLE_X1 + 8), 1, 0);
138
145
  oam_set(2, (uint8_t)(p2y + 16), (uint8_t)(PADDLE_X2 + 8), 1, 0);
139
146
  oam_set(3, (uint8_t)(p2y + 16 + 8), (uint8_t)(PADDLE_X2 + 8), 1, 0);
140
147
  oam_set(4, (uint8_t)(by + 16), (uint8_t)(bx + 8), 1, 0);
141
- oam_dma_flush();
142
148
 
143
149
  pad = joypad_read();
144
150
  if (pad & PAD_UP && p1y > COURT_TOP) p1y -= 2;
@@ -65,18 +65,6 @@ static int on_platform(s16 px, s16 py) {
65
65
  return 0;
66
66
  }
67
67
 
68
- static int blocked_below(s16 px, s16 py) {
69
- for (int i = 0; i < N_PLATFORMS; i++) {
70
- const Rect *p = &platforms[i];
71
- if (py + 8 <= p->y && py + 9 > p->y
72
- && px + 8 > p->x
73
- && px < p->x + p->w) {
74
- return 1;
75
- }
76
- }
77
- return 0;
78
- }
79
-
80
68
  int main(void) {
81
69
  /* ── BG palette (for the platform tile) ──────────────────────────
82
70
  * pal_bg_mem[i] is the BG palette. */
@@ -182,7 +170,13 @@ int main(void) {
182
170
  /* Vertical with platform-stop. */
183
171
  s32 np = py + vy;
184
172
  s16 npy = np >> 4;
185
- if (vy > 0 && blocked_below(ipx, ipy)) {
173
+ /* THE fall-through-the-floor fix: this used to be additionally
174
+ * gated on blocked_below(), which only matches when a platform
175
+ * top is within ONE pixel of the feet — but falls reach 20 px/
176
+ * frame, so the (correct) crossing test below almost never got
177
+ * to run and the player tunnelled through every platform. The
178
+ * crossing test alone is the right check. */
179
+ if (vy > 0) {
186
180
  for (int i = 0; i < N_PLATFORMS; i++) {
187
181
  const Rect *p = &platforms[i];
188
182
  if (ipy + 8 <= p->y && npy + 8 >= p->y