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
@@ -68,7 +68,16 @@ const TEMPLATES = {
68
68
  linkerConfig: { presetSrc: "presets/nes/chr-ram-runtime.cfg", dst: "chr-ram-runtime.cfg" },
69
69
  lang: "C (cc65)",
70
70
  ext: ".nes",
71
- describe: "Vertical-scrolling shooter. Player + 4 bullets + 4 enemies, AABB collision, score counter, wave spawner.",
71
+ describe: "NOVA SENTRY — complete vertical shooter: title shell (1P/2P co-op select), shared-lives co-op, bullet/enemy pools, wave spawner, score + battery hi-score, music + SFX, sprite-0-hit split (fixed HUD over a drifting starfield).",
72
+ players: "1-2 (simultaneous co-op)",
73
+ sram: "battery hi-score at $6000 (hiscore_load/save; iNES battery bit in the crt0)",
74
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "shared-lives co-op", "title/play/game-over state machine"],
75
+ techniques: [
76
+ "sprite-0-hit split scroll (fixed HUD over scrolling field)",
77
+ "vblank-budget VRAM queue (asm drain in the crt0 NMI)",
78
+ "battery SRAM hi-score (magic + checksum)",
79
+ "CHR-RAM tile upload + 1bpp font",
80
+ ],
72
81
  },
