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
@@ -62,10 +62,13 @@ void main(void) {
62
62
  tgi_line(70, 40, 70, 60);
63
63
  tgi_line(90, 40, 90, 60);
64
64
 
65
- tgi_setcolor(COLOR_WHITE);
65
+ /* Playtest: "needs better contrast" — yellow paddles + white ball pop
66
+ * against the green court far better than white-on-lightgreen +
67
+ * yellow-on-green did. */
68
+ tgi_setcolor(COLOR_YELLOW);
66
69
  tgi_bar(PADDLE_X1, (unsigned)p1y, PADDLE_X1 + PADDLE_W - 1, (unsigned)(p1y + PADDLE_H - 1));
67
70
  tgi_bar(PADDLE_X2, (unsigned)p2y, PADDLE_X2 + PADDLE_W - 1, (unsigned)(p2y + PADDLE_H - 1));
68
- tgi_setcolor(COLOR_YELLOW);
71
+ tgi_setcolor(COLOR_WHITE);
69
72
  tgi_bar((unsigned)bx, (unsigned)by, (unsigned)(bx + BALL_SIZE - 1), (unsigned)(by + BALL_SIZE - 1));
70
73
  tgi_updatedisplay();
71
74
  sfx_update();
@@ -150,6 +150,8 @@ void main(void) {
150
150
 
151
151
  vsync();
152
152
 
153
+ msx_music_tick();
154
+
153
155
  ipx = (int16_t)(px >> 4);
154
156
  ipy = (int16_t)(py >> 4);
155
157
 
@@ -144,27 +144,89 @@ static uint8_t collides(int8_t col, int8_t row) {
144
144
  return 0;
145
145
  }
146
146
 
147
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
148
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
149
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
150
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
151
+ * 4 directions, clears them, applies per-column gravity, and loops so
152
+ * cascades chain (score scales with chain depth). */
153
+ static uint8_t matched[ROWS][COLS];
154
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
155
+
156
+ static uint8_t mark_and_count(void) {
157
+ uint8_t r, c, d, len, k, cnt;
158
+ uint8_t col;
159
+ int8_t dr, dc;
160
+ int sr, sc;
161
+ cnt = 0;
162
+ for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
163
+ for (r = 0; r < ROWS; r++) {
164
+ for (c = 0; c < COLS; c++) {
165
+ col = grid[r][c];
166
+ if (col == 0) continue;
167
+ for (d = 0; d < 4; d++) {
168
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
169
+ sr = (int)r - dr; sc = (int)c - dc;
170
+ if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
171
+ && grid[sr][sc] == col) continue; /* not the run's start */
172
+ len = 1;
173
+ sr = (int)r + dr; sc = (int)c + dc;
174
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
175
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
176
+ if (len >= 3) {
177
+ sr = r; sc = c;
178
+ for (k = 0; k < len; k++) {
179
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
180
+ sr += dr; sc += dc;
181
+ }
182
+ }
183
+ }
184
+ }
185
+ }
186
+ return cnt;
187
+ }
188
+
189
+ /* collapse each column so survivors rest on the floor (in place: walk
190
+ * from the bottom, copying gems down to a write cursor, then zero above) */
191
+ static void apply_gravity(void) {
192
+ uint8_t c;
193
+ int r, w;
194
+ for (c = 0; c < COLS; c++) {
195
+ w = ROWS - 1;
196
+ for (r = ROWS - 1; r >= 0; r--) {
197
+ if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
198
+ }
199
+ for (; w >= 0; w--) grid[w][c] = 0;
200
+ }
201
+ }
202
+
203
+ static void resolve_board(void) {
204
+ uint8_t n, r, c, chain;
205
+ unsigned int amt;
206
+ chain = 0;
207
+ while (1) {
208
+ n = mark_and_count();
209
+ if (n == 0) break;
210
+ chain++;
211
+ for (r = 0; r < ROWS; r++)
212
+ for (c = 0; c < COLS; c++)
213
+ if (matched[r][c]) grid[r][c] = 0;
214
+ amt = (unsigned int)n * 10u;
215
+ if (chain > 1) amt = amt * chain;
216
+ if (score < 999) score += n;
217
+ msx_psg_tone(0, 0x180, 13); blip = 8; /* clear chime */
218
+ apply_gravity();
219
+ }
220
+ }
221
+
147
222
  static void lock_piece(void) {
148
223
  uint8_t i;
149
- int8_t r, c;
150
- uint8_t a, b, d;
224
+ int8_t r;
151
225
  for (i = 0; i < 3; i++) {
152
226
  r = (int8_t)(piece_y + i);
153
227
  if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
154
228
  }
155
- for (i = 0; i < 3; i++) {
156
- r = (int8_t)(piece_y + i);
157
- if (r < 0 || r >= ROWS) continue;
158
- for (c = 0; c <= COLS - 3; c++) {
159
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
160
- if (a != 0 && a == b && b == d) {
161
- grid[r][c] = 0; grid[r][c + 1] = 0; grid[r][c + 2] = 0;
162
- if (score < 999) score += 3;
163
- msx_psg_tone(0, 0x180, 13);
164
- blip = 8;
165
- }
166
- }
167
- }
229
+ resolve_board();
168
230
  draw_grid();
169
231
  }
170
232
 
@@ -202,6 +264,7 @@ void main(void) {
202
264
 
203
265
  for (;;) {
204
266
  vsync();
267
+ msx_music_tick();
205
268
  draw_piece(1);
206
269
 
207
270
  dir = msx_read_joystick(1);
@@ -193,6 +193,7 @@ void main(void) {
193
193
 
194
194
  for (;;) {
195
195
  vsync();
196
+ msx_music_tick();
196
197
 
197
198
  /* push sprites */
198
199
  slot = 0;
@@ -224,6 +224,7 @@ void main(void) {
224
224
 
225
225
  for (;;) {
226
226
  vsync();
227
+ msx_music_tick();
227
228
 
228
229
  dir = msx_read_joystick(1);
229
230
  if (dir == STICK_CENTER) dir = msx_read_joystick(0);
@@ -43,7 +43,7 @@ static const uint8_t TILE_LINE[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
43
43
  static const uint8_t TILE_NET[8] = {0x18,0x18,0x00,0x00,0x18,0x18,0x00,0x00};
44
44
 
45
45
  /* colour bytes (hi fg, lo bg). 3=green(dark), 12=green(light), 15=white */
46
- #define COL_FIELD 0x21 /* dark green field on black */
46
+ #define COL_FIELD 0xC1 /* light-green-on-black field (was 0x21 medium green — muddy per the contrast playtest note) */
47
47
  #define COL_LINE 0xF1 /* white line on black */
48
48
  #define COL_NET 0xF2 /* white net dashes on dark-green field */
49
49
 
@@ -118,6 +118,7 @@ void main(void) {
118
118
 
119
119
  for (;;) {
120
120
  vsync();
121
+ msx_music_tick();
121
122
 
122
123
  /* push sprites: left paddle (3 cells), right paddle (3), ball */
123
124
  slot = 0;
@@ -125,7 +126,7 @@ void main(void) {
125
126
  msx_set_sprite(slot++, PADDLE_X1, (uint8_t)(p1y + i * 8), 0, COL_SPR);
126
127
  for (i = 0; i < PADDLE_H / 8; i++)
127
128
  msx_set_sprite(slot++, PADDLE_X2, (uint8_t)(p2y + i * 8), 0, COL_SPR);
128
- msx_set_sprite(slot++, (uint8_t)bx, (uint8_t)by, 0, COL_SPR);
129
+ msx_set_sprite(slot++, (uint8_t)bx, (uint8_t)by, 0, 11); /* light-yellow ball — distinct from the white paddles (contrast playtest note) */
129
130
 
130
131
  p1 = msx_read_joystick(1);
131
132
  p2 = msx_read_joystick(2);
@@ -82,7 +82,9 @@ static const uint8_t palette[32] = {
82
82
  * Keep it equal to the BG backdrop (sky blue) or the sky renders as
83
83
  * whatever colour-0 you put here, not the BG[0] above. (Sprite colour 0 is
84
84
  * transparent regardless, so this never affects how sprites draw.) */
85
- 0x21, 0x21, 0x32, 0x30, /* sp0: player — blue (colour 0 = backdrop mirror) */
85
+ 0x21, 0x16, 0x30, 0x27, /* sp0: player — RED + white/orange trim. (Was light
86
+ * blues — nearly invisible against the sky-blue
87
+ * backdrop. Colour 0 stays the backdrop mirror.) */
86
88
  0x0F, 0x18, 0x28, 0x38, /* sp1: platforms — green */
87
89
  0x0F, 0x16, 0x06, 0x36,
88
90
  0x0F, 0x2A, 0x1A, 0x0A,
@@ -113,13 +115,18 @@ static uint8_t on_ground = 0;
113
115
 
114
116
  #define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
115
117
  #define JUMP_VEL_Q44 (-40) /* initial vy → peak ~5 tile jump */
116
- #define MOVE_SPEED 1 /* 1 px / frame */
118
+ #define MOVE_SPEED 2 /* px/frame — 1 read as 'moves slowly' in playtesting */
117
119
 
118
120
  /* AABB: player rect vs platform top edge (treat platform as 8 px tall). */
119
121
  static uint8_t landed_on(uint8_t pl_idx, uint8_t player_y) {
120
122
  const Platform *p = &platforms[pl_idx];
121
123
  uint8_t feet_y = player_y + 7;
122
- if (feet_y < p->y || feet_y > p->y + 4) return 0; /* not at top edge */
124
+ /* Window starts ONE PIXEL above the top edge: the standing snap puts the
125
+ * feet at p->y - 1, and gravity's sub-pixel trickle doesn't move the
126
+ * integer Y every frame — with the old `feet_y < p->y` cutoff the player
127
+ * "stood" with on_ground=0 most frames, so A-press jumps only registered
128
+ * on lucky frames and the idle/jump sprite flickered every frame. */
129
+ if (feet_y + 1 < p->y || feet_y > p->y + 4) return 0; /* not at top edge */
123
130
  if (px + 7 < p->x) return 0;
124
131
  if (px > p->x + (p->w << 3) - 1) return 0;
125
132
  return 1;
@@ -190,6 +197,7 @@ void main(void) {
190
197
  ppu_wait_nmi();
191
198
 
192
199
  /* ── Input ──────────────────────────────────────────────── */
200
+ sound_music_tick();
193
201
  pad = pad_poll(0);
194
202
  if ((pad & PAD_LEFT) && px > 8) px -= MOVE_SPEED;
195
203
  if ((pad & PAD_RIGHT) && px < 240) px += MOVE_SPEED;
@@ -132,8 +132,86 @@ static void spawn_piece(void) {
132
132
  }
133
133
 
134
134
  /* Lock current piece into the grid + check for match-3 horizontals. */
135
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
136
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
137
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
138
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
139
+ * 4 directions, clears them, applies per-column gravity, and loops so
140
+ * cascades chain (score scales with chain depth). */
141
+ /* matched[] lives at $0500 — OUTSIDE the linker's RAM area ($0300-$04FF,
142
+ * which grid+runtime statics nearly fill; 72 more BSS bytes overflow it).
143
+ * $0500-$07FF is real, unused NES work RAM (hw stack is $0100, shadow OAM
144
+ * $0200), so an absolute pointer there is free. */
145
+ #define matched ((uint8_t (*)[GRID_W])0x0500)
146
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
147
+
148
+ static uint8_t mark_and_count(void) {
149
+ uint8_t r, c, d, len, k, cnt;
150
+ uint8_t col;
151
+ int8_t dr, dc;
152
+ int sr, sc;
153
+ cnt = 0;
154
+ for (r = 0; r < GRID_H; r++) for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
155
+ for (r = 0; r < GRID_H; r++) {
156
+ for (c = 0; c < GRID_W; c++) {
157
+ col = grid[r][c];
158
+ if (col == EMPTY) continue;
159
+ for (d = 0; d < 4; d++) {
160
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
161
+ sr = (int)r - dr; sc = (int)c - dc;
162
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
163
+ && grid[sr][sc] == col) continue; /* not the run's start */
164
+ len = 1;
165
+ sr = (int)r + dr; sc = (int)c + dc;
166
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
167
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
168
+ if (len >= 3) {
169
+ sr = r; sc = c;
170
+ for (k = 0; k < len; k++) {
171
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
172
+ sr += dr; sc += dc;
173
+ }
174
+ }
175
+ }
176
+ }
177
+ }
178
+ return cnt;
179
+ }
180
+
181
+ /* collapse each column so survivors rest on the floor (in place: walk
182
+ * from the bottom, copying gems down to a write cursor, then zero above) */
183
+ static void apply_gravity(void) {
184
+ uint8_t c;
185
+ int r, w;
186
+ for (c = 0; c < GRID_W; c++) {
187
+ w = GRID_H - 1;
188
+ for (r = GRID_H - 1; r >= 0; r--) {
189
+ if (grid[r][c] != EMPTY) { grid[w][c] = grid[r][c]; w--; }
190
+ }
191
+ for (; w >= 0; w--) grid[w][c] = EMPTY;
192
+ }
193
+ }
194
+
195
+ static void resolve_board(void) {
196
+ uint8_t n, r, c, chain;
197
+ unsigned int amt;
198
+ chain = 0;
199
+ while (1) {
200
+ n = mark_and_count();
201
+ if (n == 0) break;
202
+ chain++;
203
+ for (r = 0; r < GRID_H; r++)
204
+ for (c = 0; c < GRID_W; c++)
205
+ if (matched[r][c]) grid[r][c] = EMPTY;
206
+ amt = (unsigned int)n * 10u;
207
+ if (chain > 1) amt = amt * chain;
208
+ (void)amt;
209
+ sound_play_tone(0, 0xC0, 6, 4); /* clear chime */
210
+ apply_gravity();
211
+ }
212
+ }
213
+
135
214
  static void lock_piece(void) {
136
- uint8_t r, c, run, run_col;
137
215
  int8_t i;
138
216
  /* Drop the 3 cells into the grid. */
139
217
  for (i = 0; i < 3; i++) {
@@ -142,26 +220,7 @@ static void lock_piece(void) {
142
220
  grid[y][piece_x] = piece_col[i];
143
221
  }
144
222
  }
145
- /* Scan each row for 3-in-a-row same color → clear. */
146
- for (r = 0; r < GRID_H; r++) {
147
- run = 1;
148
- run_col = grid[r][0];
149
- for (c = 1; c < GRID_W; c++) {
150
- if (grid[r][c] == run_col && run_col != EMPTY) {
151
- ++run;
152
- if (run >= 3) {
153
- /* Found a triple ending at c — clear back. */
154
- grid[r][c] = EMPTY;
155
- grid[r][c - 1] = EMPTY;
156
- grid[r][c - 2] = EMPTY;
157
- sound_play_tone(0, 0x100 - (c << 4), 6, 4);
158
- }
159
- } else {
160
- run = 1;
161
- run_col = grid[r][c];
162
- }
163
- }
164
- }
223
+ resolve_board();
165
224
  }
166
225
 
167
226
  /* Can the piece occupy (x, y..y+2) given the current grid? */
@@ -258,6 +317,7 @@ void main(void) {
258
317
  ppu_wait_nmi();
259
318
 
260
319
  /* ── Input ──────────────────────────────────────────────── */
320
+ sound_music_tick();
261
321
  pad = pad_poll(0);
262
322
  if ((pad & PAD_LEFT) && !(prev_pad & PAD_LEFT) && can_place(piece_x - 1, piece_y)) --piece_x;
263
323
  if ((pad & PAD_RIGHT) && !(prev_pad & PAD_RIGHT) && can_place(piece_x + 1, piece_y)) ++piece_x;
@@ -176,11 +176,23 @@ static void reset_run(void) {
176
176
  game_over_timer = 0;
177
177
  }
178
178
 
179
+ /* Galois LFSR (taps $B8), period 255 -- real per-spawn randomness.
180
+ * The old code derived the spawn column from spawn_timer, but the caller
181
+ * resets spawn_timer just before calling here, so it was CONSTANT and
182
+ * every enemy spawned in the same left column/lane. */
183
+ static uint8_t rng_state = 0xA5;
184
+ static uint8_t rand8(void) {
185
+ uint8_t lsb = (uint8_t)(rng_state & 1);
186
+ rng_state >>= 1;
187
+ if (lsb) rng_state ^= 0xB8;
188
+ return rng_state;
189
+ }
190
+
179
191
  static void spawn_obstacle(void) {
180
192
  uint8_t i;
181
193
  for (i = 0; i < MAX_OBSTACLES; i++) {
182
194
  if (!obstacles[i].alive) {
183
- obstacles[i].x = lane_x[(spawn_timer * 13) % 3];
195
+ obstacles[i].x = lane_x[rand8() % 3];
184
196
  obstacles[i].y = 0;
185
197
  obstacles[i].alive = 1;
186
198
  return;
@@ -240,6 +252,8 @@ void main(void) {
240
252
 
241
253
  ppu_wait_nmi();
242
254
 
255
+ sound_music_tick();
256
+
243
257
  p1 = pad_poll(0);
244
258
 
245
259
  if (game_over_timer > 0) {
@@ -215,6 +215,7 @@ void main(void) {
215
215
  ppu_wait_nmi();
216
216
 
217
217
  /* ── Input ───────────────────────────────────────────────── */
218
+ sound_music_tick();
218
219
  pad = pad_poll(0);
219
220
  if ((pad & PAD_LEFT) && ship_x > 8) --ship_x;
220
221
  if ((pad & PAD_RIGHT) && ship_x < 240) ++ship_x;
@@ -181,6 +181,7 @@ void main(void) {
181
181
  ppu_wait_nmi();
182
182
 
183
183
  /* ── Input ────────────────────────────────────────────────── */
184
+ sound_music_tick();
184
185
  p1 = pad_poll(0);
185
186
  p2 = pad_poll(1);
186
187
 
@@ -167,7 +167,7 @@ static u8 on_platform(int16_t ipx, int16_t ipy) {
167
167
 
168
168
  void main(void) {
169
169
  const int16_t GRAVITY = 10;
170
- const int16_t MOVE = 22;
170
+ const int16_t MOVE = 34; /* was 22 — playtest: 'slow overall' */
171
171
  const int16_t JUMP = -200;
172
172
  const int16_t MAXFALL = 300;
173
173
 
@@ -210,6 +210,8 @@ void main(void) {
210
210
  const Rect *p;
211
211
 
212
212
  waitvsync();
213
+
214
+ psg_music_tick();
213
215
  pad = pce_joy_read();
214
216
 
215
217
  ipx = px >> 4;
@@ -198,20 +198,85 @@ static void draw_piece(u8 clear) {
198
198
  }
199
199
  }
200
200
 
201
- static void clear_triples(void) {
202
- u8 r;
203
- int8_t c;
204
- u8 a, b, d;
205
- for (r = 0; r < ROWS; ++r) {
206
- for (c = 0; c <= COLS - 3; ++c) {
207
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
208
- if (a != 0 && a == b && b == d) {
209
- grid[r][c] = 0; grid[r][c + 1] = 0; grid[r][c + 2] = 0;
210
- if (score < 9999) score += 30;
211
- psg_tone(0, 0x180, 24);
212
- }
201
+ /* ── match / clear / gravity core (ported from the GBC reference puzzle).
202
+ * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
203
+ * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
204
+ * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
205
+ * 4 directions, clears them, applies per-column gravity, and loops so
206
+ * cascades chain (score scales with chain depth). */
207
+ static u8 matched[ROWS][COLS];
208
+ /* H + V only on PCE the stock cc65 pce.cfg boot bank is 8KB and the
209
+ * two diagonal passes don't fit; add them back if you free up ROM. */
210
+ static const int8_t DIRS4[2][2] = { {0,1}, {1,0} };
211
+
212
+ static u8 mark_and_count(void) {
213
+ u8 r, c, d, len, k, cnt;
214
+ u8 col;
215
+ int8_t dr, dc;
216
+ int sr, sc;
217
+ cnt = 0;
218
+ for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
219
+ for (r = 0; r < ROWS; r++) {
220
+ for (c = 0; c < COLS; c++) {
221
+ col = grid[r][c];
222
+ if (col == 0) continue;
223
+ for (d = 0; d < 2; d++) {
224
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
225
+ /* (no run-start check: a mid-run scan only re-marks already-
226
+ * marked cells, so skipping the predecessor test is pure
227
+ * code-size savings on the 8KB PCE boot bank) */
228
+ len = 1;
229
+ sr = (int)r + dr; sc = (int)c + dc;
230
+ while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
231
+ && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
232
+ if (len >= 3) {
233
+ sr = r; sc = c;
234
+ for (k = 0; k < len; k++) {
235
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
236
+ sr += dr; sc += dc;
237
+ }
213
238
  }
239
+ }
240
+ }
241
+ }
242
+ return cnt;
243
+ }
244
+
245
+ /* collapse each column so survivors rest on the floor (in place: walk
246
+ * from the bottom, copying gems down to a write cursor, then zero above) */
247
+ static void apply_gravity(void) {
248
+ u8 c;
249
+ int r, w;
250
+ for (c = 0; c < COLS; c++) {
251
+ w = ROWS - 1;
252
+ for (r = ROWS - 1; r >= 0; r--) {
253
+ if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
214
254
  }
255
+ for (; w >= 0; w--) grid[w][c] = 0;
256
+ }
257
+ }
258
+
259
+ static void resolve_board(void) {
260
+ u8 n, r, c, chain;
261
+ unsigned int amt;
262
+ chain = 0;
263
+ while (1) {
264
+ n = mark_and_count();
265
+ if (n == 0) break;
266
+ chain++;
267
+ for (r = 0; r < ROWS; r++)
268
+ for (c = 0; c < COLS; c++)
269
+ if (matched[r][c]) grid[r][c] = 0;
270
+ amt = (unsigned int)n * 10u;
271
+ if (chain > 1) amt = amt * chain;
272
+ if (score < 9999) score += amt;
273
+ psg_tone(0, 0x180, 24); /* clear chime */
274
+ apply_gravity();
275
+ }
276
+ }
277
+
278
+ static void clear_triples(void) {
279
+ resolve_board();
215
280
  }
216
281
 
217
282
  static void lock_piece(void) {
@@ -262,6 +327,7 @@ void main(void) {
262
327
  for (;;) {
263
328
  u8 fall_rate;
264
329
  waitvsync();
330
+ psg_music_tick();
265
331
 
266
332
  draw_piece(1); /* erase old piece footprint */
267
333
 
@@ -244,6 +244,7 @@ void main(void) {
244
244
  u8 slot;
245
245
  int16_t step;
246
246
  waitvsync();
247
+ psg_music_tick();
247
248
 
248
249
  /* stage sprites: player + obstacles */
249
250
  slot = 0;
@@ -225,7 +225,7 @@ static void fire(void) {
225
225
  bullets[i].x = player.x;
226
226
  bullets[i].y = (u16)(player.y - 10);
227
227
  bullets[i].alive = 1;
228
- psg_tone(2, 0x180, 26);
228
+ psg_tone(2, 0x180, 31); /* max vol — playtest said too quiet */
229
229
  sfx_timer = 4;
230
230
  return;
231
231
  }
@@ -280,11 +280,12 @@ void main(void) {
280
280
 
281
281
  for (;;) {
282
282
  waitvsync();
283
+ psg_music_tick();
283
284
  pad = pce_joy_read();
284
285
 
285
286
  /* move ship */
286
- if ((pad & PCE_JOY_LEFT) && player.x > 2) player.x -= 3;
287
- if ((pad & PCE_JOY_RIGHT) && player.x < 238) player.x += 3;
287
+ if ((pad & PCE_JOY_LEFT) && player.x > 2) player.x -= 4; /* playtest: 'slow overall' */
288
+ if ((pad & PCE_JOY_RIGHT) && player.x < 238) player.x += 4;
288
289
  if ((pad & PCE_JOY_UP) && player.y > 8) player.y -= 3;
289
290
  if ((pad & PCE_JOY_DOWN) && player.y < 208) player.y += 3;
290
291
  if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I)) fire();
@@ -318,7 +319,7 @@ void main(void) {
318
319
  enemies[j].alive = 0;
319
320
  if (score < 9999) score += 10;
320
321
  draw_score();
321
- psg_tone(3, 0x040, 28);
322
+ psg_tone(3, 0x040, 31);
322
323
  sfx_timer = 6;
323
324
  break;
324
325
  }
@@ -159,7 +159,7 @@ static void draw_scores(void) {
159
159
  static void serve_ball(u8 to_left) {
160
160
  bx = 120; by = 110;
161
161
  bdx = to_left ? -2 : 2;
162
- bdy = ((score_p1 + score_p2) & 1) ? -1 : 1;
162
+ bdy = ((score_p1 + score_p2) & 1) ? -2 : 2; /* was ±1 — rally felt slow */
163
163
  serve_timer = 40;
164
164
  }
165
165
 
@@ -199,6 +199,7 @@ void main(void) {
199
199
  u8 slot;
200
200
  int16_t target;
201
201
  waitvsync();
202
+ psg_music_tick();
202
203
 
203
204
  /* stage sprites: P1 paddle (3 segs), P2 paddle (3 segs), ball */
204
205
  slot = 0;
@@ -212,8 +213,8 @@ void main(void) {
212
213
  pad = pce_joy_read();
213
214
 
214
215
  /* P1 control */
215
- if ((pad & PCE_JOY_UP) && p1y > COURT_TOP) p1y -= 3;
216
- if ((pad & PCE_JOY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 3;
216
+ if ((pad & PCE_JOY_UP) && p1y > COURT_TOP) p1y -= 4; /* playtest: 'slow overall' */
217
+ if ((pad & PCE_JOY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 4;
217
218
 
218
219
  /* P2 chase-AI */
219
220
  target = (int16_t)(by - PADDLE_H / 2 + BALL_SIZE / 2);
@@ -14,6 +14,7 @@
14
14
  */
15
15
  #include "sms_hw.h"
16
16
  #include "sms_sfx.h"
17
+ #include "sms_music.h"
17
18
  #include <stdint.h>
18
19
 
19
20
  extern void sms_vdp_init(void);
@@ -138,6 +139,8 @@ void main(void) {
138
139
 
139
140
  sms_sprite_init();
140
141
  sfx_init();
142
+ music_init();
143
+ music_play(0); /* continuous background music ("no sound" was the playtest verdict) */
141
144
  sms_sprite_set(0, (uint8_t)(px >> 4), (uint8_t)(py >> 4), 0);
142
145
  sms_sat_upload();
143
146
  sms_vdp_display_on();
@@ -151,6 +154,7 @@ void main(void) {
151
154
  const Rect *p;
152
155
  sms_vblank_wait();
153
156
  sfx_update();
157
+ music_update();
154
158
 
155
159
  ipx = px >> 4;
156
160
  ipy = py >> 4;