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.
Files changed (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -1,201 +1,452 @@
1
- /* ── sports.c — SNES PVSnesLib two-player Pong scaffold ─────────────
1
+ /* ── sports.c — SNES head-to-head court game (complete example game) ─────────
2
2
  *
3
- * Two-player Pong. Both ports wired:
4
- * padsCurrent(0) UP/DOWN moves the left paddle (player 1)
5
- * padsCurrent(1) UP/DOWN moves the right paddle (player 2)
3
+ * A COMPLETE, working game — NET SURGE, a head-to-head court duel (Pong
4
+ * lineage): title screen, 1P vs a beatable CPU and 2P simultaneous versus
5
+ * (controller 2), first-to-5 match flow with a result screen, SPC music +
6
+ * SFX, a PRNG that keeps rallies from looping forever, and a battery-SRAM
7
+ * record (longest win streak vs the CPU) that survives power cycles.
6
8
  *
7
- * AI fallback on port 1 when no second controller is plugged in
8
- * (padsCurrent(1) returns 0). Per-side score 0-9 via consoleDrawText.
9
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
10
+ * very different one. The markers tell you what's what:
11
+ * HARDWARE IDIOM (load-bearing) — dodges a documented SNES footgun; reshape
12
+ * your gameplay around it (see TROUBLESHOOTING before changing).
13
+ * GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
14
+ * reshape freely.
15
+ *
16
+ * What depends on what:
17
+ * data.asm — font + sprite/wallpaper tiles, and sram_read16/sram_write16
18
+ * (battery SRAM lives at $70:0000, reachable only with long addressing —
19
+ * that's why they're asm). Load-bearing.
20
+ * hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
21
+ * SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
22
+ * silently stop existing — the build still succeeds.
23
+ * snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
24
+ * driver (music + 2 one-shot samples). #include'd, not separately built.
9
25
  *
10
26
  * tcc-65816 is C89 — all declarations at block top, no inline `for (u16 i …)`.
27
+ *
28
+ * Frame budget: 7 sprites, 2 collision tests, a few consoleDrawText calls —
29
+ * a tiny fraction of a frame. Plenty of headroom for fancier ball physics.
11
30
  */
12
31
 
13
32
  #include <snes.h>
14
33
  #include "snes_sfx.c"
15
34
 
16
- extern char tilfont, palfont;
17
- extern char tilsprite, palsprite;
18
- extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
35
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
36
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
37
+ #define GAME_TITLE "NET SURGE"
38
+
39
+ extern char tilfont, palfont; /* console font + text palette (data.asm) */
40
+ extern char tilsprite, palsprite; /* solid 8x8 block tile + OBJ palette */
41
+ extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
19
42
 
20
43
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
21
44
  * No public prototype in console.h, so declare it; call once per frame. */
22
45
  extern void consoleVblank(void);
23
46
 
47
+ /* data.asm exports — battery SRAM accessors ($70:0000, long addressing). */
48
+ extern u16 sram_read16(u16 offset);
49
+ extern void sram_write16(u16 offset, u16 value);
50
+
24
51
  /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
25
52
  * court reads as a real backdrop, not flat blank. Filled at runtime. */
26
53
  static u16 bg_map[32 * 32];
27
54
 
28
- #define COURT_TOP 16
29
- #define COURT_BOT 208
30
- #define PADDLE_H 24
55
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
56
+ * oamSet's FIRST arg is a BYTE OFFSET into OAM, not a slot number: slot N
57
+ * lives at byte offset N*4. Passing the raw slot writes every sprite into
58
+ * OAM bytes 0-9, corrupting each other → black/garbled screen. */
59
+ #define SPR(slot) ((slot) << 2)
60
+
61
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
62
+ * Court geometry + match rules. The court is framed by '-' rails drawn on
63
+ * the text BG at rows 4 and 23 (pixels 32-39 and 184-191); COURT_TOP/BOT
64
+ * keep the ball between them. The text grid is 32x28 cells (8px each). */
65
+ #define COURT_ROW_TOP 4
66
+ #define COURT_ROW_BOT 23
67
+ #define COURT_NET_COL 16
68
+ #define COURT_TOP 40 /* first pixel row below the top rail */
69
+ #define COURT_BOT 184 /* first pixel row of the bottom rail */
70
+ #define PADDLE_H 24 /* 3 stacked 8x8 sprites */
31
71
  #define BALL_SIZE 8
32
- #define PADDLE_X1 16
33
- #define PADDLE_X2 232
72
+ #define PADDLE_X1 16 /* P1 — left side */
73
+ #define PADDLE_X2 232 /* P2/CPU — right side */
74
+ #define WIN_SCORE 5 /* first to 5 takes the match */
34
75
 
35
- /* oamSet's FIRST arg is a BYTE OFFSET into OAM, not a slot number: slot N lives
36
- * at byte offset N*4. Passing the raw slot writes every sprite into OAM bytes
37
- * 0-9, corrupting each other → black/garbled screen. (The shmup scaffold gets
38
- * this right; sports/racing did not — SNES-1.) */
39
- #define SPR(slot) ((slot) << 2)
76
+ /* SRAM layout: [0]=magic "NS", [2]=best streak, [4]=best ^ 0xA5C3.
77
+ * Magic is written LAST in streak_save so a torn write never validates. */
78
+ #define SRAM_MAGIC 0x534Eu
40
79
 
41
- static s16 p1y, p2y, bx, by;
42
- static s8 bdx, bdy;
43
- static u16 score_p1, score_p2;
44
- static u16 serve_timer;
80
+ /* Game states the shell every example shares: title → play → result. */
81
+ #define ST_TITLE 0
82
+ #define ST_PLAY 1
83
+ #define ST_OVER 2
45
84
 
46
- static void serve_ball(u8 to_left) {
47
- bx = 124;
48
- by = 110;
49
- bdx = to_left ? -2 : 2;
50
- bdy = ((score_p1 + score_p2) & 1) ? -1 : 1;
51
- serve_timer = 30;
85
+ static u8 state;
86
+ static s16 p1y, p2y; /* paddle top Y */
87
+ static s16 bx, by; /* ball position */
88
+ static s8 bdx, bdy; /* ball velocity (px/frame) */
89
+ static u8 score_p1, score_p2;
90
+ static u8 serve_timer; /* freeze frames between points */
91
+ static u8 two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
92
+ static u8 streak; /* current 1P-vs-CPU win streak (RAM) */
93
+ static u16 best_streak; /* battery-backed record — see end_match */
94
+ static u8 new_record; /* result screen shows NEW RECORD */
95
+ static u8 sound_ok;
96
+ static u16 prev_pad0;
97
+ static char nbuf[8]; /* fmt_u16 output */
98
+
99
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call).
100
+ * A versus game NEEDS this: the SNES is fully deterministic, so without a
101
+ * noise source two fixed strategies lock into an infinite rally loop (the
102
+ * exact same few-hundred-frame cycle, forever — an idle 1P match would
103
+ * never end). random8() is ticked once per play frame so identical game
104
+ * states a few seconds apart still diverge. */
105
+ static u16 rng = 0xC0A7;
106
+ static u8 random8(void) {
107
+ u16 r = rng;
108
+ r ^= r << 7;
109
+ r ^= r >> 9;
110
+ r ^= r << 8;
111
+ rng = r;
112
+ return (u8)r;
52
113
  }
53
114
 
54
- static void reset_match(void) {
55
- p1y = 96;
56
- p2y = 96;
57
- score_p1 = 0;
58
- score_p2 = 0;
59
- serve_ball(0);
115
+ /* ── GAME LOGIC (clay) — battery-SRAM record (see sram_* in data.asm) ─────── */
116
+ static u16 streak_load(void) {
117
+ u16 v;
118
+ if (sram_read16(0) != SRAM_MAGIC) return 0;
119
+ v = sram_read16(2);
120
+ if (sram_read16(4) != (u16)(v ^ 0xA5C3u)) return 0;
121
+ return v;
60
122
  }
61
123
 
62
- static void render_scores(void) {
63
- char buf[2];
64
- buf[1] = 0;
65
- buf[0] = '0' + (score_p1 % 10);
66
- consoleDrawText(6, 2, buf);
67
- buf[0] = '0' + (score_p2 % 10);
68
- consoleDrawText(24, 2, buf);
124
+ static void streak_save(u16 v) {
125
+ sram_write16(2, v);
126
+ sram_write16(4, (u16)(v ^ 0xA5C3u));
127
+ sram_write16(0, SRAM_MAGIC); /* magic LAST torn write = no record */
69
128
  }
70
129
 
71
- /* Draw a Pong court out of font glyphs on the text BG (no extra tile
72
- * data needed): a dashed rail across the top and bottom of the playfield
73
- * plus a dashed centre net. The text grid is 32x28 cells (8px each). */
74
- #define COURT_NET_COL 16
75
- #define COURT_ROW_TOP 4
76
- #define COURT_ROW_BOT 23
130
+ /* ── GAME LOGIC (clay) text helpers ──────────────────────────────────────── */
131
+ static void fmt_u16(u16 v) { /* decimal, no leading zeros, into nbuf */
132
+ char tmp[6];
133
+ u8 n = 0, i;
134
+ do { tmp[n++] = (char)('0' + v % 10); v /= 10; } while (v);
135
+ for (i = 0; i < n; i++) nbuf[i] = tmp[n - 1 - i];
136
+ nbuf[n] = 0;
137
+ }
138
+
139
+ static void clear_row(u16 y) {
140
+ consoleDrawText(0, y, " ");
141
+ }
142
+
143
+ static void clear_rows(u16 a, u16 b) {
144
+ u16 y;
145
+ for (y = a; y <= b; y++) clear_row(y);
146
+ }
147
+
148
+ /* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side ────── */
149
+ static void serve_ball(u8 to_left) {
150
+ bx = 124;
151
+ by = 108;
152
+ bdx = to_left ? -2 : 2;
153
+ bdy = ((score_p1 + score_p2) & 1) ? -1 : 1; /* alternate the angle */
154
+ serve_timer = 30; /* half-second breather */
155
+ }
156
+
157
+ /* ── GAME LOGIC (clay) — HUD: labels row 1, scores redrawn after points ───── */
158
+ static void draw_scores(void) {
159
+ nbuf[0] = (char)('0' + score_p1); nbuf[1] = 0;
160
+ consoleDrawText(6, 1, nbuf);
161
+ nbuf[0] = (char)('0' + score_p2);
162
+ consoleDrawText(28, 1, nbuf);
163
+ }
164
+
165
+ /* Draw the court out of font glyphs on the text BG (no extra tile data
166
+ * needed): a dashed rail across the top and bottom of the playfield plus
167
+ * a dashed centre net. */
77
168
  static void draw_court(void) {
78
- char rail[29];
79
- u16 i;
80
- for (i = 0; i < 28; i++) rail[i] = '-';
81
- rail[28] = 0;
82
- consoleDrawText(2, COURT_ROW_TOP, rail);
83
- consoleDrawText(2, COURT_ROW_BOT, rail);
84
- /* Dashed centre net: a ':' every other row between the rails. */
85
- for (i = COURT_ROW_TOP + 1; i < COURT_ROW_BOT; i += 2) {
86
- consoleDrawText(COURT_NET_COL, i, ":");
169
+ char rail[29];
170
+ u16 i;
171
+ for (i = 0; i < 28; i++) rail[i] = '-';
172
+ rail[28] = 0;
173
+ consoleDrawText(2, COURT_ROW_TOP, rail);
174
+ consoleDrawText(2, COURT_ROW_BOT, rail);
175
+ for (i = COURT_ROW_TOP + 1; i < COURT_ROW_BOT; i += 2)
176
+ consoleDrawText(COURT_NET_COL, i, ":");
177
+ }
178
+
179
+ /* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
180
+ static void hide_sprites(void) {
181
+ u16 i;
182
+ for (i = 0; i < 7; i++) oamSet(SPR(i), 0, 240, 3, 0, 0, 0, 0);
183
+ }
184
+
185
+ static void title_enter(void) {
186
+ clear_rows(0, 27);
187
+ consoleDrawText((32 - (sizeof(GAME_TITLE) - 1)) / 2, 2, GAME_TITLE);
188
+ consoleDrawText(9, 4, "BEST STREAK");
189
+ fmt_u16(best_streak);
190
+ consoleDrawText(21, 4, nbuf);
191
+ consoleDrawText(9, 7, "A - 1P VS CPU");
192
+ consoleDrawText(9, 9, "B - 2P VERSUS");
193
+ consoleDrawText(8, 12, "FIRST TO 5 WINS");
194
+ hide_sprites();
195
+ state = ST_TITLE;
196
+ }
197
+
198
+ static void match_enter(u8 players) {
199
+ two_player = players;
200
+ p1y = 100; p2y = 100;
201
+ score_p1 = 0; score_p2 = 0;
202
+ new_record = 0;
203
+ serve_ball(0);
204
+ clear_rows(0, 27);
205
+ consoleDrawText(2, 1, "P1");
206
+ consoleDrawText(24, 1, two_player ? "P2 " : "CPU");
207
+ draw_scores();
208
+ draw_court();
209
+ if (sound_ok) sfx_play(1); /* serve-up blip */
210
+ state = ST_PLAY;
211
+ }
212
+
213
+ /* ── GAME LOGIC (clay) — match over: result + record bookkeeping.
214
+ * Persistence choice: for a VERSUS sports game a raw hi-score is
215
+ * meaningless (every match ends 5-x), so we persist the longest 1P win
216
+ * streak against the CPU — the stat a returning player actually chases.
217
+ * 2P matches never touch it (humans beating each other isn't a record). */
218
+ static void end_match(void) {
219
+ clear_rows(11, 17); /* result card overlays the court */
220
+ if (score_p1 >= WIN_SCORE) {
221
+ consoleDrawText(8, 12, two_player ? "P1 WINS THE MATCH" : "YOU BEAT THE CPU");
222
+ if (!two_player) {
223
+ ++streak;
224
+ if (streak > best_streak) {
225
+ best_streak = streak;
226
+ new_record = 1;
227
+ streak_save(best_streak); /* battery SRAM — see hdr.asm note up top */
228
+ }
87
229
  }
230
+ } else if (two_player) {
231
+ consoleDrawText(8, 12, "P2 WINS THE MATCH");
232
+ } else {
233
+ consoleDrawText(7, 12, "CPU TAKES THE MATCH");
234
+ streak = 0; /* the streak dies with the loss */
235
+ }
236
+ if (!two_player) {
237
+ consoleDrawText(10, 14, "STREAK");
238
+ fmt_u16(streak);
239
+ consoleDrawText(17, 14, nbuf);
240
+ if (new_record) consoleDrawText(20, 14, "- NEW RECORD");
241
+ }
242
+ consoleDrawText(10, 16, "PRESS START");
243
+ if (sound_ok) sfx_play(2); /* end-of-match flourish */
244
+ state = ST_OVER;
245
+ }
246
+
247
+ /* ── GAME LOGIC (clay) — one point scored ── */
248
+ static void score_point(u8 for_p1) {
249
+ if (for_p1) ++score_p1; else ++score_p2;
250
+ if (sound_ok) sfx_play(2);
251
+ draw_scores();
252
+ if (score_p1 >= WIN_SCORE || score_p2 >= WIN_SCORE) end_match();
253
+ else serve_ball(for_p1); /* winner of the point receives */
254
+ }
255
+
256
+ /* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
257
+ * Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU moves at 1,
258
+ * so an edge hit is exactly how a human beats it. A ±1 random "spin" on
259
+ * every return keeps rallies from repeating (see the PRNG note above). */
260
+ static void deflect(s16 paddle_y) {
261
+ s16 rel = (s16)(by + BALL_SIZE / 2) - (s16)(paddle_y + PADDLE_H / 2);
262
+ bdy = (s8)(rel >> 3);
263
+ bdy += (s8)((random8() & 2) - 1); /* spin: -1 or +1 */
264
+ if (bdy > 2) bdy = 2;
265
+ if (bdy < -2) bdy = -2;
266
+ if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
267
+ if (sound_ok) sfx_play(1);
268
+ }
269
+
270
+ /* Headless-test telemetry — written once per frame into this block. A test
271
+ * harness finds it by scanning WRAM for the "NS"+0xBD signature, then plays
272
+ * the game from real state instead of parsing pixels. Delete freely. */
273
+ static u8 telem[16];
274
+ static void telem_update(void) {
275
+ telem[0] = 'N'; telem[1] = 'S'; telem[2] = 0xBD;
276
+ telem[3] = state;
277
+ telem[4] = score_p1;
278
+ telem[5] = score_p2;
279
+ telem[6] = (u8)((sound_ok << 7) | two_player);
280
+ telem[7] = (u8)p1y;
281
+ telem[8] = (u8)p2y;
282
+ telem[9] = (u8)bx; telem[10] = (u8)(bx >> 8);
283
+ telem[11] = (u8)by;
284
+ telem[12] = serve_timer;
285
+ telem[13] = streak;
286
+ telem[14] = (u8)best_streak; telem[15] = (u8)(best_streak >> 8);
88
287
  }
89
288
 
90
289
  int main(void) {
91
- u16 p1, p2;
92
- u16 i, slot;
93
- s16 target;
94
-
95
- consoleSetTextMapPtr(0x6800);
96
- consoleSetTextGfxPtr(0x3000);
97
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
98
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
99
- setMode(BG_MODE1, 0);
100
- /* consoleInitText DMAs the font but does NOT set the PPU BG base
101
- * registers — point BG0 at the same font ($3000) + map ($6800). */
102
- bgSetGfxPtr(0, 0x3000);
103
- bgSetMapPtr(0, 0x6800, SC_32x32);
104
-
105
- /* BG1 = full-screen wallpaper so the court never reads as blank.
106
- * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
107
- * the console gfx $3000 / map $6800). Map entries use palette block 1
108
- * (0x0400) so the wallpaper palette doesn't disturb the console font
109
- * palette in block 0 (HUD/court text stays legible). */
110
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
111
- 32, 32, BG_16COLORS, 0x2000);
112
-
113
- /* Per-genre backdrop tint every SNES scaffold used to ship the same
114
- * blue checkered wallpaper ('no variety'). Recolor the wallpaper's
115
- * CGRAM entries (block 1 = entries 16+) to a court green scheme. */
116
- setPaletteColor(0, RGB5(2,9,4));
117
- setPaletteColor(17, RGB5(5,14,7));
118
- setPaletteColor(18, RGB5(3,11,5));
119
- for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
120
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
121
- bgSetEnable(1);
122
- bgSetDisable(2);
123
-
124
- oamInitGfxSet(&tilsprite, 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
125
-
126
- consoleDrawText(2, 1, "P1");
127
- consoleDrawText(28, 1, "P2");
128
- consoleDrawText(6, 26, "UP/DOWN MOVES YOUR PADDLE");
129
- draw_court(); /* dashed rails + centre net (font glyphs, no extra tiles) */
130
-
131
- /* Hide all OAM slots initially. */
132
- for (i = 0; i < 7; i++) oamSet(SPR(i), 0, 240, 3, 0, 0, 0, 0);
133
-
134
- setScreenOn();
135
- sfx_init();
136
- reset_match();
137
-
138
- while (1) {
139
- slot = 0;
140
- /* Left paddle = 3 stacked 8×8 sprites */
141
- for (i = 0; i < PADDLE_H / 8; i++) {
142
- oamSet(SPR(slot++), PADDLE_X1, (u16)(p1y + i * 8), 3, 0, 0, 0, 0);
143
- }
144
- for (i = 0; i < PADDLE_H / 8; i++) {
145
- oamSet(SPR(slot++), PADDLE_X2, (u16)(p2y + i * 8), 3, 0, 0, 0, 0);
290
+ u16 pad, pad2;
291
+ u16 i, slot;
292
+ s16 target;
293
+
294
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
295
+ * Init order: console text pointers FIRST, then mode, then BG bases.
296
+ * consoleInitText DMAs the font but does NOT set the PPU BG base
297
+ * registers point BG0 at the same font ($3000) + map ($6800) yourself
298
+ * or the text layer renders garbage. */
299
+ consoleSetTextMapPtr(0x6800);
300
+ consoleSetTextGfxPtr(0x3000);
301
+ consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font at BG char base */
302
+ consoleInitText(0, 16 * 2, &tilfont, &palfont);
303
+ setMode(BG_MODE1, 0);
304
+ bgSetGfxPtr(0, 0x3000);
305
+ bgSetMapPtr(0, 0x6800, SC_32x32);
306
+
307
+ /* BG1 = full-screen wallpaper so the court never reads as blank.
308
+ * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
309
+ * the console gfx $3000 / map $6800). Map entries use palette block 1
310
+ * (0x0400) so the wallpaper palette doesn't disturb the console font
311
+ * palette in block 0 (HUD/court text stays legible). */
312
+ bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
313
+ 32, 32, BG_16COLORS, 0x2000);
314
+
315
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────
316
+ * Court-green backdrop tint: recolor the wallpaper's CGRAM entries
317
+ * (block 1 = entries 16+). Swap these for your own arena's mood. */
318
+ setPaletteColor(0, RGB5(2, 9, 4));
319
+ setPaletteColor(17, RGB5(5, 14, 7));
320
+ setPaletteColor(18, RGB5(3, 11, 5));
321
+ for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
322
+ bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
323
+ bgSetEnable(1);
324
+ bgSetDisable(2); /* BG3 carries garbage in mode 1 */
325
+
326
+ oamInitGfxSet(&tilsprite, 32, &palsprite, 32, 0, 0x0000, OBJ_SIZE8_L16);
327
+ hide_sprites();
328
+
329
+ setScreenOn();
330
+
331
+ /* ── HARDWARE IDIOM (load-bearing) sfx_init AFTER setScreenOn, and CHECK
332
+ * the return: a wedged SPC700 must not take the video down with it. ── */
333
+ sound_ok = (sfx_init() == 0);
334
+ /* ── HARDWARE IDIOM (load-bearing) — one frame between init and the first
335
+ * command. sfx_init returns the instant the SPC echoes the jump command,
336
+ * but the driver then spends ~50 port writes initialising the DSP BEFORE
337
+ * it seeds its command edge-detector from $2140. Send a command in that
338
+ * window and the seed swallows it — music silently never starts (found
339
+ * via getAudioState: voice 1 pitch 0, ARAM prev_cmd already = 3). A
340
+ * WaitForVBlank is thousands of SPC cycles deterministic cure. ── */
341
+ WaitForVBlank();
342
+ if (sound_ok) sfx_music_play();
343
+
344
+ /* ── HARDWARE IDIOM (load-bearing) initialize EVERY mutable global.
345
+ * PVSnesLib's crt0 does NOT zero BSS, and SNES WRAM powers up dirty
346
+ * ($55 fill in snes9x). A static you never assigned holds garbage —
347
+ * here that meant two_player=0x55 picked "2P mode" paths before the
348
+ * first match ever set it. C's "statics start at 0" does not apply. ── */
349
+ best_streak = streak_load(); /* battery SRAM — 0 on first boot */
350
+ streak = 0;
351
+ two_player = 0;
352
+ score_p1 = score_p2 = 0;
353
+ p1y = p2y = 100;
354
+ bx = 124; by = 108;
355
+ bdx = bdy = 0;
356
+ serve_timer = 0;
357
+ new_record = 0;
358
+ rng = 0xC0A7; /* the data segment isn't trustworthy either */
359
+ prev_pad0 = 0;
360
+ title_enter();
361
+
362
+ while (1) {
363
+ pad = padsCurrent(0);
364
+
365
+ if (state == ST_TITLE) {
366
+ /* ── GAME LOGIC (clay) — title: A/START = 1P vs CPU, B = 2P versus ── */
367
+ if ((pad & KEY_A && !(prev_pad0 & KEY_A)) ||
368
+ (pad & KEY_START && !(prev_pad0 & KEY_START))) {
369
+ match_enter(0);
370
+ } else if (pad & KEY_B && !(prev_pad0 & KEY_B)) {
371
+ match_enter(1);
372
+ }
373
+ } else if (state == ST_OVER) {
374
+ /* Freeze the final scene; START or A returns to the title. */
375
+ if (pad & (KEY_START | KEY_A) && !(prev_pad0 & (KEY_START | KEY_A)))
376
+ title_enter();
377
+ } else {
378
+ /* ── ST_PLAY — GAME LOGIC (clay) from here down ──────────────────── */
379
+ random8(); /* tick the noise source every play frame */
380
+
381
+ /* P1 — port 0, up/down, 2px/frame. */
382
+ if ((pad & KEY_UP) && p1y > COURT_TOP) p1y -= 2;
383
+ if ((pad & KEY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
384
+
385
+ if (two_player) {
386
+ /* P2 — port 1 (controller 2), same speed: a fair versus match. */
387
+ pad2 = padsCurrent(1);
388
+ if ((pad2 & KEY_UP) && p2y > COURT_TOP) p2y -= 2;
389
+ if ((pad2 & KEY_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 2;
390
+ } else {
391
+ /* CPU — chases the ball centre at 1px/frame (half player speed)
392
+ * with a small dead zone. Beatable by design: steep deflections
393
+ * outrun it. */
394
+ target = by + BALL_SIZE / 2 - PADDLE_H / 2;
395
+ if (p2y + 2 < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
396
+ else if (p2y > target + 2 && p2y > COURT_TOP) p2y -= 1;
397
+ }
398
+
399
+ /* Ball update (frozen during the post-point serve pause). */
400
+ if (serve_timer > 0) {
401
+ serve_timer--;
402
+ } else if (state == ST_PLAY) {
403
+ bx = (s16)(bx + bdx);
404
+ by = (s16)(by + bdy);
405
+
406
+ /* Rail bounce. */
407
+ if (by < COURT_TOP) { by = COURT_TOP; bdy = (s8)(-bdy); }
408
+ if (by + BALL_SIZE > COURT_BOT) { by = COURT_BOT - BALL_SIZE; bdy = (s8)(-bdy); }
409
+
410
+ /* Paddle collisions (direction-gated so the ball can't double-hit). */
411
+ if (bdx < 0
412
+ && bx <= PADDLE_X1 + 8 && bx + BALL_SIZE >= PADDLE_X1
413
+ && by + BALL_SIZE > p1y && by < p1y + PADDLE_H) {
414
+ bdx = (s8)(-bdx);
415
+ bx = PADDLE_X1 + 8;
416
+ deflect(p1y);
146
417
  }
147
- oamSet(SPR(slot++), bx, by, 3, 0, 0, 0, 0);
148
- oamUpdate();
149
- render_scores();
150
- WaitForVBlank();
151
- consoleVblank();
152
-
153
- p1 = padsCurrent(0);
154
- p2 = padsCurrent(1);
155
-
156
- if ((p1 & KEY_UP) && p1y > COURT_TOP) p1y -= 2;
157
- if ((p1 & KEY_DOWN) && p1y < COURT_BOT - PADDLE_H) p1y += 2;
158
-
159
- if (p2 != 0) {
160
- if ((p2 & KEY_UP) && p2y > COURT_TOP) p2y -= 2;
161
- if ((p2 & KEY_DOWN) && p2y < COURT_BOT - PADDLE_H) p2y += 2;
162
- } else {
163
- target = by - PADDLE_H / 2;
164
- if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
165
- else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
418
+ if (bdx > 0
419
+ && bx + BALL_SIZE >= PADDLE_X2 && bx <= PADDLE_X2 + 8
420
+ && by + BALL_SIZE > p2y && by < p2y + PADDLE_H) {
421
+ bdx = (s8)(-bdx);
422
+ bx = PADDLE_X2 - BALL_SIZE;
423
+ deflect(p2y);
166
424
  }
167
425
 
168
- if (serve_timer > 0) {
169
- serve_timer--;
170
- } else {
171
- bx = (s16)(bx + bdx);
172
- by = (s16)(by + bdy);
173
-
174
- if (by < COURT_TOP) { by = COURT_TOP; bdy = (s8)(-bdy); }
175
- if (by + BALL_SIZE > COURT_BOT) { by = COURT_BOT - BALL_SIZE; bdy = (s8)(-bdy); }
176
-
177
- if (bdx < 0
178
- && bx <= PADDLE_X1 + 8
179
- && bx + BALL_SIZE >= PADDLE_X1
180
- && by + BALL_SIZE > p1y
181
- && by < p1y + PADDLE_H) {
182
- bdx = (s8)(-bdx);
183
- bx = PADDLE_X1 + 8;
184
- sfx_play(1); /* paddle hit */
185
- }
186
- if (bdx > 0
187
- && bx + BALL_SIZE >= PADDLE_X2
188
- && bx <= PADDLE_X2 + 8
189
- && by + BALL_SIZE > p2y
190
- && by < p2y + PADDLE_H) {
191
- bdx = (s8)(-bdx);
192
- bx = PADDLE_X2 - BALL_SIZE;
193
- sfx_play(1);
194
- }
195
-
196
- if (bx < 4) { if (score_p2 < 9) score_p2++; sfx_play(2); serve_ball(0); }
197
- if (bx > 252) { if (score_p1 < 9) score_p1++; sfx_play(2); serve_ball(1); }
198
- }
426
+ /* Off either side → point. */
427
+ if (bx < 4) score_point(0); /* past P1 → right side scores */
428
+ else if (bx > 244) score_point(1); /* past P2 → P1 scores */
429
+ }
430
+ }
431
+ prev_pad0 = pad;
432
+ telem_update();
433
+
434
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
435
+ * Stage ALL sprites, then oamUpdate(), then WaitForVBlank. PVSnesLib's
436
+ * NMI handler DMAs shadow OAM → real OAM every vblank (channel 7);
437
+ * oamUpdate marks the shadow dirty so that DMA carries THIS frame's
438
+ * positions. Stage-after-wait shows last frame's sprites. */
439
+ if (state == ST_PLAY || state == ST_OVER) {
440
+ slot = 0;
441
+ for (i = 0; i < PADDLE_H / 8; i++)
442
+ oamSet(SPR(slot++), PADDLE_X1, (u16)(p1y + i * 8), 3, 0, 0, 0, 0);
443
+ for (i = 0; i < PADDLE_H / 8; i++)
444
+ oamSet(SPR(slot++), PADDLE_X2, (u16)(p2y + i * 8), 3, 0, 0, 0, 0);
445
+ oamSet(SPR(slot), (u16)bx, (u16)by, 3, 0, 0, 0, 0);
199
446
  }
200
- return 0;
447
+ oamUpdate();
448
+ WaitForVBlank();
449
+ consoleVblank();
450
+ }
451
+ return 0;
201
452
  }