73
82
  platformer: {
74
83
  main: "templates/platformer.c",
@@ -80,7 +89,17 @@ const TEMPLATES = {
80
89
  linkerConfig: { presetSrc: "presets/nes/chr-ram-runtime.cfg", dst: "chr-ram-runtime.cfg" },
81
90
  lang: "C (cc65)",
82
91
  ext: ".nes",
83
- describe: "Single-screen platformeralso the starting point for a SIDE-SCROLLER (same genre here). Gravity + jump physics (fixed-point Y), 5 platforms, land-on-top collision, respawn on fall. This is the jump/gravity/collision core; it does NOT scroll as shipped. To make it scroll on NES you add a camera + world coords and write new nametable columns across the mirroring boundary as the camera advances (and usually a sprite-0/IRQ split for a fixed HUD) see the NES MENTAL_MODEL.md scrolling section.",
92
+ describe: "LEDGE LEAPERside-scrolling platformer: gravity + Q4.4 sub-pixel jump physics, one-way platforms, pits and spikes, coins + distance scoring, battery hi-score. 2P is classic alternating turns (P2 on controller 2) with per-player score and lives. Sprite-0-hit split: fixed HUD over a seamlessly looping scrolling level.",
93
+ players: "1-2 (alternating turns; P2 on controller 2)",
94
+ sram: "battery hi-score (hiscore_load/save)",
95
+ mechanics: ["gravity-jump physics (Q4.4 fixed point)", "one-way platform collision via column map", "horizontal scrolling with camera wall", "pits + spike hazards", "coin pickup + distance scoring", "alternating 2P turns with per-player lives"],
96
+ techniques: [
97
+ "sprite-0-hit split scroll (two-phase PPUSTATUS poll)",
98
+ "dual-nametable seamless 256px level loop",
99
+ "world-anchored sprite objects",
100
+ "queued VRAM HUD updates",
101
+ "battery SRAM hi-score",
102
+ ],
84
103
  },
85
104
  puzzle: {
86
105
  main: "templates/puzzle.c",
@@ -92,7 +111,17 @@ const TEMPLATES = {
92
111
  linkerConfig: { presetSrc: "presets/nes/chr-ram-runtime.cfg", dst: "chr-ram-runtime.cfg" },
93
112
  lang: "C (cc65)",
94
113
  ext: ".nes",
95
- describe: "Match-3 falling-block puzzle. 6×12 grid, 1×3 active piece (3 colors), rotate via A, soft-drop on DOWN, 3+-in-a-row clears in all 4 directions (H/V/diagonals) with gravity + cascade chains.",
114
+ describe: "GEM DUEL — falling-gem match-3: 1P marathon with levels and cascade chains; 2P simultaneous split-board versus where chains send garbage rows to the opponent. Battery hi-score.",
115
+ players: "1-2 (2P = simultaneous versus, split boards)",
116
+ sram: "battery hi-score (hiscore_load/save)",
117
+ mechanics: ["falling-piece control", "match-3 in 4 directions", "cascade chains with multipliers", "garbage attack rows", "soft drop + levels", "split-board versus"],
118
+ techniques: [
119
+ "vblank-budgeted board repaint (dirty-row bitmask, 1 row/frame)",
120
+ "attribute-table palette regions (2-aligned wells)",
121
+ "absolute-RAM arrays in the $0500 user scratch page",
122
+ "battery SRAM hi-score",
123
+ "stage-then-wait OAM order",
124
+ ],
96
125
  },
97
126
  sports: {
98
127
  main: "templates/sports.c",
@@ -104,7 +133,17 @@ const TEMPLATES = {
104
133
  linkerConfig: { presetSrc: "presets/nes/chr-ram-runtime.cfg", dst: "chr-ram-runtime.cfg" },
105
134
  lang: "C (cc65)",
106
135
  ext: ".nes",
107
- describe: "Two-player Pong. Port 0 = left paddle, port 1 = right paddle (AI fallback when no 2nd controller). Per-side score 0-9, ball bounces off paddles + walls. Designed for the playtest window with two USB controllers.",
136
+ describe: "COURT CLASH head-to-head court game: 1P vs a beatable CPU or 2P simultaneous versus, first to 5, battery-backed best CPU win streak.",
137
+ players: "1-2 (1P vs CPU / 2P simultaneous versus)",
138
+ sram: "longest 1P win streak vs the CPU (hiscore_load/save)",
139
+ mechanics: ["versus match flow (first-to-5, result screen)", "CPU opponent (speed-capped ball chase)", "2P simultaneous input (both ports)", "edge-hit ball deflection with random spin", "serve pause + alternating serve angle"],
140
+ techniques: [
141
+ "queued HUD text (text_draw_u16) during rendering",
142
+ "PPU-off court/title paint (vram_unsafe_set/text_draw_unsafe)",
143
+ "stage-then-wait OAM order with deterministic sprite slots",
144
+ "xorshift16 PRNG to break deterministic-rally limit cycles",
145
+ "battery PRG-RAM record via hiscore_save",
146
+ ],
108
147
  },
109
148
  racing: {
110
149
  main: "templates/racing.c",
@@ -116,7 +155,18 @@ const TEMPLATES = {
116
155
  linkerConfig: { presetSrc: "presets/nes/chr-ram-runtime.cfg", dst: "chr-ram-runtime.cfg" },
117
156
  lang: "C (cc65)",
118
157
  ext: ".nes",
119
- describe: "Endless top-down lane racer. 3 lanes, 4 obstacle slots, LEFT/RIGHT switches lanes. Speed grows with score; collision triggers a 60-frame freeze then reset.",
158
+ describe: "THROTTLE FEUD — top-down vertically-scrolling road racer: scroll_y BG scroll with the wrap-at-240 idiom, streamed roadside scenery via queued tile writes, sprite-digit HUD. 1P: 4 lanes, A/B speed, best distance to battery SRAM. 2P: simultaneous split-lane versus (solid divider, first to 3 crashes loses).",
159
+ players: "1-2 (2P = simultaneous versus, split lanes)",
160
+ sram: "best 1P distance (uint16, 1 unit = 16 scrolled px; hiscore_load/save)",
161
+ mechanics: ["lane steering", "speed control (1P)", "traffic dodging", "crash lives + invulnerability blink", "distance checkpoints", "split-lane versus"],
162
+ techniques: [
163
+ "vertical BG scroll with 240-wrap",
164
+ "streaming-row scenery via queued tile writes",
165
+ "sprite-based HUD (8-per-scanline budgeting)",
166
+ "battery SRAM hi-score",
167
+ "PPU-off full repaint screens",
168
+ "xorshift16 PRNG",
169
+ ],
120
170
  },
121
171
  /* R44 (2026-05-26): bundled-driver music demo. FamiTone2 engine +
122
172
  * cc65 bridge + example track, all ship as source under lib/asm. */
@@ -188,7 +238,18 @@ const TEMPLATES = {
188
238
  ],
189
239
  lang: "C (SDCC sm83)",
190
240
  ext: ".gb",
191
- describe: "Vertical-shmup scaffold for GB. Player ship + 4 bullets + 4 enemies, wave spawner, AABB collision, score (in WRAM). OAM slots 0/1-4/5-8 preallocated.",
241
+ describe: "METEOR MILITIA complete GB vertical shooter: press-start title shell with battery-persistent hi-score (MBC1+RAM+BATTERY declared in the crt0 header, $0A enable sequence, magic+checksum record, survives power cycles), and the GB signature — a WINDOW-layer fixed HUD (WX=7/WY=128, LCDC bit 5) over an SCY-scrolling starfield, no raster tricks. Wave spawner, AABB collisions, APU tune + SFX, divide-free painters (the sm83 has no divider). 1P by design: link-cable multiplayer can't be emulated single-instance (stated honestly in-file).",
242
+ players: "1 (one controller; link cable unemulatable single-instance)",
243
+ sram: "MBC1 cart RAM via the save_ram region (8KB) — crt0-declared battery cart, checksummed record, verified across hardReset",
244
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "lives + respawn knockback", "battery-persistent hi-score", "title/play/game-over state machine"],
245
+ techniques: [
246
+ "window-layer fixed HUD (WX+7 quirk, bottom-strip placement)",
247
+ "MBC1 $0A RAM-enable sequence",
248
+ "shadow OAM + HRAM OAM-DMA stub",
249
+ "one-item-per-vblank VRAM commit queue",
250
+ "HALT-driven vblank wait",
251
+ "divide-free pattern + decimal math",
252
+ ],
192
253
  },
193
254
  platformer: {
194
255
  main: "templates/platformer.c",
@@ -201,7 +262,18 @@ const TEMPLATES = {
201
262
  ],
202
263
  lang: "C (SDCC sm83)",
203
264
  ext: ".gb",
204
- describe: "SIDE-SCROLLING platformer for GB. Subpixel gravity + jump + land-on-top collision against a static platform list spread across a 256-px world (the full wrapping BG map). The camera follows the player and scrolls the BG via SCX each frame; the player sprite draws in screen space (worldX - camX). A=jump, d-pad=move. The world here is one BG map wide (no streaming) for a wider world, stream a new tile column into the 32-wide BG map each time the camera crosses an 8px boundary (window for a fixed HUD). See the GB MENTAL_MODEL.md 'Horizontal scrolling'. Extend with enemies, goals, pickups.",
265
+ describe: "GULLY GALLOP complete GB side-scrolling platformer: press-start title shell with battery-persistent hi-score (MBC1+RAM+BATTERY crt0 header, $0A enable sequence, magic+checksum record, survives power cycles), and the GB signature WINDOW-layer fixed HUD (WX=7/WY=128) over an SCX-scrolled, seamlessly looping 256-px column-map level. Gravity + Q4.4 sub-pixel jump physics, one-way platforms, lethal pits, drifting spikes, coins + distance scoring, one-way runner camera, APU tune + SFX, divide-free painters. 1P by design: link-cable multiplayer can't be emulated single-instance (stated honestly in-file).",
266
+ players: "1 (one controller; link cable unemulatable single-instance)",
267
+ sram: "MBC1 cart RAM via the save_ram region (8KB) — crt0-declared battery cart, checksummed record, verified across hardReset",
268
+ mechanics: ["gravity + Q4.4 jump physics", "one-way platforms (6-px landing window)", "pits + spikes + coins", "distance + coin scoring", "one-way scroll-wall camera", "lives + respawn breather", "battery-persistent hi-score"],
269
+ techniques: [
270
+ "window-layer fixed HUD (WX+7 quirk, bottom strip)",
271
+ "seamless uint8 SCX wrap over a 32-column level map",
272
+ "MBC1 $0A RAM-enable sequence",
273
+ "shadow OAM + HRAM OAM-DMA stub",
274
+ "one-item-per-vblank VRAM commit queue (wrap-aware text)",
275
+ "divide-free pattern + decimal math",
276
+ ],
205
277
  },
206
278
  puzzle: {
207
279
  main: "templates/puzzle.c",
@@ -214,7 +286,17 @@ const TEMPLATES = {
214
286
  ],
215
287
  lang: "C (SDCC sm83)",
216
288
  ext: ".gb",
217
- describe: "Match-3 falling-block puzzle scaffold for GB. 6×12 grid rendered via BG tilemap, 1×3 active piece (3 colours via 3 BG tile shapes), rotate via A, hard-drop on START, 3+-in-a-row clears in all 4 directions (H/V/diagonals) with gravity + cascade chains.",
289
+ describe: "SHALE WELL — falling-stone match-3 to the full contract (the monochrome DMG take on a jewel matcher): an 8x15 well, five stone KINDS told apart by 2bpp TILE SHAPE through one DMG BGP palette (stripe/checker/ring/brick/diamond the honest DMG answer to the GBC's six colors), move/cycle/soft-drop/hard-drop, 3+ clears in all 4 directions, gravity cascades chain for bonus, magic stone every 18th piece, levels speed up. 1P marathon (link-cable 2P unemulatable single-instance — honest in-file). Locked well rides the vblank COLLECT/FLUSH queue with an idle scrub; window-layer HUD; battery hi-score (MBC1+RAM+BATTERY, verified across power cycles); APU melody + SFX. Board arrays pinned via __at($C200) so it builds with the default recipe.",
290
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
291
+ sram: "MBC1+RAM+BATTERY, 8KB at $A000 ($0A-gated, magic+checksum), verified across hardReset",
292
+ mechanics: ["falling-trio control", "match-3 in 4 directions", "gravity + cascade chains", "magic-stone target clear", "levels", "battery hi-score"],
293
+ techniques: [
294
+ "DMG tile-shape stone kinds (one BGP palette, no CGB regs)",
295
+ "vblank COLLECT/FLUSH queue + idle scrub",
296
+ "window-layer HUD",
297
+ "__at() WRAM pinning past the shadow-OAM page (default-recipe build)",
298
+ "battery SRAM save ($0A enable dance)",
299
+ ],
218
300
  },
219
301
  sports: {
220
302
  main: "templates/sports.c",
@@ -227,7 +309,18 @@ const TEMPLATES = {
227
309
  ],
228
310
  lang: "C (SDCC sm83)",
229
311
  ext: ".gb",
230
- describe: "Player-vs-AI Pong. Game Boy hardware has only one controller port, so this is human vs chase-the-ball AI by design. Same gameplay shape as the 2P versions on platforms with two ports.",
312
+ describe: "CAROM COAST — head-to-head court game to the full contract (the monochrome DMG take on a versus paddle game): press-start title, 1P vs a beatable chase-AI CPU, first-to-5 match flow into a result screen, GB APU ch1 melody + ch2 SFX. The ball 'caroms' — rail ricochets + edge-deflection where it strikes your paddle; a +/-1 PRNG spin guarantees an idle rally ENDS (no infinite limit cycle). Paddles told apart by SHADE on the 4-grey DMG (you black OBP0, CPU lighter OBP1). Window-layer fixed HUD; score/record/result-text ride the vblank COMMIT queue (<=5 cells/frame a full line dropped in one batch). Longest 1P win streak persists to battery SRAM (MBC1+RAM+BATTERY, magic+checksum, verified across power cycles). 1P by design — link-cable 2P unemulatable single-instance (honest in-file).",
313
+ players: "1 (1P vs a beatable CPU — no link-cable 2P single-instance)",
314
+ sram: "longest 1P win streak vs the CPU (MBC1+RAM+BATTERY, magic+checksum, verified across hardReset)",
315
+ mechanics: ["versus match flow (first-to-5, result screen)", "beatable chase-AI CPU (speed-capped ball chase)", "edge-hit deflection + rail caroms with PRNG spin", "serve pause + alternating serve angle", "win-streak record that dies on a loss"],
316
+ techniques: [
317
+ "window-layer fixed HUD",
318
+ "vblank COMMIT queue for score/record/result text (<=5 cells/frame)",
319
+ "shadow-OAM + HRAM OAM-DMA stub, paddles by OBP0/OBP1 shade",
320
+ "xorshift16 PRNG to break deterministic-rally limit cycles",
321
+ "battery SRAM record via the $0A MBC1 RAM-gate dance",
322
+ "LCD-off court/title paint",
323
+ ],
231
324
  },
232
325
  racing: {
233
326
  main: "templates/racing.c",
@@ -240,7 +333,17 @@ const TEMPLATES = {
240
333
  ],
241
334
  lang: "C (SDCC sm83)",
242
335
  ext: ".gb",
243
- describe: "Endless 3-lane top-down racer. LEFT/RIGHT switches lanes, obstacle speed grows with score, 60-frame freeze + auto-reset on collision.",
336
+ describe: "TARMAC TILT top-down vertical road racer to the full contract: press-start title (honest no-2P — link cable unemulatable single-instance), the road scrolls via SCY into a 256-px map (seamless uint8 wrap, no helper — contrast taught vs NES 240 / SMS 224 garbage-row / Genesis hardware-masked plane), four lanes, A/UP accelerate + B/DOWN brake (speed 1-4), LEFT/RIGHT lane tilt, overtaking traffic pool, 3-crash lives with invuln blink, best DISTANCE to battery SRAM (magic+checksum, verified across power cycles), window-layer HUD, GB APU music + SFX, divide-free digit math.",
337
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
338
+ sram: "MBC1 cart RAM via the save_ram region (8KB) — crt0-declared battery cart, best-distance magic+checksum record, verified across hardReset",
339
+ mechanics: ["lane steering", "speed control 1-4", "overtaking traffic pool", "crash lives + invuln blink", "best-distance persistence"],
340
+ techniques: [
341
+ "SCY vertical road scroll (256-px seamless uint8 wrap)",
342
+ "window-layer fixed HUD",
343
+ "one-item-per-vblank VRAM commit queue",
344
+ "battery SRAM best-distance ($0A enable dance)",
345
+ "divide-free digit math",
346
+ ],
244
347
  },
245
348
  /* R45 — hUGEDriver music demo. Ships a compact SDCC-native music
246
349
  * driver with the upstream hUGEDriver function surface plus a
@@ -282,11 +385,81 @@ const TEMPLATES = {
282
385
  sprite_move: mk("sprite_move", "Joypad-controlled 16x16 sprite over a tiled background. d-pad moves the sprite; verified visible + responsive. Build up an action game from here."),
283
386
  music_sfx: mk("music_sfx", "HuC6280 PSG demo: a looping melody plus a button-fired SFX. Shows psg_tone/psg_off across the PSG's wavetable channels."),
284
387
  catch_game: mk("catch_game", "A complete tiny game: a paddle catches a falling object with the d-pad; full game loop with waitvsync(), two sprites, collision, scoring."),
285
- shmup: mk("shmup", "Vertical shoot-'em-up for PC Engine. Player ship + bullet/enemy object pools, a wave spawner, AABB collisions, score HUD, scrolling-band starfield BG. d-pad flies, button I fires. The base for any action shooter."),
286
- platformer: mk("platformer", "Side-scrolling platformer for PC Engine. Gravity + jump + land-on-top platform collision, a multi-screen world streamed via BG X-scroll (BXR), solid platform tiles, sub-pixel physics. d-pad moves, button I jumps."),
287
- puzzle: mk("puzzle", "Match-3 / falling-block puzzle for PC Engine. A 6x12 well drawn with BG tiles, a 1x3 active piece you move/rotate/soft-drop/hard-drop, 3+-in-a-row clears (H+V) with gravity + cascade chains, score. d-pad moves, I rotates, II hard-drops."),
288
- sports: mk("sports", "Pong-style sports game for PC Engine. Two paddles + a bouncing ball on a netted court, score to 9, paddle-deflect physics; player 2 falls back to chase-AI when no input. d-pad moves P1."),
289
- racing: mk("racing", "Top-down lane racer for PC Engine. Player car at the bottom, obstacle cars spawn from the top and slide down, LEFT/RIGHT switches lanes, speed grows with score, crash freeze + auto-reset. Scrolling road BG."),
388
+ shmup: {
389
+ ...mk("shmup", "ZENITH BARRAGE complete PCE vertical shooter: title shell with in-session hi-score (a bare HuCard can't save — BRAM is peripheral-only; the bank-$F7 TAM/$1807-unlock dance is documented in-file as the real-hardware path), and the PCE signature — a 64x32 boss built from exactly TWO 32x32 SATB entries moving as one unit. Wave spawner, AABB collisions, 3-song PSG music + SFX, banded twinkling starfield. 1P by design: geargrafx ships TurboTap disabled, so port-2 input cannot reach the game (stated honestly in-file)."),
390
+ players: "1 (stock PCE has one pad port; TurboTap exists in-core but disabled future host core-option round)",
391
+ sram: "none a bare HuCard cannot save; BRAM (bank $F7) is PERIPHERAL-ONLY on real hardware (CD-ROM² unit / Tennokoe Bank / Memory Base 128). In-session hi-score only, like the 2600/Lynx; the BRAM mapping + write-lock are documented in-file as the real-hardware path.",
392
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "multi-sprite boss with HP/phases", "lives + mercy invulnerability", "in-session hi-score (HuCards can't save)", "title/play/game-over state machine"],
393
+ techniques: [
394
+ "HuC6270 large sprites (32x32 CGX/CGY, 4-aligned patterns)",
395
+ "two-entry composite boss",
396
+ "shadow SATB + R19 vblank DMA",
397
+ "TAM bank-mapping thunks from C",
398
+ "BRAM $1807 write-unlock",
399
+ "BAT glyph font + partial HUD repaint",
400
+ "PSG divider-table music",
401
+ ],
402
+ },
403
+ platformer: {
404
+ ...mk("platformer", "GLADE DASH — complete PC Engine side-scrolling platformer: title/1P/2P-alternating-turns shell, gravity + Q4.4 sub-pixel jump physics, one-way slabs, lethal pits + spikes, coins + distance scoring, in-session hi-score (a bare HuCard can't save — BRAM is peripheral-only; the bank-$F7 TAM/$1807-unlock dance is documented in-file as the real-hardware path), 3-song PSG music + SFX. The PCE signature on top: hardware BG scroll via the BXR register with column-streaming for a 768px looping world, plus a 32x32 large multi-cell hero (one SATB entry, 4-aligned pattern) with a walk cycle. Real 2P alternating turns — the host enables the TurboTap so port-1 input reaches player 2 (verified). HONEST CAVEAT: no hardware window/raster split in the minimal lib, so the HUD is a painted band that scrolls with the world but reads continuously (a raster-IRQ BXR reset can make it truly fixed — TROUBLESHOOTING note)."),
405
+ players: "1-2 (2P alternating turns; P2 via TurboTap port 1, host-enabled — verified port-1 reaches P2)",
406
+ sram: "none — a bare HuCard cannot save; BRAM (bank $F7) is PERIPHERAL-ONLY on real hardware (CD-ROM² unit / Tennokoe Bank / Memory Base 128). In-session hi-score only, like the 2600/Lynx; the BRAM mapping + write-lock are documented in-file as the real-hardware path.",
407
+ mechanics: ["gravity + sub-pixel jump physics", "one-way platforms", "pits + spikes", "coins + distance scoring", "one-way scroll-wall camera", "alternating 2P turns (per-player score/lives)", "in-session hi-score (HuCards can't save)"],
408
+ techniques: [
409
+ "hardware BG scroll (VDC BXR) + column streaming",
410
+ "32x32 large hero sprite (CGX/CGY, 4-aligned pattern)",
411
+ "2-frame walk-cycle VRAM swap",
412
+ "shadow SATB + R19 vblank DMA",
413
+ "TAM bank-mapping thunks from C",
414
+ "BRAM $1807 write-unlock",
415
+ "TurboTap 2P via joy_read(JOY_2)",
416
+ ],
417
+ },
418
+ puzzle: {
419
+ ...mk("puzzle", "TUMBLE TIDE — complete PC Engine falling-trio versus puzzle: title/1P-marathon/2P-simultaneous-versus shell, falling-trio match-3 (4-direction clears, gravity, cascade chains, levels), in-session hi-score (a bare HuCard can't save — BRAM is peripheral-only; the bank-$F7 TAM/$1807-unlock dance is documented in-file as the real-hardware path), PSG music + SFX. The board is the VDC BAT tilemap with whole-board repaints — the inverse of the NES vblank-queue famine (taught in-file). Real 2P simultaneous versus with garbage attacks: a cascade chain floods garbage rows into your rival's well; P2 on the TurboTap (host-enabled port 1, verified). 6x12 wells, split board in versus."),
420
+ players: "1-2 (2P simultaneous versus; P2 via TurboTap port 1, host-enabled — verified port-1 reaches P2)",
421
+ sram: "none — a bare HuCard cannot save; BRAM (bank $F7) is PERIPHERAL-ONLY on real hardware (CD-ROM² unit / Tennokoe Bank / Memory Base 128). In-session hi-score only, like the 2600/Lynx; the BRAM mapping + write-lock are documented in-file as the real-hardware path.",
422
+ mechanics: ["falling-trio match-3", "4-direction line clears", "gravity + cascade chains (multiplied score)", "levels (1P speed-up)", "2P simultaneous versus split board", "garbage-row attacks", "in-session hi-score (HuCards can't save)"],
423
+ techniques: [
424
+ "whole-board VDC BAT repaint (vs NES vblank-queue famine)",
425
+ "BAT glyph font + HUD",
426
+ "shadow SATB + R19 vblank DMA (3 trio sprites/player)",
427
+ "per-colour BG sub-palettes for one cell-tile shape",
428
+ "TAM bank-mapping thunks from C",
429
+ "BRAM $1807 write-unlock",
430
+ "TurboTap 2P via joy_read(JOY_2)",
431
+ ],
432
+ },
433
+ sports: {
434
+ ...mk("sports", "SPIKE SURGE — complete PC Engine versus court game (Pong lineage): title/1P-vs-CPU/2P-simultaneous-versus shell, first-to-5 match flow with a result screen, beatable chase-AI CPU, PRNG rally spin so idle matches provably END, in-session best-win-streak record (a bare HuCard can't save — BRAM is peripheral-only; documented in-file), 3-song PSG music + SFX. Real 2P simultaneous versus: P2 on the TurboTap (host-enabled port 1, verified). Court is the VDC BAT tilemap; paddles + ball are SATB sprites."),
435
+ players: "1-2 (1P vs beatable CPU, or 2P simultaneous versus; P2 via TurboTap port 1, host-enabled — verified port-1 reaches P2)",
436
+ sram: "none — a bare HuCard cannot save; BRAM (bank $F7) is PERIPHERAL-ONLY on real hardware (CD-ROM² unit / Tennokoe Bank / Memory Base 128). In-session best-win-streak only, like the 2600/Lynx; the BRAM mapping + write-lock are documented in-file as the real-hardware path.",
437
+ mechanics: ["paddle/ball court physics", "edge-deflection parry angle", "1P beatable chase-AI CPU", "2P simultaneous versus", "first-to-5 match + result screen", "PRNG rally spin (idle matches end)", "in-session win-streak record (HuCards can't save)"],
438
+ techniques: [
439
+ "whole-screen VDC BAT paint (court)",
440
+ "BAT glyph font + HUD band",
441
+ "shadow SATB + R19 vblank DMA (7 sprites)",
442
+ "TAM bank-mapping thunks from C",
443
+ "BRAM $1807 write-unlock",
444
+ "PSG divider-table 2-channel music",
445
+ "TurboTap 2P via joy_read(JOY_2)",
446
+ ],
447
+ },
448
+ racing: {
449
+ ...mk("racing", "PINION PURSUIT — complete PC Engine top-down road racer: title/1P-race/2P-simultaneous-split-lane-versus shell, hardware BG Y-scroll road via the VDC BYR register with per-row scenery streaming (no NES 240-wrap / SMS 224-wrap — the VDC masks BYR to the 256px BAT in hardware), 1P speed control + an in-session best distance (a bare HuCard can't save — BRAM is peripheral-only; the bank-$F7 TAM/$1807-unlock dance is documented in-file as the real-hardware path), 2-channel PSG music + SFX. Real 2P simultaneous versus: P2 on the TurboTap (host-enabled port 1, verified). HONEST CAVEAT: no hardware window/raster split in the minimal lib, so the HUD is a SPRITE HUD (screen-space digits) and the title/result screens use a static road backdrop — only the play state scrolls."),
450
+ players: "1-2 (1P endless race, or 2P simultaneous split-lane versus; P2 via TurboTap port 1, host-enabled — verified port-1 reaches P2)",
451
+ sram: "none — a bare HuCard cannot save; BRAM (bank $F7) is PERIPHERAL-ONLY on real hardware (CD-ROM² unit / Tennokoe Bank / Memory Base 128). In-session best-distance only, like the 2600/Lynx; the BRAM mapping + write-lock are documented in-file as the real-hardware path.",
452
+ mechanics: ["lane steering", "speed control (1P)", "traffic pool + AABB", "crash/lives", "best-distance scoring", "2P split-lane versus", "in-session best distance (HuCards can't save)"],
453
+ techniques: [
454
+ "hardware BG Y-scroll (VDC BYR) + per-row streaming",
455
+ "sprite HUD digits (screen-space over a scrolling road)",
456
+ "shadow SATB + R19 vblank DMA",
457
+ "TAM bank-mapping thunks from C",
458
+ "BRAM $1807 write-unlock",
459
+ "PSG divider-table 2-channel music",
460
+ "TurboTap 2P via joy_read(JOY_2)",
461
+ ],
462
+ },
290
463
  };
291
464
  })(),
292
465
 
@@ -303,11 +476,74 @@ const TEMPLATES = {
303
476
  sprite_move: mk("sprite_move", "Joystick-controlled sprite on a screen-2 background. d-pad moves the sprite; verified visible + responsive. The base for any action game."),
304
477
  music_sfx: mk("music_sfx", "AY-3-8910 PSG demo: a looping melody on channel A plus a trigger-fired SFX on channel C, with an on-screen indicator."),
305
478
  catch_game: mk("catch_game", "A complete tiny game: a paddle catches falling fruit with the joystick; full game loop with vblank sync, two sprites, collision, scoring."),
306
- shmup: mk("shmup", "Vertical-shmup scaffold for MSX (screen 2). Player ship (sprite plane 0) + 4 bullet + 4 enemy object pools, a wave spawner, AABB collision, on-screen SCORE tiles, over a banded starfield filling the whole 32x24 name table. Joystick PORT 1 moves the ship (UP/DOWN/LEFT/RIGHT), trigger A (GTTRIG) fires; PSG blip on fire, noise-ish tone on a kill. Interrupt-free vsync via VDP status S#0. Extend with enemy fire, lives, scrolling stars."),
307
- platformer: mk("platformer", "Side-scrolling platformer for MSX (screen 2). Subpixel gravity/jump/land-on-top collision against a table of platforms across a 512-px (64-cell) world, drawn by COLUMN STREAMING into the wrapping screen-2 name table as the camera follows the player; the player sprite draws in screen space. Joystick LEFT/RIGHT walks, trigger A jumps (only when grounded); PSG jump blip. Interrupt-free vsync. Extend with enemies, pickups, goal."),
308
- puzzle: mk("puzzle", "Match-3 / falling-block puzzle for MSX (screen 2). A 6-wide x 12-tall well drawn with the BG tilemap (distinct R/G/B cell tiles + grey border + dim field interior so the playfield is always visible). A 1x3 active piece: joystick LEFT/RIGHT shifts, trigger A rotates the colour order, DOWN soft-drops, trigger B hard-drops; 3+-in-a-row clears in all 4 directions with gravity + cascade chains; PSG chime per clear. Interrupt-free vsync. Extend with levels/next-piece preview."),
309
- sports: mk("sports", "Pong-style 2-player sports for MSX (screen 2). Court (green field + white sidelines + dashed centre net) fills the 32x24 name table; two paddles (stacked sprites) + a ball. Player 1 = joystick PORT 1 UP/DOWN; Player 2 = joystick PORT 2 UP/DOWN, falling back to chase-the-ball AI when no second pad is present so it is playable solo. Wall/paddle bounces + scoring with PSG bonks. Interrupt-free vsync. Extend with serve angles, score display, win condition."),
310
- racing: mk("racing", "Top-down 3-lane racing for MSX (screen 2). Grey road + green-grass shoulders fill the name table; player car at the bottom, obstacle cars (object pool) spawn at the top and slide down. Joystick LEFT/RIGHT (edge-detected) switches lanes; obstacle speed grows with score; an AABB crash triggers a ~60-frame freeze then auto-reset, with a PSG crash tone. SCORE drawn as tiles. Interrupt-free vsync. Extend with pseudo-3D road, fuel, multiple cars."),
479
+ shmup: {
480
+ ...mk("shmup", "NEBULA WARDEN complete MSX vertical shooter (screen 2): title shell with 1P/2P select and session hi-score, simultaneous 2-ship co-op (P2 = joystick port 2), shared-lives arcade scoring, PSG tune-table music + noise SFX, and the MSX signature — screen-2 per-row color (three independent color thirds: depth-banded starfield, HUD band, an 8-color gradient inside one tile). Hi-score is in-session only (the bundled bluemsx build exposes no SAVE_RAM stated honestly in-file)."),
481
+ players: "1-2 (simultaneous co-op)",
482
+ sram: "none core exposes no SAVE_RAM region (in-session hi-score; ASCII8-SRAM mapper exists in-core but unsurfaced; future core round)",
483
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "2P simultaneous co-op (shared lives)", "session hi-score", "title/play/game-over state machine"],
484
+ techniques: [
485
+ "screen-2 per-row color (3 color thirds + per-8x1-row color bytes)",
486
+ "single-tile 8-color gradient",
487
+ "interrupt-free vsync via VDP S#0 poll",
488
+ "sprite Y=208 terminator + offscreen parking",
489
+ "AY-3-8910 noise SFX + per-frame tune-table music",
490
+ "dual joystick ports via GTSTCK/GTTRIG",
491
+ ],
492
+ },
493
+ platformer: {
494
+ ...mk("platformer", "MESA HOPPER — complete MSX side-scrolling platformer (screen 2): title shell with 1P / 2P-alternating-turns select (P2 on joystick port 2, per-player score + lives) and session hi-score, gravity + Q4.4 jump physics, one-way platforms, lethal pits, patrolling spikes, coin + traversal scoring, PSG tune-table music + SFX, and the MSX signature — screen-2 per-row color (3 color thirds: depth-banded fixed-screen level, HUD band, one-tile horizon gradient). Fixed single-screen arena because screen 2 has no hardware scroll (stated in-file). Hi-score is in-session only (bundled bluemsx build exposes no SAVE_RAM — stated honestly in-file)."),
495
+ players: "1-2 (alternating turns, P2 on joystick port 2)",
496
+ sram: "none — core exposes no SAVE_RAM region (in-session hi-score)",
497
+ mechanics: ["gravity + Q4.4 jump", "one-way platforms", "pits + patrolling spikes", "coin + traversal scoring", "2P alternating turns (per-player score/lives)", "session hi-score", "title/play/game-over state machine"],
498
+ techniques: [
499
+ "screen-2 per-row color (3 color thirds + per-8x1-row color bytes)",
500
+ "single-tile 8-color horizon gradient",
501
+ "fixed-screen level (no screen-2 hardware scroll)",
502
+ "interrupt-free vsync via VDP S#0 poll",
503
+ "sprite Y=208 terminator + offscreen parking",
504
+ "dual joystick ports via GTSTCK/GTTRIG",
505
+ ],
506
+ },
507
+ puzzle: {
508
+ ...mk("puzzle", "STOKE STACK — complete MSX falling-trio match-3 (screen 2): title shell with 1P-marathon / 2P-simultaneous-versus select (P2 = joystick port 2) and session hi-score, levels that speed the fall, cascade-chain scoring, 2P garbage attacks, PSG tune-table music + SFX, and the MSX signature — screen-2 per-row color (gem-colour-per-third one-tile trick + a one-tile ember gradient seam). Hi-score is in-session only (bundled bluemsx build exposes no SAVE_RAM — stated honestly in-file)."),
509
+ players: "1-2 (simultaneous versus, P2 on joystick port 2)",
510
+ sram: "none — core exposes no SAVE_RAM region (in-session hi-score)",
511
+ mechanics: ["falling-trio match-3", "4-direction runs", "cascade chains", "levels (1P speed-up)", "2P versus garbage rows", "session hi-score", "title/play/game-over state machine"],
512
+ techniques: [
513
+ "screen-2 per-row color (3 thirds + per-8x1-row bytes)",
514
+ "one-tile three-colour gem (colour-per-third)",
515
+ "single-tile ember gradient seam",
516
+ "interrupt-free vsync via VDP S#0 poll",
517
+ "sprite Y=208 terminator + offscreen parking",
518
+ "dual joystick ports via GTSTCK/GTTRIG",
519
+ ],
520
+ },
521
+ sports: {
522
+ ...mk("sports", "SPARK SWAT — complete MSX head-to-head court sports (screen 2): title shell with 1P-vs-beatable-CPU / 2P-simultaneous-versus select (P2 = joystick port 2), first-to-5 match flow into a result screen, longest-win-streak record, PSG tune-table music + SFX, and the MSX signature — screen-2 per-row color (banded court + a one-tile net 'pulse' gradient). A +/-1 PRNG deflection spin guarantees idle 1P rallies END. Record is in-session only (bundled bluemsx build exposes no SAVE_RAM — stated honestly in-file)."),
523
+ players: "1-2 (1P vs beatable CPU, or 2P simultaneous versus, P2 on joystick port 2)",
524
+ sram: "none — core exposes no SAVE_RAM region (in-session win-streak record)",
525
+ mechanics: ["paddle/ball court physics", "edge-deflection angle", "beatable chase-AI CPU", "2P simultaneous versus", "first-to-5 match + result screen", "PRNG rally spin (idle matches end)", "in-session win-streak record"],
526
+ techniques: [
527
+ "screen-2 per-row color (banded court + one-tile net pulse gradient)",
528
+ "interrupt-free vsync via VDP S#0 poll",
529
+ "sprite Y=208 terminator + offscreen parking",
530
+ "dual joystick ports via GTSTCK/GTTRIG",
531
+ "PSG tune-table music + SFX",
532
+ ],
533
+ },
534
+ racing: {
535
+ ...mk("racing", "TURBO TANGLE — complete MSX top-down four-lane road racer (screen 2): title shell with 1P / 2P-split-lane-versus select, 1P speed control (UP/A gas, DOWN/B brake, speed 1-4) banking DISTANCE, 3 crashes end the run; 2P versus shares one road (P1 left two lanes / P2 right two, P2 on port 2), first to wreck out loses. Per-row color signature (depth-banded thirds + a one-tile shimmer divider gradient), PSG music + SFX. HONEST: screen 2 has no scroll register, so the road motion is the marching lane-dash + roadside columns redrawn one phase-step per frame (static asphalt painted once) — taught against the NES's true BG scroll. Best distance is in-session only (bluemsx exposes no SAVE_RAM — stated in-file)."),
536
+ players: "1-2 (2P split-lane versus, P2 on joystick port 2)",
537
+ sram: "none — core exposes no SAVE_RAM region (in-session best distance)",
538
+ mechanics: ["lane steering", "speed control 1-4", "best-distance persistence (in-session)", "obstacle pool + AABB crashes", "crash lives", "2P split-lane versus"],
539
+ techniques: [
540
+ "software road scroll (no screen-2 hw scroll — redraw dashes/tufts per phase step)",
541
+ "screen-2 per-row color (banded thirds + one-tile shimmer divider)",
542
+ "interrupt-free vsync via VDP S#0 poll",
543
+ "dual joystick ports via GTSTCK/GTTRIG",
544
+ "PSG tune-table music + SFX",
545
+ ],
546
+ },
311
547
  };
312
548
  })(),
313
549
  };
@@ -322,6 +558,7 @@ const GBC_RUNTIME = [
322
558
  { src: "lib/c/gb_runtime.c", dst: "gb_runtime.c" },
323
559
  { src: "lib/c/gb_crt0.s", dst: "gb_crt0.s" },
324
560
  { src: "lib/c/patch-header.js", dst: "patch-header.js" },
561
+ { src: "lib/c/font.h", dst: "font.h" }, /* digits+A-Z 2bpp glyphs; every gbc example #includes it */
325
562
  ];
326
563
  const GBC_LANG = "C (SDCC sm83, GBC color)";
327
564
  TEMPLATES.gbc = {
@@ -343,37 +580,82 @@ TEMPLATES.gbc = {
343
580
  shmup: {
344
581
  main: "templates/shmup.c", runtime: GBC_RUNTIME,
345
582
  lang: GBC_LANG, ext: ".gbc",
346
- describe: "Vertical-shmup for GBC. Colorful sprites (white ship, yellow bullets, red enemies) and a starfield BG palette via BCPS/BCPD. Same sfx wiring as GB (sound_play_tone/noise).",
583
+ describe: "PHOTON DRIFT — Game Boy Color vertical shooter to the full contract: press-start title shell with battery-persistent hi-score (MBC1+RAM+BATTERY crt0 header, $0A enable dance, magic+checksum, survives power cycles), object-pool ship/bullets/enemies + wave spawner + AABB collision, GB-signature window-layer fixed HUD over an SCY-scrolled starfield — and the GBC SIGNATURE on top: TRUE per-tile color, a 4-band nebula starfield (blue/teal/green/magenta) as real CGB palettes (BCPS/BCPD) assigned per BG cell through the VRAM bank-1 attribute map, plus cyan ship / gold bullet / red enemy OBJ palettes (OCPS) — not colorized mono. GB APU music + SFX. KEY GOTCHA: HUD/text commits write bank-0 tiles only and stage text out of the vblank slice. Statics need dataLoc 0xC200. 1P by design (link-cable 2P not emulatable single-instance).",
584
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
585
+ sram: "MBC1+RAM+BATTERY, 8KB at $A000 ($0A-gated, magic+checksum), verified across hardReset",
586
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "title/play/game-over state machine", "battery hi-score"],
587
+ techniques: [
588
+ "CGB per-tile color (4-band nebula starfield via bank-1 attribute map)",
589
+ "OBJ palettes (OCPS) for ship/bullet/enemy",
590
+ "window-layer fixed HUD over SCY starfield",
591
+ "two-phase vblank commit (bank-0-only HUD + pre-staged text)",
592
+ "battery SRAM save ($0A enable dance)",
593
+ ],
347
594
  },
348
595
  platformer: {
349
596
  main: "templates/platformer.c", runtime: GBC_RUNTIME,
350
597
  lang: GBC_LANG, ext: ".gbc",
351
- describe: "SIDE-SCROLLING platformer for GBC. Full CGB color palette (BG + sprite via BCPS/OCPS) over the GB side-scroller core: subpixel gravity + jump + land-on-top collision against platforms across a 256-px world (the wrapping BG map). The camera follows the player and scrolls the BG via SCX; the player sprite draws in screen space. A=jump, d-pad=move. One BG map wide (no streaming) for a wider world, stream a new BG-map column on each 8px camera step (window for a fixed HUD). See the GBC MENTAL_MODEL.md 'Horizontal scrolling'. Extend with enemies, goals, pickups.",
598
+ describe: "SPECTRA BOUND — Game Boy Color side-scrolling platformer to the full contract: the GB runner core (Q4.4 sub-pixel gravity/jump, one-way platforms, lethal pits, drifting spikes, coins + distance scoring, one-way scroll-wall camera, seamlessly looping SCX 256-px column-map level, window-layer fixed HUD, divide-free math) with the GBC SIGNATURE on top — TRUE per-tile color: sky/grass/dirt/platform/HUD are 5 real CGB palettes (BCPS/BCPD) assigned per BG cell through the VRAM bank-1 attribute map, plus colorful player/coin/spike OBJ palettes (OCPS) not colorized mono. Press-start title, persistent battery hi-score (MBC1+RAM+BATTERY SRAM, magic+checksum, verified across power cycles), GB APU music + SFX. KEY GOTCHA: HUD/text commits write bank-0 tiles only and stage text out of the vblank slice per-cell VBK toggles or in-vblank char_tile overrun mode 3 and drop writes. Statics need dataLoc 0xC200. 1P by design (link-cable 2P not emulatable single-instance).",
599
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
600
+ sram: "MBC1+RAM+BATTERY, 8KB at $A000 ($0A-gated, magic+checksum), verified across hardReset",
601
+ mechanics: ["gravity + Q4.4 jump physics", "one-way platforms (6-px landing window)", "pits + spikes + coins", "distance + coin scoring", "one-way scroll-wall camera", "lives + respawn breather", "battery-persistent hi-score"],
602
+ techniques: [
603
+ "CGB palette RAM (BCPS/BCPD + OCPS/OCPD, mode-3 write constraint)",
604
+ "VRAM bank-1 attribute map (VBK per-tile palettes)",
605
+ "window-layer HUD",
606
+ "SCX seamless looping scroll (uint8 wrap)",
607
+ "two-phase vblank commit (bank-0-only HUD + pre-staged text)",
608
+ "battery SRAM save ($0A enable dance)",
609
+ ],
352
610
  },
353
611
  puzzle: {
354
612
  main: "templates/puzzle.c",
355
- runtime: [
356
- ...GBC_RUNTIME,
357
- { src: "lib/c/font.h", dst: "font.h" }, /* digits+A-Z 2bpp glyphs for the HUD */
358
- ],
613
+ runtime: GBC_RUNTIME,
359
614
  lang: GBC_LANG, ext: ".gbc",
360
- describe: "Falling-jewel matcher for GBC (the polished reference puzzle). 8x17 well, 6 jewel colors with " +
361
- "real CGB palettes, matches in all 4 directions (H/V/both diagonals), gravity + cascade chains, magic " +
362
- "jewel every 18th piece, level speedup, 6-digit score, title + game-over screens, SFX + toggleable " +
363
- "music. Rendering: falling column + NEXT preview are OAM sprites; the locked well is BG tiles via a " +
364
- "COLLECT/FLUSH vblank queue with an idle scrub (writes outside vblank silently drop on this core — " +
365
- "never bypass the queue). Statics need dataLoc 0xC200 (above shadow_oam at $C100) — the project build " +
366
- "recipe sets that automatically.",
615
+ describe: "CHROMA WELL — falling-jewel matcher, to the full contract: 8x15 well, 6 jewel colors as 6 REAL CGB palettes (BCPS/BCPD + the VRAM bank-1 attribute map — true per-tile color, not colorized mono), 4-direction matches with gravity cascades + chain scoring, magic jewel every 18th piece, window-layer HUD strip, persistent battery hi-score (MBC1+RAM+BATTERY SRAM, magic+checksum, verified across power cycles), title/play/game-over shell, ch1 music + ch2 SFX. The locked well paints via the COLLECT/FLUSH vblank queue (writes outside vblank silently drop — never bypass it). Statics need dataLoc 0xC200 (the project recipe sets it).",
616
+ players: "1 (handheld link-cable 2P not emulatable single-instance)",
617
+ sram: "MBC1+RAM+BATTERY, 8KB at $A000 ($0A-gated), verified across hardReset",
618
+ mechanics: ["grid logic", "falling-piece matching", "gravity + cascade chains", "scoring/levels", "battery hi-score", "title/play/game-over state machine"],
619
+ techniques: [
620
+ "CGB palette RAM (BCPS/BCPD + OCPS/OCPD, mode-3 write constraint)",
621
+ "VRAM bank-1 attribute map (VBK per-tile palettes)",
622
+ "window-layer HUD",
623
+ "vblank COLLECT/FLUSH queue + idle scrub",
624
+ "OAM DMA HRAM stub",
625
+ "battery SRAM save ($0A enable dance)",
626
+ ],
367
627
  },
368
628
  sports: {
369
629
  main: "templates/sports.c", runtime: GBC_RUNTIME,
370
630
  lang: GBC_LANG, ext: ".gbc",
371
- describe: "Pong for GBC. Player-vs-AI (one controller). Court green BG + colored paddles + paddle-hit sfx.",
631
+ describe: "HUE HUSTLE — Game Boy Color versus court game (Pong lineage) to the full contract: press-start title, 1P vs a beatable chase-AI CPU, first-to-5 match flow into a result screen, a PRNG +/-1 rally spin so an idle match provably ENDS, GB APU ch1 music + ch2 SFX, window-layer fixed HUD — and the GBC SIGNATURE: TRUE per-tile color. The two paddles are told apart by distinct CGB OBJ PALETTE (azure you / red CPU via OCPS), not DMG shade; the court is a real color scene (teal floor / gold rails / violet net) as CGB palettes assigned per BG cell through the VRAM bank-1 attribute map. Longest 1P win streak persists to battery SRAM (MBC1+RAM+BATTERY, magic+checksum, verified across power cycles). HUD/result commits write bank-0 tiles only and stage text out of the vblank slice. dataLoc 0xC200. 1P by design (link-cable 2P not emulatable single-instance).",
632
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
633
+ sram: "MBC1+RAM+BATTERY, 8KB at $A000 ($0A-gated, magic+checksum), verified across hardReset",
634
+ mechanics: ["paddle/ball physics with edge-deflection", "beatable chase-AI CPU", "first-to-5 match flow", "PRNG rally spin (idle match ends)", "battery win-streak record", "title/play/result state machine"],
635
+ techniques: [
636
+ "CGB OBJ palettes (OCPS) for distinct team paddles",
637
+ "CGB per-tile color court (bank-1 attribute map)",
638
+ "window-layer fixed HUD",
639
+ "two-phase vblank commit (bank-0-only HUD + pre-staged result text)",
640
+ "battery SRAM save ($0A enable dance)",
641
+ "xorshift16 PRNG for deterministic-versus rally break",
642
+ ],
372
643
  },
373
644
  racing: {
374
645
  main: "templates/racing.c", runtime: GBC_RUNTIME,
375
646
  lang: GBC_LANG, ext: ".gbc",
376
- describe: "3-lane racer for GBC. Asphalt BG palette + colored player/enemy cars + lane-switch + crash sfx.",
647
+ describe: "TWILIGHT LANE — Game Boy Color top-down road racer to the full contract: press-start title (honest no-2P), the road scrolls via SCY into a 256-px map (seamless uint8 wrap — contrast vs NES 240 / SMS 224 garbage-row / Genesis hardware-masked plane), four lanes, A/UP accelerate + B/DOWN brake (speed 1-4), LEFT/RIGHT lane tilt, 6-slot traffic pool, crash + 3 lives with invuln blink, window-layer HUD, best distance to battery SRAM (verified across power cycles). The GBC SIGNATURE: 5 real CGB BG palettes (violet dusk asphalt / evening grass / pine trees / cyan-glow dividers / HUD) assigned per cell via the bank-1 attribute map, plus cyan-car / red-traffic OBJ palettes (OCPS) — not colorized mono. GB APU music + SFX, two-phase vblank commit, dataLoc 0xC200.",
648
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
649
+ sram: "MBC1+RAM+BATTERY, 8KB at $A000 ($0A-gated, best-distance magic+checksum), verified across hardReset",
650
+ mechanics: ["lane steering", "speed control 1-4", "best-distance persistence", "traffic pool + AABB crashes", "crash lives + invuln blink"],
651
+ techniques: [
652
+ "CGB per-tile color road (5 palettes via bank-1 attribute map)",
653
+ "OBJ palettes (OCPS) for car + traffic",
654
+ "SCY vertical road scroll (256-px seamless uint8 wrap)",
655
+ "window-layer fixed HUD",
656
+ "two-phase vblank commit (bank-0-only HUD + streamed roadside restamp)",
657
+ "battery SRAM best-distance ($0A enable dance)",
658
+ ],
377
659
  },
378
660
  /* R45 — same hUGEDriver music_demo as GB, with BCPS/BCPD palette
379
661
  * writes so it boots in CGB mode (gambatte flips on .gbc + $0143=$80).
@@ -460,35 +742,87 @@ TEMPLATES.sms = {
460
742
  runtime: SMS_RUNTIME,
461
743
  lang: SMS_LANG,
462
744
  ext: ".sms",
463
- describe: "Vertical-shmup scaffold for SMS. Player ship + 4 bullets + 4 enemies, wave spawner, AABB collisions, score (WRAM). Pre-allocated SAT slots 0/1-4/5-8.",
745
+ describe: "ASTRO PICKET complete SMS vertical shooter: title shell with 1P/2P select and hi-score, simultaneous 2-ship co-op (P2 on port 1), PSG music + SFX, and the SMS signature LINE-INTERRUPT split (VDP register-10 line counter: fixed HUD strip over a scrolling starfield — the programmable cousin of the NES sprite-0 trick). Hi-score persists to Sega-mapper cart RAM on 64KB+ builds (verified); 32KB builds are honestly in-session.",
746
+ players: "1-2 (simultaneous co-op)",
747
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds; in-session at 32KB (gpgx maps mapper RAM only above 48KB — documented in-file)",
748
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "2P simultaneous co-op", "title/play/game-over state machine"],
749
+ techniques: [
750
+ "VDP line-interrupt split (fixed HUD over scrolling field)",
751
+ "Sega-mapper cart RAM persistence ($FFFC control)",
752
+ "PSG tune-table music + noise SFX",
753
+ "SAT slot pre-allocation (no flicker)",
754
+ "IM1 interrupt handshake (VDP status ack discipline)",
755
+ ],
464
756
  },
465
757
  platformer: {
466
758
  main: "templates/platformer.c",
467
759
  runtime: SMS_RUNTIME,
468
760
  lang: SMS_LANG,
469
761
  ext: ".sms",
470
- describe: "SIDE-SCROLLING platformer for SMS with COLUMN STREAMING. Subpixel gravity + jump + land-on-top collision against platforms across a 512-px world. The SMS name table is only 32 cells (256 px) and wraps, so the world is streamed: the camera follows the player, writes VDP R8 (-camX) for smooth pixel scroll, and each time camX crosses an 8-px boundary it rewrites the name-table column entering from the right (or left, on retreat) with the next world column. Player sprite draws in screen space. 1=jump, d-pad=move. For a fixed HUD, lock the top rows with VDP R0 bit 6. See the SMS MENTAL_MODEL.md 'Horizontal scrolling'. Extend with enemies, goals, pickups.",
762
+ describe: "GULLY VAULT — side-scrolling platformer: gravity + Q4.4 sub-pixel jump physics, one-way platforms, pits and spikes, coins + distance scoring, PSG music + SFX. 2P is classic alternating turns (P2 on port B) with per-player score and lives. The SMS signature LINE-INTERRUPT split holds a fixed HUD over the scrolling level, and the 32-cell name table wraps at exactly 256 px the level loops seamlessly with no second nametable or column streaming. Hi-score persists to Sega-mapper cart RAM on 64KB+ builds (verified incl. power-cycle); 32KB builds are honestly in-session.",
763
+ players: "1-2 (alternating turns, P2 on port B)",
764
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
765
+ mechanics: ["gravity-jump physics (Q4.4 fixed point)", "one-way platform collision", "one-way camera with scroll wall", "pits + spike hazards", "coin + distance scoring", "alternating 2P turns with per-player lives"],
766
+ techniques: [
767
+ "VDP line-interrupt split (fixed HUD over scrolling level)",
768
+ "hardware-wrapping 256-px name table as a seamless looping world (R8 = -scroll_x)",
769
+ "Sega-mapper cart RAM persistence ($FFFC control)",
770
+ "PSG tune-table music + voice-2 SFX arbitration",
771
+ "IM1 interrupt handshake (VDP status ack discipline)",
772
+ ],
471
773
  },
472
774
  puzzle: {
473
775
  main: "templates/puzzle.c",
474
776
  runtime: SMS_RUNTIME,
475
777
  lang: SMS_LANG,
476
778
  ext: ".sms",
477
- describe: "Match-3 falling-block scaffold. 6×12 grid rendered via BG tilemap (three distinct tile shapes for R/G/B cells), 1×3 active piece, rotate via B1, hard-drop via B2.",
779
+ describe: "GEODE GAMBIT — falling-trio match-3 to the full contract: 1P marathon with levels and cascade chains; 2P simultaneous split-board versus where chains send garbage rows (both wells update every frame). The board is BG tiles via sms_set_tilemap_cell a whole well repaints in one vblank (Mode-4 has the VDP bandwidth; taught against the NES's 16-entry vblank budget). Fixed HUD under the line-IRQ split, Sega-mapper cart-RAM hi-score (verified across power-cycle on 64KB), PSG music + SFX.",
780
+ players: "1-2 (2P = simultaneous versus, split boards with garbage attacks)",
781
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across hardReset); in-session at 32KB (gpgx maps mapper RAM only above 48KB — documented in-file)",
782
+ mechanics: ["falling-trio control", "match-3 in 4 directions", "cascade chains with multipliers", "garbage attack rows", "levels", "split-board simultaneous versus"],
783
+ techniques: [
784
+ "whole-well repaint via sms_set_tilemap_cell (one vblank)",
785
+ "VDP line-interrupt split (fixed HUD strip)",
786
+ "Sega-mapper cart RAM persistence ($FFFC control)",
787
+ "PSG music + voice-0 SFX arbitration",
788
+ "IM1 interrupt handshake (VDP status ack discipline)",
789
+ ],
478
790
  },
479
791
  sports: {
480
792
  main: "templates/sports.c",
481
793
  runtime: SMS_RUNTIME,
482
794
  lang: SMS_LANG,
483
795
  ext: ".sms",
484
- describe: "Two-player Pong on SMS. Both controller ports wired sms_joypad_read for P1, sms_joypad_read_p2 for P2 (reassembles the awkward split-across-$DC/$DD bit layout). AI fallback when no second pad is plugged in.",
796
+ describe: "DEUCE DASH — head-to-head court sports to the full contract: title shell with 1P-vs-CPU / 2P-versus select, a beatable chase-AI CPU, 2P simultaneous versus (P2 on PORT B via sms_joypad_read_p2), first-to-5 match flow into a result screen, PSG music + SFX. A +/-1 PRNG deflection spin guarantees idle rallies END (no infinite limit cycle). Longest 1P win streak persists to Sega-mapper cart RAM (verified across power-cycle on 64KB; honest in-session at 32KB). Fixed HUD under the SMS line-IRQ split.",
797
+ players: "1-2 (1P = vs beatable CPU; 2P = simultaneous versus, P2 on port B)",
798
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
799
+ mechanics: ["paddle vs ball court play", "position-based deflection angle", "beatable chase-AI CPU", "simultaneous 2P versus", "first-to-5 match flow + result screen", "PRNG rally spin (no limit cycle)", "longest-win-streak record"],
800
+ techniques: [
801
+ "VDP line-interrupt split (fixed HUD over the court)",
802
+ "split static/dynamic HUD to fit the vblank VRAM budget",
803
+ "Sega-mapper cart RAM persistence ($FFFC control)",
804
+ "PSG tune-table music + voice-0/1 SFX arbitration",
805
+ "PORT B P2 reassembly (sms_joypad_read_p2)",
806
+ "IM1 interrupt handshake (VDP status ack discipline)",
807
+ ],
485
808
  },
486
809
  racing: {
487
810
  main: "templates/racing.c",
488
811
  runtime: SMS_RUNTIME,
489
812
  lang: SMS_LANG,
490
813
  ext: ".sms",
491
- describe: "Endless 3-lane top-down racer. LEFT/RIGHT (edge-detected) switches lanes, obstacles slide down at speed = 2 + score/500 (capped at 4). 60-frame freeze + auto-reset on collision.",
814
+ describe: "FENDER FURY top-down vertical road racer to the full contract: 1P endless race with speed control (button1/UP gas, button2/DOWN brake, speed 1-4) and persistent best DISTANCE; 2P simultaneous split-lane VERSUS (both cars on screen, P2 on port B), solid center divider splitting territories, first to wreck out loses. The road is the BG scrolled vertically by R9 (whole-plane, latched once per frame) — the SMS twist on the Genesis full-plane VSCROLL, with the 224-px name-table Y-wrap footgun handled (vs NES 240 / Genesis 256). Streamed roadside rows, line-IRQ-split fixed HUD (sprite-digit HUD on the fixed top line + per-strip R8 road sway below), Sega-mapper cart-RAM best (verified across power-cycle on 64KB; in-session at 32KB), PSG music + SFX.",
815
+ players: "1-2 (2P = simultaneous split-lane versus, P2 on port B)",
816
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
817
+ mechanics: ["lane steering (edge-detected)", "1P speed control 1-4", "best-distance persistence", "traffic object pool + AABB crashes", "crash lives + invuln grace", "2P split-lane versus with shared road"],
818
+ techniques: [
819
+ "whole-plane R9 vertical road scroll (224-px Y-wrap)",
820
+ "streamed roadside rows",
821
+ "line-IRQ split: fixed sprite HUD + per-strip R8 road sway",
822
+ "Sega-mapper cart RAM persistence ($FFFC)",
823
+ "PSG music + multi-channel SFX",
824
+ "IM1 interrupt handshake (VDP status ack discipline)",
825
+ ],
492
826
  },
493
827
  shmup_2p: {
494
828
  main: "templates/shmup_2p.c",
@@ -559,35 +893,91 @@ TEMPLATES.gg = {
559
893
  runtime: GG_RUNTIME,
560
894
  lang: GG_LANG,
561
895
  ext: ".gg",
562
- describe: "Vertical-shmup scaffold for GG. Player ship + 4 bullets + 4 enemies, wave spawner, AABB collisions, score. Pew sfx on fire, boom on hit (PSG via gg_sfx).",
896
+ describe: "PRISM PATROL complete GG vertical shooter: press-START title shell with hi-score, PSG music + SFX, and the GG/SMS signature LINE-INTERRUPT split (fixed HUD over a scrolling starfield) taught against the GG's #1 footgun — the 160x144 window centered in the 256x192 frame (VIS_* offsets; line-counter values are FULL-frame scanlines, so the split lands at 47, not the SMS's 23). 12-bit CRAM palette shows the 4096 colors. Hi-score persists to Sega-mapper cart RAM on 64KB+ builds (verified incl. power-cycle); 32KB builds are honestly in-session.",
897
+ players: "1 (one controller; Gear-to-Gear link 2P can't be emulated single-instance — honest note in-file)",
898
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
899
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "title/play/game-over state machine", "persistent hi-score"],
900
+ techniques: [
901
+ "VDP line-interrupt split with GG-window scanline math",
902
+ "GG 160x144 visible-window placement (VIS_* offset idiom)",
903
+ "GG 12-bit CRAM palette (2-byte entries vs SMS 1-byte)",
904
+ "Sega-mapper cart RAM persistence ($FFFC control)",
905
+ "PSG note-table music + noise SFX",
906
+ "IM1 handshake + DI/EI repaint bracket (line-IRQ ack races the VDP address latch)",
907
+ ],
563
908
  },
564
909
  platformer: {
565
910
  main: "templates/platformer.c",
566
911
  runtime: GG_RUNTIME,
567
912
  lang: GG_LANG,
568
913
  ext: ".gg",
569
- describe: "SIDE-SCROLLING platformer for GG with COLUMN STREAMING. Same Mode-4 VDP as the SMS only the visible window differs (160 px wide). Subpixel gravity + jump + land-on-top collision across a 512-px world; the camera centers on the 160-px window, writes VDP R8 (-camX) for smooth pixel scroll, and streams the next world column into the wrapping 32-cell name table each time camX crosses an 8-px boundary. Player sprite draws in screen space. Jump boing via PSG sfx. 1=jump, d-pad=move. See the SMS/GG MENTAL_MODEL.md 'Horizontal scrolling'. Extend with enemies, goals, pickups.",
914
+ describe: "SCARP SPRINT — side-scrolling platformer for the Game Gear: gravity + Q4.4 sub-pixel jump physics, one-way platforms, pits and spikes, coins + distance scoring, PSG music + SFX. The GG twin of the SMS platformer, fitted to the 160x144 visible window (VIS_* offsets; the line-IRQ split lands at full-frame scanline 47, not the SMS's 23). 2P is classic alternating turns (P2 on port B) with per-player score and lives. The GG/SMS signature LINE-INTERRUPT split holds a fixed HUD over the scrolling level, and the 32-cell name table wraps at exactly 256 px the level loops seamlessly with no second nametable or column streaming. GG 12-bit CRAM palette. Hi-score persists to Sega-mapper cart RAM on 64KB+ builds (verified incl. power-cycle); 32KB builds are honestly in-session.",
915
+ players: "1-2 (alternating turns, P2 on port B; GG has 2 controller ports — not the link cable)",
916
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
917
+ mechanics: ["gravity-jump physics (Q4.4 fixed point)", "one-way platform collision", "one-way camera with scroll wall", "pits + spike hazards", "coin + distance scoring", "alternating 2P turns with per-player lives"],
918
+ techniques: [
919
+ "VDP line-interrupt split with GG-window scanline math (split at 47)",
920
+ "GG 160x144 visible-window placement (VIS_* offset idiom)",
921
+ "GG 12-bit CRAM palette (2-byte entries vs SMS 1-byte)",
922
+ "hardware-wrapping 256-px name table as a seamless looping world (R8 = -scroll_x)",
923
+ "Sega-mapper cart RAM persistence ($FFFC control)",
924
+ "IM1 handshake + DI/EI repaint bracket (line-IRQ ack races the VDP address latch)",
925
+ ],
570
926
  },
571
927
  puzzle: {
572
928
  main: "templates/puzzle.c",
573
929
  runtime: GG_RUNTIME,
574
930
  lang: GG_LANG,
575
931
  ext: ".gg",
576
- describe: "Match-3 falling-block scaffold. 6×12 grid, 3 piece, rotate via B1, hard-drop via B2. Rotate click + clear chime via PSG.",
932
+ describe: "SLUICE STACK — falling-gem versus match-3 to the full contract, fit to the GG's 160x144 visible window (VIS_* offset idiom): title shell, 1P MARATHON (levels speed the fall as you clear) and 2P SIMULTANEOUS versus (P2 on PORT B via gg_joypad_read_p2 — gpgx wires the SMS second pad for GG) on two narrow side-by-side wells where cascade chains lay garbage rows on your rival. The GG-window adaptation: wells are 5 cells wide (vs the SMS's 6) so two + a centre gutter fit the 20-col window — documented in-file. Move/cycle-colour/soft-drop/hard-drop, 3+ clears in all 4 directions, gravity cascades chain for multiplied score. Fixed HUD under the GG line-IRQ split (split at full-frame scanline 47, not the SMS's 23). 12-bit CRAM palette. Hi-score persists to Sega-mapper cart RAM (verified across power-cycle on 64KB; honest in-session at 32KB).",
933
+ players: "1-2 (1P marathon; 2P = simultaneous split-board versus, P2 on port B)",
934
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
935
+ mechanics: ["falling-trio match-3", "3+ clears in 4 directions", "gravity cascade chains (multiplied score)", "1P marathon with levels", "simultaneous 2P versus with garbage-row attacks", "persistent hi-score"],
936
+ techniques: [
937
+ "GG 160x144 visible-window placement (VIS_* offset idiom)",
938
+ "narrow-well geometry to fit two boards in 20 cols",
939
+ "VDP line-interrupt split with GG-window scanline math (split at 47)",
940
+ "whole-well repaint in one vblank (vs NES per-row queue)",
941
+ "GG 12-bit CRAM palette (2-byte entries vs SMS 1-byte)",
942
+ "PORT B P2 reassembly (gg_joypad_read_p2)",
943
+ ],
577
944
  },
578
945
  sports: {
579
946
  main: "templates/sports.c",
580
947
  runtime: GG_RUNTIME,
581
948
  lang: GG_LANG,
582
949
  ext: ".gg",
583
- describe: "Single-player Pong vs AI (GG has only one controller). Paddle hit + wall blip + score chime via PSG.",
950
+ describe: "BAFFLE BOUNCE — head-to-head court sports to the full contract, fit to the GG's 160x144 visible window (VIS_* offset idiom): title shell with 1P-vs-CPU / 2P-versus select, a beatable chase-AI CPU, 2P SIMULTANEOUS versus (P2 on PORT B via gg_joypad_read_p2 — gpgx wires the SMS second pad for GG), first-to-5 match flow into a result screen, PSG music + SFX. A +/-1 PRNG deflection spin guarantees idle rallies END. Longest 1P win streak persists to Sega-mapper cart RAM (verified across power-cycle on 64KB; honest in-session at 32KB). Fixed HUD under the GG line-IRQ split (split at full-frame scanline 47, not the SMS's 23). 12-bit CRAM palette.",
951
+ players: "1-2 (1P = vs beatable CPU; 2P = simultaneous versus, P2 on port B)",
952
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
953
+ mechanics: ["paddle vs ball court play", "position-based deflection angle", "beatable chase-AI CPU", "simultaneous 2P versus", "first-to-5 match flow + result screen", "PRNG rally spin (no limit cycle)", "longest-win-streak record"],
954
+ techniques: [
955
+ "GG 160x144 visible-window placement (VIS_* offset idiom)",
956
+ "VDP line-interrupt split with GG-window scanline math (split at 47)",
957
+ "split static/dynamic HUD to fit the vblank VRAM budget",
958
+ "GG 12-bit CRAM palette (2-byte entries vs SMS 1-byte)",
959
+ "di/ei repaint bracket (IRQ address-latch race)",
960
+ "Sega-mapper cart RAM persistence ($FFFC control)",
961
+ "PORT B P2 reassembly (gg_joypad_read_p2)",
962
+ ],
584
963
  },
585
964
  racing: {
586
965
  main: "templates/racing.c",
587
966
  runtime: GG_RUNTIME,
588
967
  lang: GG_LANG,
589
968
  ext: ".gg",
590
- describe: "Top-down 3-lane racer scaffold. L/R switches lanes, obstacles slide down + accelerate with score. Lane-switch beep + crash noise via PSG.",
969
+ describe: "CHICANE DASH — top-down vertical road racer to the full contract, the GG twin of the SMS FENDER FURY fitted to the 160x144 visible window (VIS_* offsets; line-IRQ split at full-frame scanline 47). 1P endless race with speed control (button1/UP gas, button2/DOWN brake, speed 1-4) + persistent best DISTANCE; 2P simultaneous split-lane VERSUS (both cars on screen, P2 on port B), center divider, first to wreck out loses. R9 whole-plane vertical road (224-px Y-wrap), streamed roadside rows, fixed sprite-digit HUD + per-strip R8 road sway (the chicane curve), GG 12-bit CRAM palette, Sega-mapper cart-RAM best (verified across power-cycle on 64KB; in-session at 32KB), PSG music + SFX.",
970
+ players: "1-2 (2P = simultaneous split-lane versus, P2 on port B)",
971
+ sram: "Sega-mapper cart RAM at $8000 ($FFFC bit 3) on 64KB+ builds (verified across soft reset AND power-cycle); in-session at 32KB",
972
+ mechanics: ["lane steering", "speed control 1-4", "best-distance persistence", "traffic + AABB crashes", "crash lives", "2P split-lane versus"],
973
+ techniques: [
974
+ "GG 160x144 visible-window placement (VIS_* offset idiom)",
975
+ "whole-plane R9 vertical road scroll (224-px Y-wrap)",
976
+ "line-IRQ split at GG scanline 47: fixed sprite HUD + per-strip R8 road sway",
977
+ "GG 12-bit CRAM palette",
978
+ "Sega-mapper cart RAM persistence ($FFFC control)",
979
+ "streamed roadside rows (verge cols only, off the centered title)",
980
+ ],
591
981
  },
592
982
  music_demo: {
593
983
  main: "templates/music_demo.c",
@@ -636,27 +1026,79 @@ TEMPLATES.c64 = {
636
1026
  shmup: {
637
1027
  main: "templates/shmup.c", runtime: C64_RUNTIME, runtimeDirs: C64_VENDOR_DIRS,
638
1028
  lang: C64_LANG, ext: ".prg",
639
- describe: "Vertical-shmup. Player + 3 bullets + 4 enemies via VIC-II hardware sprites. SID sfx: pew + boom.",
1029
+ describe: "ION SQUALL — complete horizontal shooter: title shell (port-2 fire = 1P, port-1 fire = 2P co-op), shared-lives co-op, bullet/enemy pools, score + session hi-score, 2-voice SID music with the signature filter sweep + voice-2 SFX, and the C64 signature raster-IRQ split (fixed score bar over a fine-scrolling starfield). Hi-score persists via a 1541 DISK SAVE when run from a .d64 (KERNAL write to drive 8, committed to the live disk; in-session only as a bare .prg) — documented in-file.",
1030
+ players: "1-2 (simultaneous co-op; P1 on joystick port 2, P2 on port 1)",
1031
+ sram: "1541 DISK SAVE — the honest C64 medium (no battery SRAM): the game writes a 2-byte record to a SEQ file 'HI' on drive 8 via the KERNAL (cbm_open/read/write), VICE commits it into the live .d64 (true-drive write-back). Requires running from a .d64 (state({op:exportDisk}) captures the save; reload restores it); as a bare .prg the save is a silent no-op (in-session only).",
1032
+ mechanics: ["projectile pools", "altitude-seeking enemy spawner", "AABB collision", "shared-lives co-op", "title/play/game-over state machine"],
1033
+ techniques: [
1034
+ "raster-IRQ split (mid-frame $D016 rewrite: fixed bar over scrolling field)",
1035
+ "dual joystick-port reads with keyboard-conflict awareness ($DC00/$DC01)",
1036
+ "9th-X-bit sprite staging ($D010 batch commit)",
1037
+ "SID filter sweep (11-bit cutoff LFO; shared volume/mode register)",
1038
+ "beam-racing coarse scroll scheduled off the bottom IRQ",
1039
+ "transition repaints budgeted to text bands (full 880-cell paints freeze ~50 frames)",
1040
+ ],
640
1041
  },
641
1042
  platformer: {
642
1043
  main: "templates/platformer.c", runtime: C64_RUNTIME, runtimeDirs: C64_VENDOR_DIRS,
643
1044
  lang: C64_LANG, ext: ".prg",
644
- describe: "SIDE-SCROLLING platformer for C64 the fiddliest scroll of all the platforms, done for real. 80-col (640-px) world; the VIC-II only fine-scrolls 0-7 px in hardware ($D016 low 3 bits), so coarse motion re-renders the 40 visible columns of screen RAM ($0400) + color RAM ($D800) from a world map each time the camera crosses a char boundary. 38-column mode ($D016 bit 3 clear) masks the edge garbage column. The player is a VIC-II hardware sprite drawn in screen space (with the $D010 X-MSB handled); SID jump sfx. Joystick port 2, B1 jumps. See the C64 MENTAL_MODEL.md 'Horizontal scrolling'. Extend with enemies, goals, pickups.",
1045
+ describe: "TALUS TROT complete C64 side-scrolling platformer: title shell (port-2 fire = 1P, port-1 fire = 2P alternating turns, per-player score/lives), gravity + Q4.4 sub-pixel jump physics, one-way platforms, pits + spikes, coins + distance scoring, raster-IRQ split (fixed score bar over a fine ($D016) + coarse (screen-RAM shift) hardware-scrolled level), 2-voice SID music with the filter sweep + SFX. Hi-score persists via 1541 disk save when run from a .d64 (in-session as a bare .prg). KEY SCROLL FINDING: shifting both screen AND color RAM per coarse step crawls cc65; a STATIC row-based color texture + screen-RAM-only shift keeps the coarse scroll real-time (taught in-file).",
1046
+ players: "1-2 (alternating turns; P1 on joystick port 2, P2 on port 1)",
1047
+ sram: "1541 DISK SAVE — the honest C64 medium (no battery SRAM): the game writes a 2-byte record to a SEQ file 'HI' on drive 8 via the KERNAL (cbm_open/read/write), VICE commits it into the live .d64 (true-drive write-back). Requires running from a .d64 (state({op:exportDisk}) captures the save; reload restores it); as a bare .prg the save is a silent no-op (in-session only).",
1048
+ mechanics: ["gravity + Q4.4 sub-pixel jump", "one-way platforms", "pits + spikes (lethal)", "coins + distance scoring", "alternating-turns 2P", "title/play/game-over state machine"],
1049
+ techniques: [
1050
+ "raster-IRQ split (fixed bar over scrolling level)",
1051
+ "fine ($D016) + coarse (screen-RAM shift) hardware scroll",
1052
+ "two-layer static-color-texture trick (keeps coarse scroll real-time)",
1053
+ "9th-X-bit sprite staging",
1054
+ "SID filter sweep",
1055
+ "dual joystick-port reads with keyboard-conflict idiom",
1056
+ ],
645
1057
  },
646
1058
  puzzle: {
647
1059
  main: "templates/puzzle.c", runtime: C64_RUNTIME, runtimeDirs: C64_VENDOR_DIRS,
648
1060
  lang: C64_LANG, ext: ".prg",
649
- describe: "Match-3 falling-block puzzle. 6×12 grid in screen RAM (40×25 char matrix), C64 color codes. Rotate click + clear chime via SID.",
1061
+ describe: "MAGMA MATCH — complete C64 falling-trio versus puzzle: title/1P-marathon/2P-simultaneous-versus shell, falling-trio match-3 (4-direction clears, per-column gravity, cascade chains, 9 levels), hi-score persisted via 1541 disk save when run from a .d64 (in-session as a bare .prg), 2-voice SID music with the filter sweep + SFX, raster-IRQ split fixed HUD. The board is screen RAM ($0400) chars + color RAM ($D800) repainted via a CELL-DIFF (shadow buffers; only changed cells touch RAM) — dodging the C64 full-repaint freeze (taught in-file, the inverse of the NES vblank-queue famine). Real 2P simultaneous versus with garbage: a cascade chain erupts garbage rows into your rival's well; P1 on control port 2, P2 on control port 1.",
1062
+ players: "1-2 (2P = simultaneous versus, split boards; P1 port 2, P2 port 1)",
1063
+ sram: "1541 DISK SAVE — the honest C64 medium (no battery SRAM): the game writes a 2-byte record to a SEQ file 'HI' on drive 8 via the KERNAL (cbm_open/read/write), VICE commits it into the live .d64 (true-drive write-back). Requires running from a .d64 (state({op:exportDisk}) captures the save; reload restores it); as a bare .prg the save is a silent no-op (in-session only).",
1064
+ mechanics: ["falling-trio control", "match-3 in 4 directions", "cascade chains with multipliers", "garbage attack rows", "soft/hard drop + levels", "simultaneous split-board versus"],
1065
+ techniques: [
1066
+ "cell-diff screen/color-RAM repaint (shadow buffers; only changed cells)",
1067
+ "raster-IRQ split fixed HUD",
1068
+ "dual joystick-port reads with keyboard-conflict idiom",
1069
+ "SID filter sweep",
1070
+ "coloured border for render-health without a full-screen repaint freeze",
1071
+ ],
650
1072
  },
651
1073
  sports: {
652
1074
  main: "templates/sports.c", runtime: C64_RUNTIME, runtimeDirs: C64_VENDOR_DIRS,
653
1075
  lang: C64_LANG, ext: ".prg",
654
- describe: "Pong with 3 hardware sprites. Joystick port 2 = P1; AI on the right paddle. SID paddle-hit + wall-bounce + score sfx.",
1076
+ describe: "DELTA DUEL — complete C64 head-to-head court sports (Pong lineage): title shell with 1P-vs-beatable-CPU / 2P-SIMULTANEOUS-versus select (P1 control port 2, P2 control port 1), first-to-5 match flow into a result screen, beatable chase-AI CPU, a +/-1 PRNG deflection spin so idle 1P rallies provably END, best 1P-vs-CPU win-streak record persisted via 1541 disk save when run from a .d64 (in-session as a bare .prg), 2-voice SID music with the filter sweep + SFX, raster-IRQ split fixed HUD. Paddles + ball are VIC-II HARDWARE SPRITES (9th-X-bit staging for the right paddle past X=255); the court is static screen-RAM chars painted once per match (no per-frame repaint).",
1077
+ players: "1-2 (2P = simultaneous versus; P1 control port 2, P2 control port 1)",
1078
+ sram: "1541 DISK SAVE — the honest C64 medium (no battery SRAM): the game writes a 2-byte record to a SEQ file 'HI' on drive 8 via the KERNAL (cbm_open/read/write), VICE commits it into the live .d64 (true-drive write-back). Requires running from a .d64 (state({op:exportDisk}) captures the save; reload restores it); as a bare .prg the save is a silent no-op (in-session only). (record = longest 1P win streak vs the CPU).",
1079
+ mechanics: ["1P vs beatable chase-AI CPU", "2P simultaneous versus", "first-to-5 match flow + result screen", "PRNG deflection spin (rallies END)", "longest-win-streak record", "title/play/result state machine"],
1080
+ techniques: [
1081
+ "VIC-II hardware sprites (paddles + ball) with 9th-X-bit batch staging ($D010)",
1082
+ "raster-IRQ split fixed HUD over a static court",
1083
+ "dual joystick-port reads with keyboard-conflict idiom ($DC00/$DC01)",
1084
+ "SID filter sweep (11-bit cutoff LFO; shared volume/mode register)",
1085
+ "static char court (no per-frame repaint; dodges the full-repaint freeze)",
1086
+ ],
655
1087
  },
656
1088
  racing: {
657
1089
  main: "templates/racing.c", runtime: C64_RUNTIME, runtimeDirs: C64_VENDOR_DIRS,
658
1090
  lang: C64_LANG, ext: ".prg",
659
- describe: "3-lane top-down racer. LEFT/RIGHT switches lanes. SID lane-switch beep + crash noise.",
1091
+ describe: "VAPOR VECTOR — complete C64 top-down vertical road racer: title/1P-race/2P-split-lane-versus shell, vertical hardware scroll via $D011 YSCROLL fine-Y + software coarse row-shift with the static-color-texture trick (coarse shift touches only screen RAM → real-time), raster-IRQ split fixed HUD over the moving road, player cars as VIC-II hardware sprites. 1P: four lanes, UP/FIRE accelerate + DOWN brake (speed 1-5), 3 crashes end the run, best DISTANCE; 2P: real simultaneous split-lane versus (P1 control port 2 left two lanes / P2 control port 1 right two), first to wreck out loses. Best DISTANCE persists via 1541 disk save when run from a .d64 (KERNAL write to drive 8, committed to the live disk; in-session only as a bare .prg). SID music + filter sweep + SFX.",
1092
+ players: "1-2 (2P = simultaneous split-lane versus; P1 control port 2, P2 control port 1)",
1093
+ sram: "1541 DISK SAVE — the honest C64 medium (no battery SRAM): the game writes a 2-byte record to a SEQ file 'HI' on drive 8 via the KERNAL (cbm_open/read/write), VICE commits it into the live .d64 (true-drive write-back). Requires running from a .d64 (state({op:exportDisk}) captures the save; reload restores it); as a bare .prg the save is a silent no-op (in-session only). (record = best distance).",
1094
+ mechanics: ["lane steering", "speed control 1-5 (1P)", "best-distance (in-session)", "traffic dodging + crashes", "crash lives", "2P split-lane versus"],
1095
+ techniques: [
1096
+ "vertical hardware scroll ($D011 fine-Y + coarse row-shift, static-color-texture trick)",
1097
+ "raster-IRQ split fixed HUD over the moving road",
1098
+ "VIC-II hardware sprite player cars ($D000+)",
1099
+ "dual joystick-port reads with keyboard-conflict idiom ($DC00/$DC01)",
1100
+ "SID filter sweep + multi-voice music/SFX",
1101
+ ],
660
1102
  },
661
1103
  music_demo: {
662
1104
  main: "templates/music_demo.c", runtime: C64_MUSIC_RUNTIME, runtimeDirs: C64_VENDOR_DIRS,
@@ -752,56 +1194,110 @@ TEMPLATES.snes = {
752
1194
  main: "templates/shmup.c",
753
1195
  extraSources: [
754
1196
  { src: "templates/shmup-data.asm", dst: "data.asm" },
1197
+ { src: "templates/shmup-hdr.asm", dst: "hdr.asm" }, /* battery-SRAM cart header */
755
1198
  ],
756
1199
  runtime: SNES_SFX_RUNTIME,
757
1200
  runtimeDirs: SNES_PVSNESLIB_VENDOR_DIRS,
758
1201
  lang: "C (tcc-65816 + PVSnesLib)",
759
1202
  ext: ".sfc",
760
- describe: "Vertical-shmup scaffold for SNES. Player ship + 6 bullets + 6 enemies, wave spawner, AABB collisions, score. SFX (pew on fire, boom on hit) via the bundled SPC700 driver + sample bank.",
1203
+ describe: "SOLAR BULWARK complete SNES vertical shooter: title shell with 1P/2P co-op select, 2P SIMULTANEOUS co-op (P2 on controller 2, port-isolated), bullet/enemy pools, wave spawner, battery-SRAM hi-score at $70:0000 (bundled hdr.asm), SPC music + SFX with the init-race idiom, Mode 1 scrolling starfield.",
1204
+ players: "1-2 (simultaneous co-op; P2 on controller 2)",
1205
+ sram: "battery SRAM at $70:0000 (CARTRIDGETYPE $02 via bundled hdr.asm; magic+checksum), verified across hardReset",
1206
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "2P simultaneous co-op", "battery hi-score", "title/play/game-over state machine"],
1207
+ techniques: [
1208
+ "battery SRAM at $70:0000 (long-addressed asm helpers)",
1209
+ "SPC700 init-race avoidance",
1210
+ "Mode 1 BG scroll + BG text HUD",
1211
+ "oamSet/oamUpdate sprite pooling",
1212
+ ],
761
1213
  },
