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,230 +1,656 @@
1
- /* ── platformer.c — NES single-screen platformer scaffold ────────────
1
+ /* ── platformer.c — NES side-scrolling platformer (complete example game) ────
2
2
  *
3
- * A complete, runnable platformer baseline. Includes:
4
- * - Player (8x8 sprite) d-pad LEFT/RIGHT moves, A jumps
5
- * - Gravity + jump physics with fixed-point Y (Q4.4: 4 frac bits)
6
- * - Static platform list (5 horizontal rects) collide-from-above only
7
- * - Walk + jump animation via tile-id swapping
8
- * - Right-edge "you win" silence; fall off bottom → respawn
3
+ * LEDGE LEAPER — a COMPLETE, working game: title screen, 1P mode and 2P
4
+ * ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
5
+ * has their own score and own 3 lives; player 2 plays on CONTROLLER 2),
6
+ * coins + distance scoring, persistent hi-score (battery SRAM), music +
7
+ * SFX, and the NES's signature sprite-0-hit split: a fixed HUD strip over
8
+ * a horizontally scrolling level.
9
9
  *
10
- * Why fixed-point Y: gravity adds < 1 pixel/frame at peak jump, so we
11
- * need sub-pixel precision. X stays integer because horizontal motion
12
- * is integer-speed (no acceleration). Standard NES platformer trick.
10
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
11
+ * very different one. The markers tell you what's what:
12
+ * HARDWARE IDIOM (load-bearing) dodges a documented NES footgun; reshape
13
+ * your gameplay around it (see TROUBLESHOOTING before changing).
14
+ * GAME LOGIC (clay) — level layout, physics tuning, scoring, art: reshape
15
+ * freely.
13
16
  *
14
- * Collision is rect-vs-rect "land on top" only — no side-collision,
15
- * no head-bump-stop. Add those when your scaffold proves out. This
16
- * keeps the baseline understandable + extendable.
17
+ * What depends on what:
18
+ * nes_runtime.{h,c} rendering/input/sound/text/hi-score library.
19
+ * chr-ram-runtime.crt0.s boot + NMI + iNES header (BATTERY bit feeds
20
+ * hiscore_load/save). Load-bearing; edit with TROUBLESHOOTING open.
17
21
  *
18
- * SINGLE-SCREEN as shipped (platforms are drawn as SPRITES, not BG).
19
- * To make it a SIDE-SCROLLER you must draw platforms into the BACKGROUND
20
- * nametables (so hardware scroll moves them), use ppu_scroll(camX, 0)
21
- * which flips the PPUCTRL nametable-select bit past 256 px for you — and
22
- * for a world wider than 512 px stream a fresh nametable column + its
23
- * attribute byte into the off-screen nametable each 8-px camera step.
24
- * The chr-ram-runtime crt0 uses VERTICAL mirroring, giving NT0 (left) +
25
- * NT1 (right) side by side = a free 512-px world. See the NES
26
- * MENTAL_MODEL.md "Backgrounds" + "NMI" sections.
22
+ * The level: a 256-px-wide COLUMN MAP (ground height + one-way platforms +
23
+ * pits) painted IDENTICALLY into both nametables, so the 8-bit X scroll
24
+ * wraps seamlessly an endless looping run of pits, platforms, coins and
25
+ * spikes. Coins/spikes are sprites that drift with the scroll (world-
26
+ * anchored while on screen, respawning at the right edge).
27
+ *
28
+ * Frame budget (NTSC, 60fps): player physics + a two-column tile probe +
29
+ * (3 coins + 2 spikes) of AABB + the sprite-0 spin (a few scanlines) fits
30
+ * comfortably in one frame; a HUD redraw is ≤12 queued VRAM writes (the
31
+ * queue drains 16 per vblank).
27
32
  */
28
33
 
29
34
  #include "nes_runtime.h"
30
35
 
31
- /* ── Tiles: player idle/jump + platform block (SPRITES, table $0000) ── */
36
+ /* The title screen renders this examples({op:'fork'}) stamps your game's
37
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
38
+ #define GAME_TITLE "LEDGE LEAPER"
39
+
40
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
41
+ * Tile art. Each 8x8 tile = 16 bytes: 8 plane-0 rows then 8 plane-1 rows
42
+ * (2bpp — plane0-only pixels use colour 1, both planes = colour 3). */
32
43
  static const uint8_t tile_blank[16] = { 0 };
33
44
  static const uint8_t tile_player_idle[16] = {
34
- 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, /* plane 0 round blob */
35
- 0, 0, 0, 0, 0, 0, 0, 0,
45
+ 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0x7E, 0x66, 0x66, /* round body + legs */
46
+ 0x00, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00, /* eyes (colour 3) */
36
47
  };
37
48
  static const uint8_t tile_player_jump[16] = {
38
- 0x18, 0x7E, 0xFF, 0xFF, 0xE7, 0xC3, 0x81, 0x00, /* plane 0 — arms up */
49
+ 0x18, 0x7E, 0xFF, 0xFF, 0xE7, 0xC3, 0x81, 0x00, /* arms up */
50
+ 0x00, 0x24, 0x24, 0x00, 0x00, 0x00, 0x00, 0x00,
51
+ };
52
+ static const uint8_t tile_coin[16] = {
53
+ 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C, /* coin disc */
54
+ 0x00, 0x3C, 0x66, 0x5A, 0x5A, 0x66, 0x3C, 0x00, /* embossed ring */
55
+ };
56
+ static const uint8_t tile_spike[16] = {
57
+ 0x00, 0x18, 0x18, 0x3C, 0x3C, 0x7E, 0x7E, 0xFF, /* ground spike */
39
58
  0, 0, 0, 0, 0, 0, 0, 0,
40
59
  };
