romdevtools 0.28.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 +51 -41
- package/CHANGELOG.md +46 -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 +1 -1
- package/src/host/LibretroHost.js +59 -1
- 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/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/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 +12 -1
- package/src/mcp/tools/watch-memory.js +4 -3
- 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/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,370 +1,885 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* PC Engine "puzzle" — a match-3 falling-block scaffold.
|
|
1
|
+
/* ── main.c — PC Engine falling-trio versus puzzle (complete example game) ─────
|
|
3
2
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
3
|
+
* TUMBLE TIDE — a COMPLETE, working game: title screen, 1P MARATHON mode
|
|
4
|
+
* (levels speed the fall as you clear) and 2P SIMULTANEOUS VERSUS mode — two
|
|
5
|
+
* 6x12 wells side by side, P1 on the stock pad, P2 on the TurboTap's second
|
|
6
|
+
* pad, both falling at once, where every cascade chain you score sends a TIDE
|
|
7
|
+
* of garbage rows rising from the bottom of your rival's well. Score +
|
|
8
|
+
* in-session hi-score (a bare HuCard can't save — see the hi-score note
|
|
9
|
+
* below), PSG music + SFX.
|
|
9
10
|
*
|
|
10
|
-
* The
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* The game: a falling-trio match-3. A vertical trio of pieces drops into a
|
|
12
|
+
* well; LEFT/RIGHT move it, I/II cycle its three colours, DOWN soft-drops,
|
|
13
|
+
* RUN hard-drops. When it lands, any straight run of 3+ same-coloured cells
|
|
14
|
+
* (horizontal, vertical, or diagonal) clears; survivors fall and cascades
|
|
15
|
+
* chain for multiplied score. First stack to reach the rim loses.
|
|
13
16
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* -
|
|
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 PCE footgun; reshape
|
|
20
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
21
|
+
* GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
|
|
17
22
|
*
|
|
18
|
-
*
|
|
23
|
+
* What depends on what:
|
|
24
|
+
* pce_hw.h / pce_video.c / pce_input.c / pce_sound.c — the helper lib
|
|
25
|
+
* (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
|
|
26
|
+
* pce_video.c say which parts are load-bearing.
|
|
27
|
+
* cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
|
|
28
|
+
* (applied automatically to example projects) gives a 32KB HuCard.
|
|
29
|
+
*
|
|
30
|
+
* 2P, honestly: the stock PC Engine has ONE controller port; 2P needs a
|
|
31
|
+
* TurboTap. The geargrafx core implements the TurboTap and the romdev host
|
|
32
|
+
* now force-ENABLES it (PLATFORM_CORE_OPTIONS pce: geargrafx_turbotap), so a
|
|
33
|
+
* second pad's input reaches the game on pad slot 2 — verified by driving
|
|
34
|
+
* port-1 input and seeing P2 move. So this game ships REAL simultaneous 2P
|
|
35
|
+
* versus. (On real hardware the player plugs a TurboTap and a second pad.)
|
|
36
|
+
*
|
|
37
|
+
* Frame budget (NTSC, 60fps) — and a TEACHING POINT vs the NES version of
|
|
38
|
+
* this game (examples/nes/templates/puzzle.c): on the NES, board repaints
|
|
39
|
+
* squeeze through a ~16-entry vblank queue, so a full-board repaint is
|
|
40
|
+
* BUDGETED across ~12 frames of dirty-row bitmask tricks. The PC Engine has
|
|
41
|
+
* no such famine: the VDC's VRAM write port streams words back-to-back, and a
|
|
42
|
+
* whole well is 24 tile rows x 12 tile cols = 288 BAT words. Two wells + the
|
|
43
|
+
* 6-entry SATB + the HUD all stream inside one vblank with budget to spare —
|
|
44
|
+
* so this version just REPAINTS THE WHOLE DIRTY WELL each time it changes (no
|
|
45
|
+
* dirty-row machinery at all). Same genre, two bandwidth worlds — fork
|
|
46
|
+
* accordingly.
|
|
19
47
|
*/
|
|
20
48
|
#include <pce.h>
|
|
21
|
-
#include <
|
|
49
|
+
#include <joystick.h> /* JOY_2 + joy_read for the 2nd pad (TurboTap port 1) */
|
|
22
50
|
#include "pce_hw.h"
|
|
23
51
|
|
|
24
|
-
/*
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
#define
|
|
52
|
+
/* pce_hw.h gives us u8/u16; the match-scan + piece coords need signed types
|
|
53
|
+
* (cells can sit above the rim at negative rows). cc65's int is 16-bit. */
|
|
54
|
+
typedef signed char s8;
|
|
55
|
+
typedef int s16;
|
|
56
|
+
|
|
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 "TUMBLE TIDE"
|
|
60
|
+
|
|
61
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
62
|
+
* VRAM map (WORD addresses — the VDC is a 16-bit-word machine; an 8x8 tile is
|
|
63
|
+
* 16 words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
|
|
64
|
+
* VRAM, so lay it out ONCE and keep the SATB out of pattern space:
|
|
65
|
+
* $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
|
|
66
|
+
* $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash)
|
|
67
|
+
* $1400 board furniture tiles (backdrop, HUD band, frame, empty cell)
|
|
68
|
+
* $1500 CELL tiles: 3 colours, each its own 8x8 tile (a 16x16 cell is 2x2)
|
|
69
|
+
* $1800 16x16 trio SPRITE cells: 3 colours
|
|
70
|
+
* $7F00 shadow SATB destination (satb_dma copies it here, VDC reads it) */
|
|
71
|
+
#define BAT_VRAM 0x0000
|
|
72
|
+
#define FONT_VRAM 0x1000
|
|
73
|
+
#define BACK_VRAM 0x1400 /* solid colour 1 — cabinet backdrop */
|
|
74
|
+
#define BAND_VRAM 0x1410 /* solid colour 2 — band behind the HUD text */
|
|
75
|
+
#define FRAME_VRAM 0x1420 /* solid colour 3 — well border */
|
|
76
|
+
#define INNER_VRAM 0x1430 /* near-black well interior + faint speck */
|
|
77
|
+
#define CELL0_VRAM 0x1500 /* locked-cell BG tile, colour A (8x8) */
|
|
78
|
+
#define CELL1_VRAM 0x1510 /* locked-cell BG tile, colour B */
|
|
79
|
+
#define CELL2_VRAM 0x1520 /* locked-cell BG tile, colour C */
|
|
80
|
+
#define SPR0_VRAM 0x1800 /* 16x16 falling-trio sprite, colour A */
|
|
81
|
+
#define SPR1_VRAM 0x1840 /* 16x16 falling-trio sprite, colour B */
|
|
82
|
+
#define SPR2_VRAM 0x1880 /* 16x16 falling-trio sprite, colour C */
|
|
32
83
|
|
|
33
84
|
#define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
|
|
34
85
|
|
|
35
|
-
|
|
36
|
-
#define
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
86
|
+
/* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
|
|
87
|
+
#define SPR_PAT(c) ((u16)((SPR0_VRAM >> 6) + (c))) /* colour 0..2 */
|
|
88
|
+
|
|
89
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
90
|
+
* Board geometry. Cells are 16x16 px (2x2 BAT tiles) — the PCE 256x224 screen
|
|
91
|
+
* has room to spare; chunky cells read better than 8-px ones. The BAT is
|
|
92
|
+
* 32 tiles wide; a 12-wide well (6 cells x 2 tiles) fits twice for the split
|
|
93
|
+
* board. Tile rows 0-1 sit under the HUD band; well interiors start at row 3. */
|
|
94
|
+
#define GRID_W 6
|
|
95
|
+
#define GRID_H 12
|
|
96
|
+
#define WELL_TR 3 /* top TILE row of the well interior */
|
|
97
|
+
#define WELL_1P_TC 10 /* 1P: single centered well (tiles 10-21) */
|
|
98
|
+
#define WELL_VS_P1 2 /* 2P: P1 interior tiles 2-13 ... */
|
|
99
|
+
#define WELL_VS_P2 18 /* P2 interior tiles 18-29 (split board) */
|
|
100
|
+
#define HUD_ROWS 2 /* BAT rows reserved for the HUD band */
|
|
101
|
+
|
|
102
|
+
#define EMPTY 0 /* cell colours 1..3 = amber/teal/magenta */
|
|
103
|
+
|
|
104
|
+
/* SATB slot plan: 3 trio sprites per player (slot order = priority). */
|
|
105
|
+
#define SLOT_TRIO(p, i) (u8)((p) * 3 + (i))
|
|
106
|
+
#define OFFSCREEN_Y 0x1F0 /* park hidden sprites below the display */
|
|
107
|
+
/* Each trio colour gets its OWN sprite sub-palette (1/2/3) so the falling
|
|
108
|
+
* pieces show their three distinct hues, matching the locked-board cells. */
|
|
109
|
+
#define PAL_TRIO(col) (u8)(col) /* colour 1..3 -> sprite sub-palette 1..3 */
|
|
110
|
+
|
|
111
|
+
/* ── GAME LOGIC (clay — reshape freely) ── game state ── */
|
|
112
|
+
static u8 grid[2][GRID_H][GRID_W]; /* the two wells (P2's unused in 1P) */
|
|
113
|
+
static s16 piece_x[2]; /* falling trio: column 0..5 */
|
|
114
|
+
static s16 piece_y[2]; /* row of its TOP cell (<0 above rim) */
|
|
115
|
+
static u8 piece_col[2][3]; /* trio colours, top to bottom */
|
|
116
|
+
static u16 score[2];
|
|
117
|
+
static u16 hiscore;
|
|
118
|
+
static u8 level; /* 1P: 1..9, speeds up the fall */
|
|
119
|
+
static u8 state; /* ST_TITLE / ST_PLAY / ST_OVER */
|
|
120
|
+
static u8 two_player;
|
|
121
|
+
|
|
122
|
+
static u8 matched[GRID_H][GRID_W];
|
|
123
|
+
static u8 well_tc[2]; /* left interior TILE column per well */
|
|
124
|
+
static u8 fall_t[2]; /* frames until next gravity step */
|
|
125
|
+
static u8 prev_pad[2]; /* for edge-triggered input */
|
|
126
|
+
static u16 cleared_total; /* 1P: cells cleared, drives the level */
|
|
127
|
+
static u8 board_dirty[2]; /* well needs a repaint this frame */
|
|
128
|
+
static u8 loser; /* who topped out (2P result text) */
|
|
129
|
+
static u16 rng = 0xACE1;
|
|
130
|
+
static u8 sfx_timer;
|
|
131
|
+
static u8 hud_dirty;
|
|
132
|
+
|
|
133
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
134
|
+
#define ST_TITLE 0
|
|
135
|
+
#define ST_PLAY 1
|
|
136
|
+
#define ST_OVER 2
|
|
137
|
+
|
|
138
|
+
#define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
|
|
139
|
+
#define GARBAGE_CAP 4 /* max garbage rows per attack */
|
|
140
|
+
|
|
141
|
+
static u16 tile_buf[16]; /* scratch for one 8x8 tile */
|
|
142
|
+
static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
|
|
143
|
+
|
|
144
|
+
/* ── GAME LOGIC (clay) — 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
|
|
145
|
+
* Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
|
|
146
|
+
* them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
|
|
147
|
+
#define G_BLANK 0
|
|
148
|
+
#define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
|
|
149
|
+
#define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
|
|
150
|
+
#define G_DASH 37
|
|
151
|
+
#define NUM_GLYPHS 38
|
|
152
|
+
|
|
153
|
+
static const u8 FONT5x7[NUM_GLYPHS][7] = {
|
|
154
|
+
{0,0,0,0,0,0,0},
|
|
155
|
+
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
|
|
156
|
+
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
|
|
157
|
+
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
|
|
158
|
+
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
|
|
159
|
+
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
|
|
160
|
+
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
|
|
161
|
+
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
|
|
162
|
+
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
|
|
163
|
+
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
|
|
164
|
+
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
|
|
165
|
+
{0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
|
|
166
|
+
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
|
|
167
|
+
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
|
|
168
|
+
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
|
|
169
|
+
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
|
|
170
|
+
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
|
|
171
|
+
{0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
|
|
172
|
+
{0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
|
|
173
|
+
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
/* ── GAME LOGIC (clay) — a 16x16 round-cell mask (16 rows × 16 bits, bit15
|
|
177
|
+
* leftmost). The falling-trio sprites use this whole; one piece of art, three
|
|
178
|
+
* colours (the colour is the PALETTE, not the bits). */
|
|
179
|
+
static const u16 cell_mask[16] = {
|
|
180
|
+
0x07E0, 0x1FF8, 0x3FFC, 0x7E7E, 0x7C3E, 0xFC3F, 0xFFFF, 0xFFFF,
|
|
181
|
+
0xFFFF, 0xFFFF, 0xFC3F, 0x7C3E, 0x7E7E, 0x3FFC, 0x1FF8, 0x07E0
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
/* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
|
|
51
185
|
static void make_solid_tile(u16 *t, u8 ci) {
|
|
52
186
|
u8 r;
|
|
53
187
|
u8 p0 = (ci & 1) ? 0xFF : 0x00;
|
|
54
188
|
u8 p1 = (ci & 2) ? 0xFF : 0x00;
|
|
55
|
-
u8 p2 = (ci & 4) ? 0xFF : 0x00;
|
|
56
|
-
u8 p3 = (ci & 8) ? 0xFF : 0x00;
|
|
57
189
|
for (r = 0; r < 8; ++r) {
|
|
58
190
|
t[r] = (u16)(p0 | (p1 << 8));
|
|
59
|
-
t[r + 8] =
|
|
191
|
+
t[r + 8] = 0;
|
|
60
192
|
}
|
|
61
193
|
}
|
|
62
194
|
|
|
63
|
-
/*
|
|
64
|
-
|
|
65
|
-
* For each of the 8 rows we pick a per-plane mask: border rows (0,7) are all
|
|
66
|
-
* `frame`; interior rows are `ci` body with the left/right edge pixels framed. */
|
|
67
|
-
static void make_block_tile(u16 *t, u8 ci, u8 frame) {
|
|
195
|
+
/* one-colour 16x16 sprite cell from a 16-row mask (colour = plane0 → index 1) */
|
|
196
|
+
static void make_sprite16(u16 vram, const u16 *mask) {
|
|
68
197
|
u8 r;
|
|
69
|
-
for (r = 0; r <
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
u8 b0 = (ci & 1) ? 0xFF : 0x00, b1 = (ci & 2) ? 0xFF : 0x00;
|
|
73
|
-
u8 b2 = (ci & 4) ? 0xFF : 0x00, b3 = (ci & 8) ? 0xFF : 0x00;
|
|
74
|
-
/* frame colour planes */
|
|
75
|
-
u8 f0 = (frame & 1) ? 0xFF : 0x00, f1 = (frame & 2) ? 0xFF : 0x00;
|
|
76
|
-
u8 f2 = (frame & 4) ? 0xFF : 0x00, f3 = (frame & 8) ? 0xFF : 0x00;
|
|
77
|
-
u8 p0, p1, p2, p3;
|
|
78
|
-
if (edge_row) {
|
|
79
|
-
p0 = f0; p1 = f1; p2 = f2; p3 = f3; /* whole row framed */
|
|
80
|
-
} else {
|
|
81
|
-
/* body fill, but pixels 0 and 7 (mask 0x81) use the frame colour */
|
|
82
|
-
p0 = (u8)((b0 & 0x7E) | (f0 & 0x81));
|
|
83
|
-
p1 = (u8)((b1 & 0x7E) | (f1 & 0x81));
|
|
84
|
-
p2 = (u8)((b2 & 0x7E) | (f2 & 0x81));
|
|
85
|
-
p3 = (u8)((b3 & 0x7E) | (f3 & 0x81));
|
|
86
|
-
}
|
|
87
|
-
t[r] = (u16)(p0 | (p1 << 8));
|
|
88
|
-
t[r + 8] = (u16)(p2 | (p3 << 8));
|
|
89
|
-
}
|
|
198
|
+
for (r = 0; r < 64; ++r) spr_buf[r] = 0;
|
|
199
|
+
for (r = 0; r < 16; ++r) spr_buf[r] = mask[r]; /* plane 0 → colour 1 */
|
|
200
|
+
load_tiles(vram, spr_buf, 64);
|
|
90
201
|
}
|
|
91
202
|
|
|
92
|
-
/*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
|
|
96
|
-
* plane0 (low byte words 0..7) = dot mask (only dot pixels)
|
|
97
|
-
* plane1 (high byte words 0..7) = 0xFF (colour 6 base, all pixels)
|
|
98
|
-
* plane2 (low byte words 8..15) = 0xFF (colour 6 base, all pixels)
|
|
99
|
-
* plane3 (high byte words 8..15)= 0 */
|
|
100
|
-
static void make_dots_tile(u16 *t) {
|
|
203
|
+
/* A locked-board cell is a chunky 8x8 "pip" tile (a 16x16 cell is 2x2 of it):
|
|
204
|
+
* a filled colour-1 square with a dark 1-px rim on every edge so adjacent
|
|
205
|
+
* cells read as separate pieces. The colour is the PALETTE, not the bits. */
|
|
206
|
+
static void make_cell_tile(u16 *t) {
|
|
101
207
|
u8 r;
|
|
102
208
|
for (r = 0; r < 8; ++r) {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
209
|
+
u16 fill = 0x00FF; /* plane0 all set → colour 1 */
|
|
210
|
+
if (r == 0 || r == 7) fill = 0; /* clear top/bottom rim rows */
|
|
211
|
+
else fill &= 0x007E; /* clear left+right edge columns */
|
|
212
|
+
t[r] = fill;
|
|
213
|
+
t[r + 8] = 0;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
static void upload_font(void) {
|
|
218
|
+
u8 g, row, bits, px;
|
|
219
|
+
for (g = 0; g < NUM_GLYPHS; ++g) {
|
|
220
|
+
for (row = 0; row < 16; ++row) tile_buf[row] = 0;
|
|
221
|
+
for (row = 0; row < 7; ++row) {
|
|
222
|
+
bits = FONT5x7[g][row];
|
|
223
|
+
px = 0;
|
|
224
|
+
if (bits & 0x10) px |= 0x40;
|
|
225
|
+
if (bits & 0x08) px |= 0x20;
|
|
226
|
+
if (bits & 0x04) px |= 0x10;
|
|
227
|
+
if (bits & 0x02) px |= 0x08;
|
|
228
|
+
if (bits & 0x01) px |= 0x04;
|
|
229
|
+
tile_buf[row] = (u16)px;
|
|
230
|
+
}
|
|
231
|
+
load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
|
|
106
232
|
}
|
|
107
233
|
}
|
|
108
234
|
|
|
109
235
|
static void upload_art(void) {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
make_solid_tile(tile_buf,
|
|
236
|
+
upload_font();
|
|
237
|
+
make_solid_tile(tile_buf, 1); load_tiles(BACK_VRAM, tile_buf, 16);
|
|
238
|
+
make_solid_tile(tile_buf, 2); load_tiles(BAND_VRAM, tile_buf, 16);
|
|
239
|
+
make_solid_tile(tile_buf, 3); load_tiles(FRAME_VRAM, tile_buf, 16);
|
|
240
|
+
/* well interior: near-black colour-1 with one faint colour-3 speck */
|
|
241
|
+
make_solid_tile(tile_buf, 1); tile_buf[4] |= 0x1000; tile_buf[4 + 8] = 0x1000;
|
|
242
|
+
load_tiles(INNER_VRAM, tile_buf, 16);
|
|
243
|
+
/* one cell tile shape, reused for all three colours (palette gives hue) */
|
|
244
|
+
make_cell_tile(tile_buf);
|
|
245
|
+
load_tiles(CELL0_VRAM, tile_buf, 16);
|
|
246
|
+
load_tiles(CELL1_VRAM, tile_buf, 16);
|
|
247
|
+
load_tiles(CELL2_VRAM, tile_buf, 16);
|
|
248
|
+
make_sprite16(SPR0_VRAM, cell_mask);
|
|
249
|
+
make_sprite16(SPR1_VRAM, cell_mask);
|
|
250
|
+
make_sprite16(SPR2_VRAM, cell_mask);
|
|
116
251
|
}
|
|
117
252
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
if (cell == 3) return BLU_VRAM;
|
|
122
|
-
return FIELD_VRAM; /* empty -> dim field interior */
|
|
253
|
+
/* cell colour 1..3 → its locked-board BG tile VRAM */
|
|
254
|
+
static u16 cell_vram(u8 col) {
|
|
255
|
+
return (col == 1) ? CELL0_VRAM : (col == 2) ? CELL1_VRAM : CELL2_VRAM;
|
|
123
256
|
}
|
|
124
257
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
258
|
+
/* ── GAME LOGIC (clay) — BAT text + board paint ──────────────────────────── */
|
|
259
|
+
static void put_glyph(u8 col, u8 row, u8 glyph) {
|
|
260
|
+
u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
|
|
261
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
128
262
|
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
129
263
|
VDC_DATA_HI = (u8)(e >> 8);
|
|
130
264
|
}
|
|
131
265
|
|
|
132
|
-
|
|
133
|
-
|
|
266
|
+
static void put_tile(u8 col, u8 row, u16 e) {
|
|
267
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
268
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
269
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static void draw_text(u8 col, u8 row, const char *s) {
|
|
273
|
+
u8 c;
|
|
274
|
+
while ((c = (u8)*s++) != 0) {
|
|
275
|
+
u8 g = G_BLANK;
|
|
276
|
+
if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
|
|
277
|
+
else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
|
|
278
|
+
else if (c == '-') g = G_DASH;
|
|
279
|
+
put_glyph(col++, row, g);
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
static void draw_num5(u8 col, u8 row, u16 v) {
|
|
284
|
+
u8 i, d[5];
|
|
285
|
+
for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
|
|
286
|
+
for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
290
|
+
* WHOLE-BOARD BAT REPAINT — the PCE's puzzle bandwidth, the inverse of the
|
|
291
|
+
* NES famine. A locked cell is two-by-two of one BG tile (on its own colour
|
|
292
|
+
* sub-palette); an empty cell is two-by-two of the INNER tile. When a board
|
|
293
|
+
* changes we simply rewrite ALL 24x12 of its BAT entries — 288 word writes
|
|
294
|
+
* straight at the VDC's VWR port (vram_set_write_addr arms the auto-
|
|
295
|
+
* incrementing address, then we stream). The whole well streams in well under
|
|
296
|
+
* a vblank; both wells + the SATB + HUD fit one frame. The NES version of THIS
|
|
297
|
+
* GAME budgets the same repaint across ~12 frames through a 16-entry queue —
|
|
298
|
+
* the PCE just blasts it. Two rules:
|
|
299
|
+
* - do the streaming inside the vblank window (we repaint just after
|
|
300
|
+
* waitvsync()), so the VDC isn't fetching the BAT for display mid-write;
|
|
301
|
+
* - keep the SATB-DMA after the BAT writes — both share the VDC and the DMA
|
|
302
|
+
* wants the address latch left where it expects it.
|
|
303
|
+
*
|
|
304
|
+
* requires: BAT 32x32 (vdc_init's MWR); well within the 32-wide BAT (it is:
|
|
305
|
+
* the split board uses tiles 2-13 and 18-29). */
|
|
306
|
+
static void paint_board(u8 p) {
|
|
307
|
+
u8 r, c, tr, tc;
|
|
308
|
+
u16 e_inner = BAT_ENTRY(0, INNER_VRAM);
|
|
309
|
+
u8 left = well_tc[p];
|
|
310
|
+
for (r = 0; r < GRID_H; r++) {
|
|
311
|
+
for (c = 0; c < GRID_W; c++) {
|
|
312
|
+
u8 col = grid[p][r][c];
|
|
313
|
+
/* each locked colour gets its own BG sub-palette (3/4/5) so the
|
|
314
|
+
* one cell-tile shape (all pixels colour index 1) renders three
|
|
315
|
+
* distinct hues; empty interior uses sub-palette 0 (backdrop). */
|
|
316
|
+
u16 e = col ? BAT_ENTRY(2 + col, cell_vram(col)) : e_inner;
|
|
317
|
+
tr = (u8)(WELL_TR + r * 2);
|
|
318
|
+
tc = (u8)(left + c * 2);
|
|
319
|
+
put_tile(tc, tr, e); /* 2x2 of the cell tile */
|
|
320
|
+
put_tile((u8)(tc + 1), tr, e);
|
|
321
|
+
put_tile(tc, (u8)(tr + 1), e);
|
|
322
|
+
put_tile((u8)(tc + 1), (u8)(tr + 1), e);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
static void paint_frame(u8 p) {
|
|
328
|
+
u8 r, tr, c;
|
|
329
|
+
u16 e = BAT_ENTRY(0, FRAME_VRAM);
|
|
330
|
+
u8 x0 = (u8)(well_tc[p] - 1);
|
|
331
|
+
u8 w = (u8)(GRID_W * 2 + 2);
|
|
332
|
+
/* top + bottom rails */
|
|
333
|
+
for (c = 0; c < w; c++) {
|
|
334
|
+
put_tile((u8)(x0 + c), (u8)(WELL_TR - 1), e);
|
|
335
|
+
put_tile((u8)(x0 + c), (u8)(WELL_TR + GRID_H * 2), e);
|
|
336
|
+
}
|
|
337
|
+
/* side rails */
|
|
338
|
+
for (r = 0; r < GRID_H * 2; r++) {
|
|
339
|
+
tr = (u8)(WELL_TR + r);
|
|
340
|
+
put_tile(x0, tr, e);
|
|
341
|
+
put_tile((u8)(x0 + GRID_W * 2 + 1), tr, e);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
/* Fill the whole 32x32 BAT: HUD band on the top rows, backdrop below. */
|
|
346
|
+
static void paint_backdrop(void) {
|
|
134
347
|
u8 r, c;
|
|
135
|
-
u16
|
|
136
|
-
|
|
348
|
+
u16 band = BAT_ENTRY(0, BAND_VRAM);
|
|
349
|
+
u16 back = BAT_ENTRY(0, BACK_VRAM);
|
|
350
|
+
for (r = 0; r < 32; r++) {
|
|
137
351
|
vram_set_write_addr((u16)(BAT_VRAM + r * 32));
|
|
138
|
-
for (c = 0; c < 32; ++
|
|
352
|
+
for (c = 0; c < 32; c++) {
|
|
353
|
+
u16 e = (r < HUD_ROWS) ? band : back;
|
|
139
354
|
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
140
355
|
VDC_DATA_HI = (u8)(e >> 8);
|
|
141
356
|
}
|
|
142
357
|
}
|
|
143
358
|
}
|
|
144
359
|
|
|
145
|
-
/*
|
|
146
|
-
static void
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
360
|
+
/* HUD: row 0 = "SC 00000 HI 00000 LV 1" (1P) or "P1 .. HI .. P2 .." (2P). */
|
|
361
|
+
static void draw_hud(void) {
|
|
362
|
+
u8 i;
|
|
363
|
+
/* clear the HUD text row before repainting (band tile under the glyphs) */
|
|
364
|
+
for (i = 0; i < 32; i++) put_tile(i, 0, BAT_ENTRY(0, BAND_VRAM));
|
|
365
|
+
if (state == ST_TITLE) {
|
|
366
|
+
draw_text(13, 0, "HI");
|
|
367
|
+
draw_num5(16, 0, hiscore);
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
if (two_player) {
|
|
371
|
+
draw_text(1, 0, "P1");
|
|
372
|
+
draw_num5(4, 0, score[0]);
|
|
373
|
+
draw_text(12, 0, "HI");
|
|
374
|
+
draw_num5(15, 0, hiscore);
|
|
375
|
+
draw_text(24, 0, "P2");
|
|
376
|
+
draw_num5(27, 0, score[1]);
|
|
377
|
+
} else {
|
|
378
|
+
draw_text(1, 0, "SC");
|
|
379
|
+
draw_num5(4, 0, score[0]);
|
|
380
|
+
draw_text(12, 0, "HI");
|
|
381
|
+
draw_num5(15, 0, hiscore);
|
|
382
|
+
draw_text(24, 0, "LV");
|
|
383
|
+
put_glyph(27, 0, (u8)(G_DIGIT + level));
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/* ── HARDWARE TRUTH: a bare HuCard CANNOT save a hi-score (in-session only) ──
|
|
388
|
+
* This was researched and corrected: earlier versions wrote the hi-score to
|
|
389
|
+
* BRAM ("backup RAM", bank $F7) and claimed it persisted across power cycles.
|
|
390
|
+
* That is NOT honest for a HuCard game. On REAL hardware a plain HuCard plugged
|
|
391
|
+
* into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM exists
|
|
392
|
+
* ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
|
|
393
|
+
* supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
|
|
394
|
+
* commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
|
|
395
|
+
* "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
|
|
396
|
+
* emulator like geargrafx exposes BRAM unconditionally, so the old code
|
|
397
|
+
* "worked" in emulation in a way the real machine never would.
|
|
398
|
+
*
|
|
399
|
+
* So this game keeps an IN-SESSION hi-score only (like the honest 2600/Lynx
|
|
400
|
+
* examples) — it survives game-overs within a power-on, resets to 0 on a cold
|
|
401
|
+
* boot. To make it ACTUALLY persist on real hardware you would target a
|
|
402
|
+
* peripheral: write to BRAM only after detecting one (and go through the System
|
|
403
|
+
* Card BIOS's 'HUBM' directory for CD saves), or move the game to a CD-ROM²
|
|
404
|
+
* build. Either is a real-hardware feature, not a property of the cartridge. */
|
|
405
|
+
static u16 hiscore_load(void) {
|
|
406
|
+
return 0; /* cold boot: no persistence on a bare HuCard */
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
static void hiscore_save(u16 v) {
|
|
410
|
+
(void)v; /* in-session only — nowhere to persist on real HW */
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
|
|
414
|
+
* PSG channel plan: 5 = melody, 4 = bass, 2/3 = SFX (tones cut by sfx_timer).
|
|
415
|
+
* PCE frequency regs are DIVIDERS: pitch ≈ 3.58MHz / (32 × value), so a
|
|
416
|
+
* BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
|
|
417
|
+
enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
|
|
418
|
+
static const u16 NOTE_DIV[17] = {
|
|
419
|
+
0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
|
|
420
|
+
};
|
|
421
|
+
/* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
|
|
422
|
+
static const u8 MEL_TITLE[16] = { A3,C4,E4,A4, G4,E4,C4,E4, F4,A4,C5,A4, G4,E4,D4,C4 };
|
|
423
|
+
static const u8 BAS_TITLE[8] = { A2N,A2N, F3,F3, C3,C3, G3,G3 };
|
|
424
|
+
static const u8 MEL_PLAY[16] = { C4,E4,G4,E4, D4,F4,A4,F4, E4,G4,C5,G4, A4,G4,E4,R };
|
|
425
|
+
static const u8 BAS_PLAY[8] = { C3,C3, F3,F3, A2N,A2N, G3,G3 };
|
|
426
|
+
static const u8 MEL_OVER[16] = { C5,R,A4,R, F4,R,E4,R, D4,R,C4,R, A2N,R,R,R };
|
|
427
|
+
|
|
428
|
+
static u8 music_song; /* reuses the ST_* ids */
|
|
429
|
+
static u8 music_step, music_timer, music_done;
|
|
430
|
+
|
|
431
|
+
static void music_set(u8 song) {
|
|
432
|
+
music_song = song;
|
|
433
|
+
music_step = 0;
|
|
434
|
+
music_timer = 0;
|
|
435
|
+
music_done = 0;
|
|
436
|
+
psg_off(4);
|
|
437
|
+
psg_off(5);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
static void music_tick(void) {
|
|
441
|
+
const u8 *mel;
|
|
442
|
+
u8 n;
|
|
443
|
+
if (music_done) return;
|
|
444
|
+
if (music_timer == 0) {
|
|
445
|
+
mel = (music_song == ST_PLAY) ? MEL_PLAY
|
|
446
|
+
: (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
|
|
447
|
+
n = mel[music_step & 15];
|
|
448
|
+
if (n != R) psg_tone(5, NOTE_DIV[n], 26);
|
|
449
|
+
else psg_off(5);
|
|
450
|
+
if (music_song != ST_OVER) { /* the game-over jingle has no bass */
|
|
451
|
+
n = ((music_step & 1) == 0)
|
|
452
|
+
? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
|
|
453
|
+
: BAS_TITLE[(music_step >> 1) & 7])
|
|
454
|
+
: R;
|
|
455
|
+
if (n != R) psg_tone(4, NOTE_DIV[n], 20);
|
|
456
|
+
}
|
|
457
|
+
++music_step;
|
|
458
|
+
if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
|
|
459
|
+
music_done = 1;
|
|
460
|
+
psg_off(4);
|
|
461
|
+
psg_off(5);
|
|
153
462
|
}
|
|
154
463
|
}
|
|
464
|
+
++music_timer;
|
|
465
|
+
if (music_timer >= 9) music_timer = 0;
|
|
155
466
|
}
|
|
156
467
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
468
|
+
/* short SFX on channels 2/3, auto-cut by sfx_timer */
|
|
469
|
+
static void sfx(u8 chan, u16 freq, u8 frames) {
|
|
470
|
+
psg_tone(chan, freq, 31);
|
|
471
|
+
if (frames > sfx_timer) sfx_timer = frames;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG ── */
|
|
475
|
+
static u8 random8(void) {
|
|
476
|
+
u16 r = rng;
|
|
477
|
+
r ^= r << 7;
|
|
478
|
+
r ^= r >> 9;
|
|
479
|
+
r ^= r << 8;
|
|
480
|
+
rng = r;
|
|
481
|
+
return (u8)r;
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
485
|
+
* Match scan: mark every straight run of 3+ same-coloured cells in all 4
|
|
486
|
+
* directions (a cell can belong to several runs — the mask de-dupes), and
|
|
487
|
+
* return how many cells matched. Runs flat-out on the HuC6280 — no need to
|
|
488
|
+
* smear it across frames like the cc65 NES version's queue dance. */
|
|
489
|
+
static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
490
|
+
|
|
491
|
+
static u8 mark_and_count(u8 p) {
|
|
492
|
+
u8 r, c, d, len, k, cnt, col;
|
|
493
|
+
s8 dr, dc;
|
|
494
|
+
s16 sr, sc;
|
|
495
|
+
cnt = 0;
|
|
496
|
+
for (r = 0; r < GRID_H; r++)
|
|
497
|
+
for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
|
|
498
|
+
for (r = 0; r < GRID_H; r++) {
|
|
499
|
+
for (c = 0; c < GRID_W; c++) {
|
|
500
|
+
col = grid[p][r][c];
|
|
501
|
+
if (col == EMPTY) continue;
|
|
502
|
+
for (d = 0; d < 4; d++) {
|
|
503
|
+
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
504
|
+
sr = (s16)r - dr; sc = (s16)c - dc;
|
|
505
|
+
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
506
|
+
&& grid[p][sr][sc] == col) continue; /* not the run's start */
|
|
507
|
+
len = 1;
|
|
508
|
+
sr = (s16)r + dr; sc = (s16)c + dc;
|
|
509
|
+
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
510
|
+
&& grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
511
|
+
if (len >= 3) {
|
|
512
|
+
sr = r; sc = c;
|
|
513
|
+
for (k = 0; k < len; k++) {
|
|
514
|
+
if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
|
|
515
|
+
sr += dr; sc += dc;
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
return cnt;
|
|
162
522
|
}
|
|
163
523
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
524
|
+
/* Collapse each column so survivors rest on the floor. */
|
|
525
|
+
static void apply_gravity(u8 p) {
|
|
526
|
+
u8 c;
|
|
527
|
+
s16 r, w;
|
|
528
|
+
for (c = 0; c < GRID_W; c++) {
|
|
529
|
+
w = GRID_H - 1;
|
|
530
|
+
for (r = GRID_H - 1; r >= 0; r--) {
|
|
531
|
+
if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
|
|
532
|
+
}
|
|
533
|
+
for (; w >= 0; w--) grid[p][w][c] = EMPTY;
|
|
534
|
+
}
|
|
167
535
|
}
|
|
168
|
-
static u8 rand_color(void) { return (u8)(1 + (next_rand() >> 8) % 3); }
|
|
169
536
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
537
|
+
/* ── GAME LOGIC (clay) — end of game (top-out). `who` topped out. ── */
|
|
538
|
+
static void game_end(u8 who) {
|
|
539
|
+
u16 best = score[0];
|
|
540
|
+
if (two_player && score[1] > best) best = score[1];
|
|
541
|
+
if (best > hiscore) {
|
|
542
|
+
hiscore = best;
|
|
543
|
+
hiscore_save(hiscore); /* in-session only (no save on a bare HuCard) */
|
|
544
|
+
}
|
|
545
|
+
loser = who;
|
|
546
|
+
sfx(3, 0x500, 24); /* game-over rumble */
|
|
547
|
+
state = ST_OVER;
|
|
548
|
+
board_dirty[0] = board_dirty[1] = 0;
|
|
549
|
+
prev_pad[0] = prev_pad[1] = 0xFF; /* require a fresh press */
|
|
550
|
+
/* paint the result screen onto the BAT */
|
|
551
|
+
paint_backdrop();
|
|
552
|
+
if (two_player)
|
|
553
|
+
draw_text(13, 8, loser ? "P1 WINS" : "P2 WINS");
|
|
554
|
+
else
|
|
555
|
+
draw_text(12, 8, "GAME OVER");
|
|
556
|
+
draw_text(11, 12, "P1");
|
|
557
|
+
draw_num5(15, 12, score[0]);
|
|
558
|
+
if (two_player) {
|
|
559
|
+
draw_text(11, 14, "P2");
|
|
560
|
+
draw_num5(15, 14, score[1]);
|
|
561
|
+
}
|
|
562
|
+
draw_text(11, 17, "HI");
|
|
563
|
+
draw_num5(15, 17, hiscore);
|
|
564
|
+
draw_text(9, 21, "RUN - TITLE");
|
|
565
|
+
music_set(ST_OVER);
|
|
176
566
|
}
|
|
177
567
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
568
|
+
/* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
|
|
569
|
+
* Returns the chain depth (0 = the lock matched nothing). */
|
|
570
|
+
static u8 resolve_board(u8 p) {
|
|
571
|
+
u8 n, r, c, chain;
|
|
572
|
+
u16 amt;
|
|
573
|
+
chain = 0;
|
|
574
|
+
for (;;) {
|
|
575
|
+
n = mark_and_count(p);
|
|
576
|
+
if (n == 0) break;
|
|
577
|
+
++chain;
|
|
578
|
+
for (r = 0; r < GRID_H; r++)
|
|
579
|
+
for (c = 0; c < GRID_W; c++)
|
|
580
|
+
if (matched[r][c]) grid[p][r][c] = EMPTY;
|
|
581
|
+
amt = (u16)n * 10;
|
|
582
|
+
if (chain > 1) amt *= chain; /* cascades pay multiplied */
|
|
583
|
+
if (score[p] < 65000u) score[p] += amt;
|
|
584
|
+
/* clear chime — pitch rises with chain depth (smaller divider) */
|
|
585
|
+
sfx(2, (u16)(0x140 - ((u16)chain << 4)), 8);
|
|
586
|
+
apply_gravity(p);
|
|
587
|
+
board_dirty[p] = 1;
|
|
588
|
+
if (!two_player) {
|
|
589
|
+
cleared_total += n;
|
|
590
|
+
while (level < 9 && cleared_total >= (u16)level * 10) ++level;
|
|
591
|
+
}
|
|
592
|
+
hud_dirty = 1;
|
|
186
593
|
}
|
|
187
|
-
return
|
|
594
|
+
return chain;
|
|
188
595
|
}
|
|
189
596
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
597
|
+
/* ── GAME LOGIC (clay) — VERSUS attack: garbage rows rise from the bottom of
|
|
598
|
+
* the victim's well (random cells with one gap — matchable, so a skilled
|
|
599
|
+
* victim digs out). The victim's stack rising means the falling trio shifts
|
|
600
|
+
* up one to stay board-aligned; if the top row is already occupied, the
|
|
601
|
+
* victim tops out and loses. ── */
|
|
602
|
+
static void garbage_insert(u8 v, u8 nrows) {
|
|
603
|
+
u8 k, c, gap;
|
|
604
|
+
s16 r;
|
|
605
|
+
sfx(3, 0x300, 8); /* incoming-garbage thud */
|
|
606
|
+
for (k = 0; k < nrows; k++) {
|
|
607
|
+
for (c = 0; c < GRID_W; c++) {
|
|
608
|
+
if (grid[v][0][c] != EMPTY) { game_end(v); return; }
|
|
609
|
+
}
|
|
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 = random8() % GRID_W;
|
|
614
|
+
for (c = 0; c < GRID_W; c++)
|
|
615
|
+
grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (u8)(1 + random8() % 3);
|
|
616
|
+
if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
|
|
617
|
+
}
|
|
618
|
+
board_dirty[v] = 1;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine. */
|
|
622
|
+
static u8 can_place(u8 p, s16 x, s16 y) {
|
|
623
|
+
s16 i, cy;
|
|
624
|
+
if (x < 0 || x >= GRID_W) return 0;
|
|
625
|
+
for (i = 0; i < 3; i++) {
|
|
626
|
+
cy = y + i;
|
|
627
|
+
if (cy < 0) continue;
|
|
628
|
+
if (cy >= GRID_H) return 0;
|
|
629
|
+
if (grid[p][cy][x] != EMPTY) return 0;
|
|
630
|
+
}
|
|
631
|
+
return 1;
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
static void spawn_piece(u8 p) {
|
|
635
|
+
piece_x[p] = GRID_W / 2;
|
|
636
|
+
piece_y[p] = -2;
|
|
637
|
+
piece_col[p][0] = (u8)(1 + random8() % 3);
|
|
638
|
+
piece_col[p][1] = (u8)(1 + random8() % 3);
|
|
639
|
+
piece_col[p][2] = (u8)(1 + random8() % 3);
|
|
640
|
+
if (!can_place(p, piece_x[p], piece_y[p])) game_end(p);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
|
|
644
|
+
static void lock_piece(u8 p) {
|
|
645
|
+
s16 i, y;
|
|
646
|
+
u8 chain;
|
|
647
|
+
for (i = 0; i < 3; i++) {
|
|
648
|
+
y = piece_y[p] + i;
|
|
649
|
+
if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
|
|
650
|
+
}
|
|
651
|
+
board_dirty[p] = 1;
|
|
652
|
+
sfx(2, 0x300, 4); /* lock thunk */
|
|
653
|
+
if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
|
|
654
|
+
chain = resolve_board(p);
|
|
655
|
+
if (state != ST_PLAY) return;
|
|
656
|
+
if (chain && two_player) {
|
|
657
|
+
garbage_insert((u8)(p ^ 1), chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
|
|
658
|
+
if (state != ST_PLAY) return; /* garbage topped them out */
|
|
659
|
+
}
|
|
660
|
+
spawn_piece(p);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
/* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
|
|
664
|
+
* (one cell per press), held DOWN soft-drops, I/II cycle the trio's colours
|
|
665
|
+
* (the classic trio "rotate"), RUN hard-drops. ── */
|
|
666
|
+
static void update_player(u8 p, u8 pad) {
|
|
667
|
+
u8 fresh, fd, t;
|
|
668
|
+
fresh = (u8)(pad & ~prev_pad[p]);
|
|
669
|
+
prev_pad[p] = pad;
|
|
670
|
+
if ((fresh & PCE_JOY_LEFT) && can_place(p, piece_x[p] - 1, piece_y[p]))
|
|
671
|
+
--piece_x[p];
|
|
672
|
+
if ((fresh & PCE_JOY_RIGHT) && can_place(p, piece_x[p] + 1, piece_y[p]))
|
|
673
|
+
++piece_x[p];
|
|
674
|
+
if (fresh & PCE_JOY_I) { /* cycle colours downward */
|
|
675
|
+
t = piece_col[p][2];
|
|
676
|
+
piece_col[p][2] = piece_col[p][1];
|
|
677
|
+
piece_col[p][1] = piece_col[p][0];
|
|
678
|
+
piece_col[p][0] = t;
|
|
679
|
+
sfx(2, 0x140, 3);
|
|
680
|
+
}
|
|
681
|
+
if (fresh & PCE_JOY_II) { /* cycle colours upward */
|
|
682
|
+
t = piece_col[p][0];
|
|
683
|
+
piece_col[p][0] = piece_col[p][1];
|
|
684
|
+
piece_col[p][1] = piece_col[p][2];
|
|
685
|
+
piece_col[p][2] = t;
|
|
686
|
+
sfx(2, 0x120, 3);
|
|
687
|
+
}
|
|
688
|
+
if (fresh & PCE_JOY_RUN) { /* hard drop */
|
|
689
|
+
while (can_place(p, piece_x[p], piece_y[p] + 1)) ++piece_y[p];
|
|
690
|
+
lock_piece(p); /* may end the game */
|
|
691
|
+
return;
|
|
692
|
+
}
|
|
693
|
+
if (pad & PCE_JOY_DOWN) fall_t[p] += 4; /* soft drop */
|
|
694
|
+
++fall_t[p];
|
|
695
|
+
fd = two_player ? VS_FALL_DELAY
|
|
696
|
+
: (u8)(32 - ((level << 1) + level)); /* 29..5 */
|
|
697
|
+
if (fall_t[p] >= fd) {
|
|
698
|
+
fall_t[p] = 0;
|
|
699
|
+
if (can_place(p, piece_x[p], piece_y[p] + 1))
|
|
700
|
+
++piece_y[p];
|
|
701
|
+
else
|
|
702
|
+
lock_piece(p); /* may end the game */
|
|
198
703
|
}
|
|
199
704
|
}
|
|
200
705
|
|
|
201
|
-
/* ──
|
|
202
|
-
*
|
|
203
|
-
*
|
|
204
|
-
*
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
for (c = 0; c < COLS; c++) {
|
|
221
|
-
col = grid[r][c];
|
|
222
|
-
if (col == 0) continue;
|
|
223
|
-
for (d = 0; d < 2; d++) {
|
|
224
|
-
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
225
|
-
/* (no run-start check: a mid-run scan only re-marks already-
|
|
226
|
-
* marked cells, so skipping the predecessor test is pure
|
|
227
|
-
* code-size savings on the 8KB PCE boot bank) */
|
|
228
|
-
len = 1;
|
|
229
|
-
sr = (int)r + dr; sc = (int)c + dc;
|
|
230
|
-
while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
|
|
231
|
-
&& grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
232
|
-
if (len >= 3) {
|
|
233
|
-
sr = r; sc = c;
|
|
234
|
-
for (k = 0; k < len; k++) {
|
|
235
|
-
if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
|
|
236
|
-
sr += dr; sc += dc;
|
|
237
|
-
}
|
|
706
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
|
|
707
|
+
* Only the falling trios are sprites (locked cells are BAT tiles): 3 SATB
|
|
708
|
+
* slots per player, 16x16 each. Cells above the rim aren't drawn — they'd
|
|
709
|
+
* poke out from under the HUD band. */
|
|
710
|
+
static void push_sprites(void) {
|
|
711
|
+
u8 p, i;
|
|
712
|
+
for (p = 0; p < 2; p++) {
|
|
713
|
+
u8 active = (state == ST_PLAY) && (p == 0 || two_player);
|
|
714
|
+
for (i = 0; i < 3; i++) {
|
|
715
|
+
s16 r = piece_y[p] + (s16)i;
|
|
716
|
+
u8 col = piece_col[p][i] ? piece_col[p][i] : 1;
|
|
717
|
+
u8 slot = SLOT_TRIO(p, i);
|
|
718
|
+
if (active && r >= 0) {
|
|
719
|
+
u16 x = (u16)((well_tc[p] + piece_x[p] * 2) * 8);
|
|
720
|
+
u16 y = (u16)((WELL_TR + r * 2) * 8);
|
|
721
|
+
set_sprite(slot, x, y, SPR_PAT((u16)(col - 1)), PAL_TRIO(col));
|
|
722
|
+
} else {
|
|
723
|
+
set_sprite(slot, 0, OFFSCREEN_Y, SPR_PAT(0), PAL_TRIO(1));
|
|
724
|
+
}
|
|
238
725
|
}
|
|
239
|
-
}
|
|
240
726
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
/* ── GAME LOGIC (clay) — screen painters (full BAT repaint per state change) ── */
|
|
730
|
+
static void paint_title(void) {
|
|
731
|
+
paint_backdrop();
|
|
732
|
+
draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 8, GAME_TITLE);
|
|
733
|
+
draw_text(10, 13, "1P RUN - I");
|
|
734
|
+
draw_text(10, 15, "2P VS - II");
|
|
735
|
+
draw_text(8, 19, "I II ROTATE RUN DROP");
|
|
736
|
+
draw_text(6, 22, "CHAINS FLOOD YOUR RIVAL");
|
|
737
|
+
draw_hud();
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
static void paint_play(void) {
|
|
741
|
+
paint_backdrop();
|
|
742
|
+
paint_frame(0);
|
|
743
|
+
paint_board(0);
|
|
744
|
+
if (two_player) {
|
|
745
|
+
paint_frame(1);
|
|
746
|
+
paint_board(1);
|
|
747
|
+
draw_text(15, 14, "VS");
|
|
254
748
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
static void
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
if (score < 9999) score += amt;
|
|
273
|
-
psg_tone(0, 0x180, 24); /* clear chime */
|
|
274
|
-
apply_gravity();
|
|
275
|
-
}
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
static void clear_triples(void) {
|
|
279
|
-
resolve_board();
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
static void lock_piece(void) {
|
|
283
|
-
u8 i;
|
|
284
|
-
int8_t r;
|
|
285
|
-
for (i = 0; i < 3; ++i) {
|
|
286
|
-
r = (int8_t)(piece_y + i);
|
|
287
|
-
if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
|
|
749
|
+
draw_hud();
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
753
|
+
static void start_game(u8 versus) {
|
|
754
|
+
u8 p, r, c;
|
|
755
|
+
two_player = versus;
|
|
756
|
+
well_tc[0] = versus ? WELL_VS_P1 : WELL_1P_TC;
|
|
757
|
+
well_tc[1] = WELL_VS_P2;
|
|
758
|
+
if (rng == 0) rng = 0xACE1;
|
|
759
|
+
for (p = 0; p < 2; p++) {
|
|
760
|
+
for (r = 0; r < GRID_H; r++)
|
|
761
|
+
for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
|
|
762
|
+
fall_t[p] = 0;
|
|
763
|
+
score[p] = 0;
|
|
764
|
+
prev_pad[p] = 0xFF; /* the button that started the game
|
|
765
|
+
* shouldn't also rotate the first trio */
|
|
288
766
|
}
|
|
289
|
-
|
|
290
|
-
|
|
767
|
+
cleared_total = 0;
|
|
768
|
+
level = 1;
|
|
769
|
+
state = ST_PLAY;
|
|
770
|
+
board_dirty[0] = 1;
|
|
771
|
+
board_dirty[1] = versus;
|
|
772
|
+
paint_play();
|
|
773
|
+
music_set(ST_PLAY);
|
|
774
|
+
sfx(2, 0x180, 6); /* start blip */
|
|
775
|
+
spawn_piece(0);
|
|
776
|
+
if (versus) spawn_piece(1);
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
780
|
+
* 2P INPUT via the TurboTap. pce_joy_read() reads pad 1 (slot 0). For pad 2 we
|
|
781
|
+
* read cc65's JOY_2 directly and translate it to the same clean PCE bitmask
|
|
782
|
+
* pce_input.c builds for pad 1. The host force-enables the TurboTap core
|
|
783
|
+
* option, so JOY_2 carries real port-1 input; without that override port 1 is
|
|
784
|
+
* dead and this would silently fall back to 1P. ── */
|
|
785
|
+
static u8 read_pad2(void) {
|
|
786
|
+
u8 raw = joy_read(JOY_2);
|
|
787
|
+
u8 m = 0;
|
|
788
|
+
if (JOY_UP(raw)) m |= PCE_JOY_UP;
|
|
789
|
+
if (JOY_DOWN(raw)) m |= PCE_JOY_DOWN;
|
|
790
|
+
if (JOY_LEFT(raw)) m |= PCE_JOY_LEFT;
|
|
791
|
+
if (JOY_RIGHT(raw)) m |= PCE_JOY_RIGHT;
|
|
792
|
+
if (JOY_BTN_1(raw)) m |= PCE_JOY_I;
|
|
793
|
+
if (JOY_BTN_2(raw)) m |= PCE_JOY_II;
|
|
794
|
+
if (JOY_BTN_3(raw)) m |= PCE_JOY_SELECT;
|
|
795
|
+
if (JOY_BTN_4(raw)) m |= PCE_JOY_RUN;
|
|
796
|
+
return m;
|
|
291
797
|
}
|
|
292
798
|
|
|
293
799
|
void main(void) {
|
|
294
|
-
u8
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
vce_set_color(
|
|
305
|
-
vce_set_color(
|
|
306
|
-
|
|
307
|
-
vce_set_color(
|
|
308
|
-
|
|
309
|
-
|
|
800
|
+
u8 pad1, pad2, newpad;
|
|
801
|
+
|
|
802
|
+
_pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
|
|
803
|
+
|
|
804
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
805
|
+
* Init order: palette → VRAM uploads → BAT paint → joypad → display ON.
|
|
806
|
+
* disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
|
|
807
|
+
* never returns and the game freezes on its first frame. */
|
|
808
|
+
/* BG sub-pal 0: backdrop/frame/interior + text-on-band. BG sub-pal 1:
|
|
809
|
+
* HUD/text (white). BG sub-pal 3: the three locked-cell hues. */
|
|
810
|
+
vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop: near-black blue */
|
|
811
|
+
vce_set_color(1, PCE_RGB(1, 1, 2)); /* cabinet block */
|
|
812
|
+
vce_set_color(2, PCE_RGB(1, 1, 1)); /* HUD band: dark grey */
|
|
813
|
+
vce_set_color(3, PCE_RGB(4, 4, 5)); /* well frame: steel */
|
|
814
|
+
vce_set_color(17, PCE_RGB(7, 7, 7)); /* pal1 text: white */
|
|
815
|
+
/* locked cells: one tile shape (colour index 1) on three BG sub-palettes
|
|
816
|
+
* (3/4/5) → three hues. Entry = sub-palette*16 + 1. */
|
|
817
|
+
vce_set_color(3 * 16 + 1, PCE_RGB(7, 5, 0)); /* pal3 c1: amber */
|
|
818
|
+
vce_set_color(4 * 16 + 1, PCE_RGB(0, 6, 5)); /* pal4 c1: teal */
|
|
819
|
+
vce_set_color(5 * 16 + 1, PCE_RGB(7, 1, 6)); /* pal5 c1: magenta */
|
|
820
|
+
/* sprite sub-palettes (256 + pal*16 + index) — the falling trio mirrors
|
|
821
|
+
* the locked-cell hues, one sub-palette per colour so all three trio
|
|
822
|
+
* colours are visible (push_sprites selects PAL_TRIO(col) per cell). */
|
|
823
|
+
vce_set_color(256 + 1 * 16 + 1, PCE_RGB(7, 5, 0)); /* spr pal1 c1: amber */
|
|
824
|
+
vce_set_color(256 + 2 * 16 + 1, PCE_RGB(0, 6, 5)); /* spr pal2 c1: teal */
|
|
825
|
+
vce_set_color(256 + 3 * 16 + 1, PCE_RGB(7, 1, 6)); /* spr pal3 c1: magenta */
|
|
310
826
|
|
|
311
827
|
upload_art();
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
fall_timer = 0;
|
|
318
|
-
rng = 0x1357;
|
|
319
|
-
prev_pad = 0;
|
|
320
|
-
sfx_timer = 0;
|
|
321
|
-
new_piece();
|
|
322
|
-
draw_grid();
|
|
828
|
+
|
|
829
|
+
hiscore = hiscore_load(); /* always 0 — no persistence on a bare HuCard */
|
|
830
|
+
state = ST_TITLE;
|
|
831
|
+
paint_title();
|
|
832
|
+
music_set(ST_TITLE);
|
|
323
833
|
|
|
324
834
|
pce_joy_init();
|
|
325
|
-
|
|
835
|
+
disp_enable();
|
|
326
836
|
|
|
327
837
|
for (;;) {
|
|
328
|
-
u8 fall_rate;
|
|
329
838
|
waitvsync();
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
839
|
+
|
|
840
|
+
/* ── vblank work first: BAT repaints + sprites + SATB DMA ──
|
|
841
|
+
* Whole-board BAT repaint (see the WHOLE-BOARD REPAINT idiom) — both
|
|
842
|
+
* dirty wells stream in this one vblank, then the SATB DMA. */
|
|
843
|
+
if (board_dirty[0]) { paint_board(0); board_dirty[0] = 0; }
|
|
844
|
+
if (two_player && board_dirty[1]) { paint_board(1); board_dirty[1] = 0; }
|
|
845
|
+
if (hud_dirty) { draw_hud(); hud_dirty = 0; }
|
|
846
|
+
push_sprites();
|
|
847
|
+
satb_dma();
|
|
848
|
+
|
|
849
|
+
music_tick();
|
|
850
|
+
if (sfx_timer) {
|
|
851
|
+
--sfx_timer;
|
|
852
|
+
if (sfx_timer == 0) { psg_off(2); psg_off(3); }
|
|
342
853
|
}
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
854
|
+
|
|
855
|
+
/* ── 2P input via the TurboTap (see read_pad2's idiom note). In 2P
|
|
856
|
+
* versus BOTH play simultaneously, so we read BOTH pads every frame;
|
|
857
|
+
* on the menus only pad 1 matters. ── */
|
|
858
|
+
pad1 = pce_joy_read();
|
|
859
|
+
pad2 = (state == ST_PLAY && two_player) ? read_pad2() : 0;
|
|
860
|
+
|
|
861
|
+
if (state == ST_TITLE) {
|
|
862
|
+
newpad = (u8)(pad1 & ~prev_pad[0]);
|
|
863
|
+
prev_pad[0] = pad1;
|
|
864
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_game(0);
|
|
865
|
+
else if (newpad & PCE_JOY_II) start_game(1);
|
|
350
866
|
continue;
|
|
351
867
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
lock_piece();
|
|
360
|
-
new_piece();
|
|
361
|
-
} else {
|
|
362
|
-
piece_y++;
|
|
868
|
+
if (state == ST_OVER) {
|
|
869
|
+
newpad = (u8)(pad1 & ~prev_pad[0]);
|
|
870
|
+
prev_pad[0] = pad1;
|
|
871
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
|
|
872
|
+
state = ST_TITLE;
|
|
873
|
+
paint_title();
|
|
874
|
+
music_set(ST_TITLE);
|
|
363
875
|
}
|
|
876
|
+
continue;
|
|
364
877
|
}
|
|
365
878
|
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
879
|
+
/* ── ST_PLAY — both players update every frame (simultaneous versus,
|
|
880
|
+
* not alternating turns). Any update can end the game, so re-check
|
|
881
|
+
* state between them. ── */
|
|
882
|
+
update_player(0, pad1);
|
|
883
|
+
if (two_player && state == ST_PLAY) update_player(1, pad2);
|
|
369
884
|
}
|
|
370
885
|
}
|