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
@@ -100,11 +100,23 @@ static void reset_run(void) {
100
100
  game_over_timer = 0;
101
101
  }
102
102
 
103
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
104
+ * The old code derived the spawn column from spawn_timer, but the caller
105
+ * resets spawn_timer just before calling here, so it was CONSTANT and
106
+ * every enemy spawned in the same left column/lane. */
107
+ static uint8_t rng_state = 0xA5;
108
+ static uint8_t rand8(void) {
109
+ uint8_t lsb = (uint8_t)(rng_state & 1);
110
+ rng_state >>= 1;
111
+ if (lsb) rng_state ^= 0xB8;
112
+ return rng_state;
113
+ }
114
+
103
115
  static void spawn_obstacle(void) {
104
116
  uint8_t i;
105
117
  for (i = 0; i < MAX_OBSTACLES; i++) {
106
118
  if (!obstacles[i].alive) {
107
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
119
+ obstacles[i].x = lane_x[rand8() % 3];
108
120
  obstacles[i].y = 0;
109
121
  obstacles[i].alive = 1;
110
122
  return;
@@ -94,11 +94,23 @@ static void fire(void) {
94
94
  }
95
95
  }
96
96
 
97
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
98
+ * The old code derived the spawn column from spawn_timer, but the caller
99
+ * resets spawn_timer just before calling here, so it was CONSTANT and
100
+ * every enemy spawned in the same left column/lane. */
101
+ static uint8_t rng_state = 0xA5;
102
+ static uint8_t rand8(void) {
103
+ uint8_t lsb = (uint8_t)(rng_state & 1);
104
+ rng_state >>= 1;
105
+ if (lsb) rng_state ^= 0xB8;
106
+ return rng_state;
107
+ }
108
+
97
109
  static void spawn(void) {
98
110
  uint8_t i;
99
111
  for (i = 0; i < MAX_ENEMIES; i++) {
100
112
  if (!enemies[i].alive) {
101
- enemies[i].x = ((spawn_timer * 37) & 0x7F) % (160 - 16) + 8;
113
+ enemies[i].x = rand8() % (160 - 16) + 8;
102
114
  enemies[i].y = 0;
103
115
  enemies[i].alive = 1;
104
116
  return;
@@ -133,15 +133,21 @@ void main(void) {
133
133
 
134
134
  while (1) {
135
135
  wait_vblank();
136
+ /* OAM DMA FIRST — at the leading edge of vblank. The old order staged
137
+ * 45 oam_set CALLS before the DMA; the SDCC call overhead pushed the
138
+ * DMA ~a third of the frame into ACTIVE display, so the sprites tore
139
+ * on one fixed scanline ("horizontal line a 3rd of the way down").
140
+ * Sprites now display the state staged LAST frame (1 frame of latency,
141
+ * imperceptible in Pong). */
142
+ oam_dma_flush();
136
143
 
137
- /* Stage OAM: left paddle (2 stacked), right paddle (2 stacked), ball. */
138
- for (i = 0; i < 40; i++) oam_set(i, 0, 0, 0, 0);
144
+ /* Stage next frame's OAM (RAM only safe any time). Slots 5-39 were
145
+ * zeroed once by oam_clear() at boot and never change. */
139
146
  oam_set(0, (uint8_t)(p1y + 16), (uint8_t)(PADDLE_X1 + 8), 1, 0);
140
147
  oam_set(1, (uint8_t)(p1y + 16 + 8), (uint8_t)(PADDLE_X1 + 8), 1, 0);
141
148
  oam_set(2, (uint8_t)(p2y + 16), (uint8_t)(PADDLE_X2 + 8), 1, 0);
142
149
  oam_set(3, (uint8_t)(p2y + 16 + 8), (uint8_t)(PADDLE_X2 + 8), 1, 0);
143
150
  oam_set(4, (uint8_t)(by + 16), (uint8_t)(bx + 8), 1, 0);
144
- oam_dma_flush();
145
151
 
146
152
  pad = joypad_read();
147
153
  if (pad & PAD_UP && p1y > COURT_TOP) p1y -= 2;
@@ -140,26 +140,87 @@ static void draw_piece(s16 col, s16 row, bool clear) {
140
140
  }
141
141
  }
142
142
 
143
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
144
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
145
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
146
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
147
+ * 4 directions, clears them, applies per-column gravity, and loops so
148
+ * cascades chain (score scales with chain depth). */
149
+ static u8 matched[ROWS][COLS];
150
+ static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
151
+
152
+ static u8 mark_and_count(void) {
153
+ u8 r, c, d, len, k, cnt;
154
+ u8 col;
155
+ s8 dr, dc;
156
+ int sr, sc;
157
+ cnt = 0;
158
+ for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
159
+ for (r = 0; r < ROWS; r++) {
160
+ for (c = 0; c < COLS; c++) {
161
+ col = grid[r][c];
162
+ if (col == 0) continue;
163
+ for (d = 0; d < 4; d++) {
164
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
165
+ sr = (int)r - dr; sc = (int)c - dc;
166
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
167
+ && grid[sr][sc] == col) continue; /* not the run's start */
168
+ len = 1;
169
+ sr = (int)r + dr; sc = (int)c + dc;
170
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
171
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
172
+ if (len >= 3) {
173
+ sr = r; sc = c;
174
+ for (k = 0; k < len; k++) {
175
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
176
+ sr += dr; sc += dc;
177
+ }
178
+ }
179
+ }
180
+ }
181
+ }
182
+ return cnt;
183
+ }
184
+
185
+ /* collapse each column so survivors rest on the floor (in place: walk
186
+ * from the bottom, copying gems down to a write cursor, then zero above) */
187
+ static void apply_gravity(void) {
188
+ u8 c;
189
+ int r, w;
190
+ for (c = 0; c < COLS; c++) {
191
+ w = ROWS - 1;
192
+ for (r = ROWS - 1; r >= 0; r--) {
193
+ if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
194
+ }
195
+ for (; w >= 0; w--) grid[w][c] = 0;
196
+ }
197
+ }
198
+
199
+ static void resolve_board(void) {
200
+ u8 n, r, c, chain;
201
+ unsigned int amt;
202
+ chain = 0;
203
+ while (1) {
204
+ n = mark_and_count();
205
+ if (n == 0) break;
206
+ chain++;
207
+ for (r = 0; r < ROWS; r++)
208
+ for (c = 0; c < COLS; c++)
209
+ if (matched[r][c]) grid[r][c] = 0;
210
+ amt = (unsigned int)n * 10u;
211
+ if (chain > 1) amt = amt * chain;
212
+ if (score < 65500u) score += amt;
213
+ sfx_tone(0, 250, 12); /* clear chime */
214
+ apply_gravity();
215
+ }
216
+ }
217
+
143
218
  static void lock_piece(void) {
144
219
  for (u16 i = 0; i < 3; i++) {
145
220
  s16 r = piece_y + i;
146
221
  if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
147
222
  }
148
- /* Check horizontal triples on the affected rows. */
149
- for (u16 i = 0; i < 3; i++) {
150
- s16 r = piece_y + i;
151
- if (r < 0 || r >= ROWS) continue;
152
- for (s16 c = 0; c <= COLS - 3; c++) {
153
- u8 a = grid[r][c], b = grid[r][c + 1], d = grid[r][c + 2];
154
- if (a != 0 && a == b && b == d) {
155
- grid[r][c] = 0;
156
- grid[r][c + 1] = 0;
157
- grid[r][c + 2] = 0;
158
- if (score < 65500u) score += 30;
159
- sfx_tone(0, 250, 12); /* triple-clear chime */
160
- }
161
- }
162
- }
223
+ resolve_board();
163
224
  draw_grid();
164
225
  }
165
226
 
@@ -129,11 +129,23 @@ static void reset_run(void) {
129
129
  game_over_timer = 0;
130
130
  }
131
131
 
132
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
133
+ * The old code derived the spawn column from spawn_timer, but the caller
134
+ * resets spawn_timer just before calling here, so it was CONSTANT and
135
+ * every enemy spawned in the same left column/lane. */
136
+ static u8 rng_state = 0xA5;
137
+ static u8 rand8(void) {
138
+ u8 lsb = (u8)(rng_state & 1);
139
+ rng_state >>= 1;
140
+ if (lsb) rng_state ^= 0xB8;
141
+ return rng_state;
142
+ }
143
+
132
144
  static void spawn_obstacle(void) {
133
145
  u16 i;
134
146
  for (i = 0; i < MAX_OBSTACLES; i++) {
135
147
  if (!obstacles[i].alive) {
136
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
148
+ obstacles[i].x = lane_x[rand8() % 3];
137
149
  obstacles[i].y = 0;
138
150
  obstacles[i].alive = TRUE;
139
151
  return;
@@ -91,11 +91,23 @@ static void fire(Obj* ship, Obj* pool) {
91
91
  }
92
92
  }
93
93
 
94
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
95
+ * The old code derived the spawn column from spawn_timer, but the caller
96
+ * resets spawn_timer just before calling here, so it was CONSTANT and
97
+ * every enemy spawned in the same left column/lane. */
98
+ static u8 rng_state = 0xA5;
99
+ static u8 rand8(void) {
100
+ u8 lsb = (u8)(rng_state & 1);
101
+ rng_state >>= 1;
102
+ if (lsb) rng_state ^= 0xB8;
103
+ return rng_state;
104
+ }
105
+
94
106
  static void spawn_enemy(void) {
95
107
  u16 i;
96
108
  for (i = 0; i < MAX_ENEMIES; i++) {
97
109
  if (!enemies[i].alive) {
98
- enemies[i].x = ((spawn_timer * 37) & 0xFF) % (320 - 16) + 8;
110
+ enemies[i].x = (s16)((((u16)rand8()) * (320 - 16)) >> 8) + 8;
99
111
  enemies[i].y = -8;
100
112
  enemies[i].alive = TRUE;
101
113
  return;
@@ -17,6 +17,7 @@
17
17
  */
18
18
  #include "gg_hw.h"
19
19
  #include "gg_sfx.h"
20
+ #include "gg_music.h"
20
21
  #include <stdint.h>
21
22
 
22
23
  extern void gg_vdp_init(void);
@@ -163,6 +164,8 @@ void main(void) {
163
164
 
164
165
  gg_sprite_init();
165
166
  sfx_init();
167
+ music_init();
168
+ music_play(0); /* continuous background music ("no sound" was the playtest verdict) */
166
169
  gg_sprite_set(0, (uint8_t)(px >> 4), (uint8_t)(py >> 4), 0);
167
170
  gg_sat_upload();
168
171
  gg_vdp_display_on();
@@ -176,6 +179,7 @@ void main(void) {
176
179
  const Rect *p;
177
180
  gg_vblank_wait();
178
181
  sfx_update();
182
+ music_update();
179
183
 
180
184
  ipx = px >> 4;
181
185
  ipy = py >> 4;
@@ -7,6 +7,7 @@
7
7
  */
8
8
  #include "gg_hw.h"
9
9
  #include "gg_sfx.h"
10
+ #include "gg_music.h"
10
11
  #include <stdint.h>
11
12
 
12
13
  extern void gg_vdp_init(void);
@@ -140,27 +141,89 @@ static uint8_t collides(int8_t col, int8_t row) {
140
141
  return 0;
141
142
  }
142
143
 
144
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
145
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
146
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
147
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
148
+ * 4 directions, clears them, applies per-column gravity, and loops so
149
+ * cascades chain (score scales with chain depth). */
150
+ static uint8_t matched[ROWS][COLS];
151
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
152
+
153
+ static uint8_t mark_and_count(void) {
154
+ uint8_t r, c, d, len, k, cnt;
155
+ uint8_t col;
156
+ int8_t dr, dc;
157
+ int sr, sc;
158
+ cnt = 0;
159
+ for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
160
+ for (r = 0; r < ROWS; r++) {
161
+ for (c = 0; c < COLS; c++) {
162
+ col = grid[r][c];
163
+ if (col == 0) continue;
164
+ for (d = 0; d < 4; d++) {
165
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
166
+ sr = (int)r - dr; sc = (int)c - dc;
167
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
168
+ && grid[sr][sc] == col) continue; /* not the run's start */
169
+ len = 1;
170
+ sr = (int)r + dr; sc = (int)c + dc;
171
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
172
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
173
+ if (len >= 3) {
174
+ sr = r; sc = c;
175
+ for (k = 0; k < len; k++) {
176
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
177
+ sr += dr; sc += dc;
178
+ }
179
+ }
180
+ }
181
+ }
182
+ }
183
+ return cnt;
184
+ }
185
+
186
+ /* collapse each column so survivors rest on the floor (in place: walk
187
+ * from the bottom, copying gems down to a write cursor, then zero above) */
188
+ static void apply_gravity(void) {
189
+ uint8_t c;
190
+ int r, w;
191
+ for (c = 0; c < COLS; c++) {
192
+ w = ROWS - 1;
193
+ for (r = ROWS - 1; r >= 0; r--) {
194
+ if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
195
+ }
196
+ for (; w >= 0; w--) grid[w][c] = 0;
197
+ }
198
+ }
199
+
200
+ static void resolve_board(void) {
201
+ uint8_t n, r, c, chain;
202
+ unsigned int amt;
203
+ chain = 0;
204
+ while (1) {
205
+ n = mark_and_count();
206
+ if (n == 0) break;
207
+ chain++;
208
+ for (r = 0; r < ROWS; r++)
209
+ for (c = 0; c < COLS; c++)
210
+ if (matched[r][c]) grid[r][c] = 0;
211
+ amt = (unsigned int)n * 10u;
212
+ if (chain > 1) amt = amt * chain;
213
+ if (score < 65500) score = (uint16_t)(score + amt);
214
+ sfx_tone(0, 200, 10); /* clear chime */
215
+ apply_gravity();
216
+ }
217
+ }
218
+
143
219
  static void lock_piece(void) {
144
220
  uint8_t i;
145
221
  int8_t r;
146
- int8_t c;
147
- uint8_t a, b, d;
148
222
  for (i = 0; i < 3; i++) {
149
223
  r = (int8_t)(piece_y + i);
150
224
  if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
151
225
  }
152
- for (i = 0; i < 3; i++) {
153
- r = (int8_t)(piece_y + i);
154
- if (r < 0 || r >= ROWS) continue;
155
- for (c = 0; c <= COLS - 3; c++) {
156
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
157
- if (a != 0 && a == b && b == d) {
158
- grid[r][c] = 0; grid[r][c + 1] = 0; grid[r][c + 2] = 0;
159
- if (score < 65500) score = (uint16_t)(score + 30);
160
- sfx_tone(0, 200, 10); /* triple-clear chime */
161
- }
162
- }
163
- }
226
+ resolve_board();
164
227
  draw_grid();
165
228
  }
166
229
 
@@ -193,12 +256,15 @@ void main(void) {
193
256
  draw_grid();
194
257
 
195
258
  sfx_init();
259
+ music_init();
260
+ music_play(0); /* continuous background music ("no sound" was the playtest verdict) */
196
261
  gg_vdp_display_on();
197
262
 
198
263
  do {
199
264
  uint8_t pad, fall_rate, t;
200
265
  gg_vblank_wait();
201
266
  sfx_update();
267
+ music_update();
202
268
  draw_piece(1);
203
269
 
204
270
  pad = gg_joypad_read();
@@ -12,6 +12,7 @@
12
12
  */
13
13
  #include "gg_hw.h"
14
14
  #include "gg_sfx.h"
15
+ #include "gg_music.h"
15
16
  #include <stdint.h>
16
17
 
17
18
  extern void gg_vdp_init(void);
@@ -141,11 +142,23 @@ static void reset_run(void) {
141
142
  game_over_timer = 0;
142
143
  }
143
144
 
145
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
146
+ * The old code derived the spawn column from spawn_timer, but the caller
147
+ * resets spawn_timer just before calling here, so it was CONSTANT and
148
+ * every enemy spawned in the same left column/lane. */
149
+ static uint8_t rng_state = 0xA5;
150
+ static uint8_t rand8(void) {
151
+ uint8_t lsb = (uint8_t)(rng_state & 1);
152
+ rng_state >>= 1;
153
+ if (lsb) rng_state ^= 0xB8;
154
+ return rng_state;
155
+ }
156
+
144
157
  static void spawn_obstacle(void) {
145
158
  uint8_t i;
146
159
  for (i = 0; i < MAX_OBSTACLES; i++) {
147
160
  if (!obstacles[i].alive) {
148
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
161
+ obstacles[i].x = lane_x[rand8() % 3];
149
162
  obstacles[i].y = VIS_Y0; /* enter at the top of the visible window */
150
163
  obstacles[i].alive = 1;
151
164
  return;
@@ -162,6 +175,8 @@ void main(void) {
162
175
  draw_track();
163
176
  gg_sprite_init();
164
177
  sfx_init();
178
+ music_init();
179
+ music_play(0); /* continuous background music ("no sound" was the playtest verdict) */
165
180
  gg_vdp_display_on();
166
181
 
167
182
  reset_run();
@@ -173,6 +188,7 @@ void main(void) {
173
188
  int16_t step;
174
189
  gg_vblank_wait();
175
190
  sfx_update();
191
+ music_update();
176
192
 
177
193
  /* Stage SAT. */
178
194
  slot = 0;
@@ -8,6 +8,7 @@
8
8
  */
9
9
  #include "gg_hw.h"
10
10
  #include "gg_sfx.h"
11
+ #include "gg_music.h"
11
12
  #include <stdint.h>
12
13
 
13
14
  extern void gg_vdp_init(void);
@@ -145,12 +146,24 @@ static void fire(void) {
145
146
  }
146
147
  }
147
148
 
149
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
150
+ * The old code derived the spawn column from spawn_timer, but the caller
151
+ * resets spawn_timer just before calling here, so it was CONSTANT and
152
+ * every enemy spawned in the same left column/lane. */
153
+ static uint8_t rng_state = 0xA5;
154
+ static uint8_t rand8(void) {
155
+ uint8_t lsb = (uint8_t)(rng_state & 1);
156
+ rng_state >>= 1;
157
+ if (lsb) rng_state ^= 0xB8;
158
+ return rng_state;
159
+ }
160
+
148
161
  static void spawn(void) {
149
162
  uint8_t i;
150
163
  for (i = 0; i < MAX_ENEMIES; i++) {
151
164
  if (!enemies[i].alive) {
152
165
  /* Spawn across the VISIBLE width (hardware X in [VIS_X0..VIS_X1-8]). */
153
- enemies[i].x = (uint8_t)(VIS_X0 + ((spawn_timer * 37u) % (VIS_W - 8)));
166
+ enemies[i].x = (uint8_t)(VIS_X0 + (rand8() % (VIS_W - 8)));
154
167
  enemies[i].y = VIS_Y0; /* enter at the top of the visible region */
155
168
  enemies[i].alive = 1;
156
169
  return;
@@ -179,6 +192,8 @@ void main(void) {
179
192
 
180
193
  gg_sprite_init();
181
194
  sfx_init();
195
+ music_init();
196
+ music_play(0); /* continuous background music ("no sound" was the playtest verdict) */
182
197
  gg_vdp_display_on();
183
198
 
184
199
  do {
@@ -186,6 +201,7 @@ void main(void) {
186
201
  uint8_t i, j;
187
202
  gg_vblank_wait();
188
203
  sfx_update();
204
+ music_update();
189
205
 
190
206
  /* Stage SAT for the new frame. */
191
207
  gg_sprite_set(0, player.x, player.y, T_SHIP);
@@ -5,6 +5,7 @@
5
5
  */
6
6
  #include "gg_hw.h"
7
7
  #include "gg_sfx.h"
8
+ #include "gg_music.h"
8
9
  #include <stdint.h>
9
10
 
10
11
  extern void gg_vdp_init(void);
@@ -125,6 +126,8 @@ void main(void) {
125
126
  draw_court();
126
127
  gg_sprite_init();
127
128
  sfx_init();
129
+ music_init();
130
+ music_play(0); /* continuous background music ("no sound" was the playtest verdict) */
128
131
  gg_vdp_display_on();
129
132
 
130
133
  reset_match();
@@ -135,6 +138,7 @@ void main(void) {
135
138
  int16_t target;
136
139
  gg_vblank_wait();
137
140
  sfx_update();
141
+ music_update();
138
142
 
139
143
  /* Stage SAT first — uploaded at vblank. */
140
144
  slot = 0;
@@ -68,11 +68,30 @@ void main(void) {
68
68
  if (btn && !prev && grounded) { vy = -6; sfx_tone(0, 100, 6); }
69
69
  prev = btn;
70
70
 
71
- vy++;
72
- if (vy > 4) vy = 4;
73
- py += vy;
74
- if (py < 0) py = 0;
75
- if (py > 96) py = 96;
76
- if (vy > 0 && on_platform(px, py)) { py = py & 0xFC; vy = 0; }
71
+ {
72
+ /* Land-on-top via a CROSSING test. The old check demanded
73
+ * py+6 == platform.y EXACTLY after the move — falls step up to
74
+ * 4px/frame, so the exact value was usually skipped (fall-through),
75
+ * and the `py & 0xFC` snap then broke the equality for the next
76
+ * frame's grounded test (couldn't jump from floating platforms). */
77
+ int16_t old_py = py;
78
+ uint8_t i;
79
+ vy++;
80
+ if (vy > 4) vy = 4;
81
+ py += vy;
82
+ if (vy > 0) {
83
+ for (i = 0; i < N_PLATFORMS; i++) {
84
+ if (old_py + 6 <= platforms[i].y && py + 6 >= platforms[i].y
85
+ && px + 6 > platforms[i].x
86
+ && px < platforms[i].x + platforms[i].w) {
87
+ py = platforms[i].y - 6;
88
+ vy = 0;
89
+ break;
90
+ }
91
+ }
92
+ }
93
+ if (py < 0) py = 0;
94
+ if (py > 96) py = 96;
95
+ }
77
96
  }
78
97
  }
@@ -47,26 +47,89 @@ static uint8_t collides(int8_t x, int8_t y) {
47
47
  return 0;
48
48
  }
49
49
 
50
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
51
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
52
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
53
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
54
+ * 4 directions, clears them, applies per-column gravity, and loops so
55
+ * cascades chain (score scales with chain depth). */
56
+ static uint8_t matched[ROWS][COLS];
57
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
58
+
59
+ static uint8_t mark_and_count(void) {
60
+ uint8_t r, c, d, len, k, cnt;
61
+ uint8_t col;
62
+ int8_t dr, dc;
63
+ int sr, sc;
64
+ cnt = 0;
65
+ for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
66
+ for (r = 0; r < ROWS; r++) {
67
+ for (c = 0; c < COLS; c++) {
68
+ col = grid[r][c];
69
+ if (col == 0) continue;
70
+ for (d = 0; d < 4; d++) {
71
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
72
+ sr = (int)r - dr; sc = (int)c - dc;
73
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
74
+ && grid[sr][sc] == col) continue; /* not the run's start */
75
+ len = 1;
76
+ sr = (int)r + dr; sc = (int)c + dc;
77
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
78
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
79
+ if (len >= 3) {
80
+ sr = r; sc = c;
81
+ for (k = 0; k < len; k++) {
82
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
83
+ sr += dr; sc += dc;
84
+ }
85
+ }
86
+ }
87
+ }
88
+ }
89
+ return cnt;
90
+ }
91
+
92
+ /* collapse each column so survivors rest on the floor (in place: walk
93
+ * from the bottom, copying gems down to a write cursor, then zero above) */
94
+ static void apply_gravity(void) {
95
+ uint8_t c;
96
+ int r, w;
97
+ for (c = 0; c < COLS; c++) {
98
+ w = ROWS - 1;
99
+ for (r = ROWS - 1; r >= 0; r--) {
100
+ if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
101
+ }
102
+ for (; w >= 0; w--) grid[w][c] = 0;
103
+ }
104
+ }
105
+
106
+ static void resolve_board(void) {
107
+ uint8_t n, r, c, chain;
108
+ unsigned int amt;
109
+ chain = 0;
110
+ while (1) {
111
+ n = mark_and_count();
112
+ if (n == 0) break;
113
+ chain++;
114
+ for (r = 0; r < ROWS; r++)
115
+ for (c = 0; c < COLS; c++)
116
+ if (matched[r][c]) grid[r][c] = 0;
117
+ amt = (unsigned int)n * 10u;
118
+ if (chain > 1) amt = amt * chain;
119
+ if (score < 65500u) score += amt;
120
+ sfx_tone(0, 60, 10); /* clear chime */
121
+ apply_gravity();
122
+ }
123
+ }
124
+
50
125
  static void lock_piece(void) {
51
- uint8_t i, c;
126
+ uint8_t i;
52
127
  int8_t r;
53
- uint8_t a, b, d;
54
128
  for (i = 0; i < 3; i++) {
55
129
  r = (int8_t)(piece_y + i);
56
130
  if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
57
131
  }
58
- for (i = 0; i < 3; i++) {
59
- r = (int8_t)(piece_y + i);
60
- if (r < 0 || r >= ROWS) continue;
61
- for (c = 0; c <= COLS - 3; c++) {
62
- a = grid[r][c]; b = grid[r][c+1]; d = grid[r][c+2];
63
- if (a != 0 && a == b && b == d) {
64
- grid[r][c] = 0; grid[r][c+1] = 0; grid[r][c+2] = 0;
65
- if (score < 65500u) score += 30;
66
- sfx_tone(0, 60, 10);
67
- }
68
- }
69
- }
132
+ resolve_board();
70
133
  }
71
134
 
72
135
  static uint8_t cell_color(uint8_t v) {
@@ -38,11 +38,23 @@ static void fire(void) {
38
38
  }
39
39
  }
40
40
 
41
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
42
+ * The old code derived the spawn column from spawn_timer, but the caller
43
+ * resets spawn_timer just before calling here, so it was CONSTANT and
44
+ * every enemy spawned in the same left column/lane. */
45
+ static uint8_t rng_state = 0xA5;
46
+ static uint8_t rand8(void) {
47
+ uint8_t lsb = (uint8_t)(rng_state & 1);
48
+ rng_state >>= 1;
49
+ if (lsb) rng_state ^= 0xB8;
50
+ return rng_state;
51
+ }
52
+
41
53
  static void spawn(void) {
42
54
  uint8_t i;
43
55
  for (i = 0; i < MAX_ENEMIES; i++) {
44
56
  if (!enemies[i].alive) {
45
- enemies[i].x = (uint8_t)(8 + ((spawn_timer * 37) & 0x7F));
57
+ enemies[i].x = (uint8_t)(8 + (rand8() % (160 - 16)));
46
58
  enemies[i].y = 0;
47
59
  enemies[i].alive = 1;
48
60
  return;