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,211 +1,503 @@
|
|
|
1
|
-
/* ── sports.c — Game Boy Advance
|
|
1
|
+
/* ── sports.c — Game Boy Advance versus court game (complete game) ────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* RALLY ROVER — a COMPLETE, working game: press-start title, 1P vs a beatable
|
|
4
|
+
* CPU on a netted court (Pong lineage), first-to-5 match flow with a result
|
|
5
|
+
* screen, a PRNG rally "spin" so an idle match provably ENDS, music + SFX, and
|
|
6
|
+
* a persistent RECORD in cartridge SRAM (longest win streak vs the CPU). The
|
|
7
|
+
* court and sprites are VIVID — the GBA's 15-bit palette gives 32768 colours,
|
|
8
|
+
* so the two paddles read as a blue team and a red team over a green court
|
|
9
|
+
* with a bright dashed net, not flat blocks on black.
|
|
6
10
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
11
|
+
* The game: your paddle (left, blue) moves UP/DOWN; the CPU paddle (right, red)
|
|
12
|
+
* chases the ball at half your top speed, so a steep edge-deflection outruns
|
|
13
|
+
* it — that's exactly how you beat it. Win a point when the ball passes the
|
|
14
|
+
* far paddle; first to 5 takes the match. Win without losing and your streak
|
|
15
|
+
* grows; the longest streak persists across power cycles.
|
|
12
16
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* —
|
|
17
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
18
|
+
* very different one. The markers tell you what's what:
|
|
19
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented GBA footgun; reshape
|
|
20
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
21
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring: reshape
|
|
22
|
+
* freely.
|
|
23
|
+
*
|
|
24
|
+
* What depends on what:
|
|
25
|
+
* gba_sfx.{h,c} — PSG sound: sfx_tone/sfx_noise one-shots + the music loop
|
|
26
|
+
* (sfx_music_tick once per frame — forget it and the game is silent).
|
|
27
|
+
* libtonc (the build links it) — VBlankIntrWait/key_poll/TTE/tonccpy.
|
|
28
|
+
*
|
|
29
|
+
* HANDHELD, SO SINGLE-PLAYER ONLY (honest note): 2P versus on the GBA means a
|
|
30
|
+
* link cable between two units — a second emulator instance this environment
|
|
31
|
+
* can't provide. So RALLY ROVER is 1P vs a beatable CPU, not split-screen
|
|
32
|
+
* versus. (Contrast the NES/Genesis sports templates, which ARE 2P versus —
|
|
33
|
+
* two controllers on one machine — AND a 1P-vs-CPU mode.)
|
|
34
|
+
*
|
|
35
|
+
* WHY THE PRNG MATTERS (a teaching point shared with the NES sports template):
|
|
36
|
+
* the GBA is fully deterministic. Without a noise source, the CPU's fixed
|
|
37
|
+
* ball-chase and the fixed wall/paddle bounces lock into an identical rally
|
|
38
|
+
* cycle that NEVER ends — the ball orbits the court forever and no point is
|
|
39
|
+
* ever scored. random8() adds a ±1 "spin" to every paddle return, so rallies
|
|
40
|
+
* always drift, break symmetry, and an idle match reaches 5-0 on its own.
|
|
16
41
|
*/
|
|
17
42
|
|
|
18
43
|
#include <tonc.h>
|
|
19
44
|
#include "gba_sfx.h"
|
|
20
45
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
#define
|
|
24
|
-
|
|
46
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
47
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
48
|
+
#define GAME_TITLE "RALLY ROVER"
|
|
49
|
+
|
|
50
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
51
|
+
* Court geometry + match rules. The court interior is bounded top/bottom by
|
|
52
|
+
* rail tiles; paddles and the ball stay between COURT_TOP and COURT_BOT (pixel
|
|
53
|
+
* rows). Paddles are 24 px tall (3 stacked 8x8 sprites), 8 px wide. */
|
|
54
|
+
#define COURT_TOP 16 /* first pixel row below the top rail */
|
|
55
|
+
#define COURT_BOT 144 /* first pixel row of the bottom rail */
|
|
56
|
+
#define PADDLE_H 24 /* 3 stacked 8x8 sprites */
|
|
57
|
+
#define PADDLE_X1 16 /* P1 — left side (you) */
|
|
58
|
+
#define PADDLE_X2 216 /* CPU — right side */
|
|
25
59
|
#define BALL_SIZE 8
|
|
26
|
-
#define PADDLE_X1 8
|
|
27
|
-
#define PADDLE_X2 (240 - 8 - PADDLE_W)
|
|
28
60
|
#define COURT_W 240
|
|
61
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
62
|
+
#define P1_PAL 0 /* OBJ palbank 0 = blue (you) */
|
|
63
|
+
#define CPU_PAL 1 /* OBJ palbank 1 = red (CPU) */
|
|
64
|
+
#define BALL_PAL 2 /* OBJ palbank 2 = white ball */
|
|
65
|
+
|
|
66
|
+
/* Sprite slot discipline (128 OAM entries; we use 8):
|
|
67
|
+
* 0..2 → P1 paddle (3 stacked 8x8)
|
|
68
|
+
* 3..5 → CPU paddle
|
|
69
|
+
* 6 → ball */
|
|
70
|
+
#define SLOT_P1 0
|
|
71
|
+
#define SLOT_CPU 3
|
|
72
|
+
#define SLOT_BALL 6
|
|
29
73
|
|
|
30
|
-
#define TILE_PADDLE 1
|
|
31
|
-
#define TILE_BALL 2
|
|
74
|
+
#define TILE_PADDLE 1 /* OBJ tile 1 = solid paddle block (4bpp 8x8) */
|
|
75
|
+
#define TILE_BALL 2 /* OBJ tile 2 = round ball (4bpp 8x8) */
|
|
32
76
|
|
|
33
|
-
/* 4bpp
|
|
34
|
-
|
|
77
|
+
/* 4bpp sprite tiles (8 rows × 32 bits; each nibble is a palette index within
|
|
78
|
+
* the sprite's palbank. Index 0 = transparent). The paddle is a solid colour-1
|
|
79
|
+
* block; its TEAM colour comes from the OBJ PALBANK at draw time (bank 0 = blue,
|
|
80
|
+
* bank 1 = red), so ONE tile serves both paddles. */
|
|
81
|
+
static const u32 tile_paddle[8] = {
|
|
35
82
|
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
36
83
|
0x11111111, 0x11111111, 0x11111111, 0x11111111,
|
|
37
84
|
};
|
|
38
|
-
/*
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
/*
|
|
45
|
-
*
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
85
|
+
/* The ball: a round white pip with a soft glint (idx 2 highlight, idx 1 body). */
|
|
86
|
+
static const u32 tile_ball[8] = {
|
|
87
|
+
0x00111100, 0x01122110, 0x11222111, 0x11221111,
|
|
88
|
+
0x11111111, 0x11111111, 0x01111110, 0x00111100,
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/* ── GAME LOGIC (clay) — BG court tiles (regular Mode-0 4bpp BG tiles).
|
|
92
|
+
* Each 8x8 4bpp tile is 8 u32 rows; each nibble is a palette index within the
|
|
93
|
+
* BG palbank we use (bank 0). Index 0 = transparent → shows the backdrop. */
|
|
94
|
+
#define BG_FLOOR 1 /* court surface (two-green dither so it isn't flat) */
|
|
95
|
+
#define BG_RAIL 2 /* top/bottom court rails */
|
|
96
|
+
#define BG_NET 3 /* dashed centre net */
|
|
97
|
+
#define BG_PIP 4 /* score pip (a lit cell — see the score-pip idiom) */
|
|
98
|
+
|
|
99
|
+
static const u32 bg_tile_floor[8] = { /* two-green checker, no flat colour */
|
|
100
|
+
0x11221122, 0x11221122, 0x22112211, 0x22112211,
|
|
101
|
+
0x11221122, 0x11221122, 0x22112211, 0x22112211,
|
|
102
|
+
};
|
|
103
|
+
static const u32 bg_tile_rail[8] = { /* solid bright rail */
|
|
104
|
+
0x33333333, 0x33333333, 0x33333333, 0x33333333,
|
|
105
|
+
0x33333333, 0x33333333, 0x33333333, 0x33333333,
|
|
49
106
|
};
|
|
50
|
-
static const u32
|
|
51
|
-
|
|
52
|
-
|
|
107
|
+
static const u32 bg_tile_net[8] = { /* dashed vertical net segment */
|
|
108
|
+
0x00033000, 0x00033000, 0x00000000, 0x00033000,
|
|
109
|
+
0x00033000, 0x00000000, 0x00033000, 0x00033000,
|
|
110
|
+
};
|
|
111
|
+
static const u32 bg_tile_pip[8] = { /* a lit score pip (filled diamond) */
|
|
112
|
+
0x00044000, 0x00444400, 0x04444440, 0x44444444,
|
|
113
|
+
0x44444444, 0x04444440, 0x00444400, 0x00044000,
|
|
53
114
|
};
|
|
54
115
|
|
|
55
|
-
|
|
116
|
+
/* ── GAME LOGIC (clay — reshape freely) — game state (plain BSS; the GBA has
|
|
117
|
+
* 256 KB of EWRAM + 32 KB of IWRAM).
|
|
118
|
+
* NOTE for headless verification: unlike the Genesis template (whose work-RAM
|
|
119
|
+
* globals are readable by symbol name), the GBA libretro core exposes NO
|
|
120
|
+
* IWRAM/EWRAM region, so a headless agent reads game state from what's ON
|
|
121
|
+
* HARDWARE — OAM (the paddles + ball), the BG0 tilemap (the court + the SCORE
|
|
122
|
+
* PIPS, see the score-pip idiom), and save_ram (the record). Keep game globals
|
|
123
|
+
* static and surface anything the harness must read onto hardware. */
|
|
124
|
+
#define ST_TITLE 0
|
|
125
|
+
#define ST_PLAY 1
|
|
126
|
+
#define ST_OVER 2
|
|
127
|
+
static u8 state;
|
|
128
|
+
|
|
129
|
+
static s16 p1y, cpuy; /* paddle top Y (pixels) */
|
|
130
|
+
static s16 bx, by; /* ball top-left position */
|
|
131
|
+
static s16 bdx, bdy; /* ball velocity (px/frame) */
|
|
132
|
+
static u8 score_p1, score_cpu; /* 0..WIN_SCORE */
|
|
133
|
+
static u8 serve_timer; /* freeze frames between points */
|
|
134
|
+
static u8 streak; /* current win streak vs CPU (RAM) */
|
|
135
|
+
static u16 record; /* battery-backed best streak — see SRAM idiom*/
|
|
136
|
+
static u8 new_record; /* result screen shows NEW RECORD */
|
|
137
|
+
static u8 win_who; /* 1 = you took the match, 0 = CPU did */
|
|
138
|
+
|
|
139
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a handful of ARM instructions).
|
|
140
|
+
* THE LOAD-BEARING DETAIL of a deterministic versus game: see the file header.
|
|
141
|
+
* Ticked once per play frame so two identical board states a few frames apart
|
|
142
|
+
* still diverge, and added as ±1 spin to every paddle return so rallies END. */
|
|
143
|
+
static u16 rng = 0xC0A7;
|
|
144
|
+
static u8 random8(void) {
|
|
145
|
+
u16 r = rng;
|
|
146
|
+
r ^= r << 7;
|
|
147
|
+
r ^= r >> 9;
|
|
148
|
+
r ^= r << 8;
|
|
149
|
+
rng = r;
|
|
150
|
+
return (u8)r;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
154
|
+
* PERSISTENT SRAM at 0x0E000000. Two footguns, both fatal-but-silent:
|
|
155
|
+
* 1. The SRAM bus is 8 BITS WIDE. Byte reads/writes only — a u16/u32
|
|
156
|
+
* access doesn't fault, it just reads the same byte mirrored (and a
|
|
157
|
+
* wide write stores one byte), so your data "almost" round-trips and
|
|
158
|
+
* then the checksum never matches. Every access below is via vu8.
|
|
159
|
+
* 2. Emulators and flashcarts detect the SAVE TYPE by scanning the ROM
|
|
160
|
+
* image for a marker string. Without "SRAM_V" in the ROM, mGBA gives
|
|
161
|
+
* the cart NO save memory at all and writes to 0x0E000000 vanish.
|
|
162
|
+
* The aligned, (used)-attributed const below plants that marker —
|
|
163
|
+
* delete it and persistence dies even though this code is untouched.
|
|
164
|
+
* Layout: 'V' 'X' record-lo record-hi checksum (xor ^ 0xA5) — magic+checksum
|
|
165
|
+
* so a fresh (0xFF-filled) cart reads as "no record" instead of garbage.
|
|
166
|
+
* PERSISTENCE CHOICE: a raw hi-score is meaningless for a versus game (every
|
|
167
|
+
* match ends 5-x), so we persist the LONGEST WIN STREAK vs the CPU — the stat
|
|
168
|
+
* a returning player actually chases.
|
|
169
|
+
* requires: nothing else — self-contained; safe to transplant whole. */
|
|
170
|
+
#define SRAM_BYTE ((volatile u8 *)0x0E000000)
|
|
171
|
+
__attribute__((used, aligned(4))) static const char sram_type_marker[] = "SRAM_V113";
|
|
56
172
|
|
|
57
|
-
static
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
173
|
+
static u16 record_load(void) {
|
|
174
|
+
u8 lo, hi;
|
|
175
|
+
if (SRAM_BYTE[0] != 'V' || SRAM_BYTE[1] != 'X') return 0;
|
|
176
|
+
lo = SRAM_BYTE[2];
|
|
177
|
+
hi = SRAM_BYTE[3];
|
|
178
|
+
if (SRAM_BYTE[4] != (u8)(lo ^ hi ^ 0xA5)) return 0;
|
|
179
|
+
return (u16)(lo | (hi << 8));
|
|
180
|
+
}
|
|
62
181
|
|
|
63
|
-
static void
|
|
182
|
+
static void record_save(u16 v) {
|
|
183
|
+
SRAM_BYTE[0] = 'V';
|
|
184
|
+
SRAM_BYTE[1] = 'X';
|
|
185
|
+
SRAM_BYTE[2] = (u8)v;
|
|
186
|
+
SRAM_BYTE[3] = (u8)(v >> 8);
|
|
187
|
+
SRAM_BYTE[4] = (u8)((u8)v ^ (u8)(v >> 8) ^ 0xA5);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/* ── GAME LOGIC (clay) — TTE text helpers ────────────────────────────────────
|
|
191
|
+
* Draw right-aligned decimal digits at pixel (x,y) WITHOUT tte_printf. The
|
|
192
|
+
* bundled libtonc's tte_printf with a %d conversion is broken (it routes
|
|
193
|
+
* through a vsnprintf path that isn't wired in this build — it garbles
|
|
194
|
+
* output AND wedges the loop when called per-frame, GBA-1). We build the
|
|
195
|
+
* string ourselves and use tte_write, which processes the #{P:x,y} position
|
|
196
|
+
* command but does NO format conversion → safe every frame. */
|
|
197
|
+
static void draw_num(int x, int y, unsigned v, int digits) {
|
|
198
|
+
char buf[24];
|
|
199
|
+
int i, n = 0;
|
|
200
|
+
buf[n++] = '#'; buf[n++] = '{'; buf[n++] = 'P'; buf[n++] = ':';
|
|
201
|
+
if (x >= 100) buf[n++] = (char)('0' + (x / 100) % 10);
|
|
202
|
+
if (x >= 10) buf[n++] = (char)('0' + (x / 10) % 10);
|
|
203
|
+
buf[n++] = (char)('0' + x % 10);
|
|
204
|
+
buf[n++] = ',';
|
|
205
|
+
if (y >= 100) buf[n++] = (char)('0' + (y / 100) % 10);
|
|
206
|
+
if (y >= 10) buf[n++] = (char)('0' + (y / 10) % 10);
|
|
207
|
+
buf[n++] = (char)('0' + y % 10);
|
|
208
|
+
buf[n++] = '}';
|
|
209
|
+
for (i = digits - 1; i >= 0; i--) { buf[n + i] = (char)('0' + (v % 10)); v /= 10; }
|
|
210
|
+
n += digits; buf[n] = 0;
|
|
211
|
+
tte_write(buf);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
215
|
+
* THE COURT IS BACKGROUND TILES on BG0 (Mode 0, a REGULAR text BG). A 32x32
|
|
216
|
+
* map (BG_REG_32x32) is one screenblock; each map entry is a u16: tile id
|
|
217
|
+
* (10 bits) + hflip/vflip + a 4-bit palbank. SE_BUILD(tile, palbank, hf, vf)
|
|
218
|
+
* packs it. Footguns this dodges:
|
|
219
|
+
* - VRAM IGNORES BYTE WRITES (a u8 store duplicates the byte into both
|
|
220
|
+
* halves of the 16-bit lane). We only ever write whole u16 SE entries
|
|
221
|
+
* (via set_cell) and tonccpy() tile data — both VRAM-safe.
|
|
222
|
+
* - TTE owns BG1 (CBB 2 / SBB 30). Keep this map (SBB 28) and our tile
|
|
223
|
+
* graphics (CBB 0) clear of those blocks or text and court corrupt each
|
|
224
|
+
* other.
|
|
225
|
+
* requires: REG_BG0CNT → CBB 0 / SBB 28 (set in main), DCNT_BG0 enabled. */
|
|
226
|
+
static SCR_ENTRY *const court_map = se_mem[28];
|
|
227
|
+
static void set_cell(int tx, int ty, u16 tile) {
|
|
228
|
+
court_map[ty * 32 + tx] = SE_BUILD(tile, 0, 0, 0);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
232
|
+
* SCORE PIPS — the headless-readable score. GBA C globals are NOT host-readable
|
|
233
|
+
* (the libretro core exposes no IWRAM/EWRAM region), so a verify harness can't
|
|
234
|
+
* read score_p1/score_cpu by symbol. To keep the score machine-checkable WITHOUT
|
|
235
|
+
* a symbol map, each point is ALSO surfaced onto hardware as a BG "pip" tile in
|
|
236
|
+
* a fixed HUD row: P1's pips grow left-to-right from tx=4, the CPU's grow
|
|
237
|
+
* right-to-left from tx=27. A harness counts BG_PIP tiles (id 4) in row 0 of
|
|
238
|
+
* screenblock 28 to read the exact score — no globals needed. This is the same
|
|
239
|
+
* "decode state from what's on hardware, not from a symbol" discipline the GBA
|
|
240
|
+
* puzzle/platformer templates use for the board and the falling piece.
|
|
241
|
+
* requires: court_map (BG0 SBB 28), BG_PIP tile uploaded, called on every
|
|
242
|
+
* score change and on court paint. */
|
|
243
|
+
#define PIP_ROW 0 /* HUD tile row holding the score pips */
|
|
244
|
+
#define PIP_P1_TX 4 /* P1 pips grow rightward from here */
|
|
245
|
+
#define PIP_CPU_TX 27 /* CPU pips grow leftward from here */
|
|
246
|
+
static void paint_pips(void) {
|
|
247
|
+
int i;
|
|
248
|
+
for (i = 0; i < WIN_SCORE; i++) {
|
|
249
|
+
set_cell(PIP_P1_TX + i, PIP_ROW, (i < score_p1) ? BG_PIP : BG_FLOOR);
|
|
250
|
+
set_cell(PIP_CPU_TX - i, PIP_ROW, (i < score_cpu) ? BG_PIP : BG_FLOOR);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/* Paint the static court: floor everywhere, a HUD band on rows 0-1, top/bottom
|
|
255
|
+
* rails, and the centre net. Done once per match start; the pips + sprites then
|
|
256
|
+
* update over it. The HUD band (rows 0-1) is FLOOR tiles so the score pips and
|
|
257
|
+
* TTE labels read clearly above the play area. */
|
|
258
|
+
static void paint_court(void) {
|
|
259
|
+
int r, c;
|
|
260
|
+
for (r = 0; r < 32; r++)
|
|
261
|
+
for (c = 0; c < 32; c++) {
|
|
262
|
+
u16 t = BG_FLOOR;
|
|
263
|
+
if (r == 2 || r == 18) t = BG_RAIL; /* court rails */
|
|
264
|
+
else if (r > 2 && r < 18 && c == 15) t = BG_NET;
|
|
265
|
+
set_cell(c, r, t);
|
|
266
|
+
}
|
|
267
|
+
paint_pips();
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side ── */
|
|
271
|
+
static void serve_ball(u8 to_left) {
|
|
64
272
|
bx = COURT_W / 2 - BALL_SIZE / 2;
|
|
65
|
-
by = (COURT_TOP + COURT_BOT) / 2;
|
|
273
|
+
by = (COURT_TOP + COURT_BOT) / 2 - BALL_SIZE / 2;
|
|
66
274
|
bdx = to_left ? -2 : 2;
|
|
67
|
-
bdy = ((score_p1 +
|
|
68
|
-
serve_timer = 30;
|
|
275
|
+
bdy = ((score_p1 + score_cpu) & 1) ? -1 : 1; /* alternate the angle */
|
|
276
|
+
serve_timer = 30; /* half-second breather */
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/* ── GAME LOGIC (clay) — HUD / screens (TTE on BG1, priority 0) ── */
|
|
280
|
+
static void draw_hud_labels(void) {
|
|
281
|
+
tte_erase_screen();
|
|
282
|
+
tte_write("#{P:8,1}YOU");
|
|
283
|
+
tte_write("#{P:200,1}CPU");
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
static void enter_title(void) {
|
|
287
|
+
state = ST_TITLE;
|
|
288
|
+
paint_court();
|
|
289
|
+
tte_erase_screen();
|
|
290
|
+
tte_write("#{P:64,40}" GAME_TITLE);
|
|
291
|
+
tte_write("#{P:76,72}PRESS START");
|
|
292
|
+
tte_write("#{P:80,92}RECORD");
|
|
293
|
+
draw_num(132, 92, record, 3);
|
|
294
|
+
tte_write("#{P:24,116}UP DOWN MOVE - 1P VS CPU");
|
|
295
|
+
tte_write("#{P:36,128}NO LINK CABLE 2P");
|
|
69
296
|
}
|
|
70
297
|
|
|
71
|
-
static void
|
|
298
|
+
static void enter_play(void) {
|
|
299
|
+
state = ST_PLAY;
|
|
72
300
|
p1y = (COURT_TOP + COURT_BOT) / 2 - PADDLE_H / 2;
|
|
73
|
-
|
|
74
|
-
score_p1 = 0;
|
|
75
|
-
|
|
301
|
+
cpuy = p1y;
|
|
302
|
+
score_p1 = 0; score_cpu = 0;
|
|
303
|
+
new_record = 0;
|
|
304
|
+
/* Stir the PRNG with time-on-title so each run differs. */
|
|
305
|
+
rng ^= (u16)REG_VCOUNT ^ ((u16)REG_VCOUNT << 7);
|
|
306
|
+
if (rng == 0) rng = 0xC0A7;
|
|
307
|
+
paint_court();
|
|
308
|
+
draw_hud_labels();
|
|
76
309
|
serve_ball(0);
|
|
77
310
|
}
|
|
78
311
|
|
|
312
|
+
static void enter_over(void) {
|
|
313
|
+
state = ST_OVER;
|
|
314
|
+
if (win_who) { /* you took the match */
|
|
315
|
+
++streak;
|
|
316
|
+
if (streak > record) {
|
|
317
|
+
record = streak;
|
|
318
|
+
new_record = 1;
|
|
319
|
+
record_save(record); /* byte-wise SRAM write — see idiom */
|
|
320
|
+
}
|
|
321
|
+
tte_write("#{P:84,56}YOU WIN");
|
|
322
|
+
} else { /* CPU took the match */
|
|
323
|
+
streak = 0; /* the streak dies with the loss */
|
|
324
|
+
tte_write("#{P:84,56}CPU WINS");
|
|
325
|
+
}
|
|
326
|
+
if (new_record) tte_write("#{P:72,72}NEW RECORD");
|
|
327
|
+
tte_write("#{P:76,92}PRESS START");
|
|
328
|
+
/* End-of-match whistle: two quick tones (won = rising, lost = falling). */
|
|
329
|
+
sfx_tone(1, win_who ? 1500 : 1100, 10);
|
|
330
|
+
sfx_tone(2, win_who ? 1750 : 900, 12);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
334
|
+
static void score_point(u8 for_p1) {
|
|
335
|
+
if (for_p1) ++score_p1; else ++score_cpu;
|
|
336
|
+
sfx_noise(8);
|
|
337
|
+
paint_pips(); /* surface the new score on hardware */
|
|
338
|
+
if (score_p1 >= WIN_SCORE) { win_who = 1; enter_over(); return; }
|
|
339
|
+
if (score_cpu >= WIN_SCORE) { win_who = 0; enter_over(); return; }
|
|
340
|
+
serve_ball(for_p1); /* loser of the point serves outward */
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
|
|
344
|
+
* Centre = flat-ish, edges = steep. Max |bdy| is 2; the CPU moves at 1, so an
|
|
345
|
+
* edge hit is exactly how a human beats it. A ±1 random "spin" on every return
|
|
346
|
+
* keeps rallies from repeating and guarantees an idle match ENDS (see header). */
|
|
347
|
+
static void deflect(s16 paddle_y) {
|
|
348
|
+
s16 rel = (by + BALL_SIZE / 2) - (paddle_y + PADDLE_H / 2);
|
|
349
|
+
bdy = (s16)(rel >> 3);
|
|
350
|
+
bdy += (s16)((random8() & 2) - 1); /* spin: -1 or +1 */
|
|
351
|
+
if (bdy > 2) bdy = 2;
|
|
352
|
+
if (bdy < -2) bdy = -2;
|
|
353
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
354
|
+
sfx_tone(1, 1500, 3);
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
/* ── GAME LOGIC (clay) — one ST_PLAY tick. Edge cases handled: the ball is
|
|
358
|
+
* frozen during the post-point serve pause; the CPU moves at half the player's
|
|
359
|
+
* top speed with a dead zone so it's beatable; collisions are direction-gated
|
|
360
|
+
* so the ball can't double-hit a paddle. May end the match (point → first-to-5).
|
|
361
|
+
*/
|
|
362
|
+
static void update_play(void) {
|
|
363
|
+
s16 target;
|
|
364
|
+
|
|
365
|
+
random8(); /* tick the noise source every frame */
|
|
366
|
+
|
|
367
|
+
/* You — UP/DOWN, 3 px/frame (key_held = continuous hold). */
|
|
368
|
+
if (key_held(KEY_UP) && p1y > COURT_TOP) p1y -= 3;
|
|
369
|
+
if (key_held(KEY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 3;
|
|
370
|
+
|
|
371
|
+
/* CPU — chases the ball centre at 1 px/frame (a third of your speed) with a
|
|
372
|
+
* small dead zone. Beatable by design: steep deflections outrun it. */
|
|
373
|
+
target = by + BALL_SIZE / 2 - PADDLE_H / 2;
|
|
374
|
+
if (cpuy + 2 < target && cpuy < COURT_BOT - PADDLE_H) cpuy += 1;
|
|
375
|
+
else if (cpuy > target + 2 && cpuy > COURT_TOP) cpuy -= 1;
|
|
376
|
+
|
|
377
|
+
/* Ball update (frozen during the post-point serve pause). */
|
|
378
|
+
if (serve_timer > 0) { --serve_timer; return; }
|
|
379
|
+
bx += bdx;
|
|
380
|
+
by += bdy;
|
|
381
|
+
|
|
382
|
+
/* Rail bounce. */
|
|
383
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sfx_tone(2, 1100, 2); }
|
|
384
|
+
if (by + BALL_SIZE > COURT_BOT) { by = COURT_BOT - BALL_SIZE; bdy = -bdy; sfx_tone(2, 1100, 2); }
|
|
385
|
+
|
|
386
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
387
|
+
if (bdx < 0
|
|
388
|
+
&& bx <= PADDLE_X1 + 8 && bx + BALL_SIZE >= PADDLE_X1
|
|
389
|
+
&& by + BALL_SIZE > p1y && by < p1y + PADDLE_H) {
|
|
390
|
+
bdx = -bdx;
|
|
391
|
+
bx = PADDLE_X1 + 8;
|
|
392
|
+
deflect(p1y);
|
|
393
|
+
}
|
|
394
|
+
if (bdx > 0
|
|
395
|
+
&& bx + BALL_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + 8
|
|
396
|
+
&& by + BALL_SIZE > cpuy && by < cpuy + PADDLE_H) {
|
|
397
|
+
bdx = -bdx;
|
|
398
|
+
bx = PADDLE_X2 - BALL_SIZE;
|
|
399
|
+
deflect(cpuy);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/* Off either side → point (loser serves outward). */
|
|
403
|
+
if (bx + BALL_SIZE < 4) score_point(0); /* past you → CPU scores */
|
|
404
|
+
if (bx > COURT_W - 4) score_point(1); /* past CPU → you score */
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/* ── GAME LOGIC (clay) — stage the sprites: 3+3 paddle tiles + the ball.
|
|
408
|
+
* Off-screen / inactive slots park at y=200. The paddles carry their TEAM
|
|
409
|
+
* colour via the OBJ PALBANK (bank 0 = blue you, bank 1 = red CPU) — one tile,
|
|
410
|
+
* two coloured paddles. ── */
|
|
411
|
+
static OBJ_ATTR obj_buffer[128];
|
|
412
|
+
static void stage_sprites(void) {
|
|
413
|
+
int i;
|
|
414
|
+
int playing = (state == ST_PLAY || state == ST_OVER);
|
|
415
|
+
for (i = 0; i < PADDLE_H / 8; i++) {
|
|
416
|
+
obj_set_attr(&obj_buffer[SLOT_P1 + i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
417
|
+
(u16)(ATTR2_PALBANK(P1_PAL) | TILE_PADDLE));
|
|
418
|
+
obj_set_pos(&obj_buffer[SLOT_P1 + i], playing ? PADDLE_X1 : 250, playing ? (p1y + i * 8) : 200);
|
|
419
|
+
obj_set_attr(&obj_buffer[SLOT_CPU + i], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
420
|
+
(u16)(ATTR2_PALBANK(CPU_PAL) | TILE_PADDLE));
|
|
421
|
+
obj_set_pos(&obj_buffer[SLOT_CPU + i], playing ? PADDLE_X2 : 250, playing ? (cpuy + i * 8) : 200);
|
|
422
|
+
}
|
|
423
|
+
obj_set_attr(&obj_buffer[SLOT_BALL], ATTR0_SQUARE, ATTR1_SIZE_8,
|
|
424
|
+
(u16)(ATTR2_PALBANK(BALL_PAL) | TILE_BALL));
|
|
425
|
+
/* The ball hides on the title and during the result freeze. */
|
|
426
|
+
obj_set_pos(&obj_buffer[SLOT_BALL], (state == ST_PLAY) ? bx : 250, (state == ST_PLAY) ? by : 200);
|
|
427
|
+
}
|
|
428
|
+
|
|
79
429
|
int main(void) {
|
|
80
|
-
/*
|
|
81
|
-
|
|
430
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
431
|
+
* Init order: tiles/palettes → oam_init → irq_init + II_VBLANK → TTE init
|
|
432
|
+
* → DISPCNT last. VBlankIntrWait() HANGS FOREVER without the vblank IRQ
|
|
433
|
+
* registered (the #1 "frozen on frame 1" cause), and enabling DISPCNT
|
|
434
|
+
* layers before their tiles/maps exist flashes garbage. TTE owns BG1
|
|
435
|
+
* (CBB 2 / SBB 30) — keep other layers off those blocks.
|
|
436
|
+
* requires: nothing prior; this IS the boot. */
|
|
437
|
+
|
|
438
|
+
/* BG palette (bank 0). Vivid court: two greens for the floor dither, a
|
|
439
|
+
* bright rail, a white net, a hot-gold score pip. The GBA's 15-bit RGB
|
|
440
|
+
* gives saturated colours the GB/NES can only hint at. */
|
|
441
|
+
pal_bg_mem[0] = RGB15(1, 4, 2); /* backdrop / transparent base */
|
|
442
|
+
pal_bg_mem[1] = RGB15(4, 18, 6); /* court green (light) */
|
|
443
|
+
pal_bg_mem[2] = RGB15(2, 11, 4); /* court green (dark) */
|
|
444
|
+
pal_bg_mem[3] = RGB15(28, 30, 31); /* rail + net (near-white) */
|
|
445
|
+
pal_bg_mem[4] = RGB15(31, 26, 6); /* score pip (hot gold) */
|
|
82
446
|
|
|
83
|
-
/*
|
|
84
|
-
tonccpy(&tile_mem[
|
|
85
|
-
tonccpy(&tile_mem[
|
|
447
|
+
/* BG tile graphics → char-block 0 (TTE uses CBB 2 — kept clear). */
|
|
448
|
+
tonccpy(&tile_mem[0][BG_FLOOR], bg_tile_floor, sizeof(bg_tile_floor));
|
|
449
|
+
tonccpy(&tile_mem[0][BG_RAIL], bg_tile_rail, sizeof(bg_tile_rail));
|
|
450
|
+
tonccpy(&tile_mem[0][BG_NET], bg_tile_net, sizeof(bg_tile_net));
|
|
451
|
+
tonccpy(&tile_mem[0][BG_PIP], bg_tile_pip, sizeof(bg_tile_pip));
|
|
86
452
|
|
|
87
|
-
|
|
453
|
+
/* Sprite tiles → OBJ char base (tile_mem[4]). */
|
|
454
|
+
tonccpy(&tile_mem[4][TILE_PADDLE], tile_paddle, sizeof(tile_paddle));
|
|
455
|
+
tonccpy(&tile_mem[4][TILE_BALL], tile_ball, sizeof(tile_ball));
|
|
456
|
+
|
|
457
|
+
/* OBJ palettes: bank 0 = blue (you), bank 1 = red (CPU), bank 2 = white
|
|
458
|
+
* ball with a pale-blue glint. One paddle tile, two team colours. */
|
|
459
|
+
pal_obj_bank[P1_PAL][1] = RGB15(8, 14, 31); /* your paddle blue */
|
|
460
|
+
pal_obj_bank[CPU_PAL][1] = RGB15(31, 8, 8); /* CPU paddle red */
|
|
461
|
+
pal_obj_bank[BALL_PAL][1] = RGB15(30, 30, 31); /* ball body white */
|
|
462
|
+
pal_obj_bank[BALL_PAL][2] = RGB15(20, 26, 31); /* ball glint pale-blue */
|
|
463
|
+
|
|
464
|
+
REG_BG0CNT = BG_CBB(0) | BG_SBB(28) | BG_REG_32x32 | BG_4BPP | BG_PRIO(2);
|
|
465
|
+
|
|
466
|
+
oam_init(obj_buffer, 128); /* hides all 128 */
|
|
88
467
|
|
|
89
|
-
/* IRQ setup — required for VBlankIntrWait() to function. */
|
|
90
468
|
irq_init(NULL);
|
|
91
469
|
irq_add(II_VBLANK, NULL);
|
|
92
470
|
|
|
93
|
-
sfx_init();
|
|
94
|
-
|
|
95
|
-
/* TTE for scores + hint (BG0). */
|
|
96
|
-
tte_init_chr4c_default(0, BG_CBB(0) | BG_SBB(31));
|
|
97
|
-
|
|
98
|
-
/* Court background on BG1 so the arena reads as a real Pong court, not
|
|
99
|
-
* flat black. Tiles in char-block 1, map in screen-block 29. */
|
|
100
|
-
pal_bg_mem[1] = RGB15(2, 12, 4); /* court green (light) */
|
|
101
|
-
pal_bg_mem[2] = CLR_WHITE; /* net */
|
|
102
|
-
pal_bg_mem[3] = RGB15(1, 8, 3); /* court green (dark) */
|
|
103
|
-
tonccpy(&tile_mem[1][TILE_COURT], tile_court, sizeof(tile_court));
|
|
104
|
-
tonccpy(&tile_mem[1][TILE_NET], tile_net, sizeof(tile_net));
|
|
105
|
-
{
|
|
106
|
-
SCR_ENTRY *cmap = se_mem[29];
|
|
107
|
-
int tx, ty;
|
|
108
|
-
for (ty = 0; ty < 32; ty++)
|
|
109
|
-
for (tx = 0; tx < 32; tx++)
|
|
110
|
-
cmap[ty * 32 + tx] = SE_BUILD(
|
|
111
|
-
(tx == 15) ? TILE_NET : TILE_COURT, 0, 0, 0);
|
|
112
|
-
}
|
|
113
|
-
REG_BG1CNT = BG_CBB(1) | BG_SBB(29) | BG_REG_32x32 | BG_4BPP | BG_PRIO(3);
|
|
471
|
+
sfx_init(); /* APU on; music loop ticks below */
|
|
114
472
|
|
|
473
|
+
/* TTE text on BG1 (4bpp char block 2, screenblock 30), priority 0 so text
|
|
474
|
+
* draws over everything. Mode 0 = all four BGs regular/tiled. */
|
|
475
|
+
tte_init_chr4c_default(1, BG_CBB(2) | BG_SBB(30));
|
|
476
|
+
REG_BG1CNT |= BG_PRIO(0);
|
|
115
477
|
REG_DISPCNT = DCNT_MODE0 | DCNT_BG0 | DCNT_BG1 | DCNT_OBJ | DCNT_OBJ_1D;
|
|
116
|
-
tte_write("#{P:16,2}P1");
|
|
117
|
-
tte_write("#{P:208,2}P2");
|
|
118
|
-
tte_write("#{P:36,150}UP/DOWN MOVES YOUR PADDLE");
|
|
119
478
|
|
|
120
|
-
|
|
479
|
+
record = record_load(); /* cartridge SRAM — 0 on first boot */
|
|
480
|
+
streak = 0;
|
|
481
|
+
enter_title();
|
|
121
482
|
|
|
122
483
|
while (1) {
|
|
484
|
+
/* Idiomatic Tonc heartbeat: wait vblank, poll keys, update, then commit
|
|
485
|
+
* OAM while still inside vblank (the update is far quicker than the
|
|
486
|
+
* 4.9ms vblank window). */
|
|
123
487
|
VBlankIntrWait();
|
|
124
488
|
key_poll();
|
|
489
|
+
sfx_music_tick(); /* forget this → silent game */
|
|
125
490
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
if (
|
|
129
|
-
|
|
130
|
-
/* AI right paddle — tracks ball. */
|
|
131
|
-
s16 target = by - PADDLE_H / 2;
|
|
132
|
-
if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 2;
|
|
133
|
-
else if (p2y > target && p2y > COURT_TOP) p2y -= 2;
|
|
134
|
-
|
|
135
|
-
if (serve_timer > 0) {
|
|
136
|
-
serve_timer--;
|
|
491
|
+
if (state == ST_TITLE) {
|
|
492
|
+
if (key_hit(KEY_START | KEY_A)) enter_play();
|
|
493
|
+
} else if (state == ST_OVER) {
|
|
494
|
+
if (key_hit(KEY_START)) enter_title();
|
|
137
495
|
} else {
|
|
138
|
-
|
|
139
|
-
by += bdy;
|
|
140
|
-
|
|
141
|
-
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sfx_tone(2, 1100, 2); }
|
|
142
|
-
if (by + BALL_SIZE > COURT_BOT) {
|
|
143
|
-
by = COURT_BOT - BALL_SIZE;
|
|
144
|
-
bdy = -bdy;
|
|
145
|
-
sfx_tone(2, 1100, 2); /* wall blip */
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
if (bdx < 0
|
|
149
|
-
&& bx <= PADDLE_X1 + PADDLE_W
|
|
150
|
-
&& bx + BALL_SIZE >= PADDLE_X1
|
|
151
|
-
&& by + BALL_SIZE > p1y
|
|
152
|
-
&& by < p1y + PADDLE_H) {
|
|
153
|
-
bdx = -bdx;
|
|
154
|
-
bx = PADDLE_X1 + PADDLE_W;
|
|
155
|
-
sfx_tone(1, 1500, 3); /* paddle hit */
|
|
156
|
-
}
|
|
157
|
-
if (bdx > 0
|
|
158
|
-
&& bx + BALL_SIZE >= PADDLE_X2
|
|
159
|
-
&& bx <= PADDLE_X2 + PADDLE_W
|
|
160
|
-
&& by + BALL_SIZE > p2y
|
|
161
|
-
&& by < p2y + PADDLE_H) {
|
|
162
|
-
bdx = -bdx;
|
|
163
|
-
bx = PADDLE_X2 - BALL_SIZE;
|
|
164
|
-
sfx_tone(1, 1500, 3);
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
if (bx + BALL_SIZE < 0) {
|
|
168
|
-
if (score_p2 < 9) score_p2++;
|
|
169
|
-
sfx_noise(20); /* point lost — buzz */
|
|
170
|
-
serve_ball(0);
|
|
171
|
-
}
|
|
172
|
-
if (bx > COURT_W) {
|
|
173
|
-
if (score_p1 < 9) score_p1++;
|
|
174
|
-
sfx_tone(1, 1900, 16); /* point won — chime */
|
|
175
|
-
serve_ball(1);
|
|
176
|
-
}
|
|
496
|
+
update_play();
|
|
177
497
|
}
|
|
178
498
|
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
* 6 = ball */
|
|
182
|
-
for (int i = 0; i < PADDLE_H / 8; i++) {
|
|
183
|
-
obj_set_attr(&obj_buffer[i],
|
|
184
|
-
ATTR0_SQUARE,
|
|
185
|
-
ATTR1_SIZE_8,
|
|
186
|
-
ATTR2_PALBANK(0) | TILE_PADDLE);
|
|
187
|
-
obj_set_pos(&obj_buffer[i], PADDLE_X1, p1y + i * 8);
|
|
188
|
-
}
|
|
189
|
-
for (int i = 0; i < PADDLE_H / 8; i++) {
|
|
190
|
-
obj_set_attr(&obj_buffer[3 + i],
|
|
191
|
-
ATTR0_SQUARE,
|
|
192
|
-
ATTR1_SIZE_8,
|
|
193
|
-
ATTR2_PALBANK(0) | TILE_PADDLE);
|
|
194
|
-
obj_set_pos(&obj_buffer[3 + i], PADDLE_X2, p2y + i * 8);
|
|
195
|
-
}
|
|
196
|
-
obj_set_attr(&obj_buffer[6],
|
|
197
|
-
ATTR0_SQUARE,
|
|
198
|
-
ATTR1_SIZE_8,
|
|
199
|
-
ATTR2_PALBANK(0) | TILE_BALL);
|
|
200
|
-
obj_set_pos(&obj_buffer[6], bx, by);
|
|
201
|
-
|
|
202
|
-
oam_copy(oam_mem, obj_buffer, 7);
|
|
203
|
-
|
|
204
|
-
/* Score digits. */
|
|
205
|
-
tte_erase_rect(28, 2, 36, 14);
|
|
206
|
-
tte_printf("#{P:28,2}%d", score_p1 % 10);
|
|
207
|
-
tte_erase_rect(220, 2, 228, 14);
|
|
208
|
-
tte_printf("#{P:220,2}%d", score_p2 % 10);
|
|
499
|
+
stage_sprites();
|
|
500
|
+
oam_copy(oam_mem, obj_buffer, 128);
|
|
209
501
|
}
|
|
210
502
|
return 0;
|
|
211
503
|
}
|