romdevtools 0.27.0 → 0.29.0

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