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,182 +1,649 @@
|
|
|
1
|
-
/* ── sports/main.c — MSX
|
|
1
|
+
/* ── sports/main.c — MSX head-to-head court sports (complete example game) ────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* SPARK SWAT — a COMPLETE, working game: title screen, 1P VS a beatable CPU
|
|
4
|
+
* and 2P SIMULTANEOUS VERSUS (P2 on JOYSTICK PORT 2), first-to-5 match flow
|
|
5
|
+
* into a result screen, a longest-win-streak record, music + SFX on the
|
|
6
|
+
* AY-3-8910 PSG, and the MSX's signature SCREEN-2 PER-ROW COLOR: the court
|
|
7
|
+
* floor, the two rails, the centre net and the HUD band all come ENTIRELY
|
|
8
|
+
* from the three independent screen-2 color thirds plus a one-tile vertical
|
|
9
|
+
* "pulse" gradient down the net — costing zero extra tiles.
|
|
5
10
|
*
|
|
6
|
-
* The
|
|
7
|
-
*
|
|
8
|
-
*
|
|
11
|
+
* The game (Pong lineage): a ball rallies between two paddles on a netted
|
|
12
|
+
* court. UP/DOWN move your paddle; where the ball strikes the paddle sets the
|
|
13
|
+
* return angle (centre = flat, edges = steep). Steep edge returns outrun the
|
|
14
|
+
* half-speed CPU — that is exactly how a human beats it. First side to 5 wins.
|
|
9
15
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
17
|
+
* very different one. The markers tell you what's what:
|
|
18
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented MSX footgun; reshape
|
|
19
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
20
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
21
|
+
* reshape freely.
|
|
16
22
|
*
|
|
17
|
-
*
|
|
23
|
+
* What depends on what:
|
|
24
|
+
* msx_hw.h / msx_vdp.c — VDP + PSG + joystick helpers (direct Z80 ports;
|
|
25
|
+
* the PSG functions carry a DI/EI guard against the BIOS KEYINT race —
|
|
26
|
+
* read msx_vdp.c before adding your own PSG pokes).
|
|
27
|
+
* msx_crt0.s — the $4000 "AB" cart header + static-init copy. Load-bearing;
|
|
28
|
+
* INIT must NEVER return, so main() ends in for(;;).
|
|
29
|
+
*
|
|
30
|
+
* A TEACHING POINT vs the Genesis version of this game
|
|
31
|
+
* (examples/genesis/templates/sports.c): the Genesis hangs its HUD on a
|
|
32
|
+
* hardware WINDOW plane (a fixed status bar at zero per-frame cost) and paints
|
|
33
|
+
* the court ONCE into plane B. The MSX has no window plane and no DMA — but
|
|
34
|
+
* screen 2 gives us three independent COLOR thirds for free, so our HUD band,
|
|
35
|
+
* court floor and rails are all one tilemap differentiated purely by which
|
|
36
|
+
* third (row band) they sit in. Same genre, a different "free" hardware gift.
|
|
37
|
+
*
|
|
38
|
+
* Controls: JOYSTICK PORT 1 (or keyboard cursors) UP/DOWN moves the left
|
|
39
|
+
* paddle. In 2P versus, JOYSTICK PORT 2 UP/DOWN moves the right paddle. On
|
|
40
|
+
* the title screen trigger A (or SPACE) starts 1P vs CPU; trigger B starts
|
|
41
|
+
* 2P versus. On the result screen any fire returns to the title.
|
|
42
|
+
*
|
|
43
|
+
* Record honesty: the bundled bluemsx core build exposes NO battery save path
|
|
44
|
+
* (retro_get_memory(SAVE_RAM) is unimplemented for MSX carts), so BEST (the
|
|
45
|
+
* longest 1P-vs-CPU win streak) lives in plain RAM: it survives title↔match
|
|
46
|
+
* cycles but NOT a power cycle / hardReset. Never fake persistence — if you
|
|
47
|
+
* need real saves, that's a future core round (ASCII8-SRAM mapper carts
|
|
48
|
+
* exist; the core just doesn't surface their RAM yet). The Genesis/NES/SMS
|
|
49
|
+
* versions of this game DO persist the same streak to cartridge SRAM.
|
|
18
50
|
*/
|
|
19
51
|
#include "msx_hw.h"
|
|
20
52
|
|
|
21
|
-
/*
|
|
53
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
54
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
55
|
+
#define GAME_TITLE "SPARK SWAT"
|
|
56
|
+
|
|
57
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
58
|
+
* Interrupt-free vblank sync: poll VDP status S#0 bit 7 (port 0x99). Reading
|
|
59
|
+
* the port ALSO clears the flag, so one read per frame = one game step per
|
|
60
|
+
* frame. We deliberately do NOT use the BIOS JIFFY counter here: this poll
|
|
61
|
+
* works even with interrupts masked, and never depends on the BIOS ISR
|
|
62
|
+
* keeping pace. (The BIOS KEYINT also reads S#0 — on rare frames it eats the
|
|
63
|
+
* flag first and this loop just waits for the next one; a one-frame hiccup,
|
|
64
|
+
* never a hang.) */
|
|
22
65
|
__sfr __at 0x99 VDPSTATUS;
|
|
23
66
|
static void vsync(void) {
|
|
24
|
-
(void)VDPSTATUS;
|
|
67
|
+
(void)VDPSTATUS; /* throw away a possibly-stale flag */
|
|
25
68
|
while (!(VDPSTATUS & 0x80)) {
|
|
26
69
|
}
|
|
27
70
|
}
|
|
28
71
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
#define
|
|
72
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
73
|
+
* Tile font: index 0 = space, 1-26 = A-Z, 27-36 = 0-9, 37 = dash, then the
|
|
74
|
+
* court tiles. One 8x8 pattern = 8 bytes, one bit per pixel; set bits draw in
|
|
75
|
+
* the tile's FOREGROUND color, clear bits in its BACKGROUND color (both come
|
|
76
|
+
* from the screen-2 color table — see the per-row-color idiom below). */
|
|
77
|
+
#define T_SPACE 0
|
|
78
|
+
#define T_A 1 /* 'A'..'Z' = T_A + (c - 'A') */
|
|
79
|
+
#define T_0 27 /* '0'..'9' = T_0 + (c - '0') */
|
|
80
|
+
#define T_DASH 37
|
|
81
|
+
#define T_FLOOR 38 /* the court surface (faint speckle) */
|
|
82
|
+
#define T_RAIL 39 /* solid top/bottom court rail */
|
|
83
|
+
#define T_NET 40 /* dashed centre net (its COLOR carries the pulse) */
|
|
84
|
+
#define NUM_TILES 41
|
|
35
85
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
86
|
+
static const uint8_t font[NUM_TILES][8] = {
|
|
87
|
+
/* SPACE */ {0,0,0,0,0,0,0,0},
|
|
88
|
+
/* 1 A */ {0x38,0x6C,0xC6,0xC6,0xFE,0xC6,0xC6,0x00},
|
|
89
|
+
/* 2 B */ {0xFC,0xC6,0xC6,0xFC,0xC6,0xC6,0xFC,0x00},
|
|
90
|
+
/* 3 C */ {0x7C,0xC6,0xC0,0xC0,0xC0,0xC6,0x7C,0x00},
|
|
91
|
+
/* 4 D */ {0xF8,0xCC,0xC6,0xC6,0xC6,0xCC,0xF8,0x00},
|
|
92
|
+
/* 5 E */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xFE,0x00},
|
|
93
|
+
/* 6 F */ {0xFE,0xC0,0xC0,0xF8,0xC0,0xC0,0xC0,0x00},
|
|
94
|
+
/* 7 G */ {0x7C,0xC6,0xC0,0xCE,0xC6,0xC6,0x7C,0x00},
|
|
95
|
+
/* 8 H */ {0xC6,0xC6,0xC6,0xFE,0xC6,0xC6,0xC6,0x00},
|
|
96
|
+
/* 9 I */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
97
|
+
/* 10 J */ {0x1E,0x06,0x06,0x06,0xC6,0xC6,0x7C,0x00},
|
|
98
|
+
/* 11 K */ {0xC6,0xCC,0xD8,0xF0,0xD8,0xCC,0xC6,0x00},
|
|
99
|
+
/* 12 L */ {0xC0,0xC0,0xC0,0xC0,0xC0,0xC0,0xFE,0x00},
|
|
100
|
+
/* 13 M */ {0xC6,0xEE,0xFE,0xD6,0xC6,0xC6,0xC6,0x00},
|
|
101
|
+
/* 14 N */ {0xC6,0xE6,0xF6,0xDE,0xCE,0xC6,0xC6,0x00},
|
|
102
|
+
/* 15 O */ {0x7C,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
103
|
+
/* 16 P */ {0xFC,0xC6,0xC6,0xFC,0xC0,0xC0,0xC0,0x00},
|
|
104
|
+
/* 17 Q */ {0x7C,0xC6,0xC6,0xC6,0xD6,0xCC,0x76,0x00},
|
|
105
|
+
/* 18 R */ {0xFC,0xC6,0xC6,0xFC,0xD8,0xCC,0xC6,0x00},
|
|
106
|
+
/* 19 S */ {0x7C,0xC0,0xC0,0x78,0x0C,0x0C,0xF8,0x00},
|
|
107
|
+
/* 20 T */ {0x7E,0x18,0x18,0x18,0x18,0x18,0x18,0x00},
|
|
108
|
+
/* 21 U */ {0xC6,0xC6,0xC6,0xC6,0xC6,0xC6,0x7C,0x00},
|
|
109
|
+
/* 22 V */ {0xC6,0xC6,0xC6,0xC6,0x6C,0x38,0x10,0x00},
|
|
110
|
+
/* 23 W */ {0xC6,0xC6,0xC6,0xD6,0xFE,0xEE,0xC6,0x00},
|
|
111
|
+
/* 24 X */ {0xC6,0x6C,0x38,0x10,0x38,0x6C,0xC6,0x00},
|
|
112
|
+
/* 25 Y */ {0x66,0x66,0x66,0x3C,0x18,0x18,0x18,0x00},
|
|
113
|
+
/* 26 Z */ {0xFE,0x0C,0x18,0x30,0x60,0xC0,0xFE,0x00},
|
|
114
|
+
/* 27 0 */ {0x7C,0xCE,0xDE,0xF6,0xE6,0xC6,0x7C,0x00},
|
|
115
|
+
/* 28 1 */ {0x18,0x38,0x18,0x18,0x18,0x18,0x7E,0x00},
|
|
116
|
+
/* 29 2 */ {0x7C,0xC6,0x06,0x1C,0x70,0xC0,0xFE,0x00},
|
|
117
|
+
/* 30 3 */ {0x7C,0xC6,0x06,0x3C,0x06,0xC6,0x7C,0x00},
|
|
118
|
+
/* 31 4 */ {0x1C,0x3C,0x6C,0xCC,0xFE,0x0C,0x0C,0x00},
|
|
119
|
+
/* 32 5 */ {0xFE,0xC0,0xFC,0x06,0x06,0xC6,0x7C,0x00},
|
|
120
|
+
/* 33 6 */ {0x3C,0x60,0xC0,0xFC,0xC6,0xC6,0x7C,0x00},
|
|
121
|
+
/* 34 7 */ {0xFE,0x06,0x0C,0x18,0x30,0x30,0x30,0x00},
|
|
122
|
+
/* 35 8 */ {0x7C,0xC6,0xC6,0x7C,0xC6,0xC6,0x7C,0x00},
|
|
123
|
+
/* 36 9 */ {0x7C,0xC6,0xC6,0x7E,0x06,0x0C,0x78,0x00},
|
|
124
|
+
/* 37 - */ {0x00,0x00,0x00,0x7E,0x00,0x00,0x00,0x00},
|
|
125
|
+
/* 38 FLOOR (sparse speckle so the arena reads as a court, not a void) */
|
|
126
|
+
{0x00,0x00,0x10,0x00,0x00,0x00,0x01,0x00},
|
|
127
|
+
/* 39 RAIL (solid border) */ {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF},
|
|
128
|
+
/* 40 NET (dashed bar — solid pixels so the COLOR pulse below shows) */
|
|
129
|
+
{0x18,0x18,0x18,0x18,0x18,0x18,0x18,0x18},
|
|
130
|
+
};
|
|
40
131
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
132
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
133
|
+
* SCREEN-2 PER-ROW COLOR — the MSX's signature background trick.
|
|
134
|
+
*
|
|
135
|
+
* Screen 2 (GRAPHIC II) is NOT "one color byte per tile" like most consoles:
|
|
136
|
+
*
|
|
137
|
+
* 1. The 256x192 screen is THREE INDEPENDENT THIRDS of 8 rows each
|
|
138
|
+
* (name-table rows 0-7, 8-15, 16-23). Each third has its OWN 2KB
|
|
139
|
+
* pattern table slice and its OWN 2KB color table slice:
|
|
140
|
+
* patterns: VRAM_PATTERN + third*0x800, colors: VRAM_COLOR + third*0x800
|
|
141
|
+
* The SAME tile index can look completely different in each third. We
|
|
142
|
+
* exploit exactly that to make a single FLOOR/RAIL/NET tile set read as a
|
|
143
|
+
* depth-shaded court: third 0 (the HUD band + top rail) gets its own
|
|
144
|
+
* bright text colors; the play thirds get a cooler court palette; the
|
|
145
|
+
* bottom third deepens toward the foreground. One tile set, three bands,
|
|
146
|
+
* zero extra tiles — the sports-genre twin of the shmup's depth starfield.
|
|
147
|
+
*
|
|
148
|
+
* 2. Within a tile, the color table holds EIGHT bytes — one per 8x1 pixel
|
|
149
|
+
* row — each packing (foreground<<4)|background from the fixed TMS9918
|
|
150
|
+
* palette. So one tile can carry an 8-color vertical gradient: T_NET's
|
|
151
|
+
* whole "energy pulse" running down the centre net is a single tile,
|
|
152
|
+
* colors only.
|
|
153
|
+
*
|
|
154
|
+
* Requires: the screen-2 table layout set by msx_set_screen2() (R3=0xFF,
|
|
155
|
+
* R4=0x03 — the "thirds" configuration), and pattern + color uploads to
|
|
156
|
+
* EVERY third a tile is used in. Tile N's slot is pattern[N*8] / color[N*8].
|
|
157
|
+
*
|
|
158
|
+
* TMS9918 fixed palette used here: 1 black, 4 dark blue, 5 light blue,
|
|
159
|
+
* 6 dark red, 7 cyan, 8 medium red, 11 light yellow, 12 green, 13 light green,
|
|
160
|
+
* 14 gray, 15 white (high nibble = fg, low nibble = bg of each row byte). */
|
|
161
|
+
static const uint8_t col_text[3] = { 0xF4, 0xF1, 0xF1 }; /* HUD white-on-blue; play/title white-on-black */
|
|
162
|
+
/* The court FLOOR speckle, banded by third: cyan-ish near the HUD, deeper blue
|
|
163
|
+
* mid-court, light-blue close — pure per-third recolor of one tile. */
|
|
164
|
+
static const uint8_t col_floor[3] = { 0x71, 0x41, 0x51 };
|
|
165
|
+
/* The court RAILS, banded so the top rail (third 0) reads bright and the
|
|
166
|
+
* bottom rail (third 2) reads cooler — same solid tile, three colors. */
|
|
167
|
+
static const uint8_t col_rail[3] = { 0xF1, 0xE1, 0xD1 };
|
|
168
|
+
/* T_NET: 8 DIFFERENT color bytes inside ONE tile = an 8-pixel-row "energy
|
|
169
|
+
* pulse" down the net (black → dark blue → cyan → white and back). The net
|
|
170
|
+
* pattern is a solid 2px bar so only the fg nibbles show. Drawn down the
|
|
171
|
+
* centre column; recolored again per third for free. */
|
|
172
|
+
static const uint8_t col_net[8] = { 0x11,0x41,0x71,0xF1,0xF1,0x71,0x41,0x11 };
|
|
44
173
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
174
|
+
static void load_tiles(void) {
|
|
175
|
+
uint8_t third, i;
|
|
176
|
+
uint16_t patbase, colbase;
|
|
177
|
+
for (third = 0; third < 3; third++) {
|
|
178
|
+
patbase = (uint16_t)(VRAM_PATTERN + ((uint16_t)third << 11));
|
|
179
|
+
colbase = (uint16_t)(VRAM_COLOR + ((uint16_t)third << 11));
|
|
180
|
+
for (i = 0; i < NUM_TILES; i++) {
|
|
181
|
+
uint8_t col;
|
|
182
|
+
/* pattern bits are the same in every third — only COLOR varies */
|
|
183
|
+
msx_vram_write((uint16_t)(patbase + ((uint16_t)i << 3)), font[i], 8);
|
|
184
|
+
if (i == T_NET) { /* the one per-pixel-row gradient */
|
|
185
|
+
msx_vram_write((uint16_t)(colbase + ((uint16_t)i << 3)), col_net, 8);
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
if (i == T_FLOOR) col = col_floor[third];
|
|
189
|
+
else if (i == T_RAIL) col = col_rail[third];
|
|
190
|
+
else col = col_text[third];
|
|
191
|
+
msx_fill_vram((uint16_t)(colbase + ((uint16_t)i << 3)), 8, col);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
49
195
|
|
|
50
|
-
/*
|
|
51
|
-
|
|
52
|
-
|
|
196
|
+
/* ── GAME LOGIC (clay — reshape freely) — name-table drawing helpers ────────
|
|
197
|
+
* Screen 2 VRAM writes are safe at any point in the frame at C speed: the
|
|
198
|
+
* TMS9918 needs ~29 Z80 cycles between VRAM accesses during active display,
|
|
199
|
+
* and SDCC-compiled loops are slower than that. (Hand-tuned asm OTIR bursts
|
|
200
|
+
* are the thing that outruns the VDP — see TROUBLESHOOTING.) */
|
|
201
|
+
static void put_tile(uint8_t col, uint8_t row, uint8_t tile) {
|
|
202
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), &tile, 1);
|
|
203
|
+
}
|
|
53
204
|
|
|
54
|
-
static
|
|
55
|
-
|
|
205
|
+
static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
206
|
+
uint8_t buf[32];
|
|
207
|
+
uint8_t n = 0;
|
|
208
|
+
while (*s && n < 32) {
|
|
209
|
+
char c = *s++;
|
|
210
|
+
if (c >= 'A' && c <= 'Z') buf[n] = (uint8_t)(T_A + c - 'A');
|
|
211
|
+
else if (c >= '0' && c <= '9') buf[n] = (uint8_t)(T_0 + c - '0');
|
|
212
|
+
else if (c == '-') buf[n] = T_DASH;
|
|
213
|
+
else buf[n] = T_SPACE;
|
|
214
|
+
n++;
|
|
215
|
+
}
|
|
216
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, n);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
static void draw_num4(uint8_t col, uint8_t row, uint16_t v) {
|
|
220
|
+
uint8_t buf[4];
|
|
221
|
+
buf[0] = (uint8_t)(T_0 + (v / 1000) % 10);
|
|
222
|
+
buf[1] = (uint8_t)(T_0 + (v / 100) % 10);
|
|
223
|
+
buf[2] = (uint8_t)(T_0 + (v / 10) % 10);
|
|
224
|
+
buf[3] = (uint8_t)(T_0 + v % 10);
|
|
225
|
+
msx_vram_write((uint16_t)(VRAM_NAME + (uint16_t)row * 32 + col), buf, 4);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/* ── GAME LOGIC (clay — reshape freely) — court geometry + match rules ───────
|
|
229
|
+
* The court fills the 32x24 screen-2 name table. Rails on name-table rows 2
|
|
230
|
+
* and 22; COURT_TOP/BOT keep the ball between them (pixels). Net down column
|
|
231
|
+
* 16. Row 0 is the HUD band (third 0's text colors make it a distinct strip). */
|
|
232
|
+
#define NET_COL 16
|
|
233
|
+
#define RAIL_TOP_ROW 2
|
|
234
|
+
#define RAIL_BOT_ROW 22
|
|
235
|
+
#define COURT_TOP 24 /* first pixel row below the top rail */
|
|
236
|
+
#define COURT_BOT 176 /* first pixel row of the bottom rail */
|
|
237
|
+
#define PADDLE_H 24 /* 3 stacked 8x8 sprites */
|
|
238
|
+
#define PADDLE_X1 16 /* P1 — left side */
|
|
239
|
+
#define PADDLE_X2 232 /* P2/CPU — right side */
|
|
240
|
+
#define BALL_W 8
|
|
241
|
+
#define BALL_H 8
|
|
242
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
243
|
+
#define P_SPEED 3 /* px/frame — both humans move at this */
|
|
244
|
+
#define CPU_SPEED 1 /* px/frame — HALF the ball's 2px/frame *
|
|
245
|
+
* horizontal speed: it cannot always reach *
|
|
246
|
+
* a steep edge return, so a human who aims *
|
|
247
|
+
* edge hits beats it (verified). Raise this *
|
|
248
|
+
* toward P_SPEED to make the CPU tougher. */
|
|
249
|
+
|
|
250
|
+
static int16_t p1y, p2y; /* paddle top Y (pixels) */
|
|
251
|
+
static int16_t bx, by; /* ball top-left (pixels) */
|
|
252
|
+
static int8_t bdx, bdy; /* ball velocity (px/frame) */
|
|
56
253
|
static uint8_t score_p1, score_p2;
|
|
57
|
-
static uint8_t serve_timer;
|
|
58
|
-
static uint8_t
|
|
254
|
+
static uint8_t serve_timer; /* freeze frames between points */
|
|
255
|
+
static uint8_t two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
|
|
256
|
+
static uint8_t streak; /* current 1P-vs-CPU win streak (RAM) */
|
|
257
|
+
static uint16_t best_streak; /* SESSION-ONLY record — see end_match + the
|
|
258
|
+
* record-honesty note at the top of file.
|
|
259
|
+
* No SAVE_RAM on this core, so it lives in
|
|
260
|
+
* plain RAM: survives title↔match cycles,
|
|
261
|
+
* NOT a power cycle (honest, not faked). */
|
|
262
|
+
static uint8_t new_record; /* result screen shows NEW RECORD */
|
|
59
263
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
264
|
+
#define ST_TITLE 0
|
|
265
|
+
#define ST_PLAY 1
|
|
266
|
+
#define ST_OVER 2
|
|
267
|
+
static uint8_t state;
|
|
268
|
+
static uint8_t prev_t1, prev_t2; /* title/over trigger edge detection */
|
|
269
|
+
|
|
270
|
+
/* ── GAME LOGIC (clay — reshape freely) — xorshift16 PRNG.
|
|
271
|
+
* A versus game NEEDS this: the MSX is fully deterministic, so without a noise
|
|
272
|
+
* source two fixed strategies lock into an infinite rally loop (the exact same
|
|
273
|
+
* cycle, forever — a match that NEVER ends). next_rand() is ticked once per
|
|
274
|
+
* play frame so identical game states a few seconds apart still diverge, and
|
|
275
|
+
* every paddle return adds a ±1 "spin" — so an idle 1P-vs-CPU match always
|
|
276
|
+
* reaches 5 in bounded time. */
|
|
277
|
+
static uint16_t rng;
|
|
278
|
+
static uint8_t next_rand(void) {
|
|
279
|
+
rng ^= (uint16_t)(rng << 7);
|
|
280
|
+
rng ^= (uint16_t)(rng >> 9);
|
|
281
|
+
rng ^= (uint16_t)(rng << 8);
|
|
282
|
+
return (uint8_t)(rng & 0xFF);
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/* ── GAME LOGIC (clay — reshape freely) — music + SFX on the AY-3-8910 ──────
|
|
286
|
+
* Channel plan: A = paddle/score blips, B = rail bonk + whistle noise, C =
|
|
287
|
+
* music. The PSG has 3 tone channels + ONE shared noise generator, mixed
|
|
288
|
+
* per-channel in reg 7. All register traffic goes through msx_psg_tone/noise/
|
|
289
|
+
* off — they wrap the PSGADDR/PSGWRITE pair in DI/EI because the BIOS KEYINT
|
|
290
|
+
* ISR clobbers the PSG address latch every frame (the bug that once silenced
|
|
291
|
+
* every MSX scaffold — see msx_vdp.c).
|
|
292
|
+
*
|
|
293
|
+
* The tune: one period entry per half-beat, 0 = rest. AY period =
|
|
294
|
+
* 1789773 / (16 * freq) — e.g. A4 (440Hz) -> 254. Ticked once per frame; a
|
|
295
|
+
* note advances every 8 frames. The lib's built-in demo loop (msx_music_tick)
|
|
296
|
+
* also uses channel C, so we switch it OFF in main() and run THIS table
|
|
297
|
+
* instead — edit this table to rescore. */
|
|
298
|
+
static const uint16_t tune[32] = {
|
|
299
|
+
285, 0, 339, 285, 254, 0, 285, 339, /* G4 E4 G4 A4 G4 E4 (bright march) */
|
|
300
|
+
427, 0, 339, 254, 339, 0, 0, 0, /* C4 E4 A4 E4 rest */
|
|
301
|
+
320, 0, 285, 254, 214, 0, 254, 285, /* F4 G4 A4 C5 A4 G4 */
|
|
302
|
+
339, 0, 285, 339, 427, 0, 0, 0, /* E4 G4 E4 C4 rest */
|
|
303
|
+
};
|
|
304
|
+
static uint8_t music_step, music_timer;
|
|
305
|
+
static uint8_t sfx_a_t, sfx_b_t; /* frames left on the A/B SFX channels */
|
|
306
|
+
|
|
307
|
+
static void music_tick(void) {
|
|
308
|
+
if (music_timer == 0) {
|
|
309
|
+
uint16_t p = tune[music_step & 31];
|
|
310
|
+
if (p) msx_psg_tone(2, p, 9);
|
|
311
|
+
else msx_psg_off(2);
|
|
312
|
+
music_step++;
|
|
72
313
|
}
|
|
314
|
+
music_timer++;
|
|
315
|
+
if (music_timer >= 8) music_timer = 0;
|
|
73
316
|
}
|
|
74
317
|
|
|
75
|
-
static void
|
|
76
|
-
|
|
318
|
+
static void sfx_tick(void) {
|
|
319
|
+
if (sfx_a_t) { sfx_a_t--; if (!sfx_a_t) msx_psg_off(0); }
|
|
320
|
+
if (sfx_b_t) { sfx_b_t--; if (!sfx_b_t) msx_psg_noise(1, 0, 0); }
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
static void sfx_hit(void) { msx_psg_tone(0, 0x200, 11); sfx_a_t = 4; }
|
|
324
|
+
static void sfx_rail(void) { msx_psg_tone(1, 0x300, 9); sfx_b_t = 3; }
|
|
325
|
+
static void sfx_point(void) { msx_psg_noise(1, 14, 13); sfx_b_t = 8; }
|
|
326
|
+
static void sfx_over(void) { msx_psg_noise(1, 28, 14); sfx_b_t = 22; }
|
|
327
|
+
static void sfx_start(void) { msx_psg_tone(0, 0x130, 12); sfx_a_t = 6; }
|
|
328
|
+
|
|
329
|
+
/* ── GAME LOGIC (clay — reshape freely) — HUD ──────────────────────────────
|
|
330
|
+
* Row 0 = the HUD band (third 0's text colors make it a distinct strip).
|
|
331
|
+
* P1 score | BEST (longest streak) | P2/CPU score. */
|
|
332
|
+
static void draw_hud_labels(void) {
|
|
333
|
+
draw_text(1, 0, "P1");
|
|
334
|
+
draw_text(12, 0, "BEST");
|
|
335
|
+
draw_text(25, 0, two_player ? "P2" : "CPU");
|
|
336
|
+
}
|
|
337
|
+
static void draw_scores(void) {
|
|
338
|
+
put_tile(4, 0, (uint8_t)(T_0 + score_p1));
|
|
339
|
+
put_tile(29, 0, (uint8_t)(T_0 + score_p2));
|
|
77
340
|
}
|
|
341
|
+
static void draw_best(void) { draw_num4(17, 0, best_streak); }
|
|
78
342
|
|
|
79
|
-
|
|
343
|
+
/* ── GAME LOGIC (clay — reshape freely) — paint the court (name table) ──────
|
|
344
|
+
* The whole 32x24 name table: HUD band on row 0, rails on rows 2 and 22, net
|
|
345
|
+
* down column 16, floor everywhere else. The per-third color idiom shades it
|
|
346
|
+
* into bands for free — this routine writes only TILE INDICES. */
|
|
347
|
+
static void clear_field(void) { msx_fill_vram(VRAM_NAME, 32u * 24u, T_SPACE); }
|
|
348
|
+
|
|
349
|
+
static void paint_court(void) {
|
|
80
350
|
uint8_t row, col, t;
|
|
81
351
|
for (row = 0; row < 24; row++) {
|
|
82
352
|
for (col = 0; col < 32; col++) {
|
|
83
|
-
t =
|
|
84
|
-
if (row
|
|
85
|
-
|
|
86
|
-
else if (
|
|
87
|
-
|
|
353
|
+
if (row == 0) t = T_SPACE; /* HUD band */
|
|
354
|
+
else if (row == RAIL_TOP_ROW
|
|
355
|
+
|| row == RAIL_BOT_ROW) t = T_RAIL;
|
|
356
|
+
else if (row > RAIL_TOP_ROW
|
|
357
|
+
&& row < RAIL_BOT_ROW
|
|
358
|
+
&& col == NET_COL) t = T_NET;
|
|
359
|
+
else if (row > RAIL_TOP_ROW
|
|
360
|
+
&& row < RAIL_BOT_ROW) t = T_FLOOR;
|
|
361
|
+
else t = T_SPACE;
|
|
362
|
+
put_tile(col, row, t);
|
|
88
363
|
}
|
|
89
364
|
}
|
|
365
|
+
draw_hud_labels();
|
|
366
|
+
draw_scores();
|
|
367
|
+
draw_best();
|
|
90
368
|
}
|
|
91
369
|
|
|
370
|
+
/* ── GAME LOGIC (clay — reshape freely) — sprites: paddles + ball ───────────
|
|
371
|
+
* 8x8 one-color hardware sprites. Plane layout: 0-2 = P1 paddle (3 stacked
|
|
372
|
+
* cells), 3-5 = P2 paddle, 6 = ball. Locked court art is tiles, not sprites,
|
|
373
|
+
* so the list never needs more than 7 planes. */
|
|
374
|
+
static const uint8_t spr_block[8] = {0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF,0xFF};
|
|
375
|
+
static const uint8_t spr_ball[8] = {0x3C,0x7E,0xFF,0xFF,0xFF,0xFF,0x7E,0x3C};
|
|
376
|
+
#define PAT_PADDLE 0
|
|
377
|
+
#define PAT_BALL 1
|
|
378
|
+
#define COL_P1 15 /* white */
|
|
379
|
+
#define COL_P2 8 /* medium red */
|
|
380
|
+
#define COL_BALL 11 /* light yellow — distinct from the white paddles */
|
|
381
|
+
|
|
382
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
383
|
+
* Sprite limits + the Y=208 terminator:
|
|
384
|
+
* - A sprite Y of 0xD0 (208) tells the TMS9918 to STOP SCANNING the
|
|
385
|
+
* attribute table — every higher-numbered plane vanishes, not just that
|
|
386
|
+
* one. (msx_clear_sprites parks ALL planes at 0xD0, which is fine at the
|
|
387
|
+
* END of the list.) To hide ONE sprite mid-list, park it OFFSCREEN at
|
|
388
|
+
* PARK_Y (192 = first line below the display) — never at 0xD0.
|
|
389
|
+
* (On MSX2's V9938 sprite mode 2 the terminator moves to 0xD8 and 0xD0
|
|
390
|
+
* is "just offscreen" — code that leans on that breaks on MSX1.)
|
|
391
|
+
* - Per scanline the TMS9918 draws only 4 sprites (V9938: 8). The two
|
|
392
|
+
* paddles sit at opposite screen edges and the ball rallies between them,
|
|
393
|
+
* so a single scanline never carries more than 2 of our 7 planes. */
|
|
394
|
+
#define PARK_Y 192
|
|
395
|
+
|
|
396
|
+
/* Push the two paddles + ball to their planes. Paddles freeze (but stay
|
|
397
|
+
* visible) on the result screen; the ball parks offscreen there and on the
|
|
398
|
+
* title. Never park at 0xD0 mid-list — see the idiom. */
|
|
399
|
+
static void push_sprites(void) {
|
|
400
|
+
uint8_t i;
|
|
401
|
+
uint8_t actors = (state != ST_TITLE); /* paddles show in play + result */
|
|
402
|
+
uint8_t ball_on = (state == ST_PLAY); /* ball only lives during a rally*/
|
|
403
|
+
for (i = 0; i < PADDLE_H / 8; i++) {
|
|
404
|
+
msx_set_sprite((uint8_t)(0 + i), PADDLE_X1,
|
|
405
|
+
actors ? (uint8_t)(p1y + i * 8) : PARK_Y, PAT_PADDLE, COL_P1);
|
|
406
|
+
msx_set_sprite((uint8_t)(3 + i), PADDLE_X2,
|
|
407
|
+
actors ? (uint8_t)(p2y + i * 8) : PARK_Y, PAT_PADDLE, COL_P2);
|
|
408
|
+
}
|
|
409
|
+
msx_set_sprite(6, (uint8_t)bx, ball_on ? (uint8_t)by : PARK_Y, PAT_BALL, COL_BALL);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
/* ── GAME LOGIC (clay — reshape freely) — serve: ball to centre, toward the
|
|
413
|
+
* chosen side. The serve angle takes a PRNG bit (not a fixed alternation) —
|
|
414
|
+
* one more place determinism is broken so idle matches can't settle. */
|
|
92
415
|
static void serve_ball(uint8_t to_left) {
|
|
93
416
|
bx = 124;
|
|
94
|
-
by =
|
|
417
|
+
by = (COURT_TOP + COURT_BOT) / 2;
|
|
95
418
|
bdx = to_left ? -2 : 2;
|
|
96
|
-
bdy = ((
|
|
97
|
-
serve_timer = 30;
|
|
419
|
+
bdy = (next_rand() & 1) ? -1 : 1;
|
|
420
|
+
serve_timer = 30; /* half-second breather */
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/* ── GAME LOGIC (clay — reshape freely) — the screens ──────────────────────
|
|
424
|
+
* Title rows land across the play thirds — recolored for free by the thirds
|
|
425
|
+
* idiom. A clean name table behind the text. */
|
|
426
|
+
static void paint_title(void) {
|
|
427
|
+
uint8_t len = 0, col;
|
|
428
|
+
const char *p = GAME_TITLE;
|
|
429
|
+
while (*p++) len++;
|
|
430
|
+
col = (uint8_t)((32 - len) / 2);
|
|
431
|
+
clear_field();
|
|
432
|
+
draw_text(col, 6, GAME_TITLE);
|
|
433
|
+
draw_text(7, 11, "1P VS CPU - FIRE A");
|
|
434
|
+
draw_text(7, 13, "2P VERSUS - FIRE B");
|
|
435
|
+
draw_text(11, 16, "FIRST TO 5");
|
|
436
|
+
draw_text(11, 19, "BEST 0000"); /* the space blanks the cell between */
|
|
437
|
+
draw_num4(16, 19, best_streak);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
static void paint_over(void) {
|
|
441
|
+
clear_field();
|
|
442
|
+
if (score_p1 >= WIN_SCORE)
|
|
443
|
+
draw_text(11, 7, two_player ? "P1 WINS" : "YOU WIN");
|
|
444
|
+
else
|
|
445
|
+
draw_text(11, 7, two_player ? "P2 WINS" : "CPU WINS");
|
|
446
|
+
draw_text(13, 10, "P1"); put_tile(16, 10, (uint8_t)(T_0 + score_p1));
|
|
447
|
+
put_tile(17, 10, T_DASH);
|
|
448
|
+
put_tile(18, 10, (uint8_t)(T_0 + score_p2)); draw_text(20, 10, two_player ? "P2" : "CPU");
|
|
449
|
+
if (new_record) draw_text(11, 13, "NEW RECORD");
|
|
450
|
+
draw_text(11, 14, "BEST"); draw_num4(16, 14, best_streak);
|
|
451
|
+
draw_text(8, 17, "FIRE FOR TITLE");
|
|
452
|
+
prev_t1 = prev_t2 = 1; /* swallow a fire still held from play */
|
|
98
453
|
}
|
|
99
454
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
455
|
+
/* ── GAME LOGIC (clay — reshape freely) — start a match ── */
|
|
456
|
+
static void start_match(uint8_t versus) {
|
|
457
|
+
two_player = versus;
|
|
458
|
+
p1y = (COURT_TOP + COURT_BOT) / 2 - PADDLE_H / 2;
|
|
459
|
+
p2y = p1y;
|
|
460
|
+
score_p1 = 0;
|
|
461
|
+
score_p2 = 0;
|
|
462
|
+
new_record = 0;
|
|
103
463
|
serve_ball(0);
|
|
464
|
+
paint_court();
|
|
465
|
+
sfx_start();
|
|
466
|
+
state = ST_PLAY;
|
|
104
467
|
}
|
|
105
468
|
|
|
106
|
-
|
|
107
|
-
|
|
469
|
+
/* ── GAME LOGIC (clay — reshape freely) — match over: result + record.
|
|
470
|
+
* Persistence choice: for a VERSUS sports game a raw hi-score is meaningless
|
|
471
|
+
* (every match ends 5-x), so we track the longest 1P win streak against the
|
|
472
|
+
* CPU — the stat a returning player actually chases. 2P matches never touch it
|
|
473
|
+
* (humans beating each other isn't a record). On THIS core the record is
|
|
474
|
+
* session-only RAM (no SAVE_RAM — see the file header); the Genesis/NES/SMS
|
|
475
|
+
* builds of this game persist the identical streak to cartridge SRAM. ── */
|
|
476
|
+
static void end_match(void) {
|
|
477
|
+
if (score_p1 >= WIN_SCORE && !two_player) {
|
|
478
|
+
++streak;
|
|
479
|
+
if (streak > best_streak) { best_streak = streak; new_record = 1; }
|
|
480
|
+
} else if (!two_player) {
|
|
481
|
+
streak = 0; /* the streak dies with the loss */
|
|
482
|
+
}
|
|
483
|
+
sfx_over();
|
|
484
|
+
paint_over();
|
|
485
|
+
state = ST_OVER;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/* ── GAME LOGIC (clay — reshape freely) — one point scored ── */
|
|
489
|
+
static void score_point(uint8_t for_p1) {
|
|
490
|
+
if (for_p1) ++score_p1; else ++score_p2;
|
|
491
|
+
sfx_point();
|
|
492
|
+
draw_scores();
|
|
493
|
+
if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
|
|
494
|
+
else serve_ball(for_p1); /* winner of the point receives */
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
/* ── GAME LOGIC (clay — reshape freely) — paddle hit: deflect by where the
|
|
498
|
+
* ball struck. Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU
|
|
499
|
+
* moves at 1 (half the ball's horizontal speed), so a steep edge return slips
|
|
500
|
+
* past it: that is exactly how a human beats the CPU. A ±1 random "spin" on
|
|
501
|
+
* every return keeps rallies from repeating (see the PRNG note above). */
|
|
502
|
+
static void deflect(int16_t paddle_y) {
|
|
503
|
+
int16_t rel = (by + BALL_H / 2) - (paddle_y + PADDLE_H / 2);
|
|
504
|
+
bdy = (int8_t)(rel >> 3);
|
|
505
|
+
bdy += (int8_t)((next_rand() & 2) - 1); /* spin: -1 or +1 */
|
|
506
|
+
if (bdy > 2) bdy = 2;
|
|
507
|
+
if (bdy < -2) bdy = -2;
|
|
508
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
509
|
+
sfx_hit();
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* ── GAME LOGIC (clay — reshape freely) — per-player paddle input ───────────
|
|
513
|
+
* P0 reads JOYSTICK PORT 1 (keyboard cursors fall back); P1 reads PORT 2. */
|
|
514
|
+
static void update_player(uint8_t p) {
|
|
515
|
+
uint8_t dir;
|
|
516
|
+
int16_t *py = (p == 0) ? &p1y : &p2y;
|
|
517
|
+
if (p == 0) {
|
|
518
|
+
dir = msx_read_joystick(1);
|
|
519
|
+
if (dir == STICK_CENTER) dir = msx_read_joystick(0);
|
|
520
|
+
} else {
|
|
521
|
+
dir = msx_read_joystick(2);
|
|
522
|
+
}
|
|
523
|
+
if ((dir == STICK_UP || dir == STICK_UL || dir == STICK_UR)
|
|
524
|
+
&& *py > COURT_TOP) *py -= P_SPEED;
|
|
525
|
+
if ((dir == STICK_DOWN || dir == STICK_DL || dir == STICK_DR)
|
|
526
|
+
&& *py < COURT_BOT - PADDLE_H) *py += P_SPEED;
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
/* ── GAME LOGIC (clay — reshape freely) — CPU paddle: chase the ball centre at
|
|
530
|
+
* CPU_SPEED, but ONLY while the ball is heading toward it (bdx > 0), and with a
|
|
531
|
+
* generous DEAD ZONE. Beatable by design, three ways stacked:
|
|
532
|
+
* - it does not start tracking back until the ball turns toward it, so a
|
|
533
|
+
* steep return aimed at the far rail clears the paddle before it reacts;
|
|
534
|
+
* - CPU_SPEED (1) is half the ball's horizontal speed (2), so on a steep
|
|
535
|
+
* return it simply can't cover the vertical distance in time;
|
|
536
|
+
* - the ±CPU_DEAD dead zone leaves a gap at the paddle edges.
|
|
537
|
+
* Raise CPU_SPEED toward P_SPEED, shrink CPU_DEAD, or drop the bdx>0 gate to
|
|
538
|
+
* make the CPU tougher. ── */
|
|
539
|
+
#define CPU_DEAD 6
|
|
540
|
+
static void update_cpu(void) {
|
|
108
541
|
int16_t target;
|
|
542
|
+
if (bdx <= 0) return; /* ball moving away — CPU rests */
|
|
543
|
+
target = by + BALL_H / 2 - PADDLE_H / 2;
|
|
544
|
+
if (p2y + CPU_DEAD < target && p2y < COURT_BOT - PADDLE_H) p2y += CPU_SPEED;
|
|
545
|
+
else if (p2y > target + CPU_DEAD && p2y > COURT_TOP) p2y -= CPU_SPEED;
|
|
546
|
+
}
|
|
109
547
|
|
|
548
|
+
void main(void) {
|
|
549
|
+
uint8_t t1, t2;
|
|
550
|
+
|
|
551
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
552
|
+
* Init order: set the video mode FIRST (INIGRP also clears VRAM — any
|
|
553
|
+
* upload done before it is wiped), then tiles, then sprites. The crt0's
|
|
554
|
+
* INIT contract means main() must NEVER return — the BIOS has nothing
|
|
555
|
+
* sane to fall back to — hence the for(;;) below. */
|
|
110
556
|
msx_set_screen2();
|
|
111
557
|
msx_clear_sprites();
|
|
112
558
|
load_tiles();
|
|
113
|
-
|
|
114
|
-
msx_vram_write((uint16_t)(VRAM_SPRPAT +
|
|
559
|
+
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_PADDLE * 8), spr_block, 8);
|
|
560
|
+
msx_vram_write((uint16_t)(VRAM_SPRPAT + PAT_BALL * 8), spr_ball, 8);
|
|
115
561
|
|
|
116
|
-
|
|
117
|
-
|
|
562
|
+
msx_music(0); /* the lib's demo loop also owns channel C —
|
|
563
|
+
* hand the channel to OUR tune table instead */
|
|
564
|
+
best_streak = 0; /* session record (no SAVE_RAM on this core) */
|
|
565
|
+
streak = 0;
|
|
566
|
+
rng = 0xACE1;
|
|
567
|
+
music_step = music_timer = 0;
|
|
568
|
+
sfx_a_t = sfx_b_t = 0;
|
|
569
|
+
prev_t1 = prev_t2 = 1; /* swallow a held trigger across state changes */
|
|
570
|
+
two_player = 0;
|
|
571
|
+
bx = 124; by = (COURT_TOP + COURT_BOT) / 2;
|
|
572
|
+
state = ST_TITLE;
|
|
573
|
+
paint_title();
|
|
118
574
|
|
|
119
575
|
for (;;) {
|
|
120
576
|
vsync();
|
|
577
|
+
music_tick();
|
|
578
|
+
sfx_tick();
|
|
121
579
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
p2 = msx_read_joystick(2);
|
|
132
|
-
|
|
133
|
-
if ((p1 == STICK_UP || p1 == STICK_UL || p1 == STICK_UR)
|
|
134
|
-
&& p1y > COURT_TOP) p1y -= 3;
|
|
135
|
-
if ((p1 == STICK_DOWN || p1 == STICK_DL || p1 == STICK_DR)
|
|
136
|
-
&& p1y < COURT_BOT - PADDLE_H) p1y += 3;
|
|
137
|
-
|
|
138
|
-
if (p2 != STICK_CENTER) {
|
|
139
|
-
if ((p2 == STICK_UP || p2 == STICK_UL || p2 == STICK_UR)
|
|
140
|
-
&& p2y > COURT_TOP) p2y -= 3;
|
|
141
|
-
if ((p2 == STICK_DOWN || p2 == STICK_DL || p2 == STICK_DR)
|
|
142
|
-
&& p2y < COURT_BOT - PADDLE_H) p2y += 3;
|
|
143
|
-
} else {
|
|
144
|
-
target = (int16_t)(by - PADDLE_H / 2);
|
|
145
|
-
if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 2;
|
|
146
|
-
else if (p2y > target && p2y > COURT_TOP) p2y -= 2;
|
|
580
|
+
if (state == ST_TITLE) {
|
|
581
|
+
/* ── GAME LOGIC (clay) — title: trig A = 1P vs CPU; trig B = 2P. */
|
|
582
|
+
t1 = (uint8_t)(gttrig(1) || gttrig(0));
|
|
583
|
+
t2 = (uint8_t)(gttrig(3) || gttrig(2));
|
|
584
|
+
if (t2 && !prev_t2) start_match(1);
|
|
585
|
+
else if (t1 && !prev_t1) start_match(0);
|
|
586
|
+
prev_t1 = t1; prev_t2 = t2;
|
|
587
|
+
push_sprites();
|
|
588
|
+
continue;
|
|
147
589
|
}
|
|
148
590
|
|
|
149
|
-
if (
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
if (bdx < 0
|
|
158
|
-
&& bx <= PADDLE_X1 + 8
|
|
159
|
-
&& bx + BALL_SIZE >= PADDLE_X1
|
|
160
|
-
&& by + BALL_SIZE > p1y
|
|
161
|
-
&& by < p1y + PADDLE_H) {
|
|
162
|
-
bdx = (int8_t)(-bdx);
|
|
163
|
-
bx = PADDLE_X1 + 8;
|
|
164
|
-
msx_psg_tone(0, 0x200, 10); blip = 4;
|
|
165
|
-
}
|
|
166
|
-
if (bdx > 0
|
|
167
|
-
&& bx + BALL_SIZE >= PADDLE_X2
|
|
168
|
-
&& bx <= PADDLE_X2 + 8
|
|
169
|
-
&& by + BALL_SIZE > p2y
|
|
170
|
-
&& by < p2y + PADDLE_H) {
|
|
171
|
-
bdx = (int8_t)(-bdx);
|
|
172
|
-
bx = (int16_t)(PADDLE_X2 - BALL_SIZE);
|
|
173
|
-
msx_psg_tone(0, 0x200, 10); blip = 4;
|
|
591
|
+
if (state == ST_OVER) {
|
|
592
|
+
/* Freeze the final frame; any fire button returns to the title. */
|
|
593
|
+
t1 = (uint8_t)(gttrig(1) || gttrig(0) || gttrig(2));
|
|
594
|
+
if (t1 && !prev_t1) {
|
|
595
|
+
state = ST_TITLE;
|
|
596
|
+
msx_clear_sprites();
|
|
597
|
+
two_player = 0;
|
|
598
|
+
paint_title();
|
|
174
599
|
}
|
|
600
|
+
prev_t1 = t1; prev_t2 = t1;
|
|
601
|
+
push_sprites();
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
175
604
|
|
|
176
|
-
|
|
177
|
-
|
|
605
|
+
/* ── ST_PLAY — GAME LOGIC (clay) ────────────────────────────────────
|
|
606
|
+
* Both players (or P1 + CPU) update EVERY frame — a simultaneous
|
|
607
|
+
* versus match, not alternating turns. */
|
|
608
|
+
next_rand(); /* tick the noise source every play frame */
|
|
609
|
+
|
|
610
|
+
update_player(0);
|
|
611
|
+
if (two_player) update_player(1);
|
|
612
|
+
else update_cpu();
|
|
613
|
+
|
|
614
|
+
/* Ball update (frozen during the post-point serve pause). */
|
|
615
|
+
if (serve_timer > 0) {
|
|
616
|
+
--serve_timer;
|
|
617
|
+
push_sprites();
|
|
618
|
+
continue;
|
|
619
|
+
}
|
|
620
|
+
bx = (int16_t)(bx + bdx);
|
|
621
|
+
by = (int16_t)(by + bdy);
|
|
622
|
+
|
|
623
|
+
/* Rail bounce. */
|
|
624
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = (int8_t)(-bdy); sfx_rail(); }
|
|
625
|
+
if (by + BALL_H > COURT_BOT) { by = COURT_BOT - BALL_H; bdy = (int8_t)(-bdy); sfx_rail(); }
|
|
626
|
+
|
|
627
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
628
|
+
if (bdx < 0
|
|
629
|
+
&& bx <= PADDLE_X1 + 8 && bx + BALL_W >= PADDLE_X1
|
|
630
|
+
&& by + BALL_H > p1y && by < p1y + PADDLE_H) {
|
|
631
|
+
bdx = (int8_t)(-bdx);
|
|
632
|
+
bx = PADDLE_X1 + 8;
|
|
633
|
+
deflect(p1y);
|
|
178
634
|
}
|
|
635
|
+
if (bdx > 0
|
|
636
|
+
&& bx + BALL_W >= PADDLE_X2 && bx <= PADDLE_X2 + 8
|
|
637
|
+
&& by + BALL_H > p2y && by < p2y + PADDLE_H) {
|
|
638
|
+
bdx = (int8_t)(-bdx);
|
|
639
|
+
bx = (int16_t)(PADDLE_X2 - BALL_W);
|
|
640
|
+
deflect(p2y);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
/* Off either side → point. */
|
|
644
|
+
if (bx < 4) score_point(0); /* past P1 → right side (P2/CPU) scores */
|
|
645
|
+
if (bx > 252) score_point(1); /* past P2/CPU → P1 scores */
|
|
179
646
|
|
|
180
|
-
|
|
647
|
+
push_sprites();
|
|
181
648
|
}
|
|
182
649
|
}
|