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,7 +1,51 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
1
|
+
/* ── puzzle.c — Atari Lynx falling-trio match-3 (complete example game) ───────
|
|
2
|
+
*
|
|
3
|
+
* A COMPLETE, working game — title screen, score + level, in-session
|
|
4
|
+
* hi-score, MIKEY music + SFX, a 1P marathon falling-trio match-3 with
|
|
5
|
+
* cascade chains and ramping levels, AND the Lynx's signature party trick:
|
|
6
|
+
* HARDWARE SPRITE SCALING. When a run of gems clears, the whole well does a
|
|
7
|
+
* SCALE POP — Suzy redraws every surviving gem at >1.0x then eases back — a
|
|
8
|
+
* pure-hardware "juice" flash that costs zero CPU pixel work.
|
|
9
|
+
*
|
|
10
|
+
* The game: a trio of three coloured gems falls into a 6x12 well. LEFT/RIGHT
|
|
11
|
+
* move it, A/B cycle its three colours, DOWN soft-drops. When it lands, any
|
|
12
|
+
* straight run of 3+ same-coloured gems (horizontal, vertical, or diagonal)
|
|
13
|
+
* clears; survivors fall and cascades chain for multiplied score. Clearing
|
|
14
|
+
* gems raises the level, which speeds the fall. Stack to the rim and it's
|
|
15
|
+
* game over.
|
|
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 Lynx footgun;
|
|
20
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
21
|
+
* GAME LOGIC (clay) — match rules, scoring, tuning, art: reshape freely.
|
|
22
|
+
*
|
|
23
|
+
* What depends on what:
|
|
24
|
+
* lynx_sfx.{h,c} — MIKEY 4-voice audio (voice 0 = move/clear SFX, voice 1 =
|
|
25
|
+
* background melody, voice 2 = lock SFX, voice 3 = noise/game-over).
|
|
26
|
+
* vendor/cc65/libsrc/lynx/ — the FULL cc65 Lynx driver source shipped into
|
|
27
|
+
* your project. The TGI driver (tgi/lynx-160-102-16.s) is REQUIRED
|
|
28
|
+
* reading when graphics misbehave: every TGI call is itself a Suzy
|
|
29
|
+
* sprite, and our scaled gem pop rides the same engine via tgi_ioctl(0).
|
|
30
|
+
*
|
|
31
|
+
* NO HARDWARE TILEMAP (read this — it is the platform's biggest "where's the
|
|
32
|
+
* board renderer?" surprise): the Lynx has NO background tilemap. Suzy is a
|
|
33
|
+
* SPRITE BLITTER, not a tile engine. So the well is drawn the honest way:
|
|
34
|
+
* the full-redraw TGI loop repaints the 6x12 grid every frame as a stack of
|
|
35
|
+
* tgi_bar fills (one filled rect per occupied cell) — cheap because the well
|
|
36
|
+
* is only 48x96 px. The falling trio + the clear-pop gems are Suzy SCALABLE
|
|
37
|
+
* sprites layered on top. See draw_well().
|
|
38
|
+
*
|
|
39
|
+
* PLAYERS: 1. This is a handheld — head-to-head on real hardware is ComLynx,
|
|
40
|
+
* a cable between TWO physical Lynx units. A single emulator instance has
|
|
41
|
+
* nobody on the other end of the cable, so this example is honestly a
|
|
42
|
+
* single-player MARATHON (no fake "P2 VERSUS" that could never work here —
|
|
43
|
+
* contrast the NES puzzle donor, which has a real split-board 2P mode).
|
|
44
|
+
*
|
|
45
|
+
* SCREEN: 160x102. The system font is 8x8, so a full row of text is 20
|
|
46
|
+
* characters — the well + HUD are kept compact to fit: a 48x96 well on the
|
|
47
|
+
* right, a slim HUD column down the left edge.
|
|
48
|
+
*/
|
|
5
49
|
|
|
6
50
|
#include <tgi.h>
|
|
7
51
|
#include <joystick.h>
|
|
@@ -9,167 +53,526 @@
|
|
|
9
53
|
#include <stdint.h>
|
|
10
54
|
#include "lynx_sfx.h"
|
|
11
55
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
#define
|
|
15
|
-
#define GRID_X 56
|
|
16
|
-
#define GRID_Y 4
|
|
56
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
57
|
+
* name here automatically. Keep it <=16 chars of A-Z 0-9 space dash. */
|
|
58
|
+
#define GAME_TITLE "QUARRY QUELL"
|
|
17
59
|
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
60
|
+
/* ── GAME LOGIC (clay — reshape freely) — well geometry (fits 160x102) ──────
|
|
61
|
+
* A 6x12 well of 8x8 cells = 48x96 px. We park it on the right so a slim HUD
|
|
62
|
+
* column lives down the left edge. WELL_PX_Y leaves a margin under the top. */
|
|
63
|
+
#define GRID_W 6
|
|
64
|
+
#define GRID_H 12
|
|
65
|
+
#define CELL 8
|
|
66
|
+
#define WELL_PX_X 92 /* left edge of the well, in pixels */
|
|
67
|
+
#define WELL_PX_Y 4 /* top edge of the well interior */
|
|
68
|
+
#define WELL_W (GRID_W * CELL) /* 48 px */
|
|
69
|
+
#define WELL_H (GRID_H * CELL) /* 96 px */
|
|
24
70
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
71
|
+
#define EMPTY 0 /* cell colours 1..3 = white/green/red */
|
|
72
|
+
|
|
73
|
+
/* ── GAME LOGIC (clay) — gem colour → TGI pen. Three distinct, readable pens
|
|
74
|
+
* (cc65 lynx.h COLOR_* indices); EMPTY cells paint as a dim recessed speck so
|
|
75
|
+
* the well reads as a playfield, not raw black. */
|
|
76
|
+
static const uint8_t gem_pen[4] = {
|
|
77
|
+
COLOR_DARKGREY, /* 0 = EMPTY (recessed cell) */
|
|
78
|
+
COLOR_WHITE, /* 1 = white gem */
|
|
79
|
+
COLOR_LIGHTGREEN, /* 2 = green gem */
|
|
80
|
+
COLOR_RED /* 3 = red gem */
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/* ── GAME LOGIC (clay) — board + small state ── */
|
|
84
|
+
#define ST_TITLE 0
|
|
85
|
+
#define ST_PLAY 1
|
|
86
|
+
#define ST_OVER 2
|
|
87
|
+
static uint8_t state;
|
|
88
|
+
|
|
89
|
+
static uint8_t grid[GRID_H][GRID_W]; /* locked gems, [row][col] */
|
|
90
|
+
static uint8_t matched[GRID_H][GRID_W]; /* scratch mask for the match scan */
|
|
91
|
+
static uint8_t piece_x; /* falling trio: column 0..5 */
|
|
92
|
+
static int8_t piece_y; /* row of its TOP cell (<0 above rim) */
|
|
93
|
+
static uint8_t piece_col[3]; /* trio colours, top to bottom */
|
|
94
|
+
static uint8_t fall_t; /* frames until the next gravity step*/
|
|
95
|
+
static unsigned score;
|
|
96
|
+
static unsigned hiscore; /* in-session only — see EEPROM note */
|
|
97
|
+
static unsigned cleared_total; /* gems cleared — drives the level */
|
|
98
|
+
static uint8_t level; /* 1..9, speeds up the fall */
|
|
99
|
+
static uint8_t prev_joy;
|
|
100
|
+
static uint8_t over_new_hi;
|
|
101
|
+
|
|
102
|
+
/* The clear-pop pulse: when >0 the well draws its gems scaled-up for a few
|
|
103
|
+
* frames (the SCALING signature), counting back down to the resting 1.0x. */
|
|
104
|
+
static uint8_t pop_timer;
|
|
105
|
+
#define POP_FRAMES 7
|
|
106
|
+
|
|
107
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
|
|
108
|
+
static uint16_t rng = 0xACE1;
|
|
109
|
+
static uint8_t rand8(void) {
|
|
110
|
+
uint16_t r = rng;
|
|
111
|
+
r ^= r << 7;
|
|
112
|
+
r ^= r >> 9;
|
|
113
|
+
r ^= r << 8;
|
|
114
|
+
rng = r;
|
|
115
|
+
return (uint8_t)r;
|
|
116
|
+
}
|
|
117
|
+
static uint8_t rand_gem(void) { return (uint8_t)(1 + rand8() % 3); }
|
|
118
|
+
|
|
119
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
120
|
+
* SUZY HARDWARE SPRITE SCALING — the Lynx signature. Suzy renders every
|
|
121
|
+
* sprite through a Sprite Control Block (SCB) it walks in cart/work RAM.
|
|
122
|
+
* Two SCB fields, HSIZE and VSIZE, are 8.8 fixed-point scale factors
|
|
123
|
+
* ($0100 = 1.0): the SAME 8x8 source pixels render at any size, every frame,
|
|
124
|
+
* for free. This game uses it two ways:
|
|
125
|
+
* - the FALLING TRIO gems are Suzy sprites drawn through this SCB at a
|
|
126
|
+
* fixed 1.0x (so forking in a depth/power-up scale is a one-line change);
|
|
127
|
+
* - the CLEAR POP — for POP_FRAMES after any match, every gem in the well
|
|
128
|
+
* is redrawn at >1.0x then eased back to 1.0x, a pure-hardware "juice"
|
|
129
|
+
* flash with zero CPU pixel cost (Suzy scales while it blits).
|
|
130
|
+
*
|
|
131
|
+
* The SCB, field by field (this is cc65's SCB_REHV_PAL from <_suzy.h>):
|
|
132
|
+
* sprctl0 bits 7-6 = bits per pixel (11 = 4bpp), bits 2-0 = sprite TYPE.
|
|
133
|
+
* TYPE_NORMAL (4) draws pens 1-15 and treats pen 0 as
|
|
134
|
+
* TRANSPARENT — that's how a round gem sits over the cell.
|
|
135
|
+
* sprctl1 bit 7 LITERAL (raw nybbles, no RLE) + bits 5-4 reload depth:
|
|
136
|
+
* REHV means "this SCB carries HPOS, VPOS, HSIZE, VSIZE". The
|
|
137
|
+
* reload bits ARE the struct layout — mismatch them and Suzy reads
|
|
138
|
+
* palette bytes as size words.
|
|
139
|
+
* sprcoll $20 = NO_COLLIDE. Match/lock collision is done in C on the grid
|
|
140
|
+
* (the collision buffer knows nothing about board cells).
|
|
141
|
+
* next pointer to the next SCB, 0 = end of chain (one blit per call).
|
|
142
|
+
* data sprite pixel data (LITERAL 4bpp format below).
|
|
143
|
+
* hpos/vpos signed SCREEN position of the sprite's top-left corner.
|
|
144
|
+
* hsize/vsize 8.8 scale — THE party trick, rewritten per draw.
|
|
145
|
+
* penpal[8] 16 nybbles mapping pixel values 0-15 → palette pens. We RECOLOUR
|
|
146
|
+
* the gem per draw here (one 8x8 art block, three gem colours) by
|
|
147
|
+
* pointing the art's pixel value 1 at the wanted pen — no extra art.
|
|
148
|
+
*
|
|
149
|
+
* LITERAL 4bpp data format (hand-encodable): each sprite LINE is
|
|
150
|
+
* [offset byte][width/2 bytes of raw nybble pixels]
|
|
151
|
+
* where offset = 1 + bytes of pixel data; a final offset of 0 ends the sprite.
|
|
152
|
+
* 8 px @ 4bpp = 4 data bytes, so every line starts with 5.
|
|
153
|
+
*
|
|
154
|
+
* Drawing: tgi_sprite(&scb) → tgi_ioctl(0, &scb) — the TGI driver's
|
|
155
|
+
* documented escape hatch (see CONTROL in vendor/cc65/libsrc/lynx/tgi/
|
|
156
|
+
* lynx-160-102-16.s). It points Suzy's SCBNEXT at your SCB, aims VIDBAS at
|
|
157
|
+
* TGI's current DRAW page (so scaled gems land in the same double-buffered
|
|
158
|
+
* frame as tgi_bar/tgi_outtextxy), fires SPRGO, and sleeps the CPU until
|
|
159
|
+
* SPRSYS reports the blit done.
|
|
160
|
+
*
|
|
161
|
+
* Requires: the cc65 crt0 Suzy init (already done before main()), and calls
|
|
162
|
+
* only between the tgi_busy() wait and tgi_updatedisplay() — i.e. while
|
|
163
|
+
* TGI's draw buffer is the blit target. Draw order = paint order: well
|
|
164
|
+
* fills first, scaled gems after, HUD text last.
|
|
165
|
+
*/
|
|
166
|
+
static SCB_REHV_PAL scb = {
|
|
167
|
+
BPP_4 | TYPE_NORMAL, /* sprctl0: 4bpp, pen 0 transparent */
|
|
168
|
+
LITERAL | REHV, /* sprctl1: literal data, HV+size SCB */
|
|
169
|
+
0x20, /* sprcoll: NO_COLLIDE */
|
|
170
|
+
0, /* next: single-SCB chain */
|
|
171
|
+
0, /* data: set per draw */
|
|
172
|
+
0, 0, /* hpos, vpos */
|
|
173
|
+
0x0100, 0x0100, /* hsize, vsize (8.8) */
|
|
174
|
+
{ 0x01, 0x23, 0x45, 0x67, 0x89, 0xAB, 0xCD, 0xEF } /* identity pens */
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
/* ── GAME LOGIC (clay) — 8x8 4bpp literal gem art ───────────────────────────
|
|
178
|
+
* A single round gem shape in pixel value 1 (plus value $F = white glint).
|
|
179
|
+
* draw_gem() recolours value 1 → the wanted gem pen via the SCB penpal, so one
|
|
180
|
+
* art block paints all three gem colours. Each line: 5, then 4 nybble bytes;
|
|
181
|
+
* a final 0 byte ends the sprite. */
|
|
182
|
+
static unsigned char spr_gem[] = {
|
|
183
|
+
5, 0x00, 0x11, 0x10, 0x00, /* . . 1 1 1 . . . round gem body */
|
|
184
|
+
5, 0x01, 0x1F, 0xF1, 0x10, /* . 1 1 F F 1 1 . (white glint) */
|
|
185
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
186
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
187
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
188
|
+
5, 0x11, 0x11, 0x11, 0x11, /* 1 1 1 1 1 1 1 1 */
|
|
189
|
+
5, 0x01, 0x11, 0x11, 0x10, /* . 1 1 1 1 1 1 . */
|
|
190
|
+
5, 0x00, 0x11, 0x10, 0x00, /* . . 1 1 1 . . . */
|
|
191
|
+
0
|
|
192
|
+
};
|
|
193
|
+
|
|
194
|
+
/* Draw the gem sprite for board cell at top-left (x,y), recoloured to `col`
|
|
195
|
+
* (1..3), at an 8.8 scale. Recolour: point art pixel value 1 at the gem's pen
|
|
196
|
+
* by rewriting the first penpal byte (values 0,1 → transparent, pen). The
|
|
197
|
+
* clear pop scales every gem about its CELL CENTRE so the flash reads as a
|
|
198
|
+
* uniform swell, not a slide. */
|
|
199
|
+
static void draw_gem(int x, int y, uint8_t col, unsigned scale) {
|
|
200
|
+
unsigned w = (8u * scale) >> 8;
|
|
201
|
+
if (w == 0) w = 1;
|
|
202
|
+
scb.penpal[0] = (uint8_t)((0u << 4) | gem_pen[col]); /* val0=transparent, val1=pen */
|
|
203
|
+
scb.data = spr_gem;
|
|
204
|
+
scb.hsize = scale;
|
|
205
|
+
scb.vsize = scale;
|
|
206
|
+
scb.hpos = x + 4 - (int)(w >> 1); /* anchor the CELL CENTRE (cells are 8 wide) */
|
|
207
|
+
scb.vpos = y + 4 - (int)(w >> 1);
|
|
208
|
+
tgi_sprite(&scb);
|
|
28
209
|
}
|
|
29
210
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
211
|
+
/* Current clear-pop scale: 1.0x at rest, swelling to ~1.5x at the pop peak and
|
|
212
|
+
* easing back. POP drives the SCALING idiom on every clear. */
|
|
213
|
+
#define POP_SCALE_PEAK 0x0180u /* 1.5x */
|
|
214
|
+
static unsigned pop_scale(void) {
|
|
215
|
+
if (pop_timer == 0) return 0x0100u;
|
|
216
|
+
/* linear ease: scale = 1.0 + (pop_timer/POP_FRAMES) * 0.5 */
|
|
217
|
+
return 0x0100u + ((unsigned)pop_timer * (POP_SCALE_PEAK - 0x0100u)) / POP_FRAMES;
|
|
36
218
|
}
|
|
37
219
|
|
|
38
|
-
|
|
220
|
+
/* ── GAME LOGIC (clay) — score text (no sprintf: it drags in ~6KB) ── */
|
|
221
|
+
static char numbuf[6];
|
|
222
|
+
static char *fmt5(unsigned v) {
|
|
39
223
|
uint8_t i;
|
|
40
|
-
|
|
41
|
-
|
|
224
|
+
for (i = 0; i < 5; i++) { numbuf[4 - i] = (char)('0' + v % 10); v /= 10; }
|
|
225
|
+
numbuf[5] = 0;
|
|
226
|
+
return numbuf;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/* ── GAME LOGIC (clay) — match scan: mark every straight run of 3+ same-
|
|
230
|
+
* coloured gems in all 4 directions (a cell can belong to several runs — the
|
|
231
|
+
* mask de-dupes), and return how many cells matched. ── */
|
|
232
|
+
static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
|
|
233
|
+
|
|
234
|
+
static uint8_t mark_and_count(void) {
|
|
235
|
+
uint8_t r, c, d, len, k, cnt, col;
|
|
236
|
+
int8_t dr, dc;
|
|
237
|
+
int sr, sc;
|
|
238
|
+
cnt = 0;
|
|
239
|
+
for (r = 0; r < GRID_H; r++)
|
|
240
|
+
for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
|
|
241
|
+
for (r = 0; r < GRID_H; r++) {
|
|
242
|
+
for (c = 0; c < GRID_W; c++) {
|
|
243
|
+
col = grid[r][c];
|
|
244
|
+
if (col == EMPTY) continue;
|
|
245
|
+
for (d = 0; d < 4; d++) {
|
|
246
|
+
dr = DIRS4[d][0]; dc = DIRS4[d][1];
|
|
247
|
+
sr = (int)r - dr; sc = (int)c - dc;
|
|
248
|
+
if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
249
|
+
&& grid[sr][sc] == col) continue; /* not the run's start */
|
|
250
|
+
len = 1;
|
|
251
|
+
sr = (int)r + dr; sc = (int)c + dc;
|
|
252
|
+
while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
|
|
253
|
+
&& grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
|
|
254
|
+
if (len >= 3) {
|
|
255
|
+
sr = r; sc = c;
|
|
256
|
+
for (k = 0; k < len; k++) {
|
|
257
|
+
if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
|
|
258
|
+
sr += dr; sc += dc;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
return cnt;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/* Collapse each column so survivors rest on the floor. */
|
|
268
|
+
static void apply_gravity(void) {
|
|
269
|
+
uint8_t c;
|
|
270
|
+
int8_t r, w;
|
|
271
|
+
for (c = 0; c < GRID_W; c++) {
|
|
272
|
+
w = GRID_H - 1;
|
|
273
|
+
for (r = GRID_H - 1; r >= 0; r--)
|
|
274
|
+
if (grid[r][c] != EMPTY) { grid[w][c] = grid[r][c]; w--; }
|
|
275
|
+
for (; w >= 0; w--) grid[w][c] = EMPTY;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
static void game_over(void) {
|
|
280
|
+
over_new_hi = 0;
|
|
281
|
+
if (score > hiscore) {
|
|
282
|
+
/* ── In-session hi-score ONLY — and here's the honest why. Real Lynx
|
|
283
|
+
* carts persist via a 93Cxx serial EEPROM on the cart PCB (cc65 even
|
|
284
|
+
* ships lynx_eeprom_read/write for it; see vendor/cc65/libsrc/lynx/
|
|
285
|
+
* eeprom.s). PROBED: the bundled handy core emulates CEEPROM internally
|
|
286
|
+
* but its libretro build exposes NO save path — retro_get_memory(
|
|
287
|
+
* SAVE_RAM) returns NULL/size 0, so nothing survives host.hardReset()
|
|
288
|
+
* and a bit-banged round-trip reads back garbage under the WASM build.
|
|
289
|
+
* Wiring the EEPROM to SAVE_RAM is a future core round; until then a fake
|
|
290
|
+
* "save" would be lying. The hi-score DOES survive title↔play cycles
|
|
291
|
+
* within one power-on. ── */
|
|
292
|
+
hiscore = score;
|
|
293
|
+
over_new_hi = 1;
|
|
294
|
+
}
|
|
295
|
+
sfx_tone(2, 240, 24); /* voice 2: low game-over drone */
|
|
296
|
+
sfx_noise(16); /* voice 3: crunch */
|
|
297
|
+
state = ST_OVER;
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
|
|
301
|
+
* Returns the chain depth (0 = the lock matched nothing). Score, level, and
|
|
302
|
+
* the clear-pop fire here. ── */
|
|
303
|
+
static uint8_t resolve_board(void) {
|
|
304
|
+
uint8_t n, r, c, chain;
|
|
305
|
+
unsigned amt;
|
|
306
|
+
chain = 0;
|
|
307
|
+
for (;;) {
|
|
308
|
+
n = mark_and_count();
|
|
309
|
+
if (n == 0) break;
|
|
310
|
+
++chain;
|
|
311
|
+
for (r = 0; r < GRID_H; r++)
|
|
312
|
+
for (c = 0; c < GRID_W; c++)
|
|
313
|
+
if (matched[r][c]) grid[r][c] = EMPTY;
|
|
314
|
+
amt = (unsigned)n * 10;
|
|
315
|
+
if (chain > 1) amt *= chain; /* cascades pay multiplied */
|
|
316
|
+
score += amt;
|
|
317
|
+
/* clear chime rises with chain depth; the SCALING clear-pop fires */
|
|
318
|
+
sfx_tone(0, (uint8_t)(70 + chain * 8), 8);
|
|
319
|
+
pop_timer = POP_FRAMES; /* trigger the hardware scale pop */
|
|
320
|
+
apply_gravity();
|
|
321
|
+
cleared_total += n;
|
|
322
|
+
while (level < 9 && cleared_total >= (unsigned)level * 10) {
|
|
323
|
+
++level;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return chain;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
|
|
330
|
+
* (pieces enter from above); below the floor or on a gem is not. */
|
|
331
|
+
static uint8_t can_place(int8_t x, int8_t y) {
|
|
332
|
+
int8_t i, cy;
|
|
333
|
+
if (x < 0 || x >= GRID_W) return 0;
|
|
42
334
|
for (i = 0; i < 3; i++) {
|
|
43
|
-
|
|
44
|
-
if (
|
|
45
|
-
if (
|
|
335
|
+
cy = (int8_t)(y + i);
|
|
336
|
+
if (cy < 0) continue;
|
|
337
|
+
if (cy >= GRID_H) return 0;
|
|
338
|
+
if (grid[cy][x] != EMPTY) return 0;
|
|
46
339
|
}
|
|
47
|
-
return
|
|
340
|
+
return 1;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
static void spawn_piece(void) {
|
|
344
|
+
piece_x = GRID_W / 2;
|
|
345
|
+
piece_y = -2;
|
|
346
|
+
piece_col[0] = rand_gem();
|
|
347
|
+
piece_col[1] = rand_gem();
|
|
348
|
+
piece_col[2] = rand_gem();
|
|
349
|
+
if (!can_place((int8_t)piece_x, piece_y)) game_over();
|
|
48
350
|
}
|
|
49
351
|
|
|
352
|
+
/* ── GAME LOGIC (clay) — land the trio, resolve, respawn. ── */
|
|
50
353
|
static void lock_piece(void) {
|
|
51
|
-
|
|
52
|
-
int8_t r;
|
|
53
|
-
uint8_t a, b, d;
|
|
354
|
+
int8_t i, y;
|
|
54
355
|
for (i = 0; i < 3; i++) {
|
|
55
|
-
|
|
56
|
-
if (
|
|
356
|
+
y = (int8_t)(piece_y + i);
|
|
357
|
+
if (y >= 0) grid[y][piece_x] = piece_col[i];
|
|
57
358
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
359
|
+
sfx_tone(2, 180, 4); /* voice 2: lock thunk */
|
|
360
|
+
if (piece_y < 0) { game_over(); return; } /* locked above the rim */
|
|
361
|
+
resolve_board();
|
|
362
|
+
if (state != ST_PLAY) return;
|
|
363
|
+
spawn_piece();
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
/* ── GAME LOGIC (clay) — start a run ── */
|
|
367
|
+
static void start_game(void) {
|
|
368
|
+
uint8_t r, c;
|
|
369
|
+
for (r = 0; r < GRID_H; r++)
|
|
370
|
+
for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
|
|
371
|
+
score = 0;
|
|
372
|
+
cleared_total = 0;
|
|
373
|
+
level = 1;
|
|
374
|
+
fall_t = 0;
|
|
375
|
+
pop_timer = 0;
|
|
376
|
+
prev_joy = 0xFF; /* the button that started the run
|
|
377
|
+
* shouldn't also rotate the first trio */
|
|
378
|
+
sfx_tone(0, 80, 8); /* start chirp */
|
|
379
|
+
state = ST_PLAY;
|
|
380
|
+
spawn_piece();
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/* ── GAME LOGIC (clay) — per-state frames. Each runs INSIDE the canonical
|
|
384
|
+
* loop below: scene already painted, tgi_updatedisplay not yet called. ── */
|
|
385
|
+
|
|
386
|
+
/* draw the locked well: frame + recessed backdrop, every occupied cell as a
|
|
387
|
+
* gem, empties as a faint speck. During a clear pop (`scaled`), occupied gems
|
|
388
|
+
* draw as SCALED Suzy sprites (the hardware flash); otherwise as flat bars
|
|
389
|
+
* (cheaper, and the gem read is identical at 1.0x). */
|
|
390
|
+
static void draw_well(uint8_t scaled) {
|
|
391
|
+
uint8_t r, c, v;
|
|
392
|
+
unsigned ps = pop_scale();
|
|
393
|
+
int px, py;
|
|
394
|
+
/* well frame + recessed backdrop so it reads as a playfield */
|
|
395
|
+
tgi_setcolor(COLOR_GREY);
|
|
396
|
+
tgi_bar(WELL_PX_X - 2, WELL_PX_Y - 2, WELL_PX_X + WELL_W + 1, WELL_PX_Y + WELL_H + 1);
|
|
397
|
+
tgi_setcolor(COLOR_BLACK);
|
|
398
|
+
tgi_bar(WELL_PX_X, WELL_PX_Y, WELL_PX_X + WELL_W - 1, WELL_PX_Y + WELL_H - 1);
|
|
399
|
+
/* empty-cell specks (always flat) */
|
|
400
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
401
|
+
for (r = 0; r < GRID_H; r++)
|
|
402
|
+
for (c = 0; c < GRID_W; c++)
|
|
403
|
+
if (grid[r][c] == EMPTY) {
|
|
404
|
+
px = WELL_PX_X + c * CELL; py = WELL_PX_Y + r * CELL;
|
|
405
|
+
tgi_bar(px + 3, py + 3, px + 4, py + 4);
|
|
406
|
+
}
|
|
407
|
+
/* gems */
|
|
408
|
+
for (r = 0; r < GRID_H; r++)
|
|
409
|
+
for (c = 0; c < GRID_W; c++) {
|
|
410
|
+
v = grid[r][c];
|
|
411
|
+
if (v == EMPTY) continue;
|
|
412
|
+
if (scaled) {
|
|
413
|
+
draw_gem(WELL_PX_X + c * CELL, WELL_PX_Y + r * CELL, v, ps);
|
|
414
|
+
} else {
|
|
415
|
+
px = WELL_PX_X + c * CELL; py = WELL_PX_Y + r * CELL;
|
|
416
|
+
tgi_setcolor(gem_pen[v]);
|
|
417
|
+
tgi_bar(px + 1, py + 1, px + 6, py + 6);
|
|
67
418
|
}
|
|
68
419
|
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
static unsigned attract_phase;
|
|
423
|
+
|
|
424
|
+
static void frame_title(uint8_t joy) {
|
|
425
|
+
/* attract: a lone gem in the title's clear zone pulses via the SCALING
|
|
426
|
+
* idiom — the same swell the clear-pop uses, shown off on the menu. */
|
|
427
|
+
unsigned t = attract_phase < 64 ? attract_phase : (127 - attract_phase);
|
|
428
|
+
unsigned s = 0x00C0u + (t * (0x0200u - 0x00C0u)) / 63u; /* 0.75x..2.0x */
|
|
429
|
+
attract_phase = (attract_phase + 2) & 127;
|
|
430
|
+
|
|
431
|
+
draw_gem(120, 10, 2, s); /* breathing green gem, top-right zone */
|
|
432
|
+
|
|
433
|
+
tgi_setcolor(COLOR_WHITE);
|
|
434
|
+
tgi_outtextxy(8, 24, GAME_TITLE);
|
|
435
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
436
|
+
tgi_outtextxy(28, 44, "PRESS A");
|
|
437
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
438
|
+
tgi_outtextxy(8, 60, "HI ");
|
|
439
|
+
tgi_outtextxy(32, 60, fmt5(hiscore));
|
|
440
|
+
tgi_outtextxy(4, 76, "1 PLAYER MARATHON"); /* handheld honesty */
|
|
441
|
+
|
|
442
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) start_game();
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
static void frame_over(uint8_t joy) {
|
|
446
|
+
draw_well(0);
|
|
447
|
+
tgi_setcolor(COLOR_DARKGREY);
|
|
448
|
+
tgi_bar(6, 30, 86, 74);
|
|
449
|
+
tgi_setcolor(COLOR_WHITE);
|
|
450
|
+
tgi_outtextxy(12, 34, "GAME OVER");
|
|
451
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
452
|
+
tgi_outtextxy(10, 46, "SCORE");
|
|
453
|
+
tgi_outtextxy(10, 56, fmt5(score));
|
|
454
|
+
if (over_new_hi) { tgi_setcolor(COLOR_LIGHTGREEN); tgi_outtextxy(8, 66, "NEW HI"); }
|
|
455
|
+
else { tgi_setcolor(COLOR_LIGHTGREY); tgi_outtextxy(8, 66, "A TITLE"); }
|
|
456
|
+
if (JOY_BTN_1(joy) && !JOY_BTN_1(prev_joy)) state = ST_TITLE;
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
/* stage the falling trio (3 gems) above the locked stack, each at 1.0x */
|
|
460
|
+
static void draw_piece(void) {
|
|
461
|
+
uint8_t i;
|
|
462
|
+
int8_t y;
|
|
463
|
+
for (i = 0; i < 3; i++) {
|
|
464
|
+
y = (int8_t)(piece_y + i);
|
|
465
|
+
if (y >= 0)
|
|
466
|
+
draw_gem(WELL_PX_X + piece_x * CELL, WELL_PX_Y + (int)y * CELL,
|
|
467
|
+
piece_col[i], 0x0100);
|
|
69
468
|
}
|
|
70
469
|
}
|
|
71
470
|
|
|
72
|
-
static
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
471
|
+
static void frame_play(uint8_t joy) {
|
|
472
|
+
uint8_t newp, fd, t;
|
|
473
|
+
|
|
474
|
+
/* ── draw: well (scaled gems while the clear-pop runs), falling trio, HUD ── */
|
|
475
|
+
draw_well(pop_timer != 0);
|
|
476
|
+
draw_piece();
|
|
477
|
+
|
|
478
|
+
tgi_setcolor(COLOR_WHITE);
|
|
479
|
+
tgi_outtextxy(4, 2, "SC");
|
|
480
|
+
tgi_outtextxy(4, 12, fmt5(score));
|
|
481
|
+
tgi_setcolor(COLOR_LIGHTGREY);
|
|
482
|
+
tgi_outtextxy(4, 28, "HI");
|
|
483
|
+
tgi_outtextxy(4, 38, fmt5(hiscore));
|
|
484
|
+
tgi_setcolor(COLOR_YELLOW);
|
|
485
|
+
tgi_outtextxy(4, 54, "LV");
|
|
486
|
+
numbuf[0] = (char)('0' + level); numbuf[1] = 0;
|
|
487
|
+
tgi_outtextxy(28, 54, numbuf);
|
|
488
|
+
|
|
489
|
+
/* ── update: edge-triggered moves; A/B cycle the trio; held DOWN soft-drops.
|
|
490
|
+
* JOY_BTN_1/2(newp) test the press-EDGE mask, so one cell/cycle per press. ── */
|
|
491
|
+
newp = (uint8_t)(joy & (uint8_t)~prev_joy);
|
|
492
|
+
if ((newp & JOY_LEFT_MASK) && can_place((int8_t)(piece_x - 1), piece_y)) --piece_x;
|
|
493
|
+
if ((newp & JOY_RIGHT_MASK) && can_place((int8_t)(piece_x + 1), piece_y)) ++piece_x;
|
|
494
|
+
if (JOY_BTN_1(newp)) { /* A: cycle colours downward */
|
|
495
|
+
t = piece_col[2];
|
|
496
|
+
piece_col[2] = piece_col[1];
|
|
497
|
+
piece_col[1] = piece_col[0];
|
|
498
|
+
piece_col[0] = t;
|
|
499
|
+
sfx_tone(0, 110, 3);
|
|
500
|
+
}
|
|
501
|
+
if (JOY_BTN_2(newp)) { /* B: cycle colours upward */
|
|
502
|
+
t = piece_col[0];
|
|
503
|
+
piece_col[0] = piece_col[1];
|
|
504
|
+
piece_col[1] = piece_col[2];
|
|
505
|
+
piece_col[2] = t;
|
|
506
|
+
sfx_tone(0, 120, 3);
|
|
507
|
+
}
|
|
508
|
+
if (joy & JOY_DOWN_MASK) fall_t += 4; /* soft drop */
|
|
509
|
+
|
|
510
|
+
if (pop_timer) pop_timer--; /* ease the clear-pop back to 1.0x */
|
|
511
|
+
|
|
512
|
+
/* gravity: faster as the level climbs (29..5 frames per row) */
|
|
513
|
+
++fall_t;
|
|
514
|
+
fd = (uint8_t)(32 - ((level << 1) + level)); /* 32 - 3*level → 29..5 */
|
|
515
|
+
if (fall_t >= fd) {
|
|
516
|
+
fall_t = 0;
|
|
517
|
+
if (can_place((int8_t)piece_x, (int8_t)(piece_y + 1)))
|
|
518
|
+
++piece_y;
|
|
519
|
+
else
|
|
520
|
+
lock_piece(); /* may end the game */
|
|
78
521
|
}
|
|
79
522
|
}
|
|
80
523
|
|
|
81
524
|
void main(void) {
|
|
82
|
-
uint8_t joy
|
|
83
|
-
uint8_t r, c, i;
|
|
84
|
-
int8_t pr;
|
|
525
|
+
uint8_t joy;
|
|
85
526
|
|
|
86
527
|
tgi_install(&lynx_160_102_16_tgi);
|
|
87
528
|
tgi_init();
|
|
88
529
|
joy_install(&lynx_stdjoy_joy);
|
|
89
|
-
sfx_init();
|
|
530
|
+
sfx_init(); /* MIKEY up; background melody starts on voice 1 */
|
|
90
531
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
532
|
+
state = ST_TITLE;
|
|
533
|
+
prev_joy = 0;
|
|
534
|
+
attract_phase = 0;
|
|
535
|
+
hiscore = 0;
|
|
94
536
|
|
|
95
537
|
for (;;) {
|
|
96
|
-
/*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
538
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
539
|
+
* CANONICAL LYNX GAME LOOP — full-redraw every frame, in this order:
|
|
540
|
+
* 1. while (tgi_busy()) { } — WAIT for the previous frame's page flip.
|
|
541
|
+
* Skipping this is the #1 "Lynx screen stays blank" trap: drawing
|
|
542
|
+
* while the swap is pending loses the frame.
|
|
543
|
+
* 2. Repaint the WHOLE scene with tgi_bar fills — NOT tgi_clear()
|
|
544
|
+
* (which can leave the framebuffer stale on this toolchain+emulator
|
|
545
|
+
* path). TGI double-buffers; the back buffer holds the frame from
|
|
546
|
+
* two flips ago, so partial redraws ghost. With no hardware tilemap
|
|
547
|
+
* (header), the WELL is repainted cell-by-cell every frame.
|
|
548
|
+
* 3. Draw every object (every TGI call and every tgi_sprite() is a
|
|
549
|
+
* synchronous Suzy blit into the SAME draw page).
|
|
550
|
+
* 4. tgi_updatedisplay() — request the page flip at next VBL.
|
|
551
|
+
* 5. sfx_update() IMMEDIATELY after — MIKEY voice writes must land in
|
|
552
|
+
* vblank: handy reschedules its timer sweep on the spot when a voice
|
|
553
|
+
* CTL bit-3 write lands, and mid-frame that sweep can preempt an
|
|
554
|
+
* in-flight Suzy blit and eat sprites (the R57 bug — history in
|
|
555
|
+
* lynx_sfx.c). sfx_tone()/sfx_noise() only STAGE; sfx_update() is
|
|
556
|
+
* the hardware flush. */
|
|
100
557
|
while (tgi_busy()) { }
|
|
101
558
|
|
|
102
|
-
/*
|
|
103
|
-
*
|
|
104
|
-
* screen as blank. A framed "well" in the centre with lit side panels
|
|
105
|
-
* keeps several distinct colours well under the threshold:
|
|
106
|
-
* - blue cabinet backdrop
|
|
107
|
-
* - dark-grey side panels flanking the well
|
|
108
|
-
* - black well interior so the falling blocks read clearly
|
|
109
|
-
* - light-grey well frame + a faint grid texture behind the cells. */
|
|
559
|
+
/* background: a dim field so no frame is a flat single colour (a >=92%
|
|
560
|
+
* single-colour frame trips the render-health audit as "blank"). */
|
|
110
561
|
tgi_setcolor(COLOR_BLUE);
|
|
111
|
-
tgi_bar(0, 0,
|
|
112
|
-
tgi_setcolor(
|
|
113
|
-
tgi_bar(0, 0,
|
|
114
|
-
tgi_bar(
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
for (c = 0; c <= COLS; c++)
|
|
123
|
-
tgi_line(GRID_X + c * CELL_PX, GRID_Y, GRID_X + c * CELL_PX, GRID_Y + ROWS * CELL_PX - 1);
|
|
124
|
-
/* well frame */
|
|
125
|
-
tgi_setcolor(COLOR_LIGHTGREY);
|
|
126
|
-
tgi_line(GRID_X - 2, GRID_Y - 2, GRID_X - 2, GRID_Y + ROWS * CELL_PX + 1);
|
|
127
|
-
tgi_line(GRID_X + COLS * CELL_PX + 1, GRID_Y - 2, GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1);
|
|
128
|
-
tgi_line(GRID_X - 2, GRID_Y + ROWS * CELL_PX + 1, GRID_X + COLS * CELL_PX + 1, GRID_Y + ROWS * CELL_PX + 1);
|
|
129
|
-
|
|
130
|
-
/* grid */
|
|
131
|
-
for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) {
|
|
132
|
-
if (grid[r][c] != 0) {
|
|
133
|
-
tgi_setcolor(cell_color(grid[r][c]));
|
|
134
|
-
tgi_bar(GRID_X + c * CELL_PX, GRID_Y + r * CELL_PX,
|
|
135
|
-
GRID_X + c * CELL_PX + CELL_PX - 1, GRID_Y + r * CELL_PX + CELL_PX - 1);
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
/* piece */
|
|
139
|
-
for (i = 0; i < 3; i++) {
|
|
140
|
-
pr = (int8_t)(piece_y + i);
|
|
141
|
-
if (pr < 0 || pr >= ROWS) continue;
|
|
142
|
-
tgi_setcolor(cell_color(piece[i]));
|
|
143
|
-
tgi_bar(GRID_X + piece_x * CELL_PX, GRID_Y + pr * CELL_PX,
|
|
144
|
-
GRID_X + piece_x * CELL_PX + CELL_PX - 1, GRID_Y + pr * CELL_PX + CELL_PX - 1);
|
|
145
|
-
}
|
|
562
|
+
tgi_bar(0, 0, 159, 101);
|
|
563
|
+
tgi_setcolor(COLOR_PURPLE);
|
|
564
|
+
tgi_bar(0, 0, 159, 2); /* top accent band */
|
|
565
|
+
tgi_bar(0, 99, 159, 101); /* bottom accent band */
|
|
566
|
+
|
|
567
|
+
joy = joy_read(JOY_1);
|
|
568
|
+
|
|
569
|
+
if (state == ST_TITLE) frame_title(joy);
|
|
570
|
+
else if (state == ST_PLAY) frame_play(joy);
|
|
571
|
+
else frame_over(joy);
|
|
572
|
+
|
|
146
573
|
tgi_updatedisplay();
|
|
147
574
|
sfx_update();
|
|
148
575
|
|
|
149
|
-
|
|
150
|
-
if (JOY_LEFT(joy) && !(prev & 4) && !collides(piece_x - 1, piece_y)) piece_x--;
|
|
151
|
-
if (JOY_RIGHT(joy) && !(prev & 8) && !collides(piece_x + 1, piece_y)) piece_x++;
|
|
152
|
-
if (JOY_BTN_1(joy) && !(prev & 0x10)) {
|
|
153
|
-
t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
|
|
154
|
-
sfx_tone(1, 40, 2);
|
|
155
|
-
}
|
|
156
|
-
if (JOY_BTN_2(joy) && !(prev & 0x20)) {
|
|
157
|
-
while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
|
|
158
|
-
lock_piece();
|
|
159
|
-
new_piece();
|
|
160
|
-
}
|
|
161
|
-
prev = (JOY_LEFT(joy) ? 4 : 0) | (JOY_RIGHT(joy) ? 8 : 0)
|
|
162
|
-
| (JOY_BTN_1(joy) ? 0x10 : 0) | (JOY_BTN_2(joy) ? 0x20 : 0);
|
|
163
|
-
|
|
164
|
-
fall_rate = JOY_DOWN(joy) ? 4 : 30;
|
|
165
|
-
if (++fall_timer >= fall_rate) {
|
|
166
|
-
fall_timer = 0;
|
|
167
|
-
if (collides(piece_x, (int8_t)(piece_y + 1))) {
|
|
168
|
-
lock_piece();
|
|
169
|
-
new_piece();
|
|
170
|
-
} else {
|
|
171
|
-
piece_y++;
|
|
172
|
-
}
|
|
173
|
-
}
|
|
576
|
+
prev_joy = joy;
|
|
174
577
|
}
|
|
175
578
|
}
|