762
1214
  platformer: {
763
1215
  main: "templates/platformer.c",
764
1216
  extraSources: [
765
1217
  { src: "templates/platformer-data.asm", dst: "data.asm" },
1218
+ { src: "templates/platformer-hdr.asm", dst: "hdr.asm" }, /* battery-SRAM cart header */
766
1219
  ],
767
1220
  runtime: SNES_SFX_RUNTIME,
768
1221
  runtimeDirs: SNES_PVSNESLIB_VENDOR_DIRS,
769
1222
  lang: "C (tcc-65816 + PVSnesLib)",
770
1223
  ext: ".sfc",
771
- describe: "SIDE-SCROLLING platformer for SNES (PVSnesLib). Subpixel gravity + jump + land-on-top collision across a 512-px world. A camera follows the player; the BG scrolls in hardware via bgSetScroll(0, camX, 0) and the player sprite draws in screen space (worldX - camX), held screen-centered while the world moves under it. Jump SFX via the bundled SPC700 driver. NOTE: uses the PVSnesLib console (text) BG, so platforms are collision-only and the scroll shows as the on-BG text sliding — for visible tiled platform art across a wide world, build a tileset with gfx2snes + bgInitTileSet on a 64-wide map and stream tilemap columns into VRAM during vblank. See the SNES MENTAL_MODEL.md 'Horizontal scrolling'. BUILD: needs language:'c', snes_sfx_data.asm in sources, apu_blob.bin as a binary include, and snes_sfx.c/.h in includePaths (all scaffolded by createGame).",
1224
+ describe: "CRAG CAPER — side-scrolling platformer to the full contract: subpixel gravity/jump physics, one-way platforms, pits + spikes, coins + distance scoring, alternating 2P turns (P2 on controller 2, per-player score and lives, GO-banner handoffs), battery-SRAM hi-score at $70:0000 (bundled hdr.asm, survives power cycles), SPC music + SFX, two-layer split (fixed HUD text layer over the scrolling level no raster tricks needed on SNES, taught vs the NES sprite-0 idiom).",
1225
+ players: "1-2 (alternating turns; P2 on controller 2)",
1226
+ sram: "battery SRAM at $70:0000 (CARTRIDGETYPE $02 via bundled hdr.asm; magic+checksum), verified across hardReset",
1227
+ mechanics: ["gravity-jump physics (sub-pixel)", "one-way platform collision", "one-way camera + world scroll", "pits + spike hazards", "coins + distance scoring", "alternating 2P turns"],
1228
+ techniques: [
1229
+ "two-layer split (fixed HUD BG over scrolling level)",
1230
+ "battery SRAM at $70:0000 (long-addressed asm helpers)",
1231
+ "SPC700 init-race avoidance",
1232
+ "telemetry block for headless verification",
1233
+ ],
772
1234
  },
