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