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,251 +1,409 @@
1
- /* ── sports.c — NES two-player Pong scaffold ─────────────────────────
1
+ /* ── sports.c — NES versus sports game (complete example game) ───────────────
2
2
  *
3
- * A complete two-player Pong baseline. Both ports are wired:
4
- * - Port 0 (player 1, left paddle) UP/DOWN move
5
- * - Port 1 (player 2, right paddle) UP/DOWN move
6
- * - Ball bounces off paddles + top/bottom walls
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
- * Designed for the romdev playtest window: with two USB
11
- * controllers plugged in, both players can play live. With one
12
- * controller, the keyboard arrow keys drive port 0 (player 1) and
13
- * port 1 acts as a wall-bot AI so single-player still works.
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
- * Frame budget (NTSC): 60 fps × paddle moves × 1 ball update +
16
- * 2 collision checks = negligible. Easily fits in vblank.
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
- * Read TROUBLESHOOTING.md before editing same NES gotchas as the
19
- * other scaffolds (two-vblank PPU warm-up, shadow_oam staging, palette
20
- * at $3F00).
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
- /* ── Tile data ────────────────────────────────────────────────────── */
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
- /* 8×8 paddle = solid white column (4 pixels wide centered) */
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 court tiles (background pattern table $1000), so the court reads as a
38
- * real Pong arena on boot instead of sprites on flat black:
39
- * BG_WALL — solid bar (idx1 white): the top/bottom rails.
40
- * BG_NET — dashed vertical bar (idx1 white): the centre net.
41
- * BG_FLOOR — a faint court-floor hatch (idx2 green) tiled across the whole
42
- * playfield so the arena surface is covered, not black. */
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, /* dashed vertical bar */
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, /* plane 0 clear */
53
- 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, 0xAA, 0x55, /* plane 1 → colour 2 checker */
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
- /* Tile indices (after upload to CHR-RAM at slot 0..). */
73
- #define T_BLANK 0
74
- #define T_PADDLE 1
75
- #define T_BALL 2
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
- /* BG0: backdrop near-black, court walls/net = white (idx1),
80
- * court-floor hatch = dark green (idx2) */
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
- /* Sprite palettes */
86
- 0x0F, 0x30, 0x16, 0x12, /* sp0: white paddle */
87
- 0x0F, 0x30, 0x16, 0x12, /* sp1: white ball */
88
- 0x0F, 0x30, 0x16, 0x12,
89
- 0x0F, 0x30, 0x16, 0x12,
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
- /* Game state (fixed-point Y in 1/16-px units). */
93
- static int16_t p1y, p2y; /* paddle top Y in pixels */
94
- static int16_t bx, by; /* ball x, y in pixels */
95
- static int8_t bdx, bdy; /* ball velocity */
96
- static uint8_t score_p1, score_p2;
97
- static uint8_t serve_timer;
98
-
99
- #define PADDLE_H 24
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 = 128;
109
- by = 112;
133
+ bx = 124;
134
+ by = 116;
110
135
  bdx = to_left ? -2 : 2;
111
- /* Alternate vertical direction each serve. */
112
- bdy = (score_p1 + score_p2) & 1 ? -1 : 1;
113
- serve_timer = 30;
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
- static void reset_match(void) {
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 i;
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
- /* Upload sprite tiles (sprite pattern table $0000). */
129
- chr_ram_upload(T_BLANK * 16, tile_blank, 16);
130
- chr_ram_upload(T_PADDLE * 16, tile_paddle, 16);
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
- /* Paint the court into the nametable while rendering is off
141
- * (vram_unsafe_set = raw write). First carpet the whole playfield with the
142
- * green floor hatch so the arena surface is covered, then lay the top/
143
- * bottom rails (rows 1 and 27) and the dashed centre net (column 15) on
144
- * top. Without the floor the screen is just sprites on flat black. */
145
- {
146
- uint16_t cc, rr;
147
- for (rr = 0; rr < 30; rr++)
148
- for (cc = 0; cc < 32; cc++)
149
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_FLOOR);
150
- for (cc = 0; cc < 32; cc++) {
151
- vram_unsafe_set((uint16_t)(0x2000 + 1 * 32 + cc), BG_WALL); /* top rail (y≈8) */
152
- vram_unsafe_set((uint16_t)(0x2000 + 27 * 32 + cc), BG_WALL); /* bottom rail(y≈216)*/
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
- oam_clear();
159
- ppu_on_all();
160
- sound_init();
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
- reset_match();
333
+ /* ── ST_PLAY ─────────────────────────────────────────────────────── */
163
334
 
164
- for (;;) {
165
- /* Stage OAM. Paddles are 3 sprites stacked (24px tall = 3×8).
166
- * Ball is one sprite. Score digits use 1 sprite each. */
167
- oam_clear();
168
- /* Paddle 1 left side */
169
- for (i = 0; i < PADDLE_H / 8; i++)
170
- oam_spr(PADDLE_X1, (uint8_t)(p1y + i * 8), T_PADDLE, 0);
171
- /* Paddle 2 — right side */
172
- for (i = 0; i < PADDLE_H / 8; i++)
173
- oam_spr(PADDLE_X2, (uint8_t)(p2y + i * 8), T_PADDLE, 1);
174
- /* Ball */
175
- oam_spr((uint8_t)bx, (uint8_t)by, T_BALL, 0);
176
- /* Scores single digit each (0..9 only; extend if you want
177
- * multi-digit). */
178
- oam_spr(48, 4, (uint8_t)(T_DIGIT0 + score_p1), 0);
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
- p1 = pad_poll(0);
186
- p2 = pad_poll(1);
187
-
188
- if (p1 & PAD_UP && p1y > COURT_TOP) p1y -= 2;
189
- if (p1 & PAD_DOWN && p1y < COURT_BOT - PADDLE_H) p1y += 2;
190
-
191
- /* Wall-bot AI on port 1 when no second controller — pad_poll(1)
192
- * returns 0 when no controller is connected. Detect by sampling
193
- * a few frames; here we simply fall back to AI when no buttons
194
- * are ever pressed. Simpler: always run AI as a *fallback*
195
- * — if p2 pressed anything this frame, use the human input. */
196
- if (p2 != 0) {
197
- if (p2 & PAD_UP && p2y > COURT_TOP) p2y -= 2;
198
- if (p2 & PAD_DOWN && p2y < COURT_BOT - PADDLE_H) p2y += 2;
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
- /* AI: chase the ball Y, biased toward ball-direction. */
201
- int16_t target = by - PADDLE_H / 2;
202
- if (p2y < target && p2y < COURT_BOT - PADDLE_H) p2y += 1;
203
- else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
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
- /* ── Ball update ──────────────────────────────────────────── */
377
+ /* Ball update (frozen during the post-point serve pause). */
207
378
  if (serve_timer > 0) {
208
- serve_timer--;
209
- } else {
210
- bx += bdx;
211
- by += bdy;
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
- /* Score: ball off either side. */
239
- if (bx < 4) {
240
- if (score_p2 < 9) score_p2++;
241
- sound_play_noise(5, 8, 8);
242
- serve_ball(0);
243
- }
244
- if (bx > 252) {
245
- if (score_p1 < 9) score_p1++;
246
- sound_play_noise(5, 8, 8);
247
- serve_ball(1);
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
  }