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.
- package/AGENTS.md +5 -3
- package/CHANGELOG.md +309 -0
- package/README.md +1 -1
- package/examples/README.md +1 -1
- package/examples/atari2600/templates/platformer.asm +18 -9
- package/examples/atari2600/templates/racing.asm +25 -4
- package/examples/atari2600/templates/shmup.asm +30 -5
- package/examples/atari2600/templates/sports.asm +41 -9
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +12 -8
- package/examples/atari7800/templates/puzzle.c +7 -4
- package/examples/atari7800/templates/racing.c +5 -2
- package/examples/atari7800/templates/shmup.c +8 -4
- package/examples/atari7800/templates/sports.c +6 -3
- package/examples/c64/templates/platformer.c +28 -24
- package/examples/c64/templates/puzzle.c +77 -16
- package/examples/c64/templates/racing.c +9 -0
- package/examples/c64/templates/shmup.c +13 -1
- package/examples/c64/templates/sports.c +9 -4
- package/examples/gb/templates/platformer.c +6 -2
- package/examples/gb/templates/puzzle.c +279 -101
- package/examples/gb/templates/racing.c +13 -1
- package/examples/gb/templates/shmup.c +13 -1
- package/examples/gb/templates/sports.c +9 -3
- package/examples/gba/templates/platformer.c +7 -13
- package/examples/gba/templates/puzzle.c +93 -15
- package/examples/gba/templates/racing.c +13 -1
- package/examples/gba/templates/shmup.c +13 -1
- package/examples/gba/templates/sports.c +17 -5
- package/examples/gbc/templates/platformer.c +6 -2
- package/examples/gbc/templates/puzzle.c +878 -178
- package/examples/gbc/templates/racing.c +13 -1
- package/examples/gbc/templates/shmup.c +13 -1
- package/examples/gbc/templates/sports.c +9 -3
- package/examples/genesis/templates/puzzle.c +76 -15
- package/examples/genesis/templates/racing.c +13 -1
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/gg/templates/platformer.c +4 -0
- package/examples/gg/templates/puzzle.c +80 -14
- package/examples/gg/templates/racing.c +17 -1
- package/examples/gg/templates/shmup.c +17 -1
- package/examples/gg/templates/sports.c +4 -0
- package/examples/lynx/templates/platformer.c +25 -6
- package/examples/lynx/templates/puzzle.c +77 -14
- package/examples/lynx/templates/shmup.c +13 -1
- package/examples/lynx/templates/sports.c +5 -2
- package/examples/msx/platformer/main.c +2 -0
- package/examples/msx/puzzle/main.c +78 -15
- package/examples/msx/racing/main.c +1 -0
- package/examples/msx/shmup/main.c +1 -0
- package/examples/msx/sports/main.c +3 -2
- package/examples/nes/templates/platformer.c +11 -3
- package/examples/nes/templates/puzzle.c +81 -21
- package/examples/nes/templates/racing.c +15 -1
- package/examples/nes/templates/shmup.c +1 -0
- package/examples/nes/templates/sports.c +1 -0
- package/examples/pce/platformer/main.c +3 -1
- package/examples/pce/puzzle/main.c +78 -12
- package/examples/pce/racing/main.c +1 -0
- package/examples/pce/shmup/main.c +5 -4
- package/examples/pce/sports/main.c +4 -3
- package/examples/sms/templates/platformer.c +4 -0
- package/examples/sms/templates/puzzle.c +80 -14
- package/examples/sms/templates/racing.c +17 -1
- package/examples/sms/templates/shmup.c +17 -1
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +4 -0
- package/examples/snes/templates/platformer.c +32 -15
- package/examples/snes/templates/puzzle.c +84 -16
- package/examples/snes/templates/racing.c +20 -1
- package/examples/snes/templates/shmup.c +20 -2
- package/examples/snes/templates/sports.c +7 -0
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +245 -10
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +11 -4
- package/src/mcp/tools/index.js +15 -1
- package/src/mcp/tools/input.js +26 -3
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/project.js +35 -9
- package/src/mcp/tools/toolchain.js +43 -10
- package/src/mcp/tools/watch-memory.js +141 -24
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +27 -6
- package/src/platforms/gb/MENTAL_MODEL.md +16 -1
- package/src/platforms/gb/TROUBLESHOOTING.md +42 -0
- package/src/platforms/gb/lib/c/patch-header.js +7 -4
- package/src/platforms/gbc/MENTAL_MODEL.md +12 -0
- package/src/platforms/gbc/TROUBLESHOOTING.md +21 -0
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/patch-header.js +7 -4
- package/src/platforms/genesis/MENTAL_MODEL.md +40 -6
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/TROUBLESHOOTING.md +13 -17
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +6 -0
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +2 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +45 -0
- package/src/platforms/nes/MENTAL_MODEL.md +10 -3
- package/src/platforms/nes/lib/c/nes_runtime.c +41 -0
- package/src/platforms/nes/lib/c/nes_runtime.h +2 -0
- package/src/platforms/pce/MENTAL_MODEL.md +9 -0
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +2 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/sms/MENTAL_MODEL.md +5 -0
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +5 -0
- package/src/playtest/playtest.js +73 -3
- 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[(
|
|
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 = (
|
|
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
|
|
138
|
-
|
|
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
|
-
|
|
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[(
|
|
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 = ((
|
|
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
|
-
|
|
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[(
|
|
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 + ((
|
|
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
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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
|
|
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
|
-
|
|
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 + ((
|
|
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;
|