romdevtools 0.28.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 +51 -41
- package/CHANGELOG.md +46 -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 +1 -1
- package/src/host/LibretroHost.js +59 -1
- 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/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/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 +12 -1
- package/src/mcp/tools/watch-memory.js +4 -3
- 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/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,251 +1,409 @@
|
|
|
1
|
-
/* ── sports.c — NES
|
|
1
|
+
/* ── sports.c — NES versus sports game (complete example game) ───────────────
|
|
2
2
|
*
|
|
3
|
-
* A
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - Miss → ball respawns at centre, opponent scores
|
|
8
|
-
* - Per-side score in the top corners as ASCII digits via OAM
|
|
3
|
+
* A COMPLETE, working game — COURT CLASH, a head-to-head court game (Pong
|
|
4
|
+
* lineage): title screen, 1P vs CPU and 2P simultaneous versus, first-to-5
|
|
5
|
+
* match flow with a result screen, queued-text HUD, music + SFX, and a
|
|
6
|
+
* battery-backed record (longest win streak vs the CPU).
|
|
9
7
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
8
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
9
|
+
* very different one. The markers tell you what's what:
|
|
10
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented NES footgun; reshape
|
|
11
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
12
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
13
|
+
* reshape freely.
|
|
14
14
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
15
|
+
* What depends on what:
|
|
16
|
+
* nes_runtime.{h,c} — rendering/input/sound/text/hi-score library.
|
|
17
|
+
* chr-ram-runtime.crt0.s — boot + NMI + iNES header (BATTERY bit feeds
|
|
18
|
+
* hiscore_load/save). Load-bearing; edit with TROUBLESHOOTING open.
|
|
17
19
|
*
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
20
|
+
* Frame budget (NTSC, 60fps): 2 paddles + 1 ball + 2 paddle collision tests
|
|
21
|
+
* + a handful of queued HUD writes — a fraction of one frame even on the
|
|
22
|
+
* 1.79MHz 6502. Plenty of headroom for fancier ball physics.
|
|
21
23
|
*/
|
|
22
24
|
|
|
23
25
|
#include "nes_runtime.h"
|
|
24
26
|
|
|
25
|
-
/*
|
|
27
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
28
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
29
|
+
#define GAME_TITLE "COURT CLASH"
|
|
30
|
+
|
|
31
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
32
|
+
* Tile art. Each 8x8 tile = 16 bytes: 8 plane-0 rows then 8 plane-1 rows
|
|
33
|
+
* (2bpp — plane0-only pixels use colour 1, both planes = colour 3). */
|
|
26
34
|
static const uint8_t tile_blank[16] = { 0 };
|
|
27
|
-
/*
|
|
35
|
+
/* Paddle = solid 4px-wide column; players stack 3 of these (24px tall). */
|
|
28
36
|
static const uint8_t tile_paddle[16] = {
|
|
29
37
|
0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C, 0x3C,
|
|
30
38
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
31
39
|
};
|
|
32
|
-
/* 8×8 ball = small filled box */
|
|
33
40
|
static const uint8_t tile_ball[16] = {
|
|
34
41
|
0x00, 0x3C, 0x7E, 0x7E, 0x7E, 0x7E, 0x3C, 0x00,
|
|
35
42
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
36
43
|
};
|
|
37
|
-
/* BG
|
|
38
|
-
*
|
|
39
|
-
* BG_WALL — solid
|
|
40
|
-
* BG_NET — dashed vertical bar (
|
|
41
|
-
* BG_FLOOR —
|
|
42
|
-
*
|
|
44
|
+
/* Court BG tiles (BACKGROUND pattern table $1000 — separate from the sprite
|
|
45
|
+
* table at $0000; the runtime's PPUCTRL setup makes that split):
|
|
46
|
+
* BG_WALL — solid rail (colour 1): the top/bottom court boundaries.
|
|
47
|
+
* BG_NET — dashed vertical bar (colour 1): the centre net.
|
|
48
|
+
* BG_FLOOR — faint hatch (colour 2): the court surface, so the arena
|
|
49
|
+
* reads as a court instead of sprites on flat black. */
|
|
43
50
|
static const uint8_t tile_wall[16] = {
|
|
44
51
|
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
|
45
52
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
46
53
|
};
|
|
47
54
|
static const uint8_t tile_net[16] = {
|
|
48
|
-
0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00,
|
|
55
|
+
0x18, 0x18, 0x00, 0x00, 0x18, 0x18, 0x00, 0x00,
|
|
49
56
|
0, 0, 0, 0, 0, 0, 0, 0,
|
|
50
57
|
};
|
|
51
58
|
static const uint8_t tile_floor[16] = {
|
|
52
|
-
0, 0, 0, 0, 0, 0, 0, 0,
|
|
53
|
-
0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55,
|
|
54
|
-
};
|
|
55
|
-
#define BG_WALL 1 /* BG slot 1 → $1010 */
|
|
56
|
-
#define BG_NET 2 /* BG slot 2 → $1020 */
|
|
57
|
-
#define BG_FLOOR 3 /* BG slot 3 → $1030 */
|
|
58
|
-
/* Digits 0-9 (3 wide × 5 tall, padded to 8×8). Used for the score HUD. */
|
|
59
|
-
static const uint8_t tile_digits[10 * 16] = {
|
|
60
|
-
/* 0 */ 0xE0,0xA0,0xA0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
61
|
-
/* 1 */ 0x40,0xC0,0x40,0x40,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
62
|
-
/* 2 */ 0xE0,0x20,0xE0,0x80,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
63
|
-
/* 3 */ 0xE0,0x20,0xE0,0x20,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
64
|
-
/* 4 */ 0xA0,0xA0,0xE0,0x20,0x20,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
65
|
-
/* 5 */ 0xE0,0x80,0xE0,0x20,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
66
|
-
/* 6 */ 0xE0,0x80,0xE0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
67
|
-
/* 7 */ 0xE0,0x20,0x20,0x40,0x40,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
68
|
-
/* 8 */ 0xE0,0xA0,0xE0,0xA0,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
69
|
-
/* 9 */ 0xE0,0xA0,0xE0,0x20,0xE0,0x00,0x00,0x00, 0,0,0,0,0,0,0,0,
|
|
59
|
+
0, 0, 0, 0, 0, 0, 0, 0,
|
|
60
|
+
0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55,
|
|
70
61
|
};
|
|
62
|
+
#define BG_WALL 1 /* BG slot 1 → CHR $1010 */
|
|
63
|
+
#define BG_NET 2 /* BG slot 2 → CHR $1020 */
|
|
64
|
+
#define BG_FLOOR 3 /* BG slot 3 → CHR $1030 */
|
|
71
65
|
|
|
72
|
-
/*
|
|
73
|
-
|
|
74
|
-
#define T_PADDLE
|
|
75
|
-
#define T_BALL
|
|
76
|
-
#define T_DIGIT0 3 /* digits live at slots 3..12 */
|
|
66
|
+
/* Sprite pattern-table slots ($0000). The font lives at BG $40+ — uploaded
|
|
67
|
+
* by font_upload(), used by all the text_draw* calls. */
|
|
68
|
+
#define T_PADDLE 1
|
|
69
|
+
#define T_BALL 2
|
|
77
70
|
|
|
78
71
|
static const uint8_t palette[32] = {
|
|
79
|
-
/*
|
|
80
|
-
*
|
|
72
|
+
/* BG: near-black backdrop, white rails/net (idx1), dark-green floor (idx2).
|
|
73
|
+
* The font also draws with idx1 → white text everywhere. */
|
|
81
74
|
0x0F, 0x30, 0x1A, 0x00,
|
|
82
75
|
0x0F, 0x30, 0x1A, 0x00,
|
|
83
76
|
0x0F, 0x30, 0x1A, 0x00,
|
|
84
77
|
0x0F, 0x30, 0x1A, 0x00,
|
|
85
|
-
/*
|
|
86
|
-
0x0F,
|
|
87
|
-
0x0F,
|
|
88
|
-
0x0F, 0x30,
|
|
89
|
-
0x0F, 0x30,
|
|
78
|
+
/* Sprites: pal 0 = P1 (blue), pal 1 = P2/CPU (red), pal 2 = ball (white) */
|
|
79
|
+
0x0F, 0x21, 0x11, 0x30,
|
|
80
|
+
0x0F, 0x16, 0x06, 0x30,
|
|
81
|
+
0x0F, 0x30, 0x10, 0x00,
|
|
82
|
+
0x0F, 0x30, 0x10, 0x00,
|
|
90
83
|
};
|
|
91
84
|
|
|
92
|
-
/*
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
#define
|
|
100
|
-
#define PADDLE_X1 16
|
|
101
|
-
#define PADDLE_X2 232
|
|
102
|
-
#define COURT_TOP 16
|
|
103
|
-
#define COURT_BOT 216
|
|
85
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
86
|
+
* Court geometry + match rules. The court is framed by BG rails on
|
|
87
|
+
* nametable rows 2 and 27; COURT_TOP/BOT keep the ball between them. */
|
|
88
|
+
#define PADDLE_H 24 /* 3 stacked 8px sprites */
|
|
89
|
+
#define PADDLE_X1 16 /* P1 — left side */
|
|
90
|
+
#define PADDLE_X2 232 /* P2/CPU — right side */
|
|
91
|
+
#define COURT_TOP 24 /* first pixel row below the top rail */
|
|
92
|
+
#define COURT_BOT 216 /* first pixel row of the bottom rail */
|
|
104
93
|
#define BALL_W 8
|
|
105
94
|
#define BALL_H 8
|
|
95
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
96
|
+
#define P1_PAL 0
|
|
97
|
+
#define P2_PAL 1
|
|
98
|
+
#define BALL_PAL 2
|
|
99
|
+
|
|
100
|
+
static int16_t p1y, p2y; /* paddle top Y (int16: collision math) */
|
|
101
|
+
static int16_t bx, by; /* ball position */
|
|
102
|
+
static int8_t bdx, bdy; /* ball velocity (px/frame) */
|
|
103
|
+
static uint8_t score_p1, score_p2;
|
|
104
|
+
static uint8_t serve_timer; /* freeze frames between points */
|
|
105
|
+
static uint8_t two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
|
|
106
|
+
static uint8_t streak; /* current 1P-vs-CPU win streak (RAM) */
|
|
107
|
+
static uint16_t best_streak; /* battery-backed record — see end_match */
|
|
108
|
+
static uint8_t new_record; /* result screen shows NEW RECORD */
|
|
109
|
+
|
|
110
|
+
/* Game states — the shell every example shares: title → play → game over. */
|
|
111
|
+
#define ST_TITLE 0
|
|
112
|
+
#define ST_PLAY 1
|
|
113
|
+
#define ST_OVER 2
|
|
114
|
+
static uint8_t state;
|
|
106
115
|
|
|
116
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call).
|
|
117
|
+
* A versus game NEEDS this: the NES is fully deterministic, so without a
|
|
118
|
+
* noise source two fixed strategies lock into an infinite rally loop (the
|
|
119
|
+
* exact same 600-frame cycle, forever). random8() is ticked once per play
|
|
120
|
+
* frame so identical game states a few seconds apart still diverge. */
|
|
121
|
+
static uint16_t rng = 0xC0A7;
|
|
122
|
+
static uint8_t random8(void) {
|
|
123
|
+
uint16_t r = rng;
|
|
124
|
+
r ^= r << 7;
|
|
125
|
+
r ^= r >> 9;
|
|
126
|
+
r ^= r << 8;
|
|
127
|
+
rng = r;
|
|
128
|
+
return (uint8_t)r;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side ── */
|
|
107
132
|
static void serve_ball(uint8_t to_left) {
|
|
108
|
-
bx =
|
|
109
|
-
by =
|
|
133
|
+
bx = 124;
|
|
134
|
+
by = 116;
|
|
110
135
|
bdx = to_left ? -2 : 2;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
136
|
+
bdy = ((score_p1 + score_p2) & 1) ? -1 : 1; /* alternate the angle */
|
|
137
|
+
serve_timer = 30; /* half-second breather */
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/* ── GAME LOGIC (clay) — HUD (queued writes; the NMI commits ≤16/vblank) ──
|
|
141
|
+
* OVERSCAN RULE: most NTSC displays/cores crop the top 8 scanlines, so
|
|
142
|
+
* nametable row 0 is invisible — HUD text lives on row 1, never row 0. */
|
|
143
|
+
static void draw_hud(void) {
|
|
144
|
+
text_draw_u16(0, 4, 1, score_p1);
|
|
145
|
+
text_draw_u16(0, 23, 1, score_p2);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
static void draw_hud_labels(void) {
|
|
149
|
+
text_draw(0, 1, 1, "P1");
|
|
150
|
+
text_draw(0, 29, 1, two_player ? "P2" : "CPU");
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/* ── GAME LOGIC (clay) — the title screen ──────────────────────────────────
|
|
154
|
+
* Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes; the queued
|
|
155
|
+
* variant would deadlock with rendering disabled — see TROUBLESHOOTING). */
|
|
156
|
+
static void paint_title(void) {
|
|
157
|
+
uint8_t r, c;
|
|
158
|
+
ppu_off();
|
|
159
|
+
/* Carpet the screen with court floor; keep rows 0-1 blank (row 0 is
|
|
160
|
+
* overscan-cropped, row 1 is where the in-game HUD will live). */
|
|
161
|
+
for (r = 0; r < 30; r++)
|
|
162
|
+
for (c = 0; c < 32; c++)
|
|
163
|
+
vram_unsafe_set((uint16_t)(0x2000 + r * 32 + c), (r == 0 || r == 1) ? 0 : BG_FLOOR);
|
|
164
|
+
text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
|
|
165
|
+
text_draw_unsafe(0x2000 + 13 * 32 + 9, "1P VS CPU - A");
|
|
166
|
+
text_draw_unsafe(0x2000 + 15 * 32 + 9, "2P VERSUS - B");
|
|
167
|
+
/* Persistent record line — the battery-backed best CPU-mode win streak. */
|
|
168
|
+
text_draw_unsafe(0x2000 + 20 * 32 + 7, "BEST STREAK");
|
|
169
|
+
{
|
|
170
|
+
uint16_t v = best_streak;
|
|
171
|
+
uint8_t d[5], i;
|
|
172
|
+
for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
|
|
173
|
+
for (i = 0; i < 5; i++) vram_unsafe_set((uint16_t)(0x2000 + 20 * 32 + 19 + i), (uint8_t)(0x40 + d[4 - i]));
|
|
174
|
+
}
|
|
175
|
+
ppu_scroll(0, 0);
|
|
176
|
+
oam_clear();
|
|
177
|
+
ppu_on_all();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/* ── GAME LOGIC (clay) — paint the court, PPU off (match start only).
|
|
181
|
+
* Once rendering is back on, ALL background changes must go through the
|
|
182
|
+
* QUEUED path (tile_set / text_draw / text_draw_u16) — a raw $2007 write
|
|
183
|
+
* mid-frame corrupts the PPU address latch and shears the screen. */
|
|
184
|
+
static void paint_court(void) {
|
|
185
|
+
uint8_t r, c;
|
|
186
|
+
ppu_off();
|
|
187
|
+
for (c = 0; c < 32; c++) {
|
|
188
|
+
vram_unsafe_set((uint16_t)(0x2000 + 0 * 32 + c), 0); /* row 0: overscan-cropped */
|
|
189
|
+
vram_unsafe_set((uint16_t)(0x2000 + 1 * 32 + c), 0); /* row 1: HUD (queued draws fill it) */
|
|
190
|
+
vram_unsafe_set((uint16_t)(0x2000 + 2 * 32 + c), BG_WALL); /* top rail */
|
|
191
|
+
vram_unsafe_set((uint16_t)(0x2000 + 27 * 32 + c), BG_WALL); /* bottom rail */
|
|
192
|
+
}
|
|
193
|
+
for (r = 3; r < 27; r++)
|
|
194
|
+
for (c = 0; c < 32; c++)
|
|
195
|
+
vram_unsafe_set((uint16_t)(0x2000 + r * 32 + c), (c == 15) ? BG_NET : BG_FLOOR);
|
|
196
|
+
for (r = 28; r < 30; r++)
|
|
197
|
+
for (c = 0; c < 32; c++)
|
|
198
|
+
vram_unsafe_set((uint16_t)(0x2000 + r * 32 + c), BG_FLOOR);
|
|
199
|
+
ppu_scroll(0, 0);
|
|
200
|
+
oam_clear();
|
|
201
|
+
ppu_on_all();
|
|
202
|
+
/* Labels + scores go through the queued path now rendering is on. */
|
|
203
|
+
draw_hud_labels();
|
|
204
|
+
draw_hud();
|
|
114
205
|
}
|
|
115
206
|
|
|
116
|
-
|
|
207
|
+
/* ── GAME LOGIC (clay) — start a match ── */
|
|
208
|
+
static void start_match(uint8_t players) {
|
|
209
|
+
two_player = players;
|
|
117
210
|
p1y = 100; p2y = 100;
|
|
118
211
|
score_p1 = 0; score_p2 = 0;
|
|
212
|
+
new_record = 0;
|
|
119
213
|
serve_ball(0);
|
|
214
|
+
paint_court();
|
|
215
|
+
state = ST_PLAY;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/* ── GAME LOGIC (clay) — match over: result + record bookkeeping.
|
|
219
|
+
* Persistence choice: for a VERSUS sports game a raw hi-score is
|
|
220
|
+
* meaningless (every match ends 5-x), so we persist the longest 1P win
|
|
221
|
+
* streak against the CPU — the stat a returning player actually chases.
|
|
222
|
+
* 2P matches never touch it (humans beating each other isn't a record). */
|
|
223
|
+
static void end_match(void) {
|
|
224
|
+
if (score_p1 >= WIN_SCORE) {
|
|
225
|
+
text_draw(0, 12, 14, "P1 WINS");
|
|
226
|
+
if (!two_player) {
|
|
227
|
+
++streak;
|
|
228
|
+
if (streak > best_streak) {
|
|
229
|
+
best_streak = streak;
|
|
230
|
+
new_record = 1;
|
|
231
|
+
/* ── HARDWARE IDIOM (load-bearing) — persists via battery PRG-RAM
|
|
232
|
+
* at $6000; works because the crt0's iNES header sets the BATTERY
|
|
233
|
+
* bit. See nes_runtime.c for the magic+checksum layout. ── */
|
|
234
|
+
hiscore_save(best_streak);
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
} else if (two_player) {
|
|
238
|
+
text_draw(0, 12, 14, "P2 WINS");
|
|
239
|
+
} else {
|
|
240
|
+
text_draw(0, 12, 14, "CPU WINS");
|
|
241
|
+
streak = 0; /* the streak dies with the loss */
|
|
242
|
+
}
|
|
243
|
+
if (new_record) text_draw(0, 11, 16, "NEW RECORD");
|
|
244
|
+
text_draw(0, 10, 18, "PRESS START");
|
|
245
|
+
/* End-of-match whistle: two quick descending tones. */
|
|
246
|
+
sound_play_tone(0, 0x0D6, 10, 8);
|
|
247
|
+
sound_play_tone(1, 0x1AA, 10, 12);
|
|
248
|
+
state = ST_OVER;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
252
|
+
static void score_point(uint8_t for_p1) {
|
|
253
|
+
if (for_p1) ++score_p1; else ++score_p2;
|
|
254
|
+
sound_play_noise(5, 8, 8);
|
|
255
|
+
draw_hud(); /* queued — safe while rendering */
|
|
256
|
+
if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
|
|
257
|
+
else serve_ball(for_p1); /* winner of the point receives */
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
|
|
261
|
+
* Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU moves at 1,
|
|
262
|
+
* so an edge hit is exactly how a human beats it. A ±1 random "spin" on
|
|
263
|
+
* every return keeps rallies from repeating (see the PRNG note above). */
|
|
264
|
+
static void deflect(int16_t paddle_y) {
|
|
265
|
+
int16_t rel = (by + BALL_H / 2) - (paddle_y + PADDLE_H / 2);
|
|
266
|
+
bdy = (int8_t)(rel >> 3);
|
|
267
|
+
bdy += (int8_t)((random8() & 2) - 1); /* spin: -1 or +1 */
|
|
268
|
+
if (bdy > 2) bdy = 2;
|
|
269
|
+
if (bdy < -2) bdy = -2;
|
|
270
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
271
|
+
sound_play_tone(0, 0x150, 8, 4);
|
|
120
272
|
}
|
|
121
273
|
|
|
122
274
|
void main(void) {
|
|
123
|
-
uint8_t
|
|
124
|
-
uint8_t p1, p2;
|
|
275
|
+
uint8_t pad, prev_pad = 0;
|
|
125
276
|
|
|
277
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
|
|
278
|
+
* Init order: PPU off → CHR upload → palette → nametable (raw writes) →
|
|
279
|
+
* OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
|
|
280
|
+
* off (raw $2007 traffic during rendering corrupts the address latch
|
|
281
|
+
* mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
|
|
282
|
+
* PPUMASK bits — don't poke those registers directly alongside it. */
|
|
126
283
|
ppu_off();
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
chr_ram_upload(
|
|
130
|
-
chr_ram_upload(
|
|
131
|
-
chr_ram_upload(T_BALL * 16, tile_ball, 16);
|
|
132
|
-
chr_ram_upload(T_DIGIT0 * 16, tile_digits, sizeof(tile_digits));
|
|
133
|
-
/* Upload court tiles to the BACKGROUND pattern table ($1010..$1030). */
|
|
134
|
-
chr_ram_upload(0x1010, tile_wall, 16);
|
|
135
|
-
chr_ram_upload(0x1020, tile_net, 16);
|
|
284
|
+
chr_ram_upload(T_PADDLE * 16, tile_paddle, 16);
|
|
285
|
+
chr_ram_upload(T_BALL * 16, tile_ball, 16);
|
|
286
|
+
chr_ram_upload(0x1010, tile_wall, 16);
|
|
287
|
+
chr_ram_upload(0x1020, tile_net, 16);
|
|
136
288
|
chr_ram_upload(0x1030, tile_floor, 16);
|
|
137
|
-
|
|
289
|
+
font_upload(); /* '0'-'9'=$40, 'A'-'Z'=$4A, '-'=$64 (BG table) */
|
|
138
290
|
palette_load(palette);
|
|
291
|
+
sound_init();
|
|
139
292
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
{
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
293
|
+
best_streak = hiscore_load(); /* battery SRAM — 0 on first boot */
|
|
294
|
+
streak = 0;
|
|
295
|
+
state = ST_TITLE;
|
|
296
|
+
paint_title();
|
|
297
|
+
|
|
298
|
+
for (;;) {
|
|
299
|
+
if (state == ST_TITLE) {
|
|
300
|
+
/* ── GAME LOGIC (clay) — title: A/START = 1P vs CPU, B = 2P versus ── */
|
|
301
|
+
oam_clear();
|
|
302
|
+
ppu_wait_nmi();
|
|
303
|
+
sound_music_tick();
|
|
304
|
+
pad = pad_poll(0);
|
|
305
|
+
if ((pad & PAD_A) && !(prev_pad & PAD_A)) start_match(0);
|
|
306
|
+
else if ((pad & PAD_B) && !(prev_pad & PAD_B)) start_match(1);
|
|
307
|
+
else if ((pad & PAD_START) && !(prev_pad & PAD_START)) start_match(0);
|
|
308
|
+
prev_pad = pad;
|
|
309
|
+
continue;
|
|
153
310
|
}
|
|
154
|
-
for (rr = 2; rr < 27; rr++)
|
|
155
|
-
vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + 15), BG_NET); /* centre net */
|
|
156
|
-
}
|
|
157
311
|
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
312
|
+
if (state == ST_OVER) {
|
|
313
|
+
/* Freeze the final scene; START or A returns to the title. Sprites
|
|
314
|
+
* still need restaging every frame — oam_clear + the same draws —
|
|
315
|
+
* because the NMI DMAs shadow OAM whether you updated it or not. */
|
|
316
|
+
{
|
|
317
|
+
uint8_t i;
|
|
318
|
+
oam_clear();
|
|
319
|
+
for (i = 0; i < PADDLE_H / 8; i++) oam_spr(PADDLE_X1, (uint8_t)(p1y + i * 8), T_PADDLE, P1_PAL);
|
|
320
|
+
for (i = 0; i < PADDLE_H / 8; i++) oam_spr(PADDLE_X2, (uint8_t)(p2y + i * 8), T_PADDLE, P2_PAL);
|
|
321
|
+
}
|
|
322
|
+
ppu_wait_nmi();
|
|
323
|
+
sound_music_tick();
|
|
324
|
+
pad = pad_poll(0);
|
|
325
|
+
if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
|
|
326
|
+
state = ST_TITLE;
|
|
327
|
+
paint_title();
|
|
328
|
+
}
|
|
329
|
+
prev_pad = pad;
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
161
332
|
|
|
162
|
-
|
|
333
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────── */
|
|
163
334
|
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
*
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
oam_spr(200, 4, (uint8_t)(T_DIGIT0 + score_p2), 1);
|
|
335
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
336
|
+
* Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
|
|
337
|
+
* real OAM at the START of vblank, copying whatever shadow OAM holds AT
|
|
338
|
+
* THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites.
|
|
339
|
+
* OAM slot = oam_spr call order: P1 paddle fills slots 0-2, P2 paddle
|
|
340
|
+
* 3-5, ball 6 — every frame, deterministically. */
|
|
341
|
+
{
|
|
342
|
+
uint8_t i;
|
|
343
|
+
oam_clear();
|
|
344
|
+
for (i = 0; i < PADDLE_H / 8; i++)
|
|
345
|
+
oam_spr(PADDLE_X1, (uint8_t)(p1y + i * 8), T_PADDLE, P1_PAL);
|
|
346
|
+
for (i = 0; i < PADDLE_H / 8; i++)
|
|
347
|
+
oam_spr(PADDLE_X2, (uint8_t)(p2y + i * 8), T_PADDLE, P2_PAL);
|
|
348
|
+
oam_spr((uint8_t)bx, (uint8_t)by, T_BALL, BALL_PAL);
|
|
349
|
+
}
|
|
180
350
|
|
|
181
351
|
ppu_wait_nmi();
|
|
182
|
-
|
|
183
|
-
/* ── Input ────────────────────────────────────────────────── */
|
|
184
352
|
sound_music_tick();
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
if (
|
|
197
|
-
|
|
198
|
-
|
|
353
|
+
|
|
354
|
+
/* ── GAME LOGIC (clay) from here down ── */
|
|
355
|
+
random8(); /* tick the noise source every play frame */
|
|
356
|
+
|
|
357
|
+
/* P1 — port 0, up/down, 2px/frame. (prev_pad tracks through play so
|
|
358
|
+
* the result screen's edge-detect doesn't eat a held button.) */
|
|
359
|
+
pad = pad_poll(0);
|
|
360
|
+
prev_pad = pad;
|
|
361
|
+
if ((pad & PAD_UP) && p1y > COURT_TOP) p1y -= 2;
|
|
362
|
+
if ((pad & PAD_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
|
|
363
|
+
|
|
364
|
+
if (two_player) {
|
|
365
|
+
/* P2 — port 1, same speed: a fair simultaneous-versus match. */
|
|
366
|
+
uint8_t pad2 = pad_poll(1);
|
|
367
|
+
if ((pad2 & PAD_UP) && p2y > COURT_TOP) p2y -= 2;
|
|
368
|
+
if ((pad2 & PAD_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 2;
|
|
199
369
|
} else {
|
|
200
|
-
/*
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
370
|
+
/* CPU — chases the ball centre at 1px/frame (half player speed) with
|
|
371
|
+
* a small dead zone. Beatable by design: steep deflections outrun it. */
|
|
372
|
+
int16_t target = by + BALL_H / 2 - PADDLE_H / 2;
|
|
373
|
+
if (p2y + 2 < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
|
|
374
|
+
else if (p2y > target + 2 && p2y > COURT_TOP) p2y -= 1;
|
|
204
375
|
}
|
|
205
376
|
|
|
206
|
-
/*
|
|
377
|
+
/* Ball update (frozen during the post-point serve pause). */
|
|
207
378
|
if (serve_timer > 0) {
|
|
208
|
-
serve_timer
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
/* Top/bottom wall bounce. */
|
|
214
|
-
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sound_play_tone(1, 0x100, 8, 4); }
|
|
215
|
-
if (by + BALL_H > COURT_BOT) { by = COURT_BOT - BALL_H; bdy = -bdy; sound_play_tone(1, 0x100, 8, 4); }
|
|
216
|
-
|
|
217
|
-
/* Paddle 1 collision (left). */
|
|
218
|
-
if (bdx < 0
|
|
219
|
-
&& bx <= PADDLE_X1 + 8
|
|
220
|
-
&& bx + BALL_W >= PADDLE_X1
|
|
221
|
-
&& by + BALL_H > p1y
|
|
222
|
-
&& by < p1y + PADDLE_H) {
|
|
223
|
-
bdx = -bdx;
|
|
224
|
-
bx = PADDLE_X1 + 8;
|
|
225
|
-
sound_play_tone(0, 0x150, 8, 4);
|
|
226
|
-
}
|
|
227
|
-
/* Paddle 2 collision (right). */
|
|
228
|
-
if (bdx > 0
|
|
229
|
-
&& bx + BALL_W >= PADDLE_X2
|
|
230
|
-
&& bx <= PADDLE_X2 + 8
|
|
231
|
-
&& by + BALL_H > p2y
|
|
232
|
-
&& by < p2y + PADDLE_H) {
|
|
233
|
-
bdx = -bdx;
|
|
234
|
-
bx = PADDLE_X2 - BALL_W;
|
|
235
|
-
sound_play_tone(0, 0x150, 8, 4);
|
|
236
|
-
}
|
|
379
|
+
--serve_timer;
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
bx += bdx;
|
|
383
|
+
by += bdy;
|
|
237
384
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
385
|
+
/* Rail bounce. */
|
|
386
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sound_play_tone(1, 0x100, 8, 4); }
|
|
387
|
+
if (by + BALL_H > COURT_BOT) { by = COURT_BOT - BALL_H; bdy = -bdy; sound_play_tone(1, 0x100, 8, 4); }
|
|
388
|
+
|
|
389
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
390
|
+
if (bdx < 0
|
|
391
|
+
&& bx <= PADDLE_X1 + 8 && bx + BALL_W >= PADDLE_X1
|
|
392
|
+
&& by + BALL_H > p1y && by < p1y + PADDLE_H) {
|
|
393
|
+
bdx = -bdx;
|
|
394
|
+
bx = PADDLE_X1 + 8;
|
|
395
|
+
deflect(p1y);
|
|
249
396
|
}
|
|
397
|
+
if (bdx > 0
|
|
398
|
+
&& bx + BALL_W >= PADDLE_X2 && bx <= PADDLE_X2 + 8
|
|
399
|
+
&& by + BALL_H > p2y && by < p2y + PADDLE_H) {
|
|
400
|
+
bdx = -bdx;
|
|
401
|
+
bx = PADDLE_X2 - BALL_W;
|
|
402
|
+
deflect(p2y);
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
/* Off either side → point. */
|
|
406
|
+
if (bx < 4) score_point(0); /* past P1 → right side scores */
|
|
407
|
+
if (bx > 244) score_point(1); /* past P2 → P1 scores */
|
|
250
408
|
}
|
|
251
409
|
}
|