41
- static const uint8_t tile_platform[16] = {
42
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* plane 0 — solid */
60
+ /* Sprite 0's marker block — fully OPAQUE (the sprite-0 hit fires on
61
+ * opaque-sprite-over-opaque-BG, colour is irrelevant). Its palette below
62
+ * makes it the same brown as the HUD bar, so it's invisible in the bar. */
63
+ static const uint8_t tile_mark[16] = {
64
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
43
65
  0, 0, 0, 0, 0, 0, 0, 0,
44
66
  };
45
67
 
46
- /* ── BG scenery tiles (BACKGROUND pattern table $1000) ────────────────
47
- * Painted into the nametable so the world reads as a real outdoor scene on
48
- * boot (sky + clouds + dirt) instead of sprites floating on flat black. The
49
- * BG backdrop colour (palette[0]) is sky blue, so the colours used here are
50
- * cloud white (idx1), dirt brown (idx2) and grass green (idx3).
51
- *
52
- * BG_CLOUD — a puffy cloud (idx1) dotted across the upper sky.
53
- * BG_DIRT — solid dirt fill (idx2) for the ground band.
54
- * BG_GRASS — a grass-topped dirt tile (idx3 cap over idx2) for the
55
- * surface row of the ground. */
56
- #define BG_CLOUD 1 /* BG tile index 1 → uploaded to $1010 */
57
- #define BG_DIRT 2 /* BG tile index 2 → uploaded to $1020 */
58
- #define BG_GRASS 3 /* BG tile index 3 → uploaded to $1030 */
68
+ /* BG tiles (BACKGROUND pattern table $1000 — separate from the sprite
69
+ * table at $0000; the runtime's PPUCTRL setup makes that split). */
59
70
  static const uint8_t bg_tile_cloud[16] = {
60
- 0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x00, 0x00, 0x00, /* plane 0 → cloud (idx1) */
71
+ 0x00, 0x18, 0x3C, 0x7E, 0x7E, 0x00, 0x00, 0x00, /* puffy cloud (idx1) */
61
72
  0, 0, 0, 0, 0, 0, 0, 0,
62
73
  };
63
74
  static const uint8_t bg_tile_dirt[16] = {
64
- 0, 0, 0, 0, 0, 0, 0, 0, /* plane 0 clear */
65
- 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, /* plane 1 → dirt (idx2) */
75
+ 0, 0, 0, 0, 0, 0, 0, 0,
76
+ 0xFF, 0xFF, 0xEF, 0xFF, 0xFF, 0xFE, 0xFF, 0xFF, /* dirt fill (idx2) */
66
77
  };
67
78
  static const uint8_t bg_tile_grass[16] = {
68
- 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* plane0: top 2 rows on */
69
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* plane1: all rows on */
70
- }; /* rows 0-1 → both planes = idx3 (grass cap); rows 2-7 → plane1 = idx2 (dirt) */
79
+ 0xFF, 0xFF, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, /* top 2 rows idx3 */
80
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* rest idx2 (dirt) */
81
+ };
82
+ /* A solid tile for the HUD bar — sprite 0 must overlap an OPAQUE BG pixel
83
+ * for the sprite-0 hit to fire (see the split idiom below). */
84
+ static const uint8_t bg_tile_hudbar[16] = {
85
+ 0, 0, 0, 0, 0, 0, 0, 0,
86
+ 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, /* solid idx2 brown */
87
+ };
88
+ #define BG_CLOUD 1
89
+ #define BG_DIRT 2
90
+ #define BG_GRASS 3 /* also used for floating platforms (grass slabs) */
91
+ #define BG_HUDBAR 4
71
92
 
72
93
  static const uint8_t palette[32] = {
73
- /* BG0: sky-blue backdrop, cloud white (idx1), dirt brown (idx2),
74
- * grass green (idx3) */
94
+ /* BG: ALL FOUR sub-palettes identical (sky, cloud white, dirt brown,
95
+ * grass green). That makes stale attribute-table bits harmless — power-on
96
+ * CIRAM is garbage, and identical sub-palettes mean any attribute value
97
+ * picks the same colours. We clear the attribute tables anyway (belt and
98
+ * braces, see paint_field). */
75
99
  0x21, 0x30, 0x17, 0x2A,
76
100
  0x21, 0x30, 0x17, 0x2A,
77
101
  0x21, 0x30, 0x17, 0x2A,
78
102
  0x21, 0x30, 0x17, 0x2A,
79
- /* The universal backdrop ($3F00) is MIRRORED at $3F10 — the sprite
80
- * palette-0 colour-0 slot. palette_load writes all 32 bytes in order, so
81
- * byte 16 (this slot) is the LAST write to that mirror and therefore wins.
82
- * Keep it equal to the BG backdrop (sky blue) or the sky renders as
83
- * whatever colour-0 you put here, not the BG[0] above. (Sprite colour 0 is
84
- * transparent regardless, so this never affects how sprites draw.) */
85
- 0x21, 0x21, 0x32, 0x30, /* sp0: playerblue (colour 0 = backdrop mirror) */
86
- 0x0F, 0x18, 0x28, 0x38, /* sp1: platformsgreen */
87
- 0x0F, 0x16, 0x06, 0x36,
88
- 0x0F, 0x2A, 0x1A, 0x0A,
103
+ /* The universal backdrop ($3F00) is MIRRORED at $3F10 — sprite palette 0
104
+ * colour 0. palette_load writes all 32 bytes in order, so this byte is
105
+ * the LAST write to the mirror and wins: keep it equal to the BG backdrop
106
+ * (sky blue) or the whole sky changes colour. (Sprite colour 0 is
107
+ * transparent regardless this never affects how sprites draw.) */
108
+ 0x21, 0x16, 0x30, 0x27, /* sp0: player red body, white/orange trim */
109
+ 0x0F, 0x17, 0x17, 0x17, /* sp1: sprite-0 marker HUD-bar brown camo */
110
+ 0x0F, 0x16, 0x06, 0x30, /* sp2: spikesdanger red */
111
+ 0x0F, 0x28, 0x27, 0x30, /* sp3: coins — gold */
89
112
  };
