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,208 +1,946 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
/* ── puzzle.c — C64 falling-trio versus puzzle (complete example game) ────────
|
|
2
|
+
*
|
|
3
|
+
* MAGMA MATCH — 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 CONTROL PORT 2, P2 on CONTROL PORT 1,
|
|
6
|
+
* both falling at once, where every cascade chain you score erupts garbage
|
|
7
|
+
* rows up from the bottom of your rival's well. Score + in-session hi-score
|
|
8
|
+
* behind the gated persistence seam, 2-voice SID music with the C64's
|
|
9
|
+
* signature filter sweep + SFX, and the C64's signature raster-IRQ split:
|
|
10
|
+
* a fixed HUD bar over the wells.
|
|
11
|
+
*
|
|
12
|
+
* The game: a falling-trio match-3. A vertical trio of blocks drops into a
|
|
13
|
+
* well; LEFT/RIGHT move it, UP cycles its three colours, FIRE hard-drops,
|
|
14
|
+
* DOWN soft-drops. When it lands, any straight run of 3+ same-coloured
|
|
15
|
+
* blocks (horizontal, vertical, or diagonal) clears; survivors fall and
|
|
16
|
+
* cascades chain for multiplied score. First stack to reach the rim loses.
|
|
17
|
+
*
|
|
18
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
19
|
+
* very different one. The markers tell you what's what:
|
|
20
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented C64 footgun; reshape
|
|
21
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
22
|
+
* GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
|
|
23
|
+
*
|
|
24
|
+
* What depends on what:
|
|
25
|
+
* c64_registers.h — VIC-II / SID / CIA symbolic addresses (header only).
|
|
26
|
+
* c64_sfx.{h,c} — one-shot SID sound effects on voice 2.
|
|
27
|
+
* The BASIC stub + crt0 come from cc65's c64 target: the .prg loads at
|
|
28
|
+
* $0801, a tiny BASIC line does SYS into the C runtime, and the KERNAL
|
|
29
|
+
* stays banked in (we lean on that for the IRQ vector — see below).
|
|
30
|
+
*
|
|
31
|
+
* Memory map this file assumes (VIC bank 0 = $0000-$3FFF):
|
|
32
|
+
* $0400 screen RAM (40×25 chars) $D800 color RAM (per-cell color)
|
|
33
|
+
* $0801 this program (code+data grow up from here)
|
|
34
|
+
* Keep the program under ~14 KB. (No hardware sprites here — the whole
|
|
35
|
+
* board is screen-RAM CHARACTERS, so the classic $0800 / $1000 sprite-data
|
|
36
|
+
* trap doesn't even come up. The falling trio is drawn as chars too.)
|
|
37
|
+
*
|
|
38
|
+
* Frame budget (PAL, 50fps) — and a TEACHING POINT vs the NES version of
|
|
39
|
+
* this game (examples/nes/templates/puzzle.c): on the NES, board repaints
|
|
40
|
+
* squeeze through a ~16-entry vblank queue, so a full-board repaint is
|
|
41
|
+
* BUDGETED across 12 frames of dirty-row bitmask tricks. The C64 has NO
|
|
42
|
+
* VRAM port — screen RAM is plain memory, writable any time, mid-frame.
|
|
43
|
+
* But the C64's famine is CPU, not bandwidth: a full 880-cell repaint of
|
|
44
|
+
* cc65-generated C costs ~50 frames (a frozen second). So this game NEVER
|
|
45
|
+
* repaints the whole screen during play — it tracks the cells that actually
|
|
46
|
+
* changed and repaints ONLY those (see the cell-diff idiom). Same genre,
|
|
47
|
+
* a different scarcity to design around — fork accordingly.
|
|
48
|
+
*/
|
|
7
49
|
|
|
8
50
|
#include "c64_registers.h"
|
|
9
51
|
#include "c64_sfx.h"
|
|
10
52
|
#include <stdint.h>
|
|
11
53
|
|
|
54
|
+
/* cc65 KERNAL disk-I/O prototypes. We DON'T #include <cbm.h> — it drags in
|
|
55
|
+
* <c64.h>, whose VIC/SID/JOY macros collide with this project's
|
|
56
|
+
* c64_registers.h (cc65 errors "macro redefinition is not identical"). These
|
|
57
|
+
* four are the stable cc65 ABI; declaring them directly avoids the clash. */
|
|
58
|
+
unsigned char __fastcall__ cbm_open(unsigned char lfn, unsigned char device,
|
|
59
|
+
unsigned char sec_addr, const char *name);
|
|
60
|
+
void __fastcall__ cbm_close(unsigned char lfn);
|
|
61
|
+
int __fastcall__ cbm_read(unsigned char lfn, void *buffer, unsigned int size);
|
|
62
|
+
int __fastcall__ cbm_write(unsigned char lfn, const void *buffer, unsigned int size);
|
|
63
|
+
|
|
64
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
65
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
66
|
+
#define GAME_TITLE "MAGMA MATCH"
|
|
67
|
+
|
|
12
68
|
#define POKE(addr, val) (*(volatile uint8_t*)(addr) = (val))
|
|
13
69
|
#define PEEK(addr) (*(volatile uint8_t*)(addr))
|
|
14
70
|
|
|
15
71
|
#define SCREEN ((volatile uint8_t*)0x0400)
|
|
16
72
|
#define COLORS ((volatile uint8_t*)0xD800)
|
|
17
73
|
|
|
74
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
75
|
+
* Board geometry. Each cell is ONE 40×25 character. Wells are 6 wide × 12
|
|
76
|
+
* tall. In 1P a single well is centred; in 2P two wells split the screen.
|
|
77
|
+
* char row 0 — HUD text: SC / HI / LV / P2 score (FIXED, the raster split)
|
|
78
|
+
* char row 1 — solid divider line (FIXED)
|
|
79
|
+
* char row 2 — blank spacer: the split lands mid-row HERE (jitter-proof)
|
|
80
|
+
* char rows 3.. — the wells (frame at WELL_TOP-1 / WELL_TOP+GRID_H) */
|
|
81
|
+
#define GRID_W 6
|
|
82
|
+
#define GRID_H 12
|
|
83
|
+
#define WELL_TOP 5 /* top char ROW of a well's interior */
|
|
84
|
+
#define WELL_1P_X 17 /* 1P: single centred well (cols 17-22) */
|
|
85
|
+
#define WELL_VS_P1 6 /* 2P: P1 interior cols 6-11 ... */
|
|
86
|
+
#define WELL_VS_P2 28 /* P2 interior cols 28-33 (split board) */
|
|
87
|
+
|
|
88
|
+
#define EMPTY 0 /* cell colours 1..3 = magma / ember / ash */
|
|
89
|
+
|
|
90
|
+
/* Char codes + colours for the board cells. A filled cell is a reverse-space
|
|
91
|
+
* solid block tinted by its colour; an empty cell is a faint speck so the
|
|
92
|
+
* well reads as a recessed playfield instead of a black void. */
|
|
93
|
+
#define CH_BLOCK 0xA0 /* reverse-space solid block (the trio/locked) */
|
|
94
|
+
#define CH_DOT 0x2E /* '.' faint speck = empty well cell */
|
|
95
|
+
#define CH_FRAME 0xE6 /* checkered frame glyph = well border */
|
|
96
|
+
#define CH_BLANK 0x20
|
|
97
|
+
/* colour 1..3 → a C64 colour code (magma reds/oranges + ash grey). */
|
|
98
|
+
static const uint8_t cell_color[4] = {
|
|
99
|
+
COLOR_DARK_GRAY, /* 0 = empty speck (dim) */
|
|
100
|
+
COLOR_LIGHT_RED, /* 1 = magma */
|
|
101
|
+
COLOR_ORANGE, /* 2 = ember */
|
|
102
|
+
COLOR_LIGHT_GRAY, /* 3 = ash */
|
|
103
|
+
};
|
|
104
|
+
|
|
105
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
106
|
+
* THE RASTER-IRQ SPLIT — the C64's classic "fixed status bar over a moving
|
|
107
|
+
* world" trick (and the gateway drug to all raster effects). Here it pins a
|
|
108
|
+
* HUD bar at the top while the wells live below it. The VIC-II has ONE
|
|
109
|
+
* $D016 fine-scroll for the whole frame; we don't scroll the wells (a puzzle
|
|
110
|
+
* board holds still), but the split is STILL the idiomatic way to guarantee
|
|
111
|
+
* the HUD's first rows render in a known, fixed scroll state regardless of
|
|
112
|
+
* what the rest of the frame does — and it gives you the per-frame heartbeat
|
|
113
|
+
* the main loop paces on. Two IRQs ping-pong per frame:
|
|
114
|
+
*
|
|
115
|
+
* line 68 (inside the blank spacer row 2): assert the board's $D016
|
|
116
|
+
* → everything below the split renders in the board's scroll state
|
|
117
|
+
* line 251 (just past the text window): assert the bar's $D016
|
|
118
|
+
* → next frame's HUD rows render fixed; this IRQ is also the
|
|
119
|
+
* game's frame heartbeat (increments frame_count)
|
|
120
|
+
*
|
|
121
|
+
* The handshake, register by register:
|
|
122
|
+
* $D012 raster compare line (low 8 bits)
|
|
123
|
+
* $D011 b7 raster compare bit 8 — MUST be 0 here (both lines < 256).
|
|
124
|
+
* Forgetting this bit is the classic "my IRQ fires on the
|
|
125
|
+
* wrong line / twice" bug when lines ≥ 256 get involved.
|
|
126
|
+
* $D01A b0 raster IRQ enable
|
|
127
|
+
* $D019 IRQ latch. ACK by WRITING THE BITS BACK (write-1-to-clear).
|
|
128
|
+
* THE LOAD-BEARING LINE: skip the ack and the IRQ re-fires the
|
|
129
|
+
* instant it returns, forever — the main loop starves and the
|
|
130
|
+
* machine looks hung.
|
|
131
|
+
* $0314/15 the KERNAL's IRQ indirection. The hardware vector ($FFFE)
|
|
132
|
+
* points into KERNAL ROM, which saves A/X/Y and jumps through
|
|
133
|
+
* $0314 — so with the KERNAL banked in (cc65 default) we just
|
|
134
|
+
* repoint $0314. Exit via jmp $EA81 (KERNAL: restore regs +
|
|
135
|
+
* rti), SKIPPING $EA31's jiffy-clock/keyboard scan.
|
|
136
|
+
* $DC0D CIA1 interrupt control. The KERNAL leaves a 60Hz CIA timer
|
|
137
|
+
* IRQ running (the jiffy clock); disable it ($7F = clear all
|
|
138
|
+
* sources) and ack it (read $DC0D) or it shares the IRQ line
|
|
139
|
+
* with the raster and fires our handler at random lines.
|
|
140
|
+
*
|
|
141
|
+
* JITTER: an IRQ only starts after the current instruction finishes, so the
|
|
142
|
+
* handler begins 0-7 cycles late, plus the KERNAL thunk (~35 cycles) — the
|
|
143
|
+
* $D016 write lands one-to-two raster lines after SPLIT_LINE. We hide that
|
|
144
|
+
* by splitting inside a UNIFORM blank row, where shifting the (invisible)
|
|
145
|
+
* pixels mid-line changes nothing. Splits next to visible detail need
|
|
146
|
+
* cycle-exact stabilization (double-IRQ trick) — don't go there until you do.
|
|
147
|
+
*
|
|
148
|
+
* The handler is ASSEMBLY-IN-C on purpose: cc65's generated C uses shared
|
|
149
|
+
* zero-page scratch registers, so a C-level IRQ body would corrupt whatever
|
|
150
|
+
* the main loop was computing. These asm lines touch only A + the flags
|
|
151
|
+
* (which the KERNAL thunk already saved). requires: KERNAL banked in,
|
|
152
|
+
* frame_count file-scope NON-static (asm %v needs the symbol). */
|
|
153
|
+
#define SPLIT_LINE 68 /* inside spacer row 2 (67-74): jitter-proof */
|
|
154
|
+
#define BOTTOM_LINE 251 /* first line below the 25-row text window */
|
|
155
|
+
#define D016_BAR 0xC0 /* fine X = 0, 38-col mode for both halves */
|
|
156
|
+
|
|
157
|
+
volatile uint8_t frame_count; /* bumped by the bottom IRQ — frame heartbeat */
|
|
158
|
+
|
|
159
|
+
void raster_irq(void) {
|
|
160
|
+
asm("lda $d019"); /* read VIC IRQ latch... */
|
|
161
|
+
asm("sta $d019"); /* ...write it back = ACK (write-1-to-clear).
|
|
162
|
+
* THE line you must not lose (see above). */
|
|
163
|
+
asm("lda $d012"); /* which raster line woke us? (self-correcting
|
|
164
|
+
* dispatch — no phase variable to desync) */
|
|
165
|
+
asm("cmp #150");
|
|
166
|
+
asm("bcs %g", at_bottom); /* ≥150 → we're at BOTTOM_LINE */
|
|
167
|
+
/* — split point (line ~68, inside the blank spacer row) — */
|
|
168
|
+
asm("lda #$C0"); /* = D016_BAR — board holds still, same scroll */
|
|
169
|
+
asm("sta $d016");
|
|
170
|
+
asm("lda #251"); /* = BOTTOM_LINE (cc65's asm %b only takes */
|
|
171
|
+
asm("sta $d012"); /* signed bytes, so these are literals — the */
|
|
172
|
+
asm("jmp $ea81"); /* #if below keeps them honest) */
|
|
173
|
+
at_bottom:
|
|
174
|
+
asm("lda #$C0"); /* = D016_BAR */
|
|
175
|
+
asm("sta $d016"); /* bar scroll for the top of the NEXT frame */
|
|
176
|
+
asm("inc %v", frame_count);/* frame heartbeat for the main loop */
|
|
177
|
+
asm("lda #%b", SPLIT_LINE);
|
|
178
|
+
asm("sta $d012"); /* next stop: the split line */
|
|
179
|
+
asm("jmp $ea81"); /* KERNAL: pla/tay/pla/tax/pla/rti */
|
|
180
|
+
}
|
|
181
|
+
#if BOTTOM_LINE != 251 || D016_BAR != 0xC0
|
|
182
|
+
#error raster_irq's asm immediates are out of sync with BOTTOM_LINE / D016_BAR
|
|
183
|
+
#endif
|
|
184
|
+
|
|
185
|
+
static void install_raster_irq(void) {
|
|
186
|
+
asm("sei"); /* no IRQs while we rewire them */
|
|
187
|
+
POKE(CIA1_PRA + 0x0D, 0x7F); /* $DC0D: disable ALL CIA1 IRQ sources
|
|
188
|
+
* (kills the KERNAL jiffy/keyboard IRQ
|
|
189
|
+
* — we read the sticks ourselves) */
|
|
190
|
+
(void)PEEK(CIA1_PRA + 0x0D); /* reading $DC0D acks anything pending */
|
|
191
|
+
POKE(0x0314, (uint8_t)((unsigned)raster_irq & 0xFF));
|
|
192
|
+
POKE(0x0315, (uint8_t)((unsigned)raster_irq >> 8));
|
|
193
|
+
POKE(VIC_CTRL1, 0x1B); /* $D011 = power-on default: screen on,
|
|
194
|
+
* 25 rows, YSCROLL=3, and bit 7 (raster
|
|
195
|
+
* compare bit 8) = 0 — both lines < 256 */
|
|
196
|
+
POKE(VIC_RASTER, SPLIT_LINE); /* first stop */
|
|
197
|
+
POKE(VIC_IRQ_ENA, 0x01); /* raster IRQ on */
|
|
198
|
+
POKE(VIC_IRQ, 0xFF); /* clear any stale latch bits */
|
|
199
|
+
asm("cli");
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/* Wait for the bottom IRQ's heartbeat. Replaces the usual poll-$D012 loop —
|
|
203
|
+
* the IRQ owns the raster now, the main loop just paces itself on it. */
|
|
204
|
+
static void wait_frame(void) {
|
|
205
|
+
uint8_t f = frame_count;
|
|
206
|
+
while (frame_count == f) { }
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ── reading BOTH
|
|
210
|
+
* joystick ports. CIA1 port A ($DC00) = control port 2, port B ($DC01) =
|
|
211
|
+
* control port 1. Active-low: a pressed switch reads 0, so invert and mask
|
|
212
|
+
* to bits 0-4 (up/down/left/right/fire).
|
|
213
|
+
*
|
|
214
|
+
* THE PORT-1 GOTCHA: $DC01 is ALSO the keyboard row register — the matrix
|
|
215
|
+
* hangs off the same CIA lines. Writing $FF to $DC00 first deselects every
|
|
216
|
+
* keyboard column, so held keys can't pull $DC01 rows low and ghost into
|
|
217
|
+
* the port-1 stick. That's also why "port 2 is the C64 game port": P1 lives
|
|
218
|
+
* there by convention, and this game puts the SECOND player on port 1.
|
|
219
|
+
* requires: install_raster_irq already disabled the KERNAL's keyboard scan,
|
|
220
|
+
* so nothing else rewrites $DC00. */
|
|
221
|
+
static uint8_t read_stick_port2(void) { /* player 1 */
|
|
222
|
+
POKE(CIA1_PRA, 0xFF);
|
|
223
|
+
return (uint8_t)(~PEEK(CIA1_PRA) & 0x1F);
|
|
224
|
+
}
|
|
225
|
+
static uint8_t read_stick_port1(void) { /* player 2 */
|
|
226
|
+
POKE(CIA1_PRA, 0xFF);
|
|
227
|
+
return (uint8_t)(~PEEK(CIA1_PRB) & 0x1F);
|
|
228
|
+
}
|
|
18
229
|
#define JOY_UP 0x01
|
|
19
230
|
#define JOY_DOWN 0x02
|
|
20
231
|
#define JOY_LEFT 0x04
|
|
21
232
|
#define JOY_RIGHT 0x08
|
|
22
233
|
#define JOY_FIRE 0x10
|
|
23
234
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
235
|
+
/* ── HARDWARE IDIOM (load-bearing) — hi-score persistence: DISK SAVE ─────────
|
|
236
|
+
* The C64 has no battery SRAM — the honest save medium is the FLOPPY. A game
|
|
237
|
+
* persists by writing a file to drive 8; VICE commits it into the live 1541
|
|
238
|
+
* disk image (true-drive GCR write-back), so a save survives a power cycle
|
|
239
|
+
* exactly as it did on real hardware. (To capture it headlessly the host does
|
|
240
|
+
* state({op:'exportDisk', path}); re-loading that .d64 restores the save.)
|
|
241
|
+
*
|
|
242
|
+
* REQUIRES THE GAME RUN FROM A DISK: build/package it as a .d64 and load THAT
|
|
243
|
+
* (loadMedia autostarts it). A bare .prg injected straight into RAM has no
|
|
244
|
+
* mounted disk to save to, so the save is a silent no-op — still honest (the
|
|
245
|
+
* value just stays in-session), it simply has nowhere to persist.
|
|
246
|
+
*
|
|
247
|
+
* We keep a 2-byte record in a SEQ file "HI" on drive 8. These are the STABLE
|
|
248
|
+
* SEAM: the game calls hiscore_load at boot and hiscore_save on a new record;
|
|
249
|
+
* reshape the record format freely, just keep the two function signatures. */
|
|
250
|
+
#define SAVE_NAME "@0:HI,S,W" /* @ = replace-if-exists; S=SEQ, W=write */
|
|
251
|
+
#define LOAD_NAME "0:HI,S,R"
|
|
252
|
+
|
|
253
|
+
static uint16_t hiscore_load(void) {
|
|
254
|
+
uint16_t v = 0;
|
|
255
|
+
uint8_t buf[2];
|
|
256
|
+
if (cbm_open(2, 8, 2, LOAD_NAME) == 0) {
|
|
257
|
+
if (cbm_read(2, buf, 2) == 2) v = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
|
|
258
|
+
cbm_close(2);
|
|
259
|
+
}
|
|
260
|
+
return v; /* 0 if the file isn't there yet (first ever boot) */
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
static void hiscore_save(uint16_t v) {
|
|
264
|
+
uint8_t buf[2];
|
|
265
|
+
buf[0] = (uint8_t)(v & 0xFF);
|
|
266
|
+
buf[1] = (uint8_t)(v >> 8);
|
|
267
|
+
if (cbm_open(2, 8, 2, SAVE_NAME) == 0) {
|
|
268
|
+
cbm_write(2, buf, 2);
|
|
269
|
+
cbm_close(2);
|
|
270
|
+
}
|
|
271
|
+
/* No disk mounted (ran as a bare .prg) -> cbm_open fails -> silent no-op. */
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/* ── GAME LOGIC (clay) — SID music: 2 voices + THE filter sweep ─────────────
|
|
275
|
+
* Voice 0 = melody (pulse), voice 1 = bass (sawtooth THROUGH THE FILTER),
|
|
276
|
+
* voice 2 is reserved for sound effects (c64_sfx). Each voice walks a
|
|
277
|
+
* (freq, frames) note table once per frame; end wraps → continuous loop.
|
|
278
|
+
*
|
|
279
|
+
* THE SID FILTER — the C64's sonic signature, and the part most "music
|
|
280
|
+
* drivers ported from other chips" miss. One analog-modeled filter, shared
|
|
281
|
+
* by all voices, four registers:
|
|
282
|
+
* $D415 cutoff low 3 bits $D416 cutoff high 8 bits (11-bit total)
|
|
283
|
+
* $D417 high nibble = resonance 0-15; low 3 bits ROUTE voices into the
|
|
284
|
+
* filter (bit0=voice0, bit1=voice1, bit2=voice2)
|
|
285
|
+
* $D418 bit4=lowpass bit5=bandpass bit6=highpass — AND master volume in
|
|
286
|
+
* bits 0-3. Volume and filter mode share a register: any "set
|
|
287
|
+
* volume" helper that writes plain $0F silently turns the filter
|
|
288
|
+
* OFF (c64_sfx's sfx_init does exactly that, so music_init runs
|
|
289
|
+
* AFTER it and re-asserts the mode bits).
|
|
290
|
+
* FOOTGUN: bit 7 of $D418 is "3OFF" — it MUTES voice 3 entirely.
|
|
291
|
+
* Set it by accident and all your sound effects vanish.
|
|
292
|
+
* The sweep: a triangle LFO walks the cutoff up and down each frame over
|
|
293
|
+
* the resonant lowpass — the bass goes from muffled to snarling and back,
|
|
294
|
+
* the "wah" that screams Commodore. Hear it change: that IS the chip. */
|
|
295
|
+
#define N_C3 0x1199u
|
|
296
|
+
#define N_D3 0x13EEu
|
|
297
|
+
#define N_E3 0x1666u
|
|
298
|
+
#define N_F3 0x1798u
|
|
299
|
+
#define N_G3 0x1AE6u
|
|
300
|
+
#define N_A3 0x1E78u
|
|
301
|
+
#define N_B3 0x2253u
|
|
302
|
+
#define N_C4 0x2333u
|
|
303
|
+
#define N_D4 0x27DDu
|
|
304
|
+
#define N_E4 0x2CCCu
|
|
305
|
+
#define N_F4 0x2F30u
|
|
306
|
+
#define N_G4 0x35CCu
|
|
307
|
+
#define N_A4 0x3CF1u
|
|
308
|
+
#define N_B4 0x44A7u
|
|
309
|
+
#define N_C5 0x4666u
|
|
310
|
+
#define N_D5 0x4FBAu
|
|
311
|
+
#define N_E5 0x5998u
|
|
312
|
+
#define N_REST 0u
|
|
313
|
+
#define STEP 9 /* frames per melodic eighth-note (~140 BPM PAL) */
|
|
314
|
+
|
|
315
|
+
typedef struct { uint16_t freq; uint8_t len; } Note;
|
|
28
316
|
|
|
29
|
-
|
|
317
|
+
/* The table IS the song — edit these to rescore your fork. A brooding minor
|
|
318
|
+
* line that suits a magma well. */
|
|
319
|
+
static const Note melody[] = {
|
|
320
|
+
{ N_A4, STEP*2 }, { N_C5, STEP }, { N_B4, STEP }, { N_A4, STEP*2 }, { N_E4, STEP*2 },
|
|
321
|
+
{ N_F4, STEP*2 }, { N_A4, STEP }, { N_C5, STEP }, { N_A4, STEP*2 }, { N_REST, STEP },
|
|
322
|
+
{ N_G4, STEP }, { N_B4, STEP }, { N_D5, STEP*2 }, { N_B4, STEP }, { N_G4, STEP*2 }, { N_D5, STEP },
|
|
323
|
+
{ N_E5, STEP }, { N_D5, STEP }, { N_C5, STEP }, { N_B4, STEP }, { N_A4, STEP*2 }, { N_REST, STEP },
|
|
324
|
+
{ N_C5, STEP }, { N_B4, STEP }, { N_A4, STEP }, { N_G4, STEP }, { N_F4, STEP*2 }, { N_E4, STEP*2 },
|
|
325
|
+
{ N_A4, STEP }, { N_C5, STEP }, { N_E5, STEP*2 }, { N_C5, STEP }, { N_A4, STEP*2 }, { N_REST, STEP },
|
|
326
|
+
};
|
|
327
|
+
static const Note bassline[] = {
|
|
328
|
+
/* Octave-pumping bass — the filter sweep chews on this. */
|
|
329
|
+
{ N_A3, STEP*3 }, { N_A3, STEP }, { N_E3, STEP*2 }, { N_A3, STEP*2 },
|
|
330
|
+
{ N_F3, STEP*3 }, { N_C3, STEP }, { N_F3, STEP*2 }, { N_C4, STEP*2 },
|
|
331
|
+
{ N_G3, STEP*3 }, { N_D3, STEP }, { N_G3, STEP*2 }, { N_B3, STEP*2 },
|
|
332
|
+
{ N_A3, STEP*3 }, { N_E3, STEP }, { N_A3, STEP*2 }, { N_C4, STEP*2 },
|
|
333
|
+
};
|
|
334
|
+
#define MELODY_LEN (sizeof(melody) / sizeof(melody[0]))
|
|
335
|
+
#define BASS_LEN (sizeof(bassline) / sizeof(bassline[0]))
|
|
30
336
|
|
|
31
|
-
static uint8_t
|
|
32
|
-
static
|
|
33
|
-
static
|
|
34
|
-
static uint8_t fall_timer;
|
|
35
|
-
static uint16_t score;
|
|
36
|
-
static uint32_t rng = 1;
|
|
337
|
+
static uint8_t m_pos[2], m_left[2];
|
|
338
|
+
static uint16_t filter_cut; /* 11-bit cutoff, 0-2047 */
|
|
339
|
+
static uint8_t filter_up;
|
|
37
340
|
|
|
38
|
-
static void
|
|
39
|
-
|
|
40
|
-
|
|
341
|
+
static void music_trigger(uint8_t v, uint16_t freq, uint8_t wave) {
|
|
342
|
+
if (freq == N_REST) {
|
|
343
|
+
POKE(SID_CTRL(v), wave); /* gate off: release tail plays */
|
|
344
|
+
return;
|
|
345
|
+
}
|
|
346
|
+
POKE(SID_FREQ_LO(v), (uint8_t)(freq & 0xFF));
|
|
347
|
+
POKE(SID_FREQ_HI(v), (uint8_t)(freq >> 8));
|
|
348
|
+
POKE(SID_CTRL(v), wave); /* gate OFF then ON — the 6581/8580 */
|
|
349
|
+
POKE(SID_CTRL(v), wave | SID_GATE); /* envelope only retriggers on the
|
|
350
|
+
* 0→1 gate edge */
|
|
41
351
|
}
|
|
42
352
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
353
|
+
static void music_init(void) {
|
|
354
|
+
/* Melody: pulse at 50% duty, snappy envelope. */
|
|
355
|
+
POKE(SID_PW_LO(0), 0x00); POKE(SID_PW_HI(0), 0x08);
|
|
356
|
+
POKE(SID_AD(0), 0x07); /* attack 0, decay 7 */
|
|
357
|
+
POKE(SID_SR(0), 0x84); /* sustain 8, release 4 */
|
|
358
|
+
/* Bass: sawtooth (harmonically rich — gives the filter teeth to chew). */
|
|
359
|
+
POKE(SID_AD(1), 0x06);
|
|
360
|
+
POKE(SID_SR(1), 0xA5);
|
|
361
|
+
/* Filter: route VOICE 1 ONLY into it (bit 1 of $D417), resonance 13/15. */
|
|
362
|
+
POKE(SID_RES_FILT, 0xD2);
|
|
363
|
+
/* Lowpass mode + master volume 15. NOTE bits shared with volume, and bit
|
|
364
|
+
* 7 (3OFF) stays 0 or voice-2 sound effects go silent — see block doc. */
|
|
365
|
+
POKE(SID_VOL_MODE, 0x1F);
|
|
366
|
+
filter_cut = 0x180; filter_up = 1;
|
|
367
|
+
m_pos[0] = m_pos[1] = 0;
|
|
368
|
+
m_left[0] = m_left[1] = 1; /* triggers both voices on the next update */
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
static void music_update(void) {
|
|
372
|
+
/* Note sequencing, one table per voice. */
|
|
373
|
+
if (--m_left[0] == 0) {
|
|
374
|
+
music_trigger(0, melody[m_pos[0]].freq, SID_PULSE);
|
|
375
|
+
m_left[0] = melody[m_pos[0]].len;
|
|
376
|
+
if (++m_pos[0] >= MELODY_LEN) m_pos[0] = 0;
|
|
56
377
|
}
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
378
|
+
if (--m_left[1] == 0) {
|
|
379
|
+
music_trigger(1, bassline[m_pos[1]].freq, SID_SAWTOOTH);
|
|
380
|
+
m_left[1] = bassline[m_pos[1]].len;
|
|
381
|
+
if (++m_pos[1] >= BASS_LEN) m_pos[1] = 0;
|
|
61
382
|
}
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
383
|
+
/* THE FILTER SWEEP — triangle LFO on the cutoff, ~10s round trip.
|
|
384
|
+
* 11-bit value split across two registers: low 3 bits in $D415,
|
|
385
|
+
* high 8 in $D416. */
|
|
386
|
+
if (filter_up) { filter_cut += 6; if (filter_cut >= 0x700) filter_up = 0; }
|
|
387
|
+
else { filter_cut -= 6; if (filter_cut <= 0x180) filter_up = 1; }
|
|
388
|
+
POKE(SID_FILTER_LO, (uint8_t)(filter_cut & 0x07));
|
|
389
|
+
POKE(SID_FILTER_HI, (uint8_t)(filter_cut >> 3));
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
/* ── GAME LOGIC (clay) — screen text. The C64 has NO VRAM port: screen RAM
|
|
393
|
+
* is plain memory, writable any time, mid-frame, no vblank dance. The only
|
|
394
|
+
* translation is ASCII → SCREEN CODES (not PETSCII!): A-Z land at 1-26;
|
|
395
|
+
* space through '?' (incl. digits) keep their ASCII values. ── */
|
|
396
|
+
static void draw_text(uint8_t row, uint8_t col, const char *s) {
|
|
397
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
398
|
+
uint8_t ch;
|
|
399
|
+
while ((ch = (uint8_t)*s++) != 0) {
|
|
400
|
+
if (ch >= 'A' && ch <= 'Z') ch -= 64; /* A-Z → screen codes 1-26 */
|
|
401
|
+
SCREEN[off] = ch; /* 32-63 map straight through */
|
|
402
|
+
COLORS[off] = COLOR_WHITE;
|
|
403
|
+
++off;
|
|
66
404
|
}
|
|
67
|
-
/* Clear the well interior to black so colored blocks stand out. */
|
|
68
|
-
for (r = 0; r < ROWS; r++)
|
|
69
|
-
for (c = 0; c < COLS; c++) SCREEN[(GRID_R + r) * 40 + GRID_C + c] = ' ';
|
|
70
405
|
}
|
|
406
|
+
/* Blank the whole 40-col row, then draw `s` on it — a clean text BAND, so
|
|
407
|
+
* message text reads cleanly over whatever the board left behind. */
|
|
408
|
+
static void draw_text_band(uint8_t row, uint8_t col, const char *s) {
|
|
409
|
+
uint8_t c;
|
|
410
|
+
volatile uint8_t *p = SCREEN + (uint16_t)row * 40;
|
|
411
|
+
for (c = 0; c < 40; c++) p[c] = CH_BLANK;
|
|
412
|
+
draw_text(row, col, s);
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
|
|
416
|
+
uint8_t i, d[5];
|
|
417
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
418
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
419
|
+
for (i = 0; i < 5; i++) {
|
|
420
|
+
SCREEN[off + i] = (uint8_t)('0' + d[4 - i]); /* digit screen code = ASCII */
|
|
421
|
+
COLORS[off + i] = COLOR_WHITE;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a few instructions) ── */
|
|
426
|
+
static uint16_t rng = 0xACE1;
|
|
427
|
+
static uint8_t random8(void) {
|
|
428
|
+
uint16_t r = rng;
|
|
429
|
+
r ^= r << 7;
|
|
430
|
+
r ^= r >> 9;
|
|
431
|
+
r ^= r << 8;
|
|
432
|
+
rng = r;
|
|
433
|
+
return (uint8_t)r;
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/* ── GAME LOGIC (clay — reshape freely) ── game state.
|
|
437
|
+
* Boards are PLAIN STATIC ARRAYS — the C64 has 38 KB of BASIC RAM free, so
|
|
438
|
+
* none of the NES version's absolute-address scratch-page gymnastics. The
|
|
439
|
+
* hot ones are file-scope NON-static so they land in the cc65 link map
|
|
440
|
+
* (build symbols) — a headless agent can resolve them by name and read/poke
|
|
441
|
+
* live state. */
|
|
442
|
+
uint8_t grid[2][GRID_H][GRID_W]; /* the two wells (P2's unused in 1P) */
|
|
443
|
+
int8_t piece_x[2]; /* falling trio: column 0..5 */
|
|
444
|
+
int8_t piece_y[2]; /* row of its TOP cell (<0 above rim) */
|
|
445
|
+
uint8_t piece_col[2][3]; /* trio colours, top to bottom */
|
|
446
|
+
uint16_t score[2];
|
|
447
|
+
uint16_t hiscore;
|
|
448
|
+
uint8_t level; /* 1P: 1..9, speeds up the fall */
|
|
449
|
+
uint8_t state; /* ST_TITLE / ST_PLAY / ST_OVER */
|
|
450
|
+
uint8_t two_player;
|
|
451
|
+
|
|
452
|
+
static uint8_t matched[GRID_H][GRID_W];
|
|
453
|
+
static uint8_t well_x[2]; /* left interior char column per well */
|
|
454
|
+
static uint8_t fall_t[2]; /* frames until next gravity step */
|
|
455
|
+
static uint8_t prev0, prev1; /* edge-triggered input per port */
|
|
456
|
+
static uint16_t cleared_total; /* 1P: cells cleared, drives the level */
|
|
457
|
+
|
|
458
|
+
#define ST_TITLE 0
|
|
459
|
+
#define ST_PLAY 1
|
|
460
|
+
#define ST_OVER 2
|
|
71
461
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
462
|
+
#define VS_FALL_DELAY 26 /* 2P: fixed gravity (frames per row) */
|
|
463
|
+
#define GARBAGE_CAP 4 /* max garbage rows per attack */
|
|
464
|
+
|
|
465
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
466
|
+
* THE CELL-DIFF REPAINT — the C64's "queued VRAM" equivalent, inverted.
|
|
467
|
+
* Screen RAM is plain memory, so there's no vblank queue to budget against
|
|
468
|
+
* (the NES version's whole drain_vram_budget machinery is moot). The C64's
|
|
469
|
+
* scarcity is CPU: a naive "repaint the whole 6x12 well every frame" is 72
|
|
470
|
+
* cells × (a colour write + a char write) of cc65 C, and a WHOLE-SCREEN
|
|
471
|
+
* 880-cell repaint costs ~50 frames — a frozen second (the TALUS TROT
|
|
472
|
+
* platformer hit exactly this; see its paint_level note).
|
|
473
|
+
*
|
|
474
|
+
* So we keep a SHADOW of what's on screen and repaint ONLY cells that
|
|
475
|
+
* changed. set_cell() compares against shadow[] and writes screen+color RAM
|
|
476
|
+
* only on a difference — most frames touch 0-3 cells (the trio that moved).
|
|
477
|
+
* A full cascade dirties the whole well, but spread across the cells that
|
|
478
|
+
* actually changed it's still a few dozen writes, not 880. THE RULE: never
|
|
479
|
+
* blit the board wholesale during play; always go through set_cell so the
|
|
480
|
+
* diff does the work. (Static screens — title, game-over — CAN repaint
|
|
481
|
+
* freely; they're not in the per-frame path.) */
|
|
482
|
+
static uint8_t shadow[25][40]; /* mirror of screen RAM char codes */
|
|
483
|
+
static uint8_t shadow_c[25][40]; /* mirror of color RAM (so a colour-only
|
|
484
|
+
* change still repaints — empty cells share
|
|
485
|
+
* CH_DOT with the backdrop but want their
|
|
486
|
+
* own well colour) */
|
|
487
|
+
|
|
488
|
+
static void set_cell(uint8_t row, uint8_t col, uint8_t ch, uint8_t color) {
|
|
489
|
+
if (shadow[row][col] == ch && shadow_c[row][col] == color) return; /* unchanged */
|
|
490
|
+
shadow[row][col] = ch;
|
|
491
|
+
shadow_c[row][col] = color;
|
|
492
|
+
{
|
|
493
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
494
|
+
SCREEN[off] = ch;
|
|
495
|
+
COLORS[off] = color;
|
|
496
|
+
}
|
|
75
497
|
}
|
|
76
498
|
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
} else {
|
|
87
|
-
SCREEN[sy * 40 + sx] = CELL_CHAR;
|
|
88
|
-
COLORS[sy * 40 + sx] = col_chr; /* col is the C64 colour id */
|
|
499
|
+
/* Paint ONE board cell at (grid r,c) for player p, honoring the falling trio
|
|
500
|
+
* overlaid on top of the locked grid. Empty = faint speck; filled = tinted
|
|
501
|
+
* solid block. */
|
|
502
|
+
static void draw_board_cell(uint8_t p, uint8_t r, uint8_t c) {
|
|
503
|
+
uint8_t v = grid[p][r][c];
|
|
504
|
+
/* Is the falling trio occupying this cell? (only for the active well) */
|
|
505
|
+
if ((p == 0 || two_player) && piece_x[p] == (int8_t)c) {
|
|
506
|
+
int8_t rel = (int8_t)((int8_t)r - piece_y[p]);
|
|
507
|
+
if (rel >= 0 && rel < 3) v = piece_col[p][rel];
|
|
89
508
|
}
|
|
509
|
+
if (v) set_cell((uint8_t)(WELL_TOP + r), (uint8_t)(well_x[p] + c),
|
|
510
|
+
CH_BLOCK, cell_color[v]);
|
|
511
|
+
else set_cell((uint8_t)(WELL_TOP + r), (uint8_t)(well_x[p] + c),
|
|
512
|
+
CH_DOT, cell_color[0]);
|
|
90
513
|
}
|
|
91
514
|
|
|
92
|
-
|
|
515
|
+
/* Repaint a whole well through the cell-diff (used on board changes — the
|
|
516
|
+
* diff means only the cells that really moved cost anything). */
|
|
517
|
+
static void draw_well(uint8_t p) {
|
|
93
518
|
uint8_t r, c;
|
|
94
|
-
for (r = 0; r <
|
|
95
|
-
for (c = 0; c <
|
|
519
|
+
for (r = 0; r < GRID_H; r++)
|
|
520
|
+
for (c = 0; c < GRID_W; c++) draw_board_cell(p, r, c);
|
|
96
521
|
}
|
|
97
522
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
for (
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
523
|
+
/* ── GAME LOGIC (clay) — the HUD bar (rows 0-1, the fixed split) ── */
|
|
524
|
+
static void draw_bar_labels(void) {
|
|
525
|
+
uint8_t c;
|
|
526
|
+
for (c = 0; c < 40; c++) { /* row 1: solid divider line */
|
|
527
|
+
SCREEN[40 + c] = CH_BLOCK;
|
|
528
|
+
COLORS[40 + c] = COLOR_DARK_GRAY;
|
|
529
|
+
SCREEN[80 + c] = CH_BLANK; /* row 2: the blank spacer the
|
|
530
|
+
* raster split hides in */
|
|
531
|
+
SCREEN[c] = CH_BLANK;
|
|
532
|
+
}
|
|
533
|
+
draw_text(0, 1, "SC");
|
|
534
|
+
draw_text(0, 12, "HI");
|
|
535
|
+
if (two_player) draw_text(0, 30, "P2");
|
|
536
|
+
else draw_text(0, 30, "LV");
|
|
537
|
+
}
|
|
538
|
+
static void draw_bar_stats(void) {
|
|
539
|
+
draw_u16(0, 4, score[0]);
|
|
540
|
+
draw_u16(0, 15, hiscore);
|
|
541
|
+
if (two_player) draw_u16(0, 33, score[1]);
|
|
542
|
+
else {
|
|
543
|
+
SCREEN[33] = (uint8_t)('0' + level);
|
|
544
|
+
COLORS[33] = COLOR_WHITE;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/* ── GAME LOGIC (clay) — paint the well frame (one cell outside the interior).
|
|
549
|
+
* Runs on state changes only (a static screen), so it may write directly. ── */
|
|
550
|
+
static void paint_frame(uint8_t p) {
|
|
551
|
+
uint8_t r, c, x0 = well_x[p];
|
|
552
|
+
for (c = (uint8_t)(x0 - 1); c <= (uint8_t)(x0 + GRID_W); c++) {
|
|
553
|
+
set_cell(WELL_TOP - 1, c, CH_FRAME, COLOR_BROWN);
|
|
554
|
+
set_cell((uint8_t)(WELL_TOP + GRID_H), c, CH_FRAME, COLOR_BROWN);
|
|
555
|
+
}
|
|
556
|
+
for (r = (uint8_t)(WELL_TOP - 1); r <= (uint8_t)(WELL_TOP + GRID_H); r++) {
|
|
557
|
+
set_cell(r, (uint8_t)(x0 - 1), CH_FRAME, COLOR_BROWN);
|
|
558
|
+
set_cell(r, (uint8_t)(x0 + GRID_W), CH_FRAME, COLOR_BROWN);
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/* Clear the whole 25-row screen to blanks (and sync the shadow). Used on
|
|
563
|
+
* state changes — a static-screen operation, cheap enough once. */
|
|
564
|
+
static void clear_screen(void) {
|
|
565
|
+
uint16_t i;
|
|
566
|
+
for (i = 0; i < 1000; i++) { SCREEN[i] = CH_BLANK; COLORS[i] = COLOR_BLACK; }
|
|
567
|
+
for (i = 0; i < 25 * 40; i++) { ((uint8_t*)shadow)[i] = CH_BLANK; ((uint8_t*)shadow_c)[i] = COLOR_BLACK; }
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/* ── GAME LOGIC (clay) — a thin ember speck band along the screen edges for
|
|
571
|
+
* the static screens (title / game-over). Just the top + bottom playfield
|
|
572
|
+
* rows get a sparse colour texture — enough to read as "alive" without the
|
|
573
|
+
* ~3-second full-screen 880-cell repaint freeze (the C64's documented
|
|
574
|
+
* full-repaint footgun; see the cell-diff idiom). The coloured BORDER (set
|
|
575
|
+
* once in main) does the heavy lifting for screen liveliness; this is garnish.
|
|
576
|
+
* Static-screen only — never per frame. ── */
|
|
577
|
+
static void paint_edge_band(void) {
|
|
578
|
+
uint8_t c;
|
|
579
|
+
volatile uint8_t *top = SCREEN + 3 * 40, *bot = SCREEN + 24 * 40;
|
|
580
|
+
volatile uint8_t *tcol = COLORS + 3 * 40, *bcol = COLORS + 24 * 40;
|
|
581
|
+
for (c = 0; c < 40; c++) {
|
|
582
|
+
uint8_t lit = (uint8_t)((c & 1) == 0);
|
|
583
|
+
top[c] = bot[c] = lit ? CH_DOT : CH_BLANK;
|
|
584
|
+
tcol[c] = bcol[c] = lit ? COLOR_BROWN : COLOR_DARK_GRAY;
|
|
585
|
+
shadow[3][c] = shadow[24][c] = top[c];
|
|
586
|
+
shadow_c[3][c] = shadow_c[24][c] = tcol[c];
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
/* ── GAME LOGIC (clay) — the title screen (static, free to repaint) ── */
|
|
591
|
+
static void paint_title(void) {
|
|
592
|
+
clear_screen();
|
|
593
|
+
paint_edge_band();
|
|
594
|
+
two_player = 0;
|
|
595
|
+
draw_bar_labels();
|
|
596
|
+
draw_bar_stats();
|
|
597
|
+
draw_text_band(8, (40 - (sizeof(GAME_TITLE) - 1)) / 2, GAME_TITLE);
|
|
598
|
+
draw_text_band(13, 12, "PORT 2 FIRE - 1P");
|
|
599
|
+
draw_text_band(15, 9, "PORT 1 FIRE - 2P VERSUS");
|
|
600
|
+
draw_text_band(18, 7, "UP ROTATE - FIRE DROP");
|
|
601
|
+
draw_text_band(20, 6, "CHAINS ERUPT ON YOUR RIVAL");
|
|
602
|
+
draw_text_band(23, 16, "HI");
|
|
603
|
+
draw_u16(23, 19, hiscore);
|
|
604
|
+
state = ST_TITLE;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
/* ── GAME LOGIC (clay) — paint the playfield (wells + HUD), static. ── */
|
|
608
|
+
static void paint_play(void) {
|
|
609
|
+
clear_screen();
|
|
610
|
+
paint_edge_band(); /* thin ember edge garnish (static) */
|
|
611
|
+
draw_bar_labels();
|
|
612
|
+
draw_bar_stats();
|
|
613
|
+
paint_frame(0);
|
|
614
|
+
draw_well(0);
|
|
615
|
+
if (two_player) {
|
|
616
|
+
paint_frame(1);
|
|
617
|
+
draw_well(1);
|
|
618
|
+
draw_text(11, 19, "VS");
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/* ── GAME LOGIC (clay) — game-over / results (static screen). ── */
|
|
623
|
+
static void paint_over(uint8_t loser) {
|
|
624
|
+
clear_screen();
|
|
625
|
+
paint_edge_band();
|
|
626
|
+
draw_bar_labels();
|
|
627
|
+
if (two_player)
|
|
628
|
+
draw_text_band(8, 16, loser ? "P1 WINS" : "P2 WINS");
|
|
629
|
+
else
|
|
630
|
+
draw_text_band(8, 15, "GAME OVER");
|
|
631
|
+
draw_text_band(12, 13, "P1");
|
|
632
|
+
draw_u16(12, 17, score[0]);
|
|
633
|
+
if (two_player) {
|
|
634
|
+
draw_text_band(14, 13, "P2");
|
|
635
|
+
draw_u16(14, 17, score[1]);
|
|
636
|
+
}
|
|
637
|
+
draw_text_band(17, 13, "HI");
|
|
638
|
+
draw_u16(17, 17, hiscore);
|
|
639
|
+
draw_text_band(21, 12, "FIRE - TITLE");
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
/* ── GAME LOGIC (clay) — end of game (top-out). `loser` topped out. ── */
|
|
643
|
+
static void game_end(uint8_t loser) {
|
|
644
|
+
uint16_t best = score[0];
|
|
645
|
+
if (two_player && score[1] > best) best = score[1];
|
|
646
|
+
if (best > hiscore) {
|
|
647
|
+
hiscore = best;
|
|
648
|
+
hiscore_save(hiscore); /* the persistence seam — see its block doc */
|
|
649
|
+
}
|
|
650
|
+
sfx_noise(24); /* game-over rumble */
|
|
651
|
+
state = ST_OVER;
|
|
652
|
+
prev0 = prev1 = 0x1F; /* swallow the held FIRE */
|
|
653
|
+
paint_over(loser);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
657
|
+
* Match scan: mark every straight run of 3+ same-coloured blocks in all 4
|
|
658
|
+
* directions (a cell can belong to several runs — the mask de-dupes), and
|
|
659
|
+
* return how many cells matched. Same routine as every other platform's
|
|
660
|
+
* version of this game. */
|
|
661
|
+
static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
662
|
+
|
|
663
|
+
static uint8_t mark_and_count(uint8_t p) {
|
|
664
|
+
uint8_t r, c, d, len, k, cnt, col;
|
|
665
|
+
int8_t dr, dc;
|
|
666
|
+
int sr, sc;
|
|
667
|
+
cnt = 0;
|
|
668
|
+
for (r = 0; r < GRID_H; r++)
|
|
669
|
+
for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
|
|
670
|
+
for (r = 0; r < GRID_H; r++) {
|
|
671
|
+
for (c = 0; c < GRID_W; c++) {
|
|
672
|
+
col = grid[p][r][c];
|
|
673
|
+
if (col == EMPTY) continue;
|
|
674
|
+
for (d = 0; d < 4; d++) {
|
|
675
|
+
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
676
|
+
sr = (int)r - dr; sc = (int)c - dc;
|
|
677
|
+
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
678
|
+
&& grid[p][sr][sc] == col) continue; /* not the run's start */
|
|
679
|
+
len = 1;
|
|
680
|
+
sr = (int)r + dr; sc = (int)c + dc;
|
|
681
|
+
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
682
|
+
&& grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
683
|
+
if (len >= 3) {
|
|
684
|
+
sr = r; sc = c;
|
|
685
|
+
for (k = 0; k < len; k++) {
|
|
686
|
+
if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
|
|
687
|
+
sr += dr; sc += dc;
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return cnt;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* Collapse each column so survivors rest on the floor (walk from the bottom,
|
|
697
|
+
* copying blocks down to a write cursor, then zero everything above it). */
|
|
698
|
+
static void apply_gravity(uint8_t p) {
|
|
699
|
+
uint8_t c;
|
|
700
|
+
int8_t r, w;
|
|
701
|
+
for (c = 0; c < GRID_W; c++) {
|
|
702
|
+
w = GRID_H - 1;
|
|
703
|
+
for (r = GRID_H - 1; r >= 0; r--) {
|
|
704
|
+
if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
|
|
112
705
|
}
|
|
706
|
+
for (; w >= 0; w--) grid[p][w][c] = EMPTY;
|
|
113
707
|
}
|
|
114
708
|
}
|
|
115
709
|
|
|
116
|
-
|
|
117
|
-
|
|
710
|
+
/* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
|
|
711
|
+
* Returns the chain depth (0 = the lock matched nothing). Repaints go
|
|
712
|
+
* through the cell-diff via draw_well. */
|
|
713
|
+
static uint8_t resolve_board(uint8_t p) {
|
|
714
|
+
uint8_t n, r, c, chain;
|
|
715
|
+
uint16_t amt;
|
|
716
|
+
chain = 0;
|
|
717
|
+
for (;;) {
|
|
718
|
+
n = mark_and_count(p);
|
|
719
|
+
if (n == 0) break;
|
|
720
|
+
++chain;
|
|
721
|
+
for (r = 0; r < GRID_H; r++)
|
|
722
|
+
for (c = 0; c < GRID_W; c++)
|
|
723
|
+
if (matched[r][c]) grid[p][r][c] = EMPTY;
|
|
724
|
+
amt = (uint16_t)n * 10;
|
|
725
|
+
if (chain > 1) amt *= chain; /* cascades pay multiplied */
|
|
726
|
+
if (score[p] < 65000) score[p] += amt;
|
|
727
|
+
/* clear chime — pitch rises with chain depth (higher freq_hi byte). */
|
|
728
|
+
sfx_tone(2, 0x00, (uint8_t)(0x30 + (chain << 2)), 8);
|
|
729
|
+
apply_gravity(p);
|
|
730
|
+
draw_well(p);
|
|
731
|
+
if (!two_player) {
|
|
732
|
+
cleared_total += n;
|
|
733
|
+
while (level < 9 && cleared_total >= (uint16_t)level * 10) ++level;
|
|
734
|
+
}
|
|
735
|
+
draw_bar_stats();
|
|
736
|
+
}
|
|
737
|
+
return chain;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
/* ── GAME LOGIC (clay) — VERSUS attack: garbage rows ERUPT up from the bottom
|
|
741
|
+
* of the victim's well (random blocks with one gap — matchable, so a skilled
|
|
742
|
+
* victim digs out). The victim's stack rising means the falling trio shifts
|
|
743
|
+
* up one to stay board-aligned; if the top row is already occupied, the
|
|
744
|
+
* victim tops out and loses. ── */
|
|
745
|
+
static void garbage_insert(uint8_t v, uint8_t nrows) {
|
|
746
|
+
uint8_t k, c, gap;
|
|
118
747
|
int8_t r;
|
|
119
|
-
|
|
748
|
+
sfx_noise(8); /* incoming-garbage thud */
|
|
749
|
+
for (k = 0; k < nrows; k++) {
|
|
750
|
+
for (c = 0; c < GRID_W; c++)
|
|
751
|
+
if (grid[v][0][c] != EMPTY) { game_end(v); return; }
|
|
752
|
+
for (r = 0; r < GRID_H - 1; r++)
|
|
753
|
+
for (c = 0; c < GRID_W; c++)
|
|
754
|
+
grid[v][r][c] = grid[v][r + 1][c];
|
|
755
|
+
gap = random8() % GRID_W;
|
|
756
|
+
for (c = 0; c < GRID_W; c++)
|
|
757
|
+
grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (uint8_t)(1 + random8() % 3);
|
|
758
|
+
if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
|
|
759
|
+
}
|
|
760
|
+
draw_well(v);
|
|
761
|
+
}
|
|
762
|
+
|
|
763
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
|
|
764
|
+
* (pieces enter from above); below the floor or on a block is not. */
|
|
765
|
+
static uint8_t can_place(uint8_t p, int8_t x, int8_t y) {
|
|
766
|
+
int8_t i, cy;
|
|
767
|
+
if (x < 0 || x >= GRID_W) return 0;
|
|
120
768
|
for (i = 0; i < 3; i++) {
|
|
121
|
-
|
|
122
|
-
if (
|
|
123
|
-
if (
|
|
769
|
+
cy = (int8_t)(y + i);
|
|
770
|
+
if (cy < 0) continue;
|
|
771
|
+
if (cy >= GRID_H) return 0;
|
|
772
|
+
if (grid[p][cy][x] != EMPTY) return 0;
|
|
124
773
|
}
|
|
125
|
-
return
|
|
774
|
+
return 1;
|
|
126
775
|
}
|
|
127
776
|
|
|
128
|
-
static void
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
777
|
+
static void spawn_piece(uint8_t p) {
|
|
778
|
+
piece_x[p] = GRID_W / 2;
|
|
779
|
+
piece_y[p] = -2;
|
|
780
|
+
piece_col[p][0] = (uint8_t)(1 + random8() % 3);
|
|
781
|
+
piece_col[p][1] = (uint8_t)(1 + random8() % 3);
|
|
782
|
+
piece_col[p][2] = (uint8_t)(1 + random8() % 3);
|
|
783
|
+
if (!can_place(p, piece_x[p], piece_y[p])) game_end(p);
|
|
134
784
|
}
|
|
135
785
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
int8_t
|
|
139
|
-
uint8_t
|
|
786
|
+
/* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
|
|
787
|
+
static void lock_piece(uint8_t p) {
|
|
788
|
+
int8_t i, y;
|
|
789
|
+
uint8_t chain;
|
|
140
790
|
for (i = 0; i < 3; i++) {
|
|
141
|
-
|
|
142
|
-
if (
|
|
791
|
+
y = (int8_t)(piece_y[p] + i);
|
|
792
|
+
if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
|
|
793
|
+
}
|
|
794
|
+
sfx_tone(2, 0x00, 0x18, 4); /* lock thunk */
|
|
795
|
+
draw_well(p);
|
|
796
|
+
if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
|
|
797
|
+
chain = resolve_board(p);
|
|
798
|
+
if (state != ST_PLAY) return;
|
|
799
|
+
if (chain && two_player) {
|
|
800
|
+
garbage_insert((uint8_t)(p ^ 1), chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
|
|
801
|
+
if (state != ST_PLAY) return; /* garbage topped them out */
|
|
143
802
|
}
|
|
803
|
+
spawn_piece(p);
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
|
|
807
|
+
* (one cell per press), held DOWN soft-drops, UP cycles the trio's colours
|
|
808
|
+
* (the classic trio "rotate"), FIRE hard-drops. P2 reads control PORT 1. ──
|
|
809
|
+
* The board cells the trio used to occupy are repainted via draw_board_cell
|
|
810
|
+
* before/after the move, so the cell-diff erases its trail and stamps its
|
|
811
|
+
* new spot — never a whole-well blit. */
|
|
812
|
+
static void erase_trio(uint8_t p) {
|
|
813
|
+
int8_t i, y;
|
|
144
814
|
for (i = 0; i < 3; i++) {
|
|
145
|
-
|
|
146
|
-
if (
|
|
147
|
-
for (c = 0; c <= COLS - 3; c++) {
|
|
148
|
-
a = grid[r][c]; b = grid[r][c+1]; d = grid[r][c+2];
|
|
149
|
-
if (a != 0 && a == b && b == d) {
|
|
150
|
-
grid[r][c] = 0;
|
|
151
|
-
grid[r][c+1] = 0;
|
|
152
|
-
grid[r][c+2] = 0;
|
|
153
|
-
if (score < 65500u) score += 30;
|
|
154
|
-
sfx_tone(0, 0x80, 0x10, 12);
|
|
155
|
-
}
|
|
156
|
-
}
|
|
815
|
+
y = (int8_t)(piece_y[p] + i);
|
|
816
|
+
if (y >= 0 && y < GRID_H) draw_board_cell(p, (uint8_t)y, (uint8_t)piece_x[p]);
|
|
157
817
|
}
|
|
158
|
-
|
|
818
|
+
}
|
|
819
|
+
|
|
820
|
+
static void stamp_trio(uint8_t p) {
|
|
821
|
+
int8_t i, y;
|
|
822
|
+
for (i = 0; i < 3; i++) {
|
|
823
|
+
y = (int8_t)(piece_y[p] + i);
|
|
824
|
+
if (y >= 0 && y < GRID_H) draw_board_cell(p, (uint8_t)y, (uint8_t)piece_x[p]);
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
static void update_player(uint8_t p, uint8_t pad, uint8_t prev) {
|
|
829
|
+
uint8_t fresh = (uint8_t)(pad & ~prev);
|
|
830
|
+
uint8_t t, fd;
|
|
831
|
+
erase_trio(p); /* lift the trio off the board */
|
|
832
|
+
if ((fresh & JOY_LEFT) && can_place(p, (int8_t)(piece_x[p] - 1), piece_y[p]))
|
|
833
|
+
--piece_x[p];
|
|
834
|
+
if ((fresh & JOY_RIGHT) && can_place(p, (int8_t)(piece_x[p] + 1), piece_y[p]))
|
|
835
|
+
++piece_x[p];
|
|
836
|
+
if (fresh & JOY_UP) { /* cycle colours downward */
|
|
837
|
+
t = piece_col[p][2];
|
|
838
|
+
piece_col[p][2] = piece_col[p][1];
|
|
839
|
+
piece_col[p][1] = piece_col[p][0];
|
|
840
|
+
piece_col[p][0] = t;
|
|
841
|
+
sfx_tone(2, 0x00, 0x28, 3);
|
|
842
|
+
}
|
|
843
|
+
if (fresh & JOY_FIRE) { /* hard drop */
|
|
844
|
+
while (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1))) ++piece_y[p];
|
|
845
|
+
lock_piece(p); /* may end the game */
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
if (pad & JOY_DOWN) fall_t[p] += 4; /* soft drop */
|
|
849
|
+
++fall_t[p];
|
|
850
|
+
fd = two_player ? VS_FALL_DELAY
|
|
851
|
+
: (uint8_t)(34 - ((level << 1) + level)); /* 31..7 */
|
|
852
|
+
if (fall_t[p] >= fd) {
|
|
853
|
+
fall_t[p] = 0;
|
|
854
|
+
if (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1)))
|
|
855
|
+
++piece_y[p];
|
|
856
|
+
else { lock_piece(p); return; } /* may end the game */
|
|
857
|
+
}
|
|
858
|
+
if (state == ST_PLAY) stamp_trio(p); /* re-stamp the trio's new spot */
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
862
|
+
static void start_game(uint8_t versus) {
|
|
863
|
+
uint8_t p, r, c;
|
|
864
|
+
two_player = versus;
|
|
865
|
+
well_x[0] = versus ? WELL_VS_P1 : WELL_1P_X;
|
|
866
|
+
well_x[1] = WELL_VS_P2;
|
|
867
|
+
/* Stir the PRNG with time-spent-on-title so runs differ. */
|
|
868
|
+
rng ^= (uint16_t)frame_count ^ ((uint16_t)frame_count << 7);
|
|
869
|
+
if (rng == 0) rng = 0xACE1;
|
|
870
|
+
for (p = 0; p < 2; p++) {
|
|
871
|
+
for (r = 0; r < GRID_H; r++)
|
|
872
|
+
for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
|
|
873
|
+
fall_t[p] = 0;
|
|
874
|
+
score[p] = 0;
|
|
875
|
+
}
|
|
876
|
+
cleared_total = 0;
|
|
877
|
+
level = 1;
|
|
878
|
+
state = ST_PLAY;
|
|
879
|
+
prev0 = prev1 = 0x1F; /* the button that started the game
|
|
880
|
+
* shouldn't also rotate the first trio */
|
|
881
|
+
paint_play();
|
|
882
|
+
spawn_piece(0);
|
|
883
|
+
if (versus) spawn_piece(1);
|
|
884
|
+
draw_well(0);
|
|
885
|
+
if (versus) draw_well(1);
|
|
886
|
+
sfx_tone(2, 0x00, 0x20, 10); /* start jingle */
|
|
159
887
|
}
|
|
160
888
|
|
|
161
889
|
void main(void) {
|
|
162
|
-
uint8_t
|
|
163
|
-
POKE(VIC_BORDER, 0x06); /* blue border frames the playfield */
|
|
164
|
-
POKE(VIC_BG0, 0x00); /* black well interior so blocks pop */
|
|
890
|
+
uint8_t pad0, pad1;
|
|
165
891
|
|
|
166
|
-
|
|
167
|
-
|
|
892
|
+
/* ── HARDWARE IDIOM (load-bearing) — boot order. VIC + SID config before
|
|
893
|
+
* the IRQ goes live; sfx_init BEFORE music_init (sfx_init writes a plain
|
|
894
|
+
* volume to $D418, music_init re-asserts the filter-mode bits on top). ── */
|
|
895
|
+
POKE(VIC_SPR_ENA, 0); /* no hardware sprites — board is chars */
|
|
896
|
+
/* A coloured BORDER (one register, zero per-frame cost) keeps the screen
|
|
897
|
+
* visibly alive even though the board itself is small over a black BG —
|
|
898
|
+
* the border is ~40% of the framebuffer, so no single colour dominates the
|
|
899
|
+
* render-health pixel scan. (Compare the platformer/shmup, which instead
|
|
900
|
+
* fill the field with a scrolling starfield; a puzzle board doesn't.) */
|
|
901
|
+
POKE(VIC_BORDER, COLOR_BROWN);
|
|
902
|
+
POKE(VIC_BG0, COLOR_BLACK);
|
|
903
|
+
POKE(VIC_CTRL2, D016_BAR); /* 38-col mode from the start */
|
|
904
|
+
POKE(CIA1_DDRA, 0xFF); /* port A drives keyboard columns */
|
|
905
|
+
POKE(CIA1_DDRB, 0x00); /* port B reads rows / stick 1 */
|
|
168
906
|
|
|
169
|
-
score = 0; fall_timer = 0;
|
|
170
907
|
sfx_init();
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
908
|
+
music_init();
|
|
909
|
+
hiscore = hiscore_load(); /* 0 until the core save round lands */
|
|
910
|
+
|
|
911
|
+
clear_screen();
|
|
912
|
+
install_raster_irq(); /* the split + heartbeat go live */
|
|
913
|
+
paint_title();
|
|
174
914
|
|
|
175
915
|
for (;;) {
|
|
176
|
-
|
|
177
|
-
|
|
916
|
+
wait_frame(); /* the line-251 IRQ paces everything */
|
|
917
|
+
|
|
918
|
+
music_update();
|
|
178
919
|
sfx_update();
|
|
179
|
-
|
|
920
|
+
pad0 = read_stick_port2(); /* P1 — control port 2 (convention) */
|
|
921
|
+
pad1 = read_stick_port1(); /* P2 — control port 1 */
|
|
180
922
|
|
|
181
|
-
if (
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if ((pad & JOY_UP) && !(prev & JOY_UP)) {
|
|
188
|
-
while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
|
|
189
|
-
lock_piece();
|
|
190
|
-
new_piece();
|
|
191
|
-
prev = pad;
|
|
923
|
+
if (state == ST_TITLE) {
|
|
924
|
+
/* Mode select doubles as a controls demo: the stick that presses FIRE
|
|
925
|
+
* picks the mode — port 2 starts 1P, port 1 starts 2P versus. */
|
|
926
|
+
if ((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) start_game(0);
|
|
927
|
+
else if ((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE)) start_game(1);
|
|
928
|
+
prev0 = pad0; prev1 = pad1;
|
|
192
929
|
continue;
|
|
193
930
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
lock_piece();
|
|
201
|
-
new_piece();
|
|
202
|
-
} else {
|
|
203
|
-
piece_y++;
|
|
204
|
-
}
|
|
931
|
+
|
|
932
|
+
if (state == ST_OVER) {
|
|
933
|
+
if (((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) ||
|
|
934
|
+
((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE))) paint_title();
|
|
935
|
+
prev0 = pad0; prev1 = pad1;
|
|
936
|
+
continue;
|
|
205
937
|
}
|
|
206
|
-
|
|
938
|
+
|
|
939
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────────
|
|
940
|
+
* Both players update EVERY frame (simultaneous versus, not alternating
|
|
941
|
+
* turns). Any update can end the game, so re-check state between them. */
|
|
942
|
+
update_player(0, pad0, prev0);
|
|
943
|
+
if (two_player && state == ST_PLAY) update_player(1, pad1, prev1);
|
|
944
|
+
prev0 = pad0; prev1 = pad1;
|
|
207
945
|
}
|
|
208
946
|
}
|