romdevtools 0.28.0 → 0.30.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 +53 -43
- package/CHANGELOG.md +91 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +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 +84 -8
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +131 -24
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +53 -10
- 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 +32 -3
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,193 +1,901 @@
|
|
|
1
|
-
/* ── sports.c — Game Boy
|
|
1
|
+
/* ── sports.c — HUE HUSTLE: Game Boy Color versus court game (complete example) ──
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
3
|
+
* A COMPLETE, working game — press-start title, 1P vs a beatable CPU on a
|
|
4
|
+
* court (Pong lineage), first-to-5 match flow into a result screen, a PRNG
|
|
5
|
+
* rally "spin" so an idle match provably ENDS, GB APU music + SFX, a
|
|
6
|
+
* window-layer fixed HUD, a persistent RECORD in battery cart RAM (the
|
|
7
|
+
* longest win streak vs the CPU) — and the Game Boy COLOR signature on top:
|
|
8
|
+
* TRUE per-tile color. The two paddles are told apart by distinct CGB
|
|
9
|
+
* PALETTE (your paddle a hot cyan, the CPU's a danger red — through OCPS),
|
|
10
|
+
* not by DMG shade; and the court is a real color scene — a teal floor, a
|
|
11
|
+
* gold rail band, a violet centre net — painted as CGB palettes assigned per
|
|
12
|
+
* BG cell through the VRAM bank-1 attribute map. Not a colorized mono game.
|
|
7
13
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
14
|
+
* THE GAME: your paddle (left) moves UP/DOWN; the ball ricochets off the
|
|
15
|
+
* rails and deflects off your paddle by where it strikes (centre = flat,
|
|
16
|
+
* edge = steep). The CPU paddle (right) chases the ball at half your top
|
|
17
|
+
* speed, so a steep edge-deflection outruns it — that's exactly how you beat
|
|
18
|
+
* it. Win the point when the ball passes the far paddle; first to 5 takes the
|
|
19
|
+
* match. Win the match and your streak grows; lose and it dies. The longest
|
|
20
|
+
* streak survives a power cycle.
|
|
10
21
|
*
|
|
11
|
-
*
|
|
22
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
23
|
+
* very different one. The markers tell you what's what:
|
|
24
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GB/GBC footgun;
|
|
25
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
26
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
27
|
+
* reshape freely.
|
|
28
|
+
*
|
|
29
|
+
* SINGLE-PLAYER, honestly: the Game Boy's "player 2" is a LINK CABLE, which
|
|
30
|
+
* one emulator instance cannot provide — a single instance cannot emulate
|
|
31
|
+
* the second Game Boy on the other end of that cable. So handheld examples
|
|
32
|
+
* ship a press-start title and a 1P-vs-CPU match instead of faking a 2P mode
|
|
33
|
+
* the platform cannot deliver. (The NES/Genesis sports templates ARE 2P
|
|
34
|
+
* versus — two controllers on ONE machine — and a 1P-vs-CPU mode too.)
|
|
35
|
+
*
|
|
36
|
+
* WHY THE PRNG MATTERS (a teaching point shared with the NES/GBA sports
|
|
37
|
+
* templates): the Game Boy is fully deterministic. Without a noise source,
|
|
38
|
+
* the CPU's fixed ball-chase and the fixed rail/paddle bounces lock into an
|
|
39
|
+
* identical rally cycle that NEVER ends — the ball orbits the court forever
|
|
40
|
+
* and no point is ever scored. random8() adds a ±1 "spin" to every paddle
|
|
41
|
+
* return, so rallies always drift, break symmetry, and an idle match reaches
|
|
42
|
+
* 5-0 on its own.
|
|
43
|
+
*
|
|
44
|
+
* What depends on what:
|
|
45
|
+
* gb_hardware.h — register names (LCDC/WX/WY/VBK/BCPS/OCPS/NRxx/...) + masks.
|
|
46
|
+
* gb_runtime.{h,c} — vblank wait (HALT-driven), joypad, shadow OAM + the
|
|
47
|
+
* OAM-DMA-from-HRAM routine, VRAM-safe memcpy, APU helpers (shared GB).
|
|
48
|
+
* gb_crt0.s — boot + interrupt vectors + the cartridge header window. It
|
|
49
|
+
* DECLARES the cart as MBC1+RAM+BATTERY ($0147=$03, $0149=$02): that
|
|
50
|
+
* header is what makes the SRAM record persist (the GB equivalent of
|
|
51
|
+
* the NES iNES BATTERY bit).
|
|
52
|
+
* font.h — 0-9 A-Z 2bpp glyphs for all text.
|
|
53
|
+
*
|
|
54
|
+
* WRAM NOTE: build with dataLoc:0xC200 so our statics sit ABOVE shadow_oam
|
|
55
|
+
* ($C100) — else oam_clear() would zero our state. The project recipe sets
|
|
56
|
+
* that automatically.
|
|
57
|
+
*
|
|
58
|
+
* RENDERING — the hard-won architecture (details at each routine below):
|
|
59
|
+
* - The two paddles and the ball are OBJ sprites (OAM), not BG tiles; moving
|
|
60
|
+
* them is just an OAM rewrite — and each paddle's TEAM colour is its OBJ
|
|
61
|
+
* palette (OCPS), so one tile reads cyan or red purely by its OAM attr.
|
|
62
|
+
* - The court is BG tiles + per-cell bank-1 palette attributes, painted once
|
|
63
|
+
* with the LCD off at match start.
|
|
64
|
+
* - The HUD lives on the WINDOW layer — a fixed strip at the bottom, immune
|
|
65
|
+
* to BG scrolling. Score digits + result text go through a small vblank
|
|
66
|
+
* COMMIT queue (bank-0 tile writes only — the GBC two-phase discipline).
|
|
67
|
+
* - We NEVER toggle the LCD in-game. LCD-off is used only for the
|
|
68
|
+
* full-screen title <-> court transitions, where palette RAM is also
|
|
69
|
+
* (re)loaded (palette writes during active display are dropped — mode 3).
|
|
12
70
|
*/
|
|
13
|
-
|
|
14
71
|
#include "gb_hardware.h"
|
|
15
72
|
#include "gb_runtime.h"
|
|
73
|
+
#include "font.h"
|
|
74
|
+
|
|
75
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
76
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
77
|
+
#define GAME_TITLE "HUE HUSTLE"
|
|
16
78
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
79
|
+
/* ── GAME LOGIC (clay — reshape freely) ── court geometry + match rules.
|
|
80
|
+
* Pixel coords. The court interior is bounded top/bottom by rail tiles; the
|
|
81
|
+
* paddles + ball stay between COURT_TOP and COURT_BOT. */
|
|
82
|
+
#define COURT_TOP 24 /* first pixel row below the top rail */
|
|
83
|
+
#define COURT_BOT 128 /* first pixel row of the bottom rail */
|
|
84
|
+
#define PADDLE_H 24 /* 3 stacked 8x8 sprites */
|
|
85
|
+
#define PADDLE_X1 16 /* P1 — left side (you) */
|
|
86
|
+
#define PADDLE_X2 136 /* CPU — right side */
|
|
20
87
|
#define BALL_SIZE 8
|
|
21
|
-
#define
|
|
22
|
-
#define
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
88
|
+
#define SCREEN_W 160
|
|
89
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
90
|
+
|
|
91
|
+
/* Tile slots in the $8000 table ($8000 unsigned addressing — LCDC bit 4 set
|
|
92
|
+
* below). Sprites + BG share the table.
|
|
93
|
+
* T_BALL/T_PADDLE are OBJ; the paddles' TEAM colour comes from the OBJ
|
|
94
|
+
* palette (OAM attr bits 0-2 → OCPS), NOT from DMG shade.
|
|
95
|
+
* T_FLOOR/T_RAIL/T_NET dress the court; FONT_BASE.. are the glyphs. */
|
|
96
|
+
#define T_PADDLE 1
|
|
97
|
+
#define T_BALL 2
|
|
98
|
+
#define T_FLOOR 3
|
|
99
|
+
#define T_RAIL 4
|
|
100
|
+
#define T_NET 5
|
|
101
|
+
#define T_BLANK 0
|
|
102
|
+
#define FONT_BASE 16 /* digit d = 16+d, letter L = 16+10+idx (see font.h) */
|
|
30
103
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
*
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
104
|
+
#define ST_TITLE 0
|
|
105
|
+
#define ST_PLAY 1
|
|
106
|
+
#define ST_OVER 2
|
|
107
|
+
|
|
108
|
+
/* VRAM tile maps. BG playfield = $9800; the window HUD = $9C00 (offset
|
|
109
|
+
* $400 in the same VRAM pointer — see the WINDOW HUD idiom below). */
|
|
110
|
+
#define VRAM ((volatile uint8_t *)0x9800)
|
|
111
|
+
#define WIN_OFF 0x400
|
|
112
|
+
|
|
113
|
+
/* ── GAME LOGIC (clay — reshape freely) ── tile pixel data (2bpp).
|
|
114
|
+
* Each 8x8 tile = 16 bytes, 2 bytes per row (low plane then high plane); a
|
|
115
|
+
* pixel's 2-bit value = (hi<<1)|lo selects a colour WITHIN whichever CGB
|
|
116
|
+
* palette the cell's bank-1 attribute (BG) or the sprite's OAM attr (OBJ)
|
|
117
|
+
* chose. So one paddle tile reads cyan or red purely by its OAM attr. */
|
|
118
|
+
static const uint8_t tile_paddle[16] = { /* solid block; OBJ palette picks colour */
|
|
119
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
120
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
121
|
+
};
|
|
122
|
+
static const uint8_t tile_ball[16] = { /* round pip, bright core (value 3) */
|
|
123
|
+
0x3C,0x3C, 0x7E,0x7E, 0xFF,0xFF, 0xFF,0xFF,
|
|
124
|
+
0xFF,0xFF, 0xFF,0xFF, 0x7E,0x7E, 0x3C,0x3C,
|
|
125
|
+
};
|
|
126
|
+
static const uint8_t tile_floor[16] = { /* faint dither (never flat — value 1) */
|
|
127
|
+
0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
|
|
128
|
+
0x00,0x00, 0x22,0x00, 0x00,0x00, 0x88,0x00,
|
|
43
129
|
};
|
|
44
|
-
static const uint8_t
|
|
45
|
-
|
|
46
|
-
|
|
130
|
+
static const uint8_t tile_rail[16] = { /* solid rail (value 3) */
|
|
131
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
132
|
+
0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF, 0xFF,0xFF,
|
|
47
133
|
};
|
|
48
|
-
static const uint8_t
|
|
49
|
-
|
|
50
|
-
|
|
134
|
+
static const uint8_t tile_net[16] = { /* dashed vertical net segment (value 3) */
|
|
135
|
+
0x18,0x18, 0x18,0x18, 0x00,0x00, 0x00,0x00,
|
|
136
|
+
0x18,0x18, 0x18,0x18, 0x00,0x00, 0x00,0x00,
|
|
51
137
|
};
|
|
52
|
-
#define T_COURT 2
|
|
53
|
-
#define T_NET 3
|
|
54
|
-
#define T_WALL 4
|
|
55
138
|
|
|
56
|
-
|
|
57
|
-
|
|
139
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
140
|
+
* The CGB palette TABLE (the colours themselves are art; the LOADER below is
|
|
141
|
+
* the hardware idiom). 15-bit BGR: 5 bits each, blue in the high bits — RGB()
|
|
142
|
+
* packs it. Colour 0 of a BG palette is the cell's "background" shade; for
|
|
143
|
+
* OBJ palettes colour 0 is transparent (the scene shows through). */
|
|
144
|
+
#define RGB(r,g,b) ((uint16_t)(((uint16_t)(b)<<10)|((uint16_t)(g)<<5)|(r)))
|
|
58
145
|
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
146
|
+
/* BG palette slots (bank-1 attribute byte bits 0-2 select one of these). The
|
|
147
|
+
* court reads as a genuine colour scene — a teal floor, a gold rail band, a
|
|
148
|
+
* violet net — so a wide-area hue census sees several DISTINCT hues, the proof
|
|
149
|
+
* the cart is doing per-tile CGB colour and not 4-shade-green DMG. */
|
|
150
|
+
#define PAL_FLOOR 0 /* teal court surface */
|
|
151
|
+
#define PAL_RAIL 1 /* gold top/bottom rails */
|
|
152
|
+
#define PAL_NET 2 /* violet centre net */
|
|
153
|
+
#define PAL_HUD 3 /* HUD strip + all text (dark→white) */
|
|
63
154
|
|
|
64
|
-
static
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
155
|
+
static const uint16_t bg_palettes[8][4] = {
|
|
156
|
+
/* 0 floor */ { RGB(1,6,7), RGB(3,16,18), RGB(4,22,22), RGB(6,28,28) },
|
|
157
|
+
/* 1 rail */ { RGB(8,6,1), RGB(20,15,2), RGB(28,21,3), RGB(31,26,2) },
|
|
158
|
+
/* 2 net */ { RGB(8,2,12), RGB(16,4,24), RGB(20,4,30), RGB(24,6,31) },
|
|
159
|
+
/* 3 hud */ { RGB(2,2,6), RGB(8,9,16), RGB(2,2,6), RGB(31,31,31) },
|
|
160
|
+
/* 4 spare */ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
161
|
+
/* 5 spare */ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
162
|
+
/* 6 spare */ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
163
|
+
/* 7 spare */ { RGB(0,0,0), RGB(10,10,10), RGB(20,20,20), RGB(31,31,31) },
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/* OBJ palette slots (OAM attr bits 0-2 select one of these). Colour 0 is
|
|
167
|
+
* always transparent. THIS is how the two paddles read as two TEAMS on the
|
|
168
|
+
* GBC: same solid tile, different OBJ palette — the colour answer to the GB
|
|
169
|
+
* template's "tell them apart by DMG shade". */
|
|
170
|
+
#define OPAL_YOU 0 /* your paddle — hot cyan */
|
|
171
|
+
#define OPAL_CPU 1 /* CPU paddle — danger red */
|
|
172
|
+
#define OPAL_BALL 2 /* ball — bright white */
|
|
173
|
+
|
|
174
|
+
static const uint16_t obj_palettes[8][4] = {
|
|
175
|
+
/* 0 you */ { 0, RGB(6,14,31), RGB(2,8,26), RGB(16,24,31) },
|
|
176
|
+
/* 1 cpu */ { 0, RGB(31,5,5), RGB(22,2,2), RGB(31,18,14) },
|
|
177
|
+
/* 2 ball */ { 0, RGB(31,31,20), RGB(28,28,28), RGB(31,31,31) },
|
|
178
|
+
/* 3 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
179
|
+
/* 4 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
180
|
+
/* 5 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
181
|
+
/* 6 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
182
|
+
/* 7 spare */ { 0, RGB(20,20,20), RGB(31,31,31), RGB(10,10,10) },
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
186
|
+
* CGB palette RAM — the BCPS/BCPD (BG) and OCPS/OCPD (OBJ) port pairs.
|
|
187
|
+
* requires: a .gbc build (CGB flag $0143 set — the build pipeline does it);
|
|
188
|
+
* on a DMG build these registers are dead and you get 4-shade green.
|
|
189
|
+
*
|
|
190
|
+
* Palette RAM is NOT memory-mapped: it's 64 bytes (8 palettes × 4 colours ×
|
|
191
|
+
* 2 bytes, little-endian 15-bit BGR) behind an index/data port pair.
|
|
192
|
+
* BCPS = 0x80 | index — set write index; bit 7 = AUTO-INCREMENT, so a
|
|
193
|
+
* burst of BCPD writes walks the whole 64 bytes.
|
|
194
|
+
*
|
|
195
|
+
* TIMING FOOTGUN: palette RAM belongs to the PPU. Writes during active
|
|
196
|
+
* display (mode 3) are IGNORED on real hardware — same constraint as VRAM.
|
|
197
|
+
* Load palettes with the LCD OFF (boot / transitions, as here) or inside
|
|
198
|
+
* vblank — never a mid-frame burst. */
|
|
199
|
+
static void load_bg_palettes(void) {
|
|
200
|
+
uint8_t p, i;
|
|
201
|
+
BCPS = 0x80; /* index 0, auto-increment on */
|
|
202
|
+
for (p = 0; p < 8; p++)
|
|
203
|
+
for (i = 0; i < 4; i++) {
|
|
204
|
+
BCPD = (uint8_t)(bg_palettes[p][i] & 0xFF);
|
|
205
|
+
BCPD = (uint8_t)((bg_palettes[p][i] >> 8) & 0xFF);
|
|
206
|
+
}
|
|
70
207
|
}
|
|
71
208
|
|
|
72
|
-
static void
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
209
|
+
static void load_obj_palettes(void) {
|
|
210
|
+
uint8_t p, i;
|
|
211
|
+
OCPS = 0x80;
|
|
212
|
+
for (p = 0; p < 8; p++)
|
|
213
|
+
for (i = 0; i < 4; i++) {
|
|
214
|
+
OCPD = (uint8_t)(obj_palettes[p][i] & 0xFF);
|
|
215
|
+
OCPD = (uint8_t)((obj_palettes[p][i] >> 8) & 0xFF);
|
|
216
|
+
}
|
|
78
217
|
}
|
|
79
218
|
|
|
219
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
220
|
+
* WRAM layout — keep state ABOVE the shadow-OAM page.
|
|
221
|
+
* The OAM-DMA shadow buffer is pinned by the runtime at $C100 (one page,
|
|
222
|
+
* $C100-$C19F). SDCC allocates ordinary statics upward from $C000; pass
|
|
223
|
+
* dataLoc:0xC200 to the build recipe (the project recipe does) so the
|
|
224
|
+
* auto-allocated _DATA segment can't collide with shadow_oam. $C200-$DFFF is
|
|
225
|
+
* free work RAM. */
|
|
226
|
+
|
|
227
|
+
/* ── GAME LOGIC (clay — reshape freely) ── game state (small — auto _DATA) */
|
|
228
|
+
static uint8_t state; /* ST_TITLE / ST_PLAY / ST_OVER */
|
|
229
|
+
static uint8_t p1y, cpuy; /* paddle top Y (pixels) */
|
|
230
|
+
static int16_t bx, by; /* ball top-left position (signed math) */
|
|
231
|
+
static int8_t bdx, bdy; /* ball velocity (px/frame) */
|
|
232
|
+
static uint8_t score_p1, score_cpu; /* 0..WIN_SCORE */
|
|
233
|
+
static uint8_t serve_timer; /* freeze frames between points */
|
|
234
|
+
static uint8_t streak; /* current 1P win streak vs CPU (RAM) */
|
|
235
|
+
static uint16_t record; /* battery-backed best streak — see SRAM */
|
|
236
|
+
static uint8_t new_record; /* result screen shows NEW RECORD */
|
|
237
|
+
static uint8_t win_who; /* 1 = you took the match, 0 = CPU did */
|
|
238
|
+
static uint8_t prev_pad;
|
|
239
|
+
|
|
240
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG. THE LOAD-BEARING DETAIL of a
|
|
241
|
+
* deterministic versus game (see the file header). Ticked once per play
|
|
242
|
+
* frame so two identical board states a few frames apart still diverge, and
|
|
243
|
+
* added as ±1 spin to every paddle return so rallies END. Kept 16-bit on
|
|
244
|
+
* purpose — sm83 has no fast 32-bit shifts; a wider generator degenerates. */
|
|
245
|
+
static uint16_t rng = 0xC0A7;
|
|
246
|
+
static uint8_t random8(void) {
|
|
247
|
+
rng ^= rng << 7;
|
|
248
|
+
rng ^= rng >> 9;
|
|
249
|
+
rng ^= rng << 8;
|
|
250
|
+
return (uint8_t)(rng >> 8);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
254
|
+
* BATTERY SRAM record — persistent saves on a Game Boy cart.
|
|
255
|
+
* requires: gb_crt0.s declaring MBC1+RAM+BATTERY in the cartridge header
|
|
256
|
+
* ($0147=$03, $0149=$02 → 8KB at $A000-$BFFF). With a ROM-only header the
|
|
257
|
+
* $A000 region is OPEN BUS: writes vanish, reads return garbage, and
|
|
258
|
+
* nothing tells you why. The header is the save system.
|
|
259
|
+
*
|
|
260
|
+
* The MBC powers up with cart RAM DISABLED. The $0A-enable dance:
|
|
261
|
+
* 1. write $0A to anywhere in $0000-$1FFF → RAM enabled
|
|
262
|
+
* 2. read/write $A000-$BFFF → real battery RAM
|
|
263
|
+
* 3. write $00 to $0000-$1FFF → RAM disabled again
|
|
264
|
+
* ALWAYS re-disable after access — that's what makes a yanked cartridge /
|
|
265
|
+
* dying battery corrupt at most the bytes mid-write, not the whole save.
|
|
266
|
+
*
|
|
267
|
+
* First boot is GARBAGE, not zeros: battery RAM holds whatever the silicon
|
|
268
|
+
* woke up with. The magic bytes + checksum below tell "my save" from
|
|
269
|
+
* "factory noise" — without them a fresh cart shows a junk streak record.
|
|
270
|
+
*
|
|
271
|
+
* PERSISTENCE CHOICE: a raw hi-score is meaningless for a versus game (every
|
|
272
|
+
* match ends 5-x), so we persist the LONGEST WIN STREAK vs the CPU.
|
|
273
|
+
*
|
|
274
|
+
* Save block at $A000: 'H' 'S' rec-lo rec-hi ck (ck = lo^hi^$A5)
|
|
275
|
+
* No timing constraints — SRAM is not VRAM; access it any time. */
|
|
276
|
+
#define SRAM_BASE ((volatile uint8_t *)0xA000)
|
|
277
|
+
#define MBC_RAMG (*(volatile uint8_t *)0x0000) /* MBC1 RAM-gate register */
|
|
278
|
+
|
|
279
|
+
static uint16_t record_load(void) {
|
|
280
|
+
uint16_t v = 0;
|
|
281
|
+
MBC_RAMG = 0x0A; /* enable cart RAM */
|
|
282
|
+
if (SRAM_BASE[0] == 'H' && SRAM_BASE[1] == 'S'
|
|
283
|
+
&& SRAM_BASE[4] == (uint8_t)(SRAM_BASE[2] ^ SRAM_BASE[3] ^ 0xA5)) {
|
|
284
|
+
v = (uint16_t)(SRAM_BASE[2] | ((uint16_t)SRAM_BASE[3] << 8));
|
|
285
|
+
}
|
|
286
|
+
MBC_RAMG = 0x00; /* ALWAYS re-disable */
|
|
287
|
+
return v;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
static void record_save(uint16_t v) {
|
|
291
|
+
uint8_t lo = (uint8_t)(v & 0xFF), hi = (uint8_t)(v >> 8);
|
|
292
|
+
MBC_RAMG = 0x0A;
|
|
293
|
+
SRAM_BASE[0] = 'H';
|
|
294
|
+
SRAM_BASE[1] = 'S';
|
|
295
|
+
SRAM_BASE[2] = lo;
|
|
296
|
+
SRAM_BASE[3] = hi;
|
|
297
|
+
SRAM_BASE[4] = (uint8_t)(lo ^ hi ^ 0xA5);
|
|
298
|
+
MBC_RAMG = 0x00;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/* ── GAME LOGIC (clay — reshape freely) ── sound effects.
|
|
302
|
+
* A tiny note sequencer driving square channel 2 directly. Each note has a
|
|
303
|
+
* real volume-decay envelope (NR22) so it fades instead of clicking off.
|
|
304
|
+
* sfx_tick() advances one step per frame; multi-note effects become little
|
|
305
|
+
* arpeggios. GB period p ⇒ freq = 131072/(2048-p); higher p = higher note. */
|
|
306
|
+
#define P_C4 1548
|
|
307
|
+
#define P_G4 1714
|
|
308
|
+
#define P_A4 1750
|
|
309
|
+
#define P_C5 1797
|
|
310
|
+
#define P_E5 1849
|
|
311
|
+
#define P_G5 1881
|
|
312
|
+
#define P_C6 1923
|
|
313
|
+
|
|
314
|
+
#define SFX_STEPS 3
|
|
315
|
+
static uint16_t sfx_p[SFX_STEPS];
|
|
316
|
+
static uint8_t sfx_v[SFX_STEPS];
|
|
317
|
+
static uint8_t sfx_d[SFX_STEPS];
|
|
318
|
+
static uint8_t sfx_f[SFX_STEPS];
|
|
319
|
+
static uint8_t sfx_n, sfx_i, sfx_t;
|
|
320
|
+
|
|
321
|
+
static void sfx_tick(void) {
|
|
322
|
+
if (sfx_i >= sfx_n) return;
|
|
323
|
+
if (sfx_t != 0) { sfx_t--; return; }
|
|
324
|
+
NR21 = sfx_d[sfx_i];
|
|
325
|
+
NR22 = sfx_v[sfx_i];
|
|
326
|
+
NR23 = (uint8_t)(sfx_p[sfx_i] & 0xFF);
|
|
327
|
+
NR24 = (uint8_t)(0x80 | (sfx_p[sfx_i] >> 8)); /* trigger (envelope ends it) */
|
|
328
|
+
sfx_t = sfx_f[sfx_i];
|
|
329
|
+
sfx_i++;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
static void sfx_go(uint8_t n) { sfx_n = n; sfx_i = 0; sfx_t = 0; sfx_tick(); }
|
|
333
|
+
|
|
334
|
+
static void sfx_rail(void) { /* short blip — ball off a rail */
|
|
335
|
+
sfx_p[0] = P_A4; sfx_v[0] = 0xA1; sfx_d[0] = 0x40; sfx_f[0] = 3;
|
|
336
|
+
sfx_go(1);
|
|
337
|
+
}
|
|
338
|
+
static void sfx_paddle(void) { /* brighter blip — paddle return */
|
|
339
|
+
sfx_p[0] = P_C6; sfx_v[0] = 0xC2; sfx_d[0] = 0x80; sfx_f[0] = 4;
|
|
340
|
+
sfx_go(1);
|
|
341
|
+
}
|
|
342
|
+
static void sfx_point(void) { /* two-note drop — a point scored */
|
|
343
|
+
sfx_p[0] = P_C5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 4;
|
|
344
|
+
sfx_p[1] = P_G4; sfx_v[1] = 0xD3; sfx_d[1] = 0x80; sfx_f[1] = 8;
|
|
345
|
+
sfx_go(2);
|
|
346
|
+
}
|
|
347
|
+
static void sfx_win(void) { /* rising fanfare — you took the match */
|
|
348
|
+
sfx_p[0] = P_C5; sfx_v[0] = 0xD2; sfx_d[0] = 0x80; sfx_f[0] = 5;
|
|
349
|
+
sfx_p[1] = P_E5; sfx_v[1] = 0xD2; sfx_d[1] = 0x80; sfx_f[1] = 5;
|
|
350
|
+
sfx_p[2] = P_G5; sfx_v[2] = 0xD3; sfx_d[2] = 0x80; sfx_f[2] = 12;
|
|
351
|
+
sfx_go(3);
|
|
352
|
+
}
|
|
353
|
+
static void sfx_lose(void) { /* falling — CPU took the match */
|
|
354
|
+
sfx_p[0] = P_G4; sfx_v[0] = 0xC3; sfx_d[0] = 0x80; sfx_f[0] = 8;
|
|
355
|
+
sfx_p[1] = P_C4; sfx_v[1] = 0xC5; sfx_d[1] = 0x80; sfx_f[1] = 20;
|
|
356
|
+
sfx_go(2);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/* ── GAME LOGIC (clay — reshape freely) ── background music.
|
|
360
|
+
* A looping square-wave lead on channel 1 (SFX live on channel 2, so they
|
|
361
|
+
* mix and the effects cut through the music). music_tick() plays one melody
|
|
362
|
+
* step every 12 frames, re-triggering ch1 at a steady volume. Toggle on/off
|
|
363
|
+
* with SELECT — defaults ON.
|
|
364
|
+
*
|
|
365
|
+
* The melody is the GB 11-bit period split into low/high BYTE arrays (NR13 +
|
|
366
|
+
* NR14 low 3 bits) — period p ⇒ freq 131072/(2048-p). hi == 0xFF marks a
|
|
367
|
+
* rest. Arpeggios over a C - Am - F - G chord loop, 8 steps each. */
|
|
368
|
+
static const uint8_t mel_lo[32] = {
|
|
369
|
+
0x06,0x39,0x59,0x83, 0x59,0x39,0x06,0x00, /* C E G C6 G E C - */
|
|
370
|
+
0xD6,0x06,0x39,0x6B, 0x39,0x06,0xD6,0x00, /* A C E A5 E C A - */
|
|
371
|
+
0x88,0xD6,0x06,0x44, 0x06,0xD6,0x88,0x00, /* F A C F5 C A F - */
|
|
372
|
+
0xB2,0xF7,0x21,0x59, 0x21,0xF7,0xB2,0x00, /* G B D G5 D B G - */
|
|
373
|
+
};
|
|
374
|
+
static const uint8_t mel_hi[32] = { /* high 3 bits; 0xFF = rest */
|
|
375
|
+
0x07,0x07,0x07,0x07, 0x07,0x07,0x07,0xFF,
|
|
376
|
+
0x06,0x07,0x07,0x07, 0x07,0x07,0x06,0xFF,
|
|
377
|
+
0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
|
|
378
|
+
0x06,0x06,0x07,0x07, 0x07,0x06,0x06,0xFF,
|
|
379
|
+
};
|
|
380
|
+
static uint8_t music_on;
|
|
381
|
+
static uint8_t music_idx;
|
|
382
|
+
static uint8_t music_timer;
|
|
383
|
+
|
|
384
|
+
static void music_note(uint8_t idx) {
|
|
385
|
+
uint8_t hi = mel_hi[idx];
|
|
386
|
+
if (hi == 0xFF) { NR12 = 0x00; NR14 = 0x80; return; } /* rest: silence ch1 */
|
|
387
|
+
NR10 = 0x00; /* no sweep */
|
|
388
|
+
NR11 = 0x80; /* 50% duty, no length counter */
|
|
389
|
+
NR12 = 0x90; /* volume 9, no envelope (steady lead) */
|
|
390
|
+
NR13 = mel_lo[idx];
|
|
391
|
+
NR14 = (uint8_t)(0x80 | hi); /* trigger + freq high bits */
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
static void music_tick(void) {
|
|
395
|
+
if (!music_on) return;
|
|
396
|
+
if (music_timer == 0) {
|
|
397
|
+
music_note(music_idx);
|
|
398
|
+
music_timer = 12;
|
|
399
|
+
if (++music_idx >= 32) music_idx = 0;
|
|
400
|
+
}
|
|
401
|
+
music_timer--;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
static void music_toggle(void) {
|
|
405
|
+
music_on = (uint8_t)(!music_on);
|
|
406
|
+
music_idx = 0;
|
|
407
|
+
music_timer = 0;
|
|
408
|
+
if (!music_on) { NR12 = 0x00; NR14 = 0x80; } /* kill the lead immediately */
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/* ── rendering ─────────────────────────────────────────────────────── */
|
|
412
|
+
/* copy one 16-byte 2bpp tile into VRAM tile slot `slot` ($8000 + slot*16) */
|
|
80
413
|
static void upload_tile(uint8_t slot, const uint8_t *src) {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
memcpy_vram(dst, src, 16);
|
|
414
|
+
/* memcpy_vram (pointer-walk) — NOT an indexed dst[i]=src[i] loop, which
|
|
415
|
+
* SDCC sm83 miscompiles when dst points into VRAM ($8000-$9FFF). */
|
|
416
|
+
memcpy_vram((uint8_t *)(0x8000 + (uint16_t)slot * 16), src, 16);
|
|
85
417
|
}
|
|
86
418
|
|
|
87
|
-
/*
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
419
|
+
/* font.h glyphs are already 2bpp (16 bytes each) — straight copy. */
|
|
420
|
+
static void upload_font(void) {
|
|
421
|
+
uint8_t g;
|
|
422
|
+
for (g = 0; g < FONT_GLYPHS; g++)
|
|
423
|
+
memcpy_vram((uint8_t *)(0x8000 + (uint16_t)(FONT_BASE + g) * 16),
|
|
424
|
+
&font_data[g * 16], 16);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/* map an ASCII char to its font tile slot (digits, then A-Z; space → blank) */
|
|
428
|
+
static uint8_t font_slot(char ch) {
|
|
429
|
+
if (ch >= '0' && ch <= '9') return (uint8_t)(FONT_BASE + (ch - '0'));
|
|
430
|
+
if (ch >= 'A' && ch <= 'Z') return (uint8_t)(FONT_BASE + 10 + (ch - 'A'));
|
|
431
|
+
return T_BLANK;
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
435
|
+
* Per-tile color — the VRAM bank-1 attribute map (VBK register).
|
|
436
|
+
* requires: CGB mode (see the palette idiom above); writes in a VRAM-safe
|
|
437
|
+
* window (LCD off, or a bounded vblank batch).
|
|
438
|
+
*
|
|
439
|
+
* VRAM is TWO 8KB banks behind the same $8000-$9FFF window; VBK ($FF4F)
|
|
440
|
+
* selects which one the CPU sees. Bank 0 holds tile pixels + the index maps.
|
|
441
|
+
* Bank 1 at the SAME map address holds one ATTRIBUTE byte per cell:
|
|
442
|
+
* bits 0-2 palette 0-7 ← this game's whole BG colour system
|
|
443
|
+
* bit 3 tile VRAM bank
|
|
444
|
+
* bit 5/6 H/V flip
|
|
445
|
+
* bit 7 BG-over-OBJ priority
|
|
446
|
+
* So colouring a cell is a write PAIR: tile index with VBK=0, attribute with
|
|
447
|
+
* VBK=1, at the SAME offset.
|
|
448
|
+
*
|
|
449
|
+
* FOOTGUN: VBK is global state. Forget to restore VBK=0 and every later
|
|
450
|
+
* "tile" write lands in the attribute map — the screen turns into garbage
|
|
451
|
+
* colours while the tile data you wrote is simply gone. Always end VBK=0
|
|
452
|
+
* (every routine here does). */
|
|
453
|
+
static void set_cell(uint8_t mx, uint8_t my, uint8_t tile, uint8_t pal) {
|
|
454
|
+
uint16_t off = (uint16_t)my * 32 + mx;
|
|
455
|
+
VBK = 0;
|
|
456
|
+
VRAM[off] = tile;
|
|
457
|
+
VBK = 1;
|
|
458
|
+
VRAM[off] = pal;
|
|
459
|
+
VBK = 0;
|
|
460
|
+
}
|
|
461
|
+
/* same write-pair, into the WINDOW's map at $9C00 (window HUD idiom) */
|
|
462
|
+
static void set_wcell(uint8_t wx, uint8_t wy, uint8_t tile, uint8_t pal) {
|
|
463
|
+
uint16_t off = WIN_OFF + (uint16_t)wy * 32 + wx;
|
|
464
|
+
VBK = 0;
|
|
465
|
+
VRAM[off] = tile;
|
|
466
|
+
VBK = 1;
|
|
467
|
+
VRAM[off] = pal;
|
|
468
|
+
VBK = 0;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/* draw a NUL-terminated string into the BG map starting at (col,row) */
|
|
472
|
+
static void draw_text(uint8_t col, uint8_t row, const char *s) {
|
|
473
|
+
uint8_t i;
|
|
474
|
+
for (i = 0; s[i] != 0; i++)
|
|
475
|
+
set_cell((uint8_t)(col + i), row, font_slot(s[i]), PAL_HUD);
|
|
476
|
+
}
|
|
477
|
+
/* draw a NUL-terminated string into the WINDOW map starting at (col,row) */
|
|
478
|
+
static void draw_wtext(uint8_t col, uint8_t row, const char *s) {
|
|
479
|
+
uint8_t i;
|
|
480
|
+
for (i = 0; s[i] != 0; i++)
|
|
481
|
+
set_wcell((uint8_t)(col + i), row, font_slot(s[i]), PAL_HUD);
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
485
|
+
* WINDOW-layer HUD — a fixed strip the BG scroll can never move.
|
|
486
|
+
* requires: LCDC bits 5 (window on) + 6 (window map = $9C00), WX/WY set,
|
|
487
|
+
* and HUD text written to the $9C00 map (set_wcell), not the $9800 one.
|
|
488
|
+
*
|
|
489
|
+
* The window is the GB's second BG plane: same tile data, its OWN 32x32
|
|
490
|
+
* map, drawn OVER the BG starting at screen position (WX-7, WY) and extending
|
|
491
|
+
* to the bottom-right. It ignores SCX/SCY completely — scroll the playfield
|
|
492
|
+
* all you want, the HUD strip stays put. On CGB the window cells take bank-1
|
|
493
|
+
* palette attributes exactly like the BG (set_wcell writes both banks).
|
|
494
|
+
*
|
|
495
|
+
* Gotchas:
|
|
496
|
+
* - WX is offset by 7: WX=7 is the left edge. WX<7 glitches on hardware.
|
|
497
|
+
* - The window has its OWN line counter: it renders ITS map from window
|
|
498
|
+
* row 0 downward, regardless of WY — our HUD lives at $9C00 row 0.
|
|
499
|
+
*
|
|
500
|
+
* Window HUD layout (window map row 0): YOU d CPU d REC ddd
|
|
501
|
+
* Static labels drawn once at transitions; the digits go through the vblank
|
|
502
|
+
* commit queue (bank-0 tile writes only — see the two-phase commit). */
|
|
503
|
+
#define WINY 136 /* screen y where the strip starts */
|
|
504
|
+
#define HUD_YOU_X 4 /* your score digit, window row 0 */
|
|
505
|
+
#define HUD_CPU_X 11 /* CPU score digit, window row 0 */
|
|
506
|
+
#define HUD_REC_X 17 /* streak record digits (3), window row 0 */
|
|
507
|
+
|
|
508
|
+
/* paint the whole window strip: blank backdrop + labels (LCD off only) */
|
|
509
|
+
static void draw_window_static(void) {
|
|
510
|
+
uint8_t x;
|
|
511
|
+
for (x = 0; x < 20; x++) set_wcell(x, 0, T_BLANK, PAL_HUD);
|
|
512
|
+
draw_wtext(0, 0, "YOU");
|
|
513
|
+
draw_wtext(6, 0, "CPU");
|
|
514
|
+
draw_wtext(13, 0, "REC");
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/* draw every dynamic HUD value directly (LCD off / transitions only —
|
|
518
|
+
* in-game updates go through the queue). REC is a 3-digit record. */
|
|
519
|
+
static void draw_hud_now(void) {
|
|
520
|
+
uint16_t v = record;
|
|
521
|
+
uint8_t d2 = (uint8_t)(v % 10); v /= 10;
|
|
522
|
+
uint8_t d1 = (uint8_t)(v % 10); v /= 10;
|
|
523
|
+
uint8_t d0 = (uint8_t)(v % 10);
|
|
524
|
+
set_wcell(HUD_YOU_X, 0, (uint8_t)(FONT_BASE + score_p1), PAL_HUD);
|
|
525
|
+
set_wcell(HUD_CPU_X, 0, (uint8_t)(FONT_BASE + score_cpu), PAL_HUD);
|
|
526
|
+
set_wcell(HUD_REC_X, 0, (uint8_t)(FONT_BASE + d0), PAL_HUD);
|
|
527
|
+
set_wcell(HUD_REC_X + 1, 0, (uint8_t)(FONT_BASE + d1), PAL_HUD);
|
|
528
|
+
set_wcell(HUD_REC_X + 2, 0, (uint8_t)(FONT_BASE + d2), PAL_HUD);
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
532
|
+
* Two-phase vblank COMMIT — the #1 GBC footgun, dodged.
|
|
533
|
+
* requires: update_sprites + hud_commit as the FIRST two things after
|
|
534
|
+
* wait_vblank (in that order), at most a few cells committed per frame,
|
|
535
|
+
* and no LCDC bit-7 toggling in-game.
|
|
536
|
+
*
|
|
537
|
+
* This core silently DROPS a VRAM write that lands during active display, AND
|
|
538
|
+
* a too-LONG batch overruns the ~10-line vblank window and drops its tail.
|
|
539
|
+
* On CGB it's WORSE: a naive set_wcell() per HUD cell toggles VBK twice +
|
|
540
|
+
* writes two banks PER cell — a handful of cells is dozens of VBK writes in
|
|
541
|
+
* one vblank, which overruns and silently drops the tail cells. THE FIX (the
|
|
542
|
+
* two-phase discipline shared with the GBC shmup/platformer): the HUD cells'
|
|
543
|
+
* bank-1 ATTRIBUTE bytes are constant PAL_HUD (painted once at LCD-off and
|
|
544
|
+
* never changed), so the per-frame commit only rewrites BANK-0 TILE bytes —
|
|
545
|
+
* VBK=0 ONCE, then a tight pointer walk. Game logic only sets a dirty flag;
|
|
546
|
+
* hud_commit() (the FIRST VRAM work after the OAM DMA, inside vblank) drains a
|
|
547
|
+
* SMALL batch. Result text is pre-converted to tile indices at full-frame
|
|
548
|
+
* time (font_slot's compare chain per char is exactly the work that blows the
|
|
549
|
+
* budget), so the vblank commit is a dumb byte copy. */
|
|
550
|
+
#define MSG_BUDGET 5 /* result-text cells written per frame */
|
|
551
|
+
static uint8_t hud_dirty; /* score digits need re-committing */
|
|
552
|
+
static uint8_t rec_dirty; /* record digits need re-committing */
|
|
553
|
+
static uint8_t msg_active; /* result text is being drained */
|
|
554
|
+
static uint8_t msg_i; /* next result cell to write */
|
|
555
|
+
static uint8_t msg_l1[12], msg_l2[12]; /* pre-converted result tile rows */
|
|
556
|
+
#define MSG_L1_COL 5 /* result line 1 (winner) at BG (5,7) */
|
|
557
|
+
#define MSG_L1_ROW 7
|
|
558
|
+
#define MSG_L2_COL 4 /* result line 2 (prompt) at BG (4,9) */
|
|
559
|
+
#define MSG_L2_ROW 9
|
|
560
|
+
|
|
561
|
+
/* the window map, bank 0 (tile bytes) — attributes stay PAL_HUD */
|
|
562
|
+
#define WIN_TILE ((volatile uint8_t *)0x9C00)
|
|
563
|
+
/* the BG map, bank 0 — result text rides the depth palette already painted */
|
|
564
|
+
#define BG_TILE ((volatile uint8_t *)0x9800)
|
|
565
|
+
|
|
566
|
+
static void stage_msg(const char *s, uint8_t *out) {
|
|
567
|
+
uint8_t i;
|
|
568
|
+
for (i = 0; s[i] != 0; i++) out[i] = font_slot(s[i]);
|
|
569
|
+
out[i] = 0xFF; /* terminator */
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
/* len of a 0xFF-terminated staged row */
|
|
573
|
+
static uint8_t msg_len(const uint8_t *q) {
|
|
574
|
+
uint8_t i = 0;
|
|
575
|
+
while (q[i] != 0xFF) i++;
|
|
576
|
+
return i;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
static void hud_commit(void) {
|
|
580
|
+
uint8_t n, len1, j;
|
|
581
|
+
if (hud_dirty) { /* the two score digits */
|
|
582
|
+
hud_dirty = 0;
|
|
583
|
+
VBK = 0; /* attributes already PAL_HUD */
|
|
584
|
+
WIN_TILE[HUD_YOU_X] = (uint8_t)(FONT_BASE + score_p1);
|
|
585
|
+
WIN_TILE[HUD_CPU_X] = (uint8_t)(FONT_BASE + score_cpu);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
if (rec_dirty) { /* the 3 record digits (rare) */
|
|
589
|
+
uint16_t v = record;
|
|
590
|
+
uint8_t d2 = (uint8_t)(v % 10); v /= 10;
|
|
591
|
+
uint8_t d1 = (uint8_t)(v % 10); v /= 10;
|
|
592
|
+
uint8_t d0 = (uint8_t)(v % 10);
|
|
593
|
+
rec_dirty = 0;
|
|
594
|
+
VBK = 0;
|
|
595
|
+
WIN_TILE[HUD_REC_X] = (uint8_t)(FONT_BASE + d0);
|
|
596
|
+
WIN_TILE[HUD_REC_X + 1] = (uint8_t)(FONT_BASE + d1);
|
|
597
|
+
WIN_TILE[HUD_REC_X + 2] = (uint8_t)(FONT_BASE + d2);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
if (!msg_active) return;
|
|
601
|
+
/* Drain MSG_BUDGET cells per frame across BOTH result rows: msg_i runs
|
|
602
|
+
* 0..len1-1 over line 1, then len1..len1+len2-1 over line 2. The BG map's
|
|
603
|
+
* bank-1 attribute bytes were painted PAL_HUD/depth palettes already, so
|
|
604
|
+
* the text rides them — a dumb BANK-0 byte copy, no per-cell VBK. */
|
|
605
|
+
len1 = msg_len(msg_l1);
|
|
606
|
+
VBK = 0;
|
|
607
|
+
for (n = 0; n < MSG_BUDGET; n++) {
|
|
608
|
+
j = msg_i;
|
|
609
|
+
if (j < len1) {
|
|
610
|
+
BG_TILE[(uint16_t)MSG_L1_ROW * 32 + MSG_L1_COL + j] = msg_l1[j];
|
|
611
|
+
} else {
|
|
612
|
+
j -= len1;
|
|
613
|
+
if (msg_l2[j] == 0xFF) { msg_active = 0; break; }
|
|
614
|
+
BG_TILE[(uint16_t)MSG_L2_ROW * 32 + MSG_L2_COL + j] = msg_l2[j];
|
|
615
|
+
}
|
|
616
|
+
msg_i++;
|
|
98
617
|
}
|
|
99
|
-
}
|
|
100
618
|
}
|
|
101
619
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
reset_match();
|
|
133
|
-
|
|
134
|
-
while (1) {
|
|
135
|
-
wait_vblank();
|
|
136
|
-
/* OAM DMA FIRST — at the leading edge of vblank. The old order staged
|
|
137
|
-
* 45 oam_set CALLS before the DMA; the SDCC call overhead pushed the
|
|
138
|
-
* DMA ~a third of the frame into ACTIVE display, so the sprites tore
|
|
139
|
-
* on one fixed scanline ("horizontal line a 3rd of the way down").
|
|
140
|
-
* Sprites now display the state staged LAST frame (1 frame of latency,
|
|
141
|
-
* imperceptible in Pong). */
|
|
142
|
-
oam_dma_flush();
|
|
143
|
-
|
|
144
|
-
/* Stage next frame's OAM (RAM only — safe any time). Slots 5-39 were
|
|
145
|
-
* zeroed once by oam_clear() at boot and never change. */
|
|
146
|
-
oam_set(0, (uint8_t)(p1y + 16), (uint8_t)(PADDLE_X1 + 8), 1, 0);
|
|
147
|
-
oam_set(1, (uint8_t)(p1y + 16 + 8), (uint8_t)(PADDLE_X1 + 8), 1, 0);
|
|
148
|
-
oam_set(2, (uint8_t)(p2y + 16), (uint8_t)(PADDLE_X2 + 8), 1, 0);
|
|
149
|
-
oam_set(3, (uint8_t)(p2y + 16 + 8), (uint8_t)(PADDLE_X2 + 8), 1, 0);
|
|
150
|
-
oam_set(4, (uint8_t)(by + 16), (uint8_t)(bx + 8), 1, 0);
|
|
151
|
-
|
|
152
|
-
pad = joypad_read();
|
|
153
|
-
if (pad & PAD_UP && p1y > COURT_TOP) p1y -= 2;
|
|
154
|
-
if (pad & PAD_DOWN && p1y < COURT_BOT - PADDLE_H) p1y += 2;
|
|
155
|
-
|
|
156
|
-
/* Right-paddle AI — chase ball Y. */
|
|
157
|
-
target = by - PADDLE_H / 2;
|
|
158
|
-
if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
|
|
159
|
-
else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
|
|
160
|
-
|
|
161
|
-
if (serve_timer > 0) {
|
|
162
|
-
serve_timer--;
|
|
620
|
+
/* begin draining the staged result text through the vblank queue */
|
|
621
|
+
static void start_msg(void) { msg_active = 1; msg_i = 0; }
|
|
622
|
+
|
|
623
|
+
/* ── GAME LOGIC (clay — reshape freely) ── stage the shadow OAM for THIS frame.
|
|
624
|
+
* The two paddles = sprites 0-5 (3 stacked each); the ball = sprite 6. Then
|
|
625
|
+
* flush OAM. update_sprites MUST be the first VRAM/OAM work after wait_vblank:
|
|
626
|
+
* the OAM DMA has to land in vblank, or sprites tear near the top. Your paddle
|
|
627
|
+
* uses OBJ palette OPAL_YOU (cyan); the CPU's uses OPAL_CPU (red) — one tile,
|
|
628
|
+
* two TEAM-coloured paddles via the OAM attr's low 3 bits (OCPS). */
|
|
629
|
+
static void update_sprites(void) {
|
|
630
|
+
/* Write shadow_oam ($C100) directly with a walking pointer — calling
|
|
631
|
+
* oam_set() seven times burns vblank to SDCC call overhead. */
|
|
632
|
+
uint8_t *o = (uint8_t *)0xC100;
|
|
633
|
+
uint8_t i, playing = (state == ST_PLAY || state == ST_OVER);
|
|
634
|
+
if (playing) {
|
|
635
|
+
for (i = 0; i < PADDLE_H / 8; i++) { /* P1 paddle (you) — cyan */
|
|
636
|
+
*o++ = (uint8_t)(p1y + i * 8 + 16);
|
|
637
|
+
*o++ = (uint8_t)(PADDLE_X1 + 8);
|
|
638
|
+
*o++ = T_PADDLE; *o++ = OPAL_YOU;
|
|
639
|
+
}
|
|
640
|
+
for (i = 0; i < PADDLE_H / 8; i++) { /* CPU paddle — red */
|
|
641
|
+
*o++ = (uint8_t)(cpuy + i * 8 + 16);
|
|
642
|
+
*o++ = (uint8_t)(PADDLE_X2 + 8);
|
|
643
|
+
*o++ = T_PADDLE; *o++ = OPAL_CPU;
|
|
644
|
+
}
|
|
645
|
+
if (state == ST_PLAY) { /* ball (hidden on result) */
|
|
646
|
+
*o++ = (uint8_t)(by + 16);
|
|
647
|
+
*o++ = (uint8_t)(bx + 8);
|
|
648
|
+
*o++ = T_BALL; *o++ = OPAL_BALL;
|
|
649
|
+
} else { *o++ = 0; *o++ = 0; *o++ = 0; *o++ = 0; }
|
|
163
650
|
} else {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
651
|
+
for (i = 0; i < 28; i++) *o++ = 0; /* title: hide all 7 slots */
|
|
652
|
+
}
|
|
653
|
+
/* Trigger the OAM DMA via the HRAM stub directly. A = high byte of
|
|
654
|
+
* shadow_oam ($C100). */
|
|
655
|
+
((void (*)(uint8_t))0xFF80)(0xC1);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
659
|
+
* LCD-off transitions. Only flip LCDC bit 7 to 0 DURING VBLANK. Killing the
|
|
660
|
+
* LCD mid-scanline is the classic "damages real DMG hardware" move;
|
|
661
|
+
* emulators shrug, real units can be permanently marked. wait_vblank()
|
|
662
|
+
* first, always. blit_on enables BG + OBJ + the WINDOW (map $9C00). NEVER
|
|
663
|
+
* call these from the in-game loop (the off-frame blanks the whole screen). */
|
|
664
|
+
static void blit_off(void) { wait_vblank(); LCDC = 0; }
|
|
665
|
+
static void blit_on(void) {
|
|
666
|
+
LCDC = LCDC_LCD_ON | LCDC_BG_ON | LCDC_OBJ_ON | LCDC_TILE_DATA_LO
|
|
667
|
+
| LCDC_WINDOW_ON | LCDC_WINDOW_MAP_HI;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/* ── GAME LOGIC (clay — reshape freely) ── paint the static court (LCD off):
|
|
671
|
+
* teal floor everywhere, gold top/bottom rails, a violet centre net — each a
|
|
672
|
+
* real CGB palette through the bank-1 attribute map. Rows 0-16 are the 136-px
|
|
673
|
+
* play area; the window HUD owns the bottom 8 px. */
|
|
674
|
+
static void draw_court(void) {
|
|
675
|
+
uint8_t x, y;
|
|
676
|
+
for (y = 0; y < 18; y++)
|
|
677
|
+
for (x = 0; x < 20; x++) set_cell(x, y, T_FLOOR, PAL_FLOOR);
|
|
678
|
+
for (x = 0; x < 20; x++) {
|
|
679
|
+
set_cell(x, 2, T_RAIL, PAL_RAIL); /* top rail (y = 16) */
|
|
680
|
+
set_cell(x, 15, T_RAIL, PAL_RAIL); /* bottom rail (y = 120) */
|
|
681
|
+
}
|
|
682
|
+
for (y = 3; y < 15; y++)
|
|
683
|
+
set_cell(10, y, T_NET, PAL_NET); /* centre net (x = 80) */
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
/* ── GAME LOGIC (clay — reshape freely) ── title screen: court backdrop +
|
|
687
|
+
* name + prompt + the honest no-2P note. */
|
|
688
|
+
static void draw_title(void) {
|
|
689
|
+
draw_court();
|
|
690
|
+
draw_text((uint8_t)((20 - (sizeof(GAME_TITLE) - 1)) / 2), 4, GAME_TITLE);
|
|
691
|
+
draw_text(4, 6, "PRESS START");
|
|
692
|
+
draw_text(5, 8, "1P VS CPU");
|
|
693
|
+
draw_text(3, 10, "NO LINK 2P");
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* ── GAME LOGIC (clay — reshape freely) ── serve: ball to centre, toward
|
|
697
|
+
* the chosen side; alternate the vertical angle each serve. */
|
|
698
|
+
static void serve_ball(uint8_t to_left) {
|
|
699
|
+
bx = SCREEN_W / 2 - BALL_SIZE / 2;
|
|
700
|
+
by = (COURT_TOP + COURT_BOT) / 2 - BALL_SIZE / 2;
|
|
701
|
+
bdx = to_left ? -2 : 2;
|
|
702
|
+
bdy = ((score_p1 + score_cpu) & 1) ? -1 : 1;
|
|
703
|
+
serve_timer = 30; /* half-second breather */
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/* ── GAME LOGIC (clay — reshape freely) ── paddle hit: deflect by where the
|
|
707
|
+
* ball struck. Centre = flat-ish, edges = steep. Max |bdy| is 2; the CPU
|
|
708
|
+
* moves at 1, so an edge hit outruns it — exactly how you beat it. A ±1
|
|
709
|
+
* random "spin" on every return keeps rallies from repeating and guarantees
|
|
710
|
+
* an idle match ENDS (see header). */
|
|
711
|
+
static void deflect(uint8_t paddle_y) {
|
|
712
|
+
int16_t rel = (by + BALL_SIZE / 2) - (int16_t)(paddle_y + PADDLE_H / 2);
|
|
713
|
+
bdy = (int8_t)(rel >> 3);
|
|
714
|
+
bdy += (int8_t)((random8() & 2) - 1); /* spin: -1 or +1 */
|
|
715
|
+
if (bdy > 2) bdy = 2;
|
|
716
|
+
if (bdy < -2) bdy = -2;
|
|
717
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
718
|
+
sfx_paddle();
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/* ── GAME LOGIC (clay — reshape freely) ── enter each state ── */
|
|
722
|
+
static void enter_title(void) {
|
|
723
|
+
state = ST_TITLE;
|
|
724
|
+
msg_active = 0;
|
|
725
|
+
blit_off();
|
|
726
|
+
load_bg_palettes(); /* (re)load CGB palettes with the LCD off */
|
|
727
|
+
load_obj_palettes();
|
|
728
|
+
draw_title();
|
|
729
|
+
draw_window_static();
|
|
730
|
+
draw_hud_now();
|
|
731
|
+
blit_on();
|
|
732
|
+
update_sprites();
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
static void enter_play(void) {
|
|
736
|
+
state = ST_PLAY;
|
|
737
|
+
p1y = (COURT_TOP + COURT_BOT) / 2 - PADDLE_H / 2;
|
|
738
|
+
cpuy = p1y;
|
|
739
|
+
score_p1 = 0; score_cpu = 0;
|
|
740
|
+
new_record = 0;
|
|
741
|
+
rng ^= DIV; /* stir from a free-running timer */
|
|
742
|
+
if (rng == 0) rng = 0xC0A7;
|
|
743
|
+
blit_off();
|
|
744
|
+
load_bg_palettes();
|
|
745
|
+
load_obj_palettes();
|
|
746
|
+
draw_court();
|
|
747
|
+
draw_window_static();
|
|
748
|
+
draw_hud_now();
|
|
749
|
+
blit_on();
|
|
750
|
+
update_sprites();
|
|
751
|
+
hud_dirty = 0; msg_active = 0;
|
|
752
|
+
serve_ball(0);
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
static void enter_over(void) {
|
|
756
|
+
state = ST_OVER;
|
|
757
|
+
if (win_who) { /* you took the match */
|
|
758
|
+
++streak;
|
|
759
|
+
if (streak > record) {
|
|
760
|
+
record = streak;
|
|
761
|
+
new_record = 1;
|
|
762
|
+
record_save(record); /* battery SRAM — survives power-off */
|
|
763
|
+
}
|
|
764
|
+
stage_msg("YOU WIN", msg_l1);
|
|
765
|
+
sfx_win();
|
|
766
|
+
} else { /* CPU took the match */
|
|
767
|
+
streak = 0; /* the streak dies with the loss */
|
|
768
|
+
stage_msg("CPU WINS", msg_l1);
|
|
769
|
+
sfx_lose();
|
|
770
|
+
}
|
|
771
|
+
stage_msg(new_record ? "NEW RECORD" : "PRESS START", msg_l2);
|
|
772
|
+
/* push the final score + (possibly new) record through the vblank queue —
|
|
773
|
+
* direct window writes here land outside vblank and drop on this core. */
|
|
774
|
+
hud_dirty = 1;
|
|
775
|
+
rec_dirty = 1;
|
|
776
|
+
start_msg(); /* then drain the two result lines */
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
/* ── GAME LOGIC (clay — reshape freely) ── one point scored ── */
|
|
780
|
+
static void score_point(uint8_t for_p1) {
|
|
781
|
+
if (for_p1) ++score_p1; else ++score_cpu;
|
|
782
|
+
sfx_point();
|
|
783
|
+
hud_dirty = 1; /* queued — safe while rendering */
|
|
784
|
+
if (score_p1 >= WIN_SCORE) { win_who = 1; enter_over(); return; }
|
|
785
|
+
if (score_cpu >= WIN_SCORE) { win_who = 0; enter_over(); return; }
|
|
786
|
+
serve_ball(for_p1); /* loser of the point serves outward */
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
/* ── GAME LOGIC (clay — reshape freely) ── one ST_PLAY tick. The ball is
|
|
790
|
+
* frozen during the post-point serve pause; the CPU moves at half the
|
|
791
|
+
* player's top speed with a dead zone so it's beatable; collisions are
|
|
792
|
+
* direction-gated so the ball can't double-hit a paddle. */
|
|
793
|
+
static void update_play(uint8_t pad) {
|
|
794
|
+
int16_t target;
|
|
795
|
+
|
|
796
|
+
random8(); /* tick the noise source every frame */
|
|
797
|
+
|
|
798
|
+
/* You — UP/DOWN, 2 px/frame (held, continuous). */
|
|
799
|
+
if ((pad & PAD_UP) && p1y > COURT_TOP) p1y -= 2;
|
|
800
|
+
if ((pad & PAD_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
|
|
801
|
+
|
|
802
|
+
/* CPU — chases the ball centre at 1 px/frame with a small dead zone. */
|
|
803
|
+
target = by + BALL_SIZE / 2 - PADDLE_H / 2;
|
|
804
|
+
if ((int16_t)cpuy + 2 < target && cpuy < COURT_BOT - PADDLE_H) cpuy += 1;
|
|
805
|
+
else if ((int16_t)cpuy > target + 2 && cpuy > COURT_TOP) cpuy -= 1;
|
|
806
|
+
|
|
807
|
+
/* Ball update (frozen during the serve pause). */
|
|
808
|
+
if (serve_timer > 0) { --serve_timer; return; }
|
|
809
|
+
bx += bdx;
|
|
810
|
+
by += bdy;
|
|
811
|
+
|
|
812
|
+
/* Rail bounce. */
|
|
813
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sfx_rail(); }
|
|
814
|
+
if (by + BALL_SIZE > COURT_BOT) { by = COURT_BOT - BALL_SIZE; bdy = -bdy; sfx_rail(); }
|
|
815
|
+
|
|
816
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
817
|
+
if (bdx < 0
|
|
818
|
+
&& bx <= PADDLE_X1 + 8 && bx + BALL_SIZE >= PADDLE_X1
|
|
819
|
+
&& by + BALL_SIZE > p1y && by < p1y + PADDLE_H) {
|
|
820
|
+
bdx = -bdx;
|
|
176
821
|
bx = PADDLE_X1 + 8;
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
&& by < p2y + PADDLE_H) {
|
|
184
|
-
bdx = (int8_t)(-bdx);
|
|
822
|
+
deflect(p1y);
|
|
823
|
+
}
|
|
824
|
+
if (bdx > 0
|
|
825
|
+
&& bx + BALL_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + 8
|
|
826
|
+
&& by + BALL_SIZE > cpuy && by < cpuy + PADDLE_H) {
|
|
827
|
+
bdx = -bdx;
|
|
185
828
|
bx = PADDLE_X2 - BALL_SIZE;
|
|
186
|
-
|
|
187
|
-
|
|
829
|
+
deflect(cpuy);
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
/* Off either side → point (loser serves outward). */
|
|
833
|
+
if (bx + BALL_SIZE < 4) score_point(0); /* past you → CPU scores */
|
|
834
|
+
if (bx > SCREEN_W - 4) score_point(1); /* past CPU → you score */
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
void main(void) {
|
|
838
|
+
uint8_t pad;
|
|
839
|
+
|
|
840
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
841
|
+
* Boot order: LCD defaults (installs the OAM-DMA HRAM stub) → vblank IRQ
|
|
842
|
+
* (so wait_vblank HALTs instead of busy-polling LY — the poll runs at
|
|
843
|
+
* ~1/30 speed on this core) → APU on → LCD OFF → then all the bulk VRAM +
|
|
844
|
+
* palette work (tiles, font, palettes, court). Tile/font/map/palette
|
|
845
|
+
* uploads REQUIRE a VRAM-safe window and boot does them all at once, so
|
|
846
|
+
* LCD-off is the only sane choice here. The window position registers are
|
|
847
|
+
* plain I/O — set once, they hold. */
|
|
848
|
+
lcd_init_default();
|
|
849
|
+
enable_vblank_irq();
|
|
850
|
+
sound_init();
|
|
851
|
+
oam_dma_init_hram();
|
|
852
|
+
oam_clear();
|
|
853
|
+
music_on = 1; /* background music on by default (SELECT toggles) */
|
|
854
|
+
LCDC = 0;
|
|
855
|
+
WY = WINY; /* window HUD strip: bottom 8 pixel rows */
|
|
856
|
+
WX = 7; /* WX is offset by 7 — this is the left edge */
|
|
857
|
+
|
|
858
|
+
upload_tile(T_PADDLE, tile_paddle);
|
|
859
|
+
upload_tile(T_BALL, tile_ball);
|
|
860
|
+
upload_tile(T_FLOOR, tile_floor);
|
|
861
|
+
upload_tile(T_RAIL, tile_rail);
|
|
862
|
+
upload_tile(T_NET, tile_net);
|
|
863
|
+
upload_font();
|
|
864
|
+
|
|
865
|
+
load_bg_palettes(); /* the CGB BG palettes — court colours + HUD */
|
|
866
|
+
load_obj_palettes(); /* you (cyan) / CPU (red) / ball OBJ palettes */
|
|
867
|
+
|
|
868
|
+
record = record_load(); /* battery SRAM — 0 on a fresh cart */
|
|
869
|
+
streak = 0;
|
|
870
|
+
enter_title();
|
|
871
|
+
|
|
872
|
+
/* Main loop, one pass per frame. The order is deliberate: the two VRAM/
|
|
873
|
+
* OAM writers (sprites, then the bounded HUD commit) run FIRST so they
|
|
874
|
+
* land inside vblank; audio and game logic follow. */
|
|
875
|
+
while (1) {
|
|
876
|
+
wait_vblank();
|
|
877
|
+
update_sprites(); /* OAM DMA FIRST — must land in vblank (no tear) */
|
|
878
|
+
hud_commit(); /* then the few queued HUD/result writes (bank 0) */
|
|
879
|
+
sfx_tick();
|
|
880
|
+
music_tick();
|
|
881
|
+
|
|
882
|
+
pad = joypad_read();
|
|
883
|
+
|
|
884
|
+
/* SELECT toggles the background music, in any state */
|
|
885
|
+
if ((pad & PAD_SELECT) && !(prev_pad & PAD_SELECT)) music_toggle();
|
|
886
|
+
|
|
887
|
+
if (state == ST_TITLE) {
|
|
888
|
+
/* ── GAME LOGIC (clay) ── press-start title (handheld: no 2P
|
|
889
|
+
* mode select — see the header note) */
|
|
890
|
+
if (((pad & PAD_START) && !(prev_pad & PAD_START))
|
|
891
|
+
|| ((pad & PAD_A) && !(prev_pad & PAD_A))) enter_play();
|
|
892
|
+
} else if (state == ST_PLAY) {
|
|
893
|
+
update_play(pad);
|
|
894
|
+
} else { /* ST_OVER — START/A returns to the title (shows the record) */
|
|
895
|
+
if (((pad & PAD_START) && !(prev_pad & PAD_START))
|
|
896
|
+
|| ((pad & PAD_A) && !(prev_pad & PAD_A))) enter_title();
|
|
897
|
+
}
|
|
188
898
|
|
|
189
|
-
|
|
190
|
-
if (bx > 156) { if (score_p1 < 9) score_p1++; sound_play_noise(8); serve_ball(1); }
|
|
899
|
+
prev_pad = pad;
|
|
191
900
|
}
|
|
192
|
-
}
|
|
193
901
|
}
|