romdevtools 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -177
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -180
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -156
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +19 -6
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +64 -19
|
@@ -1,250 +1,815 @@
|
|
|
1
|
-
/* ── puzzle/main.c — MSX match-3
|
|
1
|
+
/* ── puzzle/main.c — MSX falling-trio match-3 puzzle (complete example game) ──
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
* the
|
|
3
|
+
* STOKE STACK — a COMPLETE, working game: title screen, 1P MARATHON mode
|
|
4
|
+
* (levels speed the fall as you clear) and 2P SIMULTANEOUS VERSUS mode —
|
|
5
|
+
* two 6x12 wells side by side, P1 on JOYSTICK PORT 1, P2 on JOYSTICK PORT 2,
|
|
6
|
+
* both falling at once, where every cascade CHAIN you score stokes the heat
|
|
7
|
+
* under your rival: a garbage row rises from the bottom of their well.
|
|
8
|
+
* Score + session hi-score, music + SFX on the AY-3-8910 PSG, and the MSX's
|
|
9
|
+
* signature SCREEN-2 PER-ROW COLOR: the two wells, the HUD band, and a
|
|
10
|
+
* one-tile vertical "ember" gradient seam come ENTIRELY from the three
|
|
11
|
+
* independent color thirds, costing zero extra tiles.
|
|
5
12
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
13
|
+
* The game: a falling-trio match-3. A vertical trio of gems drops into a well;
|
|
14
|
+
* LEFT/RIGHT move it, trigger A cycles its three colours, DOWN soft-drops,
|
|
15
|
+
* trigger B hard-drops. When it lands, any straight run of 3+ same-coloured
|
|
16
|
+
* gems (horizontal, vertical, or diagonal) clears; survivors fall and cascades
|
|
17
|
+
* chain for multiplied score. In versus, the stack that reaches the rim loses.
|
|
10
18
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
19
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
20
|
+
* very different one. The markers tell you what's what:
|
|
21
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented MSX footgun; reshape
|
|
22
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
23
|
+
* GAME LOGIC (clay) — match rules, garbage, scoring, tuning, art: reshape
|
|
24
|
+
* freely.
|
|
16
25
|
*
|
|
17
|
-
*
|
|
26
|
+
* What depends on what:
|
|
27
|
+
* msx_hw.h / msx_vdp.c — VDP + PSG + joystick helpers (direct Z80 ports;
|
|
28
|
+
* the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
|
|
29
|
+
* read msx_vdp.c before adding your own PSG pokes).
|
|
30
|
+
* msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
|
|
31
|
+
* INIT must NEVER return, so main() ends in for(;;).
|
|
32
|
+
*
|
|
33
|
+
* Frame budget — and a TEACHING POINT vs the Genesis version of this game
|
|
34
|
+
* (examples/genesis/templates/puzzle.c): the Genesis mirrors each well in RAM
|
|
35
|
+
* and repaints it as ONE queued DMA rect in vblank. The MSX has no DMA: every
|
|
36
|
+
* dirty cell is a per-byte VRAM port write (msx_vram_write). But screen-2 VRAM
|
|
37
|
+
* writes are CHEAP at C speed (~29 Z80 cycles between VDP accesses, and SDCC
|
|
38
|
+
* loops are slower than that — see TROUBLESHOOTING), and we only ever repaint
|
|
39
|
+
* the cells that CHANGED (the active trio + a board redraw after a lock), so a
|
|
40
|
+
* worst-case double cascade still lands inside one frame. Same genre, two
|
|
41
|
+
* bandwidth worlds — fork accordingly.
|
|
42
|
+
*
|
|
43
|
+
* Controls: JOYSTICK PORT 1 (or keyboard cursors) plays well 0 — LEFT/RIGHT
|
|
44
|
+
* move, trigger A (or SPACE) cycles colours, DOWN soft-drops, trigger B
|
|
45
|
+
* hard-drops. In 2P versus, JOYSTICK PORT 2 plays well 1 the same way. On
|
|
46
|
+
* the title screen trigger A starts 1P marathon; trigger B starts 2P versus.
|
|
47
|
+
*
|
|
48
|
+
* Hi-score honesty: the bundled bluemsx core build exposes NO battery save
|
|
49
|
+
* path (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so the
|
|
50
|
+
* hi-score lives in plain RAM: it survives title↔game cycles but NOT a
|
|
51
|
+
* power cycle / hardReset. Never fake persistence — if you need real saves,
|
|
52
|
+
* that's a future core round (ASCII8-SRAM mapper carts exist; the core just
|
|
53
|
+
* doesn't surface their RAM yet).
|
|
18
54
|
*/
|
|
19
55
|
#include "msx_hw.h"
|
|
20
56
|
|
|
21
|
-
/*
|
|
57
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
58
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
59
|
+
#define GAME_TITLE "STOKE STACK"
|
|
60
|
+
|
|
61
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
62
|
+
* Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
|
|
63
|
+
* the port ALSO clears the flag, so one read per frame = one game step per
|
|
64
|
+
* frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
|
|
65
|
+
* works even with interrupts masked, and never depends on the BIOS ISR
|
|
66
|
+
* keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
|
|
67
|
+
* flag first and this loop just waits for the next one; a one-frame hiccup,
|
|
68
|
+
* never a hang.) */
|
|
22
69
|
__sfr __at 0x99 VDPSTATUS;
|
|
23
70
|
static void vsync(void) {
|
|
24
|
-
(void)VDPSTATUS;
|
|
71
|
+
(void)VDPSTATUS; /* throw away a possibly-stale flag */
|
|
25
72
|
while (!(VDPSTATUS & 0x80)) {
|
|
26
73
|
}
|
|
27
74
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
#define
|
|
35
|
-
#define
|
|
36
|
-
#define
|
|
37
|
-
#define
|
|
38
|
-
#define
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
*
|
|
47
|
-
|
|
48
|
-
#define
|
|
49
|
-
#define
|
|
50
|
-
#define
|
|
51
|
-
#define
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
static
|
|
75
|
+
|
|
76
|
+
/* ── GAME LOGIC (clay — reshape freely) — board geometry ─────────────────────
|
|
77
|
+
* Two 6x12 wells on the 32x24 screen-2 name table. Cells are ONE 8x8 tile
|
|
78
|
+
* each (the MSX screen is 256x192 — a 6-wide well is 48 px, both wells plus a
|
|
79
|
+
* "VS" gap fit comfortably). Row 0 is the HUD band; the wells live in the air
|
|
80
|
+
* thirds and rest on the ground third. */
|
|
81
|
+
#define GRID_W 6
|
|
82
|
+
#define GRID_H 12
|
|
83
|
+
#define WELL_TOP 6 /* name-table row of the well's TOP interior cell */
|
|
84
|
+
#define WELL_1P_LX 13 /* 1P: single centered well, interior cols 13-18 */
|
|
85
|
+
#define WELL_VS_LX0 4 /* 2P: P1 well interior cols 4-9 ... */
|
|
86
|
+
#define WELL_VS_LX1 22 /* P2 well interior cols 22-27 (split board) */
|
|
87
|
+
|
|
88
|
+
#define EMPTY 0 /* cell colours 1..3 = ruby / emerald / sapphire */
|
|
89
|
+
|
|
90
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
91
|
+
* Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
|
|
92
|
+
* board tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits draw in
|
|
93
|
+
* the tile's FOREGROUND color, clear bits in its BACKGROUND color (both come
|
|
94
|
+
* from the screen-2 color table — see the per-row-color idiom below). */
|
|
95
|
+
#define T_SPACE 0
|
|
96
|
+
#define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
|
|
97
|
+
#define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
|
|
98
|
+
#define T_DASH 37
|
|
99
|
+
#define T_FIELD 38 /* empty well interior cell (recessed, faint speck)*/
|
|
100
|
+
#define T_FRAME 39 /* well border */
|
|
101
|
+
#define T_GEM 40 /* a locked gem cell (its COLOR picks the colour) */
|
|
102
|
+
#define T_EMBER 41 /* the per-8x1-row gradient strip (see below) */
|
|
103
|
+
#define NUM_TILES 42
|
|
104
|
+
|
|
105
|
+
static const uint8_t font[NUM_TILES][8] = {
|
|
106
|
+
/* SPACE */ {0,0,0,0,0,0,0,0},
|
|
107
|
+
/* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
|
|
108
|
+
/* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
|
|
109
|
+
/* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
|
|
110
|
+
/* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
|
|
111
|
+
/* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
|
|
112
|
+
/* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
|
|
113
|
+
/* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
|
|
114
|
+
/* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
|
|
115
|
+
/* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
116
|
+
/* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
|
|
117
|
+
/* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
|
|
118
|
+
/* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
|
|
119
|
+
/* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
|
|
120
|
+
/* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
|
|
121
|
+
/* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
122
|
+
/* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
|
|
123
|
+
/* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
|
|
124
|
+
/* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
|
|
125
|
+
/* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
|
|
126
|
+
/* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
|
127
|
+
/* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
128
|
+
/* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
|
|
129
|
+
/* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
|
|
130
|
+
/* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
|
|
131
|
+
/* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
|
|
132
|
+
/* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
|
|
133
|
+
/* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
|
|
134
|
+
/* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
135
|
+
/* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
|
|
136
|
+
/* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
|
|
137
|
+
/* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
|
|
138
|
+
/* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
|
|
139
|
+
/* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
|
|
140
|
+
/* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
|
|
141
|
+
/* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
|
|
142
|
+
/* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
|
|
143
|
+
/* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
|
144
|
+
/* 38 FIELD (recessed cell + faint speck) */
|
|
145
|
+
{0x00,0x00,0x00,0x00,0x10,0x00,0x00,0x00},
|
|
146
|
+
/* 39 FRAME (solid border) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
|
|
147
|
+
/* 40 GEM (rounded gem; its COLOR byte picks ruby/emerald/sapphire) */
|
|
148
|
+
{0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C},
|
|
149
|
+
/* 41 EMBER (solid fg — its COLOR bytes paint the gradient) */
|
|
150
|
+
{0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
154
|
+
* SCREEN-2 PER-ROW COLOR — the MSX's signature background trick, AND the
|
|
155
|
+
* reason a single GEM tile can show THREE colours.
|
|
156
|
+
*
|
|
157
|
+
* Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
|
|
158
|
+
*
|
|
159
|
+
* 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
|
|
160
|
+
* (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
|
|
161
|
+
* pattern table slice and its OWN 2KB color table slice:
|
|
162
|
+
* patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
|
|
163
|
+
* The SAME tile index can look completely different in each third. We
|
|
164
|
+
* exploit exactly that to draw the THREE gem colours from ONE T_GEM tile:
|
|
165
|
+
* the gem pattern is uploaded identically to all three thirds, but each
|
|
166
|
+
* third's color byte for T_GEM is a DIFFERENT foreground (ruby / emerald /
|
|
167
|
+
* sapphire). A gem's colour is therefore chosen by WHICH NAME-TABLE ROW
|
|
168
|
+
* it sits in — so the board is laid out three rows per colour band (see
|
|
169
|
+
* gem_tile_for / well row→third mapping below). One piece of art, three
|
|
170
|
+
* colours, zero extra tiles — the puzzle-genre twin of the shmup's
|
|
171
|
+
* depth-banded starfield.
|
|
172
|
+
*
|
|
173
|
+
* 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
|
|
174
|
+
* row — each packing (foreground<<4)|background from the fixed TMS9918
|
|
175
|
+
* palette. So one tile can carry an 8-color vertical gradient
|
|
176
|
+
* (T_EMBER's whole "forge glow" seam is a single tile, colors only).
|
|
177
|
+
*
|
|
178
|
+
* Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
|
|
179
|
+
* R4=0x03 — the "thirds" configuration), and pattern + color uploads to
|
|
180
|
+
* EVERY third a tile is used in. Tile N's slot is pattern[N*8] / color[N*8].
|
|
181
|
+
*
|
|
182
|
+
* TMS9918 fixed palette used here: 1 black, 4 dark blue, 5 light blue,
|
|
183
|
+
* 6 dark red, 8 cyan, 9 light red, 11 light yellow, 12 green, 14 gray,
|
|
184
|
+
* 15 white (high nibble = fg, low nibble = bg of each row byte). */
|
|
185
|
+
static const uint8_t col_text[3] = { 0xF4, 0xF1, 0xF1 }; /* HUD white-on-blue; title/play white-on-black */
|
|
186
|
+
static const uint8_t col_field[3] = { 0x41, 0x41, 0x41 }; /* recessed cell: dark-blue speck on black */
|
|
187
|
+
static const uint8_t col_frame[3] = { 0xE1, 0xE1, 0xE1 }; /* well border: gray on black, every third */
|
|
188
|
+
/* THE GEM-COLOUR-PER-THIRD trick: T_GEM's foreground in each third is a
|
|
189
|
+
* different gem colour. Board rows are bucketed into colour bands by third so
|
|
190
|
+
* a locked gem renders in its colour with no per-cell palette work:
|
|
191
|
+
* third 0 (top, rows 0-7) → ruby (fg 9 light red)
|
|
192
|
+
* third 1 (middle, rows 8-15)→ emerald (fg 12 green)
|
|
193
|
+
* third 2 (bottom, rows 16-23)→ sapphire(fg 5 light blue)
|
|
194
|
+
* ...so the in-well colour a gem SHOWS depends on its name-table row third.
|
|
195
|
+
* (gem_tile_for() picks the row a colour-c gem must occupy — see below.) */
|
|
196
|
+
static const uint8_t col_gem[3] = { 0x91, 0xC1, 0x51 }; /* ruby / emerald / sapphire, each on black */
|
|
197
|
+
/* T_EMBER: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row forge
|
|
198
|
+
* glow (black → dark red → light red → yellow → white and back down). The
|
|
199
|
+
* pattern is solid 0xFF so only the fg nibbles show. Drawn as the seam row
|
|
200
|
+
* directly under each well. */
|
|
201
|
+
static const uint8_t col_ember[8] = { 0x11,0x61,0x91,0xB1,0xF1,0xB1,0x91,0x61 };
|
|
202
|
+
|
|
203
|
+
static void load_tiles(void) {
|
|
204
|
+
uint8_t third, i;
|
|
205
|
+
uint16_t patbase, colbase;
|
|
206
|
+
for (third = 0; third < 3; third++) {
|
|
207
|
+
patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
|
|
208
|
+
colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
|
|
209
|
+
for (i = 0; i < NUM_TILES; i++) {
|
|
210
|
+
uint8_t col;
|
|
211
|
+
/* pattern bits are the same in every third — only COLOR varies */
|
|
212
|
+
msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
|
|
213
|
+
if (i == T_EMBER) { /* the one per-pixel-row gradient */
|
|
214
|
+
msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_ember, 8);
|
|
215
|
+
continue;
|
|
216
|
+
}
|
|
217
|
+
if (i == T_FIELD) col = col_field[third];
|
|
218
|
+
else if (i == T_FRAME) col = col_frame[third];
|
|
219
|
+
else if (i == T_GEM) col = col_gem[third]; /* colour per third */
|
|
220
|
+
else col = col_text[third];
|
|
221
|
+
msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/* ── GAME LOGIC (clay — reshape freely) — name-table drawing helpers ────────
|
|
227
|
+
* Screen 2 VRAM writes are safe at any point in the frame at C speed: the
|
|
228
|
+
* TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
|
|
229
|
+
* and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
|
|
230
|
+
* are the thing that outruns the VDP — see TROUBLESHOOTING.) */
|
|
231
|
+
static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
|
|
232
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
236
|
+
uint8_t buf[32];
|
|
237
|
+
uint8_t n = 0;
|
|
238
|
+
while (*s && n < 32) {
|
|
239
|
+
char c = *s++;
|
|
240
|
+
if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
|
|
241
|
+
else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
|
|
242
|
+
else if (c == '-') buf[n] = T_DASH;
|
|
243
|
+
else buf[n] = T_SPACE;
|
|
244
|
+
n++;
|
|
245
|
+
}
|
|
246
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
|
|
250
|
+
uint8_t buf[4];
|
|
251
|
+
buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
|
|
252
|
+
buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
|
|
253
|
+
buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
|
|
254
|
+
buf[3] = (uint8_t)(T_0 + v % 10);
|
|
255
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────────
|
|
259
|
+
* Boards are plain static arrays. The falling trio is drawn with SPRITES (so
|
|
260
|
+
* it floats over the well tiles without disturbing them); locked gems are
|
|
261
|
+
* name-table tiles. */
|
|
262
|
+
static uint8_t grid[2][GRID_H][GRID_W]; /* the two wells (well 1 unused in 1P) */
|
|
263
|
+
static int8_t piece_x[2]; /* falling trio: column 0..5 */
|
|
264
|
+
static int8_t piece_y[2]; /* row of its TOP cell (<0 above rim) */
|
|
265
|
+
static uint8_t piece_col[2][3]; /* trio colours, top to bottom */
|
|
266
|
+
static uint8_t fall_t[2]; /* frames until next gravity step */
|
|
267
|
+
static uint16_t score[2];
|
|
268
|
+
static uint16_t hiscore; /* SESSION-ONLY: plain RAM. The bundled
|
|
269
|
+
* bluemsx build exposes no SAVE_RAM region,
|
|
270
|
+
* so there is nothing battery-backed to
|
|
271
|
+
* write — survives title↔game cycles, not a
|
|
272
|
+
* power cycle (honest, not faked). */
|
|
273
|
+
static uint8_t level; /* 1P: 1..9, speeds up the fall */
|
|
274
|
+
static uint16_t cleared_total; /* 1P: gems cleared, drives the level */
|
|
275
|
+
static uint8_t well_lx[2]; /* left interior name-table col per well */
|
|
276
|
+
static uint8_t two_player; /* mode chosen on the title screen */
|
|
59
277
|
static uint16_t rng;
|
|
60
|
-
static uint8_t blip;
|
|
61
278
|
|
|
62
|
-
|
|
279
|
+
#define ST_TITLE 0
|
|
280
|
+
#define ST_PLAY 1
|
|
281
|
+
#define ST_OVER 2
|
|
282
|
+
static uint8_t state;
|
|
283
|
+
static uint8_t over_loser; /* 2P: which well topped out (P that LOST) */
|
|
284
|
+
static uint8_t prev_t1, prev_t2; /* title/over trigger edge detection */
|
|
285
|
+
/* per-player edge memory for in-play input (move/rotate/drop) */
|
|
286
|
+
static uint8_t prev_dir[2], prev_a[2], prev_b[2];
|
|
287
|
+
|
|
288
|
+
#define FALL_VS 24 /* 2P: fixed gravity (frames per row) */
|
|
289
|
+
#define GARBAGE_CAP 4 /* max garbage rows per attack */
|
|
290
|
+
|
|
291
|
+
/* xorshift16 PRNG — a few dozen cycles, no tables. */
|
|
292
|
+
static uint8_t next_rand(void) {
|
|
63
293
|
rng ^= (uint16_t)(rng << 7);
|
|
64
294
|
rng ^= (uint16_t)(rng >> 9);
|
|
65
295
|
rng ^= (uint16_t)(rng << 8);
|
|
66
|
-
return rng;
|
|
296
|
+
return (uint8_t)(rng & 0xFF);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/* ── GAME LOGIC (clay — reshape freely) — music + SFX on the AY-3-8910 ──────
|
|
300
|
+
* Channel plan: A = move/rotate/clear blips, B = lock/garbage noise, C =
|
|
301
|
+
* music. The PSG has 3 tone channels + ONE shared noise generator, mixed
|
|
302
|
+
* per-channel in reg 7. All register traffic goes through msx_psg_tone/noise/
|
|
303
|
+
* off — they wrap the PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT
|
|
304
|
+
* ISR clobbers the PSG address latch every frame (the bug that once silenced
|
|
305
|
+
* every MSX scaffold — see msx_vdp.c).
|
|
306
|
+
*
|
|
307
|
+
* The tune: one period entry per half-beat, 0 = rest. AY period =
|
|
308
|
+
* 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame; a
|
|
309
|
+
* note advances every 8 frames. The lib's built-in demo loop (msx_music_tick)
|
|
310
|
+
* also uses channel C, so we switch it OFF in main() and run THIS table
|
|
311
|
+
* instead — edit this table to rescore. */
|
|
312
|
+
static const uint16_t tune[32] = {
|
|
313
|
+
339, 0, 285, 339, 427, 0, 339, 285, /* E4 G4 E4 C4 E4 G4 (steady groove) */
|
|
314
|
+
254, 0, 285, 339, 285, 0, 0, 0, /* A4 G4 E4 G4 rest */
|
|
315
|
+
380, 0, 339, 285, 254, 0, 285, 339, /* D4 E4 G4 A4 G4 E4 */
|
|
316
|
+
427, 0, 339, 285, 339, 0, 0, 0, /* C4 E4 G4 E4 rest */
|
|
317
|
+
};
|
|
318
|
+
static uint8_t music_step, music_timer;
|
|
319
|
+
static uint8_t sfx_a_t, sfx_b_t; /* frames left on the A/B SFX channels */
|
|
320
|
+
|
|
321
|
+
static void music_tick(void) {
|
|
322
|
+
if (music_timer == 0) {
|
|
323
|
+
uint16_t p = tune[music_step & 31];
|
|
324
|
+
if (p) msx_psg_tone(2, p, 9);
|
|
325
|
+
else msx_psg_off(2);
|
|
326
|
+
music_step++;
|
|
327
|
+
}
|
|
328
|
+
music_timer++;
|
|
329
|
+
if (music_timer >= 8) music_timer = 0;
|
|
67
330
|
}
|
|
68
|
-
static uint8_t rand_color(void) { return (uint8_t)(1 + (xorshift() % 3)); }
|
|
69
331
|
|
|
70
|
-
static
|
|
71
|
-
if (
|
|
72
|
-
if (
|
|
73
|
-
if (c == 3) return T_B;
|
|
74
|
-
return T_FIELD; /* empty cell shows the dim field, not the backdrop */
|
|
332
|
+
static void sfx_tick(void) {
|
|
333
|
+
if (sfx_a_t) { sfx_a_t--; if (!sfx_a_t) msx_psg_off(0); }
|
|
334
|
+
if (sfx_b_t) { sfx_b_t--; if (!sfx_b_t) msx_psg_noise(1, 0, 0); }
|
|
75
335
|
}
|
|
76
336
|
|
|
77
|
-
static void
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
337
|
+
static void sfx_move(void) { msx_psg_tone(0, 0x300, 8); sfx_a_t = 2; }
|
|
338
|
+
static void sfx_rotate(void){ msx_psg_tone(0, 0x200, 9); sfx_a_t = 3; }
|
|
339
|
+
static void sfx_clear(uint8_t chain) {
|
|
340
|
+
/* pitch rises with chain depth (smaller period = higher note) */
|
|
341
|
+
uint16_t p = (uint16_t)(0x180 - ((uint16_t)chain << 5));
|
|
342
|
+
msx_psg_tone(0, p, 12); sfx_a_t = 6;
|
|
343
|
+
}
|
|
344
|
+
static void sfx_lock(void) { msx_psg_tone(1, 0x040, 10); sfx_b_t = 3; }
|
|
345
|
+
static void sfx_garbage(void){ msx_psg_noise(1, 16, 13); sfx_b_t = 8; }
|
|
346
|
+
static void sfx_over(void) { msx_psg_noise(1, 28, 14); sfx_b_t = 22; }
|
|
347
|
+
|
|
348
|
+
/* ── GAME LOGIC (clay — reshape freely) — well row→third gem colour ──────────
|
|
349
|
+
* The screen-2 thirds idiom means a gem's COLOUR is decided by which screen
|
|
350
|
+
* third (name-table row band) its cell lands in. The well spans name-table
|
|
351
|
+
* rows WELL_TOP..WELL_TOP+11 (6..17), which straddles two thirds:
|
|
352
|
+
* rows 6,7 → third 0 → ruby
|
|
353
|
+
* rows 8..15 → third 1 → emerald
|
|
354
|
+
* rows 16,17 → third 2 → sapphire
|
|
355
|
+
* That is a fixed visual banding of the SHARED T_GEM tile, NOT the logical
|
|
356
|
+
* gem colour (the logical colour lives in grid[]). For a forked game that
|
|
357
|
+
* wants logical colour == shown colour at every cell, give each colour its own
|
|
358
|
+
* tile index and upload three patterns instead — costs 2 extra tiles. Here we
|
|
359
|
+
* keep the one-tile trick and accept the painterly banding; matches still run
|
|
360
|
+
* on the LOGICAL grid[] colours, so play is unaffected. */
|
|
361
|
+
static uint8_t well_row(uint8_t r) { return (uint8_t)(WELL_TOP + r); }
|
|
362
|
+
|
|
363
|
+
/* draw one well interior cell (logical grid[p][r][c]) into the name table */
|
|
364
|
+
static void draw_cell(uint8_t p, uint8_t r, uint8_t c) {
|
|
365
|
+
uint8_t tile = grid[p][r][c] ? T_GEM : T_FIELD;
|
|
366
|
+
put_tile((uint8_t)(well_lx[p] + c), well_row(r), tile);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
/* repaint a full well interior from grid[] (used after a lock/cascade) */
|
|
370
|
+
static void draw_well(uint8_t p) {
|
|
371
|
+
uint8_t r, c;
|
|
372
|
+
for (r = 0; r < GRID_H; r++)
|
|
373
|
+
for (c = 0; c < GRID_W; c++) draw_cell(p, r, c);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/* paint a well's gray frame (top/bottom/sides) + the ember seam under it */
|
|
377
|
+
static void paint_frame(uint8_t p) {
|
|
378
|
+
uint8_t r, lx = well_lx[p];
|
|
379
|
+
uint8_t top = (uint8_t)(WELL_TOP - 1), bot = (uint8_t)(WELL_TOP + GRID_H);
|
|
380
|
+
uint8_t c;
|
|
381
|
+
for (c = 0; c < GRID_W + 2; c++) {
|
|
382
|
+
put_tile((uint8_t)(lx - 1 + c), top, T_FRAME);
|
|
383
|
+
put_tile((uint8_t)(lx - 1 + c), bot, T_FRAME);
|
|
384
|
+
}
|
|
385
|
+
for (r = 0; r < GRID_H; r++) {
|
|
386
|
+
put_tile((uint8_t)(lx - 1), well_row(r), T_FRAME);
|
|
387
|
+
put_tile((uint8_t)(lx + GRID_W), well_row(r), T_FRAME);
|
|
388
|
+
}
|
|
389
|
+
/* ember seam: the gradient tile row directly beneath the well frame */
|
|
390
|
+
for (c = 0; c < GRID_W + 2; c++)
|
|
391
|
+
put_tile((uint8_t)(lx - 1 + c), (uint8_t)(bot + 1), T_EMBER);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/* ── GAME LOGIC (clay — reshape freely) — sprites: the falling trio ──────────
|
|
395
|
+
* 8x8 one-color hardware sprites — 3 per player (the three gems of the active
|
|
396
|
+
* trio). Plane layout: 0-2 = P1 trio, 3-5 = P2 trio. Locked gems are tiles,
|
|
397
|
+
* not sprites, so the well never needs more than 6 sprite planes. */
|
|
398
|
+
static const uint8_t spr_gem[8] = {0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C};
|
|
399
|
+
#define PAT_GEM 0
|
|
400
|
+
/* sprite colour per logical gem colour 1..3 (matches the tile banding intent) */
|
|
401
|
+
static const uint8_t spr_col[4] = { 15, 9, 12, 5 }; /* [0] unused; ruby/emerald/sapphire */
|
|
402
|
+
|
|
403
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
404
|
+
* Sprite limits + the Y=208 terminator:
|
|
405
|
+
* - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
|
|
406
|
+
* attribute table — every higher-numbered plane vanishes, not just that
|
|
407
|
+
* one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
|
|
408
|
+
* END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
|
|
409
|
+
* PARK_Y (192 = first line below the display) — never at 0xD0.
|
|
410
|
+
* (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
|
|
411
|
+
* is "just offscreen" — code that leans on that breaks on MSX1.)
|
|
412
|
+
* - Per scanline the TMS9918 draws only 4 sprites (V9938: 8). The two trios
|
|
413
|
+
* never share a scanline (the wells are side by side), and one trio is 3
|
|
414
|
+
* vertically-stacked gems = 3 sprites on different rows, so a row pileup
|
|
415
|
+
* can't exceed 1-2 here. */
|
|
416
|
+
#define PARK_Y 192
|
|
417
|
+
|
|
418
|
+
/* cell (col,row in a well) → screen pixel position of its sprite */
|
|
419
|
+
static uint8_t cell_px(uint8_t p, int8_t c) { return (uint8_t)((well_lx[p] + c) * 8); }
|
|
420
|
+
static uint8_t cell_py(int8_t r) { return (uint8_t)((WELL_TOP + r) * 8); }
|
|
421
|
+
|
|
422
|
+
/* Push the two trios to their sprite planes. A gem above the rim (row < 0) or
|
|
423
|
+
* an inactive well parks offscreen at PARK_Y, NEVER 0xD0 — see the idiom. */
|
|
424
|
+
static void push_sprites(void) {
|
|
425
|
+
uint8_t p, i, plane;
|
|
426
|
+
for (p = 0; p < 2; p++) {
|
|
427
|
+
uint8_t active = (state == ST_PLAY) && (p == 0 || two_player);
|
|
428
|
+
for (i = 0; i < 3; i++) {
|
|
429
|
+
int8_t r = (int8_t)(piece_y[p] + (int8_t)i);
|
|
430
|
+
plane = (uint8_t)(p * 3 + i);
|
|
431
|
+
if (active && r >= 0 && r < GRID_H)
|
|
432
|
+
msx_set_sprite(plane, cell_px(p, piece_x[p]), cell_py(r),
|
|
433
|
+
PAT_GEM, spr_col[piece_col[p][i]]);
|
|
434
|
+
else
|
|
435
|
+
msx_set_sprite(plane, cell_px(p, piece_x[p]), PARK_Y,
|
|
436
|
+
PAT_GEM, spr_col[piece_col[p][i]]);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
100
439
|
}
|
|
101
440
|
|
|
102
|
-
/*
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
441
|
+
/* ── GAME LOGIC (clay — reshape freely) — HUD ──────────────────────────────
|
|
442
|
+
* Row 0 = the HUD band (third 0's text colors make it a distinct strip).
|
|
443
|
+
* 1P: SC=score, HI=hi-score, LV=level. 2P: P1 score, HI, P2 score. */
|
|
444
|
+
static void draw_hud_labels(void) {
|
|
445
|
+
if (two_player) {
|
|
446
|
+
draw_text(1, 0, "P1");
|
|
447
|
+
draw_text(13, 0, "HI");
|
|
448
|
+
draw_text(25, 0, "P2");
|
|
449
|
+
} else {
|
|
450
|
+
draw_text(1, 0, "SC");
|
|
451
|
+
draw_text(13, 0, "HI");
|
|
452
|
+
draw_text(25, 0, "LV");
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
static void draw_scores(void) {
|
|
456
|
+
if (two_player) { draw_num4(4, 0, score[0]); draw_num4(28, 0, score[1]); }
|
|
457
|
+
else { draw_num4(4, 0, score[0]); }
|
|
106
458
|
}
|
|
459
|
+
static void draw_hi(void) { draw_num4(16, 0, hiscore); }
|
|
460
|
+
static void draw_level(void) { if (!two_player) put_tile(28, 0, (uint8_t)(T_0 + level)); }
|
|
107
461
|
|
|
108
|
-
/*
|
|
109
|
-
*
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
462
|
+
/* ── GAME LOGIC (clay — reshape freely) — match scan ────────────────────────
|
|
463
|
+
* Mark every straight run of 3+ same-coloured gems in all 4 directions (a
|
|
464
|
+
* cell can belong to several runs — the mask de-dupes), and return how many
|
|
465
|
+
* cells matched. Runs on the LOGICAL grid[] colours (independent of the
|
|
466
|
+
* thirds tile banding). */
|
|
467
|
+
static uint8_t matched[GRID_H][GRID_W];
|
|
468
|
+
static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
469
|
+
|
|
470
|
+
static uint8_t mark_and_count(uint8_t p) {
|
|
471
|
+
uint8_t r, c, d, len, k, cnt, col;
|
|
472
|
+
int8_t dr, dc;
|
|
473
|
+
int16_t sr, sc;
|
|
474
|
+
cnt = 0;
|
|
475
|
+
for (r = 0; r < GRID_H; r++)
|
|
476
|
+
for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
|
|
477
|
+
for (r = 0; r < GRID_H; r++) {
|
|
478
|
+
for (c = 0; c < GRID_W; c++) {
|
|
479
|
+
col = grid[p][r][c];
|
|
480
|
+
if (col == EMPTY) continue;
|
|
481
|
+
for (d = 0; d < 4; d++) {
|
|
482
|
+
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
483
|
+
sr = (int16_t)r - dr; sc = (int16_t)c - dc;
|
|
484
|
+
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
485
|
+
&& grid[p][sr][sc] == col) continue; /* not the run's start */
|
|
486
|
+
len = 1;
|
|
487
|
+
sr = (int16_t)r + dr; sc = (int16_t)c + dc;
|
|
488
|
+
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
489
|
+
&& grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
490
|
+
if (len >= 3) {
|
|
491
|
+
sr = r; sc = c;
|
|
492
|
+
for (k = 0; k < len; k++) {
|
|
493
|
+
if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
|
|
494
|
+
sr += dr; sc += dc;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
117
498
|
}
|
|
118
499
|
}
|
|
500
|
+
return cnt;
|
|
119
501
|
}
|
|
120
502
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
503
|
+
/* Collapse each column so survivors rest on the floor. */
|
|
504
|
+
static void apply_gravity(uint8_t p) {
|
|
505
|
+
uint8_t c;
|
|
506
|
+
int8_t r, w;
|
|
507
|
+
for (c = 0; c < GRID_W; c++) {
|
|
508
|
+
w = GRID_H - 1;
|
|
509
|
+
for (r = GRID_H - 1; r >= 0; r--) {
|
|
510
|
+
if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
|
|
511
|
+
}
|
|
512
|
+
for (; w >= 0; w--) grid[p][w][c] = EMPTY;
|
|
513
|
+
}
|
|
125
514
|
}
|
|
126
515
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
516
|
+
/* forward decls for the clear→attack→end chain */
|
|
517
|
+
static void game_over(uint8_t loser);
|
|
518
|
+
static void garbage_insert(uint8_t v, uint8_t nrows);
|
|
519
|
+
static void spawn_piece(uint8_t p);
|
|
520
|
+
|
|
521
|
+
/* ── GAME LOGIC (clay — reshape freely) — clear matches, drop survivors,
|
|
522
|
+
* chain cascades. Returns the chain depth (0 = the lock matched nothing). ── */
|
|
523
|
+
static uint8_t resolve_board(uint8_t p) {
|
|
524
|
+
uint8_t n, r, c, chain;
|
|
525
|
+
uint16_t amt;
|
|
526
|
+
chain = 0;
|
|
527
|
+
for (;;) {
|
|
528
|
+
n = mark_and_count(p);
|
|
529
|
+
if (n == 0) break;
|
|
530
|
+
++chain;
|
|
531
|
+
for (r = 0; r < GRID_H; r++)
|
|
532
|
+
for (c = 0; c < GRID_W; c++)
|
|
533
|
+
if (matched[r][c]) grid[p][r][c] = EMPTY;
|
|
534
|
+
amt = (uint16_t)n * 10;
|
|
535
|
+
if (chain > 1) amt = (uint16_t)(amt * chain); /* cascades pay more */
|
|
536
|
+
if (score[p] < 9999) {
|
|
537
|
+
score[p] = (uint16_t)(score[p] + amt);
|
|
538
|
+
if (score[p] > 9999) score[p] = 9999;
|
|
539
|
+
}
|
|
540
|
+
sfx_clear(chain);
|
|
541
|
+
apply_gravity(p);
|
|
542
|
+
if (!two_player) {
|
|
543
|
+
cleared_total += n;
|
|
544
|
+
while (level < 9 && cleared_total >= (uint16_t)level * 10) {
|
|
545
|
+
++level;
|
|
546
|
+
draw_level();
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
draw_scores();
|
|
550
|
+
}
|
|
551
|
+
return chain;
|
|
133
552
|
}
|
|
134
553
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
554
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
|
|
555
|
+
* (pieces enter from above); below the floor or on a gem is not. */
|
|
556
|
+
static uint8_t can_place(uint8_t p, int8_t x, int8_t y) {
|
|
557
|
+
int8_t i, cy;
|
|
558
|
+
if (x < 0 || x >= GRID_W) return 0;
|
|
139
559
|
for (i = 0; i < 3; i++) {
|
|
140
|
-
|
|
141
|
-
if (
|
|
142
|
-
if (
|
|
560
|
+
cy = (int8_t)(y + i);
|
|
561
|
+
if (cy < 0) continue;
|
|
562
|
+
if (cy >= GRID_H) return 0;
|
|
563
|
+
if (grid[p][cy][x] != EMPTY) return 0;
|
|
143
564
|
}
|
|
144
|
-
return
|
|
565
|
+
return 1;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
static void spawn_piece(uint8_t p) {
|
|
569
|
+
piece_x[p] = GRID_W / 2;
|
|
570
|
+
piece_y[p] = -2;
|
|
571
|
+
piece_col[p][0] = (uint8_t)(1 + next_rand() % 3);
|
|
572
|
+
piece_col[p][1] = (uint8_t)(1 + next_rand() % 3);
|
|
573
|
+
piece_col[p][2] = (uint8_t)(1 + next_rand() % 3);
|
|
574
|
+
prev_a[p] = prev_b[p] = 1; /* swallow the drop button that just locked */
|
|
575
|
+
if (!can_place(p, piece_x[p], piece_y[p])) game_over(p);
|
|
145
576
|
}
|
|
146
577
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
578
|
+
/* ── GAME LOGIC (clay — reshape freely) — land the trio, resolve, attack,
|
|
579
|
+
* respawn. ── */
|
|
580
|
+
static void lock_piece(uint8_t p) {
|
|
581
|
+
int8_t i, y;
|
|
582
|
+
uint8_t chain;
|
|
151
583
|
for (i = 0; i < 3; i++) {
|
|
152
|
-
|
|
153
|
-
if (
|
|
584
|
+
y = (int8_t)(piece_y[p] + i);
|
|
585
|
+
if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
|
|
154
586
|
}
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
msx_psg_tone(0, 0x180, 13);
|
|
164
|
-
blip = 8;
|
|
165
|
-
}
|
|
166
|
-
}
|
|
587
|
+
sfx_lock();
|
|
588
|
+
if (piece_y[p] < 0) { draw_well(p); game_over(p); return; } /* locked above rim */
|
|
589
|
+
chain = resolve_board(p);
|
|
590
|
+
draw_well(p);
|
|
591
|
+
if (state != ST_PLAY) return;
|
|
592
|
+
if (chain && two_player) {
|
|
593
|
+
garbage_insert((uint8_t)(p ^ 1), chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
|
|
594
|
+
if (state != ST_PLAY) return; /* garbage topped them out */
|
|
167
595
|
}
|
|
168
|
-
|
|
596
|
+
spawn_piece(p);
|
|
169
597
|
}
|
|
170
598
|
|
|
171
|
-
|
|
172
|
-
|
|
599
|
+
/* ── GAME LOGIC (clay — reshape freely) — VERSUS attack: garbage rows rise
|
|
600
|
+
* from the bottom of the victim's well (random gems with one gap — matchable,
|
|
601
|
+
* so a skilled victim digs out). If the top row is already occupied the victim
|
|
602
|
+
* tops out and loses. ── */
|
|
603
|
+
static void garbage_insert(uint8_t v, uint8_t nrows) {
|
|
604
|
+
uint8_t k, c, gap;
|
|
173
605
|
int8_t r;
|
|
174
|
-
|
|
175
|
-
for (
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
606
|
+
sfx_garbage();
|
|
607
|
+
for (k = 0; k < nrows; k++) {
|
|
608
|
+
for (c = 0; c < GRID_W; c++)
|
|
609
|
+
if (grid[v][0][c] != EMPTY) { draw_well(v); game_over(v); return; }
|
|
610
|
+
for (r = 0; r < GRID_H - 1; r++)
|
|
611
|
+
for (c = 0; c < GRID_W; c++)
|
|
612
|
+
grid[v][r][c] = grid[v][r + 1][c];
|
|
613
|
+
gap = (uint8_t)(next_rand() % GRID_W);
|
|
614
|
+
for (c = 0; c < GRID_W; c++)
|
|
615
|
+
grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (uint8_t)(1 + next_rand() % 3);
|
|
616
|
+
if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
|
|
617
|
+
}
|
|
618
|
+
draw_well(v);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* ── GAME LOGIC (clay — reshape freely) — per-player input + gravity ─────────
|
|
622
|
+
* P0 reads JOYSTICK PORT 1 (keyboard cursors fall back); P1 reads PORT 2.
|
|
623
|
+
* Edge-triggered moves (one cell per press), held DOWN soft-drops, trigger A
|
|
624
|
+
* cycles the trio's colours, trigger B hard-drops. */
|
|
625
|
+
static void update_player(uint8_t p) {
|
|
626
|
+
uint8_t dir, a, b, fd, soft, moved_l, moved_r, t;
|
|
627
|
+
if (p == 0) {
|
|
628
|
+
dir = msx_read_joystick(1);
|
|
629
|
+
if (dir == STICK_CENTER) dir = msx_read_joystick(0);
|
|
630
|
+
a = (uint8_t)(gttrig(1) || gttrig(0)); /* trig A or SPACE */
|
|
631
|
+
b = gttrig(3); /* port-1 button 2 */
|
|
632
|
+
} else {
|
|
633
|
+
dir = msx_read_joystick(2);
|
|
634
|
+
a = gttrig(2); /* port-2 trigger A */
|
|
635
|
+
b = gttrig(4); /* port-2 button 2 */
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
moved_l = (uint8_t)(dir == STICK_LEFT || dir == STICK_UL || dir == STICK_DL);
|
|
639
|
+
moved_r = (uint8_t)(dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR);
|
|
640
|
+
soft = (uint8_t)(dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR);
|
|
641
|
+
|
|
642
|
+
if (moved_l && !(prev_dir[p] & 1) && can_place(p, (int8_t)(piece_x[p] - 1), piece_y[p])) {
|
|
643
|
+
--piece_x[p]; sfx_move();
|
|
644
|
+
}
|
|
645
|
+
if (moved_r && !(prev_dir[p] & 2) && can_place(p, (int8_t)(piece_x[p] + 1), piece_y[p])) {
|
|
646
|
+
++piece_x[p]; sfx_move();
|
|
647
|
+
}
|
|
648
|
+
prev_dir[p] = (uint8_t)((moved_l ? 1 : 0) | (moved_r ? 2 : 0));
|
|
649
|
+
|
|
650
|
+
if (a && !prev_a[p]) { /* cycle colours downward */
|
|
651
|
+
t = piece_col[p][2];
|
|
652
|
+
piece_col[p][2] = piece_col[p][1];
|
|
653
|
+
piece_col[p][1] = piece_col[p][0];
|
|
654
|
+
piece_col[p][0] = t;
|
|
655
|
+
sfx_rotate();
|
|
656
|
+
}
|
|
657
|
+
prev_a[p] = a;
|
|
658
|
+
|
|
659
|
+
if (b && !prev_b[p]) { /* hard drop */
|
|
660
|
+
prev_b[p] = b;
|
|
661
|
+
while (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1))) ++piece_y[p];
|
|
662
|
+
lock_piece(p); /* may end the game */
|
|
663
|
+
return;
|
|
664
|
+
}
|
|
665
|
+
prev_b[p] = b;
|
|
666
|
+
|
|
667
|
+
/* gravity: soft-drop adds extra ticks; level/mode set the base rate */
|
|
668
|
+
fd = two_player ? FALL_VS : (uint8_t)(32 - ((level << 1) + level)); /* 29..5 */
|
|
669
|
+
fall_t[p] = (uint8_t)(fall_t[p] + (soft ? 4 : 1));
|
|
670
|
+
if (fall_t[p] >= fd) {
|
|
671
|
+
fall_t[p] = 0;
|
|
672
|
+
if (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1)))
|
|
673
|
+
++piece_y[p];
|
|
674
|
+
else
|
|
675
|
+
lock_piece(p); /* may end the game */
|
|
180
676
|
}
|
|
181
677
|
}
|
|
182
678
|
|
|
679
|
+
/* ── GAME LOGIC (clay — reshape freely) — screens ──────────────────────────
|
|
680
|
+
* Title rows land in third 1 / third 2 — recolored for free by the thirds
|
|
681
|
+
* idiom. A clean name table behind the text. */
|
|
682
|
+
static void clear_field(void) { msx_fill_vram(VRAM_NAME, 32u * 24u, T_SPACE); }
|
|
683
|
+
|
|
684
|
+
static void paint_title(void) {
|
|
685
|
+
uint8_t len = 0, col;
|
|
686
|
+
const char *p = GAME_TITLE;
|
|
687
|
+
while (*p++) len++;
|
|
688
|
+
col = (uint8_t)((32 - len) / 2);
|
|
689
|
+
clear_field();
|
|
690
|
+
draw_text(col, 6, GAME_TITLE);
|
|
691
|
+
draw_text(7, 11, "1P START - FIRE A");
|
|
692
|
+
draw_text(7, 13, "2P VERSUS - FIRE B");
|
|
693
|
+
draw_text(12, 18, "HI 0000"); /* the space blanks the cell between */
|
|
694
|
+
draw_num4(15, 18, hiscore);
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
static void paint_play(void) {
|
|
698
|
+
clear_field();
|
|
699
|
+
paint_frame(0);
|
|
700
|
+
draw_well(0);
|
|
701
|
+
if (two_player) {
|
|
702
|
+
paint_frame(1);
|
|
703
|
+
draw_well(1);
|
|
704
|
+
draw_text(15, 12, "VS");
|
|
705
|
+
}
|
|
706
|
+
draw_hud_labels();
|
|
707
|
+
draw_scores();
|
|
708
|
+
draw_hi();
|
|
709
|
+
draw_level();
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
static void start_game(uint8_t versus) {
|
|
713
|
+
uint8_t p, r, c;
|
|
714
|
+
two_player = versus;
|
|
715
|
+
well_lx[0] = versus ? WELL_VS_LX0 : WELL_1P_LX;
|
|
716
|
+
well_lx[1] = WELL_VS_LX1;
|
|
717
|
+
for (p = 0; p < 2; p++) {
|
|
718
|
+
for (r = 0; r < GRID_H; r++)
|
|
719
|
+
for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
|
|
720
|
+
fall_t[p] = 0;
|
|
721
|
+
score[p] = 0;
|
|
722
|
+
piece_x[p] = GRID_W / 2;
|
|
723
|
+
piece_y[p] = -2;
|
|
724
|
+
prev_dir[p] = 0;
|
|
725
|
+
prev_a[p] = prev_b[p] = 1; /* swallow the button that started us */
|
|
726
|
+
}
|
|
727
|
+
cleared_total = 0;
|
|
728
|
+
level = 1;
|
|
729
|
+
state = ST_PLAY;
|
|
730
|
+
paint_play();
|
|
731
|
+
spawn_piece(0);
|
|
732
|
+
if (versus) spawn_piece(1);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
static void game_over(uint8_t loser) {
|
|
736
|
+
uint16_t best = score[0];
|
|
737
|
+
if (two_player && score[1] > best) best = score[1];
|
|
738
|
+
if (best > hiscore) { hiscore = best; }
|
|
739
|
+
over_loser = loser;
|
|
740
|
+
sfx_over();
|
|
741
|
+
state = ST_OVER;
|
|
742
|
+
clear_field();
|
|
743
|
+
if (two_player) draw_text(11, 7, loser ? "P1 WINS" : "P2 WINS");
|
|
744
|
+
else draw_text(11, 7, "GAME OVER");
|
|
745
|
+
draw_text(9, 10, "P1"); draw_num4(13, 10, score[0]);
|
|
746
|
+
if (two_player) { draw_text(9, 12, "P2"); draw_num4(13, 12, score[1]); }
|
|
747
|
+
draw_text(11, 14, "HI"); draw_num4(15, 14, hiscore);
|
|
748
|
+
draw_text(8, 17, "FIRE FOR TITLE");
|
|
749
|
+
prev_t1 = prev_t2 = 1; /* swallow a fire still held from play */
|
|
750
|
+
}
|
|
751
|
+
|
|
183
752
|
void main(void) {
|
|
184
|
-
uint8_t
|
|
753
|
+
uint8_t i, t1, t2;
|
|
185
754
|
|
|
755
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
756
|
+
* Init order: set the video mode FIRST (INIGRP also clears VRAM — any
|
|
757
|
+
* upload done before it is wiped), then tiles, then sprites. The crt0's
|
|
758
|
+
* INIT contract means main() must NEVER return — the BIOS has nothing
|
|
759
|
+
* sane to fall back to — hence the for(;;) below. */
|
|
186
760
|
msx_set_screen2();
|
|
187
761
|
msx_clear_sprites();
|
|
188
762
|
load_tiles();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
for (r = 0; r < ROWS; r++)
|
|
192
|
-
for (c = 0; c < COLS; c++) grid[r][c] = 0;
|
|
763
|
+
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_GEM * 8), spr_gem, 8);
|
|
193
764
|
|
|
194
|
-
|
|
195
|
-
|
|
765
|
+
msx_music(0); /* the lib's demo loop also owns channel C —
|
|
766
|
+
* hand the channel to OUR tune table instead */
|
|
767
|
+
hiscore = 0; /* session hi-score (no SAVE_RAM on this core) */
|
|
196
768
|
rng = 0xACE1;
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
769
|
+
music_step = music_timer = 0;
|
|
770
|
+
sfx_a_t = sfx_b_t = 0;
|
|
771
|
+
for (i = 0; i < 2; i++) { prev_dir[i] = 0; prev_a[i] = prev_b[i] = 1; }
|
|
772
|
+
prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
|
|
773
|
+
two_player = 0;
|
|
774
|
+
state = ST_TITLE;
|
|
775
|
+
paint_title();
|
|
202
776
|
|
|
203
777
|
for (;;) {
|
|
204
778
|
vsync();
|
|
205
|
-
|
|
779
|
+
music_tick();
|
|
780
|
+
sfx_tick();
|
|
206
781
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if ((dir == STICK_RIGHT || dir == STICK_UR || dir == STICK_DR)
|
|
216
|
-
&& !(prev_dir == STICK_RIGHT || prev_dir == STICK_UR || prev_dir == STICK_DR)
|
|
217
|
-
&& !collides((int8_t)(piece_x + 1), piece_y)) piece_x++;
|
|
218
|
-
|
|
219
|
-
if (ta && !prev_ta) {
|
|
220
|
-
t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
|
|
221
|
-
msx_psg_tone(1, 0x280, 8); blip = 4;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
if (tb && !prev_tb) {
|
|
225
|
-
while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
|
|
226
|
-
lock_piece();
|
|
227
|
-
new_piece();
|
|
228
|
-
prev_dir = dir; prev_ta = ta; prev_tb = tb;
|
|
229
|
-
if (blip) { blip--; if (!blip) { msx_psg_off(0); msx_psg_off(1); } }
|
|
782
|
+
if (state == ST_TITLE) {
|
|
783
|
+
/* ── GAME LOGIC (clay) — title: trig A = 1P; trig B = 2P versus. */
|
|
784
|
+
t1 = (uint8_t)(gttrig(1) || gttrig(0));
|
|
785
|
+
t2 = (uint8_t)(gttrig(3) || gttrig(2));
|
|
786
|
+
if (t2 && !prev_t2) start_game(1);
|
|
787
|
+
else if (t1 && !prev_t1) start_game(0);
|
|
788
|
+
prev_t1 = t1; prev_t2 = t2;
|
|
789
|
+
push_sprites();
|
|
230
790
|
continue;
|
|
231
791
|
}
|
|
232
792
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
new_piece();
|
|
242
|
-
} else {
|
|
243
|
-
piece_y++;
|
|
793
|
+
if (state == ST_OVER) {
|
|
794
|
+
/* Freeze the final frame; any fire button returns to the title. */
|
|
795
|
+
t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
|
|
796
|
+
if (t1 && !prev_t1) {
|
|
797
|
+
state = ST_TITLE;
|
|
798
|
+
msx_clear_sprites();
|
|
799
|
+
two_player = 0;
|
|
800
|
+
paint_title();
|
|
244
801
|
}
|
|
802
|
+
prev_t1 = t1; prev_t2 = t1;
|
|
803
|
+
push_sprites();
|
|
804
|
+
continue;
|
|
245
805
|
}
|
|
246
|
-
draw_piece(0);
|
|
247
806
|
|
|
248
|
-
|
|
807
|
+
/* ── ST_PLAY — GAME LOGIC (clay) — both players update EVERY frame
|
|
808
|
+
* (simultaneous versus, not alternating turns). Any update can end
|
|
809
|
+
* the game, so re-check state between them. */
|
|
810
|
+
update_player(0);
|
|
811
|
+
if (two_player && state == ST_PLAY) update_player(1);
|
|
812
|
+
|
|
813
|
+
push_sprites();
|
|
249
814
|
}
|
|
250
815
|
}
|