773
1235
  puzzle: {
774
1236
  main: "templates/puzzle.c",
775
1237
  extraSources: [
776
1238
  { src: "templates/puzzle-data.asm", dst: "data.asm" },
1239
+ { src: "templates/puzzle-hdr.asm", dst: "hdr.asm" }, /* battery-SRAM cart header */
777
1240
  ],
778
1241
  runtime: SNES_SFX_RUNTIME,
779
1242
  runtimeDirs: SNES_PVSNESLIB_VENDOR_DIRS,
780
1243
  lang: "C (tcc-65816 + PVSnesLib)",
781
1244
  ext: ".sfc",
782
- describe: "Match-3 falling-block puzzle for SNES. 6×12 grid (text mode), rotate/soft-drop/hard-drop, 3+-in-a-row clears in all 4 directions with gravity + cascade chains. Rotate click + clear chime via bundled SPC700 sfx.",
1245
+ describe: "JEWEL JOUST — falling-trio match-3 to the full contract: 1P marathon + 2P SIMULTANEOUS split-board versus with garbage attacks (random matchable rows with one gap — a skilled victim digs out), 4-direction clears with cascade chains, battery-SRAM hi-score at $70:0000 (bundled hdr.asm, survives power cycles), SPC music + SFX, animated title jewel stripe.",
1246
+ players: "1-2 (2P = simultaneous versus, split boards with garbage attacks)",
1247
+ sram: "battery SRAM at $70:0000 (CARTRIDGETYPE $02 via bundled hdr.asm; magic+checksum), verified across hardReset",
1248
+ mechanics: ["falling-trio control", "match-3 in 4 directions", "cascade chains", "garbage attack rows", "split-board versus", "battery hi-score"],
1249
+ techniques: [
1250
+ "battery SRAM at $70:0000 (long-addressed asm helpers)",
1251
+ "SPC700 init-race avoidance",
1252
+ "BG tilemap board repaints + frozen-board game-over",
1253
+ "telemetry block for headless verification",
1254
+ ],
783
1255
  },
