romdevtools 0.28.0 → 0.30.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 +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- 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 +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,333 +1,766 @@
|
|
|
1
|
-
/* ── puzzle.c — Genesis
|
|
1
|
+
/* ── puzzle.c — Genesis falling-gem versus puzzle (complete example game) ─────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* SHARD SIEGE — a COMPLETE, working game: title screen, 1P MARATHON mode
|
|
4
|
+
* (levels speed the fall as you clear) and 2P SIMULTANEOUS VERSUS mode —
|
|
5
|
+
* two 6x12 wells side by side, P1 on controller 1, P2 on controller 2,
|
|
6
|
+
* both falling at once, where every cascade chain you score lays SIEGE to
|
|
7
|
+
* the other well: garbage rows rise from the bottom of your rival's board.
|
|
8
|
+
* Score + persistent hi-score (cartridge SRAM), music + SFX.
|
|
7
9
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
10
|
+
* The game: a falling-trio match-3. A vertical trio of gems drops into a
|
|
11
|
+
* well; LEFT/RIGHT move it, A/B cycle its three colours, DOWN soft-drops,
|
|
12
|
+
* C hard-drops. When it lands, any straight run of 3+ same-coloured gems
|
|
13
|
+
* (horizontal, vertical, or diagonal) clears; survivors fall and cascades
|
|
14
|
+
* chain for multiplied score. First stack to reach the rim loses.
|
|
12
15
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
16
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
17
|
+
* very different one. The markers tell you what's what:
|
|
18
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented Genesis footgun;
|
|
19
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
20
|
+
* GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
|
|
15
21
|
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
22
|
+
* What depends on what:
|
|
23
|
+
* genesis_sfx.{h,c} — PSG sound wrapper (tones + noise + a background
|
|
24
|
+
* melody loop). For full FM music see the xgm2_demo template — PSG keeps
|
|
25
|
+
* this a single-file game.
|
|
26
|
+
* rom_header.c (SGDK) — the Sega header at $100. Its 'RA' block at $1B0
|
|
27
|
+
* DECLARES the cartridge SRAM that hiscore_load/save below depend on
|
|
28
|
+
* (see the SRAM idiom). The build assembles it automatically.
|
|
29
|
+
*
|
|
30
|
+
* Frame budget (NTSC, 60 fps) — and a TEACHING POINT vs the NES version of
|
|
31
|
+
* this game (examples/nes/templates/puzzle.c): on the NES, board repaints
|
|
32
|
+
* squeeze through a ~16-entry vblank queue, so a full-board repaint is
|
|
33
|
+
* BUDGETED across 12 frames of dirty-row bitmask tricks. The Genesis has
|
|
34
|
+
* no such famine: each dirty well is mirrored in a RAM buffer and queued
|
|
35
|
+
* as ONE DMA rect (576 bytes); the H40 vblank DMA window moves ~7 KB, so
|
|
36
|
+
* BOTH wells + 6 SAT entries + HUD land in a single vblank with most of
|
|
37
|
+
* the budget unspent. Same genre, two bandwidth worlds — fork accordingly.
|
|
19
38
|
*/
|
|
20
39
|
|
|
21
40
|
#include <genesis.h>
|
|
22
41
|
#include "genesis_sfx.h"
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#define
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
/*
|
|
44
|
-
*
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
43
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
44
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
45
|
+
#define GAME_TITLE "SHARD SIEGE"
|
|
46
|
+
|
|
47
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
48
|
+
* CONTROLLER MAPPING — two layers, both bite:
|
|
49
|
+
*
|
|
50
|
+
* On the pad: SGDK's JOY_readJoypad(JOY_1/JOY_2) returns BUTTON_A/B/C/
|
|
51
|
+
* START/UP/DOWN/LEFT/RIGHT as a bitmask. Here A/B cycle the trio's
|
|
52
|
+
* colours, C hard-drops (thumbs rest on C — give it the decisive action).
|
|
53
|
+
*
|
|
54
|
+
* Driving this game HEADLESSLY through an emulator (libretro/gpgx): the
|
|
55
|
+
* core maps Genesis A/B/C onto libretro Y/B/A. So setInput({y:true})
|
|
56
|
+
* presses GENESIS A (rotate/1P-start here), setInput({b:true}) presses
|
|
57
|
+
* GENESIS B (rotate/2P-select), and setInput({a:true}) presses GENESIS C
|
|
58
|
+
* (hard drop) — NOT Genesis A. Getting this wrong looks like "the game
|
|
59
|
+
* ignores input". START is start.
|
|
60
|
+
*/
|
|
61
|
+
|
|
62
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
63
|
+
* Board geometry. Cells are 16x16 px (2x2 tiles) — the Genesis 320x224
|
|
64
|
+
* screen has room to spare; chunky gems read better than 8-px ones.
|
|
65
|
+
* Tile rows 0-1 sit under the WINDOW HUD; well frames at row 2 and 27. */
|
|
66
|
+
#define GRID_W 6
|
|
67
|
+
#define GRID_H 12
|
|
68
|
+
#define WELL_TY 3 /* top TILE row of the well interior */
|
|
69
|
+
#define WELL_1P_TX 14 /* 1P: single centered well (tiles 14-25) */
|
|
70
|
+
#define WELL_VS_P1 3 /* 2P: P1 interior tiles 3-14 ... */
|
|
71
|
+
#define WELL_VS_P2 25 /* P2 interior tiles 25-36 (split board)*/
|
|
72
|
+
#define HUD_ROWS 2 /* window rows reserved for the HUD */
|
|
73
|
+
|
|
74
|
+
#define EMPTY 0 /* cell colours 1..3 = ruby/emerald/sapphire */
|
|
75
|
+
|
|
76
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
77
|
+
* Tile art. Genesis tiles are 4bpp: each u32 row = 8 pixels, one hex
|
|
78
|
+
* nibble per pixel = a colour index into the tile's palette line (0 =
|
|
79
|
+
* transparent). Everything game-side (board, frames, gems, trio sprites)
|
|
80
|
+
* lives on PAL1; the backdrop plane uses PAL2; PAL0 keeps the SGDK font.
|
|
81
|
+
*
|
|
82
|
+
* KEY TRICK: the three gem colours are the SAME 16x16 shape. The four
|
|
83
|
+
* quarter tiles are drawn ONCE with fill nibble 1 (rim 5 / glint 4 shared),
|
|
84
|
+
* and the other two colours are GENERATED at boot by remapping nibble 1 ->
|
|
85
|
+
* 2 / 3 into a RAM buffer before upload — one piece of art, three tiles. */
|
|
86
|
+
#define T_FRAME (TILE_USER_INDEX + 0) /* well border */
|
|
87
|
+
#define T_CELL (TILE_USER_INDEX + 1) /* empty (recessed) cell quarter */
|
|
88
|
+
#define T_BACK (TILE_USER_INDEX + 2) /* plane B cabinet backdrop */
|
|
89
|
+
#define T_BAND (TILE_USER_INDEX + 3) /* plane B flat band behind HUD */
|
|
90
|
+
#define T_GEM (TILE_USER_INDEX + 4) /* 12 tiles: 3 colours x 4 quarters */
|
|
91
|
+
|
|
92
|
+
static const u32 tile_frame[8] = {
|
|
93
|
+
0x77777777, 0x76666667, 0x76666667, 0x76666667,
|
|
94
|
+
0x76666667, 0x76666667, 0x76666667, 0x77777777,
|
|
48
95
|
};
|
|
49
|
-
static const u32
|
|
50
|
-
|
|
51
|
-
|
|
96
|
+
static const u32 tile_cell[8] = { /* near-black well + faint speck */
|
|
97
|
+
0x99999999, 0x99999999, 0x99999999, 0x99989999,
|
|
98
|
+
0x99999999, 0x99999999, 0x99999999, 0x99999999,
|
|
52
99
|
};
|
|
53
|
-
static const u32
|
|
54
|
-
|
|
55
|
-
|
|
100
|
+
static const u32 tile_back[8] = { /* framed cabinet block */
|
|
101
|
+
0x11111111, 0x12222221, 0x12222221, 0x12222221,
|
|
102
|
+
0x12222221, 0x12222221, 0x12222221, 0x11111111,
|
|
56
103
|
};
|
|
57
|
-
static const u32
|
|
104
|
+
static const u32 tile_band[8] = {
|
|
58
105
|
0x33333333, 0x33333333, 0x33333333, 0x33333333,
|
|
59
106
|
0x33333333, 0x33333333, 0x33333333, 0x33333333,
|
|
60
107
|
};
|
|
108
|
+
/* Gem quarters in SPRITE TILE ORDER — Genesis 2x2 sprites take their four
|
|
109
|
+
* tiles COLUMN-MAJOR: base+0 top-left, +1 bottom-left, +2 top-right,
|
|
110
|
+
* +3 bottom-right. The tilemap placement below indexes the same way. */
|
|
111
|
+
static const u32 gem_quarter[4][8] = {
|
|
112
|
+
{ 0x00005555, 0x00551111, 0x05114411, 0x05144111, /* top-left */
|
|
113
|
+
0x51141111, 0x51111111, 0x51111111, 0x51111111 },
|
|
114
|
+
{ 0x51111111, 0x51111111, 0x51111111, 0x51111111, /* bottom-left */
|
|
115
|
+
0x05111111, 0x05111111, 0x00551111, 0x00005555 },
|
|
116
|
+
{ 0x55550000, 0x11115500, 0x11111150, 0x11111150, /* top-right */
|
|
117
|
+
0x11111115, 0x11111115, 0x11111115, 0x11111115 },
|
|
118
|
+
{ 0x11111115, 0x11111115, 0x11111115, 0x11111115, /* bottom-right */
|
|
119
|
+
0x11111150, 0x11111150, 0x11115500, 0x55550000 },
|
|
120
|
+
};
|
|
121
|
+
static u32 gem_ram[3][4][8]; /* colours 1..3, built at boot */
|
|
122
|
+
|
|
123
|
+
static void build_gem_tiles(void) {
|
|
124
|
+
u16 k, t, r, i;
|
|
125
|
+
for (k = 0; k < 3; k++)
|
|
126
|
+
for (t = 0; t < 4; t++)
|
|
127
|
+
for (r = 0; r < 8; r++) {
|
|
128
|
+
u32 v = gem_quarter[t][r], out = 0;
|
|
129
|
+
for (i = 0; i < 8; i++) {
|
|
130
|
+
u32 nib = (v >> (i * 4)) & 0xF;
|
|
131
|
+
if (nib == 1) nib = 1 + k; /* fill -> this colour */
|
|
132
|
+
out |= nib << (i * 4);
|
|
133
|
+
}
|
|
134
|
+
gem_ram[k][t][r] = out;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/* ── GAME LOGIC (clay — reshape freely) ── game state.
|
|
139
|
+
* Boards are PLAIN STATIC ARRAYS — the Genesis has 64 KB of work RAM, so
|
|
140
|
+
* none of the NES version's absolute-address scratch-page gymnastics.
|
|
141
|
+
* The hot ones are deliberately NON-static: they then appear in the GNU-ld
|
|
142
|
+
* map (build symbols), so a headless agent can resolve them by name and
|
|
143
|
+
* read/poke live state (symbols -> memory) — same trick as the
|
|
144
|
+
* two_plane_parallax template's g_player_x. */
|
|
145
|
+
u8 grid[2][GRID_H][GRID_W]; /* the two wells (P2's unused in 1P) */
|
|
146
|
+
s16 piece_x[2]; /* falling trio: column 0..5 */
|
|
147
|
+
s16 piece_y[2]; /* row of its TOP cell (<0 above rim) */
|
|
148
|
+
u8 piece_col[2][3]; /* trio colours, top to bottom */
|
|
149
|
+
u16 score[2];
|
|
150
|
+
u16 hiscore;
|
|
151
|
+
u8 level; /* 1P: 1..9, speeds up the fall */
|
|
152
|
+
u8 state; /* ST_TITLE / ST_PLAY / ST_OVER */
|
|
153
|
+
u8 two_player;
|
|
154
|
+
|
|
155
|
+
static u8 matched[GRID_H][GRID_W];
|
|
156
|
+
static u8 well_tx[2]; /* left interior TILE column per well */
|
|
157
|
+
static u8 fall_t[2]; /* frames until next gravity step */
|
|
158
|
+
static u16 prev_pad[2]; /* for edge-triggered input */
|
|
159
|
+
static u16 cleared_total; /* 1P: gems cleared, drives the level */
|
|
160
|
+
static u8 board_dirty[2]; /* well needs a repaint this frame */
|
|
161
|
+
static u16 rng = 0xACE1;
|
|
162
|
+
|
|
163
|
+
#define ST_TITLE 0
|
|
164
|
+
#define ST_PLAY 1
|
|
165
|
+
#define ST_OVER 2
|
|
166
|
+
|
|
167
|
+
#define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
|
|
168
|
+
#define GARBAGE_CAP 4 /* max garbage rows per attack */
|
|
169
|
+
|
|
170
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a few 68k instructions) ── */
|
|
171
|
+
static u8 random8(void) {
|
|
172
|
+
u16 r = rng;
|
|
173
|
+
r ^= r << 7;
|
|
174
|
+
r ^= r >> 9;
|
|
175
|
+
r ^= r << 8;
|
|
176
|
+
rng = r;
|
|
177
|
+
return (u8)r;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
181
|
+
* CARTRIDGE SRAM — the Genesis battery-save mechanism, three parts:
|
|
182
|
+
*
|
|
183
|
+
* 1. The ROM HEADER declares it: bytes $1B0.. hold 'R','A', a type word
|
|
184
|
+
* ($F820 = battery-backed, byte-wide on ODD addresses — the classic
|
|
185
|
+
* cart wiring), then start/end addresses $200000/$20FFFF. SGDK's
|
|
186
|
+
* rom_header.c (assembled into every build) already declares exactly
|
|
187
|
+
* this — no linker work needed. Emulators allocate the save RAM by
|
|
188
|
+
* READING THIS HEADER; no 'RA' block = writes to $200000+ go nowhere.
|
|
189
|
+
* 2. The MAPPER GATE: writing 1 to $A130F1 banks SRAM into $200000+,
|
|
190
|
+
* 0 banks the ROM back in. SGDK's SRAM_enable()/SRAM_disable() do
|
|
191
|
+
* this. ALWAYS disable after access — on carts >2 MB the SRAM window
|
|
192
|
+
* shadows ROM, and leaving it enabled corrupts later ROM fetches.
|
|
193
|
+
* 3. ODD-BYTE ADDRESSING: SRAM_readByte/writeByte(offset) access 68k
|
|
194
|
+
* address $200001 + offset*2. Headlessly, the emulator's save_ram
|
|
195
|
+
* region interleaves with dead even bytes: SGDK offset k lives at
|
|
196
|
+
* save_ram[k*2 + 1] (the even bytes read back $FF).
|
|
197
|
+
*
|
|
198
|
+
* Hi-score record layout (SGDK offsets): 0='H' 1='S' 2=lo 3=hi
|
|
199
|
+
* 4=checksum(lo^hi^$A5). Fresh SRAM is all $FF — the magic+checksum
|
|
200
|
+
* rejects it (and any corruption) so first boot shows 0, not 65535.
|
|
201
|
+
*
|
|
202
|
+
* Emulator note (verified against gpgx): the core sizes its save_ram
|
|
203
|
+
* region by scanning for the last non-$FF byte, so the region reads as
|
|
204
|
+
* EMPTY until the first write below lands — that's why hiscore_init runs
|
|
205
|
+
* at the very top of main(). Real hardware and .srm-restoring frontends
|
|
206
|
+
* have no such wrinkle. */
|
|
207
|
+
static u16 hiscore_load(void) {
|
|
208
|
+
u8 m0, m1, lo, hi, ck;
|
|
209
|
+
SRAM_enableRO();
|
|
210
|
+
m0 = SRAM_readByte(0);
|
|
211
|
+
m1 = SRAM_readByte(1);
|
|
212
|
+
lo = SRAM_readByte(2);
|
|
213
|
+
hi = SRAM_readByte(3);
|
|
214
|
+
ck = SRAM_readByte(4);
|
|
215
|
+
SRAM_disable();
|
|
216
|
+
if (m0 == 'H' && m1 == 'S' && ck == (u8)(lo ^ hi ^ 0xA5))
|
|
217
|
+
return ((u16)hi << 8) | lo;
|
|
218
|
+
return 0;
|
|
219
|
+
}
|
|
61
220
|
|
|
62
|
-
static
|
|
221
|
+
static void hiscore_save(u16 sc) {
|
|
222
|
+
u8 lo = (u8)sc, hi = (u8)(sc >> 8);
|
|
223
|
+
SRAM_enable();
|
|
224
|
+
SRAM_writeByte(0, 'H');
|
|
225
|
+
SRAM_writeByte(1, 'S');
|
|
226
|
+
SRAM_writeByte(2, lo);
|
|
227
|
+
SRAM_writeByte(3, hi);
|
|
228
|
+
SRAM_writeByte(4, (u8)(lo ^ hi ^ 0xA5));
|
|
229
|
+
SRAM_disable();
|
|
230
|
+
}
|
|
63
231
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
static
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
232
|
+
/* Format-on-first-boot: if the magic is absent (fresh battery), write a
|
|
233
|
+
* valid zero record immediately so the save file exists from frame one. */
|
|
234
|
+
static void hiscore_init(void) {
|
|
235
|
+
hiscore = hiscore_load();
|
|
236
|
+
if (hiscore == 0) hiscore_save(0);
|
|
237
|
+
}
|
|
70
238
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
239
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
240
|
+
* DMA-QUEUED TILEMAP WRITES — the board repaint path, and where the
|
|
241
|
+
* Genesis earns its keep for puzzle games. Each well keeps a full tilemap
|
|
242
|
+
* MIRROR in work RAM (24 tile rows x 12 tile cols = 576 bytes of attrs);
|
|
243
|
+
* when game logic dirties a board we rebuild the mirror and queue it with
|
|
244
|
+
* ONE VDP_setTileMapDataRect(..., DMA_QUEUE) call. SYS_doVBlankProcess
|
|
245
|
+
* flushes the queue during vblank, where VRAM bandwidth lives (~7 KB per
|
|
246
|
+
* H40 vblank — both wells together use 1.2 KB, so a worst-case double
|
|
247
|
+
* cascade repaints in ONE frame; the NES version budgets the same repaint
|
|
248
|
+
* across 12). Three rules make it safe:
|
|
249
|
+
* - the mirror buffers are STATIC: the queue reads them AT FLUSH TIME,
|
|
250
|
+
* so stack buffers are gone by then, shipping garbage to VRAM;
|
|
251
|
+
* - everything VRAM-bound in the loop goes through DMA_QUEUE (sprites
|
|
252
|
+
* too) so writes land in vblank, never mid-scanline;
|
|
253
|
+
* - the queue holds 80 entries (a rect = one entry per row, so a well
|
|
254
|
+
* is 24) — flush every frame and you'll never overflow it.
|
|
255
|
+
* Mid-frame VDP_drawTextBG / VDP_fillTileMapRect port writes (HUD numbers,
|
|
256
|
+
* state-change repaints) are FINE on Genesis — the VDP FIFO absorbs them.
|
|
257
|
+
* That freedom is exactly what the NES does not give you. */
|
|
258
|
+
static u16 wellmap[2][GRID_H * 2 * GRID_W * 2]; /* 576 bytes per well */
|
|
259
|
+
|
|
260
|
+
static void queue_board(u8 p) {
|
|
261
|
+
u16 r, c, q, base;
|
|
262
|
+
u16 *m = wellmap[p];
|
|
263
|
+
for (r = 0; r < GRID_H; r++) {
|
|
264
|
+
for (c = 0; c < GRID_W; c++) {
|
|
265
|
+
u8 v = grid[p][r][c];
|
|
266
|
+
base = v ? (u16)(T_GEM + (v - 1) * 4) : T_CELL;
|
|
267
|
+
for (q = 0; q < 4; q++) { /* column-major quarters */
|
|
268
|
+
u16 tile = v ? (u16)(base + q) : T_CELL;
|
|
269
|
+
m[(r * 2 + (q & 1)) * (GRID_W * 2) + c * 2 + (q >> 1)] =
|
|
270
|
+
TILE_ATTR_FULL(PAL1, 0, 0, 0, tile);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
VDP_setTileMapDataRect(BG_A, m, well_tx[p], WELL_TY,
|
|
275
|
+
GRID_W * 2, GRID_H * 2, GRID_W * 2, DMA_QUEUE);
|
|
76
276
|
}
|
|
77
277
|
|
|
78
|
-
|
|
278
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
279
|
+
* WINDOW-PLANE HUD — the fixed status bar. The window is a third tilemap
|
|
280
|
+
* that REPLACES plane A wherever it's shown and IGNORES ALL SCROLLING —
|
|
281
|
+
* a hardware-fixed HUD with zero per-frame cost. (The NES needs a sprite-0
|
|
282
|
+
* raster trick for this; on Genesis it's one register.)
|
|
283
|
+
* VDP_setWindowOnTop(2) shows it on the top 2 cell rows; text goes in with
|
|
284
|
+
* VDP_drawTextBG(WINDOW, ...). Two footguns:
|
|
285
|
+
* - The window only lives at screen edges (top/bottom N rows or left/
|
|
286
|
+
* right N columns) — it cannot float mid-screen.
|
|
287
|
+
* - It replaces plane A ONLY: plane B and sprites still render behind/
|
|
288
|
+
* over it. We paint plane B's top rows with a flat dark band so HUD
|
|
289
|
+
* text always reads, and the trio sprites never rise above y=24
|
|
290
|
+
* (rows above the rim are simply not drawn). */
|
|
291
|
+
static void hud_init(void) {
|
|
292
|
+
VDP_setWindowOnTop(HUD_ROWS);
|
|
293
|
+
}
|
|
79
294
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
piece_y = -3;
|
|
295
|
+
/* ── GAME LOGIC (clay) — HUD text (window plane, redrawn only on change) ── */
|
|
296
|
+
static void draw_u16(VDPPlane plane, u16 v, u16 x, u16 y) {
|
|
297
|
+
char buf[8];
|
|
298
|
+
uintToStr(v, buf, 5);
|
|
299
|
+
VDP_drawTextBG(plane, buf, x, y);
|
|
86
300
|
}
|
|
87
301
|
|
|
88
|
-
static u8
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
302
|
+
static u8 hud_dirty_layout = 1; /* set when the HUD layout changes (title<->play) */
|
|
303
|
+
|
|
304
|
+
static void draw_hud(void) {
|
|
305
|
+
char b[2];
|
|
306
|
+
if (state == ST_TITLE) {
|
|
307
|
+
hud_dirty_layout = 1; /* next play entry repaints its layout */
|
|
308
|
+
VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS);
|
|
309
|
+
VDP_drawTextBG(WINDOW, "HI", 18, 0);
|
|
310
|
+
draw_u16(WINDOW, hiscore, 21, 0);
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
313
|
+
/* Entering play from the title leaves the title HUD's glyphs behind
|
|
314
|
+
* (different column layout) — clear the row before the play layout, or
|
|
315
|
+
* the leftovers merge into garbage like "HIH0000000". */
|
|
316
|
+
if (hud_dirty_layout) { VDP_clearTextAreaBG(WINDOW, 0, 0, 40, HUD_ROWS); hud_dirty_layout = 0; }
|
|
317
|
+
VDP_drawTextBG(WINDOW, two_player ? "P1" : "SC", 1, 0);
|
|
318
|
+
draw_u16(WINDOW, score[0], 4, 0);
|
|
319
|
+
VDP_drawTextBG(WINDOW, "HI", 16, 0);
|
|
320
|
+
draw_u16(WINDOW, hiscore, 19, 0);
|
|
321
|
+
if (two_player) {
|
|
322
|
+
VDP_drawTextBG(WINDOW, "P2", 30, 0);
|
|
323
|
+
draw_u16(WINDOW, score[1], 33, 0);
|
|
324
|
+
} else {
|
|
325
|
+
VDP_drawTextBG(WINDOW, "LV", 30, 0);
|
|
326
|
+
b[0] = '0' + level; b[1] = 0;
|
|
327
|
+
VDP_drawTextBG(WINDOW, b, 33, 0);
|
|
94
328
|
}
|
|
95
329
|
}
|
|
96
330
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
331
|
+
/* ── GAME LOGIC (clay) — paint the planes ───────────────────────────────────
|
|
332
|
+
* Plane B (backdrop) is painted ONCE at boot and never touched again.
|
|
333
|
+
* Plane A is repainted on state changes (title text ↔ wells ↔ results);
|
|
334
|
+
* inside the loop only the queued board rects and HUD numbers change. */
|
|
335
|
+
static void paint_backdrop(void) {
|
|
336
|
+
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_BAND),
|
|
337
|
+
0, 0, 64, HUD_ROWS);
|
|
338
|
+
VDP_fillTileMapRect(BG_B, TILE_ATTR_FULL(PAL2, 0, 0, 0, T_BACK),
|
|
339
|
+
0, HUD_ROWS, 64, 32 - HUD_ROWS);
|
|
101
340
|
}
|
|
102
341
|
|
|
103
|
-
static void
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
342
|
+
static void paint_frame(u8 p) {
|
|
343
|
+
u16 attr = TILE_ATTR_FULL(PAL1, 0, 0, 0, T_FRAME);
|
|
344
|
+
u16 x0 = well_tx[p] - 1;
|
|
345
|
+
VDP_fillTileMapRect(BG_A, attr, x0, WELL_TY - 1, GRID_W * 2 + 2, 1);
|
|
346
|
+
VDP_fillTileMapRect(BG_A, attr, x0, WELL_TY + GRID_H * 2, GRID_W * 2 + 2, 1);
|
|
347
|
+
VDP_fillTileMapRect(BG_A, attr, x0, WELL_TY, 1, GRID_H * 2);
|
|
348
|
+
VDP_fillTileMapRect(BG_A, attr, x0 + GRID_W * 2 + 1, WELL_TY, 1, GRID_H * 2);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
static void paint_play(void) {
|
|
352
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
353
|
+
paint_frame(0);
|
|
354
|
+
if (two_player) {
|
|
355
|
+
paint_frame(1);
|
|
356
|
+
VDP_drawTextBG(BG_A, "VS", 19, 14);
|
|
117
357
|
}
|
|
358
|
+
draw_hud();
|
|
118
359
|
}
|
|
119
360
|
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
361
|
+
/* ── GAME LOGIC (clay) — the title screen (text on plane A) ── */
|
|
362
|
+
static void paint_title(void) {
|
|
363
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
364
|
+
VDP_drawTextBG(BG_A, GAME_TITLE, (40 - (sizeof(GAME_TITLE) - 1)) / 2, 8);
|
|
365
|
+
VDP_drawTextBG(BG_A, "1P START - A", 14, 14);
|
|
366
|
+
VDP_drawTextBG(BG_A, "2P VERSUS - B", 13, 16);
|
|
367
|
+
VDP_drawTextBG(BG_A, "A B ROTATE - C DROP", 10, 20);
|
|
368
|
+
VDP_drawTextBG(BG_A, "CHAINS BESIEGE YOUR RIVAL", 7, 22);
|
|
369
|
+
draw_hud();
|
|
124
370
|
}
|
|
125
371
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
col * 2 + dx + 6,
|
|
139
|
-
r * 2 + dy + 1);
|
|
372
|
+
/* ── GAME LOGIC (clay) — the game-over / results screen ── */
|
|
373
|
+
static void paint_over(u8 loser) {
|
|
374
|
+
VDP_clearPlane(BG_A, TRUE);
|
|
375
|
+
if (two_player)
|
|
376
|
+
VDP_drawTextBG(BG_A, loser ? "P1 WINS" : "P2 WINS", 16, 8);
|
|
377
|
+
else
|
|
378
|
+
VDP_drawTextBG(BG_A, "GAME OVER", 15, 8);
|
|
379
|
+
VDP_drawTextBG(BG_A, "P1", 13, 12);
|
|
380
|
+
draw_u16(BG_A, score[0], 17, 12);
|
|
381
|
+
if (two_player) {
|
|
382
|
+
VDP_drawTextBG(BG_A, "P2", 13, 14);
|
|
383
|
+
draw_u16(BG_A, score[1], 17, 14);
|
|
140
384
|
}
|
|
385
|
+
VDP_drawTextBG(BG_A, "HI", 13, 17);
|
|
386
|
+
draw_u16(BG_A, hiscore, 17, 17);
|
|
387
|
+
VDP_drawTextBG(BG_A, "START - TITLE", 13, 21);
|
|
141
388
|
}
|
|
142
389
|
|
|
143
|
-
/* ──
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
390
|
+
/* ── GAME LOGIC (clay) — end of game (top-out). `loser` topped out. ── */
|
|
391
|
+
static void game_end(u8 loser) {
|
|
392
|
+
u16 best = score[0];
|
|
393
|
+
if (two_player && score[1] > best) best = score[1];
|
|
394
|
+
if (best > hiscore) {
|
|
395
|
+
hiscore = best;
|
|
396
|
+
hiscore_save(hiscore); /* battery SRAM — see the SRAM idiom */
|
|
397
|
+
}
|
|
398
|
+
sfx_noise(20); /* game-over rumble */
|
|
399
|
+
state = ST_OVER;
|
|
400
|
+
board_dirty[0] = board_dirty[1] = 0; /* plane A is the results
|
|
401
|
+
* screen now — a stale queued
|
|
402
|
+
* board rect would stamp gems
|
|
403
|
+
* over it */
|
|
404
|
+
prev_pad[0] = 0xFFFF; /* require a fresh press */
|
|
405
|
+
paint_over(loser);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
409
|
+
* Match scan: mark every straight run of 3+ same-coloured gems in all 4
|
|
410
|
+
* directions (a cell can belong to several runs — the mask de-dupes), and
|
|
411
|
+
* return how many cells matched. Runs flat-out on the 68000 — no need to
|
|
412
|
+
* smear it across frames like the cc65 version. */
|
|
150
413
|
static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
151
414
|
|
|
152
|
-
static u8 mark_and_count(
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
415
|
+
static u8 mark_and_count(u8 p) {
|
|
416
|
+
u8 r, c, d, len, k, cnt, col;
|
|
417
|
+
s8 dr, dc;
|
|
418
|
+
s16 sr, sc;
|
|
419
|
+
cnt = 0;
|
|
420
|
+
for (r = 0; r < GRID_H; r++)
|
|
421
|
+
for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
|
|
422
|
+
for (r = 0; r < GRID_H; r++) {
|
|
423
|
+
for (c = 0; c < GRID_W; c++) {
|
|
424
|
+
col = grid[p][r][c];
|
|
425
|
+
if (col == EMPTY) continue;
|
|
426
|
+
for (d = 0; d < 4; d++) {
|
|
427
|
+
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
428
|
+
sr = (s16)r - dr; sc = (s16)c - dc;
|
|
429
|
+
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
430
|
+
&& grid[p][sr][sc] == col) continue; /* not the run's start */
|
|
431
|
+
len = 1;
|
|
432
|
+
sr = (s16)r + dr; sc = (s16)c + dc;
|
|
433
|
+
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
434
|
+
&& grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
435
|
+
if (len >= 3) {
|
|
436
|
+
sr = r; sc = c;
|
|
437
|
+
for (k = 0; k < len; k++) {
|
|
438
|
+
if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
|
|
439
|
+
sr += dr; sc += dc;
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return cnt;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
/* Collapse each column so survivors rest on the floor (walk from the bottom,
|
|
449
|
+
* copying gems down to a write cursor, then zero everything above it). */
|
|
450
|
+
static void apply_gravity(u8 p) {
|
|
451
|
+
u8 c;
|
|
452
|
+
s16 r, w;
|
|
453
|
+
for (c = 0; c < GRID_W; c++) {
|
|
454
|
+
w = GRID_H - 1;
|
|
455
|
+
for (r = GRID_H - 1; r >= 0; r--) {
|
|
456
|
+
if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
|
|
178
457
|
}
|
|
179
|
-
|
|
458
|
+
for (; w >= 0; w--) grid[p][w][c] = EMPTY;
|
|
180
459
|
}
|
|
181
|
-
}
|
|
182
|
-
return cnt;
|
|
183
460
|
}
|
|
184
461
|
|
|
185
|
-
/*
|
|
186
|
-
*
|
|
187
|
-
static
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
462
|
+
/* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
|
|
463
|
+
* Returns the chain depth (0 = the lock matched nothing). */
|
|
464
|
+
static u8 resolve_board(u8 p) {
|
|
465
|
+
u8 n, r, c, chain;
|
|
466
|
+
u16 amt;
|
|
467
|
+
chain = 0;
|
|
468
|
+
for (;;) {
|
|
469
|
+
n = mark_and_count(p);
|
|
470
|
+
if (n == 0) break;
|
|
471
|
+
++chain;
|
|
472
|
+
for (r = 0; r < GRID_H; r++)
|
|
473
|
+
for (c = 0; c < GRID_W; c++)
|
|
474
|
+
if (matched[r][c]) grid[p][r][c] = EMPTY;
|
|
475
|
+
amt = (u16)n * 10;
|
|
476
|
+
if (chain > 1) amt *= chain; /* cascades pay multiplied */
|
|
477
|
+
if (score[p] < 65000) score[p] += amt;
|
|
478
|
+
/* clear chime — pitch rises with chain depth (smaller divider =
|
|
479
|
+
* higher note on the PSG) */
|
|
480
|
+
sfx_tone(0, (u16)(360 - ((u16)chain << 5)), 10);
|
|
481
|
+
apply_gravity(p);
|
|
482
|
+
board_dirty[p] = 1;
|
|
483
|
+
if (!two_player) {
|
|
484
|
+
cleared_total += n;
|
|
485
|
+
while (level < 9 && cleared_total >= (u16)level * 10) ++level;
|
|
486
|
+
}
|
|
487
|
+
draw_hud();
|
|
194
488
|
}
|
|
195
|
-
|
|
196
|
-
}
|
|
489
|
+
return chain;
|
|
197
490
|
}
|
|
198
491
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
492
|
+
/* ── GAME LOGIC (clay) — VERSUS attack: garbage rows rise from the bottom of
|
|
493
|
+
* the victim's well (random gems with one gap — matchable, so a skilled
|
|
494
|
+
* victim digs out). The victim's stack rising means the falling trio shifts
|
|
495
|
+
* up one to stay board-aligned; if the top row is already occupied, the
|
|
496
|
+
* victim tops out and loses. ── */
|
|
497
|
+
static void garbage_insert(u8 v, u8 nrows) {
|
|
498
|
+
u8 k, c, gap;
|
|
499
|
+
s16 r;
|
|
500
|
+
sfx_noise(8); /* incoming-garbage thud */
|
|
501
|
+
for (k = 0; k < nrows; k++) {
|
|
502
|
+
for (c = 0; c < GRID_W; c++) {
|
|
503
|
+
if (grid[v][0][c] != EMPTY) { game_end(v); return; }
|
|
504
|
+
}
|
|
505
|
+
for (r = 0; r < GRID_H - 1; r++)
|
|
506
|
+
for (c = 0; c < GRID_W; c++)
|
|
507
|
+
grid[v][r][c] = grid[v][r + 1][c];
|
|
508
|
+
gap = random8() % GRID_W;
|
|
509
|
+
for (c = 0; c < GRID_W; c++)
|
|
510
|
+
grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (u8)(1 + random8() % 3);
|
|
511
|
+
if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
|
|
512
|
+
}
|
|
513
|
+
board_dirty[v] = 1;
|
|
216
514
|
}
|
|
217
515
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
516
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
|
|
517
|
+
* (pieces enter from above); below the floor or on a gem is not. */
|
|
518
|
+
static u8 can_place(u8 p, s16 x, s16 y) {
|
|
519
|
+
s16 i, cy;
|
|
520
|
+
if (x < 0 || x >= GRID_W) return 0;
|
|
521
|
+
for (i = 0; i < 3; i++) {
|
|
522
|
+
cy = y + i;
|
|
523
|
+
if (cy < 0) continue;
|
|
524
|
+
if (cy >= GRID_H) return 0;
|
|
525
|
+
if (grid[p][cy][x] != EMPTY) return 0;
|
|
222
526
|
}
|
|
223
|
-
|
|
224
|
-
|
|
527
|
+
return 1;
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
static void spawn_piece(u8 p) {
|
|
531
|
+
piece_x[p] = GRID_W / 2;
|
|
532
|
+
piece_y[p] = -2;
|
|
533
|
+
piece_col[p][0] = (u8)(1 + random8() % 3);
|
|
534
|
+
piece_col[p][1] = (u8)(1 + random8() % 3);
|
|
535
|
+
piece_col[p][2] = (u8)(1 + random8() % 3);
|
|
536
|
+
if (!can_place(p, piece_x[p], piece_y[p])) game_end(p);
|
|
225
537
|
}
|
|
226
538
|
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
539
|
+
/* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
|
|
540
|
+
static void lock_piece(u8 p) {
|
|
541
|
+
s16 i, y;
|
|
542
|
+
u8 chain;
|
|
543
|
+
for (i = 0; i < 3; i++) {
|
|
544
|
+
y = piece_y[p] + i;
|
|
545
|
+
if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
|
|
546
|
+
}
|
|
547
|
+
board_dirty[p] = 1;
|
|
548
|
+
sfx_tone(1, 700, 4); /* lock thunk */
|
|
549
|
+
if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
|
|
550
|
+
chain = resolve_board(p);
|
|
551
|
+
if (state != ST_PLAY) return;
|
|
552
|
+
if (chain && two_player) {
|
|
553
|
+
garbage_insert(p ^ 1, chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
|
|
554
|
+
if (state != ST_PLAY) return; /* garbage topped them out */
|
|
233
555
|
}
|
|
234
|
-
|
|
556
|
+
spawn_piece(p);
|
|
235
557
|
}
|
|
236
558
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
559
|
+
/* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
|
|
560
|
+
* (one cell per press), held DOWN soft-drops, A/B cycle the trio's colours
|
|
561
|
+
* (the classic trio "rotate"), C hard-drops. P2 reads CONTROLLER 2. ── */
|
|
562
|
+
static void update_player(u8 p) {
|
|
563
|
+
u16 pad, fresh;
|
|
564
|
+
u8 t, fd;
|
|
565
|
+
pad = JOY_readJoypad(p ? JOY_2 : JOY_1);
|
|
566
|
+
fresh = pad & ~prev_pad[p];
|
|
567
|
+
prev_pad[p] = pad;
|
|
568
|
+
if ((fresh & BUTTON_LEFT) && can_place(p, piece_x[p] - 1, piece_y[p]))
|
|
569
|
+
--piece_x[p];
|
|
570
|
+
if ((fresh & BUTTON_RIGHT) && can_place(p, piece_x[p] + 1, piece_y[p]))
|
|
571
|
+
++piece_x[p];
|
|
572
|
+
if (fresh & BUTTON_A) { /* cycle colours downward */
|
|
573
|
+
t = piece_col[p][2];
|
|
574
|
+
piece_col[p][2] = piece_col[p][1];
|
|
575
|
+
piece_col[p][1] = piece_col[p][0];
|
|
576
|
+
piece_col[p][0] = t;
|
|
577
|
+
sfx_tone(1, 320, 3);
|
|
578
|
+
}
|
|
579
|
+
if (fresh & BUTTON_B) { /* cycle colours upward */
|
|
580
|
+
t = piece_col[p][0];
|
|
581
|
+
piece_col[p][0] = piece_col[p][1];
|
|
582
|
+
piece_col[p][1] = piece_col[p][2];
|
|
583
|
+
piece_col[p][2] = t;
|
|
584
|
+
sfx_tone(1, 280, 3);
|
|
585
|
+
}
|
|
586
|
+
if (fresh & BUTTON_C) { /* hard drop */
|
|
587
|
+
while (can_place(p, piece_x[p], piece_y[p] + 1)) ++piece_y[p];
|
|
588
|
+
lock_piece(p); /* may end the game */
|
|
589
|
+
return;
|
|
590
|
+
}
|
|
591
|
+
if (pad & BUTTON_DOWN) fall_t[p] += 4; /* soft drop */
|
|
592
|
+
++fall_t[p];
|
|
593
|
+
fd = two_player ? VS_FALL_DELAY
|
|
594
|
+
: (u8)(32 - ((level << 1) + level)); /* 29..5 */
|
|
595
|
+
if (fall_t[p] >= fd) {
|
|
596
|
+
fall_t[p] = 0;
|
|
597
|
+
if (can_place(p, piece_x[p], piece_y[p] + 1))
|
|
598
|
+
++piece_y[p];
|
|
599
|
+
else
|
|
600
|
+
lock_piece(p); /* may end the game */
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
|
|
605
|
+
* Only the falling trios are sprites (locked gems are plane-A tiles): 3
|
|
606
|
+
* SAT slots per player, 16x16 each. Cells above the rim aren't drawn —
|
|
607
|
+
* they'd poke out from under the HUD band. */
|
|
608
|
+
#define HIDE_Y (-32)
|
|
609
|
+
static void stage_sprites(void) {
|
|
610
|
+
u16 p, i, slot;
|
|
611
|
+
for (p = 0; p < 2; p++) {
|
|
612
|
+
u8 active = (state == ST_PLAY) && (p == 0 || two_player);
|
|
613
|
+
for (i = 0; i < 3; i++) {
|
|
614
|
+
s16 r = piece_y[p] + (s16)i;
|
|
615
|
+
u8 col = piece_col[p][i] ? piece_col[p][i] : 1;
|
|
616
|
+
slot = p * 3 + i;
|
|
617
|
+
if (active && r >= 0)
|
|
618
|
+
VDP_setSprite(slot,
|
|
619
|
+
(s16)((well_tx[p] + piece_x[p] * 2) << 3),
|
|
620
|
+
(s16)((WELL_TY + r * 2) << 3),
|
|
621
|
+
SPRITE_SIZE(2, 2),
|
|
622
|
+
TILE_ATTR_FULL(PAL1, 1, 0, 0, T_GEM + (col - 1) * 4));
|
|
623
|
+
else
|
|
624
|
+
/* Hidden sprites park at y = -32 (above the screen).
|
|
625
|
+
* NEVER hide with x = -128..0 — a SAT x of 0 is the VDP's
|
|
626
|
+
* sprite-masking trigger and silently blanks every lower-
|
|
627
|
+
* priority sprite on those scanlines. */
|
|
628
|
+
VDP_setSprite(slot, 8, HIDE_Y, SPRITE_SIZE(2, 2),
|
|
629
|
+
TILE_ATTR_FULL(PAL1, 1, 0, 0, T_GEM));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
/* ── HARDWARE IDIOM (load-bearing) — CHAIN the sprite list before
|
|
633
|
+
* uploading. VDP_setSprite does NOT set the SAT link byte, and link 0
|
|
634
|
+
* means "end of list": skip this and the VDP draws sprite 0 only.
|
|
635
|
+
* VDP_linkSprites(0, 6) links slots 0..5; the queued DMA flushes the
|
|
636
|
+
* 6 SAT entries during vblank. ── */
|
|
637
|
+
VDP_linkSprites(0, 6);
|
|
638
|
+
VDP_updateSprites(6, DMA_QUEUE);
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
642
|
+
static void start_game(u8 versus) {
|
|
643
|
+
u8 p, r, c;
|
|
644
|
+
two_player = versus;
|
|
645
|
+
well_tx[0] = versus ? WELL_VS_P1 : WELL_1P_TX;
|
|
646
|
+
well_tx[1] = WELL_VS_P2;
|
|
647
|
+
/* Stir the PRNG with time-spent-on-title so runs differ. */
|
|
648
|
+
rng ^= (u16)vtimer ^ ((u16)vtimer << 7);
|
|
649
|
+
if (rng == 0) rng = 0xACE1;
|
|
650
|
+
for (p = 0; p < 2; p++) {
|
|
651
|
+
for (r = 0; r < GRID_H; r++)
|
|
652
|
+
for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
|
|
653
|
+
fall_t[p] = 0;
|
|
654
|
+
score[p] = 0;
|
|
655
|
+
prev_pad[p] = 0xFFFF; /* the button that started the game
|
|
656
|
+
* shouldn't also rotate the first trio */
|
|
657
|
+
}
|
|
658
|
+
cleared_total = 0;
|
|
659
|
+
level = 1;
|
|
660
|
+
state = ST_PLAY;
|
|
661
|
+
paint_play();
|
|
662
|
+
board_dirty[0] = 1;
|
|
663
|
+
board_dirty[1] = versus;
|
|
664
|
+
spawn_piece(0);
|
|
665
|
+
if (versus) spawn_piece(1);
|
|
666
|
+
sfx_tone(0, 200, 10); /* start jingle */
|
|
243
667
|
}
|
|
244
668
|
|
|
245
669
|
int main(bool hard) {
|
|
670
|
+
u16 pad, fresh;
|
|
246
671
|
(void)hard;
|
|
247
672
|
|
|
248
|
-
/*
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
/*
|
|
263
|
-
*
|
|
264
|
-
*
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
673
|
+
/* SRAM first — before any VDP work. The save file then exists within
|
|
674
|
+
* the game's first frames of life, which is what lets a frontend (or
|
|
675
|
+
* a headless host) see a non-empty save_ram region as early as
|
|
676
|
+
* possible (see the SRAM idiom note on gpgx's size scan). */
|
|
677
|
+
hiscore_init();
|
|
678
|
+
|
|
679
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
680
|
+
* Init order: window size before window text, tiles + palettes before
|
|
681
|
+
* tilemaps that reference them. SGDK's boot already did the dangerous
|
|
682
|
+
* part (VDP regs, Z80, vblank int). No scrolling here, so the scroll
|
|
683
|
+
* mode stays at its boot default — if you add scrolling, set
|
|
684
|
+
* VDP_setScrollingMode FIRST (see the platformer template). */
|
|
685
|
+
hud_init();
|
|
686
|
+
|
|
687
|
+
/* Palettes: PAL1 = board + gems + trio sprites, PAL2 = plane B
|
|
688
|
+
* backdrop, PAL0 index 15 = the SGDK font. Colours are BGR, 3 bits
|
|
689
|
+
* per channel: 0x0BGR with E = full. */
|
|
690
|
+
PAL_setColor(15, 0x0EEE); /* font white */
|
|
691
|
+
PAL_setColor(16 + 1, 0x022E); /* gem 1 ruby */
|
|
692
|
+
PAL_setColor(16 + 2, 0x02C2); /* gem 2 emerald */
|
|
693
|
+
PAL_setColor(16 + 3, 0x0E62); /* gem 3 sapphire */
|
|
694
|
+
PAL_setColor(16 + 4, 0x0EEE); /* gem glint */
|
|
695
|
+
PAL_setColor(16 + 5, 0x0222); /* gem rim */
|
|
696
|
+
PAL_setColor(16 + 6, 0x0666); /* frame steel */
|
|
697
|
+
PAL_setColor(16 + 7, 0x0AAA); /* frame lip */
|
|
698
|
+
PAL_setColor(16 + 8, 0x0421); /* empty-cell speck */
|
|
699
|
+
PAL_setColor(16 + 9, 0x0200); /* well interior near-black */
|
|
700
|
+
PAL_setColor(32 + 1, 0x0202); /* backdrop block border */
|
|
701
|
+
PAL_setColor(32 + 2, 0x0413); /* backdrop block fill */
|
|
702
|
+
PAL_setColor(32 + 3, 0x0101); /* HUD band near-black */
|
|
703
|
+
|
|
704
|
+
VDP_loadTileData(tile_frame, T_FRAME, 1, DMA);
|
|
705
|
+
VDP_loadTileData(tile_cell, T_CELL, 1, DMA);
|
|
706
|
+
VDP_loadTileData(tile_back, T_BACK, 1, DMA);
|
|
707
|
+
VDP_loadTileData(tile_band, T_BAND, 1, DMA);
|
|
708
|
+
build_gem_tiles();
|
|
709
|
+
VDP_loadTileData((u32 *)gem_ram, T_GEM, 12, DMA); /* 3 colours x 4 */
|
|
710
|
+
|
|
711
|
+
paint_backdrop(); /* plane B: painted once */
|
|
712
|
+
sfx_init(); /* PSG: sfx channels + background melody */
|
|
713
|
+
|
|
714
|
+
state = ST_TITLE;
|
|
715
|
+
prev_pad[0] = 0xFFFF;
|
|
716
|
+
paint_title();
|
|
288
717
|
|
|
289
718
|
while (TRUE) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
&& !collides(piece_x + 1, piece_y)) piece_x++;
|
|
299
|
-
if ((pad & BUTTON_A) && !(prev & BUTTON_A)) {
|
|
300
|
-
u8 t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
|
|
301
|
-
sfx_tone(2, 450, 2); /* rotate click */
|
|
302
|
-
}
|
|
303
|
-
if ((pad & BUTTON_START) && !(prev & BUTTON_START)) {
|
|
304
|
-
/* Hard-drop. */
|
|
305
|
-
while (!collides(piece_x, piece_y + 1)) piece_y++;
|
|
306
|
-
lock_piece();
|
|
307
|
-
new_piece();
|
|
308
|
-
prev = pad;
|
|
309
|
-
render_score();
|
|
719
|
+
if (state == ST_TITLE) {
|
|
720
|
+
/* ── GAME LOGIC (clay) — title: A/C/START = 1P, B = 2P versus ── */
|
|
721
|
+
stage_sprites();
|
|
722
|
+
pad = JOY_readJoypad(JOY_1);
|
|
723
|
+
fresh = pad & ~prev_pad[0];
|
|
724
|
+
if (fresh & (BUTTON_A | BUTTON_C | BUTTON_START)) start_game(0);
|
|
725
|
+
else if (fresh & BUTTON_B) start_game(1);
|
|
726
|
+
else prev_pad[0] = pad;
|
|
310
727
|
sfx_update();
|
|
311
728
|
SYS_doVBlankProcess();
|
|
312
729
|
continue;
|
|
313
730
|
}
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
731
|
+
|
|
732
|
+
if (state == ST_OVER) {
|
|
733
|
+
/* Results screen; START or A/C returns to the title. */
|
|
734
|
+
stage_sprites();
|
|
735
|
+
pad = JOY_readJoypad(JOY_1);
|
|
736
|
+
fresh = pad & ~prev_pad[0];
|
|
737
|
+
if (fresh & (BUTTON_START | BUTTON_A | BUTTON_C)) {
|
|
738
|
+
state = ST_TITLE;
|
|
739
|
+
prev_pad[0] = 0xFFFF; /* swallow the held START */
|
|
740
|
+
paint_title();
|
|
322
741
|
} else {
|
|
323
|
-
|
|
742
|
+
prev_pad[0] = pad;
|
|
324
743
|
}
|
|
744
|
+
sfx_update();
|
|
745
|
+
SYS_doVBlankProcess();
|
|
746
|
+
continue;
|
|
325
747
|
}
|
|
326
748
|
|
|
327
|
-
/*
|
|
328
|
-
|
|
749
|
+
/* ── ST_PLAY ──────────────────────────────────────────────────── */
|
|
750
|
+
|
|
751
|
+
/* ── GAME LOGIC (clay — reshape freely) — both players update
|
|
752
|
+
* EVERY frame (simultaneous versus, not alternating turns). Any
|
|
753
|
+
* update can end the game, so re-check state between them. */
|
|
754
|
+
update_player(0);
|
|
755
|
+
if (two_player && state == ST_PLAY) update_player(1);
|
|
756
|
+
|
|
757
|
+
if (state == ST_PLAY) {
|
|
758
|
+
/* Queue dirty board repaints — see the DMA-queue idiom. */
|
|
759
|
+
if (board_dirty[0]) { queue_board(0); board_dirty[0] = 0; }
|
|
760
|
+
if (two_player && board_dirty[1]) { queue_board(1); board_dirty[1] = 0; }
|
|
761
|
+
}
|
|
329
762
|
|
|
330
|
-
|
|
763
|
+
stage_sprites();
|
|
331
764
|
sfx_update();
|
|
332
765
|
SYS_doVBlankProcess();
|
|
333
766
|
}
|