90
113
 
91
- /* ── Platforms (static rectangles) ───────────────────────────────── */
92
- typedef struct { uint8_t x, y, w; } Platform; /* 8 px tall, w in tiles */
93
- #define NUM_PLATFORMS 5
94
- static const Platform platforms[NUM_PLATFORMS] = {
95
- { 16, 200, 20 }, /* ground band */
96
- { 40, 168, 4 },
97
- { 104, 144, 4 },
98
- { 168, 168, 4 },
99
- { 200, 120, 4 },
114
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
115
+ * The level a 32-column map; world x = (screen x + scroll) mod 256.
116
+ * ground_row[c] — nametable row of the ground's grass top, 0xFF = pit.
117
+ * plat_row[c] row of a one-way floating platform, 0 = none.
118
+ * Rows are nametable rows (y = row*8). Playfield rows are 3..29. */
119
+ #define NO_GROUND 0xFF
120
+ static const uint8_t ground_row[32] = {
121
+ 26, 26, 26, 26, 26, 26, 26, 26, /* start runway */
122
+ 26, NO_GROUND, NO_GROUND, 26, 26, 26, 26, 26, /* pit 1 (16 px) */
123
+ 26, 26, 26, 26, NO_GROUND, NO_GROUND, NO_GROUND, /* pit 2 (24 px) */
124
+ 26, 26, 26, 26, 26, 26, 26, 26, 26,
125
+ };
126
+ static const uint8_t plat_row[32] = {
127
+ 0, 0, 0, 0, 21, 21, 21, 0, /* slab before pit 1 */
128
+ 0, 0, 0, 0, 0, 0, 20, 20, /* slab mid-level */
129
+ 20, 0, 0, 0, 0, 0, 0, 0,
130
+ 0, 21, 21, 21, 0, 0, 0, 0, /* slab near the loop */
100
131
  };
101
132
 
102
- #define TILE_PLAYER_IDLE 1
103
- #define TILE_PLAYER_JUMP 2
104
- #define TILE_PLATFORM 3
105
- #define PLAYER_PAL 0
106
- #define PLATFORM_PAL 1
133
+ #define TILE_PLAYER_IDLE 1
134
+ #define TILE_PLAYER_JUMP 2
135
+ #define TILE_COIN 3
136
+ #define TILE_SPIKE 4
137
+ #define TILE_MARK 5
138
+ #define PLAYER_PAL 0
139
+ #define MARK_PAL 1
140
+ #define SPIKE_PAL 2
141
+ #define COIN_PAL 3
142
+
143
+ /* HUD layout (mind the OVERSCAN: most NTSC displays/cores crop the top 8
144
+ * scanlines, so nametable row 0 is invisible — never put text there):
145
+ * row 0 — blank (cropped by overscan)
146
+ * row 1 — HUD text (P# / lives / SC / HI)
147
+ * row 2 — solid bar: the visual divider AND sprite 0's opaque anchor
148
+ * row 3+ — the scrolling playfield
149
+ * The HUD strip always renders with scroll (0,0) from nametable 0, so HUD
150
+ * text lives ONLY in nametable 0 — it can never scroll into view twice. */
151
+ #define HUD_ROWS 3
152
+ #define START_LIVES 3
107
153
 
108
- /* ── Player state ────────────────────────────────────────────────── */
109
- static uint8_t px = 24;
110
- static uint16_t py_q44 = (200 - 8) << 4; /* Q4.4 start above leftmost platform */
111
- static int8_t vy_q44 = 0;
112
- static uint8_t on_ground = 0;
154
+ /* ── GAME LOGIC (clay) — physics + tuning ── */
155
+ #define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
156
+ #define JUMP_VEL_Q44 (-40) /* launch vy (Q4.4) ~50 px / ~6 tile apex */
157
+ #define MAX_VY_Q44 80 /* terminal velocity, 5 px/frame — MUST stay *
158
+ * under 6: the landing probe's 6-px window *
159
+ * can't catch a faster fall (tunnelling) */
160
+ #define MOVE_SPEED 2 /* px/frame walk + scroll speed */
161
+ #define SCROLL_WALL 112 /* px: past this the world scrolls, not you */
162
+ #define GROUND_TOP 208 /* ground_row 26 * 8 */
163
+ #define SPIKE_Y 200 /* spikes stand on the ground */
164
+ #define NUM_COINS 3
165
+ #define NUM_SPIKES 2
113
166
 
114
- #define GRAVITY_Q44 1 /* +1/16 px per frame per frame */
115
- #define JUMP_VEL_Q44 (-40) /* initial vy peak ~5 tile jump */
116
- #define MOVE_SPEED 1 /* 1 px / frame */
167
+ static uint8_t px; /* player screen x */
168
+ static uint16_t py_q44; /* player y, Q4.4 fixed point gravity
169
+ * adds <1 px/frame near the jump apex,
170
+ * so we need sub-pixel precision */
171
+ static int8_t vy_q44;
172
+ static uint8_t on_ground;
173
+ static uint8_t scroll_x; /* level scroll — uint8 wraps at 256 = *
174
+ * exactly one level loop (seamless) */
175
+ static uint8_t dist_sub; /* sub-counter: 64 px scrolled = +1 pt */
176
+ static uint8_t coin_x[NUM_COINS], coin_y[NUM_COINS];
177
+ static uint8_t spike_x[NUM_SPIKES], spike_active[NUM_SPIKES];
117
178
 
118
- /* AABB: player rect vs platform top edge (treat platform as 8 px tall). */
119
- static uint8_t landed_on(uint8_t pl_idx, uint8_t player_y) {
120
- const Platform *p = &platforms[pl_idx];
121
- uint8_t feet_y = player_y + 7;
122
- if (feet_y < p->y || feet_y > p->y + 4) return 0; /* not at top edge */
123
- if (px + 7 < p->x) return 0;
124
- if (px > p->x + (p->w << 3) - 1) return 0;
125
- return 1;
179
+ /* Players: index 0 = P1 (controller 1), 1 = P2 (controller 2 alternating
180
+ * turns, arcade-classic style). Each has own score + own lives; the HUD shows the
181
+ * CURRENT player's numbers. */
182
+ static uint8_t two_player;
183
+ static uint8_t cur_player;
184
+ static uint8_t p_lives[2];
185
+ static uint16_t p_score[2];
186
+ static uint16_t hiscore;
187
+ static uint8_t turn_pause; /* freeze frames after a turn change */
188
+ static uint16_t rng = 0xC0DE;
189
+
190
+ /* Game states — the shell every example shares: title → play → game over. */
191
+ #define ST_TITLE 0
192
+ #define ST_PLAY 1
193
+ #define ST_OVER 2
194
+ static uint8_t state;
195
+ static uint8_t prev_pad;
196
+
197
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
198
+ static uint8_t random8(void) {
199
+ uint16_t r = rng;
200
+ r ^= r << 7;
201
+ r ^= r >> 9;
202
+ r ^= r << 8;
203
+ rng = r;
204
+ return (uint8_t)r;
126
205
  }
127
206
 
128
- void main(void) {
129
- uint8_t pad, prev_pad = 0;
130
- uint8_t player_y, i;
207
+ static uint8_t dist8(uint8_t a, uint8_t b) {
208
+ return (a > b) ? (a - b) : (b - a);
209
+ }
131
210
 
132
- ppu_off();
211
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
212
+ * Sprite-0-hit split scroll — THE classic NES technique (the fixed
213
+ * status bar over a scrolling field in countless NES classics). The PPU has ONE scroll for the whole
214
+ * frame; to keep the HUD fixed while the playfield scrolls, you change the
215
+ * scroll MID-FRAME, and sprite 0 is your timing signal:
216
+ *
217
+ * 1. Sprite 0 (the FIRST sprite staged each frame) sits inside the HUD,
218
+ * overlapping an OPAQUE background pixel (our solid HUD bar tile).
219
+ * 2. The NMI commits scroll (0,0) at vblank — the HUD renders unscrolled.
220
+ * 3. After ppu_wait_nmi(), poll PPUSTATUS bit 6 in TWO phases: first wait
221
+ * for it to CLEAR (the stale flag from the previous frame survives all
222
+ * of vblank and only clears at the pre-render line), then wait for it
223
+ * to SET — the exact pixel where sprite 0's opaque pixel overlaps
224
+ * opaque background.
225
+ * 4. THEN write the playfield scroll to PPUSCROLL — everything below the
226
+ * HUD renders with the new scroll.
227
+ *
228
+ * Requires: sprite 0 staged FIRST (oam_spr call order = OAM order), an
229
+ * opaque BG pixel under it, ppu_scroll(0,0) left as the frame scroll, and
230
+ * this poll running EVERY frame (miss a frame and the field jumps).
231
+ * Mid-frame X-scroll needs only the two PPUSCROLL writes below. (Mid-frame
232
+ * Y needs the 4-write $2006/$2005 dance — see TROUBLESHOOTING before
233
+ * attempting; X covers the HUD-over-scrolling-field pattern.)
234
+ * The two-phase spin burns from vblank start to the hit scanline — about
235
+ * 35 scanlines of CPU every frame. Budget for it: your game logic gets the
236
+ * rest of the visible frame, which is plenty for a game this size. */
237
+ #define PPUSTATUS_REG (*(volatile uint8_t *)0x2002)
238
+ #define PPUSCROLL_REG (*(volatile uint8_t *)0x2005)
239
+ static void split_after_hud(void) {
240
+ uint8_t timeout = 240;
241
+ /* FOOTGUN: the hit flag from the frame JUST RENDERED stays set all the
242
+ * way through vblank — it only clears at the next pre-render line. We're
243
+ * called right after ppu_wait_nmi() (i.e. inside vblank), so polling for
244
+ * "set" alone exits INSTANTLY on the stale flag and the PPUSCROLL write
245
+ * lands during vblank — scrolling the WHOLE next frame, HUD included
246
+ * (the shear is subtle: it looks like the HUD "drifting"). The classic
247
+ * fix is the two-phase poll: wait for the stale flag to CLEAR (the
248
+ * pre-render line), then wait for THIS frame's hit to SET. */
249
+ while (PPUSTATUS_REG & 0x40) {
250
+ if (--timeout == 0) return; /* flag stuck: bail, keep scroll (0,0) */
251
+ }
252
+ timeout = 240;
253
+ while (!(PPUSTATUS_REG & 0x40)) {
254
+ if (--timeout == 0) return; /* rendering off / sprite-0 missing: bail */
255
+ }
256
+ PPUSCROLL_REG = scroll_x; /* playfield X scroll (below the HUD) */
257
+ PPUSCROLL_REG = 0;
258
+ }
259
+
260
+ /* Stage sprite 0 = an 8x8 opaque block over the HUD BAR row (OAM y is
261
+ * scanline-1, so y=16 renders scanlines 17-24 = nametable row 2 = the bar —
262
+ * opaque-on-opaque, so the hit fires INSIDE the bar and the scroll change
263
+ * lands below it, never shearing the text row). Must be the FIRST oam_spr
264
+ * call of the frame (OAM order = call order; the split needs index 0). */
265
+ static void stage_sprite0(void) {
266
+ oam_spr(4, (HUD_ROWS - 1) * 8, TILE_MARK, MARK_PAL);
267
+ }
133
268
 
134
- chr_ram_upload(0x0000, tile_blank, 16);
135
- chr_ram_upload(0x0010, tile_player_idle, 16);
136
- chr_ram_upload(0x0020, tile_player_jump, 16);
137
- chr_ram_upload(0x0030, tile_platform, 16);
269
+ /* ── GAME LOGIC (clay) — HUD text (queued writes; NMI commits next vblank) ── */
270
+ static void draw_hud(void) {
271
+ tile_set(0, 1, 1, (uint8_t)(0x41 + cur_player)); /* '1' or '2' */
272
+ tile_set(0, 3, 1, 0x40 + p_lives[cur_player]); /* lives as a digit */
273
+ text_draw_u16(0, 9, 1, p_score[cur_player]);
274
+ text_draw_u16(0, 19, 1, hiscore);
275
+ }
138
276
 
139
- /* BG scenery tiles go in pattern table 1 ($1000) — where the default
140
- * PPUCTRL points the background fetch. */
141
- chr_ram_upload(0x1010, bg_tile_cloud, 16);
142
- chr_ram_upload(0x1020, bg_tile_dirt, 16);
143
- chr_ram_upload(0x1030, bg_tile_grass, 16);
277
+ static void draw_hud_labels(void) {
278
+ text_draw(0, 0, 1, "P");
279
+ text_draw(0, 6, 1, "SC");
280
+ text_draw(0, 16, 1, "HI");
281
+ }
144
282
 
145
- palette_load(palette);
283
+ /* PPU-off digit painter (the queued text_draw_u16 needs rendering ON). */
284
+ static void digits_unsafe(uint16_t ppu_addr, uint16_t v) {
285
+ uint8_t d[5], i;
286
+ for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
287
+ for (i = 0; i < 5; i++) vram_unsafe_set(ppu_addr + i, (uint8_t)(0x40 + d[4 - i]));
288
+ }
146
289
 
147
- /* Paint the outdoor scene into the nametable while rendering is off
148
- * (vram_unsafe_set = raw write; tile_set's NMI queue would deadlock before
149
- * ppu_on). Sky-blue backdrop shows through the empty upper rows; we dot
150
- * clouds across it, cap the ground with a grass row, and fill the bottom
151
- * band with solid dirt. The sprite platforms still draw on top of this. */
152
- {
153
- uint16_t r, c;
154
- /* clouds scattered through the upper sky (rows 2..9) */
155
- for (r = 2; r < 10; r++)
156
- for (c = (uint16_t)((r * 3) % 6); c < 32; c += 6)
157
- vram_unsafe_set((uint16_t)(0x2000 + r * 32 + c), BG_CLOUD);
158
- /* grass cap row + solid dirt to the bottom of the screen */
159
- for (c = 0; c < 32; c++)
160
- vram_unsafe_set((uint16_t)(0x2000 + 24 * 32 + c), BG_GRASS);
161
- for (r = 25; r < 30; r++)
162
- for (c = 0; c < 32; c++)
163
- vram_unsafe_set((uint16_t)(0x2000 + r * 32 + c), BG_DIRT);
290
+ /* ── GAME LOGIC (clay) the title screen ──────────────────────────────────
291
+ * Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes; the queued
292
+ * variant would deadlock with rendering disabled see TROUBLESHOOTING). */
293
+ static void paint_title(void) {
294
+ uint16_t a = 0x2000;
295
+ uint8_t r, c, t;
296
+ ppu_off();
297
+ for (r = 0; r < 30; r++) {
298
+ for (c = 0; c < 32; c++) {
299
+ t = 0; /* sky backdrop */
300
+ if (r == 26) t = BG_GRASS;
301
+ else if (r > 26) t = BG_DIRT;
302
+ vram_unsafe_set(a, t);
303
+ ++a;
304
+ }
164
305
  }
306
+ text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
307
+ text_draw_unsafe(0x2000 + 13 * 32 + 10, "1P START - A");
308
+ text_draw_unsafe(0x2000 + 15 * 32 + 10, "2P TURNS - B");
309
+ text_draw_unsafe(0x2000 + 20 * 32 + 10, "HI");
310
+ digits_unsafe(0x2000 + 20 * 32 + 13, hiscore);
311
+ ppu_scroll(0, 0);
312
+ oam_clear();
313
+ ppu_on_all();
314
+ }
165
315
 
316
+ /* ── GAME LOGIC (clay) — paint the level from the column map ───────────────
317
+ * Painted into BOTH nametables (vertical mirroring puts NT0 + NT1 side by
318
+ * side = a 512-px canvas). Identical copies + a 256-px-periodic level make
319
+ * the uint8 scroll wrap PERFECTLY seamless: the visible window always shows
320
+ * the same content at world x mod 256, so the run loops forever. */
321
+ static void paint_field(void) {
322
+ uint16_t base, a;
323
+ uint8_t nt, r, c, t, g;
324
+ ppu_off();
325
+ for (nt = 0; nt < 2; nt++) {
326
+ base = nt ? 0x2400 : 0x2000;
327
+ a = base;
328
+ for (c = 0; c < 32; c++) { vram_unsafe_set(a, 0); ++a; } /* row 0: overscan */
329
+ for (c = 0; c < 32; c++) { vram_unsafe_set(a, 0); ++a; } /* row 1: HUD text */
330
+ for (c = 0; c < 32; c++) { vram_unsafe_set(a, BG_HUDBAR); ++a; } /* row 2: bar */
331
+ for (r = HUD_ROWS; r < 30; r++) {
332
+ for (c = 0; c < 32; c++) {
333
+ g = ground_row[c];
334
+ t = 0;
335
+ if (r == plat_row[c]) t = BG_GRASS; /* floating slab */
336
+ else if (g != NO_GROUND) {
337
+ if (r == g) t = BG_GRASS; /* ground surface */
338
+ else if (r > g) t = BG_DIRT; /* ground body */
339
+ }
340
+ if (t == 0 && r >= 4 && r <= 9) {
341
+ if (((r * 7 + c * 5) & 15) == 0) t = BG_CLOUD;
342
+ }
343
+ vram_unsafe_set(a, t);
344
+ ++a;
345
+ }
346
+ }
347
+ /* Attribute table → palette 0 everywhere. CIRAM powers on as garbage;
348
+ * with our identical BG sub-palettes it wouldn't show, but clear it so
349
+ * forks that diverge the palettes don't inherit a latent bug. */
350
+ a = base + 0x3C0;
351
+ for (c = 0; c < 64; c++) { vram_unsafe_set(a, 0); ++a; }
352
+ }
353
+ ppu_scroll(0, 0);
166
354
  oam_clear();
167
355
  ppu_on_all();
356
+ /* Labels go through the queued path once rendering is on. */
357
+ draw_hud_labels();
358
+ }
359
+
360
+ /* ── GAME LOGIC (clay) — the game-over results screen ── */
361
+ static void paint_over(void) {
362
+ uint16_t a = 0x2000;
363
+ uint16_t i;
364
+ ppu_off();
365
+ for (i = 0; i < 960; i++) { vram_unsafe_set(a, 0); ++a; }
366
+ text_draw_unsafe(0x2000 + 8 * 32 + 11, "GAME OVER");
367
+ text_draw_unsafe(0x2000 + 12 * 32 + 9, "P1");
368
+ digits_unsafe(0x2000 + 12 * 32 + 13, p_score[0]);
369
+ if (two_player) {
370
+ text_draw_unsafe(0x2000 + 14 * 32 + 9, "P2");
371
+ digits_unsafe(0x2000 + 14 * 32 + 13, p_score[1]);
372
+ }
373
+ text_draw_unsafe(0x2000 + 17 * 32 + 9, "HI");
374
+ digits_unsafe(0x2000 + 17 * 32 + 13, hiscore);
375
+ text_draw_unsafe(0x2000 + 21 * 32 + 9, "START - TITLE");
376
+ ppu_scroll(0, 0);
377
+ oam_clear();
378
+ ppu_on_all();
379
+ }
380
+
381
+ /* ── GAME LOGIC (clay) — coins + spikes (sprite objects in the world) ── */
382
+ static const uint8_t coin_heights[4] = { 184, 160, 128, 152 };
383
+ static void respawn_coin(uint8_t i) {
384
+ coin_x[i] = (uint8_t)(232 + (random8() & 15)); /* enter at the right */
385
+ coin_y[i] = coin_heights[random8() & 3];
386
+ }
387
+
388
+ static void try_spawn_spike(uint8_t i) {
389
+ /* Anchor only over ground: an inactive spike rolls a low per-frame
390
+ * chance, and only spawns if the level column entering at the right
391
+ * edge has ground under it (never floats over a pit). */
392
+ uint8_t c = (uint8_t)(248 + scroll_x) >> 3;
393
+ if (ground_row[c] == NO_GROUND) return;
394
+ if (random8() > 4) return;
395
+ spike_x[i] = 248;
396
+ spike_active[i] = 1;
397
+ }
398
+
399
+ /* ── GAME LOGIC (clay) — start a turn / a run ── */
400
+ static void begin_turn(void) {
401
+ px = 24;
402
+ py_q44 = (uint16_t)(GROUND_TOP - 8) << 4;
403
+ vy_q44 = 0;
404
+ on_ground = 1;
405
+ scroll_x = 0;
406
+ dist_sub = 0;
407
+ coin_x[0] = 88; coin_y[0] = 184;
408
+ coin_x[1] = 152; coin_y[1] = 160;
409
+ coin_x[2] = 216; coin_y[2] = 128;
410
+ spike_x[0] = 136; spike_active[0] = 1; /* both anchored on ground at */
411
+ spike_x[1] = 224; spike_active[1] = 1; /* scroll 0 — see ground_row */
412
+ turn_pause = 48; /* "P1/P2 ready" breather */
413
+ prev_pad = 0xFF; /* swallow held buttons across *
414
+ * the turn change */
415
+ ppu_scroll(0, 0);
416
+ draw_hud();
417
+ }
418
+
419
+ static void start_game(uint8_t players) {
420
+ two_player = players;
421
+ cur_player = 0;
422
+ p_score[0] = p_score[1] = 0;
423
+ p_lives[0] = START_LIVES;
424
+ p_lives[1] = players ? START_LIVES : 0;
425
+ paint_field();
426
+ begin_turn();
427
+ sound_play_tone(0, 0x0FD, 8, 8); /* start jingle (A4) */
428
+ state = ST_PLAY;
429
+ }
430
+
431
+ static void game_over(void) {
432
+ uint16_t best = p_score[0];
433
+ if (two_player && p_score[1] > best) best = p_score[1];
434
+ if (best > hiscore) {
435
+ hiscore = best;
436
+ /* ── HARDWARE IDIOM (load-bearing) — persists via battery PRG-RAM at
437
+ * $6000; works because the crt0's iNES header sets the BATTERY bit.
438
+ * See nes_runtime.c for the magic+checksum layout. ── */
439
+ hiscore_save(hiscore);
440
+ }
441
+ state = ST_OVER;
442
+ paint_over();
443
+ }
444
+
445
+ /* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
446
+ static void kill_player(void) {
447
+ uint8_t other;
448
+ sound_play_noise(12, 12, 14);
449
+ if (p_lives[cur_player] > 0) --p_lives[cur_player];
450
+ if (two_player) {
451
+ other = cur_player ^ 1;
452
+ if (p_lives[other] > 0) cur_player = other; /* swap turns */
453
+ else if (p_lives[cur_player] == 0) { game_over(); return; }
454
+ } else if (p_lives[0] == 0) {
455
+ game_over();
456
+ return;
457
+ }
458
+ begin_turn();
459
+ }
460
+
461
+ /* ── GAME LOGIC (clay) — landing probe against the column map ──────────────
462
+ * One-way platforms, classic NES style: only catch the player while FALLING
463
+ * through a narrow window at the surface. The window is 6 px tall —
464
+ * top-1 (the standing snap parks feet at top, and gravity's sub-pixel
465
+ * trickle doesn't move the integer Y every frame; without the -1 slack the
466
+ * player "stands" with on_ground=0 most frames, so jumps only register on
467
+ * lucky frames and the idle/jump sprite flickers) through top+4 (so a
468
+ * 5 px/frame terminal-velocity fall can't step over it). */
469
+ static uint8_t land_top(uint8_t c, uint8_t feet) {
470
+ uint8_t r, top;
471
+ r = plat_row[c];
472
+ if (r) {
473
+ top = r << 3;
474
+ if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
475
+ }
476
+ r = ground_row[c];
477
+ if (r != NO_GROUND) {
478
+ top = r << 3;
479
+ if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4)) return top;
480
+ }
481
+ return 0;
482
+ }
483
+
484
+ void main(void) {
485
+ uint8_t i, pad, delta, y8, feet, c0, c1, top, killed;
486
+ uint8_t player_y;
487
+
488
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
489
+ * Init order: PPU off → CHR upload → palette → nametable (raw writes) →
490
+ * OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
491
+ * off (raw $2007 traffic during rendering corrupts the address latch
492
+ * mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
493
+ * PPUMASK bits — don't poke those registers directly alongside it. */
494
+ ppu_off();
495
+ chr_ram_upload(0x0000, tile_blank, 16);
496
+ chr_ram_upload(0x0010, tile_player_idle, 16);
497
+ chr_ram_upload(0x0020, tile_player_jump, 16);
498
+ chr_ram_upload(0x0030, tile_coin, 16);
499
+ chr_ram_upload(0x0040, tile_spike, 16);
500
+ chr_ram_upload(0x0050, tile_mark, 16);
501
+ chr_ram_upload(0x1010, bg_tile_cloud, 16);
502
+ chr_ram_upload(0x1020, bg_tile_dirt, 16);
503
+ chr_ram_upload(0x1030, bg_tile_grass, 16);
504
+ chr_ram_upload(0x1040, bg_tile_hudbar, 16);
505
+ font_upload();
506
+ palette_load(palette);
168
507
  sound_init();
169
508
 
509
+ hiscore = hiscore_load(); /* battery SRAM — 0 on first boot */
510
+ state = ST_TITLE;
511
+ paint_title();
512
+
170
513
  for (;;) {
171
- player_y = (uint8_t)(py_q44 >> 4);
514
+ if (state == ST_TITLE) {
515
+ /* ── GAME LOGIC (clay) — title: A = 1P, B = 2P alternating turns ── */
516
+ oam_clear();
517
+ ppu_wait_nmi();
518
+ sound_music_tick();
519
+ pad = pad_poll(0);
520
+ if ((pad & PAD_A) && !(prev_pad & PAD_A)) start_game(0);
521
+ else if ((pad & PAD_B) && !(prev_pad & PAD_B)) start_game(1);
522
+ else if ((pad & PAD_START) && !(prev_pad & PAD_START)) start_game(0);
523
+ prev_pad = pad;
524
+ continue;
525
+ }
172
526
 
173
- /* ── Stage sprites BEFORE ppu_wait_nmi() ────────────────────
174
- * The NMI handler DMAs shadow OAM at vblank-start, so oam_clear +
175
- * oam_spr MUST run before the wait below — flip the order and the
176
- * visible frame lags / shows stale sprites. */
177
- oam_clear();
178
- /* player tile depends on whether airborne */
179
- oam_spr(px, player_y,
180
- on_ground ? TILE_PLAYER_IDLE : TILE_PLAYER_JUMP,
181
- PLAYER_PAL);
182
- /* draw each platform tile (one OAM entry per 8 px column) */
183
- for (i = 0; i < NUM_PLATFORMS; i++) {
184
- uint8_t k;
185
- for (k = 0; k < platforms[i].w; k++) {
186
- oam_spr(platforms[i].x + (k << 3), platforms[i].y, TILE_PLATFORM, PLATFORM_PAL);
527
+ if (state == ST_OVER) {
528
+ /* Results screen (scroll 0, no split needed); START or A → title. */
529
+ oam_clear();
530
+ ppu_wait_nmi();
531
+ sound_music_tick();
532
+ pad = pad_poll(0);
533
+ if ((pad & (PAD_START | PAD_A)) && !(prev_pad & (PAD_START | PAD_A))) {
534
+ state = ST_TITLE;
535
+ paint_title();
187
536
  }
537
+ prev_pad = pad;
538
+ continue;
188
539
  }
189
540
 
541
+ /* ── ST_PLAY ─────────────────────────────────────────────────────── */
542
+
543
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
544
+ * Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
545
+ * real OAM at the START of vblank, copying whatever shadow OAM holds AT
546
+ * THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites.
547
+ * Sprite 0 (the split marker) must be staged FIRST — OAM order is
548
+ * oam_spr call order, and the split idiom needs it at index 0. */
549
+ player_y = (uint8_t)(py_q44 >> 4);
550
+ oam_clear();
551
+ stage_sprite0();
552
+ /* Blink the player during the turn-change breather. */
553
+ if (turn_pause == 0 || (turn_pause & 4))
554
+ oam_spr(px, player_y,
555
+ on_ground ? TILE_PLAYER_IDLE : TILE_PLAYER_JUMP, PLAYER_PAL);
556
+ for (i = 0; i < NUM_COINS; i++)
557
+ oam_spr(coin_x[i], coin_y[i], TILE_COIN, COIN_PAL);
558
+ for (i = 0; i < NUM_SPIKES; i++)
559
+ if (spike_active[i]) oam_spr(spike_x[i], SPIKE_Y, TILE_SPIKE, SPIKE_PAL);
560
+
190
561
  ppu_wait_nmi();
562
+ split_after_hud(); /* the sprite-0 split — every frame */
563
+ sound_music_tick();
191
564
 
192
- /* ── Input ──────────────────────────────────────────────── */
193
- pad = pad_poll(0);
194
- if ((pad & PAD_LEFT) && px > 8) px -= MOVE_SPEED;
195
- if ((pad & PAD_RIGHT) && px < 240) px += MOVE_SPEED;
196
- /* Jump on rising-edge of A while on ground. */
565
+ if (turn_pause) { /* freeze gameplay, keep the frame honest */
566
+ --turn_pause;
567
+ continue;
568
+ }
569
+
570
+ /* ── GAME LOGIC (clay) from here down ──────────────────────────────
571
+ * Input — the CURRENT player's controller (alternating turns: P2 is
572
+ * on controller 2). Past SCROLL_WALL the world scrolls instead of the
573
+ * player (the camera never scrolls back — the classic one-way camera). */
574
+ pad = pad_poll(cur_player);
575
+ delta = 0;
576
+ if (pad & PAD_RIGHT) {
577
+ if (px < SCROLL_WALL) px += MOVE_SPEED;
578
+ else { scroll_x += MOVE_SPEED; delta = MOVE_SPEED; }
579
+ }
580
+ if ((pad & PAD_LEFT) && px > 8) px -= MOVE_SPEED;
197
581
  if ((pad & PAD_A) && !(prev_pad & PAD_A) && on_ground) {
198
582
  vy_q44 = JUMP_VEL_Q44;
199
583
  on_ground = 0;
200
- sound_play_tone(0, 0x150, 6, 6);
584
+ sound_play_tone(0, 0x150, 6, 6); /* jump whoop */
201
585
  }
202
586
  prev_pad = pad;
203
587
 
204
- /* ── Physics ────────────────────────────────────────────── */
205
- /* gravity */
206
- if (vy_q44 < 80) vy_q44 += GRAVITY_Q44; /* terminal velocity */
588
+ /* World objects drift left as the level scrolls (world-anchored). */
589
+ if (delta) {
590
+ dist_sub += delta;
591
+ if (dist_sub >= 64) { /* distance pay */
592
+ dist_sub -= 64;
593
+ ++p_score[cur_player];
594
+ draw_hud();
595
+ }
596
+ for (i = 0; i < NUM_COINS; i++) {
597
+ if (coin_x[i] < 16 + delta) respawn_coin(i);
598
+ else coin_x[i] -= delta;
599
+ }
600
+ for (i = 0; i < NUM_SPIKES; i++) {
601
+ if (!spike_active[i]) continue;
602
+ if (spike_x[i] < 16 + delta) spike_active[i] = 0;
603
+ else spike_x[i] -= delta;
604
+ }
605
+ }
606
+ for (i = 0; i < NUM_SPIKES; i++)
607
+ if (!spike_active[i]) try_spawn_spike(i);
608
+
609
+ /* Physics: gravity + sub-pixel Y. */
610
+ if (vy_q44 < MAX_VY_Q44) vy_q44 += GRAVITY_Q44;
207
611
  py_q44 += vy_q44;
612
+ y8 = (uint8_t)(py_q44 >> 4);
208
613
 
209
- /* ── Ground / platform collision (descending only) ──────── */
210
- on_ground = 0;
211
- if (vy_q44 >= 0) { /* falling */
212
- for (i = 0; i < NUM_PLATFORMS; i++) {
213
- if (landed_on(i, (uint8_t)(py_q44 >> 4))) {
214
- py_q44 = (platforms[i].y - 8) << 4;
215
- vy_q44 = 0;
216
- on_ground = 1;
217
- break;
218
- }
614
+ /* Fell into a pit (below the screen) lose the turn. */
615
+ if (y8 >= 232) {
616
+ kill_player();
617
+ continue;
618
+ }
619
+
620
+ /* Landing — probe the two level columns under the player's feet. */
621
+ if (vy_q44 >= 0) {
622
+ feet = y8 + 8;
623
+ c0 = (uint8_t)(px + scroll_x) >> 3;
624
+ c1 = (uint8_t)(px + scroll_x + 7) >> 3;
625
+ top = land_top(c0, feet);
626
+ if (top == 0) top = land_top(c1, feet);
627
+ if (top) {
628
+ py_q44 = (uint16_t)(top - 8) << 4;
629
+ vy_q44 = 0;
630
+ if (!on_ground) sound_play_tone(1, 0x2A0, 3, 2); /* landing thud */
631
+ on_ground = 1;
632
+ } else {
633
+ on_ground = 0; /* walked off */
219
634
  }
220
635
  }
221
636
 
222
- /* ── Off-bottom respawn at top-left ───────────────────── */
223
- if ((py_q44 >> 4) >= 232) {
224
- px = 24;
225
- py_q44 = 100 << 4;
226
- vy_q44 = 0;
227
- sound_play_noise(8, 8, 8); /* respawn buzz */
637
+ /* Coins (collect) + spikes (death). */
638
+ for (i = 0; i < NUM_COINS; i++) {
639
+ if (dist8(coin_x[i], px) < 8 && dist8(coin_y[i], y8) < 8) {
640
+ p_score[cur_player] += 10;
641
+ sound_play_tone(0, 0x0D6, 8, 5); /* coin ping */
642
+ draw_hud();
643
+ respawn_coin(i);
644
+ }
645
+ }
646
+ killed = 0;
647
+ for (i = 0; i < NUM_SPIKES; i++) {
648
+ if (!spike_active[i]) continue;
649
+ if (dist8(spike_x[i], px) < 7 && dist8(SPIKE_Y, y8) < 7) {
650
+ killed = 1;
651
+ break;
652
+ }
228
653
  }
654
+ if (killed) kill_player();
229
655
  }
230
656
  }