784
1256
  sports: {
785
1257
  main: "templates/sports.c",
786
1258
  extraSources: [
787
1259
  { src: "templates/sports-data.asm", dst: "data.asm" },
1260
+ { src: "templates/sports-hdr.asm", dst: "hdr.asm" }, /* battery-SRAM cart header */
788
1261
  ],
789
1262
  runtime: SNES_SFX_RUNTIME,
790
1263
  runtimeDirs: SNES_PVSNESLIB_VENDOR_DIRS,
791
1264
  lang: "C (tcc-65816 + PVSnesLib)",
792
1265
  ext: ".sfc",
793
- describe: "Two-player Pong on SNES. padsCurrent(0)/padsCurrent(1) wire both ports. Paddle-hit + score sfx via bundled SPC700 driver.",
1266
+ describe: "NET SURGE — complete versus court game: title shell with 1P-vs-CPU and 2P simultaneous versus (padsCurrent(0)/(1)), first to 5 with a result screen, beatable CPU, PRNG rally spin (deterministic rallies provably end), battery-SRAM best-CPU-win-streak record, SPC music + SFX with the init-race idiom.",
1267
+ players: "1-2 (1P vs CPU / 2P simultaneous versus)",
1268
+ sram: "battery SRAM at $70:0000 (CARTRIDGETYPE $02 via bundled hdr.asm; magic+checksum), verified across hardReset",
1269
+ mechanics: ["versus match flow (first-to-5, result screen)", "beatable CPU", "2P simultaneous input on both pads", "PRNG rally spin", "persistent best streak"],
1270
+ techniques: [
1271
+ "battery SRAM at $70:0000 (long-addressed asm helpers)",
1272
+ "SPC700 init-race avoidance (WaitForVBlank before first command)",
1273
+ "BG text HUD",
1274
+ "PRNG tick to break deterministic-rally limit cycles",
1275
+ ],
794
1276
  },
795
1277
  racing: {
796
1278
  main: "templates/racing.c",
797
1279
  extraSources: [
798
1280
  { src: "templates/racing-data.asm", dst: "data.asm" },
1281
+ { src: "templates/racing-hdr.asm", dst: "hdr.asm" }, /* battery-SRAM cart header — without it saves silently don't persist */
799
1282
  ],
800
1283
  runtime: SNES_SFX_RUNTIME,
801
1284
  runtimeDirs: SNES_PVSNESLIB_VENDOR_DIRS,
802
1285
  lang: "C (tcc-65816 + PVSnesLib)",
803
1286
  ext: ".sfc",
804
- describe: "Endless 3-lane top-down racer for SNES. LEFT/RIGHT switches lanes, obstacles slide down at growing speed. Two sprite tiles (player + enemy), score in BG text overlay.",
1287
+ describe: "EMBER CIRCUIT — the Mode 7 racer: a rotating-perspective ground plane (per-scanline matrix via 5 HDMA channels + a hardware-multiply table builder rebuilt every frame) — steer to yaw the camera and the whole world swings around your car. Ring circuit with lap timing, 1P time trial, 2P relay duel (P2 on controller 2), battery-SRAM best time (header-declared, survives power cycles), SPC music + surface SFX, Mode-1 HUD strip split above the Mode 7 ground.",
1288
+ players: "1-2 (relay duel — P1 laps, then P2 on controller 2; lower time wins)",
1289
+ sram: "battery SRAM at $70:0000 (CARTRIDGETYPE $02 via the bundled hdr.asm; magic+checksum, magic written last), verified across hardReset",
1290
+ mechanics: ["heading+speed driving model (fixed-point sin table)", "ring-track surface model (per-row half-width tables)", "quadrant lap counter", "lap timing + DNF cap", "persistent best time (torn-write-safe)", "title/ready/race/result state machine"],
1291
+ techniques: [
1292
+ "Mode 7 rotating perspective (HDMA matrix per 2-line band)",
1293
+ "BGMODE mid-frame split (Mode 1 HUD over Mode 7 ground)",
1294
+ "double-buffered HDMA tables flipped in vblank",
1295
+ "S-CPU hardware multiplier from asm",
1296
+ "Mode 7 VMAIN low/high byte streams (dmaCopyVram7)",
1297
+ "write-twice M7 register protocol",
1298
+ "HDMA-vs-OAM-DMA channel budgeting (PVSnesLib NMI owns ch 7)",
1299
+ "SPC700 driver init-race avoidance",
1300
+ ],
805
1301
  },
806
1302
  // R46: continuous-music demo on the SPC700 driver. Showcases
807
1303
  // sfx_music_play / sfx_music_stop alongside the existing sfx_play
@@ -930,7 +1426,18 @@ TEMPLATES.genesis = {
930
1426
  runtimeDirs: SGDK_RUNTIME_DIRS,
931
1427
  lang: SGDK_LANG,
932
1428
  ext: ".bin",
933
- describe: "Vertical-shmup genre scaffold. Player ship + 6 bullet slots + 6 enemy slots (object pools, no malloc), wave spawner, AABB collisions, score. Pre-allocated SAT slot ranges (0=player, 1-6=bullets, 7-12=enemies) so no flicker.",
1429
+ describe: "PULSAR RAMPART complete vertical shooter: title shell (1P/2P co-op select), 2P SIMULTANEOUS co-op (P2 on controller 2, palette-swap ship, shared lives + score), bullet/enemy pools on fixed SAT slots, wave spawner, SRAM hi-score, PSG music + SFX, and the Genesis vertical-shooter signature: VSCROLL_COLUMN per-column scroll — a three-depth falling starfield from one plane, under a hardware-fixed WINDOW-plane HUD.",
1430
+ players: "1-2 (simultaneous co-op; P2 on controller 2, shared lives + score)",
1431
+ sram: "header-declared cartridge SRAM at $200000 odd bytes (hi-score magic+checksum record), verified across hardReset",
1432
+ mechanics: ["object pools (bullets/enemies)", "wave spawner", "autofire cooldown", "2P simultaneous co-op", "shared-lives arcade scoring", "SRAM hi-score save"],
1433
+ techniques: [
1434
+ "per-column vertical scroll (VSCROLL_COLUMN three-depth starfield)",
1435
+ "window-plane fixed HUD",
1436
+ "sprite palette-swap second player",
1437
+ "cartridge SRAM via the $A130F1 mapper gate",
1438
+ "DMA_QUEUE vblank batching",
1439
+ "fixed SAT slot pooling (VDP_linkSprites chain)",
1440
+ ],
934
1441
  },
