romdevtools 0.27.0 → 0.29.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 +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- 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 +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- 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 -177
- 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 -180
- 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 -156
- 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 +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- 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 +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- 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 +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- 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 +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 +32 -11
- 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 +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- 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 +19 -6
- 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 +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- 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/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- 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/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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/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 +11 -5
- 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 +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- 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 +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- 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 +64 -19
|
@@ -1,228 +1,675 @@
|
|
|
1
|
-
/* ── puzzle.c — SNES
|
|
1
|
+
/* ── puzzle.c — SNES falling-jewel versus puzzle (complete example game) ──────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* A COMPLETE, working game — title screen, 1P marathon (levels speed the
|
|
4
|
+
* fall) and 2P SIMULTANEOUS split-board versus with garbage attacks,
|
|
5
|
+
* score + persistent hi-score (battery SRAM, survives power cycles),
|
|
6
|
+
* SPC music + SFX, and the board rendered the SNES way: a WRAM shadow
|
|
7
|
+
* tilemap blasted to VRAM by DMA every single frame.
|
|
7
8
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
9
|
+
* The game: a falling-trio match-3. A trio of jewels falls into a 6x12 well;
|
|
10
|
+
* LEFT/RIGHT move it, A/B cycle its three colours, DOWN soft-drops. When it
|
|
11
|
+
* lands, any straight run of 3+ same-coloured jewels (horizontal, vertical,
|
|
12
|
+
* or diagonal) clears; survivors fall and cascades chain for multiplied score.
|
|
13
|
+
*
|
|
14
|
+
* 2P VERSUS design (simultaneous, split board): two 6x12 wells side by side —
|
|
15
|
+
* P1 left on controller 1, P2 right on controller 2 (padsCurrent(1) — that's
|
|
16
|
+
* the entire 2P wiring), both falling at once. Clears ATTACK: every chain
|
|
17
|
+
* step you score sends one garbage row (random jewels with one gap, capped
|
|
18
|
+
* at 4 per attack) rising from the bottom of the opponent's well. First
|
|
19
|
+
* player whose stack reaches the top loses.
|
|
20
|
+
*
|
|
21
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
22
|
+
* very different one. The markers tell you what's what:
|
|
23
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented SNES footgun; reshape
|
|
24
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
25
|
+
* GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
|
|
26
|
+
*
|
|
27
|
+
* What depends on what:
|
|
28
|
+
* data.asm — console font, the 8-tile board/jewel tileset + palette
|
|
29
|
+
* (shared by BG2 and the OBJ sprites), and sram_read16/write16.
|
|
30
|
+
* Load-bearing.
|
|
31
|
+
* hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
|
|
32
|
+
* SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
|
|
33
|
+
* silently stop existing — the build still succeeds.
|
|
34
|
+
* snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
|
|
35
|
+
* driver (music + 2 one-shot samples). #include'd, not separately built.
|
|
36
|
+
*
|
|
37
|
+
* ── SNES vs NES: THE SAME GAME, TWO RENDER BUDGETS (teaching note) ──────────
|
|
38
|
+
* The NES build of this exact game (examples/nes puzzle) has to DRIP board
|
|
39
|
+
* repaints through a queued-VRAM path: the NMI drains at most 16 queue bytes
|
|
40
|
+
* per vblank, so a cascade repaints ONE dirty row per frame and a full-board
|
|
41
|
+
* sweep takes 12 frames. On the SNES none of that machinery exists: the whole
|
|
42
|
+
* 32x32 board tilemap lives in WRAM (board_map below) and general-purpose DMA
|
|
43
|
+
* copies all 2 KB of it to VRAM EVERY frame inside vblank (~12 scanlines of
|
|
44
|
+
* the ~38 available — bus speed makes the budget problem evaporate). Game
|
|
45
|
+
* logic just rewrites WRAM whenever it likes, with zero dirty-row tracking
|
|
46
|
+
* toward the PPU; a 12-row double-cascade lands on screen in ONE frame.
|
|
47
|
+
*
|
|
48
|
+
* Frame budget: input + gravity for two trios is nothing; the spike is
|
|
49
|
+
* resolve_board() at lock time (full 4-direction match scan over 72 cells in
|
|
50
|
+
* tcc-compiled C). It can spill a frame past vblank — that shows as (at
|
|
51
|
+
* most) a one-frame hitch on the falling pieces, never corruption, because
|
|
52
|
+
* the shadow map is only DMA'd after WaitForVBlank.
|
|
53
|
+
*
|
|
54
|
+
* VRAM BUDGET (word addresses):
|
|
55
|
+
* $2000- board tileset (8 tiles) $3000- console font (96 glyphs)
|
|
56
|
+
* $4000- board map (BG2, 32x32) $6000- OBJ tiles (same 8 tiles)
|
|
57
|
+
* $6800- console text map (BG1)
|
|
12
58
|
*/
|
|
13
59
|
|
|
14
60
|
#include <snes.h>
|
|
15
61
|
#include "snes_sfx.c"
|
|
16
62
|
|
|
17
|
-
|
|
18
|
-
|
|
63
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
64
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
65
|
+
#define GAME_TITLE "JEWEL JOUST"
|
|
66
|
+
|
|
67
|
+
extern char tilfont, palfont; /* console font + text palette (data.asm) */
|
|
68
|
+
extern char tilboard, palboard; /* board/jewel tiles + palette */
|
|
19
69
|
|
|
20
70
|
/* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
|
|
21
71
|
* No public prototype in console.h, so declare it; call once per frame. */
|
|
22
72
|
extern void consoleVblank(void);
|
|
23
73
|
|
|
24
|
-
/*
|
|
25
|
-
|
|
26
|
-
|
|
74
|
+
/* data.asm exports — battery SRAM accessors ($70:0000 long addressing). */
|
|
75
|
+
extern u16 sram_read16(u16 offset);
|
|
76
|
+
extern void sram_write16(u16 offset, u16 value);
|
|
77
|
+
|
|
78
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
79
|
+
* Board geometry. Tile coordinates are free on the SNES: unlike the NES there
|
|
80
|
+
* is NO attribute table — every 4bpp map entry carries its own palette bits —
|
|
81
|
+
* so wells can sit at ANY column (the NES version must keep them 2-aligned). */
|
|
82
|
+
#define GRID_W 6
|
|
83
|
+
#define GRID_H 12
|
|
84
|
+
#define GRID_CELLS (GRID_W * GRID_H)
|
|
85
|
+
#define WELL_TY 8 /* top tile row of the well interior */
|
|
86
|
+
#define WELL_1P_TX 13 /* 1P: single centered well (cols 13-18) */
|
|
87
|
+
#define WELL_VS_P1 4 /* 2P: P1 well cols 4-9 ... */
|
|
88
|
+
#define WELL_VS_P2 22 /* P2 well cols 22-27 (split board) */
|
|
89
|
+
|
|
90
|
+
#define EMPTY 0 /* cell colours 1..3 = ruby/emerald/amber */
|
|
27
91
|
|
|
28
|
-
|
|
29
|
-
#define
|
|
92
|
+
/* board tileset indices — MUST match the tile order in data.asm */
|
|
93
|
+
#define BG_BLANK 0
|
|
94
|
+
#define BG_WALL 1
|
|
95
|
+
#define BG_DITHER 2
|
|
96
|
+
#define BG_INNER 3
|
|
97
|
+
#define BG_GEM_BASE 4 /* tiles 4/5/6 = jewel colours 1/2/3 */
|
|
30
98
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
static s16 piece_x;
|
|
34
|
-
static s16 piece_y;
|
|
35
|
-
static u16 fall_timer;
|
|
36
|
-
static u16 score;
|
|
37
|
-
static u32 rng = 1;
|
|
99
|
+
/* BG2 map entries select CGRAM palette block 1 (vhopppcc cccccccc). */
|
|
100
|
+
#define MAP_PAL1 0x0400
|
|
38
101
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
102
|
+
#define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
|
|
103
|
+
#define GARBAGE_CAP 4 /* max garbage rows per attack */
|
|
104
|
+
|
|
105
|
+
/* SRAM layout: [0]=magic "JJ", [2]=hi-score, [4]=hi ^ 0xA5C3.
|
|
106
|
+
* Magic is written LAST in hi_save so a torn write never validates. */
|
|
107
|
+
#define SRAM_MAGIC 0x4A4Au
|
|
108
|
+
|
|
109
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
110
|
+
#define ST_TITLE 0
|
|
111
|
+
#define ST_PLAY 1
|
|
112
|
+
#define ST_OVER 2
|
|
113
|
+
|
|
114
|
+
static u8 state;
|
|
115
|
+
static u8 two_player; /* mode chosen on the title screen */
|
|
116
|
+
static u8 sound_ok;
|
|
117
|
+
static u8 well_tx[2]; /* left tile column of each well */
|
|
118
|
+
static u8 piece_x[2]; /* falling trio: column 0..5 */
|
|
119
|
+
static s8 piece_y[2]; /* row of its TOP cell (<0 = above rim) */
|
|
120
|
+
static u8 piece_col[2][3]; /* trio colours, top to bottom */
|
|
121
|
+
static u8 fall_t[2]; /* frames until next gravity step */
|
|
122
|
+
static u16 prev_pad[2]; /* per-player edge-triggered input */
|
|
123
|
+
static u16 prev_pad0; /* shell (title/game-over) edge detect */
|
|
124
|
+
static u16 score[2];
|
|
125
|
+
static u16 hiscore;
|
|
126
|
+
static u16 cleared_total; /* 1P: jewels cleared, drives the level */
|
|
127
|
+
static u8 level; /* 1P: 1..9, speeds up the fall */
|
|
128
|
+
static u8 board_dirty[2]; /* well cells changed → recompose shadow map */
|
|
129
|
+
static u8 garb_rows[2]; /* garbage rows RECEIVED (telemetry + tuning) */
|
|
130
|
+
static u16 frames; /* free-running frame counter (PRNG stir) */
|
|
131
|
+
static u16 rng = 0xACE1;
|
|
132
|
+
static char tbuf[8]; /* 5-digit number formatter output */
|
|
133
|
+
|
|
134
|
+
/* the two boards, flattened (row*GRID_W+col); P2's right after P1's */
|
|
135
|
+
static u8 grid[2 * GRID_CELLS];
|
|
136
|
+
static u8 matched[GRID_CELLS];
|
|
137
|
+
#define GRIDOF(p) (grid + ((p) ? GRID_CELLS : 0))
|
|
138
|
+
|
|
139
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
140
|
+
* The board's WRAM shadow tilemap. This 2 KB array IS the screen: game code
|
|
141
|
+
* writes map entries here whenever it likes (any time, mid-frame, mid-logic),
|
|
142
|
+
* and the main loop DMAs the whole thing to VRAM word $4000 right after
|
|
143
|
+
* WaitForVBlank — full repaint, every frame, no queue, no dirty-row budget
|
|
144
|
+
* (see the NES-contrast note in the header). The ONLY rule is the DMA's:
|
|
145
|
+
* VRAM writes land correctly ONLY during vblank/forced blank, so the
|
|
146
|
+
* dmaCopyVram call must stay where it is, between WaitForVBlank and the
|
|
147
|
+
* frame's logic. Writing board_map itself is always safe. */
|
|
148
|
+
static u16 board_map[32 * 32];
|
|
149
|
+
|
|
150
|
+
/* headless-test telemetry — magic "JW"+0xBD; a test harness scans WRAM for
|
|
151
|
+
* it and plays the game from real state instead of parsing pixels. Costs a
|
|
152
|
+
* few byte-writes per frame; delete freely. */
|
|
153
|
+
static u8 telem[24];
|
|
154
|
+
|
|
155
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
156
|
+
static u8 random8(void) {
|
|
157
|
+
u16 r = rng;
|
|
158
|
+
r ^= r << 7;
|
|
159
|
+
r ^= r >> 9;
|
|
160
|
+
r ^= r << 8;
|
|
161
|
+
rng = r;
|
|
162
|
+
return (u8)r;
|
|
44
163
|
}
|
|
45
164
|
|
|
46
|
-
|
|
165
|
+
/* cell colour → board tile (empty cells show the faint speck, not raw
|
|
166
|
+
* backdrop, so the well reads as a recessed playfield). */
|
|
167
|
+
static u16 cell_entry(u8 col) {
|
|
168
|
+
return (u16)(col ? (u8)(BG_GEM_BASE - 1 + col) : BG_INNER) | MAP_PAL1;
|
|
169
|
+
}
|
|
47
170
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
171
|
+
/* ── GAME LOGIC (clay) — shadow-map painters ─────────────────────────────────
|
|
172
|
+
* All of these only touch board_map (WRAM); the per-frame DMA makes them
|
|
173
|
+
* visible. paint_board is the per-change repaint: ~72 u16 stores, cheap
|
|
174
|
+
* enough to run whole-board whenever anything locked/cleared/shifted. */
|
|
175
|
+
static void map_fill(u8 tile) {
|
|
176
|
+
u16 i, e;
|
|
177
|
+
e = (u16)tile | MAP_PAL1;
|
|
178
|
+
for (i = 0; i < 32 * 32; i++) board_map[i] = e;
|
|
55
179
|
}
|
|
56
180
|
|
|
57
|
-
static void
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
piece_x = COLS / 2 - 1;
|
|
62
|
-
piece_y = -3;
|
|
181
|
+
static void map_row_fill(u8 row, u8 tile) {
|
|
182
|
+
u16 i, base;
|
|
183
|
+
base = (u16)row << 5;
|
|
184
|
+
for (i = 0; i < 32; i++) board_map[base + i] = (u16)tile | MAP_PAL1;
|
|
63
185
|
}
|
|
64
186
|
|
|
65
|
-
static void
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
187
|
+
static void paint_board(u8 p) {
|
|
188
|
+
u8 r, c;
|
|
189
|
+
u16 base;
|
|
190
|
+
u8 *g = GRIDOF(p);
|
|
191
|
+
for (r = 0; r < GRID_H; r++) {
|
|
192
|
+
base = ((u16)(WELL_TY + r) << 5) + well_tx[p];
|
|
193
|
+
for (c = 0; c < GRID_W; c++)
|
|
194
|
+
board_map[base + c] = cell_entry(*g++);
|
|
195
|
+
}
|
|
71
196
|
}
|
|
72
197
|
|
|
73
|
-
static void
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
198
|
+
static void paint_well_frame(u8 p) {
|
|
199
|
+
u8 r, c, x0;
|
|
200
|
+
u16 e;
|
|
201
|
+
x0 = well_tx[p];
|
|
202
|
+
e = (u16)BG_WALL | MAP_PAL1;
|
|
203
|
+
for (c = (u8)(x0 - 1); c <= (u8)(x0 + GRID_W); c++) {
|
|
204
|
+
board_map[((u16)(WELL_TY - 1) << 5) + c] = e;
|
|
205
|
+
board_map[((u16)(WELL_TY + GRID_H) << 5) + c] = e;
|
|
206
|
+
}
|
|
207
|
+
for (r = (u8)(WELL_TY - 1); r <= (u8)(WELL_TY + GRID_H); r++) {
|
|
208
|
+
board_map[((u16)r << 5) + (u16)(x0 - 1)] = e;
|
|
209
|
+
board_map[((u16)r << 5) + (u16)(x0 + GRID_W)] = e;
|
|
210
|
+
}
|
|
78
211
|
}
|
|
79
212
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
213
|
+
/* title backdrop: dither cabinet, a clear band for the menu text, and a
|
|
214
|
+
* jewel stripe under the logo (the attract twist below scrolls its hues). */
|
|
215
|
+
static void paint_title_map(void) {
|
|
216
|
+
u8 r, c;
|
|
217
|
+
map_fill(BG_DITHER);
|
|
218
|
+
for (r = 2; r <= 6; r++) map_row_fill(r, BG_BLANK);
|
|
219
|
+
for (r = 13; r <= 17; r++) map_row_fill(r, BG_BLANK);
|
|
220
|
+
for (c = 0; c < 32; c++) {
|
|
221
|
+
board_map[(25u << 5) + c] = (u16)BG_WALL | MAP_PAL1;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static void paint_title_stripe(u8 phase) {
|
|
226
|
+
u8 c;
|
|
227
|
+
for (c = 10; c < 22; c++)
|
|
228
|
+
board_map[(7u << 5) + c] =
|
|
229
|
+
(u16)(BG_GEM_BASE + (u8)((c + phase) % 3)) | MAP_PAL1;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static void paint_play_map(void) {
|
|
233
|
+
u8 r;
|
|
234
|
+
map_fill(BG_DITHER);
|
|
235
|
+
for (r = 0; r < 3; r++) map_row_fill(r, BG_BLANK); /* clean HUD band */
|
|
236
|
+
paint_well_frame(0);
|
|
237
|
+
paint_board(0);
|
|
238
|
+
if (two_player) { paint_well_frame(1); paint_board(1); }
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/* ── GAME LOGIC (clay) — text helpers (console BG1, queued via consoleVblank) */
|
|
242
|
+
static void fmt5(u16 v) {
|
|
243
|
+
s8 i;
|
|
244
|
+
for (i = 4; i >= 0; i--) { tbuf[i] = (char)('0' + (v % 10)); v /= 10; }
|
|
245
|
+
tbuf[5] = 0;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
static void clear_rows(u8 a, u8 b) {
|
|
249
|
+
u8 y;
|
|
250
|
+
for (y = a; y <= b; y++)
|
|
251
|
+
consoleDrawText(0, y, " ");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static void draw_hud_num(u8 p) {
|
|
255
|
+
if (p == 0) { fmt5(score[0]); consoleDrawText(2, 2, tbuf); }
|
|
256
|
+
else {
|
|
257
|
+
if (two_player) fmt5(score[1]); else fmt5(level);
|
|
258
|
+
consoleDrawText(24, 2, tbuf);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
static void draw_hi(u8 x, u8 y) {
|
|
263
|
+
fmt5(hiscore);
|
|
264
|
+
consoleDrawText(x, y, tbuf);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* ── GAME LOGIC (clay) — hi-score in battery SRAM (see sram_* in data.asm) ── */
|
|
268
|
+
static u16 hi_load(void) {
|
|
269
|
+
u16 v;
|
|
270
|
+
if (sram_read16(0) != SRAM_MAGIC) return 0;
|
|
271
|
+
v = sram_read16(2);
|
|
272
|
+
if (sram_read16(4) != (u16)(v ^ 0xA5C3u)) return 0;
|
|
273
|
+
return v;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
static void hi_save(u16 v) {
|
|
277
|
+
sram_write16(2, v);
|
|
278
|
+
sram_write16(4, (u16)(v ^ 0xA5C3u));
|
|
279
|
+
sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
283
|
+
* Match scan: mark every straight run of 3+ same-coloured jewels in all 4
|
|
284
|
+
* directions (a cell can belong to several runs — the mask de-dupes), and
|
|
285
|
+
* return how many cells matched. This is the resolve-time spike the header's
|
|
286
|
+
* frame-budget note talks about. */
|
|
287
|
+
static const s8 DR4[4] = { 0, 1, 1, 1 };
|
|
288
|
+
static const s8 DC4[4] = { 1, 0, 1, -1 };
|
|
289
|
+
|
|
290
|
+
static u8 mark_and_count(u8 p) {
|
|
291
|
+
u8 d, len, k, cnt, col;
|
|
292
|
+
s16 r, c, sr, sc, dr, dc;
|
|
293
|
+
u8 *g = GRIDOF(p);
|
|
294
|
+
cnt = 0;
|
|
295
|
+
for (k = 0; k < GRID_CELLS; k++) matched[k] = 0;
|
|
296
|
+
for (r = 0; r < GRID_H; r++) {
|
|
297
|
+
for (c = 0; c < GRID_W; c++) {
|
|
298
|
+
col = g[r * GRID_W + c];
|
|
299
|
+
if (col == EMPTY) continue;
|
|
300
|
+
for (d = 0; d < 4; d++) {
|
|
301
|
+
dr = DR4[d]; dc = DC4[d];
|
|
302
|
+
sr = r - dr; sc = c - dc;
|
|
303
|
+
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
304
|
+
&& g[sr * GRID_W + sc] == col) continue; /* not the run's start */
|
|
305
|
+
len = 1;
|
|
306
|
+
sr = r + dr; sc = c + dc;
|
|
307
|
+
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
308
|
+
&& g[sr * GRID_W + sc] == col) { len++; sr += dr; sc += dc; }
|
|
309
|
+
if (len >= 3) {
|
|
310
|
+
sr = r; sc = c;
|
|
311
|
+
for (k = 0; k < len; k++) {
|
|
312
|
+
if (!matched[sr * GRID_W + sc]) { matched[sr * GRID_W + sc] = 1; cnt++; }
|
|
313
|
+
sr += dr; sc += dc;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
91
317
|
}
|
|
318
|
+
}
|
|
319
|
+
return cnt;
|
|
92
320
|
}
|
|
93
321
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
322
|
+
/* Collapse each column so survivors rest on the floor (walk from the bottom,
|
|
323
|
+
* copying jewels down to a write cursor, then zero everything above it). */
|
|
324
|
+
static void apply_gravity(u8 p) {
|
|
325
|
+
s16 c, r, w;
|
|
326
|
+
u8 *g = GRIDOF(p);
|
|
327
|
+
for (c = 0; c < GRID_W; c++) {
|
|
328
|
+
w = GRID_H - 1;
|
|
329
|
+
for (r = GRID_H - 1; r >= 0; r--) {
|
|
330
|
+
if (g[r * GRID_W + c] != EMPTY) { g[w * GRID_W + c] = g[r * GRID_W + c]; w--; }
|
|
102
331
|
}
|
|
103
|
-
|
|
332
|
+
for (; w >= 0; w--) g[w * GRID_W + c] = EMPTY;
|
|
333
|
+
}
|
|
104
334
|
}
|
|
105
335
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
336
|
+
/* ── GAME LOGIC (clay) — end of game (top-out). `loser` topped out. ── */
|
|
337
|
+
static void game_end(u8 loser) {
|
|
338
|
+
u16 best = score[0];
|
|
339
|
+
if (two_player && score[1] > best) best = score[1];
|
|
340
|
+
if (best > hiscore) {
|
|
341
|
+
hiscore = best;
|
|
342
|
+
hi_save(hiscore); /* battery SRAM — survives power-off */
|
|
343
|
+
draw_hi(13, 2);
|
|
344
|
+
}
|
|
345
|
+
if (sound_ok) sfx_play(2); /* game-over thud */
|
|
346
|
+
if (two_player) consoleDrawText(12, 22, loser ? "P1 WINS" : "P2 WINS");
|
|
347
|
+
else consoleDrawText(11, 22, "GAME OVER");
|
|
348
|
+
consoleDrawText(9, 24, "START - TITLE");
|
|
349
|
+
prev_pad0 = 0xFFFF; /* require a fresh press */
|
|
350
|
+
state = ST_OVER;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
|
|
354
|
+
* Returns the chain depth (0 = the lock matched nothing). The repaint is
|
|
355
|
+
* just board_dirty=1: the whole well redraws into the shadow map this frame
|
|
356
|
+
* and the next vblank's DMA shows it — chains land instantly on screen. */
|
|
357
|
+
static u8 resolve_board(u8 p) {
|
|
358
|
+
u8 n, k, chain;
|
|
359
|
+
u16 amt;
|
|
360
|
+
u8 *g = GRIDOF(p);
|
|
361
|
+
chain = 0;
|
|
362
|
+
for (;;) {
|
|
363
|
+
n = mark_and_count(p);
|
|
364
|
+
if (n == 0) break;
|
|
365
|
+
++chain;
|
|
366
|
+
for (k = 0; k < GRID_CELLS; k++)
|
|
367
|
+
if (matched[k]) g[k] = EMPTY;
|
|
368
|
+
amt = (u16)n * 10;
|
|
369
|
+
if (chain > 1) amt *= chain; /* cascades pay multiplied */
|
|
370
|
+
score[p] += amt;
|
|
371
|
+
draw_hud_num(p);
|
|
372
|
+
if (sound_ok) sfx_play(2); /* clear chime */
|
|
373
|
+
apply_gravity(p);
|
|
374
|
+
board_dirty[p] = 1;
|
|
375
|
+
if (!two_player) {
|
|
376
|
+
cleared_total += n;
|
|
377
|
+
while (level < 9 && cleared_total >= (u16)level * 10) {
|
|
378
|
+
++level;
|
|
379
|
+
draw_hud_num(1); /* 1P: slot 1 shows the level */
|
|
380
|
+
}
|
|
113
381
|
}
|
|
382
|
+
}
|
|
383
|
+
return chain;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/* ── GAME LOGIC (clay) — VERSUS attack: garbage rows rise from the bottom of
|
|
387
|
+
* the victim's well (random jewels with one gap — matchable, so a skilled
|
|
388
|
+
* victim digs out). The victim's stack rising means the falling trio shifts
|
|
389
|
+
* up one to stay board-relative; if the top row is already occupied, the
|
|
390
|
+
* victim tops out and loses. ── */
|
|
391
|
+
static void garbage_insert(u8 v, u8 nrows) {
|
|
392
|
+
u8 k, c, gap;
|
|
393
|
+
s16 r;
|
|
394
|
+
u8 *g = GRIDOF(v);
|
|
395
|
+
if (sound_ok) sfx_play(2); /* incoming-garbage thud */
|
|
396
|
+
for (k = 0; k < nrows; k++) {
|
|
397
|
+
for (c = 0; c < GRID_W; c++)
|
|
398
|
+
if (g[c] != EMPTY) { board_dirty[v] = 1; game_end(v); return; }
|
|
399
|
+
for (r = 0; r < GRID_H - 1; r++)
|
|
400
|
+
for (c = 0; c < GRID_W; c++)
|
|
401
|
+
g[r * GRID_W + c] = g[(r + 1) * GRID_W + c];
|
|
402
|
+
gap = random8() % GRID_W;
|
|
403
|
+
for (c = 0; c < GRID_W; c++)
|
|
404
|
+
g[(GRID_H - 1) * GRID_W + c] = (c == gap) ? EMPTY : (u8)(1 + random8() % 3);
|
|
405
|
+
if (piece_y[v] > -3) --piece_y[v]; /* keep the trio board-relative */
|
|
406
|
+
++garb_rows[v];
|
|
407
|
+
}
|
|
408
|
+
board_dirty[v] = 1;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
|
|
412
|
+
* (pieces enter from above); below the floor or on a jewel is not. */
|
|
413
|
+
static u8 can_place(u8 p, s16 x, s16 y) {
|
|
414
|
+
s16 i, cy;
|
|
415
|
+
u8 *g = GRIDOF(p);
|
|
416
|
+
if (x < 0 || x >= GRID_W) return 0;
|
|
417
|
+
for (i = 0; i < 3; i++) {
|
|
418
|
+
cy = y + i;
|
|
419
|
+
if (cy < 0) continue;
|
|
420
|
+
if (cy >= GRID_H) return 0;
|
|
421
|
+
if (g[cy * GRID_W + x] != EMPTY) return 0;
|
|
422
|
+
}
|
|
423
|
+
return 1;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
static void spawn_piece(u8 p) {
|
|
427
|
+
piece_x[p] = GRID_W / 2;
|
|
428
|
+
piece_y[p] = -2;
|
|
429
|
+
piece_col[p][0] = (u8)(1 + random8() % 3);
|
|
430
|
+
piece_col[p][1] = (u8)(1 + random8() % 3);
|
|
431
|
+
piece_col[p][2] = (u8)(1 + random8() % 3);
|
|
432
|
+
if (!can_place(p, (s16)piece_x[p], (s16)piece_y[p])) game_end(p);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
|
|
436
|
+
static void lock_piece(u8 p) {
|
|
437
|
+
s16 i, y;
|
|
438
|
+
u8 chain;
|
|
439
|
+
u8 *g = GRIDOF(p);
|
|
440
|
+
for (i = 0; i < 3; i++) {
|
|
441
|
+
y = piece_y[p] + i;
|
|
442
|
+
if (y >= 0) g[y * GRID_W + piece_x[p]] = piece_col[p][i];
|
|
443
|
+
}
|
|
444
|
+
board_dirty[p] = 1;
|
|
445
|
+
if (sound_ok) sfx_play(1); /* lock click */
|
|
446
|
+
if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
|
|
447
|
+
chain = resolve_board(p);
|
|
448
|
+
if (state != ST_PLAY) return;
|
|
449
|
+
if (chain && two_player) {
|
|
450
|
+
garbage_insert(p ^ 1, chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
|
|
451
|
+
if (state != ST_PLAY) return; /* garbage topped them out */
|
|
452
|
+
}
|
|
453
|
+
spawn_piece(p);
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
|
|
457
|
+
* (one cell per press), held DOWN soft-drops. A/B cycle the trio's colours
|
|
458
|
+
* — the classic trio "rotate". P2's pad is just padsCurrent(1). ── */
|
|
459
|
+
static void update_player(u8 p) {
|
|
460
|
+
u16 pad, newp;
|
|
461
|
+
u8 fd, t;
|
|
462
|
+
pad = padsCurrent(p);
|
|
463
|
+
newp = pad & (u16)~prev_pad[p];
|
|
464
|
+
prev_pad[p] = pad;
|
|
465
|
+
if ((newp & KEY_LEFT) && can_place(p, (s16)(piece_x[p] - 1), (s16)piece_y[p]))
|
|
466
|
+
--piece_x[p];
|
|
467
|
+
if ((newp & KEY_RIGHT) && can_place(p, (s16)(piece_x[p] + 1), (s16)piece_y[p]))
|
|
468
|
+
++piece_x[p];
|
|
469
|
+
if (newp & KEY_A) { /* cycle colours downward */
|
|
470
|
+
t = piece_col[p][2];
|
|
471
|
+
piece_col[p][2] = piece_col[p][1];
|
|
472
|
+
piece_col[p][1] = piece_col[p][0];
|
|
473
|
+
piece_col[p][0] = t;
|
|
474
|
+
if (sound_ok) sfx_play(1);
|
|
475
|
+
}
|
|
476
|
+
if (newp & KEY_B) { /* cycle colours upward */
|
|
477
|
+
t = piece_col[p][0];
|
|
478
|
+
piece_col[p][0] = piece_col[p][1];
|
|
479
|
+
piece_col[p][1] = piece_col[p][2];
|
|
480
|
+
piece_col[p][2] = t;
|
|
481
|
+
if (sound_ok) sfx_play(1);
|
|
482
|
+
}
|
|
483
|
+
if (pad & KEY_DOWN) fall_t[p] += 4; /* soft drop */
|
|
484
|
+
++fall_t[p];
|
|
485
|
+
fd = two_player ? VS_FALL_DELAY
|
|
486
|
+
: (u8)(32 - level * 3); /* 1P: 29..5 frames per row */
|
|
487
|
+
if (fall_t[p] >= fd) {
|
|
488
|
+
fall_t[p] = 0;
|
|
489
|
+
if (can_place(p, (s16)piece_x[p], (s16)(piece_y[p] + 1)))
|
|
490
|
+
++piece_y[p];
|
|
491
|
+
else
|
|
492
|
+
lock_piece(p); /* may end the game */
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
497
|
+
* The falling trios are the ONLY sprites (board jewels are BG tiles — only
|
|
498
|
+
* what moves every frame earns OAM slots). oamSet's first arg is a BYTE
|
|
499
|
+
* OFFSET into OAM (slot*4), its gfxoffset is a tile INDEX into the OBJ page.
|
|
500
|
+
* Hiding = parking at y=240 (no oamSetEx churn). oamUpdate() queues the
|
|
501
|
+
* shadow table; PVSnesLib's VBlank ISR DMAs it to hardware on channel 7
|
|
502
|
+
* every NMI — so stage sprites BEFORE WaitForVBlank, never after. */
|
|
503
|
+
static void stage_pieces(void) {
|
|
504
|
+
u8 p, i, n;
|
|
505
|
+
s8 y;
|
|
506
|
+
for (p = 0; p < 2; p++) {
|
|
114
507
|
for (i = 0; i < 3; i++) {
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if (score < 65500) score += 30;
|
|
124
|
-
sfx_play(2); /* triple-clear chime */
|
|
125
|
-
}
|
|
126
|
-
}
|
|
508
|
+
n = (u8)((p * 3 + i) << 2);
|
|
509
|
+
y = (s8)(piece_y[p] + i);
|
|
510
|
+
if (state == ST_PLAY && y >= 0 && (p == 0 || two_player))
|
|
511
|
+
oamSet(n, (u16)((well_tx[p] + piece_x[p]) << 3),
|
|
512
|
+
(u16)((WELL_TY + (u8)y) << 3), 3, 0, 0,
|
|
513
|
+
(u16)(BG_GEM_BASE - 1 + piece_col[p][i]), 0);
|
|
514
|
+
else
|
|
515
|
+
oamSet(n, 0, 240, 3, 0, 0, 0, 0); /* y=240 = hidden */
|
|
127
516
|
}
|
|
128
|
-
|
|
517
|
+
}
|
|
129
518
|
}
|
|
130
519
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
520
|
+
/* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
|
|
521
|
+
static void title_enter(void) {
|
|
522
|
+
clear_rows(0, 27);
|
|
523
|
+
consoleDrawText(10, 3, GAME_TITLE);
|
|
524
|
+
consoleDrawText(11, 5, "HI"); draw_hi(14, 5);
|
|
525
|
+
consoleDrawText(8, 14, "A - 1P MARATHON");
|
|
526
|
+
consoleDrawText(8, 16, "B - 2P VERSUS");
|
|
527
|
+
consoleDrawText(2, 26, "LR MOVE A B SPIN DOWN DROP");
|
|
528
|
+
paint_title_map();
|
|
529
|
+
paint_title_stripe(0);
|
|
530
|
+
prev_pad0 = 0xFFFF; /* swallow the press that ENTERED this state — without
|
|
531
|
+
* this, the START that left the game-over screen
|
|
532
|
+
* instantly starts a new 1P run (classic edge-detect
|
|
533
|
+
* reuse bug) */
|
|
534
|
+
state = ST_TITLE;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
static void start_game(u8 versus) {
|
|
538
|
+
u8 p, k;
|
|
539
|
+
two_player = versus;
|
|
540
|
+
well_tx[0] = versus ? WELL_VS_P1 : WELL_1P_TX;
|
|
541
|
+
well_tx[1] = WELL_VS_P2;
|
|
542
|
+
/* Stir the PRNG with time-spent-on-title so runs differ. */
|
|
543
|
+
rng ^= (u16)((frames << 7) | frames);
|
|
544
|
+
if (rng == 0) rng = 0xACE1;
|
|
545
|
+
for (p = 0; p < 2; p++) {
|
|
546
|
+
u8 *g = GRIDOF(p);
|
|
547
|
+
for (k = 0; k < GRID_CELLS; k++) g[k] = EMPTY;
|
|
548
|
+
fall_t[p] = 0;
|
|
549
|
+
score[p] = 0;
|
|
550
|
+
garb_rows[p] = 0;
|
|
551
|
+
board_dirty[p] = 0;
|
|
552
|
+
prev_pad[p] = 0xFFFF; /* the button that started the game
|
|
553
|
+
* shouldn't also spin the first trio */
|
|
554
|
+
}
|
|
555
|
+
cleared_total = 0;
|
|
556
|
+
level = 1;
|
|
557
|
+
clear_rows(0, 27);
|
|
558
|
+
/* HUD: labels row 1, numbers row 2 */
|
|
559
|
+
consoleDrawText(2, 1, versus ? "P1" : "SC");
|
|
560
|
+
consoleDrawText(13, 1, "HI");
|
|
561
|
+
consoleDrawText(24, 1, versus ? "P2" : "LV");
|
|
562
|
+
draw_hud_num(0);
|
|
563
|
+
draw_hi(13, 2);
|
|
564
|
+
draw_hud_num(1);
|
|
565
|
+
paint_play_map();
|
|
566
|
+
state = ST_PLAY;
|
|
567
|
+
spawn_piece(0);
|
|
568
|
+
if (versus) spawn_piece(1);
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/* Headless-test telemetry — see the static block's comment. */
|
|
572
|
+
static void telem_update(void) {
|
|
573
|
+
telem[0] = 'J'; telem[1] = 'W'; telem[2] = 0xBD;
|
|
574
|
+
telem[3] = state;
|
|
575
|
+
telem[4] = (u8)((sound_ok << 7) | two_player);
|
|
576
|
+
telem[5] = level;
|
|
577
|
+
telem[6] = (u8)score[0]; telem[7] = (u8)(score[0] >> 8);
|
|
578
|
+
telem[8] = (u8)score[1]; telem[9] = (u8)(score[1] >> 8);
|
|
579
|
+
telem[10] = piece_x[0]; telem[11] = (u8)piece_y[0];
|
|
580
|
+
telem[12] = piece_x[1]; telem[13] = (u8)piece_y[1];
|
|
581
|
+
telem[14] = (u8)hiscore; telem[15] = (u8)(hiscore >> 8);
|
|
582
|
+
telem[16] = garb_rows[0]; telem[17] = garb_rows[1];
|
|
583
|
+
telem[18] = (u8)(piece_col[0][0] | (piece_col[0][1] << 2) | (piece_col[0][2] << 4));
|
|
584
|
+
telem[19] = (u8)(piece_col[1][0] | (piece_col[1][1] << 2) | (piece_col[1][2] << 4));
|
|
585
|
+
telem[20] = (u8)((u16)grid); telem[21] = (u8)((u16)grid >> 8);
|
|
586
|
+
telem[22] = (u8)cleared_total; telem[23] = (u8)(cleared_total >> 8);
|
|
139
587
|
}
|
|
140
588
|
|
|
141
589
|
int main(void) {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
u16 i;
|
|
145
|
-
u8 t;
|
|
146
|
-
|
|
147
|
-
consoleSetTextMapPtr(0x6800);
|
|
148
|
-
consoleSetTextGfxPtr(0x3000);
|
|
149
|
-
consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
|
|
150
|
-
consoleInitText(0, 16 * 2, &tilfont, &palfont);
|
|
151
|
-
setMode(BG_MODE1, 0);
|
|
152
|
-
/* consoleInitText DMAs the font but does NOT set the PPU BG base
|
|
153
|
-
* registers — point BG0 at the same font ($3000) + map ($6800). */
|
|
154
|
-
bgSetGfxPtr(0, 0x3000);
|
|
155
|
-
bgSetMapPtr(0, 0x6800, SC_32x32);
|
|
156
|
-
|
|
157
|
-
/* BG1 = full-screen wallpaper so the playfield never reads as blank.
|
|
158
|
-
* Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
|
|
159
|
-
* the console gfx $3000 / map $6800). Map entries use palette block 1
|
|
160
|
-
* (0x0400) so the wallpaper palette doesn't disturb the console font
|
|
161
|
-
* palette in block 0 (HUD/grid text stays legible). */
|
|
162
|
-
bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
|
|
163
|
-
32, 32, BG_16COLORS, 0x2000);
|
|
164
|
-
for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
|
|
165
|
-
bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
|
|
166
|
-
bgSetEnable(1);
|
|
167
|
-
bgSetDisable(2);
|
|
168
|
-
|
|
169
|
-
for (r = 0; r < ROWS; r++)
|
|
170
|
-
for (c = 0; c < COLS; c++)
|
|
171
|
-
grid[r][c] = 0;
|
|
172
|
-
|
|
173
|
-
score = 0;
|
|
174
|
-
fall_timer = 0;
|
|
175
|
-
new_piece();
|
|
176
|
-
|
|
177
|
-
consoleDrawText(14, 2, "SCORE");
|
|
178
|
-
consoleDrawText(2, 26, "LR MOVE A ROT START DROP");
|
|
179
|
-
draw_grid();
|
|
180
|
-
|
|
181
|
-
/* Screen ON first, THEN sound. sfx_init() must run AFTER setScreenOn()
|
|
182
|
-
* (snes_sfx.h:63) — if the SPC stalls before the screen is on you get a
|
|
183
|
-
* black/forced-blank screen forever. */
|
|
184
|
-
setScreenOn();
|
|
185
|
-
sfx_init();
|
|
186
|
-
|
|
187
|
-
while (1) {
|
|
188
|
-
pad = padsCurrent(0);
|
|
189
|
-
draw_piece(1);
|
|
190
|
-
|
|
191
|
-
if ((pad & KEY_LEFT) && !(prev & KEY_LEFT)
|
|
192
|
-
&& !collides(piece_x - 1, piece_y)) piece_x--;
|
|
193
|
-
if ((pad & KEY_RIGHT) && !(prev & KEY_RIGHT)
|
|
194
|
-
&& !collides(piece_x + 1, piece_y)) piece_x++;
|
|
195
|
-
if ((pad & KEY_A) && !(prev & KEY_A)) {
|
|
196
|
-
t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
|
|
197
|
-
sfx_play(1); /* rotate click */
|
|
198
|
-
}
|
|
199
|
-
if ((pad & KEY_START) && !(prev & KEY_START)) {
|
|
200
|
-
while (!collides(piece_x, piece_y + 1)) piece_y++;
|
|
201
|
-
lock_piece();
|
|
202
|
-
new_piece();
|
|
203
|
-
prev = pad;
|
|
204
|
-
render_score();
|
|
205
|
-
WaitForVBlank();
|
|
206
|
-
consoleVblank();
|
|
207
|
-
continue;
|
|
208
|
-
}
|
|
209
|
-
prev = pad;
|
|
210
|
-
|
|
211
|
-
fall_rate = (pad & KEY_DOWN) ? 4 : 30;
|
|
212
|
-
if (++fall_timer >= fall_rate) {
|
|
213
|
-
fall_timer = 0;
|
|
214
|
-
if (collides(piece_x, piece_y + 1)) {
|
|
215
|
-
lock_piece();
|
|
216
|
-
new_piece();
|
|
217
|
-
} else {
|
|
218
|
-
piece_y++;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
590
|
+
u16 pad, newp;
|
|
591
|
+
u8 i;
|
|
221
592
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
593
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
594
|
+
* Init order: console text pointers FIRST, then mode, then VRAM uploads
|
|
595
|
+
* while the screen is still off (forced blank = unrestricted VRAM access;
|
|
596
|
+
* once the screen is on, only the vblank DMA path below may touch VRAM).
|
|
597
|
+
* consoleInitText DMAs the font but does NOT set the PPU BG base registers
|
|
598
|
+
* — point BG1 at the same font/map yourself. */
|
|
599
|
+
consoleSetTextMapPtr(0x6800);
|
|
600
|
+
consoleSetTextGfxPtr(0x3000);
|
|
601
|
+
consoleSetTextOffset(0x0000);
|
|
602
|
+
consoleInitText(0, 16 * 2, &tilfont, &palfont);
|
|
603
|
+
setMode(BG_MODE1, 0);
|
|
604
|
+
bgSetGfxPtr(0, 0x3000);
|
|
605
|
+
bgSetMapPtr(0, 0x6800, SC_32x32);
|
|
606
|
+
|
|
607
|
+
/* BG2 = the board layer: 8-tile set → VRAM $2000, palette → CGRAM block 1
|
|
608
|
+
* (map entries carry MAP_PAL1 so the console font palette in block 0 stays
|
|
609
|
+
* untouched), shadow map → VRAM $4000. */
|
|
610
|
+
bgInitTileSet(1, (u8 *)&tilboard, (u8 *)&palboard, 1,
|
|
611
|
+
256, 32, BG_16COLORS, 0x2000);
|
|
612
|
+
paint_title_map();
|
|
613
|
+
bgInitMapSet(1, (u8 *)board_map, sizeof(board_map), SC_32x32, 0x4000);
|
|
614
|
+
bgSetEnable(1);
|
|
615
|
+
bgSetDisable(2); /* BG3 carries garbage in mode 1 */
|
|
616
|
+
setPaletteColor(0, RGB5(2, 2, 6)); /* backdrop: near-black indigo */
|
|
617
|
+
|
|
618
|
+
/* OBJ: the SAME 8 board tiles → OBJ base $6000 + palette → OBJ pal 0, so
|
|
619
|
+
* falling jewels match locked jewels exactly. 8x8 sprites (OBJ_SIZE8_L16,
|
|
620
|
+
* size bit stays small). */
|
|
621
|
+
oamInitGfxSet((u8 *)&tilboard, 256, (u8 *)&palboard, 32, 0, 0x6000,
|
|
622
|
+
OBJ_SIZE8_L16);
|
|
623
|
+
for (i = 0; i < 6; i++) oamSet((u8)(i << 2), 0, 240, 3, 0, 0, 0, 0);
|
|
624
|
+
|
|
625
|
+
setScreenOn();
|
|
626
|
+
|
|
627
|
+
/* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
|
|
628
|
+
* the return: a wedged SPC700 must not take the video down with it. ── */
|
|
629
|
+
sound_ok = (sfx_init() == 0);
|
|
630
|
+
/* ── HARDWARE IDIOM (load-bearing) — one frame between init and the first
|
|
631
|
+
* command. sfx_init returns the instant the SPC echoes the jump command,
|
|
632
|
+
* but the driver then spends ~50 port writes initialising the DSP BEFORE
|
|
633
|
+
* it seeds its command edge-detector from $2140. Send a command in that
|
|
634
|
+
* window and the seed swallows it — music silently never starts. A
|
|
635
|
+
* WaitForVBlank is thousands of SPC cycles — deterministic cure. ── */
|
|
636
|
+
WaitForVBlank();
|
|
637
|
+
if (sound_ok) sfx_music_play();
|
|
638
|
+
|
|
639
|
+
hiscore = hi_load(); /* battery SRAM — 0 on first boot */
|
|
640
|
+
title_enter();
|
|
641
|
+
|
|
642
|
+
while (1) {
|
|
643
|
+
pad = padsCurrent(0);
|
|
644
|
+
newp = pad & (u16)~prev_pad0;
|
|
645
|
+
prev_pad0 = pad;
|
|
646
|
+
|
|
647
|
+
if (state == ST_TITLE) {
|
|
648
|
+
/* ── GAME LOGIC (clay) — title: A/START = 1P, B = 2P versus; the jewel
|
|
649
|
+
* stripe cycles its hues (board_map is live every frame — free juice) */
|
|
650
|
+
if ((frames & 31) == 0) paint_title_stripe((u8)(frames >> 5));
|
|
651
|
+
if (newp & (KEY_A | KEY_START)) start_game(0);
|
|
652
|
+
else if (newp & KEY_B) start_game(1);
|
|
653
|
+
} else if (state == ST_PLAY) {
|
|
654
|
+
/* ── GAME LOGIC (clay — reshape freely) ── */
|
|
655
|
+
update_player(0);
|
|
656
|
+
if (two_player && state == ST_PLAY) update_player(1);
|
|
657
|
+
if (board_dirty[0]) { paint_board(0); board_dirty[0] = 0; }
|
|
658
|
+
if (board_dirty[1]) { paint_board(1); board_dirty[1] = 0; }
|
|
659
|
+
} else { /* ST_OVER — boards stay frozen on screen */
|
|
660
|
+
if (newp & (KEY_START | KEY_A)) title_enter();
|
|
226
661
|
}
|
|
227
|
-
|
|
662
|
+
|
|
663
|
+
stage_pieces(); /* sprites staged BEFORE the vblank wait */
|
|
664
|
+
telem_update();
|
|
665
|
+
frames++;
|
|
666
|
+
oamUpdate();
|
|
667
|
+
|
|
668
|
+
WaitForVBlank();
|
|
669
|
+
/* vblank-only writes — FIRST after the wait: the full-board DMA (see the
|
|
670
|
+
* shadow-map idiom above + the NES-contrast note in the header). */
|
|
671
|
+
dmaCopyVram((u8 *)board_map, 0x4000, sizeof(board_map));
|
|
672
|
+
consoleVblank();
|
|
673
|
+
}
|
|
674
|
+
return 0;
|
|
228
675
|
}
|