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,254 +1,672 @@
|
|
|
1
|
-
/*
|
|
2
|
-
* PC Engine "sports" — a Pong-style two-paddle scaffold.
|
|
1
|
+
/* ── main.c — PC Engine versus court game (complete example game) ────────────
|
|
3
2
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* NES/Genesis/SNES/GB/SMS sports scaffolds.
|
|
3
|
+
* SPIKE SURGE — a COMPLETE, working head-to-head court game (Pong lineage):
|
|
4
|
+
* title screen, 1P vs a beatable CPU and 2P SIMULTANEOUS VERSUS (P1 on the
|
|
5
|
+
* stock pad, P2 on the TurboTap's second pad), first-to-5 match flow with a
|
|
6
|
+
* result screen, PSG music + SFX, and an in-session record (your longest win
|
|
7
|
+
* streak vs the CPU; a bare HuCard can't save — see the record note).
|
|
10
8
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
9
|
+
* The game: two paddles, one "pulse" bouncing between them. UP/DOWN move your
|
|
10
|
+
* paddle; the pulse deflects off paddles (steeper the further from centre you
|
|
11
|
+
* parry it) and the top/bottom court rails. A pulse past either edge scores
|
|
12
|
+
* for the other side and re-serves. First to 5 takes the match.
|
|
14
13
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* -
|
|
14
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
15
|
+
* very different one. The markers tell you what's what:
|
|
16
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented PCE footgun; reshape
|
|
17
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
18
|
+
* GAME LOGIC (clay) — court art, pulse physics, CPU skill, scoring rules:
|
|
19
|
+
* reshape freely.
|
|
18
20
|
*
|
|
19
|
-
*
|
|
21
|
+
* What depends on what:
|
|
22
|
+
* pce_hw.h / pce_video.c / pce_input.c / pce_sound.c — the helper lib
|
|
23
|
+
* (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
|
|
24
|
+
* pce_video.c say which parts are load-bearing.
|
|
25
|
+
* cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
|
|
26
|
+
* (applied automatically to example projects) gives a 32KB HuCard.
|
|
27
|
+
*
|
|
28
|
+
* 2P, honestly: the stock PC Engine has ONE controller port; 2P needs a
|
|
29
|
+
* TurboTap. The geargrafx core implements the TurboTap and the romdev host
|
|
30
|
+
* now force-ENABLES it (PLATFORM_CORE_OPTIONS pce: geargrafx_turbotap), so a
|
|
31
|
+
* second pad's input reaches the game on pad slot 2 — verified by driving
|
|
32
|
+
* port-1 input and seeing P2's paddle move. So this game ships REAL
|
|
33
|
+
* simultaneous 2P versus. (On real hardware the player plugs a TurboTap and a
|
|
34
|
+
* second pad.) The CPU opponent only exists in 1P mode.
|
|
35
|
+
*
|
|
36
|
+
* Frame budget (NTSC, 60fps, 7.16MHz 65C02-class CPU): 2 paddles + 1 pulse +
|
|
37
|
+
* 2 paddle AABB tests + a 7-entry SATB copy in vblank — a tiny fraction of a
|
|
38
|
+
* frame. Plenty of headroom for fancier physics.
|
|
20
39
|
*/
|
|
21
40
|
#include <pce.h>
|
|
22
|
-
#include <
|
|
41
|
+
#include <joystick.h> /* JOY_2 + joy_read for the 2nd pad (TurboTap port 1) */
|
|
23
42
|
#include "pce_hw.h"
|
|
24
43
|
|
|
25
|
-
/*
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
#define
|
|
44
|
+
/* pce_hw.h gives us u8/u16; the pulse position + deflection math need signed
|
|
45
|
+
* types (the pulse can sit above the rim mid-bounce). cc65's int is 16-bit. */
|
|
46
|
+
typedef signed char s8;
|
|
47
|
+
typedef int s16;
|
|
48
|
+
|
|
49
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
50
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
51
|
+
#define GAME_TITLE "SPIKE SURGE"
|
|
52
|
+
|
|
53
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
54
|
+
* VRAM map (WORD addresses — the VDC is a 16-bit-word machine; an 8x8 tile is
|
|
55
|
+
* 16 words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
|
|
56
|
+
* VRAM, so lay it out ONCE and keep the SATB out of pattern space:
|
|
57
|
+
* $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
|
|
58
|
+
* $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash)
|
|
59
|
+
* $1400 court furniture tiles (floor, rail, net, HUD band)
|
|
60
|
+
* $1800 16x16 sprite cells: paddle, pulse */
|
|
61
|
+
#define BAT_VRAM 0x0000
|
|
62
|
+
#define FONT_VRAM 0x1000
|
|
63
|
+
#define FLOOR_VRAM 0x1400 /* court field (BG colour 1) */
|
|
64
|
+
#define RAIL_VRAM 0x1410 /* top/bottom rails + sidelines (BG colour 2) */
|
|
65
|
+
#define NET_VRAM 0x1420 /* dashed centre net */
|
|
66
|
+
#define BAND_VRAM 0x1430 /* flat band behind the HUD text */
|
|
67
|
+
#define PADDLE_VRAM 0x1800 /* 16x16 paddle segment */
|
|
68
|
+
#define PULSE_VRAM 0x1840 /* 16x16 pulse */
|
|
33
69
|
|
|
34
70
|
#define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
|
|
35
71
|
|
|
36
|
-
|
|
37
|
-
#define
|
|
38
|
-
#define
|
|
39
|
-
#define BALL_SIZE 12
|
|
40
|
-
#define PADDLE_X1 16
|
|
41
|
-
#define PADDLE_X2 224
|
|
72
|
+
/* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
|
|
73
|
+
#define PADDLE_PAT (PADDLE_VRAM >> 6)
|
|
74
|
+
#define PULSE_PAT (PULSE_VRAM >> 6)
|
|
42
75
|
|
|
43
|
-
/*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
76
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
77
|
+
* Court geometry + match rules. The 256x224 court is framed by rail tiles on
|
|
78
|
+
* BAT rows 2 and 27; COURT_TOP/BOT keep the pulse between them. Rows 0-1 are
|
|
79
|
+
* the HUD band. Paddles are 3 stacked 16px sprite segments (48px tall). */
|
|
80
|
+
#define COURT_TOP 24 /* first pixel row below the top rail */
|
|
81
|
+
#define COURT_BOT 216 /* first pixel row of the bottom rail */
|
|
82
|
+
#define PADDLE_H 48 /* 3 stacked 16px sprite segments */
|
|
83
|
+
#define PADDLE_X1 16 /* P1 — left side */
|
|
84
|
+
#define PADDLE_X2 224 /* P2/CPU — right side */
|
|
85
|
+
#define PULSE_SIZE 12
|
|
86
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
87
|
+
#define P1_SPEED 3 /* px/frame — both humans move at this */
|
|
88
|
+
#define CPU_SPEED 1 /* px/frame — third speed: clearly beatable */
|
|
89
|
+
#define BALL_VMAX 3 /* max |bdy| — exceeds CPU_SPEED so a steep *
|
|
90
|
+
* edge parry outruns the CPU (the win) */
|
|
91
|
+
|
|
92
|
+
/* SATB slot plan (slot order = priority): 0-2 P1 paddle, 3-5 P2 paddle, 6
|
|
93
|
+
* pulse. PAL plan: paddles on their own sprite sub-palettes so P1/P2 differ. */
|
|
94
|
+
#define SLOT_P1 0
|
|
95
|
+
#define SLOT_P2 3
|
|
96
|
+
#define SLOT_PULSE 6
|
|
97
|
+
#define PAL_P1 0
|
|
98
|
+
#define PAL_P2 1
|
|
99
|
+
#define PAL_PULSE 2
|
|
100
|
+
#define OFFSCREEN_Y 0x1F0 /* park hidden sprites below the display */
|
|
57
101
|
|
|
58
|
-
/*
|
|
59
|
-
static
|
|
60
|
-
static
|
|
102
|
+
/* ── GAME LOGIC (clay — reshape freely) ── game state ── */
|
|
103
|
+
static s16 p1y, p2y; /* paddle top Y (signed: collision math) */
|
|
104
|
+
static s16 bx, by; /* pulse top-left, pixels */
|
|
105
|
+
static s8 bdx, bdy; /* pulse velocity (px/frame) */
|
|
61
106
|
static u8 score_p1, score_p2;
|
|
62
|
-
static u8 serve_timer;
|
|
63
|
-
static u8
|
|
64
|
-
static
|
|
65
|
-
static u16
|
|
107
|
+
static u8 serve_timer; /* freeze frames between points */
|
|
108
|
+
static u8 two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
|
|
109
|
+
static u8 streak; /* current 1P-vs-CPU win streak (RAM) */
|
|
110
|
+
static u16 best_streak; /* in-session record — see end_match */
|
|
111
|
+
static u8 new_record; /* result screen shows NEW RECORD */
|
|
112
|
+
static u8 state; /* ST_TITLE / ST_PLAY / ST_OVER */
|
|
113
|
+
static u8 prev_pad; /* edge-triggered menu input */
|
|
66
114
|
static u8 sfx_timer;
|
|
115
|
+
static u8 hud_dirty;
|
|
116
|
+
|
|
117
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
118
|
+
#define ST_TITLE 0
|
|
119
|
+
#define ST_PLAY 1
|
|
120
|
+
#define ST_OVER 2
|
|
121
|
+
|
|
122
|
+
static u16 tile_buf[16]; /* scratch for one 8x8 tile */
|
|
123
|
+
static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
|
|
124
|
+
|
|
125
|
+
/* ── GAME LOGIC (clay) — 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
|
|
126
|
+
* Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
|
|
127
|
+
* them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
|
|
128
|
+
#define G_BLANK 0
|
|
129
|
+
#define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
|
|
130
|
+
#define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
|
|
131
|
+
#define G_DASH 37
|
|
132
|
+
#define NUM_GLYPHS 38
|
|
133
|
+
|
|
134
|
+
static const u8 FONT5x7[NUM_GLYPHS][7] = {
|
|
135
|
+
{0,0,0,0,0,0,0},
|
|
136
|
+
{0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
|
|
137
|
+
{0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
|
|
138
|
+
{0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
|
|
139
|
+
{0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
|
|
140
|
+
{0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
|
|
141
|
+
{0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
|
|
142
|
+
{0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
|
|
143
|
+
{0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
|
|
144
|
+
{0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
|
|
145
|
+
{0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
|
|
146
|
+
{0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
|
|
147
|
+
{0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
|
|
148
|
+
{0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
|
|
149
|
+
{0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
|
|
150
|
+
{0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
|
|
151
|
+
{0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
|
|
152
|
+
{0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
|
|
153
|
+
{0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
|
|
154
|
+
{0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
|
|
155
|
+
};
|
|
67
156
|
|
|
157
|
+
/* ── GAME LOGIC (clay) — sprite masks (16 rows × 16 bits, bit15 leftmost) ──
|
|
158
|
+
* The paddle is a solid 8px-wide bar centred in the 16px cell; the pulse is a
|
|
159
|
+
* round blip. Colour is the PALETTE, not the bits (one shape, three sub-pals). */
|
|
160
|
+
static const u16 paddle_mask[16] = {
|
|
161
|
+
0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0,
|
|
162
|
+
0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0, 0x0FF0
|
|
163
|
+
};
|
|
164
|
+
static const u16 pulse_mask[16] = {
|
|
165
|
+
0x0000, 0x0000, 0x07E0, 0x0FF0, 0x1FF8, 0x1FF8, 0x3FFC, 0x3FFC,
|
|
166
|
+
0x3FFC, 0x3FFC, 0x1FF8, 0x1FF8, 0x0FF0, 0x07E0, 0x0000, 0x0000
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
/* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
|
|
68
170
|
static void make_solid_tile(u16 *t, u8 ci) {
|
|
69
171
|
u8 r;
|
|
70
172
|
u8 p0 = (ci & 1) ? 0xFF : 0x00;
|
|
71
173
|
u8 p1 = (ci & 2) ? 0xFF : 0x00;
|
|
72
|
-
u8 p2 = (ci & 4) ? 0xFF : 0x00;
|
|
73
|
-
u8 p3 = (ci & 8) ? 0xFF : 0x00;
|
|
74
174
|
for (r = 0; r < 8; ++r) {
|
|
75
175
|
t[r] = (u16)(p0 | (p1 << 8));
|
|
76
|
-
t[r + 8] =
|
|
176
|
+
t[r + 8] = 0;
|
|
77
177
|
}
|
|
78
178
|
}
|
|
79
179
|
|
|
80
|
-
/* net tile:
|
|
180
|
+
/* net tile: court floor (colour 1) with a colour-2 dashed centre column */
|
|
81
181
|
static void make_net_tile(u16 *t) {
|
|
82
182
|
u8 r;
|
|
83
183
|
for (r = 0; r < 8; ++r) {
|
|
84
|
-
u8 dash = (r < 5);
|
|
85
|
-
u8 p1 = dash ? 0x18 : 0x00;
|
|
86
|
-
t[r] = (u16)(0x00FF | (p1 << 8));
|
|
184
|
+
u8 dash = (r < 5); /* dashed: top 5 rows of each tile */
|
|
185
|
+
u8 p1 = dash ? 0x18 : 0x00; /* centre 2 px -> colour 2 (plane1) */
|
|
186
|
+
t[r] = (u16)(0x00FF | (p1 << 8)); /* plane0 full (floor) + dash */
|
|
87
187
|
t[r + 8] = 0x0000;
|
|
88
188
|
}
|
|
89
189
|
}
|
|
90
190
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
for (r = 0; r < 64; ++r) spr_buf[r] = 0;
|
|
94
|
-
/* a solid 8px-wide vertical bar centred in the 16px cell, colour 1 */
|
|
95
|
-
for (r = 0; r < 16; ++r) spr_buf[r] = 0x0FF0;
|
|
96
|
-
load_tiles(PADDLE_VRAM, spr_buf, 64);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
static void make_ball_sprite(void) {
|
|
100
|
-
static const u16 ball[16] = {
|
|
101
|
-
0x0000, 0x0000, 0x07E0, 0x0FF0, 0x1FF8, 0x1FF8, 0x3FFC, 0x3FFC,
|
|
102
|
-
0x3FFC, 0x3FFC, 0x1FF8, 0x1FF8, 0x0FF0, 0x07E0, 0x0000, 0x0000
|
|
103
|
-
};
|
|
191
|
+
/* one-colour 16x16 sprite cell from a 16-row mask (colour = plane0 → index 1) */
|
|
192
|
+
static void make_sprite16(u16 vram, const u16 *mask) {
|
|
104
193
|
u8 r;
|
|
105
194
|
for (r = 0; r < 64; ++r) spr_buf[r] = 0;
|
|
106
|
-
for (r = 0; r < 16; ++r) spr_buf[r] =
|
|
107
|
-
load_tiles(
|
|
195
|
+
for (r = 0; r < 16; ++r) spr_buf[r] = mask[r]; /* plane 0 → colour 1 */
|
|
196
|
+
load_tiles(vram, spr_buf, 64);
|
|
108
197
|
}
|
|
109
198
|
|
|
110
199
|
static void upload_font(void) {
|
|
111
|
-
u8 g, row, bits,
|
|
200
|
+
u8 g, row, bits, px;
|
|
112
201
|
for (g = 0; g < NUM_GLYPHS; ++g) {
|
|
113
202
|
for (row = 0; row < 16; ++row) tile_buf[row] = 0;
|
|
114
203
|
for (row = 0; row < 7; ++row) {
|
|
115
204
|
bits = FONT5x7[g][row];
|
|
116
|
-
|
|
117
|
-
if (bits & 0x10)
|
|
118
|
-
if (bits & 0x08)
|
|
119
|
-
if (bits & 0x04)
|
|
120
|
-
if (bits & 0x02)
|
|
121
|
-
if (bits & 0x01)
|
|
122
|
-
tile_buf[row] = (u16)
|
|
205
|
+
px = 0;
|
|
206
|
+
if (bits & 0x10) px |= 0x40;
|
|
207
|
+
if (bits & 0x08) px |= 0x20;
|
|
208
|
+
if (bits & 0x04) px |= 0x10;
|
|
209
|
+
if (bits & 0x02) px |= 0x08;
|
|
210
|
+
if (bits & 0x01) px |= 0x04;
|
|
211
|
+
tile_buf[row] = (u16)px;
|
|
123
212
|
}
|
|
124
213
|
load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
|
|
125
214
|
}
|
|
126
215
|
}
|
|
127
216
|
|
|
128
|
-
static void
|
|
217
|
+
static void upload_art(void) {
|
|
218
|
+
upload_font();
|
|
219
|
+
make_solid_tile(tile_buf, 1); load_tiles(FLOOR_VRAM, tile_buf, 16);
|
|
220
|
+
make_solid_tile(tile_buf, 2); load_tiles(RAIL_VRAM, tile_buf, 16);
|
|
221
|
+
make_net_tile(tile_buf); load_tiles(NET_VRAM, tile_buf, 16);
|
|
222
|
+
make_solid_tile(tile_buf, 3); load_tiles(BAND_VRAM, tile_buf, 16);
|
|
223
|
+
make_sprite16(PADDLE_VRAM, paddle_mask);
|
|
224
|
+
make_sprite16(PULSE_VRAM, pulse_mask);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/* ── GAME LOGIC (clay) — BAT text + court paint ──────────────────────────── */
|
|
228
|
+
static void put_glyph(u8 col, u8 row, u8 glyph) {
|
|
229
|
+
u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
|
|
230
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
231
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
232
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
static void put_tile(u8 col, u8 row, u16 e) {
|
|
236
|
+
vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
|
|
237
|
+
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
238
|
+
VDC_DATA_HI = (u8)(e >> 8);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
static void draw_text(u8 col, u8 row, const char *s) {
|
|
242
|
+
u8 c;
|
|
243
|
+
while ((c = (u8)*s++) != 0) {
|
|
244
|
+
u8 g = G_BLANK;
|
|
245
|
+
if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
|
|
246
|
+
else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
|
|
247
|
+
else if (c == '-') g = G_DASH;
|
|
248
|
+
put_glyph(col++, row, g);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
static void draw_num5(u8 col, u8 row, u16 v) {
|
|
253
|
+
u8 i, d[5];
|
|
254
|
+
for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
|
|
255
|
+
for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
259
|
+
* WHOLE-SCREEN BAT PAINT — the PCE's bandwidth (the inverse of the NES vblank
|
|
260
|
+
* famine; the puzzle template's match-3 board exploits the same thing). The
|
|
261
|
+
* court is just BG tiles; when a screen changes we rewrite ALL 32x32 BAT
|
|
262
|
+
* entries — 1024 word writes straight at the VDC's VWR port. The whole map
|
|
263
|
+
* streams in well under a vblank, so this game NEVER touches the tilemap inside
|
|
264
|
+
* the frame loop (only on a state change: title → play → result). Two rules:
|
|
265
|
+
* - do the streaming with the address latch armed by vram_set_write_addr(),
|
|
266
|
+
* which auto-increments as we feed VDC_DATA_LO/HI;
|
|
267
|
+
* - keep the SATB DMA (satb_dma) after the BAT writes — both share the VDC.
|
|
268
|
+
*
|
|
269
|
+
* requires: BAT 32x32 (vdc_init's MWR). */
|
|
270
|
+
static void paint_court(void) {
|
|
129
271
|
u8 r, c;
|
|
130
|
-
u16
|
|
131
|
-
u16
|
|
132
|
-
u16
|
|
272
|
+
u16 floor = BAT_ENTRY(0, FLOOR_VRAM);
|
|
273
|
+
u16 rail = BAT_ENTRY(0, RAIL_VRAM);
|
|
274
|
+
u16 net = BAT_ENTRY(0, NET_VRAM);
|
|
275
|
+
u16 band = BAT_ENTRY(0, BAND_VRAM);
|
|
133
276
|
u16 e;
|
|
134
|
-
for (r = 0; r < 32; ++
|
|
277
|
+
for (r = 0; r < 32; r++) {
|
|
135
278
|
vram_set_write_addr((u16)(BAT_VRAM + r * 32));
|
|
136
|
-
for (c = 0; c < 32; ++
|
|
137
|
-
if (r
|
|
138
|
-
else if (
|
|
139
|
-
else if (c ==
|
|
140
|
-
else
|
|
279
|
+
for (c = 0; c < 32; c++) {
|
|
280
|
+
if (r < 2) e = band; /* HUD band */
|
|
281
|
+
else if (r == 2 || r == 27) e = rail; /* top/bottom rails */
|
|
282
|
+
else if (c == 1 || c == 30) e = rail; /* sidelines */
|
|
283
|
+
else if (c == 16 && r > 2 && r < 27) e = net; /* centre net */
|
|
284
|
+
else e = floor; /* court surface */
|
|
141
285
|
VDC_DATA_LO = (u8)(e & 0xFF);
|
|
142
286
|
VDC_DATA_HI = (u8)(e >> 8);
|
|
143
287
|
}
|
|
144
288
|
}
|
|
145
289
|
}
|
|
146
290
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
291
|
+
/* HUD (row 0): "P1 n BEST nnnnn CPU n" (or "P2" in 2P mode). */
|
|
292
|
+
static void draw_hud(void) {
|
|
293
|
+
u8 i;
|
|
294
|
+
/* clear the HUD text row before repainting (band tile under the glyphs) */
|
|
295
|
+
for (i = 0; i < 32; i++) put_tile(i, 0, BAT_ENTRY(0, BAND_VRAM));
|
|
296
|
+
if (state == ST_TITLE) {
|
|
297
|
+
draw_text(11, 0, "BEST");
|
|
298
|
+
draw_num5(16, 0, best_streak);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
draw_text(1, 0, "P1");
|
|
302
|
+
put_glyph(4, 0, (u8)(G_DIGIT + score_p1));
|
|
303
|
+
draw_text(11, 0, "BEST");
|
|
304
|
+
draw_num5(16, 0, best_streak);
|
|
305
|
+
draw_text(24, 0, two_player ? "P2" : "CPU");
|
|
306
|
+
put_glyph(28, 0, (u8)(G_DIGIT + score_p2));
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/* ── HARDWARE TRUTH: a bare HuCard CANNOT save the win streak (in-session) ──
|
|
310
|
+
* This was researched and corrected: earlier versions wrote the longest 1P win
|
|
311
|
+
* streak to BRAM ("backup RAM", bank $F7) and claimed it persisted across power
|
|
312
|
+
* cycles. That is NOT honest for a HuCard game. On REAL hardware a plain HuCard
|
|
313
|
+
* plugged into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM
|
|
314
|
+
* exists ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
|
|
315
|
+
* supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
|
|
316
|
+
* commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
|
|
317
|
+
* "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
|
|
318
|
+
* emulator like geargrafx exposes BRAM unconditionally, so the old code
|
|
319
|
+
* "worked" in emulation in a way the real machine never would.
|
|
320
|
+
*
|
|
321
|
+
* The record we track is still the longest 1P win streak vs the CPU (a raw
|
|
322
|
+
* hi-score is meaningless when every match ends 5-x; 2P matches never touch
|
|
323
|
+
* it) — but IN-SESSION only, resetting to 0 on a cold boot like the honest
|
|
324
|
+
* 2600/Lynx examples. To ACTUALLY persist on real hardware you would target a
|
|
325
|
+
* peripheral (BRAM behind a detect, or a CD-ROM² build) — a real-hardware
|
|
326
|
+
* feature, not a property of the cartridge. */
|
|
327
|
+
static u16 record_load(void) {
|
|
328
|
+
return 0; /* cold boot: no persistence on a bare HuCard */
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
static void record_save(u16 v) {
|
|
332
|
+
(void)v; /* in-session only — nowhere to persist on real HW */
|
|
152
333
|
}
|
|
153
334
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
335
|
+
/* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
|
|
336
|
+
* PSG channel plan: 5 = melody, 4 = bass, 2/3 = SFX (tones cut by sfx_timer).
|
|
337
|
+
* PCE frequency regs are DIVIDERS: pitch ≈ 3.58MHz / (32 × value), so a
|
|
338
|
+
* BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
|
|
339
|
+
enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
|
|
340
|
+
static const u16 NOTE_DIV[17] = {
|
|
341
|
+
0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
|
|
342
|
+
};
|
|
343
|
+
/* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
|
|
344
|
+
static const u8 MEL_TITLE[16] = { G4,B4,D5,G4, E4,G4,B4,E4, A4,C5,E5,A4, D5,B4,G4,D4 };
|
|
345
|
+
static const u8 BAS_TITLE[8] = { G3,G3, C3,C3, A2N,A2N, D4,D4 };
|
|
346
|
+
static const u8 MEL_PLAY[16] = { E4,G4,E4,A4, G4,E4,D4,E4, C4,E4,G4,C5, B4,G4,E4,R };
|
|
347
|
+
static const u8 BAS_PLAY[8] = { A2N,A2N, C3,C3, G3,G3, A2N,A2N };
|
|
348
|
+
static const u8 MEL_OVER[16] = { C5,R,G4,R, E4,R,C4,R, D4,R,E4,R, G4,R,R,R };
|
|
349
|
+
|
|
350
|
+
static u8 music_song; /* reuses the ST_* ids */
|
|
351
|
+
static u8 music_step, music_timer, music_done;
|
|
352
|
+
|
|
353
|
+
static void music_set(u8 song) {
|
|
354
|
+
music_song = song;
|
|
355
|
+
music_step = 0;
|
|
356
|
+
music_timer = 0;
|
|
357
|
+
music_done = 0;
|
|
358
|
+
psg_off(4);
|
|
359
|
+
psg_off(5);
|
|
157
360
|
}
|
|
158
361
|
|
|
362
|
+
static void music_tick(void) {
|
|
363
|
+
const u8 *mel;
|
|
364
|
+
u8 n;
|
|
365
|
+
if (music_done) return;
|
|
366
|
+
if (music_timer == 0) {
|
|
367
|
+
mel = (music_song == ST_PLAY) ? MEL_PLAY
|
|
368
|
+
: (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
|
|
369
|
+
n = mel[music_step & 15];
|
|
370
|
+
if (n != R) psg_tone(5, NOTE_DIV[n], 26);
|
|
371
|
+
else psg_off(5);
|
|
372
|
+
if (music_song != ST_OVER) { /* the result jingle has no bass */
|
|
373
|
+
n = ((music_step & 1) == 0)
|
|
374
|
+
? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
|
|
375
|
+
: BAS_TITLE[(music_step >> 1) & 7])
|
|
376
|
+
: R;
|
|
377
|
+
if (n != R) psg_tone(4, NOTE_DIV[n], 20);
|
|
378
|
+
}
|
|
379
|
+
++music_step;
|
|
380
|
+
if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
|
|
381
|
+
music_done = 1;
|
|
382
|
+
psg_off(4);
|
|
383
|
+
psg_off(5);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
++music_timer;
|
|
387
|
+
if (music_timer >= 9) music_timer = 0;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* short SFX on channels 2/3, auto-cut by sfx_timer */
|
|
391
|
+
static void sfx(u8 chan, u16 freq, u8 frames) {
|
|
392
|
+
psg_tone(chan, freq, 31);
|
|
393
|
+
if (frames > sfx_timer) sfx_timer = frames;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG ─────────────────────────────────────
|
|
397
|
+
* A versus game NEEDS this: the PCE is fully deterministic, so without a noise
|
|
398
|
+
* source two fixed strategies lock into an infinite rally loop (the exact same
|
|
399
|
+
* cycle, forever — a match that never ends). random8() is ticked once per play
|
|
400
|
+
* frame so identical game states a few seconds apart still diverge, and every
|
|
401
|
+
* paddle return adds a ±1 "spin" (see deflect). This is what makes an idle
|
|
402
|
+
* 1P-vs-CPU match provably END. */
|
|
403
|
+
static u16 rng = 0xC0A7;
|
|
404
|
+
static u8 random8(void) {
|
|
405
|
+
u16 r = rng;
|
|
406
|
+
r ^= r << 7;
|
|
407
|
+
r ^= r >> 9;
|
|
408
|
+
r ^= r << 8;
|
|
409
|
+
rng = r;
|
|
410
|
+
return (u8)r;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/* ── GAME LOGIC (clay) — serve: pulse to centre, toward the chosen side.
|
|
414
|
+
* The serve angle takes a PRNG bit (not a fixed alternation) — one more place
|
|
415
|
+
* determinism is broken so idle matches can't settle into a cycle. */
|
|
159
416
|
static void serve_ball(u8 to_left) {
|
|
160
|
-
bx = 120;
|
|
417
|
+
bx = 120;
|
|
418
|
+
by = (COURT_TOP + COURT_BOT) / 2;
|
|
161
419
|
bdx = to_left ? -2 : 2;
|
|
162
|
-
bdy = ((
|
|
163
|
-
serve_timer = 40;
|
|
420
|
+
bdy = (random8() & 1) ? -2 : 2;
|
|
421
|
+
serve_timer = 40; /* breather between points */
|
|
164
422
|
}
|
|
165
423
|
|
|
166
|
-
|
|
424
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the pulse struck.
|
|
425
|
+
* Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU moves at 2 too,
|
|
426
|
+
* but the random spin + steep edge parries are exactly how a human beats it. */
|
|
427
|
+
static void deflect(s16 paddle_y) {
|
|
428
|
+
s16 rel = (by + PULSE_SIZE / 2) - (paddle_y + PADDLE_H / 2);
|
|
429
|
+
bdy = (s8)(rel >> 3); /* edge parry → steep (up to ±3) */
|
|
430
|
+
bdy += (s8)((random8() & 2) - 1); /* spin: -1 or +1 */
|
|
431
|
+
if (bdy > BALL_VMAX) bdy = BALL_VMAX;
|
|
432
|
+
if (bdy < -BALL_VMAX) bdy = -BALL_VMAX;
|
|
433
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat pulse */
|
|
434
|
+
sfx(2, 0x200, 4);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
/* ── GAME LOGIC (clay) — screen painters (full BAT repaint per state change) ── */
|
|
438
|
+
static void paint_title(void) {
|
|
439
|
+
paint_court();
|
|
440
|
+
draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 8, GAME_TITLE);
|
|
441
|
+
draw_text(10, 13, "1P VS CPU - I");
|
|
442
|
+
draw_text(10, 15, "2P VERSUS - II");
|
|
443
|
+
draw_text(11, 19, "FIRST TO 5");
|
|
444
|
+
draw_text(5, 22, "UP DOWN PARRY THE PULSE");
|
|
445
|
+
draw_hud();
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
static void paint_play(void) {
|
|
449
|
+
paint_court();
|
|
450
|
+
draw_hud();
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
static void paint_over(void) {
|
|
454
|
+
paint_court();
|
|
455
|
+
if (score_p1 >= WIN_SCORE)
|
|
456
|
+
draw_text(13, 8, "P1 WINS");
|
|
457
|
+
else
|
|
458
|
+
draw_text(12, 8, two_player ? "P2 WINS" : "CPU WINS");
|
|
459
|
+
put_glyph(14, 11, (u8)(G_DIGIT + score_p1));
|
|
460
|
+
draw_text(16, 11, "-");
|
|
461
|
+
put_glyph(18, 11, (u8)(G_DIGIT + score_p2));
|
|
462
|
+
if (new_record) draw_text(11, 14, "NEW RECORD");
|
|
463
|
+
draw_text(8, 21, "RUN - TITLE");
|
|
464
|
+
draw_hud();
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/* ── GAME LOGIC (clay) — start a match ── */
|
|
468
|
+
static void start_match(u8 players) {
|
|
469
|
+
two_player = players;
|
|
470
|
+
p1y = (COURT_TOP + COURT_BOT) / 2 - PADDLE_H / 2;
|
|
471
|
+
p2y = p1y;
|
|
472
|
+
score_p1 = 0;
|
|
473
|
+
score_p2 = 0;
|
|
474
|
+
new_record = 0;
|
|
475
|
+
serve_ball(0);
|
|
476
|
+
state = ST_PLAY;
|
|
477
|
+
paint_play();
|
|
478
|
+
music_set(ST_PLAY);
|
|
479
|
+
sfx(2, 0x180, 6); /* start blip */
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/* ── GAME LOGIC (clay) — match over: result + record bookkeeping ── */
|
|
483
|
+
static void end_match(void) {
|
|
484
|
+
if (score_p1 >= WIN_SCORE && !two_player) {
|
|
485
|
+
++streak;
|
|
486
|
+
if (streak > best_streak) {
|
|
487
|
+
best_streak = streak;
|
|
488
|
+
new_record = 1;
|
|
489
|
+
record_save(best_streak); /* in-session only (no save on a bare HuCard) */
|
|
490
|
+
}
|
|
491
|
+
} else if (!two_player) {
|
|
492
|
+
streak = 0; /* the streak dies with the loss */
|
|
493
|
+
}
|
|
494
|
+
state = ST_OVER;
|
|
495
|
+
prev_pad = 0xFF; /* require a fresh press on the result */
|
|
496
|
+
/* End-of-match whistle: two quick descending tones. */
|
|
497
|
+
sfx(2, 0x300, 8);
|
|
498
|
+
sfx(3, 0x500, 14);
|
|
499
|
+
paint_over();
|
|
500
|
+
music_set(ST_OVER);
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
504
|
+
static void score_point(u8 for_p1) {
|
|
505
|
+
if (for_p1) ++score_p1; else ++score_p2;
|
|
506
|
+
sfx(3, 0x100, 8);
|
|
507
|
+
hud_dirty = 1;
|
|
508
|
+
if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
|
|
509
|
+
else serve_ball(for_p1); /* winner of the point receives */
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
/* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
|
|
513
|
+
* Fixed SATB slots: 0-2 P1 paddle, 3-5 P2 paddle, 6 pulse. Paddles freeze on
|
|
514
|
+
* the result screen; the pulse only shows in play. Hidden slots park below the
|
|
515
|
+
* display at OFFSCREEN_Y. */
|
|
516
|
+
static void push_sprites(void) {
|
|
167
517
|
u8 i;
|
|
518
|
+
u8 actors = (state != ST_TITLE); /* paddles show in play + result */
|
|
519
|
+
u8 pulse_on = (state == ST_PLAY);
|
|
520
|
+
for (i = 0; i < 3; i++) {
|
|
521
|
+
set_sprite((u8)(SLOT_P1 + i), PADDLE_X1,
|
|
522
|
+
actors ? (u16)(p1y + (s16)(i * 16)) : OFFSCREEN_Y,
|
|
523
|
+
PADDLE_PAT, PAL_P1);
|
|
524
|
+
set_sprite((u8)(SLOT_P2 + i), PADDLE_X2,
|
|
525
|
+
actors ? (u16)(p2y + (s16)(i * 16)) : OFFSCREEN_Y,
|
|
526
|
+
PADDLE_PAT, PAL_P2);
|
|
527
|
+
}
|
|
528
|
+
set_sprite(SLOT_PULSE, (u16)bx, pulse_on ? (u16)by : OFFSCREEN_Y,
|
|
529
|
+
PULSE_PAT, PAL_PULSE);
|
|
530
|
+
}
|
|
168
531
|
|
|
169
|
-
|
|
532
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
533
|
+
* 2P INPUT via the TurboTap. pce_joy_read() reads pad 1 (slot 0). For pad 2 we
|
|
534
|
+
* read cc65's JOY_2 directly and translate it to the same clean PCE bitmask
|
|
535
|
+
* pce_input.c builds for pad 1. The host force-enables the TurboTap core
|
|
536
|
+
* option, so JOY_2 carries real port-1 input; without that override port 1 is
|
|
537
|
+
* dead and this would silently fall back to 1P. ── */
|
|
538
|
+
static u8 read_pad2(void) {
|
|
539
|
+
u8 raw = joy_read(JOY_2);
|
|
540
|
+
u8 m = 0;
|
|
541
|
+
if (JOY_UP(raw)) m |= PCE_JOY_UP;
|
|
542
|
+
if (JOY_DOWN(raw)) m |= PCE_JOY_DOWN;
|
|
543
|
+
if (JOY_LEFT(raw)) m |= PCE_JOY_LEFT;
|
|
544
|
+
if (JOY_RIGHT(raw)) m |= PCE_JOY_RIGHT;
|
|
545
|
+
if (JOY_BTN_1(raw)) m |= PCE_JOY_I;
|
|
546
|
+
if (JOY_BTN_2(raw)) m |= PCE_JOY_II;
|
|
547
|
+
if (JOY_BTN_3(raw)) m |= PCE_JOY_SELECT;
|
|
548
|
+
if (JOY_BTN_4(raw)) m |= PCE_JOY_RUN;
|
|
549
|
+
return m;
|
|
550
|
+
}
|
|
170
551
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
vce_set_color(1, PCE_RGB(0, 4, 1)); /* BG c1: court green */
|
|
174
|
-
vce_set_color(2, PCE_RGB(7, 7, 7)); /* BG c2: white lines/net/digit */
|
|
175
|
-
vce_set_color(256, PCE_RGB(0, 0, 0)); /* spr pal0 transparent */
|
|
176
|
-
vce_set_color(257, PCE_RGB(7, 7, 7)); /* spr pal0 c1: white paddle */
|
|
177
|
-
vce_set_color(272, PCE_RGB(0, 0, 0)); /* spr pal1 transparent */
|
|
178
|
-
vce_set_color(273, PCE_RGB(7, 7, 0)); /* spr pal1 c1: yellow ball */
|
|
552
|
+
void main(void) {
|
|
553
|
+
u8 pad1, pad2, newpad;
|
|
179
554
|
|
|
180
|
-
|
|
181
|
-
make_solid_tile(tile_buf, 1); load_tiles(GREEN_VRAM, tile_buf, 16);
|
|
182
|
-
make_solid_tile(tile_buf, 2); load_tiles(LINE_VRAM, tile_buf, 16);
|
|
183
|
-
make_net_tile(tile_buf); load_tiles(NET_VRAM, tile_buf, 16);
|
|
184
|
-
make_paddle_sprite();
|
|
185
|
-
make_ball_sprite();
|
|
555
|
+
_pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
|
|
186
556
|
|
|
187
|
-
|
|
557
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
558
|
+
* Init order: palette → VRAM uploads → BAT paint → joypad → display ON.
|
|
559
|
+
* disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
|
|
560
|
+
* never returns and the game freezes on its first frame. */
|
|
561
|
+
/* BG sub-pal 0: court (floor/rail/net/band). BG sub-pal 1: HUD/text white. */
|
|
562
|
+
vce_set_color(0, PCE_RGB(0, 1, 0)); /* backdrop: dark green */
|
|
563
|
+
vce_set_color(1, PCE_RGB(0, 4, 1)); /* court floor green */
|
|
564
|
+
vce_set_color(2, PCE_RGB(7, 7, 7)); /* rails / net: white */
|
|
565
|
+
vce_set_color(3, PCE_RGB(1, 2, 1)); /* HUD band: dark green-grey */
|
|
566
|
+
vce_set_color(17, PCE_RGB(7, 7, 7)); /* pal1 text: white */
|
|
567
|
+
/* sprite sub-palettes (256 + pal*16 + index) — P1 cyan, P2 red, pulse
|
|
568
|
+
* yellow, each on its own sub-palette so the paddles read as two sides. */
|
|
569
|
+
vce_set_color(256 + 0 * 16 + 1, PCE_RGB(2, 6, 7)); /* spr pal0 c1: P1 cyan */
|
|
570
|
+
vce_set_color(256 + 1 * 16 + 1, PCE_RGB(7, 1, 1)); /* spr pal1 c1: P2 red */
|
|
571
|
+
vce_set_color(256 + 2 * 16 + 1, PCE_RGB(7, 7, 0)); /* spr pal2 c1: pulse amber */
|
|
188
572
|
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
573
|
+
upload_art();
|
|
574
|
+
|
|
575
|
+
best_streak = record_load(); /* always 0 — no persistence on a bare HuCard */
|
|
576
|
+
streak = 0;
|
|
577
|
+
state = ST_TITLE;
|
|
578
|
+
paint_title();
|
|
579
|
+
music_set(ST_TITLE);
|
|
194
580
|
|
|
195
581
|
pce_joy_init();
|
|
196
582
|
disp_enable();
|
|
197
583
|
|
|
198
584
|
for (;;) {
|
|
199
|
-
u8 slot;
|
|
200
|
-
int16_t target;
|
|
201
585
|
waitvsync();
|
|
202
586
|
|
|
203
|
-
/*
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
set_sprite(slot++, PADDLE_X1, (u16)(p1y + i * 16), PADDLE_VRAM >> 6, 0);
|
|
207
|
-
for (i = 0; i < 3; ++i)
|
|
208
|
-
set_sprite(slot++, PADDLE_X2, (u16)(p2y + i * 16), PADDLE_VRAM >> 6, 0);
|
|
209
|
-
set_sprite(slot++, (u16)bx, (u16)by, BALL_VRAM >> 6, 1);
|
|
587
|
+
/* ── vblank work first: queued HUD repaint + sprites + SATB DMA ── */
|
|
588
|
+
if (hud_dirty) { draw_hud(); hud_dirty = 0; }
|
|
589
|
+
push_sprites();
|
|
210
590
|
satb_dma();
|
|
211
591
|
|
|
212
|
-
|
|
592
|
+
music_tick();
|
|
593
|
+
if (sfx_timer) {
|
|
594
|
+
--sfx_timer;
|
|
595
|
+
if (sfx_timer == 0) { psg_off(2); psg_off(3); }
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/* ── 2P input via the TurboTap (see read_pad2's idiom note). In 2P
|
|
599
|
+
* versus BOTH play simultaneously, so we read BOTH pads every frame;
|
|
600
|
+
* on the menus only pad 1 matters. ── */
|
|
601
|
+
pad1 = pce_joy_read();
|
|
602
|
+
pad2 = (state == ST_PLAY && two_player) ? read_pad2() : 0;
|
|
603
|
+
|
|
604
|
+
if (state == ST_TITLE) {
|
|
605
|
+
newpad = (u8)(pad1 & ~prev_pad);
|
|
606
|
+
prev_pad = pad1;
|
|
607
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_match(0);
|
|
608
|
+
else if (newpad & PCE_JOY_II) start_match(1);
|
|
609
|
+
continue;
|
|
610
|
+
}
|
|
611
|
+
if (state == ST_OVER) {
|
|
612
|
+
newpad = (u8)(pad1 & ~prev_pad);
|
|
613
|
+
prev_pad = pad1;
|
|
614
|
+
if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
|
|
615
|
+
state = ST_TITLE;
|
|
616
|
+
paint_title();
|
|
617
|
+
music_set(ST_TITLE);
|
|
618
|
+
}
|
|
619
|
+
continue;
|
|
620
|
+
}
|
|
213
621
|
|
|
214
|
-
/*
|
|
215
|
-
|
|
216
|
-
|
|
622
|
+
/* ── ST_PLAY ────────────────────────────────────────────────────────
|
|
623
|
+
* tick the noise source every play frame so idle matches diverge. */
|
|
624
|
+
random8();
|
|
217
625
|
|
|
218
|
-
/*
|
|
219
|
-
|
|
220
|
-
if (
|
|
221
|
-
else if (p2y > target && p2y > COURT_TOP) p2y -= 2;
|
|
626
|
+
/* P1 — pad 1 (port 0), UP/DOWN. */
|
|
627
|
+
if ((pad1 & PCE_JOY_UP) && p1y > COURT_TOP) p1y -= P1_SPEED;
|
|
628
|
+
if ((pad1 & PCE_JOY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += P1_SPEED;
|
|
222
629
|
|
|
223
|
-
if (
|
|
224
|
-
|
|
630
|
+
if (two_player) {
|
|
631
|
+
/* P2 — TurboTap pad 2 (port 1), same speed: a fair versus match. */
|
|
632
|
+
if ((pad2 & PCE_JOY_UP) && p2y > COURT_TOP) p2y -= P1_SPEED;
|
|
633
|
+
if ((pad2 & PCE_JOY_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += P1_SPEED;
|
|
225
634
|
} else {
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
if (
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
if (bdx < 0 && bx <= PADDLE_X1 + 12 && bx + BALL_SIZE >= PADDLE_X1 &&
|
|
234
|
-
by + BALL_SIZE > p1y && by < p1y + PADDLE_H) {
|
|
235
|
-
bdx = (int8_t)(-bdx);
|
|
236
|
-
bx = PADDLE_X1 + 12;
|
|
237
|
-
psg_tone(0, 0x200, 22); sfx_timer = 4;
|
|
238
|
-
}
|
|
239
|
-
/* right paddle */
|
|
240
|
-
if (bdx > 0 && bx + BALL_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + 12 &&
|
|
241
|
-
by + BALL_SIZE > p2y && by < p2y + PADDLE_H) {
|
|
242
|
-
bdx = (int8_t)(-bdx);
|
|
243
|
-
bx = (int16_t)(PADDLE_X2 - BALL_SIZE);
|
|
244
|
-
psg_tone(0, 0x200, 22); sfx_timer = 4;
|
|
245
|
-
}
|
|
635
|
+
/* CPU — chases the pulse centre at a third of the player speed
|
|
636
|
+
* with a small dead zone. Beatable by design: a steep edge parry
|
|
637
|
+
* (|bdy| up to 3) outruns the CPU's 1px/frame tracking. */
|
|
638
|
+
s16 target = by + PULSE_SIZE / 2 - PADDLE_H / 2;
|
|
639
|
+
if (p2y + 2 < target && p2y < COURT_BOT - PADDLE_H) p2y += CPU_SPEED;
|
|
640
|
+
else if (p2y > target + 2 && p2y > COURT_TOP) p2y -= CPU_SPEED;
|
|
641
|
+
}
|
|
246
642
|
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
643
|
+
/* Pulse update (frozen during the post-point serve pause). */
|
|
644
|
+
if (serve_timer > 0) { --serve_timer; continue; }
|
|
645
|
+
bx = (s16)(bx + bdx);
|
|
646
|
+
by = (s16)(by + bdy);
|
|
647
|
+
|
|
648
|
+
/* Rail bounce. */
|
|
649
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = (s8)(-bdy); sfx(3, 0x280, 4); }
|
|
650
|
+
if (by + PULSE_SIZE > COURT_BOT) { by = (s16)(COURT_BOT - PULSE_SIZE); bdy = (s8)(-bdy); sfx(3, 0x280, 4); }
|
|
651
|
+
|
|
652
|
+
/* Paddle collisions (direction-gated so the pulse can't double-hit). */
|
|
653
|
+
if (bdx < 0
|
|
654
|
+
&& bx <= PADDLE_X1 + 12 && bx + PULSE_SIZE >= PADDLE_X1
|
|
655
|
+
&& by + PULSE_SIZE > p1y && by < p1y + PADDLE_H) {
|
|
656
|
+
bdx = (s8)(-bdx);
|
|
657
|
+
bx = PADDLE_X1 + 12;
|
|
658
|
+
deflect(p1y);
|
|
659
|
+
}
|
|
660
|
+
if (bdx > 0
|
|
661
|
+
&& bx + PULSE_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + 12
|
|
662
|
+
&& by + PULSE_SIZE > p2y && by < p2y + PADDLE_H) {
|
|
663
|
+
bdx = (s8)(-bdx);
|
|
664
|
+
bx = (s16)(PADDLE_X2 - PULSE_SIZE);
|
|
665
|
+
deflect(p2y);
|
|
250
666
|
}
|
|
251
667
|
|
|
252
|
-
|
|
668
|
+
/* Off either side → point. */
|
|
669
|
+
if (bx < 2) score_point(0); /* past P1 → right side scores */
|
|
670
|
+
if (bx > 246) score_point(1); /* past P2 → P1 scores */
|
|
253
671
|
}
|
|
254
672
|
}
|