935
1442
  platformer: {
936
1443
  main: "templates/platformer.c",
@@ -938,7 +1445,18 @@ TEMPLATES.genesis = {
938
1445
  runtimeDirs: SGDK_RUNTIME_DIRS,
939
1446
  lang: SGDK_LANG,
940
1447
  ext: ".bin",
941
- describe: "SIDE-SCROLLING platformer for Genesis. Subpixel gravity + jump + land-on-top collision against a static platform list spread across a 512-px world. Camera follows the player; Plane A scrolls with the world via VDP_setHorizontalScroll, Plane B scrolls at half-rate for parallax. A=jump, d-pad=move. The world here is one 64-cell plane wide (no streaming) for a wider world, stream the column entering view each 8-px camera step (see Genesis MENTAL_MODEL.md 'How Sonic-style large maps REALLY work'). NOTE: it redraws nothing per frame (scroll is hardware) — for a from-scratch smooth-scroll/parallax starting point with ZERO loop-time tilemap writes, see template:'two_plane_parallax'. Extend with enemies, goals, pickups.",
1448
+ describe: "CINDER SPRINT complete side-scrolling platformer: title/1P/2P-alternating-turns shell, coins + distance scoring, SRAM hi-score, PSG music + SFX, and the Genesis signature dual-plane parallax (HSCROLL_TILE strip bands: plane A 1:1, plane B sky 1/8 + mountains 1/2) under a hardware-fixed WINDOW-plane HUD. Endless 512-px looping world, zero per-frame tilemap writes.",
1449
+ players: "1-2 (alternating turns; P2 on controller 2)",
1450
+ sram: "header-declared cartridge SRAM at $200000 odd bytes (hi-score magic+checksum record)",
1451
+ mechanics: ["scrolling camera", "gravity + one-way platform collision", "coin pickups + hazards", "distance scoring", "2P alternating turns", "SRAM hi-score save"],
1452
+ techniques: [
1453
+ "dual-plane parallax (HSCROLL_TILE strip bands)",
1454
+ "window-plane fixed HUD",
1455
+ "cartridge SRAM via the $A130F1 mapper gate",
1456
+ "DMA_QUEUE vblank batching",
1457
+ "SAT link-chain sprites (VDP_linkSprites)",
1458
+ "seamless 512-px plane-wrap camera",
1459
+ ],
942
1460
  },
943
1461
  two_plane_parallax: {
944
1462
  main: "templates/two_plane_parallax.c",
@@ -954,7 +1472,16 @@ TEMPLATES.genesis = {
954
1472
  runtimeDirs: SGDK_RUNTIME_DIRS,
955
1473
  lang: SGDK_LANG,
956
1474
  ext: ".bin",
957
- describe: "Match-3 falling-block puzzle genre scaffold. 6×12 grid, 1×3 active piece (3 colours), rotate via A, soft-drop on DOWN, hard-drop on START, 3+-in-a-row clears in all 4 directions with gravity + cascade chains. xorshift RNG so cell colours actually vary.",
1475
+ describe: "SHARD SIEGE — falling-trio match-3 to the full contract: 1P marathon with levels and cascade chains; 2P simultaneous split-board versus where chains send garbage rows (both wells update every frame — the Genesis has the VDP bandwidth, taught against the NES's 16-entry vblank budget). Whole-well repaints go as ONE DMA-queued rect. Battery-SRAM hi-score under a WINDOW-plane HUD, PSG music + SFX.",
1476
+ players: "1-2 (2P = simultaneous versus, split boards with garbage attacks)",
1477
+ sram: "header-declared cartridge SRAM at $200000 odd bytes (hi-score magic+checksum), verified across hardReset",
1478
+ mechanics: ["falling-trio control", "match-3 in 4 directions", "cascade chains with multipliers", "garbage attack rows", "levels", "split-board versus"],
1479
+ techniques: [
1480
+ "whole-well repaint as one DMA-queued tilemap rect",
1481
+ "window-plane fixed HUD (layout-change row clear)",
1482
+ "cartridge SRAM via the $A130F1 mapper gate",
1483
+ "PSG music + SFX",
1484
+ ],
958
1485
  },
959
1486
  sports: {
960
1487
  main: "templates/sports.c",
@@ -962,7 +1489,17 @@ TEMPLATES.genesis = {
962
1489
  runtimeDirs: SGDK_RUNTIME_DIRS,
963
1490
  lang: SGDK_LANG,
964
1491
  ext: ".bin",
965
- describe: "Two-player Pong via JOY_1 + JOY_2. AI fallback on port 2 when no second controller. Per-side score 0-9 rendered via VDP_drawText, ball bounces off paddles + court walls. Designed for the playtest window with hot-plugged controllers.",
1492
+ describe: "VOLT VOLLEY complete versus court game: title shell with 1P-vs-CPU and 2P simultaneous versus (P2 on controller 2), first to 5 with a result screen, beatable half-speed CPU, PRNG spin so rallies never loop, battery-SRAM best-CPU-win-streak record under a hardware-fixed WINDOW-plane HUD, PSG music + SFX.",
1493
+ players: "1-2 (1P vs CPU / 2P simultaneous versus)",
1494
+ sram: "header-declared cartridge SRAM at $200000 odd bytes (best win streak vs CPU, magic+checksum), verified across hardReset",
1495
+ mechanics: ["versus match flow (first-to-5, result screen)", "beatable CPU (speed-capped, dead zone, edge-deflection counterplay)", "2P simultaneous input", "PRNG rally spin + random serve angle", "persistent best streak"],
1496
+ techniques: [
1497
+ "window-plane fixed HUD over a plane-B band",
1498
+ "cartridge SRAM via the $A130F1 mapper gate",
1499
+ "SAT link-chain sprites + the sprite-mask x=0 footgun",
1500
+ "PRNG tick to break deterministic-rally limit cycles",
1501
+ "PSG music + SFX",
1502
+ ],
966
1503
  },
967
1504
  racing: {
968
1505
  main: "templates/racing.c",
@@ -970,7 +1507,17 @@ TEMPLATES.genesis = {
970
1507
  runtimeDirs: SGDK_RUNTIME_DIRS,
971
1508
  lang: SGDK_LANG,
972
1509
  ext: ".bin",
973
- describe: "Endless top-down 3-lane racer. LEFT/RIGHT switches lanes, obstacles slide down at increasing speed as score climbs. Game-over on collision with 60-frame freeze then auto-reset.",
1510
+ describe: "MIRAGE MILE — complete top-down road racer: VSCROLL road plane (hardware scroll — contrast with the NES 240-wrap taught in-file), a LIVE per-scanline HSCROLL_LINE heat-haze band (the only live line-scroll demo in the example set), WINDOW HUD, 1P speed control with best-distance battery SRAM (survives power cycles), 2P simultaneous split-lane versus on controller 2, PSG music + SFX.",
1511
+ players: "1-2 (2P = simultaneous versus, split lanes)",
1512
+ sram: "header-declared cartridge SRAM at $200000 odd bytes (best distance, magic+checksum), verified across hardReset",
1513
+ mechanics: ["lane steering", "speed control (1P)", "traffic dodging", "crash lives", "distance scoring", "split-lane versus"],
1514
+ techniques: [
1515
+ "vertical plane scroll (hardware VSCROLL)",
1516
+ "per-scanline HSCROLL_LINE heat-haze band (live line scroll)",
1517
+ "window-plane fixed HUD",
1518
+ "cartridge SRAM via the $A130F1 mapper gate",
1519
+ "PSG music + SFX",
1520
+ ],
974
1521
  },
975
1522
  shmup_2p: {
976
1523
  main: "templates/shmup_2p.c",
@@ -1026,27 +1573,76 @@ TEMPLATES.lynx = {
1026
1573
  shmup: {
1027
1574
  main: "templates/shmup.c", runtime: LYNX_RUNTIME, runtimeDirs: LYNX_VENDOR_DIRS,
1028
1575
  lang: LYNX_LANG, ext: ".lnx",
1029
- describe: "Vertical-shmup. Player + 4 bullets + 4 enemies (object pools), AABB collisions. MIKEY pew + boom sfx.",
1576
+ describe: "VOID PLUNGE — complete Lynx depth-dive shooter: title shell with attract demo, in-session hi-score, and the Lynx signature — Suzy HARDWARE sprite scaling: divers grow 2px to 20px as they approach (HSIZE/VSIZE recomputed per frame from depth, hitbox tracking the hardware scale, far kills pay more). MIKEY 4-voice music + SFX. Honest 1P (ComLynx needs a second Lynx); honest no-save (handy's libretro build exposes no SAVE_RAM — probed; cart 93Cxx EEPROM is the real-hardware path, future core round).",
1577
+ players: "1 (handheld — ComLynx multiplayer needs a second physical Lynx)",
1578
+ sram: "none — probe: regionSize(save_ram)=0, retro_get_memory(SAVE_RAM)=NULL; cart EEPROM named in-file as the real path (future core round)",
1579
+ mechanics: ["depth-corridor enemy dives (screen-Y as depth)", "scaled collision boxes (hitbox = hardware sprite size)", "range-weighted scoring", "projectile pool", "level ramp", "title/play/game-over state machine", "attract-mode demo"],
1580
+ techniques: [
1581
+ "Suzy hardware sprite scaling (SCB HSIZE/VSIZE 8.8, per-frame rescale)",
1582
+ "raw SCB authoring (literal 4bpp data, penpal remap) via tgi_ioctl(0)",
1583
+ "canonical TGI full-redraw loop (tgi_busy wait → draw → updatedisplay)",
1584
+ "vblank-deferred MIKEY voice writes",
1585
+ ],
1030
1586
  },
1031
1587
  platformer: {
1032
1588
  main: "templates/platformer.c", runtime: LYNX_RUNTIME, runtimeDirs: LYNX_VENDOR_DIRS,
1033
1589
  lang: LYNX_LANG, ext: ".lnx",
1034
- describe: "Single-screen platformer. Subpixel gravity + jump + 5 platforms. MIKEY jump sfx.",
1590
+ describe: "RIDGE ROMP — complete Lynx side-scrolling platformer: title shell with breathing-gem attract, gravity + Q4.4 sub-pixel jump physics, one-way platforms, lethal pits, spikes, coins + distance scoring, in-session hi-score, MIKEY 4-voice music + SFX. The Lynx signature — Suzy HARDWARE sprite scaling — runs throughout: collectible gems pulse 0.75x to 1.75x every frame (HSIZE/VSIZE in the SCB, grab box tracking the live scale) and the hero rides the same scaling SCB path. Scrolling is a software camera over a looping 384px column map (the Lynx has no hardware tilemap/scroll), redrawing the visible slice each frame. Honest 1P (ComLynx needs a second Lynx); honest no-save (handy's libretro build exposes no SAVE_RAM — probed; cart 93Cxx EEPROM is the real-hardware path, future core round).",
1591
+ players: "1 (handheld — ComLynx multiplayer needs a second physical Lynx)",
1592
+ sram: "none — probe: regionSize(save_ram)=0, retro_get_memory(SAVE_RAM)=NULL; cart EEPROM named in-file as the real path (future core round)",
1593
+ mechanics: ["gravity + Q4.4 sub-pixel jump physics", "one-way platforms (4-px landing window)", "lethal pits + spikes", "coins + scaling gems (live-size grab box)", "distance scoring", "software-camera scrolling over a looping column map", "title/play/game-over state machine"],
1594
+ techniques: [
1595
+ "Suzy hardware sprite scaling (SCB HSIZE/VSIZE 8.8, per-frame rescale — hero + pulsing gems)",
1596
+ "raw SCB authoring (literal 4bpp data, penpal remap) via tgi_ioctl(0)",
1597
+ "software-camera scrolling (no hardware tilemap — redraw visible slice per frame)",
1598
+ "canonical TGI full-redraw loop (tgi_busy wait -> draw -> updatedisplay)",
1599
+ "vblank-deferred MIKEY voice writes",
1600
+ ],
1035
1601
  },
1036
1602
  puzzle: {
1037
1603
  main: "templates/puzzle.c", runtime: LYNX_RUNTIME, runtimeDirs: LYNX_VENDOR_DIRS,
1038
1604
  lang: LYNX_LANG, ext: ".lnx",
1039
- describe: "Match-3 puzzle. 6×12 grid via tgi_bar. Rotate click + clear chime via MIKEY.",
1605
+ describe: "QUARRY QUELL — complete Lynx falling-trio match-3: 1P marathon with cascade chains + ramping levels (29->5 frames/row), a 6x12 well + slim HUD fit into 160x102, 4-direction 3+ clears with multiplied cascade scoring, in-session hi-score, MIKEY 4-voice music + SFX. The Lynx signature Suzy HARDWARE sprite scaling is woven in: the trio renders as scaling SCB sprites and every match fires a clear-pop scale flash (well gems swell >1.0x then ease back). One 8x8 gem art recoloured per-draw via the SCB penpal (1 art block, 3 colours); the well repaints cell-by-cell each frame (no hardware tilemap). Honest 1P (ComLynx needs a 2nd Lynx); honest no-save (handy exposes no SAVE_RAM — probed; cart EEPROM is the real path).",
1606
+ players: "1 (handheld — ComLynx multiplayer needs a second physical Lynx)",
1607
+ sram: "none — probe: regionSize(save_ram)=0, retro_get_memory(SAVE_RAM)=NULL; cart EEPROM named in-file as the real path",
1608
+ mechanics: ["falling-trio match-3", "4-direction 3+ clears", "gravity + cascade chains (multiplied score)", "ramping levels", "session hi-score", "title/play/game-over state machine"],
1609
+ techniques: [
1610
+ "Suzy hardware sprite scaling (SCB clear-pop flash on every match)",
1611
+ "one art block, 3 colours via SCB penpal recolour",
1612
+ "cell-by-cell well repaint (no hardware tilemap)",
1613
+ "canonical TGI full-redraw loop (tgi_busy -> draw -> updatedisplay)",
1614
+ "vblank-deferred MIKEY voice writes",
1615
+ ],
1040
1616
  },
1041
1617
  sports: {
1042
1618
  main: "templates/sports.c", runtime: LYNX_RUNTIME, runtimeDirs: LYNX_VENDOR_DIRS,
1043
1619
  lang: LYNX_LANG, ext: ".lnx",
1044
- describe: "Pong vs AI (handheld = one controller). MIKEY paddle-hit + wall-bounce + score sfx.",
1620
+ describe: "PULSE PARRY — complete Lynx versus court game fit to 160x102: 1P vs a beatable chase-AI CPU (deflect at the paddle edge to out-angle it), first-to-5 -> result screen, a PRNG +/-1 rally spin so an idle match provably ENDS, in-session win-streak record, MIKEY 4-voice music + SFX. The Lynx signature Suzy HARDWARE sprite scaling is woven in two ways: the ball is a scaling SCB sprite whose HSIZE/VSIZE tracks its speed (fast volleys loom larger), and the result screen pops the winner glyph to ~2.0x then eases back. Honest 1P (ComLynx needs a second physical Lynx); honest no-save (handy exposes no SAVE_RAM — probed; cart EEPROM is the real path).",
1621
+ players: "1 (handheld — ComLynx multiplayer needs a second physical Lynx)",
1622
+ sram: "none — probe: regionSize(save_ram)=0, retro_get_memory(SAVE_RAM)=NULL; in-session win-streak; cart EEPROM named in-file as the real path",
1623
+ mechanics: ["paddle vs ball court play", "edge-deflection angle", "beatable chase-AI CPU", "first-to-5 match flow + result screen", "PRNG rally spin (no limit cycle)", "in-session win-streak record"],
1624
+ techniques: [
1625
+ "Suzy hardware sprite scaling (ball scales with speed + result-screen pop)",
1626
+ "raw SCB authoring via tgi_ioctl(0)",
1627
+ "canonical TGI full-redraw loop (tgi_busy -> draw -> updatedisplay)",
1628
+ "xorshift16 PRNG to break deterministic-rally limit cycles",
1629
+ "vblank-deferred MIKEY voice writes",
1630
+ ],
1045
1631
  },
1046
1632
  racing: {
1047
1633
  main: "templates/racing.c", runtime: LYNX_RUNTIME, runtimeDirs: LYNX_VENDOR_DIRS,
1048
1634
  lang: LYNX_LANG, ext: ".lnx",
1049
- describe: "3-lane top-down racer. MIKEY lane-switch beep + crash noise.",
1635
+ describe: "DEPTH DODGE — complete Lynx top-down vertical road racer fit to 160x102: title shell with an approaching-car attract, 1P endless run with LEFT/RIGHT lane steering + UP/DOWN speed control (1-5), 3 lives, best-distance record, MIKEY 4-voice music + SFX. The Lynx signature Suzy HARDWARE sprite scaling is the CORE mechanic — obstacle cars enter tiny at the horizon and SWELL toward you as they approach (HSIZE/VSIZE recomputed per frame from screen-Y, the hitbox tracking the live hardware scale), an OutRun-ish pseudo-3D depth built from honest sprite scaling, NOT Mode-7. Result screen pops the glyph to ~2.0x then eases back. The road has no hardware tilemap/scroll: the full-redraw loop repaints it each frame and the lane-dash phase animation IS the scroll. Honest 1P (ComLynx needs a second physical Lynx); honest no-save (handy exposes no SAVE_RAM — probed; cart EEPROM is the real path).",
1636
+ players: "1 (handheld — ComLynx multiplayer needs a second physical Lynx)",
1637
+ sram: "none — probe: regionSize(save_ram)=0, retro_get_memory(SAVE_RAM)=NULL; in-session best distance; cart EEPROM named in-file as the real path",
1638
+ mechanics: ["3-lane top-down racing", "lane steering + speed control (1-5)", "depth-scaled approaching obstacles (hitbox = hardware sprite size)", "distance scoring", "3 crashes end the run", "in-session best-distance record", "attract-mode demo"],
1639
+ techniques: [
1640
+ "Suzy hardware sprite scaling for pseudo-3D depth (SCB HSIZE/VSIZE 8.8, per-frame rescale from screen-Y)",
1641
+ "raw SCB authoring (literal 4bpp data, penpal recolour) via tgi_ioctl(0)",
1642
+ "phase-animated road scroll (no hardware tilemap/scroll — redraw + dash phase per frame)",
1643
+ "canonical TGI full-redraw loop (tgi_busy wait -> draw -> updatedisplay)",
1644
+ "vblank-deferred MIKEY voice writes",
1645
+ ],
1050
1646
  },
1051
1647
  };
1052
1648
 
@@ -1134,7 +1730,18 @@ TEMPLATES.gba = {
1134
1730
  runtimeDirs: GBA_LIBTONC_RUNTIME_DIRS,
1135
1731
  lang: GBA_TONC_LANG,
1136
1732
  ext: ".gba",
1137
- describe: "Vertical shmup scaffold (Tonc). Player ship + 6 bullets + 6 enemies (fixed object pools), AABB collision, enemy wave spawner, TTE score readout. ~150 lines.",
1733
+ describe: "GYRE GUNNER vertical shooter built around the GBA's affine hardware: a rotating, zoom-pulsing vortex backdrop (affine BG2, Mode 1, the 8.8 matrix + reference-point pivot taught register-by-register) and a spinning, scale-pulsing 32x32 boss (OAM affine slot 0, double-size flag). Waves gate the boss fight; hi-score persists in cartridge SRAM ('SRAM_V' marker, byte-wide bus discipline), verified across power cycles. 1P (handheld — link-cable 2P not emulatable single-instance).",
1734
+ players: "1 (handheld; link-cable 2P not emulatable single-instance)",
1735
+ sram: "cartridge SRAM at 0x0E000000 ('SRAM_V' ROM marker for save-type detection; magic+checksum record), verified across hardReset",
1736
+ mechanics: ["projectile pools", "wave spawner", "AABB collision", "affine boss with HP + sine strafe + minions", "SRAM-persistent hi-score", "title/play/game-over state machine"],
1737
+ techniques: [
1738
+ "affine background (BG2PA-PD 8.8 matrix + BG2X/Y centered pivot, Mode 1)",
1739
+ "affine sprite (OAM affine slots, double-size flag)",
1740
+ "8bpp BG tiles + 1-byte affine map via VRAM-safe staging",
1741
+ "TTE palbank-15 coexistence (8bpp palette footgun)",
1742
+ "PSG music loop + SFX channel discipline",
1743
+ "lu_sin/lu_cos fixed-point math",
1744
+ ],
1138
1745
  },
1139
1746
  platformer: {
1140
1747
  main: "templates/platformer.c",
@@ -1142,7 +1749,17 @@ TEMPLATES.gba = {
1142
1749
  runtimeDirs: GBA_LIBTONC_RUNTIME_DIRS,
1143
1750
  lang: GBA_TONC_LANG,
1144
1751
  ext: ".gba",
1145
- describe: "SIDE-SCROLLING platformer for GBA (Tonc). Subpixel physics (1px = 16 subpixels), gravity + jump + land-on-top collision against platforms in a 512-px world. BG0 is a 64x32 map (whole world fits, no streaming); the camera follows the player via REG_BG0HOFS; the TTE HUD on BG1 stays fixed. For a world wider than 512 px, stream map columns as the camera advances (see GBA MENTAL_MODEL.md). Extend with enemies, goals, pickups.",
1752
+ describe: "GEAR GROTTO — complete GBA side-scrolling platformer: press-start title with battery-persistent cartridge-SRAM hi-score ('SRAM_V' marker, byte-wide bus, magic+checksum, verified across power cycles), gravity + Q.4 sub-pixel jump physics, one-way platforms, lethal pits, coins + distance scoring, DMA/PSG music + SFX. The GBA signature is an AFFINE OBJ hazard a spinning, scale-pulsing 32x32 gear (OAM affine slot 0, double-size, 8.8 matrix taught register-by-register). The scrolling tile level is a Mode-0 64x32 BG that wraps in hardware at 512 px (cam & 511 / col & 63) for a seamlessly looping endless run under a fixed TTE HUD. 1P by design link-cable 2P can't be emulated single-instance (stated honestly in-file).",
1753
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
1754
+ sram: "cartridge SRAM hi-score at 0x0E000000 ('SRAM_V' marker, byte-wide bus, magic+checksum; survives power cycles)",
1755
+ mechanics: ["gravity-jump physics (Q.4 fixed point)", "one-way platform collision via column map", "endless one-way runner camera", "lethal pits", "coin pickup + distance scoring", "spinning affine gear hazard"],
1756
+ techniques: [
1757
+ "OAM affine sprite (slot 0, double-size, 8.8 matrix)",
1758
+ "hardware-wrapping 64x32 BG as a seamless looping world (cam & 511)",
1759
+ "world-anchored sprite objects recycled ahead of the camera",
1760
+ "cartridge SRAM hi-score (SRAM_V marker + byte-wide bus)",
1761
+ "TTE HUD on a fixed second BG",
1762
+ ],
1146
1763
  },
1147
1764
  puzzle: {
1148
1765
  main: "templates/puzzle.c",
@@ -1150,7 +1767,17 @@ TEMPLATES.gba = {
1150
1767
  runtimeDirs: GBA_LIBTONC_RUNTIME_DIRS,
1151
1768
  lang: GBA_TONC_LANG,
1152
1769
  ext: ".gba",
1153
- describe: "Match-3 falling-block scaffold (Tonc). 6x12 grid drawn as BG tiles, 1x3 active piece with LEFT/RIGHT shift, A rotate, DOWN soft-drop, START hard-drop. Horizontal triples clear + score.",
1770
+ describe: "FACET FALL — complete GBA falling-jewel match-3: press-start title, 1P marathon (handheld — link-cable 2P not emulatable single-instance), falling-trio with 4-direction clears, cascade chains, levels that speed the fall, vivid faceted jewels (15-bit palette, one shape remapped to 3 colour slices), DMA/PSG music + SFX, persistent cartridge-SRAM hi-score ('SRAM_V' marker, byte-wide bus, magic+checksum, verified across power cycles). Teaches the BG0-tilemap well + the no-vblank-queue-famine repaint contrast vs the NES.",
1771
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
1772
+ sram: "cartridge SRAM hi-score at 0x0E000000 ('SRAM_V' marker, byte-wide bus, magic+checksum; survives power cycles)",
1773
+ mechanics: ["falling-trio control", "match-3 in 4 directions", "cascade chains with multipliers", "levels that speed the fall", "battery hi-score"],
1774
+ techniques: [
1775
+ "BG0 tilemap board (faceted jewels via 15-bit palette slices)",
1776
+ "full-tilemap repaint (no vblank queue needed — GBA bandwidth)",
1777
+ "cartridge SRAM hi-score (SRAM_V marker + byte-wide bus)",
1778
+ "libtonc key_hit/key_held edge input",
1779
+ "headless decode from VRAM/OAM/save_ram (GBA C globals not host-readable)",
1780
+ ],
1154
1781
  },
1155
1782
  sports: {
1156
1783
  main: "templates/sports.c",
@@ -1158,7 +1785,18 @@ TEMPLATES.gba = {
1158
1785
  runtimeDirs: GBA_LIBTONC_RUNTIME_DIRS,
1159
1786
  lang: GBA_TONC_LANG,
1160
1787
  ext: ".gba",
1161
- describe: "Pong scaffold (Tonc). Single-controller GBA right paddle is AI ball-tracker. 24px paddles built from 3 stacked 8x8 sprites, ball collisions, score 0-9 via TTE. Real 2P on GBA needs link cable (out of scope).",
1788
+ describe: "RALLY ROVER — complete GBA versus court game (Pong lineage): press-start title, 1P vs a beatable CPU (chases at a third your speed with a dead-zone — steep edge-deflections beat it), first-to-5 match flow into a result screen, a PRNG +/-1 rally spin so an idle match provably ENDS (the deterministic-versus footgun, taught in-file), DMA/PSG music + SFX, vivid 15-bit court (blue vs red teams via OBJ palbank, white net/rails/ball). Persistent RECORD = longest win streak vs the CPU in cartridge SRAM ('SRAM_V' marker, byte-wide bus, magic+checksum, verified across power cycles). KEY IDIOM: the score is surfaced onto hardware as BG score-pip tiles so headless verification reads it from VRAM (GBA C globals are not host-readable). 1P by design — link-cable 2P not emulatable single-instance (stated honestly in-file).",
1789
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
1790
+ sram: "cartridge SRAM record at 0x0E000000 — longest CPU-mode win streak ('SRAM_V' marker, byte-wide bus, magic+checksum; survives power cycles)",
1791
+ mechanics: ["versus match flow (first-to-5, result screen)", "beatable CPU opponent (speed-capped ball chase with dead zone)", "edge-hit ball deflection with PRNG spin", "serve pause + alternating serve angle", "battery win-streak record"],
1792
+ techniques: [
1793
+ "BG0 score-pip HUD (score surfaced to VRAM for headless decode)",
1794
+ "one paddle tile, two team colours via OBJ palbank",
1795
+ "xorshift16 PRNG spin to break deterministic-rally limit cycles",
1796
+ "cartridge SRAM record (SRAM_V marker + byte-wide bus)",
1797
+ "TTE HUD/labels on a fixed second BG",
1798
+ "headless decode from OAM/VRAM/save_ram (GBA C globals not host-readable)",
1799
+ ],
1162
1800
  },
1163
1801
  racing: {
1164
1802
  main: "templates/racing.c",
@@ -1166,7 +1804,17 @@ TEMPLATES.gba = {
1166
1804
  runtimeDirs: GBA_LIBTONC_RUNTIME_DIRS,
1167
1805
  lang: GBA_TONC_LANG,
1168
1806
  ext: ".gba",
1169
- describe: "Top-down 3-lane racer scaffold (Tonc). Player car bottom, obstacles spawn from top + slide down, L/R switches lanes, AABB crash detection, 60-frame freeze + reset. Score is frames-since-crash.",
1807
+ describe: "VERGE PILOT — complete GBA top-down road racer: press-start title, 1P endless race (handheld link-cable 2P not emulatable single-instance, stated honestly in-file), lane steering + A/B throttle, traffic dodging with crash/lives, vivid 15-bit colour, DMA/PSG music + SFX, persistent best distance in cartridge SRAM ('SRAM_V' marker, byte-wide bus, magic+checksum, verified across power cycles). The GBA signature is an AFFINE BG2 ROAD (Mode 1, the console's Mode-7 trick) that recedes/scrolls, scales with speed, and banks as you steer — the 8.8 matrix taught register-by-register (a single-matrix demo; a full per-scanline perspective floor is noted as the heavier next step).",
1808
+ players: "1 (handheld — link-cable 2P not emulatable single-instance)",
1809
+ sram: "cartridge SRAM best distance at 0x0E000000 ('SRAM_V' marker, byte-wide bus, magic+checksum; survives power cycles)",
1810
+ mechanics: ["lane steering", "A/B throttle", "traffic dodging", "crash + lives", "best-distance persistence"],
1811
+ techniques: [
1812
+ "affine BG2 road (Mode 1, 8.8 matrix: recede + scale-with-speed + bank)",
1813
+ "cartridge SRAM best-distance (SRAM_V marker + byte-wide bus)",
1814
+ "OBJ car steering across lanes",
1815
+ "DMA/PSG music + SFX",
1816
+ "headless decode from OAM/VRAM/save_ram (GBA C globals not host-readable)",
1817
+ ],
1170
1818
  },
1171
1819
  // Opt-in libgba path for users who prefer the devkitPro SDK or are
1172
1820
  // porting an existing libgba codebase.
@@ -1255,42 +1903,102 @@ TEMPLATES.atari2600 = {
1255
1903
  ext: ".a26",
1256
1904
  describe: "Gallery-shooter (Space-Invaders-shaped) done with the RIGHT TIA objects, not playfield 'barcode' bars: P0 = double-width cannon, P1 + NUSIZ1=%011 = a row of THREE hardware-replicated invaders (one GRP1 write draws all three), M0 = the player shot. Aliens march left/right and drop a step at the edges; fire with the joystick button. The honest 2600-idiomatic way to do this genre — extend by reusing P1 lower for shields or adding M1 as an alien bomb. Verified: marches + renders cannon/aliens/shot.",
1257
1905
  },
1258
- // ── Genre scaffolds ───────────────────────────────────────────────
1259
- // The 2600 maps cleanly onto only SOME of the five canonical genres.
1906
+ // ── Genre games (all five, complete to the contract) ───────────────
1260
1907
  // shmup + sports are the console's native idioms (Space Invaders /
1261
1908
  // Pong); racing (top-down) and platformer (single-screen) are honest,
1262
- // period-correct fits. puzzle (match-3) is deliberately ABSENT see
1263
- // the note after this block: a 6x12 multi-colour grid is not
1264
- // renderable on a tilemap-less, one-COLUPF-per-line, 2-player TIA, so
1265
- // shipping a "puzzle" key would mean shipping something that isn't a
1266
- // recognizable match-3. Genre id == template key (createGame maps 1:1).
1909
+ // period-correct fits. puzzle is a MEMORY MATCH-PAIRS game (TILE TWINS),
1910
+ // NOT match-3: a 6x12 multi-colour falling-block grid is not renderable
1911
+ // on a tilemap-less, one-COLUPF-per-line TIA — but a static, turn-based
1912
+ // match-pairs board drawn as full-width COLUPF bands IS a clean fit and
1913
+ // is a real puzzle. Genre id == template key (fork maps 1:1).
1267
1914
  shmup: {
1268
1915
  main: "templates/shmup.asm",
1269
1916
  runtime: [],
1270
1917
  lang: "6507 assembly (dasm)",
1271
1918
  ext: ".a26",
1272
- describe: "SHMUPthe 2600's flagship genre (Space Invaders / Galaxian / Demon Attack). Gallery shooter done with the RIGHT TIA objects: P0 = double-width cannon, P1 + NUSIZ1=%011 = a row of THREE hardware-replicated invaders (one GRP1 write draws all three), M0 = the player shot. Aliens march left/right and drop a step at the edges; fire with the joystick button. Same proven body as the `mini_invaders` template. Extend with M1 as an alien bomb or reuse P1 lower for shields.",
1919
+ describe: "FLAK FRENZY Atari 2600 gallery shooter (a genre the TIA suits) to the full contract: a drawn FLAK/FRENZY title banner (asymmetric playfield, not text mode), title/play/game-over state machine, P0 ship + P1 with NUSIZ replication for a 2x3 invader formation (one GRP1 write draws the whole replicated row) + M0 shot, TIA hardware-collision hit detection, score + in-session hi-score on the title (honest no-battery), TIA SFX + a title jingle + game-over tune on separate voices, RIOT-timer frame pacing, the SBC-#15 RESP/HMOVE positioning idiom, SWCHA per-check re-read discipline. 1P by design a gallery-shooter kernel already spends its scanline budget on the ship + replicated formation + shot (2P alternating turns left as a cheap fork).",
1920
+ players: "1 (honest — gallery-shooter kernel budget; 2P alternating turns left as a fork)",
1921
+ sram: "none — no persistent storage on real 2600 hardware; hi-score is in-session only",
1922
+ mechanics: ["player ship + shot", "NUSIZ-replicated invader formation", "TIA hardware-collision hit detection", "formation march + score", "session hi-score", "title/play/game-over state machine"],
1923
+ techniques: [
1924
+ "drawn asymmetric-playfield title banner (no text mode)",
1925
+ "P1 + NUSIZ replication (one write draws the row)",
1926
+ "SBC-#15 RESP/HMOVE fine positioning",
1927
+ "SWCHA per-check re-read discipline",
1928
+ "RIOT-timer frame pacing",
1929
+ "score-mode dual-color HUD + multi-voice TIA music/SFX",
1930
+ ],
1273
1931
  },
1274
1932
  sports: {
1275
1933
  main: "templates/sports.asm",
1276
1934
  runtime: [],
1277
1935
  lang: "6507 assembly (dasm)",
1278
1936
  ext: ".a26",
1279
- describe: "SPORTSPong, the 2600's archetypal sport (Combat / Video Olympics). Two 8-px paddles (P0 left, P1 right), one 2-px ball (BL), top+bottom walls via reflected playfield. Joystick UP/DOWN drives the left paddle; the right paddle is AI (chases the ball's Y). Blip on wall bounce, chime on score. Same proven body as the `paddle` template. Demonstrates multi-object positioning (RESP0/RESP1/RESBL) + the 2-line kernel. Add a real P2 on the second port (SWCHA low nibble) to make it head-to-head.",
1937
+ describe: "RAPID RALLY complete 2600 head-to-head paddle game: drawn title screen, 1P vs AI or 2P versus (port-1 stick drives the right paddle), rally counter, TIA SFX + title jingle, auto-return to title, IN-SESSION hi-score (no battery on real 2600 hardware stated honestly in-source). Teaches the machine itself: 2-line kernel, RESP positioning, SWCHA re-read discipline, score-mode dual color.",
1938
+ players: "1-2 (1P vs AI / 2P simultaneous versus)",
1939
+ sram: "none — the 2600 has no persistent storage on real hardware; hi-score is in-session only",
1940
+ mechanics: ["paddle versus (1P AI / 2P)", "rally counter", "score-to-limit match flow", "auto title return", "session hi-score"],
1941
+ techniques: [
1942
+ "2-line kernel (racing the beam)",
1943
+ "RESP0/RESP1/RESBL coarse+HMxx fine positioning",
1944
+ "SWCHA per-check re-read (both sticks, one register)",
1945
+ "score-mode dual-color HUD",
1946
+ "TIA sound effects + title jingle",
1947
+ ],
1280
1948
  },
1281
1949
  racing: {
1282
1950
  main: "templates/racing.asm",
1283
1951
  runtime: [],
1284
1952
  lang: "6507 assembly (dasm)",
1285
1953
  ext: ".a26",
1286
- describe: "RACING — top-down vertical-scroll lane racer, the honest 2600 racing idiom (Enduro-style; pseudo-3D road projection needs a per-line table the 4 KB/76-cycle starter budget can't spare). P0 = your car near the bottom (LEFT/RIGHT to weave), reflected playfield draws the two road rails + a dashed centre line that scrolls upward to convey speed, P1 + M0 = descending traffic/hazards you must dodge. Speed (and score) ramps the longer you survive; a TIA-collision crash flashes the screen red and resets your speed. Extend with M1 as a 3rd hazard or NUSIZ1 for two-abreast traffic.",
1954
+ describe: "SWERVE STREAK complete Atari 2600 top-down road racer to the full contract: a drawn SWERVE/STREAK title banner (asymmetric playfield, not text mode), title/play/game-over state machine, P0 player car (LEFT/RIGHT to weave) on a reflected-playfield road (PF0 rails + a PF2 centre dash that crawls DOWN every frame via a scroll-phase offset to convey speed), P1/M0 descending traffic you dodge, TIA hardware-collision crash, distance score + in-session hi-score on the title (honest no-battery), TIA engine/crash SFX + a title jingle + game-over tune, RIOT-timer pacing, SBC-#15 RESP positioning. 1P by design — the road kernel already spends its scanline budget on the road + your car + a rival + a hazard (2P best-distance alternating runs left as a fork). HONEST: the 2600 has no hardware scroll, so forward motion is the dashed line + descending traffic animated each frame.",
1955
+ players: "1 (honest — road kernel budget; 2P best-distance alternating runs left as a fork)",
1956
+ sram: "none — no persistent storage on real 2600 hardware; best distance is in-session only",
1957
+ mechanics: ["lane weaving", "descending traffic dodging", "TIA hardware-collision crash", "distance scoring + speed ramp", "session hi-score", "title/play/game-over state machine"],
1958
+ techniques: [
1959
+ "reflected-playfield road (PF0 rails + PF2 dash)",
1960
+ "dashed centre line scroll-phase (fake forward motion, no hw scroll)",
1961
+ "descending traffic objects (P1/M0)",
1962
+ "drawn asymmetric-playfield title banner",
1963
+ "SBC-#15 RESP/HMOVE positioning + SWCHA re-read discipline",
1964
+ "score-mode dual-color HUD + multi-voice TIA music/SFX",
1965
+ ],
1287
1966
  },
1288
1967
  platformer: {
1289
1968
  main: "templates/platformer.asm",
1290
1969
  runtime: [],
1291
1970
  lang: "6507 assembly (dasm)",
1292
1971
  ext: ".a26",
1293
- describe: "PLATFORMERSINGLE-SCREEN (Pitfall! / Montezuma / Kangaroo idiom). The 2600 has NO hardware scroll, no tilemap, 128 B RAM — a smooth side-scroller is not the honest fit (real games flip whole screens). This ships the genre CORE: fixed-point gravity + a jump arc (FIRE button), and land-on-top collision tested in CODE (not TIA collision, since you must know WHICH surface to stand on) against a 4-entry platform table drawn as horizontal playfield bars (the only TIA object wide enough to be a platform). Joystick walks L/R. Extend with ladders (UP/DOWN over a ladder x-span), an enemy on P1, a thrown rock on M0, or Pitfall-style screen-flipping at the edges. NOT a scroller single screen by design.",
1972
+ describe: "PERCH PATROL complete Atari 2600 single-screen platformer (Pitfall! / Montezuma / Kangaroo idiom) to the full contract: a drawn PERCH/PATROL title banner (asymmetric playfield, not text mode), title/play/game-over state machine, P0 hero with fixed-point gravity + a jump arc (FIRE), land-on-ledge collision tested in CODE against a per-row playfield LEVEL table (the same table the kernel draws, so picture and physics never disagree), a bouncing coin (BL) to grab and a patrolling spike (M0) to dodge via TIA hardware-collision, score + in-session hi-score on the title (honest no-battery), TIA SFX + a title jingle + game-over tune, RIOT-timer pacing, SBC-#15 RESP/HMOVE positioning. The 2600 has NO hardware scroll/tilemap the honest platformer is a FIXED screen (real games flip whole screens), so this one is too.",
1973
+ players: "1 (honest — single-screen kernel budget; an enemy/second hero is left as a fork)",
1974
+ sram: "none — no persistent storage on real 2600 hardware; hi-score is in-session only",
1975
+ mechanics: ["gravity + jump arc", "land-on-ledge collision in CODE (PF LEVEL table)", "coin pickup (BL) + spike dodge (M0) via TIA collision", "score + session hi-score", "title/play/game-over state machine"],
1976
+ techniques: [
1977
+ "per-row playfield LEVEL table as the level (code + picture share it)",
1978
+ "reflect-mode symmetric arena (author the left half only)",
1979
+ "drawn asymmetric-playfield title banner (no text mode)",
1980
+ "SBC-#15 RESP/HMOVE fine positioning + SWCHA re-read discipline",
1981
+ "RIOT-timer frame pacing",
1982
+ "score-mode dual-color HUD + multi-voice TIA music/SFX",
1983
+ ],
1984
+ },
1985
+ puzzle: {
1986
+ main: "templates/puzzle.asm",
1987
+ runtime: [],
1988
+ lang: "6507 assembly (dasm)",
1989
+ ext: ".a26",
1990
+ describe: "TILE TWINS — complete Atari 2600 memory match-pairs puzzle to the full contract: a drawn TILE/TWINS title banner (asymmetric playfield), title/play/game-over state machine, an 8-tile board (4 pairs) drawn as a vertical stack of full-width playfield BANDS (one COLUPF per band = per-tile color, the honest way to show distinct values on a tilemap-less TIA), a joystick UP/DOWN cursor with a bright separator-bar highlight, FIRE to flip a tile, match-clears with a chime + mismatch flip-back after a pause, a move counter + session best (fewest flips), TIA SFX + a title jingle + win tune, RIOT-timer pacing. A REAL puzzle (deliberate, turn-based, memory-driven) — not a reflex game — and a clean 2600 fit since a static turn-based board needs no per-frame motion. The board is a Fisher-Yates LFSR shuffle, fair every game.",
1991
+ players: "1 (turn-based memory puzzle; alternating-2P fewest-flips is left as a fork)",
1992
+ sram: "none — no persistent storage on real 2600 hardware; best is in-session only",
1993
+ mechanics: ["8-tile / 4-pair memory board", "cursor move + flip", "match-clear + mismatch flip-back", "Fisher-Yates board shuffle", "move counter + session best", "title/play/win state machine"],
1994
+ techniques: [
1995
+ "playfield BANDS as tiles (per-band COLUPF = per-tile color)",
1996
+ "separator-bar cursor highlight (lit gap line on the selected band)",
1997
+ "8-bit LFSR + Fisher-Yates shuffle",
1998
+ "drawn asymmetric-playfield title banner (no text mode)",
1999
+ "SWCHA active-low direction edge-detect (Up=bit4 Down=bit5)",
2000
+ "score-mode dual-color HUD + multi-voice TIA music/SFX",
2001
+ ],
1294
2002
  },
1295
2003
  };
1296
2004
 
@@ -1333,35 +2041,93 @@ TEMPLATES.atari7800 = {
1333
2041
  runtime: ATARI7800_SFX_RUNTIME,
1334
2042
  lang: "C (cc65)",
1335
2043
  ext: ".a78",
1336
- describe: "Per-object-DL shmup. Each game object (player, bullet, enemy) is one MARIA 5-byte DL header. 4 bullets + 4 enemies in one zone, with X mutated per frame. Demonstrates per-frame DL rebuild + how the 7800 differs from sprite-table consoles.",
2044
+ describe: "COMET FLURRY — dense-field meteor shooter built on MARIA's signature object quantity: 24 meteors + 2 ships + 4 shots = 30 independent display-list objects, beyond what the 2600 or stock NES can draw. 1P and 2P simultaneous co-op (shared life pool), score-scaled difficulty, two-voice TIA music with SFX voice-stealing, session hi-score (honest: the bundled prosystem core has no High Score Cart support — comments wire the real HSC path for a future core round).",
2045
+ players: "1-2 (simultaneous co-op; port-1 fire starts it)",
2046
+ sram: "none — 7800 persistence is the High Score Cart, unimplemented in the bundled core (SAVE_RAM size 0); in-session hi-score with the HSC path documented",
2047
+ mechanics: ["dense-swarm dodging", "twin-ship co-op (shared life pool)", "shot/meteor scoring (fast rocks pay more)", "score-scaled difficulty", "spawn-shield shimmer invulnerability", "session hi-score"],
2048
+ techniques: [
2049
+ "per-scanline display-list pool (120 one-line zones, 3-objects-per-line DMA budget)",
2050
+ "DLL zone repointing under DMA-off for state transitions",
2051
+ "RAM-canvas text via wide DL entries (no text mode)",
2052
+ "#pragma optimize(on) as the cc65 frame budget",
2053
+ "two-voice TIA music with SFX voice arbitration",
2054
+ "SWCHA nibble-order input idiom",
2055
+ ],
1337
2056
  },
1338
2057
  platformer: {
1339
2058
  main: "templates/platformer.c",
1340
2059
  runtime: ATARI7800_SFX_RUNTIME,
1341
2060
  lang: "C (cc65)",
1342
2061
  ext: ".a78",
1343
- describe: "Single-screen platformer in one zone. Subpixel gravity + jump + ground detection. Vertical movement faked via row-offset stamping into the player's 24-row canvas (Y is encoded by which row of the canvas the sprite starts at).",
2062
+ describe: "STRATA STRIDE — complete Atari 7800 single-screen platformer built on MARIA's signature object quantity: a multi-tier arena (long floor + lethal pit + three one-way slabs) of ledges, coins and spikes all drawn as display-list objects — more than a 2600 draws. Title/1P/2P-alternating-turns shell (P2 on joystick port 1, per-player score + lives), gravity + sub-pixel jump physics, coins with an all-collected bonus, two-voice TIA music with SFX voice-stealing, session hi-score. HONEST CAVEATS: MARIA has no hardware scroll so the arena is fixed single-screen; the bundled prosystem core has no High Score Cart, so hi-score is in-session only (the real HSC path is documented in-file).",
2063
+ players: "1-2 (alternating turns; port-1 fire selects 2P, per-player score + lives)",
2064
+ sram: "none — 7800 persistence is the High Score Cart, unimplemented in the bundled core (SAVE_RAM size 0); in-session hi-score with the HSC path documented",
2065
+ mechanics: ["gravity + sub-pixel jump", "one-way ledges", "lethal pit + spikes", "coin + all-collected-bonus scoring", "2P alternating turns (per-player score/lives)", "session hi-score", "title/play/game-over state machine"],
2066
+ techniques: [
2067
+ "per-scanline display-list pool (120 one-line zones, 3-objects-per-line DMA budget)",
2068
+ "ledges-as-objects (no tilemap / no hardware scroll)",
2069
+ "DLL zone repointing under DMA-off for state transitions",
2070
+ "RAM-canvas text via wide DL entries (no text mode)",
2071
+ "#pragma optimize(on) as the cc65 frame budget",
2072
+ "two-voice TIA music with SFX voice arbitration",
2073
+ "SWCHA nibble-order input idiom (both ports for 2P)",
2074
+ ],
1344
2075
  },
1345
2076
  puzzle: {
1346
2077
  main: "templates/puzzle.c",
1347
2078
  runtime: ATARI7800_SFX_RUNTIME,
1348
2079
  lang: "C (cc65)",
1349
2080
  ext: ".a78",
1350
- describe: "Match-3 falling-block puzzle. 6×12 grid via per-cell MARIA DL entries (one 5-byte header per filled cell). Three palettes for R/G/B colours. Active piece is 3 extra DL entries.",
2081
+ describe: "PIVOT PURGE — complete Atari 7800 falling-trio match-3: title/1P-marathon/2P-simultaneous-versus shell, 4-direction clears, cascade chains with multipliers, levels (1P speed-up), 2P split-board versus with capped garbage attacks (P1 port 0 / P2 port 1, both wells falling at once), two-voice TIA music with SFX voice-stealing, session hi-score. KEY MARIA IDIOM: a puzzle well is MARIA's worst case (6 cells = 6 objects on the same 8 scanlines, double the ~3/line DMA budget), so each well row is composited into a 14-byte RAM canvas and drawn as ONE wide 5-byte object per scanline which is what lets TWO wells fit for 2P. Well DLs rebuilt on board-change, the trio overlaid per frame (naive per-frame re-emit overran 60Hz ~19x). HONEST CAVEATS: no hardware tilemap; prosystem has no HSC, so hi-score is in-session only (HSC path documented in-file). #pragma optimize(on) is load-bearing.",
2082
+ players: "1-2 (2P simultaneous split-board versus; P1 port 0 / P2 port 1)",
2083
+ sram: "none — prosystem has no High Score Cart (SAVE_RAM size 0); in-session hi-score with the HSC path documented",
2084
+ mechanics: ["falling-trio match-3", "4-direction clears", "cascade chains (multiplied score)", "levels (1P speed-up)", "2P simultaneous versus split board", "garbage-row attacks", "session hi-score"],
2085
+ techniques: [
2086
+ "one-wide-object-per-well-row (frame baked into the RAM canvas, trio overlaid)",
2087
+ "REBUILD-vs-PATCH (well DLs built on board-change, trio per frame)",
2088
+ "per-scanline display-list pool + RAM3 line pushing for RAM fit",
2089
+ "#pragma optimize(on) as the cc65 frame budget",
2090
+ "two-voice TIA music with SFX voice arbitration",
2091
+ "SWCHA nibble-order input (both ports for 2P)",
2092
+ ],
1351
2093
  },
1352
2094
  sports: {
1353
2095
  main: "templates/sports.c",
1354
2096
  runtime: ATARI7800_SFX_RUNTIME,
1355
2097
  lang: "C (cc65)",
1356
2098
  ext: ".a78",
1357
- describe: "Two-player Pong on Atari 7800. Both joystick ports wired SWCHA bits 4-7 = P1, bits 0-3 = P2. AI fallback when P2 isn't plugged in. Three per-object MARIA DL entries (paddles + ball) drawn into tall thin canvases that fit in the 7800's 4 KB RAM.",
2099
+ describe: "FLUX FENCE complete Atari 7800 versus court game (Pong lineage): title/1P-vs-beatable-CPU/2P-simultaneous-versus shell (P2 on joystick port 1), first-to-5 match flow with a result screen, PRNG rally spin so an idle match always ENDS, two-voice TIA music with SFX voice-stealing, in-session record (longest 1P-vs-CPU win streak). The two paddles, the ball, and the dashed centre net are all MARIA display-list objects emitted into the per-scanline pool — a court is the sparse/easy case of the same object budget the 7800 shmup spends on a swarm. HONEST CAVEATS: no hardware tilemap; prosystem has no HSC, so the record is in-session only (the HSC path is documented in-file). #pragma optimize(on) is load-bearing.",
2100
+ players: "1-2 (1P vs beatable CPU; 2P simultaneous versus, P1 port 0 / P2 port 1)",
2101
+ sram: "none — prosystem has no High Score Cart (SAVE_RAM size 0); in-session win-streak record with the HSC path documented",
2102
+ mechanics: ["1P vs beatable CPU", "2P simultaneous versus", "first-to-5 match -> result screen", "PRNG rally spin (idle match ends)", "angle-deflection paddle physics", "in-session win-streak record"],
2103
+ techniques: [
2104
+ "paddles/ball/net as per-scanline display-list objects",
2105
+ "per-scanline display-list pool (120 one-line zones, 3-objects-per-line DMA budget)",
2106
+ "DLL zone repointing under DMA-off for state transitions",
2107
+ "RAM-canvas text via wide DL entries (no text mode)",
2108
+ "#pragma optimize(on) as the cc65 frame budget",
2109
+ "two-voice TIA music with SFX voice arbitration",
2110
+ "SWCHA nibble-order input (both ports for 2P)",
2111
+ ],
1358
2112
  },
1359
2113
  racing: {
1360
2114
  main: "templates/racing.c",
1361
2115
  runtime: ATARI7800_SFX_RUNTIME,
1362
2116
  lang: "C (cc65)",
1363
2117
  ext: ".a78",
1364
- describe: "Endless 3-lane top-down racer. Per-object MARIA DL pattern (same as the 7800 shmup) each game object is one 5-byte DL header pointing at a static tile in ROM. LEFT/RIGHT switches lanes, obstacle speed grows with score.",
2118
+ describe: "PISTON PINCH — complete Atari 7800 top-down road racer built on MARIA's signature object quantity: a thick descending traffic stream (up to 10 cars) + player car(s), all display-list objects. Title/1P-race/2P-simultaneous-split-lane-versus shell (P2 on joystick port 1, P1 left two lanes / P2 right two), 1P speed control (UP/A gas, DOWN/B brake, speed 1-4) banking best DISTANCE, 3 crashes end the run; 2P shares one road, first to wreck out loses. Two-voice TIA music + SFX voice-stealing. KEY 7800 IDIOM: MARIA has NO scroll register, so vertical road motion is FAKED — the centre lane DASHES march downward (per-frame DLL phase repoint, no scroll) and traffic descends as objects. HONEST CAVEATS: prosystem has no High Score Cart, so best distance is in-session only (HSC path documented in-file). #pragma optimize(on) is load-bearing.",
2119
+ players: "1-2 (2P simultaneous split-lane versus; P1 port 0 / P2 port 1)",
2120
+ sram: "none — prosystem has no High Score Cart (SAVE_RAM size 0); in-session best distance with the HSC path documented",
2121
+ mechanics: ["top-down lane racing", "1P speed control (gas/brake)", "descending traffic dodging", "best-distance (in-session)", "2P simultaneous split-lane versus", "crash/lives rules", "title/play/game-over state machine"],
2122
+ techniques: [
2123
+ "marching-dash fake scroll (no MARIA scroll register)",
2124
+ "descending-traffic object stream (MARIA quantity signature)",
2125
+ "per-scanline display-list pool (cars-as-objects, <=3/line DMA budget)",
2126
+ "DLL zone repointing under DMA-off for state transitions",
2127
+ "RAM-canvas text via wide DL entries (no text mode)",
2128
+ "#pragma optimize(on) as the cc65 frame budget",
2129
+ "SWCHA nibble-order input (both ports for 2P)",
2130
+ ],
1365
2131
  },
1366
2132
  music_demo: {
1367
2133
  main: "templates/music_demo.c",
@@ -1769,7 +2535,7 @@ Compiles **C89**, not C99/C11. Stick to:
1769
2535
  buildBlock = "```js\nbuild({ output: \"run\", platform: \"" + platform + "\", sourcePath: \"" + mainFilename + "\", frames: 240 })\n```";
1770
2536
  }
1771
2537
 
1772
- let filesSection = `## Files\n\n- \`${mainFilename}\` — your game's entry point.\n`;
2538
+ let filesSection = `- \`${mainFilename}\` — the game. Title screen, game loop, all the GAME LOGIC clay.\n`;
1773
2539
  if (tmpl?.runtime) {
1774
2540
  for (const { dst } of tmpl.runtime) {
1775
2541
  if (dst === "patch-header.js") {
@@ -1780,9 +2546,9 @@ Compiles **C89**, not C99/C11. Stick to:
1780
2546
  `(\`node patch-header.js game.gb\`) — a zero-install stand-in for RGBDS's rgbfix when you ` +
1781
2547
  `rebuild OUTSIDE romdev with stock SDCC. romdev's own builds fix the header automatically.\n`;
1782
2548
  } else if (dst.endsWith("_crt0.s")) {
1783
- filesSection += `- \`${dst}\` — startup assembly (reset/interrupt vectors, RAM init; routed as the crt0 by the project build). **You own this.**\n`;
2549
+ filesSection += `- \`${dst}\` — startup assembly (reset/interrupt vectors, RAM init; routed as the crt0 by the project build). **Load-bearing**: replacing a bundled crt0 once black-screened every project on a platform for a month. Edit with the platform TROUBLESHOOTING doc open.\n`;
1784
2550
  } else {
1785
- filesSection += `- \`${dst}\` — runtime helper. **You own this** edit or replace at will.\n`;
2551
+ filesSection += `- \`${dst}\` — runtime library (rendering/input/sound helpers the game calls). Yours to extend; the HARDWARE IDIOM markers inside say which parts are load-bearing.\n`;
1786
2552
  }
1787
2553
  }
1788
2554
  }
@@ -1799,7 +2565,41 @@ Compiles **C89**, not C99/C11. Stick to:
1799
2565
  // source" variant, shown second.
1800
2566
  const projectBuildBlock =
1801
2567
  "```js\nbuild({\n output: \"project\",\n platform: \"" + platform + "\",\n path: \"" + projPath + "\",\n outputPath: \"" + name + romExt + "\",\n})\n```";
1802
- const readme = `# ${title ?? name}\n\nA ${lang} project for ${platform}, scaffolded by romdev.\n\n${tmpl?.describe ? tmpl.describe + "\n\n" : ""}${filesSection}${c89Note}## Build + run with romdev\n\nThe whole project directory builds in ONE call — romdev infers the toolchain, crt0, and linker from the directory, so you don't pass a file manifest:\n\n${projectBuildBlock}\n\nAdd \`output:"run"\` instead of \`"project"\` to also load + run + screenshot in the same round trip. Re-run the exact same call after every edit.\n\n<details>\n<summary>Alternative: build from a hand-specified source manifest (when compiling edited loose source, not a project dir)</summary>\n\n${buildBlock}\n</details>\n\n## Iterating\n\n- Edit \`${mainFilename}\` (or any of the runtime / crt0 / cfg files — they're yours).\n- Re-run the \`build({output:"project"|"run", path})\` call above to see your changes — it builds + (for run) loads + runs + screenshots in one round trip.\n- Inspect at byte level: \`memory({op:"read"})\`, \`sprites({op:"inspect"})\`, \`palette({source:"live"})\`, \`background({view:"rendered"})\`.\n- Open a playtest window for human eyes: \`playtest({op:"open"})\` — returns immediately, the window follows your rebuilds, and the emulator stays live for every other tool.\n`;
2568
+ const readme = `# ${title ?? name}
2569
+
2570
+ **A working ${platform} starting point** (${lang}) — forked from the romdev \`${platform}/${template ?? "default"}\` example. It builds, runs, and renders RIGHT NOW, before you change a line.
2571
+
2572
+ This is SCAFFOLDING, not a finished game. The gameplay is deliberately thin — treat it as placeholder and make it yours. Its value is that the hard part is already done and working: the ${platform} boot sequence, hardware init, and APIs are wired up correctly, so you evolve a running ROM instead of getting a long chain of fragile setup right from a blank file.
2573
+
2574
+ ${tmpl?.describe ? tmpl.describe + "\n\n" : ""}## How to make it yours
2575
+
2576
+ Modify ONE thing at a time and re-run the build after each change — the working game is your regression oracle (it rendered before your edit; if it stops, your last edit broke it):
2577
+
2578
+ ${projectBuildBlock}
2579
+
2580
+ Use \`output:"run"\` to build + load + run + screenshot in one round trip. Don't start over in a blank file — retro bring-up is a chain of fragile hardware init with no partial credit; evolve this game instead, even into a very different game.
2581
+
2582
+ ## Marker legend (read before restructuring anything)
2583
+
2584
+ - \`/* ── HARDWARE IDIOM (load-bearing) ── */\` — this code dodges a documented hardware footgun (the comment says which). **Reshape your gameplay around these regions**; if you must change one, read the cited TROUBLESHOOTING entry first. Each block's header lists what it needs (interrupt hooks, memory regions, register modes) — that's also what a transplant into another game must satisfy.
2585
+ - \`/* ── GAME LOGIC (clay) ── */\` — enemy patterns, scoring, art, tuning. **Reshape freely** — this is where your game happens.
2586
+
2587
+ Need a technique this game doesn't have (another example does)? \`examples({op:"show", example:"<platform>/<name>", technique:"..."})\` extracts that example's marked block with its dependency header — graft it here instead of rewriting it.
2588
+
2589
+ ## Files
2590
+
2591
+ ${filesSection}${c89Note}<details>
2592
+ <summary>Alternative: build from a hand-specified source manifest (when compiling edited loose source, not a project dir)</summary>
2593
+
2594
+ ${buildBlock}
2595
+ </details>
2596
+
2597
+ ## Inspecting + playtesting
2598
+
2599
+ - Byte level: \`memory({op:"read"})\`, \`sprites({op:"inspect"})\`, \`palette({source:"live"})\`, \`background({view:"rendered"})\`.
2600
+ - No-vision render health: \`frame({op:"verify"})\` — "is the game actually rendering?" in one call.
2601
+ - Human eyes: \`playtest({op:"open"})\` — a live window that follows your rebuilds; the emulator stays available to every other tool.
2602
+ `;
1803
2603
  await fs.writeFile(path.join(projPath, "README.md"), readme, "utf-8");
1804
2604
  writtenFiles.push("README.md");
1805
2605
 
@@ -1950,62 +2750,230 @@ async function createGameCore({ platform, genre, name, path: projPath, title, ov
1950
2750
  return { ...result, genre, template: templateId };
1951
2751
  }
1952
2752
 
2753
+ // ── The examples tool — the fork-don't-create surface (0.29.0) ──────────────
2754
+ // "Scaffold" died as a concept: there are no empty frames, only complete
2755
+ // working example games. Making a new game = forking the nearest example and
2756
+ // modifying it. See internal plan: the weak-model case for this is that retro
2757
+ // bring-up is a long conjunction of fragile steps with zero partial credit —
2758
+ // modifying a working game converts "get 15 things right" into "change 2
2759
+ // while 13 keep working", with a bisectable regression oracle.
2760
+
2761
+ const CANONICAL_GENRES = ["shmup", "platformer", "puzzle", "sports", "racing"];
2762
+ const HANDHELDS = new Set(["gb", "gbc", "gba", "gg", "lynx"]);
2763
+
2764
+ // Mechanics inventory per genre — what an agent learns by forking each.
2765
+ // (Hardware-technique anchors get added per-game as the Complete Game
2766
+ // Contract lands; list derives the rest from the manifest.)
2767
+ const GENRE_MECHANICS = {
2768
+ shmup: ["scrolling field", "projectile pools", "enemy waves + spawning", "collision (point/rect)", "score + lives"],
2769
+ platformer: ["side-scrolling camera", "gravity + jump arc", "tile collision (walk/land/fall)", "world map"],
2770
+ puzzle: ["grid logic", "piece falling + lock", "match detection (4-dir)", "gravity cascades + chain scoring"],
2771
+ sports: ["versus court", "ball physics + paddle bounce", "2P input (second pad, AI fallback)", "serve/score states"],
2772
+ racing: ["forward-scrolling road", "lane steering", "obstacle spawning", "speed/crash states"],
2773
+ };
2774
+
2775
+ // Fork guidance for genres we don't ship — points at the nearest core loop.
2776
+ const UNCOVERED_GENRE_GUIDANCE =
2777
+ "No example matches your genre exactly? Fork the NEAREST CORE LOOP and reshape it: " +
2778
+ "RPG/adventure → puzzle (grid + state machines) or platformer (world + camera); " +
2779
+ "tower defense → shmup (spawning + projectiles); " +
2780
+ "card/board game → puzzle (grid + turn logic); " +
2781
+ "beat-em-up → platformer (movement + collision); " +
2782
+ "pinball/breakout → sports (ball physics). " +
2783
+ "Fork for the core loop; read other examples (op:'show') for techniques to graft.";
2784
+
2785
+ /** "<platform>/<template>" → {platform, template}; also accepts separate args. */
2786
+ function resolveExampleId({ example, platform, template }) {
2787
+ if (example) {
2788
+ const m = /^([a-z0-9]+)\/(.+)$/.exec(example);
2789
+ if (!m) throw new Error(`examples: bad example id '${example}' — use "<platform>/<name>" (e.g. "nes/shmup"). examples({op:'list'}) shows them all.`);
2790
+ return { platform: m[1], template: m[2] };
2791
+ }
2792
+ if (platform && template) return { platform, template };
2793
+ throw new Error("examples: pass `example` (\"nes/shmup\") or `platform` + `template`.");
2794
+ }
2795
+
2796
+ /** One list entry from a TEMPLATES manifest record (defaults derived). */
2797
+ function exampleEntry(platform, templateId, tmpl) {
2798
+ const isGame = CANONICAL_GENRES.includes(templateId);
2799
+ const players = tmpl.players ?? (templateId === "sports" && !HANDHELDS.has(platform) ? 2 : 1);
2800
+ return {
2801
+ example: `${platform}/${templateId}`,
2802
+ kind: tmpl.kind ?? (isGame ? "game" : "reference"),
2803
+ ...(isGame ? { genre: templateId } : {}),
2804
+ description: tmpl.describe ?? "",
2805
+ mechanics: tmpl.mechanics ?? (isGame ? GENRE_MECHANICS[templateId] : []),
2806
+ // Hardware techniques demonstrated, each with a file + marker anchor for
2807
+ // op:'show' extraction — populated per-game as the contract lands.
2808
+ techniques: tmpl.techniques ?? [],
2809
+ players,
2810
+ sram: tmpl.sram ?? false,
2811
+ };
2812
+ }
2813
+
2814
+ /** Extract HARDWARE IDIOM / GAME LOGIC marked blocks from source text. */
2815
+ function extractMarkedBlocks(text) {
2816
+ const blocks = [];
2817
+ const re = /\/\* ── (HARDWARE IDIOM|GAME LOGIC)([^\n]*?)── \*\/([\s\S]*?)(?=\/\* ── (?:HARDWARE IDIOM|GAME LOGIC)|$)/g;
2818
+ let m;
2819
+ while ((m = re.exec(text)) !== null) {
2820
+ blocks.push({ kind: m[1], header: m[2].trim(), body: m[3].trimEnd() });
2821
+ }
2822
+ return blocks;
2823
+ }
2824
+
1953
2825
  export function registerProjectTools(server, z) {
1954
2826
  server.tool(
1955
- "scaffold",
1956
- "Scaffold a new homebrew project, a genre-shaped game, or drop starter snippets. `op`: 'project' | 'game' | " +
2827
+ "examples",
2828
+ "The example-game library one buildable, rendering starting point per platform×genre, and the ONLY way to start " +
2829
+ "a new project: **never start from a blank file — fork the nearest example and modify it into your game, even a " +
2830
+ "very different game.** These are SCAFFOLDING, not showcases: the gameplay is intentionally thin (treat it as " +
2831
+ "placeholder and reshape it) — their value is that they already carry the platform's boot sequence, APIs, and " +
2832
+ "syntax wired up and WORKING, so you change 2 things while 13 keep working instead of getting 15 right from " +
2833
+ "nothing. (Retro bring-up is a long chain of fragile hardware init with zero partial credit; a working game is a " +
2834
+ "regression oracle.) `op`: 'list' | 'fork' | 'show' | " +
1957
2835
  "'snippets' | 'copySnippets'.\n" +
1958
- "'project': a new project dir starter main source + every runtime file the template needs (headers, crt0, " +
1959
- "linker .cfg) + README + .gitignore, SELF-CONTAINED so it rebuilds with stock cc65/sdcc elsewhere. `template` " +
1960
- "defaults to the platform's smallest visible-and-runnable program (most have hello_sprite/tile_engine too). " +
1961
- "`withSnippets:true` also drops every vetted snippet alongside main.\n" +
1962
- "'game': a genre-shaped game picks the right template + runtime + crt0 + linker for the `genre` (shmup / " +
1963
- "platformer / puzzle / sports / racing). Scaffolds a complete working ROM you build+run+screenshot in one " +
1964
- "round trip, then fill in gameplay on the known-good baseline.\n" +
1965
- "'snippets': browse/fetch a platform's vetted starter snippets `mode:'list'` (names only), 'get' (one, needs " +
1966
- "`snippetName`), 'getAll' (joined; needs `outputPath` or `inline:true`). 'copySnippets': write every snippet straight to `destinationDir` " +
1967
- "(bytes never pass through context); `include` whitelists a subset.",
2836
+ "'list': the mechanics mapevery example with its kind (game vs minimal reference), mechanics inventory, " +
2837
+ "hardware techniques demonstrated (with file+marker anchors for op:'show'), players, SRAM. Use it to pick the " +
2838
+ "example whose CORE LOOP is nearest your game; fork that one, then op:'show' OTHER examples for techniques to graft.\n" +
2839
+ "'fork': copy an example into a NEW project dir as YOUR game — sources + every runtime file + crt0 + linker cfg + " +
2840
+ "README, self-contained, renamed throughout (project name, game title where the code carries one). Builds and runs " +
2841
+ "before you change a line. Then: modify one thing at a time, re-running build({output:'run'}) after each.\n" +
2842
+ "'show': read a donor example WITHOUT forking it — a whole file, or one marked technique block (extracted by its " +
2843
+ "HARDWARE IDIOM marker, including the dependency header that says what the block needs to survive a transplant).\n" +
2844
+ "'snippets'/'copySnippets': the legacy vetted-snippet library (browse/fetch/copy). Prefer forking + grafting from " +
2845
+ "real games; snippets remain for one-off references.",
1968
2846
  {
1969
- op: z.enum(["project", "game", "snippets", "copySnippets"]).describe("new project; genre game; browse/fetch snippets; or copy snippets to a dir."),
1970
- platform: z.string().describe("Platform id (nes, gb, gbc, snes, genesis, sms, gg, c64, gba, lynx, atari7800, ...)."),
1971
- name: z.string().optional().describe("op=project/game: project name (used for output binary)."),
1972
- path: z.string().optional().describe("op=project/game: absolute path where the project dir is created."),
1973
- title: z.string().optional().describe("op=project/game: human-readable title in the README."),
1974
- overwrite: z.boolean().default(false).describe("op=project/game: allow writing into an existing non-empty dir. op=copySnippets: overwrite existing files (else skip them)."),
1975
- // project
1976
- template: z.string().optional().describe("op=project: template id ('default' | 'hello_sprite' | 'tile_engine' on NES/GB/GBC; 'default' elsewhere)."),
1977
- withSnippets: z.boolean().default(false).describe("op=project: also drop every vetted snippet alongside main (= scaffold copySnippets after)."),
1978
- verbose: z.boolean().default(false).describe("op=project/game: echo the FULL flat file manifest (incl. vendor/** toolchain copies) as `allFiles`. Default false — the response lists only project-OWNED files you edit (`files`) plus a `vendorFileCount`, since the vendored toolchain copies are on disk and never need echoing (they're 35 of 44 entries on NES, ~270 on SGDK Genesis). Set true only if you specifically need every path in the response."),
1979
- // game
1980
- genre: z.string().optional().describe("op=game: 'shmup' | 'platformer' | 'puzzle' | 'sports' | 'racing'."),
1981
- // snippets
1982
- mode: z.enum(["list", "get", "getAll"]).default("list").describe("op=snippets: 'list' (names), 'get' (one, needs name), 'getAll' (joined)."),
1983
- snippetName: z.string().optional().describe("op=snippets mode:'get': snippet name ('read_pad') or filename ('read_pad.s')."),
2847
+ op: z.enum(["list", "fork", "show", "snippets", "copySnippets"]).describe("list the library; fork an example into your game; show donor source/technique without forking; legacy snippets."),
2848
+ platform: z.string().optional().describe("op=list: filter to one platform. op=fork/show/snippets/copySnippets: platform id (or encode it in `example`)."),
2849
+ example: z.string().optional().describe("op=fork/show: example id as \"<platform>/<name>\" (e.g. \"nes/shmup\", \"gb/puzzle\") — from op:'list'."),
2850
+ template: z.string().optional().describe("op=fork/show: example name when passing `platform` separately (alias of the id's second half)."),
2851
+ name: z.string().optional().describe("op=fork: YOUR game's name (project dir naming, output binary, and the in-game title where the example carries one). Required."),
2852
+ path: z.string().optional().describe("op=fork: absolute path where the project dir is created. Required."),
2853
+ title: z.string().optional().describe("op=fork: human-readable title for the README (defaults to `name`)."),
2854
+ overwrite: z.boolean().default(false).describe("op=fork: allow writing into an existing non-empty dir. op=copySnippets: overwrite existing files."),
2855
+ verbose: z.boolean().default(false).describe("op=fork: echo the FULL flat file manifest incl. vendor/** (default: only the files you own + a vendorFileCount)."),
2856
+ file: z.string().optional().describe("op=show: which file of the example to read (default: the main source)."),
2857
+ technique: z.string().optional().describe("op=show: extract ONE marked technique block whose HARDWARE IDIOM header matches this string (case-insensitive substring), instead of the whole file."),
2858
+ // legacy snippets passthrough
2859
+ mode: z.enum(["list", "get", "getAll"]).default("list").describe("op=snippets: 'list' (names), 'get' (one, needs snippetName), 'getAll' (joined)."),
2860
+ snippetName: z.string().optional().describe("op=snippets mode:'get': snippet name."),
1984
2861
  language: z.string().optional().describe("op=snippets/copySnippets: filter 'c' | 'asm'."),
1985
2862
  outputPath: z.string().optional().describe("op=snippets mode:'getAll': write the joined snippets here (or inline:true)."),
1986
- inline: z.boolean().default(false).describe("op=snippets mode:'getAll': return `combined` in the response instead of writing."),
1987
- // copySnippets
1988
- destinationDir: z.string().optional().describe("op=copySnippets: directory to write each snippet into (created if needed)."),
1989
- include: z.array(z.string()).optional().describe("op=copySnippets: whitelist of bare snippet names to copy."),
2863
+ inline: z.boolean().default(false).describe("op=snippets mode:'getAll': return `combined` inline."),
2864
+ destinationDir: z.string().optional().describe("op=copySnippets: directory to write snippets into."),
2865
+ include: z.array(z.string()).optional().describe("op=copySnippets: whitelist of snippet names."),
1990
2866
  },
1991
2867
  safeTool(async (args) => {
1992
2868
  switch (args.op) {
1993
- case "project": {
1994
- if (!args.name || !args.path) throw new Error("scaffold({op:'project'}): `name` and `path` are required.");
1995
- return jsonContent(await createProjectImpl(args));
2869
+ case "list": {
2870
+ const platforms = args.platform ? [args.platform] : Object.keys(TEMPLATES);
2871
+ const examples = [];
2872
+ for (const p of platforms) {
2873
+ const t = TEMPLATES[p];
2874
+ if (!t) continue;
2875
+ for (const id of Object.keys(t)) examples.push(exampleEntry(p, id, t[id]));
2876
+ }
2877
+ // Games first (the forkable starting points), references after.
2878
+ examples.sort((a, b) => (a.kind === b.kind ? a.example.localeCompare(b.example) : a.kind === "game" ? -1 : 1));
2879
+ return jsonContent({
2880
+ count: examples.length,
2881
+ doctrine: "Fork the example whose CORE LOOP matches your game; op:'show' the others for techniques to graft. " +
2882
+ "Ranked: nearest fork alone > fork + one graft > fork + many grafts — prefer the leftmost that gets your game made.",
2883
+ uncoveredGenres: UNCOVERED_GENRE_GUIDANCE,
2884
+ examples,
2885
+ });
2886
+ }
2887
+ case "fork": {
2888
+ const { platform, template } = resolveExampleId(args);
2889
+ if (!args.name || !args.path) throw new Error("examples({op:'fork'}): `name` and `path` are required (your game's name + where to create it).");
2890
+ if (!TEMPLATES[platform]?.[template]) {
2891
+ const have = TEMPLATES[platform] ? Object.keys(TEMPLATES[platform]).join(", ") : "(no examples for this platform)";
2892
+ throw new Error(`examples({op:'fork'}): no example '${platform}/${template}'. This platform has: ${have}.`);
2893
+ }
2894
+ const result = await createProjectImpl({
2895
+ platform, template, name: args.name, path: args.path, title: args.title,
2896
+ overwrite: args.overwrite, verbose: args.verbose,
2897
+ });
2898
+ // Rename the game THROUGH: where the example carries a GAME_TITLE
2899
+ // define, stamp the new name so the title screen says YOUR game
2900
+ // (identity transfer is the cheap defense against base-game-concept
2901
+ // leakage — an agent working on "CAVERN RUN" treats leftover shmup
2902
+ // scoring as a bug in ITS game).
2903
+ let titleStamped = false;
2904
+ try {
2905
+ const fs = await import("node:fs/promises");
2906
+ const path = await import("node:path");
2907
+ const stamp = String(args.name).toUpperCase().replace(/[^A-Z0-9 \-]/g, "").slice(0, 16) || "MY GAME";
2908
+ for (const f of result.files ?? []) {
2909
+ if (!/\.(c|h|s|asm)$/i.test(f)) continue;
2910
+ const fp = path.join(result.path, f);
2911
+ let src;
2912
+ try { src = await fs.readFile(fp, "utf-8"); } catch { continue; }
2913
+ const re = /(#define\s+GAME_TITLE\s+")[^"]*(")/;
2914
+ if (re.test(src)) {
2915
+ await fs.writeFile(fp, src.replace(re, `$1${stamp}$2`), "utf-8");
2916
+ titleStamped = true;
2917
+ }
2918
+ }
2919
+ } catch { /* best-effort; the fork itself succeeded */ }
2920
+ return jsonContent({
2921
+ ...result,
2922
+ forkedFrom: `${platform}/${template}`,
2923
+ template,
2924
+ ...(CANONICAL_GENRES.includes(template) ? { genre: template } : {}),
2925
+ ...(titleStamped ? { gameTitle: true } : {}),
2926
+ note: `Forked ${platform}/${template} → '${args.name}'. It builds and runs RIGHT NOW — verify with the build({output:"run"}) call in its README before changing anything, then modify ONE thing at a time, re-running after each. The README's marker legend says which regions are hardware idiom (reshape gameplay around them) vs game logic (clay).`,
2927
+ });
1996
2928
  }
1997
- case "game": {
1998
- if (!args.genre || !args.name || !args.path) throw new Error("scaffold({op:'game'}): `genre`, `name`, `path` are required.");
1999
- return jsonContent(await createGameCore(args));
2929
+ case "show": {
2930
+ const { platform, template } = resolveExampleId(args);
2931
+ const tmpl = TEMPLATES[platform]?.[template];
2932
+ if (!tmpl) {
2933
+ const have = TEMPLATES[platform] ? Object.keys(TEMPLATES[platform]).join(", ") : "(none)";
2934
+ throw new Error(`examples({op:'show'}): no example '${platform}/${template}'. This platform has: ${have}.`);
2935
+ }
2936
+ const fs = await import("node:fs/promises");
2937
+ const path = await import("node:path");
2938
+ const { fileURLToPath } = await import("node:url");
2939
+ const baseDir = path.dirname(fileURLToPath(import.meta.url));
2940
+ const exDir = path.resolve(baseDir, "..", "..", "..", "examples");
2941
+ // Default file = the template's `main` source (relative to
2942
+ // examples/<platform>/). An explicit `file` resolves the same way.
2943
+ const rel = args.file ?? tmpl.main;
2944
+ if (!rel) throw new Error(`examples({op:'show'}): example '${platform}/${template}' has no default source — pass the file arg.`);
2945
+ const fp = path.resolve(exDir, platform, rel);
2946
+ if (!fp.startsWith(exDir)) throw new Error("examples({op:'show'}): file path escapes the examples directory.");
2947
+ let text;
2948
+ try { text = await fs.readFile(fp, "utf-8"); }
2949
+ catch { throw new Error(`examples({op:'show'}): can't read '${rel}' for ${platform}/${template}.`); }
2950
+ if (args.technique) {
2951
+ const blocks = extractMarkedBlocks(text).filter((b) => b.kind === "HARDWARE IDIOM");
2952
+ const hit = blocks.find((b) => b.header.toLowerCase().includes(args.technique.toLowerCase()));
2953
+ if (!hit) {
2954
+ return jsonContent({
2955
+ example: `${platform}/${template}`, technique: args.technique, found: false,
2956
+ availableTechniques: blocks.map((b) => b.header),
2957
+ note: blocks.length
2958
+ ? "No HARDWARE IDIOM block matches — availableTechniques lists this file's blocks."
2959
+ : "This example has no marked technique blocks yet (markers land as games reach the Complete Game Contract). op:'show' without `technique` returns the whole file.",
2960
+ });
2961
+ }
2962
+ return jsonContent({
2963
+ example: `${platform}/${template}`, technique: hit.header, found: true,
2964
+ code: hit.body,
2965
+ note: "The block header states its DEPENDENCIES (interrupt hooks, memory regions, register modes) — satisfy those in your game before transplanting the code.",
2966
+ });
2967
+ }
2968
+ return jsonContent({ example: `${platform}/${template}`, file: rel, source: text });
2000
2969
  }
2001
2970
  case "snippets":
2002
- // map snippetName -> name for the core's expected shape.
2003
2971
  return await starterSnippetsCore({ ...args, name: args.snippetName });
2004
2972
  case "copySnippets": {
2005
- if (!args.destinationDir) throw new Error("scaffold({op:'copySnippets'}): `destinationDir` is required.");
2973
+ if (!args.destinationDir) throw new Error("examples({op:'copySnippets'}): `destinationDir` is required.");
2006
2974
  return await copyStarterSnippetsCore({ ...args, overwrite: args.overwrite ?? true });
2007
2975
  }
2008
- default: throw new Error(`scaffold: unknown op '${args.op}'`);
2976
+ default: throw new Error(`examples: unknown op '${args.op}'`);
2009
2977
  }
2010
2978
  }),
2011
2979
  );