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,341 +1,591 @@
|
|
|
1
|
-
/* ── puzzle.c — Game Boy Advance
|
|
1
|
+
/* ── puzzle.c — Game Boy Advance falling-jewel match-3 (complete game) ────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* FACET FALL — a COMPLETE, working game: press-start title, a falling-trio
|
|
4
|
+
* match-3 in a 6x12 well, cascade chains, levels that speed the fall, score +
|
|
5
|
+
* persistent hi-score (cartridge SRAM), and DMA/PSG music + SFX. The jewels
|
|
6
|
+
* are VIVID — the GBA's 15-bit palette gives 32768 colours, so each gem reads
|
|
7
|
+
* as a faceted stone (a bright glint, a mid body, a dark rim) rather than a
|
|
8
|
+
* flat block, and the well sits in a framed cabinet.
|
|
7
9
|
*
|
|
8
|
-
* The
|
|
9
|
-
*
|
|
10
|
+
* The game: a vertical trio of three jewels (each its own colour) falls into
|
|
11
|
+
* the well. LEFT/RIGHT move it, A/B cycle its three colours (the classic
|
|
12
|
+
* trio "rotate"), DOWN soft-drops, START hard-drops. When it lands, any
|
|
13
|
+
* straight run of 3+ same-coloured jewels (horizontal, vertical, OR diagonal)
|
|
14
|
+
* clears; survivors fall and cascades chain for multiplied score. Clear enough
|
|
15
|
+
* and the level ticks up and the fall quickens. Stack to the rim and it's over.
|
|
10
16
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
17
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
18
|
+
* very different one. The markers tell you what's what:
|
|
19
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GBA footgun; reshape
|
|
20
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
21
|
+
* GAME LOGIC (clay) — match rules, tuning, art, scoring: reshape freely.
|
|
14
22
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
23
|
+
* What depends on what:
|
|
24
|
+
* gba_sfx.{h,c} — PSG sound: sfx_tone/sfx_noise one-shots + the music loop
|
|
25
|
+
* (sfx_music_tick once per frame — forget it and the game is silent).
|
|
26
|
+
* libtonc (the build links it) — VBlankIntrWait/key_poll/TTE/tonccpy.
|
|
17
27
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
28
|
+
* HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P versus on the GBA means a
|
|
29
|
+
* link cable between two units — a second emulator instance this environment
|
|
30
|
+
* can't provide. So FACET FALL is a 1P MARATHON: clear, chain, level up, and
|
|
31
|
+
* push your hi-score. (Contrast the NES/Genesis puzzle templates, which ARE
|
|
32
|
+
* split-screen 2P versus — two controllers on one machine.)
|
|
33
|
+
*
|
|
34
|
+
* BANDWIDTH NOTE — and a TEACHING POINT vs the GB/NES version of this game
|
|
35
|
+
* (examples/nes/templates/puzzle.c): on the NES a full-board repaint must
|
|
36
|
+
* squeeze through a ~16-entry vblank tile queue, BUDGETED across 12 frames of
|
|
37
|
+
* dirty-row-bitmask tricks. The GBA has no such famine — its BG tilemap is
|
|
38
|
+
* plain VRAM you write whenever you like. So FACET FALL just REPAINTS THE
|
|
39
|
+
* WHOLE WELL (72 cells = 72 u16 SE writes) every time the board changes, in
|
|
40
|
+
* one go, no queue, no dirty-row gymnastics. Same genre, two bandwidth
|
|
41
|
+
* worlds — fork accordingly.
|
|
20
42
|
*/
|
|
21
43
|
|
|
22
44
|
#include <tonc.h>
|
|
23
45
|
#include "gba_sfx.h"
|
|
24
46
|
|
|
25
|
-
/*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
47
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
48
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
49
|
+
#define GAME_TITLE "FACET FALL"
|
|
50
|
+
|
|
51
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
52
|
+
* Board geometry. The well is 6 wide x 12 tall, each cell ONE 8x8 BG tile.
|
|
53
|
+
* Origin in BG-tile coords: the playfield sits at pixel (well_tx*8, WELL_TY*8)
|
|
54
|
+
* with the HUD above and the controls hint below. */
|
|
55
|
+
#define GRID_W 6
|
|
56
|
+
#define GRID_H 12
|
|
57
|
+
#define WELL_TY 3 /* top tile row of the well interior */
|
|
58
|
+
#define WELL_TX 16 /* left tile col of the well interior (centered)*/
|
|
59
|
+
|
|
60
|
+
#define EMPTY 0 /* cell colours 1..3 = ruby / emerald / sapphire */
|
|
61
|
+
|
|
62
|
+
/* ── GAME LOGIC (clay) — BG tile art (regular Mode-0 4bpp BG tiles).
|
|
63
|
+
* Each 8x8 4bpp tile is 8 u32 rows; each nibble is a palette index within the
|
|
64
|
+
* BG palbank we use (bank 0). Index 0 = transparent → shows the backdrop
|
|
65
|
+
* colour. KEY TRICK: the three jewel tiles are the SAME faceted SHAPE drawn on
|
|
66
|
+
* three different palette indices (glint/body/rim per colour), so a cell
|
|
67
|
+
* changes colour by changing its TILE id — no palette rewrite, no attribute
|
|
68
|
+
* juggling. The faceted look (a 1-3-1 nibble gradient) is what makes the
|
|
69
|
+
* jewels pop on the GBA's wide palette. */
|
|
70
|
+
#define BG_BACK 1 /* cabinet backdrop dither */
|
|
71
|
+
#define BG_BAND 2 /* flat band behind the HUD text */
|
|
72
|
+
#define BG_FRAME 3 /* well frame / border */
|
|
73
|
+
#define BG_INNER 4 /* empty well cell (recessed, faint speck) */
|
|
74
|
+
#define BG_GEM1 5 /* jewel colour 1 (ruby) */
|
|
75
|
+
#define BG_GEM2 6 /* jewel colour 2 (emerald) */
|
|
76
|
+
#define BG_GEM3 7 /* jewel colour 3 (sapphire) */
|
|
77
|
+
|
|
78
|
+
/* cell colour (1..3) → BG tile id; empty shows the recessed inner cell. */
|
|
79
|
+
static u16 bg_tile_for(u8 col) {
|
|
80
|
+
return col ? (u16)(BG_GEM1 - 1 + col) : BG_INNER;
|
|
37
81
|
}
|
|
38
82
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
#define TILE_BLANK 0
|
|
43
|
-
#define TILE_RED 1
|
|
44
|
-
#define TILE_GREEN 2
|
|
45
|
-
#define TILE_BLUE 3
|
|
46
|
-
|
|
47
|
-
/* Grid origin in BG-tile coords. */
|
|
48
|
-
#define GRID_TX 4
|
|
49
|
-
#define GRID_TY 2
|
|
50
|
-
|
|
51
|
-
static const u32 tile_red[8] = {
|
|
52
|
-
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
53
|
-
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
54
|
-
};
|
|
55
|
-
static const u32 tile_green[8] = {
|
|
56
|
-
0x22222222, 0x22222222, 0x22222222, 0x22222222,
|
|
57
|
-
0x22222222, 0x22222222, 0x22222222, 0x22222222,
|
|
83
|
+
static const u32 bg_tile_back[8] = { /* steel dither cabinet */
|
|
84
|
+
0x12121212, 0x21212121, 0x12121212, 0x21212121,
|
|
85
|
+
0x12121212, 0x21212121, 0x12121212, 0x21212121,
|
|
58
86
|
};
|
|
59
|
-
static const u32
|
|
87
|
+
static const u32 bg_tile_band[8] = { /* solid band behind the HUD */
|
|
60
88
|
0x33333333, 0x33333333, 0x33333333, 0x33333333,
|
|
61
89
|
0x33333333, 0x33333333, 0x33333333, 0x33333333,
|
|
62
90
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
/* Solid light-grey wall tile for the well border. */
|
|
67
|
-
static const u32 tile_wall[8] = {
|
|
68
|
-
0x55555555, 0x55555555, 0x55555555, 0x55555555,
|
|
69
|
-
0x55555555, 0x55555555, 0x55555555, 0x55555555,
|
|
91
|
+
static const u32 bg_tile_frame[8] = { /* bevelled steel border */
|
|
92
|
+
0x44444444, 0x43333334, 0x43333334, 0x43333334,
|
|
93
|
+
0x43333334, 0x43333334, 0x43333334, 0x44444444,
|
|
70
94
|
};
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
95
|
+
static const u32 bg_tile_inner[8] = { /* recessed empty cell + speck */
|
|
96
|
+
0x00000000, 0x00000000, 0x00000000, 0x00011000,
|
|
97
|
+
0x00011000, 0x00000000, 0x00000000, 0x00000000,
|
|
98
|
+
};
|
|
99
|
+
/* One faceted-jewel SHAPE per colour. The nibbles are a tiny gradient:
|
|
100
|
+
* 2 = glint (bright), 1 = body (mid), 3 = rim (dark). build_gem_tiles()
|
|
101
|
+
* remaps {1,2,3} into each colour's three palette indices so the single
|
|
102
|
+
* shape becomes three distinct, vivid stones. */
|
|
103
|
+
static const u32 gem_shape[8] = {
|
|
104
|
+
0x00033300, 0x00321230, 0x03212210, 0x32122130,
|
|
105
|
+
0x32112130, 0x03211230, 0x00321330, 0x00033300,
|
|
76
106
|
};
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
static
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
107
|
+
static u32 gem_ram[3][8]; /* colours 1..3, built at boot */
|
|
108
|
+
|
|
109
|
+
/* ── GAME LOGIC (clay) — remap the one jewel shape into three vivid colours.
|
|
110
|
+
* Shape nibble n in {1,2,3} → palette index (3*k + n) for colour k, so each
|
|
111
|
+
* jewel uses its own 3-index slice of the palbank (glint/body/rim). */
|
|
112
|
+
static void build_gem_tiles(void) {
|
|
113
|
+
int k, r, i;
|
|
114
|
+
for (k = 0; k < 3; k++)
|
|
115
|
+
for (r = 0; r < 8; r++) {
|
|
116
|
+
u32 v = gem_shape[r], out = 0;
|
|
117
|
+
for (i = 0; i < 8; i++) {
|
|
118
|
+
u32 nib = (v >> (i * 4)) & 0xF;
|
|
119
|
+
if (nib) nib = (u32)(3 * k + nib); /* into this colour's slice */
|
|
120
|
+
out |= nib << (i * 4);
|
|
121
|
+
}
|
|
122
|
+
gem_ram[k][r] = out;
|
|
123
|
+
}
|
|
93
124
|
}
|
|
94
125
|
|
|
95
|
-
|
|
126
|
+
/* ── GAME LOGIC (clay — reshape freely) — game state (plain BSS; the GBA has
|
|
127
|
+
* 256 KB of EWRAM + 32 KB of IWRAM, so none of the NES version's
|
|
128
|
+
* absolute-address scratch-page gymnastics).
|
|
129
|
+
* NOTE for headless verification: unlike the Genesis template (whose work-RAM
|
|
130
|
+
* globals are readable by symbol name), the GBA libretro core exposes NO
|
|
131
|
+
* IWRAM/EWRAM region, so a headless agent reads game state from what's ON
|
|
132
|
+
* HARDWARE — the BG0 tilemap (the locked well, in VRAM), OAM (the falling
|
|
133
|
+
* trio), and save_ram (the hi-score). The verify harness decodes those. */
|
|
134
|
+
#define ST_TITLE 0
|
|
135
|
+
#define ST_PLAY 1
|
|
136
|
+
#define ST_OVER 2
|
|
137
|
+
static u8 state;
|
|
138
|
+
|
|
139
|
+
static u8 grid[GRID_H][GRID_W]; /* the well: 0 = empty, 1..3 = colour */
|
|
140
|
+
static s16 piece_x; /* falling trio: column 0..5 */
|
|
141
|
+
static s16 piece_y; /* row of its TOP cell (<0 = above rim) */
|
|
142
|
+
static u8 piece_col[3]; /* trio colours, top to bottom */
|
|
143
|
+
static u16 score, hiscore;
|
|
144
|
+
static u16 cleared_total; /* gems cleared, drives the level */
|
|
145
|
+
static u8 level; /* 1..9, speeds up the fall */
|
|
146
|
+
|
|
147
|
+
static u8 matched[GRID_H][GRID_W];/* match scan scratch */
|
|
148
|
+
static u16 fall_t; /* frames until next gravity step */
|
|
149
|
+
|
|
150
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a handful of ARM instructions) ── */
|
|
151
|
+
static u16 rng = 0xACE1;
|
|
152
|
+
static u8 random8(void) {
|
|
153
|
+
u16 r = rng;
|
|
154
|
+
r ^= r << 7;
|
|
155
|
+
r ^= r >> 9;
|
|
156
|
+
r ^= r << 8;
|
|
157
|
+
rng = r;
|
|
158
|
+
return (u8)r;
|
|
159
|
+
}
|
|
96
160
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
161
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
162
|
+
* PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
|
|
163
|
+
* 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
|
|
164
|
+
* access doesn't fault, it just reads the same byte mirrored (and a
|
|
165
|
+
* wide write stores one byte), so your data "almost" round-trips and
|
|
166
|
+
* then the checksum never matches. Every access below is via vu8.
|
|
167
|
+
* 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
|
|
168
|
+
* image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
|
|
169
|
+
* the cart NO save memory at all and writes to 0x0E000000 vanish.
|
|
170
|
+
* The aligned, (used)-attributed const below plants that marker —
|
|
171
|
+
* delete it and persistence dies even though this code is untouched.
|
|
172
|
+
* Layout: 'V' 'X' score-lo score-hi checksum (xor ^ 0xA5) — magic+checksum
|
|
173
|
+
* so a fresh (0xFF-filled) cart reads as "no record" instead of garbage.
|
|
174
|
+
* requires: nothing else — self-contained; safe to transplant whole. */
|
|
175
|
+
#define SRAM_BYTE ((volatile u8 *)0x0E000000)
|
|
176
|
+
__attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
|
|
177
|
+
|
|
178
|
+
static u16 hiscore_load(void) {
|
|
179
|
+
u8 lo, hi;
|
|
180
|
+
if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
|
|
181
|
+
lo = SRAM_BYTE[2];
|
|
182
|
+
hi = SRAM_BYTE[3];
|
|
183
|
+
if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
|
|
184
|
+
return (u16)(lo | (hi << 8));
|
|
103
185
|
}
|
|
104
186
|
|
|
105
|
-
static
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
}
|
|
187
|
+
static void hiscore_save(u16 v) {
|
|
188
|
+
SRAM_BYTE[0] = 'V';
|
|
189
|
+
SRAM_BYTE[1] = 'X';
|
|
190
|
+
SRAM_BYTE[2] = (u8)v;
|
|
191
|
+
SRAM_BYTE[3] = (u8)(v >> 8);
|
|
192
|
+
SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
|
|
112
193
|
}
|
|
113
194
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
195
|
+
/* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
|
|
196
|
+
* Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
|
|
197
|
+
* bundled libtonc's tte_printf with a %d conversion is broken (it routes
|
|
198
|
+
* through a vsnprintf path that isn't wired in this build — it garbles
|
|
199
|
+
* output AND wedges the loop when called per-frame, GBA-1). We build the
|
|
200
|
+
* string ourselves and use tte_write, which processes the #{P:x,y} position
|
|
201
|
+
* command but does NO format conversion → safe every frame. */
|
|
202
|
+
static void draw_num(int x, int y, unsigned v, int digits) {
|
|
203
|
+
char buf[24];
|
|
204
|
+
int i, n = 0;
|
|
205
|
+
buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
|
|
206
|
+
if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
|
|
207
|
+
if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
|
|
208
|
+
buf[n++] = (char)('0' + x % 10);
|
|
209
|
+
buf[n++] = ',';
|
|
210
|
+
if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
|
|
211
|
+
if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
|
|
212
|
+
buf[n++] = (char)('0' + y % 10);
|
|
213
|
+
buf[n++] = '}';
|
|
214
|
+
for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
|
|
215
|
+
n += digits; buf[n] = 0;
|
|
216
|
+
tte_write(buf);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
220
|
+
* THE WELL IS BACKGROUND TILES on BG0 (Mode 0, a REGULAR text BG). A 32x32
|
|
221
|
+
* map (BG_REG_32x32) is one screenblock; each map entry is a u16: tile id
|
|
222
|
+
* (10 bits) + hflip/vflip + a 4-bit palbank. SE_BUILD(tile, palbank, hf, vf)
|
|
223
|
+
* packs it. Footguns this dodges:
|
|
224
|
+
* - VRAM IGNORES BYTE WRITES (a u8 store duplicates the byte into both
|
|
225
|
+
* halves of the 16-bit lane). We only ever write whole u16 SE entries
|
|
226
|
+
* (via this helper) and tonccpy() tile data — both VRAM-safe.
|
|
227
|
+
* - TTE owns BG1 (CBB 2 / SBB 30). Keep this map (SBB 28) and our tile
|
|
228
|
+
* graphics (CBB 0) clear of those blocks or text and well corrupt each
|
|
229
|
+
* other.
|
|
230
|
+
* Unlike the NES, there is NO vblank-queue famine here — set_cell can run
|
|
231
|
+
* any time, so paint_well() just rewrites all 72 cells when the board changes.
|
|
232
|
+
* requires: REG_BG0CNT → CBB 0 / SBB 28 (set in main), DCNT_BG0 enabled. */
|
|
233
|
+
static SCR_ENTRY *const well_map = se_mem[28];
|
|
234
|
+
static void set_cell(int tx, int ty, u16 tile) {
|
|
235
|
+
well_map[ty * 32 + tx] = SE_BUILD(tile, 0, 0, 0);
|
|
120
236
|
}
|
|
121
237
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
238
|
+
/* Repaint the whole well interior from the grid (cheap on GBA — see idiom). */
|
|
239
|
+
static void paint_well(void) {
|
|
240
|
+
int r, c;
|
|
241
|
+
for (r = 0; r < GRID_H; r++)
|
|
242
|
+
for (c = 0; c < GRID_W; c++)
|
|
243
|
+
set_cell(WELL_TX + c, WELL_TY + r, bg_tile_for(grid[r][c]));
|
|
126
244
|
}
|
|
127
245
|
|
|
128
|
-
static
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
for (
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
246
|
+
/* Paint the static cabinet: backdrop dither, HUD band, well frame. Done once
|
|
247
|
+
* per state entry (the interior is then repainted by paint_well). */
|
|
248
|
+
static void paint_cabinet(void) {
|
|
249
|
+
int r, c, x0 = WELL_TX - 1;
|
|
250
|
+
for (r = 0; r < 32; r++)
|
|
251
|
+
for (c = 0; c < 32; c++)
|
|
252
|
+
set_cell(c, r, (r < 2) ? BG_BAND : BG_BACK); /* rows 0-1 = HUD band */
|
|
253
|
+
/* well frame: a border box around the interior */
|
|
254
|
+
for (c = 0; c <= GRID_W + 1; c++) {
|
|
255
|
+
set_cell(x0 + c, WELL_TY - 1, BG_FRAME);
|
|
256
|
+
set_cell(x0 + c, WELL_TY + GRID_H, BG_FRAME);
|
|
138
257
|
}
|
|
258
|
+
for (r = 0; r < GRID_H; r++) {
|
|
259
|
+
set_cell(x0, WELL_TY + r, BG_FRAME);
|
|
260
|
+
set_cell(x0 + GRID_W + 1, WELL_TY + r, BG_FRAME);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
|
|
265
|
+
static void draw_hud_numbers(void) {
|
|
266
|
+
tte_erase_rect(28, 4, 70, 12); draw_num(28, 4, score, 5);
|
|
267
|
+
tte_erase_rect(116, 4, 158, 12); draw_num(116, 4, hiscore, 5);
|
|
268
|
+
tte_erase_rect(210, 4, 220, 12); draw_num(210, 4, level, 1);
|
|
139
269
|
}
|
|
140
270
|
|
|
141
|
-
static
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
271
|
+
static void draw_hud_labels(void) {
|
|
272
|
+
tte_erase_screen();
|
|
273
|
+
tte_write("#{P:8,4}SC");
|
|
274
|
+
tte_write("#{P:96,4}HI");
|
|
275
|
+
tte_write("#{P:196,4}LV");
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
static void enter_title(void) {
|
|
279
|
+
state = ST_TITLE;
|
|
280
|
+
paint_cabinet();
|
|
281
|
+
for (int r = 0; r < GRID_H; r++)
|
|
282
|
+
for (int c = 0; c < GRID_W; c++) { grid[r][c] = EMPTY; set_cell(WELL_TX + c, WELL_TY + r, BG_INNER); }
|
|
283
|
+
tte_erase_screen();
|
|
284
|
+
tte_write("#{P:64,40}" GAME_TITLE);
|
|
285
|
+
tte_write("#{P:76,72}PRESS START");
|
|
286
|
+
tte_write("#{P:88,92}HI");
|
|
287
|
+
draw_num(112, 92, hiscore, 5);
|
|
288
|
+
tte_write("#{P:32,116}DPAD MOVE - AB SPIN");
|
|
289
|
+
tte_write("#{P:48,128}START DROP - 1P MARATHON");
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
static void spawn_piece(void);
|
|
293
|
+
static void enter_play(void) {
|
|
294
|
+
int r, c;
|
|
295
|
+
state = ST_PLAY;
|
|
296
|
+
for (r = 0; r < GRID_H; r++)
|
|
297
|
+
for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
|
|
298
|
+
score = 0; level = 1; cleared_total = 0; fall_t = 0;
|
|
299
|
+
/* Stir the PRNG with time-on-title so each run differs. */
|
|
300
|
+
rng ^= (u16)REG_VCOUNT ^ ((u16)REG_VCOUNT << 7);
|
|
301
|
+
if (rng == 0) rng = 0xACE1;
|
|
302
|
+
paint_cabinet();
|
|
303
|
+
paint_well();
|
|
304
|
+
draw_hud_labels();
|
|
305
|
+
draw_hud_numbers();
|
|
306
|
+
/* No need to swallow the START that began the run: key_hit only fires on a
|
|
307
|
+
* fresh press (curr & ~prev), so the held START doesn't also hard-drop. */
|
|
308
|
+
spawn_piece();
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
static void enter_over(void) {
|
|
312
|
+
state = ST_OVER;
|
|
313
|
+
if (score > hiscore) {
|
|
314
|
+
hiscore = score;
|
|
315
|
+
hiscore_save(hiscore); /* byte-wise SRAM write — see the SRAM idiom */
|
|
316
|
+
draw_hud_numbers();
|
|
147
317
|
}
|
|
148
|
-
|
|
318
|
+
sfx_noise(20); /* game-over rumble */
|
|
319
|
+
tte_write("#{P:84,60}GAME OVER");
|
|
320
|
+
tte_write("#{P:76,80}PRESS START");
|
|
149
321
|
}
|
|
150
322
|
|
|
151
|
-
/* ──
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
* cascades chain (score scales with chain depth). */
|
|
157
|
-
static u8 matched[ROWS][COLS];
|
|
323
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
324
|
+
* Match scan: mark every straight run of 3+ same-coloured jewels in all 4
|
|
325
|
+
* directions (a cell can belong to several runs — the mask de-dupes), and
|
|
326
|
+
* return how many cells matched. Runs flat-out on the ARM7 — no need to smear
|
|
327
|
+
* it across frames like the cc65 (NES) version. */
|
|
158
328
|
static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
159
329
|
|
|
160
330
|
static u8 mark_and_count(void) {
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
331
|
+
u8 r, c, d, len, k, cnt, col;
|
|
332
|
+
s8 dr, dc;
|
|
333
|
+
s16 sr, sc;
|
|
334
|
+
cnt = 0;
|
|
335
|
+
for (r = 0; r < GRID_H; r++)
|
|
336
|
+
for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
|
|
337
|
+
for (r = 0; r < GRID_H; r++) {
|
|
338
|
+
for (c = 0; c < GRID_W; c++) {
|
|
339
|
+
col = grid[r][c];
|
|
340
|
+
if (col == EMPTY) continue;
|
|
341
|
+
for (d = 0; d < 4; d++) {
|
|
342
|
+
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
343
|
+
sr = (s16)r - dr; sc = (s16)c - dc;
|
|
344
|
+
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
345
|
+
&& grid[sr][sc] == col) continue; /* not the run's start */
|
|
346
|
+
len = 1;
|
|
347
|
+
sr = (s16)r + dr; sc = (s16)c + dc;
|
|
348
|
+
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
349
|
+
&& grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
350
|
+
if (len >= 3) {
|
|
351
|
+
sr = r; sc = c;
|
|
352
|
+
for (k = 0; k < len; k++) {
|
|
353
|
+
if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
|
|
354
|
+
sr += dr; sc += dc;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
186
358
|
}
|
|
187
|
-
}
|
|
188
359
|
}
|
|
189
|
-
|
|
190
|
-
return cnt;
|
|
360
|
+
return cnt;
|
|
191
361
|
}
|
|
192
362
|
|
|
193
|
-
/*
|
|
194
|
-
*
|
|
363
|
+
/* Collapse each column so survivors rest on the floor (walk from the bottom,
|
|
364
|
+
* copying jewels down to a write cursor, then zero everything above it). */
|
|
195
365
|
static void apply_gravity(void) {
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
366
|
+
u8 c;
|
|
367
|
+
s16 r, w;
|
|
368
|
+
for (c = 0; c < GRID_W; c++) {
|
|
369
|
+
w = GRID_H - 1;
|
|
370
|
+
for (r = GRID_H - 1; r >= 0; r--) {
|
|
371
|
+
if (grid[r][c] != EMPTY) { grid[w][c] = grid[r][c]; w--; }
|
|
372
|
+
}
|
|
373
|
+
for (; w >= 0; w--) grid[w][c] = EMPTY;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
|
|
378
|
+
* Returns the chain depth (0 = the lock matched nothing). Score, level and the
|
|
379
|
+
* full-well repaint happen here. */
|
|
380
|
+
static u8 resolve_board(void) {
|
|
381
|
+
u8 n, r, c, chain;
|
|
382
|
+
u16 amt;
|
|
383
|
+
chain = 0;
|
|
384
|
+
for (;;) {
|
|
385
|
+
n = mark_and_count();
|
|
386
|
+
if (n == 0) break;
|
|
387
|
+
++chain;
|
|
388
|
+
for (r = 0; r < GRID_H; r++)
|
|
389
|
+
for (c = 0; c < GRID_W; c++)
|
|
390
|
+
if (matched[r][c]) grid[r][c] = EMPTY;
|
|
391
|
+
amt = (u16)n * 10;
|
|
392
|
+
if (chain > 1) amt *= chain; /* cascades pay multiplied */
|
|
393
|
+
if (score < 65000u) score += amt;
|
|
394
|
+
/* clear chime — pitch rises with chain depth (bigger freq code = higher) */
|
|
395
|
+
sfx_tone(1, (u16)(1500 + ((u16)chain << 6)), 8);
|
|
396
|
+
apply_gravity();
|
|
397
|
+
cleared_total += n;
|
|
398
|
+
while (level < 9 && cleared_total >= (u16)level * 10) ++level;
|
|
399
|
+
}
|
|
400
|
+
if (chain) { paint_well(); draw_hud_numbers(); }
|
|
401
|
+
return chain;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
|
|
405
|
+
* (pieces enter from above); below the floor or on a jewel is not. */
|
|
406
|
+
static u8 can_place(s16 x, s16 y) {
|
|
407
|
+
s16 i, cy;
|
|
408
|
+
if (x < 0 || x >= GRID_W) return 0;
|
|
409
|
+
for (i = 0; i < 3; i++) {
|
|
410
|
+
cy = y + i;
|
|
411
|
+
if (cy < 0) continue;
|
|
412
|
+
if (cy >= GRID_H) return 0;
|
|
413
|
+
if (grid[cy][x] != EMPTY) return 0;
|
|
202
414
|
}
|
|
203
|
-
|
|
204
|
-
}
|
|
415
|
+
return 1;
|
|
205
416
|
}
|
|
206
417
|
|
|
207
|
-
static void
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (
|
|
214
|
-
chain++;
|
|
215
|
-
for (r = 0; r < ROWS; r++)
|
|
216
|
-
for (c = 0; c < COLS; c++)
|
|
217
|
-
if (matched[r][c]) grid[r][c] = 0;
|
|
218
|
-
amt = (unsigned int)n * 10u;
|
|
219
|
-
if (chain > 1) amt = amt * chain;
|
|
220
|
-
if (score < 65500u) score += amt;
|
|
221
|
-
sfx_tone(1, 1700, 10); /* clear chime */
|
|
222
|
-
apply_gravity();
|
|
223
|
-
}
|
|
418
|
+
static void spawn_piece(void) {
|
|
419
|
+
piece_x = GRID_W / 2;
|
|
420
|
+
piece_y = -2;
|
|
421
|
+
piece_col[0] = (u8)(1 + random8() % 3);
|
|
422
|
+
piece_col[1] = (u8)(1 + random8() % 3);
|
|
423
|
+
piece_col[2] = (u8)(1 + random8() % 3);
|
|
424
|
+
if (!can_place(piece_x, piece_y)) enter_over(); /* well full → game over */
|
|
224
425
|
}
|
|
225
426
|
|
|
427
|
+
/* ── GAME LOGIC (clay) — land the trio, resolve, respawn. ── */
|
|
226
428
|
static void lock_piece(void) {
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
429
|
+
s16 i, y;
|
|
430
|
+
for (i = 0; i < 3; i++) {
|
|
431
|
+
y = piece_y + i;
|
|
432
|
+
if (y >= 0) grid[y][piece_x] = piece_col[i];
|
|
230
433
|
}
|
|
434
|
+
paint_well();
|
|
435
|
+
sfx_tone(2, 900, 4); /* lock thunk */
|
|
436
|
+
if (piece_y < 0) { enter_over(); return; } /* locked above the rim */
|
|
231
437
|
resolve_board();
|
|
232
|
-
|
|
438
|
+
if (state != ST_PLAY) return;
|
|
439
|
+
spawn_piece();
|
|
233
440
|
}
|
|
234
441
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
SCR_ENTRY *map = se_mem[28];
|
|
261
|
-
for (int i = 0; i < 32 * 32; i++) map[i] = SE_BUILD(TILE_BACK, 0, 0, 0);
|
|
262
|
-
/* Well border — playtest: "needs border around play area". One wall
|
|
263
|
-
* cell left/right of the grid columns + a floor row underneath. */
|
|
264
|
-
for (int r = 0; r <= ROWS; r++) {
|
|
265
|
-
map[(GRID_TY + r) * 32 + (GRID_TX - 1)] = SE_BUILD(TILE_WALL, 0, 0, 0);
|
|
266
|
-
map[(GRID_TY + r) * 32 + (GRID_TX + COLS)] = SE_BUILD(TILE_WALL, 0, 0, 0);
|
|
442
|
+
/* ── GAME LOGIC (clay) — input + gravity. Edge-triggered moves (key_hit = one
|
|
443
|
+
* cell per press), held DOWN soft-drops, A/B cycle the trio's colours, START
|
|
444
|
+
* hard-drops. libtonc's key_poll() (called once per frame in main) maintains
|
|
445
|
+
* the curr/prev key state that key_hit/key_held read — that's the idiomatic
|
|
446
|
+
* Tonc edge-trigger, no hand-rolled prev-mask needed. May end the game
|
|
447
|
+
* (lock → top-out). ── */
|
|
448
|
+
static void update_play(void) {
|
|
449
|
+
u8 t, fd;
|
|
450
|
+
|
|
451
|
+
if (key_hit(KEY_LEFT) && can_place(piece_x - 1, piece_y)) --piece_x;
|
|
452
|
+
if (key_hit(KEY_RIGHT) && can_place(piece_x + 1, piece_y)) ++piece_x;
|
|
453
|
+
if (key_hit(KEY_A)) { /* cycle colours downward */
|
|
454
|
+
t = piece_col[2]; piece_col[2] = piece_col[1];
|
|
455
|
+
piece_col[1] = piece_col[0]; piece_col[0] = t;
|
|
456
|
+
sfx_tone(2, 1300, 3);
|
|
457
|
+
}
|
|
458
|
+
if (key_hit(KEY_B)) { /* cycle colours upward */
|
|
459
|
+
t = piece_col[0]; piece_col[0] = piece_col[1];
|
|
460
|
+
piece_col[1] = piece_col[2]; piece_col[2] = t;
|
|
461
|
+
sfx_tone(2, 1400, 3);
|
|
462
|
+
}
|
|
463
|
+
if (key_hit(KEY_START)) { /* hard drop */
|
|
464
|
+
while (can_place(piece_x, piece_y + 1)) ++piece_y;
|
|
465
|
+
lock_piece();
|
|
466
|
+
return;
|
|
267
467
|
}
|
|
268
|
-
for (int c = -1; c <= COLS; c++)
|
|
269
|
-
map[(GRID_TY + ROWS) * 32 + (GRID_TX + c)] = SE_BUILD(TILE_WALL, 0, 0, 0);
|
|
270
468
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
469
|
+
if (key_held(KEY_DOWN)) fall_t += 4; /* soft drop */
|
|
470
|
+
++fall_t;
|
|
471
|
+
fd = (u8)(32 - ((level << 1) + level)); /* 29 (lv1) .. 5 (lv9) */
|
|
472
|
+
if (fall_t >= fd) {
|
|
473
|
+
fall_t = 0;
|
|
474
|
+
if (can_place(piece_x, piece_y + 1)) ++piece_y;
|
|
475
|
+
else lock_piece(); /* may end the game */
|
|
476
|
+
}
|
|
477
|
+
}
|
|
275
478
|
|
|
276
|
-
|
|
479
|
+
/* ── GAME LOGIC (clay) — stage the falling trio's sprites. The LOCKED well is
|
|
480
|
+
* BG tiles (only what moves every frame earns OAM slots): 3 sprites for the
|
|
481
|
+
* trio. Cells above the rim aren't drawn — they'd poke out over the HUD band.
|
|
482
|
+
* Off-screen / inactive slots park at y=200. ── */
|
|
483
|
+
static OBJ_ATTR obj_buffer[128];
|
|
484
|
+
#define TILE_TRIO 1 /* OBJ tile 1 = the faceted jewel sprite (4bpp 8x8) */
|
|
485
|
+
|
|
486
|
+
static void stage_sprites(void) {
|
|
487
|
+
int i;
|
|
488
|
+
int playing = (state == ST_PLAY);
|
|
489
|
+
for (i = 0; i < 3; i++) {
|
|
490
|
+
s16 r = piece_y + (s16)i;
|
|
491
|
+
u8 col = piece_col[i] ? piece_col[i] : 1;
|
|
492
|
+
if (playing && r >= 0) {
|
|
493
|
+
obj_set_attr(&obj_buffer[i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
494
|
+
(u16)(ATTR2_PALBANK(col - 1) | TILE_TRIO));
|
|
495
|
+
obj_set_pos(&obj_buffer[i], (WELL_TX + piece_x) * 8, (WELL_TY + r) * 8);
|
|
496
|
+
} else {
|
|
497
|
+
obj_set_attr(&obj_buffer[i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
498
|
+
(u16)(ATTR2_PALBANK(0) | TILE_TRIO));
|
|
499
|
+
obj_set_pos(&obj_buffer[i], 250, 200);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
}
|
|
277
503
|
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
504
|
+
int main(void) {
|
|
505
|
+
int k;
|
|
506
|
+
|
|
507
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
508
|
+
* Init order: tiles/palettes → oam_init → irq_init + II_VBLANK → TTE init
|
|
509
|
+
* → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the vblank IRQ
|
|
510
|
+
* registered (the #1 "frozen on frame 1" cause), and enabling DISPCNT
|
|
511
|
+
* layers before their tiles/maps exist flashes garbage. TTE owns BG1
|
|
512
|
+
* (CBB 2 / SBB 30) — keep other layers off those blocks.
|
|
513
|
+
* requires: nothing prior; this IS the boot. */
|
|
514
|
+
|
|
515
|
+
/* BG palette (bank 0). Vivid faceted jewels: each colour gets a 3-index
|
|
516
|
+
* slice — body / glint / rim. The GBA's 15-bit RGB gives saturated stones
|
|
517
|
+
* the GB/NES can only hint at. */
|
|
518
|
+
pal_bg_mem[0] = RGB15(2, 2, 5); /* backdrop / transparent base */
|
|
519
|
+
pal_bg_mem[1] = RGB15(7, 7, 10); /* cabinet dither A */
|
|
520
|
+
pal_bg_mem[2] = RGB15(4, 4, 7); /* cabinet dither B */
|
|
521
|
+
pal_bg_mem[3] = RGB15(2, 2, 3); /* HUD band near-black */
|
|
522
|
+
pal_bg_mem[BG_FRAME] = RGB15(10, 11, 14); /* frame steel */
|
|
523
|
+
pal_bg_mem[BG_FRAME+1] = RGB15(18, 19, 23); /* frame lip */
|
|
524
|
+
/* jewel palette slices: index (3*k + n), n in 1..3 → body/glint/rim.
|
|
525
|
+
* colour 1 = ruby, 2 = emerald, 3 = sapphire. */
|
|
526
|
+
pal_bg_mem[3*0+1] = RGB15(26, 4, 8); pal_bg_mem[3*0+2] = RGB15(31, 18, 20); pal_bg_mem[3*0+3] = RGB15(13, 1, 3);
|
|
527
|
+
pal_bg_mem[3*1+1] = RGB15(4, 24, 8); pal_bg_mem[3*1+2] = RGB15(20, 31, 18); pal_bg_mem[3*1+3] = RGB15(1, 11, 4);
|
|
528
|
+
pal_bg_mem[3*2+1] = RGB15(6, 12, 30); pal_bg_mem[3*2+2] = RGB15(20, 24, 31); pal_bg_mem[3*2+3] = RGB15(2, 4, 14);
|
|
529
|
+
|
|
530
|
+
/* BG tile graphics → char-block 0 (TTE uses CBB 2 — kept clear). */
|
|
531
|
+
tonccpy(&tile_mem[0][BG_BACK], bg_tile_back, sizeof(bg_tile_back));
|
|
532
|
+
tonccpy(&tile_mem[0][BG_BAND], bg_tile_band, sizeof(bg_tile_band));
|
|
533
|
+
tonccpy(&tile_mem[0][BG_FRAME], bg_tile_frame, sizeof(bg_tile_frame));
|
|
534
|
+
tonccpy(&tile_mem[0][BG_INNER], bg_tile_inner, sizeof(bg_tile_inner));
|
|
535
|
+
build_gem_tiles();
|
|
536
|
+
tonccpy(&tile_mem[0][BG_GEM1], gem_ram[0], sizeof(gem_ram[0]));
|
|
537
|
+
tonccpy(&tile_mem[0][BG_GEM2], gem_ram[1], sizeof(gem_ram[1]));
|
|
538
|
+
tonccpy(&tile_mem[0][BG_GEM3], gem_ram[2], sizeof(gem_ram[2]));
|
|
539
|
+
|
|
540
|
+
/* Trio sprite: the same faceted jewel shape at OBJ tile 1, drawn per
|
|
541
|
+
* colour via three OBJ palbanks (0/1/2) that mirror the BG jewel slices. */
|
|
542
|
+
tonccpy(&tile_mem[4][TILE_TRIO], gem_shape, sizeof(gem_shape));
|
|
543
|
+
/* The sprite tile uses the RAW gem_shape (nibbles 1/2/3 = body/glint/rim),
|
|
544
|
+
* so the trio's colour is picked by the OBJ PALBANK at draw time: bank k
|
|
545
|
+
* carries colour-(k+1)'s body/glint/rim at indices 1/2/3. One tile, three
|
|
546
|
+
* vivid jewels — exactly mirroring the BG jewel palette slices. */
|
|
547
|
+
for (k = 0; k < 3; k++) {
|
|
548
|
+
pal_obj_bank[k][1] = pal_bg_mem[3*k+1]; /* body */
|
|
549
|
+
pal_obj_bank[k][2] = pal_bg_mem[3*k+2]; /* glint */
|
|
550
|
+
pal_obj_bank[k][3] = pal_bg_mem[3*k+3]; /* rim */
|
|
551
|
+
}
|
|
281
552
|
|
|
282
|
-
|
|
283
|
-
|
|
553
|
+
REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(2);
|
|
554
|
+
|
|
555
|
+
oam_init(obj_buffer, 128); /* hides all 128 */
|
|
284
556
|
|
|
285
|
-
/* IRQ setup — required for VBlankIntrWait() to function. */
|
|
286
557
|
irq_init(NULL);
|
|
287
558
|
irq_add(II_VBLANK, NULL);
|
|
288
559
|
|
|
289
|
-
sfx_init();
|
|
290
|
-
|
|
291
|
-
|
|
560
|
+
sfx_init(); /* APU on; music loop ticks below */
|
|
561
|
+
|
|
562
|
+
/* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so text
|
|
563
|
+
* draws over everything. Mode 0 = all four BGs regular/tiled. */
|
|
564
|
+
tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
|
|
565
|
+
REG_BG1CNT |= BG_PRIO(0);
|
|
566
|
+
REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_OBJ | DCNT_OBJ_1D;
|
|
292
567
|
|
|
293
|
-
|
|
568
|
+
hiscore = hiscore_load(); /* cartridge SRAM — 0 on first boot */
|
|
569
|
+
enter_title();
|
|
294
570
|
|
|
295
571
|
while (1) {
|
|
572
|
+
/* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then
|
|
573
|
+
* commit OAM while still inside vblank (the update is far quicker than
|
|
574
|
+
* the 4.9ms vblank window). */
|
|
296
575
|
VBlankIntrWait();
|
|
297
576
|
key_poll();
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
if ((now & KEY_RIGHT) && !(prev & KEY_RIGHT)
|
|
307
|
-
&& !collides(piece_x + 1, piece_y)) piece_x++;
|
|
308
|
-
if ((now & KEY_A) && !(prev & KEY_A)) {
|
|
309
|
-
u8 t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
|
|
310
|
-
sfx_tone(2, 1400, 3); /* rotate click */
|
|
311
|
-
}
|
|
312
|
-
if ((now & KEY_START) && !(prev & KEY_START)) {
|
|
313
|
-
while (!collides(piece_x, piece_y + 1)) piece_y++;
|
|
314
|
-
lock_piece();
|
|
315
|
-
new_piece();
|
|
316
|
-
prev = now;
|
|
317
|
-
tte_erase_rect(88 + 6*8, 8, 88 + 11*8, 16);
|
|
318
|
-
draw_score(88 + 6*8, score);
|
|
319
|
-
continue;
|
|
577
|
+
sfx_music_tick(); /* forget this → silent game */
|
|
578
|
+
|
|
579
|
+
if (state == ST_TITLE) {
|
|
580
|
+
if (key_hit(KEY_START | KEY_A)) enter_play();
|
|
581
|
+
} else if (state == ST_OVER) {
|
|
582
|
+
if (key_hit(KEY_START)) enter_title();
|
|
583
|
+
} else {
|
|
584
|
+
update_play();
|
|
320
585
|
}
|
|
321
|
-
prev = now;
|
|
322
|
-
|
|
323
|
-
u16 fall_rate = (now & KEY_DOWN) ? 4 : 30;
|
|
324
|
-
if (++fall_timer >= fall_rate) {
|
|
325
|
-
fall_timer = 0;
|
|
326
|
-
if (collides(piece_x, piece_y + 1)) {
|
|
327
|
-
lock_piece();
|
|
328
|
-
new_piece();
|
|
329
|
-
} else {
|
|
330
|
-
piece_y++;
|
|
331
|
-
}
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
/* Re-draw piece in its new position. */
|
|
335
|
-
draw_piece(piece_x, piece_y, 0);
|
|
336
586
|
|
|
337
|
-
|
|
338
|
-
|
|
587
|
+
stage_sprites();
|
|
588
|
+
oam_copy(oam_mem, obj_buffer, 128);
|
|
339
589
|
}
|
|
340
590
|
return 0;
|
|
341
591
|
}
|