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,106 +1,154 @@
|
|
|
1
|
-
/* ── puzzle.c — NES
|
|
1
|
+
/* ── puzzle.c — NES falling-gem versus puzzle (complete example game) ─────────
|
|
2
2
|
*
|
|
3
|
-
* A
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - d-pad LEFT/RIGHT moves the piece, DOWN soft-drops, A rotates
|
|
8
|
-
* (cycles the 3 colors of the active piece)
|
|
9
|
-
* - Auto-fall every 30 frames (~0.5s)
|
|
10
|
-
* - Match-3 horizontal detection — clears horizontally aligned
|
|
11
|
-
* triples of the same color. Add vertical + diagonal as exercise.
|
|
3
|
+
* A COMPLETE, working game — title screen, 1P marathon and 2P simultaneous
|
|
4
|
+
* VERSUS modes, levels, score + persistent hi-score (battery SRAM), music +
|
|
5
|
+
* SFX, and a background-tile playfield driven through the queued VRAM path
|
|
6
|
+
* (the load-bearing trick of every NES puzzle game).
|
|
12
7
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
8
|
+
* The game: a falling-trio match-3. A trio of gems falls into a 6x12 well; LEFT/RIGHT
|
|
9
|
+
* move it, A/B cycle its three colours, DOWN soft-drops. When it lands, any
|
|
10
|
+
* straight run of 3+ same-coloured gems (horizontal, vertical, or diagonal)
|
|
11
|
+
* clears; survivors fall and cascades chain for multiplied score.
|
|
12
|
+
*
|
|
13
|
+
* 2P VERSUS design (simultaneous, split board): two 6x12 wells side by side —
|
|
14
|
+
* P1 left, P2 right — each driven by its own controller, both falling at
|
|
15
|
+
* once. Clears ATTACK: every chain step you score sends one garbage row
|
|
16
|
+
* (random gems with one gap, capped at 4 per attack) rising from the bottom
|
|
17
|
+
* of the opponent's well. First player whose stack reaches the top loses.
|
|
18
|
+
* Both update each frame; the whole thing fits the budget because the boards
|
|
19
|
+
* are background tiles and only the two falling trios are sprites (6 OAM
|
|
20
|
+
* entries total).
|
|
21
|
+
*
|
|
22
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
23
|
+
* very different one. The markers tell you what's what:
|
|
24
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented NES footgun; reshape
|
|
25
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
26
|
+
* GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
|
|
27
|
+
*
|
|
28
|
+
* What depends on what:
|
|
29
|
+
* nes_runtime.{h,c} — rendering/input/sound/text/hi-score library.
|
|
30
|
+
* chr-ram-runtime.crt0.s — boot + NMI + iNES header (BATTERY bit feeds
|
|
31
|
+
* hiscore_load/save). Load-bearing; edit with TROUBLESHOOTING open.
|
|
32
|
+
*
|
|
33
|
+
* Frame budget (NTSC, 60fps): steady state is tiny — input + gravity for two
|
|
34
|
+
* pieces, ≤6 sprites, ≤11 queued VRAM bytes (one board row + one HUD number).
|
|
35
|
+
* The spike is resolve_board() at lock time (full 4-direction match scan over
|
|
36
|
+
* 72 cells in cc65 code): it can spill a frame or two past vblank. That's
|
|
37
|
+
* fine — the NMI keeps rendering and the queue keeps draining, so it shows
|
|
38
|
+
* as (at most) a one-frame hitch on the falling pieces, never corruption.
|
|
20
39
|
*/
|
|
21
40
|
|
|
22
41
|
#include "nes_runtime.h"
|
|
23
42
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
#define
|
|
27
|
-
#define ORIGIN_Y 16 /* px — top row anchor */
|
|
28
|
-
#define CELL_PX 8 /* one tile per cell */
|
|
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 "GEM DUEL"
|
|
29
46
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
#define
|
|
47
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
48
|
+
* Board geometry. The wells are placed on EVEN tile coordinates on purpose —
|
|
49
|
+
* see the attribute-table idiom below before moving them. */
|
|
50
|
+
#define GRID_W 6
|
|
51
|
+
#define GRID_H 12
|
|
52
|
+
#define WELL_TY 8 /* top tile row of the well interior */
|
|
53
|
+
#define WELL_1P_TX 12 /* 1P: single centered well (cols 12-17) */
|
|
54
|
+
#define WELL_VS_P1 4 /* 2P: P1 well cols 4-9 ... */
|
|
55
|
+
#define WELL_VS_P2 22 /* P2 well cols 22-27 (split board) */
|
|
34
56
|
|
|
35
|
-
#define
|
|
36
|
-
#define TILE_BLOCK_G 2
|
|
37
|
-
#define TILE_BLOCK_B 3
|
|
57
|
+
#define EMPTY 0 /* cell colours 1..3 = white/green/red */
|
|
38
58
|
|
|
39
|
-
/* ──
|
|
59
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
60
|
+
* Tile art. Each 8x8 tile = 16 bytes: 8 plane-0 rows then 8 plane-1 rows
|
|
61
|
+
* (2bpp — plane0-only pixels use colour 1, plane1-only colour 2, both = 3).
|
|
62
|
+
* KEY TRICK: the three gem tiles are the SAME shape on different planes, so
|
|
63
|
+
* a cell changes colour by changing its TILE — no attribute-table rewrite
|
|
64
|
+
* (attributes cover 16x16 px, way coarser than one 8x8 cell). */
|
|
40
65
|
static const uint8_t tile_blank[16] = { 0 };
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
* plane1=1 → idx 2 (G), both planes set → idx 3 (B). */
|
|
44
|
-
static const uint8_t tile_block_r[16] = {
|
|
45
|
-
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
66
|
+
static const uint8_t tile_gem1[16] = { /* colour 1 (white) */
|
|
67
|
+
0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
|
|
46
68
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
47
69
|
};
|
|
48
|
-
static const uint8_t
|
|
70
|
+
static const uint8_t tile_gem2[16] = { /* colour 2 (green) */
|
|
49
71
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
50
|
-
|
|
72
|
+
0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
|
|
51
73
|
};
|
|
52
|
-
static const uint8_t
|
|
53
|
-
|
|
54
|
-
|
|
74
|
+
static const uint8_t tile_gem3[16] = { /* colour 3 (red) */
|
|
75
|
+
0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
|
|
76
|
+
0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
|
|
55
77
|
};
|
|
56
|
-
/* BG
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
* BG_WALL — solid bordered block (idx 1, steel blue): the well frame.
|
|
62
|
-
* BG_BRICK — a brick/dither pattern (idx 2) tiling the cabinet wall that
|
|
63
|
-
* surrounds the well, so the whole screen is covered.
|
|
64
|
-
* BG_INNER — a faint grid speck (idx 3) lining the inside of the well so
|
|
65
|
-
* empty cells read as a recessed playfield, not a black hole. */
|
|
66
|
-
static const uint8_t tile_wall[16] = {
|
|
67
|
-
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
68
|
-
0, 0, 0, 0, 0, 0, 0, 0,
|
|
78
|
+
/* BG furniture (background pattern table $1000 — separate from the sprite
|
|
79
|
+
* table at $0000; the runtime's PPUCTRL setup makes that split). */
|
|
80
|
+
static const uint8_t tile_wall[16] = { /* well frame, colour 3 */
|
|
81
|
+
0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF,
|
|
82
|
+
0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF,
|
|
69
83
|
};
|
|
70
|
-
static const uint8_t
|
|
71
|
-
0, 0, 0, 0, 0, 0, 0, 0,
|
|
72
|
-
|
|
84
|
+
static const uint8_t tile_dither[16] = { /* cabinet backdrop, colour 2 */
|
|
85
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
86
|
+
0x55, 0x00, 0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00,
|
|
73
87
|
};
|
|
74
|
-
static const uint8_t tile_inner[16] = {
|
|
75
|
-
|
|
76
|
-
|
|
88
|
+
static const uint8_t tile_inner[16] = { /* empty-cell speck, colour 1 */
|
|
89
|
+
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
|
|
90
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
77
91
|
};
|
|
78
|
-
#define BG_WALL
|
|
79
|
-
#define
|
|
80
|
-
#define BG_INNER
|
|
92
|
+
#define BG_WALL 1
|
|
93
|
+
#define BG_DITHER 2
|
|
94
|
+
#define BG_INNER 3
|
|
95
|
+
#define BG_GEM_BASE 4 /* BG tiles 4/5/6 = gem colours 1/2/3 */
|
|
81
96
|
|
|
82
97
|
static const uint8_t palette[32] = {
|
|
83
|
-
/*
|
|
84
|
-
*
|
|
85
|
-
|
|
86
|
-
0x0F,
|
|
87
|
-
0x0F,
|
|
88
|
-
0x0F,
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
0x0F,
|
|
92
|
-
0x0F,
|
|
93
|
-
0x0F,
|
|
98
|
+
/* BG pal 0 = WELL INTERIOR: gem colours (white/green/red on black).
|
|
99
|
+
* BG pal 1 = everything else: white text, dark-grey dither, blue frame.
|
|
100
|
+
* The attribute table below assigns pal 0 to the wells, pal 1 elsewhere. */
|
|
101
|
+
0x0F, 0x30, 0x2A, 0x16,
|
|
102
|
+
0x0F, 0x30, 0x00, 0x11,
|
|
103
|
+
0x0F, 0x30, 0x00, 0x11,
|
|
104
|
+
0x0F, 0x30, 0x00, 0x11,
|
|
105
|
+
/* Sprite pal 0 mirrors BG pal 0 so the falling trio matches locked gems. */
|
|
106
|
+
0x0F, 0x30, 0x2A, 0x16,
|
|
107
|
+
0x0F, 0x30, 0x2A, 0x16,
|
|
108
|
+
0x0F, 0x30, 0x2A, 0x16,
|
|
109
|
+
0x0F, 0x30, 0x2A, 0x16,
|
|
94
110
|
};
|
|
95
111
|
|
|
96
|
-
/* ──
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
112
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
113
|
+
* Big arrays live OUTSIDE the linker's RAM area. The chr-ram-runtime preset
|
|
114
|
+
* places C BSS in $0300-$04FF (512 bytes), and the runtime's own statics
|
|
115
|
+
* (256-byte shadow attribute table + VRAM queue) already eat most of it —
|
|
116
|
+
* two 72-byte boards plus a 72-byte match mask would overflow the segment
|
|
117
|
+
* and the LINK fails. The preset reserves $0500-$05FF as the USER SCRATCH
|
|
118
|
+
* PAGE for exactly this (the linker never places anything there); these
|
|
119
|
+
* three arrays use 216 of its 256 bytes. DO NOT stray past $05FF: the cc65
|
|
120
|
+
* C parameter stack owns $0600-$06FF and the music driver's scratch page
|
|
121
|
+
* is $0700-$07FF — writes there corrupt live state silently. Bonus: fixed
|
|
122
|
+
* addresses make the boards trivially inspectable from the debugger
|
|
123
|
+
* (P1 board at $0500, P2 at $0548, match mask at $0590). */
|
|
124
|
+
#define grid_of(p) ((uint8_t (*)[GRID_W])((p) ? 0x0548 : 0x0500))
|
|
125
|
+
#define matched ((uint8_t (*)[GRID_W])0x0590)
|
|
126
|
+
|
|
127
|
+
/* ── GAME LOGIC (clay — reshape freely) ── small state (fits normal BSS). */
|
|
128
|
+
#define ST_TITLE 0
|
|
129
|
+
#define ST_PLAY 1
|
|
130
|
+
#define ST_OVER 2
|
|
131
|
+
static uint8_t state;
|
|
132
|
+
static uint8_t two_player; /* mode chosen on the title screen */
|
|
133
|
+
static uint8_t well_tx[2]; /* left tile column of each well */
|
|
134
|
+
static uint8_t piece_x[2]; /* falling trio: column 0..5 */
|
|
135
|
+
static int8_t piece_y[2]; /* row of its TOP cell (<0 = above rim) */
|
|
136
|
+
static uint8_t piece_col[2][3]; /* trio colours, top to bottom */
|
|
137
|
+
static uint8_t fall_t[2]; /* frames until next gravity step */
|
|
138
|
+
static uint8_t prev_pad[2]; /* for edge-triggered input */
|
|
139
|
+
static uint16_t score[2];
|
|
140
|
+
static uint16_t hiscore;
|
|
141
|
+
static uint16_t cleared_total; /* 1P: gems cleared, drives the level */
|
|
142
|
+
static uint8_t level; /* 1P: 1..9, speeds up the fall */
|
|
143
|
+
static uint16_t dirty_rows[2]; /* bitmask: board rows needing repaint */
|
|
144
|
+
static uint8_t hud_dirty[2]; /* score (or level) number needs redraw */
|
|
145
|
+
static uint8_t drain_turn; /* which player's row repaints this frame */
|
|
146
|
+
static uint16_t rng = 0xACE1;
|
|
103
147
|
|
|
148
|
+
#define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
|
|
149
|
+
#define GARBAGE_CAP 4 /* max garbage rows per attack */
|
|
150
|
+
|
|
151
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
104
152
|
static uint8_t random8(void) {
|
|
105
153
|
uint16_t r = rng;
|
|
106
154
|
r ^= r << 7;
|
|
@@ -110,61 +158,93 @@ static uint8_t random8(void) {
|
|
|
110
158
|
return (uint8_t)r;
|
|
111
159
|
}
|
|
112
160
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
161
|
+
/* cell colour → BG tile (empty cells show the faint speck, not raw black,
|
|
162
|
+
* so the well reads as a recessed playfield). */
|
|
163
|
+
static uint8_t bg_tile_for(uint8_t col) {
|
|
164
|
+
return col ? (uint8_t)(BG_GEM_BASE - 1 + col) : BG_INNER;
|
|
117
165
|
}
|
|
118
166
|
|
|
119
|
-
static
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
default: return 0;
|
|
125
|
-
}
|
|
167
|
+
static void mark_row_dirty(uint8_t p, int8_t r) {
|
|
168
|
+
if (r >= 0 && r < GRID_H) dirty_rows[p] |= (uint16_t)1 << r;
|
|
169
|
+
}
|
|
170
|
+
static void mark_all_dirty(uint8_t p) {
|
|
171
|
+
dirty_rows[p] = 0x0FFF; /* all 12 rows */
|
|
126
172
|
}
|
|
127
173
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
174
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
175
|
+
* The board is BACKGROUND TILES, updated only through the QUEUED path
|
|
176
|
+
* (tile_set / text_draw). The NMI drains at most 16 queue entries per
|
|
177
|
+
* vblank — that is the entire write bandwidth you get while rendering is on.
|
|
178
|
+
* NEVER vram_unsafe_set while rendering: raw $2007 traffic mid-frame
|
|
179
|
+
* corrupts the PPU address latch and shears the screen.
|
|
180
|
+
*
|
|
181
|
+
* So repaints are BUDGETED: board changes mark rows dirty, and this drainer
|
|
182
|
+
* repaints ONE row (6 cells) + ONE HUD number (5 digits) per frame — 11
|
|
183
|
+
* entries, safely inside the 16. A full-board repaint (cascade + gravity)
|
|
184
|
+
* spreads over up to 12 frames per player (~0.2s) — you SEE the well sweep
|
|
185
|
+
* top-to-bottom, which puzzle players read as a clear animation. Free juice.
|
|
186
|
+
* (Overflowing the queue doesn't corrupt anything — tile_set blocks until
|
|
187
|
+
* the NMI drains a slot — but every blocked push silently costs a whole
|
|
188
|
+
* frame, so a naive 72-cell repaint would freeze the game for ~4 frames.) */
|
|
189
|
+
static void drain_vram_budget(void) {
|
|
190
|
+
uint8_t p, r, c;
|
|
191
|
+
uint8_t (*g)[GRID_W];
|
|
192
|
+
/* One dirty board row, alternating players so neither well starves. */
|
|
193
|
+
p = drain_turn;
|
|
194
|
+
drain_turn ^= 1;
|
|
195
|
+
if (!dirty_rows[p]) p ^= 1;
|
|
196
|
+
if (dirty_rows[p]) {
|
|
197
|
+
g = grid_of(p);
|
|
198
|
+
for (r = 0; r < GRID_H; r++) {
|
|
199
|
+
if (dirty_rows[p] & ((uint16_t)1 << r)) {
|
|
200
|
+
for (c = 0; c < GRID_W; c++)
|
|
201
|
+
tile_set(0, (uint8_t)(well_tx[p] + c), (uint8_t)(WELL_TY + r),
|
|
202
|
+
bg_tile_for(g[r][c]));
|
|
203
|
+
dirty_rows[p] &= (uint16_t)~((uint16_t)1 << r);
|
|
204
|
+
break; /* one row per frame — that's the budget */
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
/* One HUD number per frame. HUD LAYOUT RULE (overscan): nametable row 0
|
|
209
|
+
* is cropped on NTSC — all HUD text sits on rows 1-2, never row 0. */
|
|
210
|
+
if (hud_dirty[0]) {
|
|
211
|
+
text_draw_u16(0, 2, 2, score[0]);
|
|
212
|
+
hud_dirty[0] = 0;
|
|
213
|
+
} else if (hud_dirty[1]) {
|
|
214
|
+
if (two_player) text_draw_u16(0, 22, 2, score[1]);
|
|
215
|
+
else text_draw_u16(0, 22, 2, level);
|
|
216
|
+
hud_dirty[1] = 0;
|
|
217
|
+
}
|
|
132
218
|
}
|
|
133
219
|
|
|
134
|
-
/*
|
|
135
|
-
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
*
|
|
139
|
-
* 4 directions, clears them, applies per-column gravity, and loops so
|
|
140
|
-
* cascades chain (score scales with chain depth). */
|
|
141
|
-
/* matched[] lives at $0500 — OUTSIDE the linker's RAM area ($0300-$04FF,
|
|
142
|
-
* which grid+runtime statics nearly fill; 72 more BSS bytes overflow it).
|
|
143
|
-
* $0500-$07FF is real, unused NES work RAM (hw stack is $0100, shadow OAM
|
|
144
|
-
* $0200), so an absolute pointer there is free. */
|
|
145
|
-
#define matched ((uint8_t (*)[GRID_W])0x0500)
|
|
220
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
221
|
+
* Match scan: mark every straight run of 3+ same-coloured gems in all 4
|
|
222
|
+
* directions (a cell can belong to several runs — the mask de-dupes), and
|
|
223
|
+
* return how many cells matched. This is the resolve-time spike the header's
|
|
224
|
+
* frame-budget note talks about. */
|
|
146
225
|
static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
147
226
|
|
|
148
|
-
static uint8_t mark_and_count(
|
|
149
|
-
uint8_t r, c, d, len, k, cnt;
|
|
150
|
-
uint8_t col;
|
|
227
|
+
static uint8_t mark_and_count(uint8_t p) {
|
|
228
|
+
uint8_t r, c, d, len, k, cnt, col;
|
|
151
229
|
int8_t dr, dc;
|
|
152
230
|
int sr, sc;
|
|
231
|
+
uint8_t (*g)[GRID_W] = grid_of(p);
|
|
153
232
|
cnt = 0;
|
|
154
|
-
for (r = 0; r < GRID_H; r++)
|
|
233
|
+
for (r = 0; r < GRID_H; r++)
|
|
234
|
+
for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
|
|
155
235
|
for (r = 0; r < GRID_H; r++) {
|
|
156
236
|
for (c = 0; c < GRID_W; c++) {
|
|
157
|
-
col =
|
|
237
|
+
col = g[r][c];
|
|
158
238
|
if (col == EMPTY) continue;
|
|
159
239
|
for (d = 0; d < 4; d++) {
|
|
160
240
|
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
161
241
|
sr = (int)r - dr; sc = (int)c - dc;
|
|
162
242
|
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
163
|
-
&&
|
|
243
|
+
&& g[sr][sc] == col) continue; /* not the run's start */
|
|
164
244
|
len = 1;
|
|
165
245
|
sr = (int)r + dr; sc = (int)c + dc;
|
|
166
246
|
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
167
|
-
&&
|
|
247
|
+
&& g[sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
168
248
|
if (len >= 3) {
|
|
169
249
|
sr = r; sc = c;
|
|
170
250
|
for (k = 0; k < len; k++) {
|
|
@@ -178,176 +258,417 @@ static uint8_t mark_and_count(void) {
|
|
|
178
258
|
return cnt;
|
|
179
259
|
}
|
|
180
260
|
|
|
181
|
-
/*
|
|
182
|
-
*
|
|
183
|
-
static void apply_gravity(
|
|
261
|
+
/* Collapse each column so survivors rest on the floor (walk from the bottom,
|
|
262
|
+
* copying gems down to a write cursor, then zero everything above it). */
|
|
263
|
+
static void apply_gravity(uint8_t p) {
|
|
184
264
|
uint8_t c;
|
|
185
|
-
|
|
265
|
+
int8_t r, w;
|
|
266
|
+
uint8_t (*g)[GRID_W] = grid_of(p);
|
|
186
267
|
for (c = 0; c < GRID_W; c++) {
|
|
187
268
|
w = GRID_H - 1;
|
|
188
269
|
for (r = GRID_H - 1; r >= 0; r--) {
|
|
189
|
-
if (
|
|
270
|
+
if (g[r][c] != EMPTY) { g[w][c] = g[r][c]; w--; }
|
|
190
271
|
}
|
|
191
|
-
for (; w >= 0; w--)
|
|
272
|
+
for (; w >= 0; w--) g[w][c] = EMPTY;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/* ── GAME LOGIC (clay) — end of game (top-out). `loser` topped out. ── */
|
|
277
|
+
static void game_end(uint8_t loser) {
|
|
278
|
+
uint16_t best = score[0];
|
|
279
|
+
if (two_player && score[1] > best) best = score[1];
|
|
280
|
+
if (best > hiscore) {
|
|
281
|
+
hiscore = best;
|
|
282
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
283
|
+
* Persists via battery PRG-RAM at $6000; works because the crt0's iNES
|
|
284
|
+
* header sets the BATTERY bit. See nes_runtime.c for the magic+checksum
|
|
285
|
+
* layout (first boot reads garbage — the checksum rejects it). ── */
|
|
286
|
+
hiscore_save(hiscore);
|
|
192
287
|
}
|
|
288
|
+
sound_play_noise(8, 12, 16); /* game-over rumble */
|
|
289
|
+
if (two_player) text_draw(0, 12, 22, loser ? "P1 WINS" : "P2 WINS");
|
|
290
|
+
else text_draw(0, 11, 22, "GAME OVER");
|
|
291
|
+
text_draw(0, 9, 24, "START - TITLE");
|
|
292
|
+
prev_pad[0] = 0xFF; /* require a fresh press */
|
|
293
|
+
state = ST_OVER;
|
|
193
294
|
}
|
|
194
295
|
|
|
195
|
-
|
|
296
|
+
/* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
|
|
297
|
+
* Returns the chain depth (0 = the lock matched nothing). Score and repaints
|
|
298
|
+
* happen here; the actual VRAM writes trickle out via drain_vram_budget. */
|
|
299
|
+
static uint8_t resolve_board(uint8_t p) {
|
|
196
300
|
uint8_t n, r, c, chain;
|
|
197
|
-
|
|
301
|
+
uint16_t amt;
|
|
302
|
+
uint8_t (*g)[GRID_W] = grid_of(p);
|
|
198
303
|
chain = 0;
|
|
199
|
-
|
|
200
|
-
n = mark_and_count();
|
|
304
|
+
for (;;) {
|
|
305
|
+
n = mark_and_count(p);
|
|
201
306
|
if (n == 0) break;
|
|
202
|
-
chain
|
|
307
|
+
++chain;
|
|
203
308
|
for (r = 0; r < GRID_H; r++)
|
|
204
309
|
for (c = 0; c < GRID_W; c++)
|
|
205
|
-
if (matched[r][c])
|
|
206
|
-
amt = (
|
|
207
|
-
if (chain > 1) amt
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
310
|
+
if (matched[r][c]) g[r][c] = EMPTY;
|
|
311
|
+
amt = (uint16_t)n * 10;
|
|
312
|
+
if (chain > 1) amt *= chain; /* cascades pay multiplied */
|
|
313
|
+
score[p] += amt;
|
|
314
|
+
hud_dirty[p] = 1;
|
|
315
|
+
/* clear chime — rises with chain depth */
|
|
316
|
+
sound_play_tone(0, (uint16_t)(0x120 - ((uint16_t)chain << 4)), 8, 8);
|
|
317
|
+
apply_gravity(p);
|
|
318
|
+
mark_all_dirty(p); /* gravity moved everything */
|
|
319
|
+
if (!two_player) {
|
|
320
|
+
cleared_total += n;
|
|
321
|
+
while (level < 9 && cleared_total >= (uint16_t)level * 10) {
|
|
322
|
+
++level;
|
|
323
|
+
hud_dirty[1] = 1; /* 1P: slot 1 shows the level */
|
|
324
|
+
}
|
|
325
|
+
}
|
|
211
326
|
}
|
|
327
|
+
return chain;
|
|
212
328
|
}
|
|
213
329
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
330
|
+
/* ── GAME LOGIC (clay) — VERSUS attack: garbage rows rise from the bottom of
|
|
331
|
+
* the victim's well (random gems with one gap — matchable, so a skilled
|
|
332
|
+
* victim digs out). The victim's stack rising into row <0 territory means
|
|
333
|
+
* the falling trio shifts up one to stay aligned; if the rim row is already
|
|
334
|
+
* occupied, the victim tops out and loses. ── */
|
|
335
|
+
static void garbage_insert(uint8_t v, uint8_t nrows) {
|
|
336
|
+
uint8_t k, c, gap;
|
|
337
|
+
int8_t r;
|
|
338
|
+
uint8_t (*g)[GRID_W] = grid_of(v);
|
|
339
|
+
sound_play_noise(10, 8, 8); /* incoming-garbage thud */
|
|
340
|
+
for (k = 0; k < nrows; k++) {
|
|
341
|
+
for (c = 0; c < GRID_W; c++) {
|
|
342
|
+
if (g[0][c] != EMPTY) { game_end(v); return; }
|
|
221
343
|
}
|
|
344
|
+
for (r = 0; r < GRID_H - 1; r++)
|
|
345
|
+
for (c = 0; c < GRID_W; c++)
|
|
346
|
+
g[r][c] = g[r + 1][c];
|
|
347
|
+
gap = random8() % GRID_W;
|
|
348
|
+
for (c = 0; c < GRID_W; c++)
|
|
349
|
+
g[GRID_H - 1][c] = (c == gap) ? EMPTY : (uint8_t)(1 + random8() % 3);
|
|
350
|
+
if (piece_y[v] > -3) --piece_y[v]; /* keep the trio board-relative */
|
|
222
351
|
}
|
|
223
|
-
|
|
352
|
+
mark_all_dirty(v);
|
|
224
353
|
}
|
|
225
354
|
|
|
226
|
-
/* Can the
|
|
227
|
-
|
|
228
|
-
|
|
355
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
|
|
356
|
+
* (pieces enter from above); below the floor or on a gem is not. */
|
|
357
|
+
static uint8_t can_place(uint8_t p, int8_t x, int8_t y) {
|
|
358
|
+
int8_t i, cy;
|
|
359
|
+
uint8_t (*g)[GRID_W] = grid_of(p);
|
|
229
360
|
if (x < 0 || x >= GRID_W) return 0;
|
|
230
361
|
for (i = 0; i < 3; i++) {
|
|
231
|
-
|
|
232
|
-
if (cy < 0) continue;
|
|
233
|
-
if (cy >= GRID_H) return 0;
|
|
234
|
-
if (
|
|
362
|
+
cy = (int8_t)(y + i);
|
|
363
|
+
if (cy < 0) continue;
|
|
364
|
+
if (cy >= GRID_H) return 0;
|
|
365
|
+
if (g[cy][x] != EMPTY) return 0;
|
|
235
366
|
}
|
|
236
367
|
return 1;
|
|
237
368
|
}
|
|
238
369
|
|
|
239
|
-
void
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
chr_ram_upload(0x0020, tile_block_g, 16);
|
|
248
|
-
chr_ram_upload(0x0030, tile_block_b, 16);
|
|
249
|
-
chr_ram_upload(0x1010, tile_wall, 16); /* BG slot 1 (background table) */
|
|
250
|
-
chr_ram_upload(0x1020, tile_brick, 16); /* BG slot 2 (cabinet wall) */
|
|
251
|
-
chr_ram_upload(0x1030, tile_inner, 16); /* BG slot 3 (well interior) */
|
|
252
|
-
palette_load(palette);
|
|
370
|
+
static void spawn_piece(uint8_t p) {
|
|
371
|
+
piece_x[p] = GRID_W / 2;
|
|
372
|
+
piece_y[p] = -2;
|
|
373
|
+
piece_col[p][0] = (uint8_t)(1 + random8() % 3);
|
|
374
|
+
piece_col[p][1] = (uint8_t)(1 + random8() % 3);
|
|
375
|
+
piece_col[p][2] = (uint8_t)(1 + random8() % 3);
|
|
376
|
+
if (!can_place(p, (int8_t)piece_x[p], piece_y[p])) game_end(p);
|
|
377
|
+
}
|
|
253
378
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
uint16_t cc, rr;
|
|
265
|
-
/* whole-screen cabinet brick */
|
|
266
|
-
for (rr = 0; rr < 30; rr++)
|
|
267
|
-
for (cc = 0; cc < 32; cc++)
|
|
268
|
-
vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_BRICK);
|
|
269
|
-
/* recessed well interior (inside the frame) */
|
|
270
|
-
for (rr = gy0; rr < gy1; rr++)
|
|
271
|
-
for (cc = gx0; cc < gx1; cc++)
|
|
272
|
-
vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_INNER);
|
|
273
|
-
/* steel frame one cell out around the well */
|
|
274
|
-
for (cc = gx0 - 1; cc <= gx1; cc++) {
|
|
275
|
-
vram_unsafe_set((uint16_t)(0x2000 + (gy0 - 1) * 32 + cc), BG_WALL); /* top */
|
|
276
|
-
vram_unsafe_set((uint16_t)(0x2000 + gy1 * 32 + cc), BG_WALL); /* bottom */
|
|
379
|
+
/* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
|
|
380
|
+
static void lock_piece(uint8_t p) {
|
|
381
|
+
int8_t i, y;
|
|
382
|
+
uint8_t chain;
|
|
383
|
+
uint8_t (*g)[GRID_W] = grid_of(p);
|
|
384
|
+
for (i = 0; i < 3; i++) {
|
|
385
|
+
y = (int8_t)(piece_y[p] + i);
|
|
386
|
+
if (y >= 0) {
|
|
387
|
+
g[y][piece_x[p]] = piece_col[p][i];
|
|
388
|
+
mark_row_dirty(p, y);
|
|
277
389
|
}
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
390
|
+
}
|
|
391
|
+
sound_play_tone(1, 0x1C0, 4, 3); /* lock thunk */
|
|
392
|
+
if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
|
|
393
|
+
chain = resolve_board(p);
|
|
394
|
+
if (state != ST_PLAY) return;
|
|
395
|
+
if (chain && two_player) {
|
|
396
|
+
garbage_insert(p ^ 1, chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
|
|
397
|
+
if (state != ST_PLAY) return; /* garbage topped them out */
|
|
398
|
+
}
|
|
399
|
+
spawn_piece(p);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
|
|
403
|
+
* (one cell per press), held DOWN soft-drops. A/B cycle the trio's colours
|
|
404
|
+
* — the classic trio "rotate". ── */
|
|
405
|
+
static void update_player(uint8_t p) {
|
|
406
|
+
uint8_t pad, newp, fd, t;
|
|
407
|
+
pad = pad_poll(p);
|
|
408
|
+
newp = (uint8_t)(pad & (uint8_t)~prev_pad[p]);
|
|
409
|
+
prev_pad[p] = pad;
|
|
410
|
+
if ((newp & PAD_LEFT) && can_place(p, (int8_t)(piece_x[p] - 1), piece_y[p]))
|
|
411
|
+
--piece_x[p];
|
|
412
|
+
if ((newp & PAD_RIGHT) && can_place(p, (int8_t)(piece_x[p] + 1), piece_y[p]))
|
|
413
|
+
++piece_x[p];
|
|
414
|
+
if (newp & PAD_A) { /* cycle colours downward */
|
|
415
|
+
t = piece_col[p][2];
|
|
416
|
+
piece_col[p][2] = piece_col[p][1];
|
|
417
|
+
piece_col[p][1] = piece_col[p][0];
|
|
418
|
+
piece_col[p][0] = t;
|
|
419
|
+
sound_play_tone(1, 0x0A0, 3, 2);
|
|
420
|
+
}
|
|
421
|
+
if (newp & PAD_B) { /* cycle colours upward */
|
|
422
|
+
t = piece_col[p][0];
|
|
423
|
+
piece_col[p][0] = piece_col[p][1];
|
|
424
|
+
piece_col[p][1] = piece_col[p][2];
|
|
425
|
+
piece_col[p][2] = t;
|
|
426
|
+
sound_play_tone(1, 0x0C0, 3, 2);
|
|
427
|
+
}
|
|
428
|
+
if (pad & PAD_DOWN) fall_t[p] += 4; /* soft drop */
|
|
429
|
+
++fall_t[p];
|
|
430
|
+
fd = two_player ? VS_FALL_DELAY
|
|
431
|
+
: (uint8_t)(32 - ((level << 1) + level)); /* 29..5 */
|
|
432
|
+
if (fall_t[p] >= fd) {
|
|
433
|
+
fall_t[p] = 0;
|
|
434
|
+
if (can_place(p, (int8_t)piece_x[p], (int8_t)(piece_y[p] + 1)))
|
|
435
|
+
++piece_y[p];
|
|
436
|
+
else
|
|
437
|
+
lock_piece(p); /* may end the game */
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/* Stage the falling trio's sprites (board gems are BG tiles, NOT sprites —
|
|
442
|
+
* only what moves every frame earns OAM slots). */
|
|
443
|
+
static void stage_piece(uint8_t p) {
|
|
444
|
+
uint8_t i;
|
|
445
|
+
int8_t y;
|
|
446
|
+
for (i = 0; i < 3; i++) {
|
|
447
|
+
y = (int8_t)(piece_y[p] + i);
|
|
448
|
+
if (y >= 0)
|
|
449
|
+
oam_spr((uint8_t)((well_tx[p] + piece_x[p]) << 3),
|
|
450
|
+
(uint8_t)((WELL_TY + (uint8_t)y) << 3),
|
|
451
|
+
piece_col[p][i], 0);
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
/* 5-digit number with the PPU off (the queued text_draw_u16 would deadlock
|
|
456
|
+
* before rendering is enabled — same rule as text_draw_unsafe). */
|
|
457
|
+
static void text_u16_unsafe(uint16_t addr, uint16_t v) {
|
|
458
|
+
uint8_t d[5], i;
|
|
459
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
460
|
+
for (i = 0; i < 5; i++)
|
|
461
|
+
vram_unsafe_set((uint16_t)(addr + i), (uint8_t)(0x40 + d[4 - i]));
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
465
|
+
* Attribute table = palette per 16x16-PIXEL area (one 2-bit quadrant per
|
|
466
|
+
* 2x2 TILES). The wells use BG palette 0 (gem colours) and everything else
|
|
467
|
+
* palette 1 (text/frame/dither) — which only works because the wells are
|
|
468
|
+
* aligned to EVEN tile coordinates (WELL_TY=8; well columns 4/12/22), so
|
|
469
|
+
* every attribute quadrant is fully inside or fully outside a well. Move a
|
|
470
|
+
* well to an odd column and its edge quadrants straddle the boundary —
|
|
471
|
+
* half-recoloured gems. Keep wells 2-aligned (or budget palettes so
|
|
472
|
+
* neighbouring regions share one). */
|
|
473
|
+
static uint8_t quad_pal(uint8_t tc, uint8_t tr) {
|
|
474
|
+
if (tr >= WELL_TY && tr < WELL_TY + GRID_H) {
|
|
475
|
+
if (tc >= well_tx[0] && tc < well_tx[0] + GRID_W) return 0;
|
|
476
|
+
if (two_player && tc >= well_tx[1] && tc < well_tx[1] + GRID_W) return 0;
|
|
477
|
+
}
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
static void paint_attributes(void) {
|
|
482
|
+
uint8_t ar, ac, b;
|
|
483
|
+
for (ar = 0; ar < 8; ar++) {
|
|
484
|
+
for (ac = 0; ac < 8; ac++) {
|
|
485
|
+
b = (uint8_t)( quad_pal((uint8_t)(ac * 4), (uint8_t)(ar * 4))
|
|
486
|
+
| (quad_pal((uint8_t)(ac * 4 + 2), (uint8_t)(ar * 4)) << 2)
|
|
487
|
+
| (quad_pal((uint8_t)(ac * 4), (uint8_t)(ar * 4 + 2)) << 4)
|
|
488
|
+
| (quad_pal((uint8_t)(ac * 4 + 2), (uint8_t)(ar * 4 + 2)) << 6));
|
|
489
|
+
vram_unsafe_set((uint16_t)(0x23C0 + ar * 8 + ac), b);
|
|
281
490
|
}
|
|
282
491
|
}
|
|
492
|
+
}
|
|
283
493
|
|
|
494
|
+
/* ── GAME LOGIC (clay) — the title screen ──────────────────────────────────
|
|
495
|
+
* Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes; the queued
|
|
496
|
+
* variant would deadlock with rendering disabled — see TROUBLESHOOTING). */
|
|
497
|
+
static void paint_title(void) {
|
|
498
|
+
uint8_t r, c;
|
|
499
|
+
ppu_off();
|
|
500
|
+
for (r = 0; r < 30; r++)
|
|
501
|
+
for (c = 0; c < 32; c++)
|
|
502
|
+
vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + c),
|
|
503
|
+
(r < 2) ? 0 : BG_DITHER);
|
|
504
|
+
for (c = 0; c < 64; c++) /* whole screen → palette 1 */
|
|
505
|
+
vram_unsafe_set((uint16_t)(0x23C0 + c), 0x55);
|
|
506
|
+
text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
|
|
507
|
+
text_draw_unsafe(0x2000 + 13 * 32 + 10, "1P START - A");
|
|
508
|
+
text_draw_unsafe(0x2000 + 15 * 32 + 9, "2P VERSUS - B");
|
|
509
|
+
text_draw_unsafe(0x2000 + 20 * 32 + 10, "HI");
|
|
510
|
+
text_u16_unsafe((uint16_t)(0x2000 + 20 * 32 + 13), hiscore);
|
|
511
|
+
ppu_scroll(0, 0);
|
|
284
512
|
oam_clear();
|
|
285
513
|
ppu_on_all();
|
|
286
|
-
|
|
514
|
+
}
|
|
287
515
|
|
|
516
|
+
/* ── GAME LOGIC (clay) — paint the playfield: cabinet dither, well frames,
|
|
517
|
+
* recessed interiors, HUD labels + starting numbers. PPU off throughout. ── */
|
|
518
|
+
static void paint_well(uint8_t p) {
|
|
519
|
+
uint8_t r, c, x0;
|
|
520
|
+
x0 = well_tx[p];
|
|
521
|
+
for (c = (uint8_t)(x0 - 1); c <= (uint8_t)(x0 + GRID_W); c++) {
|
|
522
|
+
vram_unsafe_set((uint16_t)(0x2000 + (WELL_TY - 1) * 32 + c), BG_WALL);
|
|
523
|
+
vram_unsafe_set((uint16_t)(0x2000 + (WELL_TY + GRID_H) * 32 + c), BG_WALL);
|
|
524
|
+
}
|
|
525
|
+
for (r = (uint8_t)(WELL_TY - 1); r <= (uint8_t)(WELL_TY + GRID_H); r++) {
|
|
526
|
+
vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + (x0 - 1)), BG_WALL);
|
|
527
|
+
vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + (x0 + GRID_W)), BG_WALL);
|
|
528
|
+
}
|
|
288
529
|
for (r = 0; r < GRID_H; r++)
|
|
289
|
-
for (c = 0; c < GRID_W; c++)
|
|
290
|
-
|
|
530
|
+
for (c = 0; c < GRID_W; c++)
|
|
531
|
+
vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)(WELL_TY + r) * 32 + x0 + c),
|
|
532
|
+
BG_INNER);
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
static void paint_play(void) {
|
|
536
|
+
uint8_t r, c;
|
|
537
|
+
ppu_off();
|
|
538
|
+
/* Cabinet dither everywhere; rows 0-2 blank (row 0 = overscan-cropped,
|
|
539
|
+
* rows 1-2 = the HUD band — keep text on a clean background). */
|
|
540
|
+
for (r = 0; r < 30; r++)
|
|
541
|
+
for (c = 0; c < 32; c++)
|
|
542
|
+
vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + c),
|
|
543
|
+
(r < 3) ? 0 : BG_DITHER);
|
|
544
|
+
paint_well(0);
|
|
545
|
+
if (two_player) paint_well(1);
|
|
546
|
+
paint_attributes();
|
|
547
|
+
/* HUD: labels row 1, numbers row 2 (row 0 NEVER — overscan). */
|
|
548
|
+
text_draw_unsafe(0x2000 + 32 + 4, two_player ? "P1" : "SC");
|
|
549
|
+
text_draw_unsafe(0x2000 + 32 + 14, "HI");
|
|
550
|
+
text_draw_unsafe(0x2000 + 32 + 24, two_player ? "P2" : "LV");
|
|
551
|
+
text_u16_unsafe(0x2000 + 64 + 2, 0);
|
|
552
|
+
text_u16_unsafe(0x2000 + 64 + 12, hiscore);
|
|
553
|
+
text_u16_unsafe(0x2000 + 64 + 22, two_player ? 0 : 1);
|
|
554
|
+
ppu_scroll(0, 0);
|
|
555
|
+
oam_clear();
|
|
556
|
+
ppu_on_all();
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
560
|
+
static void start_game(uint8_t versus) {
|
|
561
|
+
uint8_t p, r, c;
|
|
562
|
+
uint8_t (*g)[GRID_W];
|
|
563
|
+
two_player = versus;
|
|
564
|
+
well_tx[0] = versus ? WELL_VS_P1 : WELL_1P_TX;
|
|
565
|
+
well_tx[1] = WELL_VS_P2;
|
|
566
|
+
/* Stir the PRNG with time-spent-on-title so runs differ. */
|
|
567
|
+
rng ^= (uint16_t)(((uint16_t)nmi_counter << 7) | nmi_counter);
|
|
568
|
+
if (rng == 0) rng = 0xACE1;
|
|
569
|
+
for (p = 0; p < 2; p++) {
|
|
570
|
+
g = grid_of(p);
|
|
571
|
+
for (r = 0; r < GRID_H; r++)
|
|
572
|
+
for (c = 0; c < GRID_W; c++) g[r][c] = EMPTY;
|
|
573
|
+
fall_t[p] = 0;
|
|
574
|
+
score[p] = 0;
|
|
575
|
+
hud_dirty[p] = 0;
|
|
576
|
+
dirty_rows[p] = 0;
|
|
577
|
+
prev_pad[p] = 0xFF; /* the button that started the game
|
|
578
|
+
* shouldn't also rotate the first trio */
|
|
579
|
+
}
|
|
580
|
+
cleared_total = 0;
|
|
581
|
+
level = 1;
|
|
582
|
+
drain_turn = 0;
|
|
583
|
+
paint_play();
|
|
584
|
+
state = ST_PLAY;
|
|
585
|
+
spawn_piece(0);
|
|
586
|
+
if (versus) spawn_piece(1);
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
void main(void) {
|
|
590
|
+
uint8_t pad, newp;
|
|
591
|
+
|
|
592
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
593
|
+
* Init order: PPU off → CHR upload → palette → nametable (raw writes) →
|
|
594
|
+
* OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
|
|
595
|
+
* off (raw $2007 traffic during rendering corrupts the address latch
|
|
596
|
+
* mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
|
|
597
|
+
* PPUMASK bits — don't poke those registers directly alongside it. */
|
|
598
|
+
ppu_off();
|
|
599
|
+
chr_ram_upload(0x0000, tile_blank, 16); /* sprite table: trio gems */
|
|
600
|
+
chr_ram_upload(0x0010, tile_gem1, 16);
|
|
601
|
+
chr_ram_upload(0x0020, tile_gem2, 16);
|
|
602
|
+
chr_ram_upload(0x0030, tile_gem3, 16);
|
|
603
|
+
chr_ram_upload(0x1010, tile_wall, 16); /* BG table: furniture + gems */
|
|
604
|
+
chr_ram_upload(0x1020, tile_dither, 16);
|
|
605
|
+
chr_ram_upload(0x1030, tile_inner, 16);
|
|
606
|
+
chr_ram_upload(0x1040, tile_gem1, 16);
|
|
607
|
+
chr_ram_upload(0x1050, tile_gem2, 16);
|
|
608
|
+
chr_ram_upload(0x1060, tile_gem3, 16);
|
|
609
|
+
font_upload();
|
|
610
|
+
palette_load(palette);
|
|
611
|
+
sound_init();
|
|
612
|
+
|
|
613
|
+
hiscore = hiscore_load(); /* battery SRAM — 0 on first boot */
|
|
614
|
+
state = ST_TITLE;
|
|
615
|
+
prev_pad[0] = 0xFF;
|
|
616
|
+
paint_title();
|
|
291
617
|
|
|
292
618
|
for (;;) {
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
619
|
+
if (state == ST_TITLE) {
|
|
620
|
+
/* ── GAME LOGIC (clay) — title: A/START = 1P, B = 2P versus ── */
|
|
621
|
+
oam_clear();
|
|
622
|
+
ppu_wait_nmi();
|
|
623
|
+
sound_music_tick();
|
|
624
|
+
pad = pad_poll(0);
|
|
625
|
+
newp = (uint8_t)(pad & (uint8_t)~prev_pad[0]);
|
|
626
|
+
prev_pad[0] = pad;
|
|
627
|
+
if (newp & PAD_A) start_game(0);
|
|
628
|
+
else if (newp & PAD_B) start_game(1);
|
|
629
|
+
else if (newp & PAD_START) start_game(0);
|
|
630
|
+
continue;
|
|
305
631
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
632
|
+
|
|
633
|
+
if (state == ST_OVER) {
|
|
634
|
+
/* Freeze the boards (trios hidden); finish trickling out any queued
|
|
635
|
+
* repaints; START or A returns to the title. */
|
|
636
|
+
oam_clear();
|
|
637
|
+
ppu_wait_nmi();
|
|
638
|
+
sound_music_tick();
|
|
639
|
+
drain_vram_budget();
|
|
640
|
+
pad = pad_poll(0);
|
|
641
|
+
newp = (uint8_t)(pad & (uint8_t)~prev_pad[0]);
|
|
642
|
+
prev_pad[0] = pad;
|
|
643
|
+
if (newp & (PAD_START | PAD_A)) {
|
|
644
|
+
/* Flush the queue BEFORE repainting: paint_title turns the PPU off,
|
|
645
|
+
* and any still-queued board writes would otherwise land on top of
|
|
646
|
+
* the freshly painted title when the NMI comes back. */
|
|
647
|
+
ppu_wait_nmi();
|
|
648
|
+
ppu_wait_nmi();
|
|
649
|
+
state = ST_TITLE;
|
|
650
|
+
prev_pad[0] = 0xFF;
|
|
651
|
+
paint_title();
|
|
314
652
|
}
|
|
653
|
+
continue;
|
|
315
654
|
}
|
|
316
655
|
|
|
317
|
-
|
|
656
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────── */
|
|
318
657
|
|
|
319
|
-
/* ──
|
|
658
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
659
|
+
* Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
|
|
660
|
+
* real OAM at the START of vblank, copying whatever shadow OAM holds AT
|
|
661
|
+
* THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites. */
|
|
662
|
+
oam_clear();
|
|
663
|
+
stage_piece(0);
|
|
664
|
+
if (two_player) stage_piece(1);
|
|
665
|
+
|
|
666
|
+
ppu_wait_nmi();
|
|
320
667
|
sound_music_tick();
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (
|
|
326
|
-
uint8_t tmp = piece_col[0];
|
|
327
|
-
piece_col[0] = piece_col[1];
|
|
328
|
-
piece_col[1] = piece_col[2];
|
|
329
|
-
piece_col[2] = tmp;
|
|
330
|
-
}
|
|
331
|
-
/* DOWN: soft-drop (faster fall) */
|
|
332
|
-
if (pad & PAD_DOWN) fall_timer += 4;
|
|
333
|
-
prev_pad = pad;
|
|
334
|
-
|
|
335
|
-
/* ── Auto-fall ──────────────────────────────────────────── */
|
|
336
|
-
++fall_timer;
|
|
337
|
-
if (fall_timer >= 30) {
|
|
338
|
-
fall_timer = 0;
|
|
339
|
-
if (can_place(piece_x, piece_y + 1)) {
|
|
340
|
-
++piece_y;
|
|
341
|
-
} else {
|
|
342
|
-
lock_piece();
|
|
343
|
-
spawn_piece();
|
|
344
|
-
/* Game over: piece can't be placed at spawn. Just reset grid. */
|
|
345
|
-
if (!can_place(piece_x, piece_y)) {
|
|
346
|
-
for (r = 0; r < GRID_H; r++)
|
|
347
|
-
for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
|
|
348
|
-
sound_play_noise(8, 8, 12); /* game-over buzz */
|
|
349
|
-
}
|
|
350
|
-
}
|
|
351
|
-
}
|
|
668
|
+
|
|
669
|
+
/* ── GAME LOGIC (clay — reshape freely) ── */
|
|
670
|
+
update_player(0);
|
|
671
|
+
if (two_player && state == ST_PLAY) update_player(1);
|
|
672
|
+
if (state == ST_PLAY) drain_vram_budget();
|
|
352
673
|
}
|
|
353
674
|
}
|