romdevtools 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,106 +1,154 @@
1
- /* ── puzzle.c — NES match-3 falling-block scaffold ──────────────────
1
+ /* ── puzzle.c — NES falling-gem versus puzzle (complete example game) ─────────
2
2
  *
3
- * A Columns/Tetris-ish baseline. Includes:
4
- * - 6x12 grid in CHR-RAM-rendered "blocks" (sprites, not BG, so we
5
- * can move pieces without an attribute-table refresh dance)
6
- * - 1 active piece (a 1x3 vertical column of three colored blocks)
7
- * - d-pad LEFT/RIGHT moves the piece, DOWN soft-drops, A rotates
8
- * (cycles the 3 colors of the active piece)
9
- * - Auto-fall every 30 frames (~0.5s)
10
- * - Match-3 horizontal detection — clears horizontally aligned
11
- * triples of the same color. Add vertical + diagonal as exercise.
3
+ * A COMPLETE, working game — title screen, 1P marathon and 2P simultaneous
4
+ * VERSUS modes, levels, score + persistent hi-score (battery SRAM), music +
5
+ * SFX, and a background-tile playfield driven through the queued VRAM path
6
+ * (the load-bearing trick of every NES puzzle game).
12
7
  *
13
- * Scope-shaped choice: we render every block as a sprite (one OAM
14
- * entry per occupied cell). 6×12 = 72 sprites max well under the 64
15
- * hardware sprite limit per scanline, but at the edge of NES per-frame
16
- * total. A real puzzle game would render the well in the BG nametable
17
- * and only the active piece as sprites. Keeping it all-sprites here
18
- * trades 8-sprites-per-scanline flicker for code simplicity. Profile
19
- * before optimizing.
8
+ * The game: a falling-trio match-3. A trio of gems falls into a 6x12 well; LEFT/RIGHT
9
+ * move it, A/B cycle its three colours, DOWN soft-drops. When it lands, any
10
+ * straight run of 3+ same-coloured gems (horizontal, vertical, or diagonal)
11
+ * clears; survivors fall and cascades chain for multiplied score.
12
+ *
13
+ * 2P VERSUS design (simultaneous, split board): two 6x12 wells side by side —
14
+ * P1 left, P2 right — each driven by its own controller, both falling at
15
+ * once. Clears ATTACK: every chain step you score sends one garbage row
16
+ * (random gems with one gap, capped at 4 per attack) rising from the bottom
17
+ * of the opponent's well. First player whose stack reaches the top loses.
18
+ * Both update each frame; the whole thing fits the budget because the boards
19
+ * are background tiles and only the two falling trios are sprites (6 OAM
20
+ * entries total).
21
+ *
22
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
23
+ * very different one. The markers tell you what's what:
24
+ * HARDWARE IDIOM (load-bearing) — dodges a documented NES footgun; reshape
25
+ * your gameplay around it (see TROUBLESHOOTING before changing).
26
+ * GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
27
+ *
28
+ * What depends on what:
29
+ * nes_runtime.{h,c} — rendering/input/sound/text/hi-score library.
30
+ * chr-ram-runtime.crt0.s — boot + NMI + iNES header (BATTERY bit feeds
31
+ * hiscore_load/save). Load-bearing; edit with TROUBLESHOOTING open.
32
+ *
33
+ * Frame budget (NTSC, 60fps): steady state is tiny — input + gravity for two
34
+ * pieces, ≤6 sprites, ≤11 queued VRAM bytes (one board row + one HUD number).
35
+ * The spike is resolve_board() at lock time (full 4-direction match scan over
36
+ * 72 cells in cc65 code): it can spill a frame or two past vblank. That's
37
+ * fine — the NMI keeps rendering and the queue keeps draining, so it shows
38
+ * as (at most) a one-frame hitch on the falling pieces, never corruption.
20
39
  */
21
40
 
22
41
  #include "nes_runtime.h"
23
42
 
24
- #define GRID_W 6
25
- #define GRID_H 12
26
- #define ORIGIN_X 80 /* px — leftmost column anchor */
27
- #define ORIGIN_Y 16 /* px — top row anchor */
28
- #define CELL_PX 8 /* one tile per cell */
43
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
44
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
45
+ #define GAME_TITLE "GEM DUEL"
29
46
 
30
- #define EMPTY 0
31
- #define COL_R 1
32
- #define COL_G 2
33
- #define COL_B 3
47
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
48
+ * Board geometry. The wells are placed on EVEN tile coordinates on purpose —
49
+ * see the attribute-table idiom below before moving them. */
50
+ #define GRID_W 6
51
+ #define GRID_H 12
52
+ #define WELL_TY 8 /* top tile row of the well interior */
53
+ #define WELL_1P_TX 12 /* 1P: single centered well (cols 12-17) */
54
+ #define WELL_VS_P1 4 /* 2P: P1 well cols 4-9 ... */
55
+ #define WELL_VS_P2 22 /* P2 well cols 22-27 (split board) */
34
56
 
35
- #define TILE_BLOCK_R 1
36
- #define TILE_BLOCK_G 2
37
- #define TILE_BLOCK_B 3
57
+ #define EMPTY 0 /* cell colours 1..3 = white/green/red */
38
58
 
39
- /* ── Tiles: 3 colored 8x8 blocks ─────────────────────────────────── */
59
+ /* ── GAME LOGIC (clay reshape freely) ──────────────────────────────────────
60
+ * Tile art. Each 8x8 tile = 16 bytes: 8 plane-0 rows then 8 plane-1 rows
61
+ * (2bpp — plane0-only pixels use colour 1, plane1-only colour 2, both = 3).
62
+ * KEY TRICK: the three gem tiles are the SAME shape on different planes, so
63
+ * a cell changes colour by changing its TILE — no attribute-table rewrite
64
+ * (attributes cover 16x16 px, way coarser than one 8x8 cell). */
40
65
  static const uint8_t tile_blank[16] = { 0 };
41
- /* All three blocks use the same shape, but different palette indices.
42
- * We achieve that by varying the plane bits: plane0=1 → idx 1 (R),
43
- * plane1=1 → idx 2 (G), both planes set → idx 3 (B). */
44
- static const uint8_t tile_block_r[16] = {
45
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
66
+ static const uint8_t tile_gem1[16] = { /* colour 1 (white) */
67
+ 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
46
68
  0, 0, 0, 0, 0, 0, 0, 0,
47
69
  };
48
- static const uint8_t tile_block_g[16] = {
70
+ static const uint8_t tile_gem2[16] = { /* colour 2 (green) */
49
71
  0, 0, 0, 0, 0, 0, 0, 0,
50
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
72
+ 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
51
73
  };
52
- static const uint8_t tile_block_b[16] = {
53
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
54
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
74
+ static const uint8_t tile_gem3[16] = { /* colour 3 (red) */
75
+ 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
76
+ 0x3C, 0x7E, 0xFF, 0xFF, 0xFF, 0xFF, 0x7E, 0x3C,
55
77
  };
56
- /* BG scenery tiles painted into the nametable so the playfield reads as a
57
- * real machine on boot (without them the screen is just sprites on flat
58
- * black). All live in the BACKGROUND pattern table ($1000); the block tiles
59
- * above are sprites ($0000).
60
- *
61
- * BG_WALL — solid bordered block (idx 1, steel blue): the well frame.
62
- * BG_BRICK — a brick/dither pattern (idx 2) tiling the cabinet wall that
63
- * surrounds the well, so the whole screen is covered.
64
- * BG_INNER — a faint grid speck (idx 3) lining the inside of the well so
65
- * empty cells read as a recessed playfield, not a black hole. */
66
- static const uint8_t tile_wall[16] = {
67
- 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
68
- 0, 0, 0, 0, 0, 0, 0, 0,
78
+ /* BG furniture (background pattern table $1000 separate from the sprite
79
+ * table at $0000; the runtime's PPUCTRL setup makes that split). */
80
+ static const uint8_t tile_wall[16] = { /* well frame, colour 3 */
81
+ 0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF,
82
+ 0xFF, 0xFF, 0xE7, 0xC3, 0xC3, 0xE7, 0xFF, 0xFF,
69
83
  };
70
- static const uint8_t tile_brick[16] = {
71
- 0, 0, 0, 0, 0, 0, 0, 0, /* plane 0 clear */
72
- 0xFF, 0x80, 0x80, 0x80, 0xFF, 0x08, 0x08, 0x08, /* plane 1 → colour 2 brick */
84
+ static const uint8_t tile_dither[16] = { /* cabinet backdrop, colour 2 */
85
+ 0, 0, 0, 0, 0, 0, 0, 0,
86
+ 0x55, 0x00, 0xAA, 0x00, 0x55, 0x00, 0xAA, 0x00,
73
87
  };
74
- static const uint8_t tile_inner[16] = {
75
- 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, /* plane 0 specks */
76
- 0x88, 0x00, 0x00, 0x00, 0x88, 0x00, 0x00, 0x00, /* plane 1 too → colour 3 */
88
+ static const uint8_t tile_inner[16] = { /* empty-cell speck, colour 1 */
89
+ 0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
90
+ 0, 0, 0, 0, 0, 0, 0, 0,
77
91
  };
78
- #define BG_WALL 1 /* BG tile index 1 → uploaded to $1010 */
79
- #define BG_BRICK 2 /* BG tile index 2 → uploaded to $1020 */
80
- #define BG_INNER 3 /* BG tile index 3 → uploaded to $1030 */
92
+ #define BG_WALL 1
93
+ #define BG_DITHER 2
94
+ #define BG_INNER 3
95
+ #define BG_GEM_BASE 4 /* BG tiles 4/5/6 = gem colours 1/2/3 */
81
96
 
82
97
  static const uint8_t palette[32] = {
83
- /* BG0: backdrop near-black, wall = steel blue (idx1), brick = brown
84
- * (idx2), inner grid = dark grey (idx3). */
85
- 0x0F, 0x11, 0x17, 0x00,
86
- 0x0F, 0x11, 0x17, 0x00,
87
- 0x0F, 0x11, 0x17, 0x00,
88
- 0x0F, 0x11, 0x17, 0x00,
89
- /* Sprite palette 0: idx1=red, idx2=green, idx3=blue */
90
- 0x0F, 0x16, 0x1A, 0x12,
91
- 0x0F, 0x16, 0x1A, 0x12,
92
- 0x0F, 0x16, 0x1A, 0x12,
93
- 0x0F, 0x16, 0x1A, 0x12,
98
+ /* BG pal 0 = WELL INTERIOR: gem colours (white/green/red on black).
99
+ * BG pal 1 = everything else: white text, dark-grey dither, blue frame.
100
+ * The attribute table below assigns pal 0 to the wells, pal 1 elsewhere. */
101
+ 0x0F, 0x30, 0x2A, 0x16,
102
+ 0x0F, 0x30, 0x00, 0x11,
103
+ 0x0F, 0x30, 0x00, 0x11,
104
+ 0x0F, 0x30, 0x00, 0x11,
105
+ /* Sprite pal 0 mirrors BG pal 0 so the falling trio matches locked gems. */
106
+ 0x0F, 0x30, 0x2A, 0x16,
107
+ 0x0F, 0x30, 0x2A, 0x16,
108
+ 0x0F, 0x30, 0x2A, 0x16,
109
+ 0x0F, 0x30, 0x2A, 0x16,
94
110
  };
95
111
 
96
- /* ── State ───────────────────────────────────────────────────────── */
97
- static uint8_t grid[GRID_H][GRID_W]; /* 0 = empty, 1..3 = color */
98
- static uint8_t piece_x = GRID_W / 2; /* column 0..GRID_W-1 */
99
- static int8_t piece_y = -3; /* row; piece is 3 cells tall above this y */
100
- static uint8_t piece_col[3] = { COL_R, COL_G, COL_B }; /* top, mid, bot */
101
- static uint8_t fall_timer = 0;
102
- static uint16_t rng = 0xBEEF;
112
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
113
+ * Big arrays live OUTSIDE the linker's RAM area. The chr-ram-runtime preset
114
+ * places C BSS in $0300-$04FF (512 bytes), and the runtime's own statics
115
+ * (256-byte shadow attribute table + VRAM queue) already eat most of it —
116
+ * two 72-byte boards plus a 72-byte match mask would overflow the segment
117
+ * and the LINK fails. The preset reserves $0500-$05FF as the USER SCRATCH
118
+ * PAGE for exactly this (the linker never places anything there); these
119
+ * three arrays use 216 of its 256 bytes. DO NOT stray past $05FF: the cc65
120
+ * C parameter stack owns $0600-$06FF and the music driver's scratch page
121
+ * is $0700-$07FF — writes there corrupt live state silently. Bonus: fixed
122
+ * addresses make the boards trivially inspectable from the debugger
123
+ * (P1 board at $0500, P2 at $0548, match mask at $0590). */
124
+ #define grid_of(p) ((uint8_t (*)[GRID_W])((p) ? 0x0548 : 0x0500))
125
+ #define matched ((uint8_t (*)[GRID_W])0x0590)
126
+
127
+ /* ── GAME LOGIC (clay — reshape freely) ── small state (fits normal BSS). */
128
+ #define ST_TITLE 0
129
+ #define ST_PLAY 1
130
+ #define ST_OVER 2
131
+ static uint8_t state;
132
+ static uint8_t two_player; /* mode chosen on the title screen */
133
+ static uint8_t well_tx[2]; /* left tile column of each well */
134
+ static uint8_t piece_x[2]; /* falling trio: column 0..5 */
135
+ static int8_t piece_y[2]; /* row of its TOP cell (<0 = above rim) */
136
+ static uint8_t piece_col[2][3]; /* trio colours, top to bottom */
137
+ static uint8_t fall_t[2]; /* frames until next gravity step */
138
+ static uint8_t prev_pad[2]; /* for edge-triggered input */
139
+ static uint16_t score[2];
140
+ static uint16_t hiscore;
141
+ static uint16_t cleared_total; /* 1P: gems cleared, drives the level */
142
+ static uint8_t level; /* 1P: 1..9, speeds up the fall */
143
+ static uint16_t dirty_rows[2]; /* bitmask: board rows needing repaint */
144
+ static uint8_t hud_dirty[2]; /* score (or level) number needs redraw */
145
+ static uint8_t drain_turn; /* which player's row repaints this frame */
146
+ static uint16_t rng = 0xACE1;
147
+
148
+ #define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
149
+ #define GARBAGE_CAP 4 /* max garbage rows per attack */
103
150
 
151
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
104
152
  static uint8_t random8(void) {
105
153
  uint16_t r = rng;
106
154
  r ^= r << 7;
@@ -110,184 +158,517 @@ static uint8_t random8(void) {
110
158
  return (uint8_t)r;
111
159
  }
112
160
 
113
- static void random_piece_colors(void) {
114
- piece_col[0] = 1 + (random8() % 3);
115
- piece_col[1] = 1 + (random8() % 3);
116
- piece_col[2] = 1 + (random8() % 3);
161
+ /* cell colour → BG tile (empty cells show the faint speck, not raw black,
162
+ * so the well reads as a recessed playfield). */
163
+ static uint8_t bg_tile_for(uint8_t col) {
164
+ return col ? (uint8_t)(BG_GEM_BASE - 1 + col) : BG_INNER;
117
165
  }
118
166
 
119
- static uint8_t tile_for_color(uint8_t col) {
120
- switch (col) {
121
- case COL_R: return TILE_BLOCK_R;
122
- case COL_G: return TILE_BLOCK_G;
123
- case COL_B: return TILE_BLOCK_B;
124
- default: return 0;
125
- }
167
+ static void mark_row_dirty(uint8_t p, int8_t r) {
168
+ if (r >= 0 && r < GRID_H) dirty_rows[p] |= (uint16_t)1 << r;
126
169
  }
127
-
128
- static void spawn_piece(void) {
129
- piece_x = GRID_W / 2;
130
- piece_y = -3;
131
- random_piece_colors();
170
+ static void mark_all_dirty(uint8_t p) {
171
+ dirty_rows[p] = 0x0FFF; /* all 12 rows */
132
172
  }
133
173
 
134
- /* Lock current piece into the grid + check for match-3 horizontals. */
135
- static void lock_piece(void) {
136
- uint8_t r, c, run, run_col;
137
- int8_t i;
138
- /* Drop the 3 cells into the grid. */
139
- for (i = 0; i < 3; i++) {
140
- int8_t y = piece_y + i;
141
- if (y >= 0 && y < GRID_H) {
142
- grid[y][piece_x] = piece_col[i];
174
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
175
+ * The board is BACKGROUND TILES, updated only through the QUEUED path
176
+ * (tile_set / text_draw). The NMI drains at most 16 queue entries per
177
+ * vblank — that is the entire write bandwidth you get while rendering is on.
178
+ * NEVER vram_unsafe_set while rendering: raw $2007 traffic mid-frame
179
+ * corrupts the PPU address latch and shears the screen.
180
+ *
181
+ * So repaints are BUDGETED: board changes mark rows dirty, and this drainer
182
+ * repaints ONE row (6 cells) + ONE HUD number (5 digits) per frame — 11
183
+ * entries, safely inside the 16. A full-board repaint (cascade + gravity)
184
+ * spreads over up to 12 frames per player (~0.2s) — you SEE the well sweep
185
+ * top-to-bottom, which puzzle players read as a clear animation. Free juice.
186
+ * (Overflowing the queue doesn't corrupt anything — tile_set blocks until
187
+ * the NMI drains a slot — but every blocked push silently costs a whole
188
+ * frame, so a naive 72-cell repaint would freeze the game for ~4 frames.) */
189
+ static void drain_vram_budget(void) {
190
+ uint8_t p, r, c;
191
+ uint8_t (*g)[GRID_W];
192
+ /* One dirty board row, alternating players so neither well starves. */
193
+ p = drain_turn;
194
+ drain_turn ^= 1;
195
+ if (!dirty_rows[p]) p ^= 1;
196
+ if (dirty_rows[p]) {
197
+ g = grid_of(p);
198
+ for (r = 0; r < GRID_H; r++) {
199
+ if (dirty_rows[p] & ((uint16_t)1 << r)) {
200
+ for (c = 0; c < GRID_W; c++)
201
+ tile_set(0, (uint8_t)(well_tx[p] + c), (uint8_t)(WELL_TY + r),
202
+ bg_tile_for(g[r][c]));
203
+ dirty_rows[p] &= (uint16_t)~((uint16_t)1 << r);
204
+ break; /* one row per frame — that's the budget */
205
+ }
143
206
  }
144
207
  }
145
- /* Scan each row for 3-in-a-row same color clear. */
208
+ /* One HUD number per frame. HUD LAYOUT RULE (overscan): nametable row 0
209
+ * is cropped on NTSC — all HUD text sits on rows 1-2, never row 0. */
210
+ if (hud_dirty[0]) {
211
+ text_draw_u16(0, 2, 2, score[0]);
212
+ hud_dirty[0] = 0;
213
+ } else if (hud_dirty[1]) {
214
+ if (two_player) text_draw_u16(0, 22, 2, score[1]);
215
+ else text_draw_u16(0, 22, 2, level);
216
+ hud_dirty[1] = 0;
217
+ }
218
+ }
219
+
220
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
221
+ * Match scan: mark every straight run of 3+ same-coloured gems in all 4
222
+ * directions (a cell can belong to several runs — the mask de-dupes), and
223
+ * return how many cells matched. This is the resolve-time spike the header's
224
+ * frame-budget note talks about. */
225
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
226
+
227
+ static uint8_t mark_and_count(uint8_t p) {
228
+ uint8_t r, c, d, len, k, cnt, col;
229
+ int8_t dr, dc;
230
+ int sr, sc;
231
+ uint8_t (*g)[GRID_W] = grid_of(p);
232
+ cnt = 0;
233
+ for (r = 0; r < GRID_H; r++)
234
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
146
235
  for (r = 0; r < GRID_H; r++) {
147
- run = 1;
148
- run_col = grid[r][0];
149
- for (c = 1; c < GRID_W; c++) {
150
- if (grid[r][c] == run_col && run_col != EMPTY) {
151
- ++run;
152
- if (run >= 3) {
153
- /* Found a triple ending at c clear back. */
154
- grid[r][c] = EMPTY;
155
- grid[r][c - 1] = EMPTY;
156
- grid[r][c - 2] = EMPTY;
157
- sound_play_tone(0, 0x100 - (c << 4), 6, 4);
236
+ for (c = 0; c < GRID_W; c++) {
237
+ col = g[r][c];
238
+ if (col == EMPTY) continue;
239
+ for (d = 0; d < 4; d++) {
240
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
241
+ sr = (int)r - dr; sc = (int)c - dc;
242
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
243
+ && g[sr][sc] == col) continue; /* not the run's start */
244
+ len = 1;
245
+ sr = (int)r + dr; sc = (int)c + dc;
246
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
247
+ && g[sr][sc] == col) { len++; sr += dr; sc += dc; }
248
+ if (len >= 3) {
249
+ sr = r; sc = c;
250
+ for (k = 0; k < len; k++) {
251
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
252
+ sr += dr; sc += dc;
253
+ }
158
254
  }
159
- } else {
160
- run = 1;
161
- run_col = grid[r][c];
162
255
  }
163
256
  }
164
257
  }
258
+ return cnt;
259
+ }
260
+
261
+ /* Collapse each column so survivors rest on the floor (walk from the bottom,
262
+ * copying gems down to a write cursor, then zero everything above it). */
263
+ static void apply_gravity(uint8_t p) {
264
+ uint8_t c;
265
+ int8_t r, w;
266
+ uint8_t (*g)[GRID_W] = grid_of(p);
267
+ for (c = 0; c < GRID_W; c++) {
268
+ w = GRID_H - 1;
269
+ for (r = GRID_H - 1; r >= 0; r--) {
270
+ if (g[r][c] != EMPTY) { g[w][c] = g[r][c]; w--; }
271
+ }
272
+ for (; w >= 0; w--) g[w][c] = EMPTY;
273
+ }
274
+ }
275
+
276
+ /* ── GAME LOGIC (clay) — end of game (top-out). `loser` topped out. ── */
277
+ static void game_end(uint8_t loser) {
278
+ uint16_t best = score[0];
279
+ if (two_player && score[1] > best) best = score[1];
280
+ if (best > hiscore) {
281
+ hiscore = best;
282
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
283
+ * Persists via battery PRG-RAM at $6000; works because the crt0's iNES
284
+ * header sets the BATTERY bit. See nes_runtime.c for the magic+checksum
285
+ * layout (first boot reads garbage — the checksum rejects it). ── */
286
+ hiscore_save(hiscore);
287
+ }
288
+ sound_play_noise(8, 12, 16); /* game-over rumble */
289
+ if (two_player) text_draw(0, 12, 22, loser ? "P1 WINS" : "P2 WINS");
290
+ else text_draw(0, 11, 22, "GAME OVER");
291
+ text_draw(0, 9, 24, "START - TITLE");
292
+ prev_pad[0] = 0xFF; /* require a fresh press */
293
+ state = ST_OVER;
294
+ }
295
+
296
+ /* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
297
+ * Returns the chain depth (0 = the lock matched nothing). Score and repaints
298
+ * happen here; the actual VRAM writes trickle out via drain_vram_budget. */
299
+ static uint8_t resolve_board(uint8_t p) {
300
+ uint8_t n, r, c, chain;
301
+ uint16_t amt;
302
+ uint8_t (*g)[GRID_W] = grid_of(p);
303
+ chain = 0;
304
+ for (;;) {
305
+ n = mark_and_count(p);
306
+ if (n == 0) break;
307
+ ++chain;
308
+ for (r = 0; r < GRID_H; r++)
309
+ for (c = 0; c < GRID_W; c++)
310
+ if (matched[r][c]) g[r][c] = EMPTY;
311
+ amt = (uint16_t)n * 10;
312
+ if (chain > 1) amt *= chain; /* cascades pay multiplied */
313
+ score[p] += amt;
314
+ hud_dirty[p] = 1;
315
+ /* clear chime — rises with chain depth */
316
+ sound_play_tone(0, (uint16_t)(0x120 - ((uint16_t)chain << 4)), 8, 8);
317
+ apply_gravity(p);
318
+ mark_all_dirty(p); /* gravity moved everything */
319
+ if (!two_player) {
320
+ cleared_total += n;
321
+ while (level < 9 && cleared_total >= (uint16_t)level * 10) {
322
+ ++level;
323
+ hud_dirty[1] = 1; /* 1P: slot 1 shows the level */
324
+ }
325
+ }
326
+ }
327
+ return chain;
328
+ }
329
+
330
+ /* ── GAME LOGIC (clay) — VERSUS attack: garbage rows rise from the bottom of
331
+ * the victim's well (random gems with one gap — matchable, so a skilled
332
+ * victim digs out). The victim's stack rising into row <0 territory means
333
+ * the falling trio shifts up one to stay aligned; if the rim row is already
334
+ * occupied, the victim tops out and loses. ── */
335
+ static void garbage_insert(uint8_t v, uint8_t nrows) {
336
+ uint8_t k, c, gap;
337
+ int8_t r;
338
+ uint8_t (*g)[GRID_W] = grid_of(v);
339
+ sound_play_noise(10, 8, 8); /* incoming-garbage thud */
340
+ for (k = 0; k < nrows; k++) {
341
+ for (c = 0; c < GRID_W; c++) {
342
+ if (g[0][c] != EMPTY) { game_end(v); return; }
343
+ }
344
+ for (r = 0; r < GRID_H - 1; r++)
345
+ for (c = 0; c < GRID_W; c++)
346
+ g[r][c] = g[r + 1][c];
347
+ gap = random8() % GRID_W;
348
+ for (c = 0; c < GRID_W; c++)
349
+ g[GRID_H - 1][c] = (c == gap) ? EMPTY : (uint8_t)(1 + random8() % 3);
350
+ if (piece_y[v] > -3) --piece_y[v]; /* keep the trio board-relative */
351
+ }
352
+ mark_all_dirty(v);
165
353
  }
166
354
 
167
- /* Can the piece occupy (x, y..y+2) given the current grid? */
168
- static uint8_t can_place(int8_t x, int8_t y) {
169
- int8_t i;
355
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
356
+ * (pieces enter from above); below the floor or on a gem is not. */
357
+ static uint8_t can_place(uint8_t p, int8_t x, int8_t y) {
358
+ int8_t i, cy;
359
+ uint8_t (*g)[GRID_W] = grid_of(p);
170
360
  if (x < 0 || x >= GRID_W) return 0;
171
361
  for (i = 0; i < 3; i++) {
172
- int8_t cy = y + i;
173
- if (cy < 0) continue; /* above the well — allowed */
174
- if (cy >= GRID_H) return 0; /* below the well — blocked */
175
- if (grid[cy][x] != EMPTY) return 0;
362
+ cy = (int8_t)(y + i);
363
+ if (cy < 0) continue;
364
+ if (cy >= GRID_H) return 0;
365
+ if (g[cy][x] != EMPTY) return 0;
176
366
  }
177
367
  return 1;
178
368
  }
179
369
 
180
- void main(void) {
181
- uint8_t pad, prev_pad = 0;
182
- uint8_t r, c;
183
- int8_t i;
184
-
185
- ppu_off();
186
- chr_ram_upload(0x0000, tile_blank, 16); /* sprite slot 0 */
187
- chr_ram_upload(0x0010, tile_block_r, 16); /* sprite slots 1..3 */
188
- chr_ram_upload(0x0020, tile_block_g, 16);
189
- chr_ram_upload(0x0030, tile_block_b, 16);
190
- chr_ram_upload(0x1010, tile_wall, 16); /* BG slot 1 (background table) */
191
- chr_ram_upload(0x1020, tile_brick, 16); /* BG slot 2 (cabinet wall) */
192
- chr_ram_upload(0x1030, tile_inner, 16); /* BG slot 3 (well interior) */
193
- palette_load(palette);
370
+ static void spawn_piece(uint8_t p) {
371
+ piece_x[p] = GRID_W / 2;
372
+ piece_y[p] = -2;
373
+ piece_col[p][0] = (uint8_t)(1 + random8() % 3);
374
+ piece_col[p][1] = (uint8_t)(1 + random8() % 3);
375
+ piece_col[p][2] = (uint8_t)(1 + random8() % 3);
376
+ if (!can_place(p, (int8_t)piece_x[p], piece_y[p])) game_end(p);
377
+ }
194
378
 
195
- /* Draw the cabinet + well into the nametable while rendering is off
196
- * (vram_unsafe_set = raw PPU write; the friendly tile_set queue would
197
- * deadlock before ppu_on). The grid is 6 wide × 12 tall at pixel origin
198
- * (80,16) → tile cols 10..15, rows 2..13; frame it one cell out. We first
199
- * tile the WHOLE screen with brick (the machine cabinet), then carve the
200
- * recessed well interior, then stamp the steel-blue frame on top — so the
201
- * screen is fully covered instead of sprites floating on black. */
202
- {
203
- uint16_t gx0 = ORIGIN_X / 8, gy0 = ORIGIN_Y / 8; /* 10, 2 */
204
- uint16_t gx1 = gx0 + GRID_W, gy1 = gy0 + GRID_H; /* 16, 14 (exclusive) */
205
- uint16_t cc, rr;
206
- /* whole-screen cabinet brick */
207
- for (rr = 0; rr < 30; rr++)
208
- for (cc = 0; cc < 32; cc++)
209
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_BRICK);
210
- /* recessed well interior (inside the frame) */
211
- for (rr = gy0; rr < gy1; rr++)
212
- for (cc = gx0; cc < gx1; cc++)
213
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_INNER);
214
- /* steel frame one cell out around the well */
215
- for (cc = gx0 - 1; cc <= gx1; cc++) {
216
- vram_unsafe_set((uint16_t)(0x2000 + (gy0 - 1) * 32 + cc), BG_WALL); /* top */
217
- vram_unsafe_set((uint16_t)(0x2000 + gy1 * 32 + cc), BG_WALL); /* bottom */
379
+ /* ── GAME LOGIC (clay) land the trio, resolve, attack, respawn. ── */
380
+ static void lock_piece(uint8_t p) {
381
+ int8_t i, y;
382
+ uint8_t chain;
383
+ uint8_t (*g)[GRID_W] = grid_of(p);
384
+ for (i = 0; i < 3; i++) {
385
+ y = (int8_t)(piece_y[p] + i);
386
+ if (y >= 0) {
387
+ g[y][piece_x[p]] = piece_col[p][i];
388
+ mark_row_dirty(p, y);
218
389
  }
219
- for (rr = gy0 - 1; rr <= gy1; rr++) {
220
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + (gx0 - 1)), BG_WALL); /* left */
221
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + gx1), BG_WALL); /* right */
390
+ }
391
+ sound_play_tone(1, 0x1C0, 4, 3); /* lock thunk */
392
+ if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
393
+ chain = resolve_board(p);
394
+ if (state != ST_PLAY) return;
395
+ if (chain && two_player) {
396
+ garbage_insert(p ^ 1, chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
397
+ if (state != ST_PLAY) return; /* garbage topped them out */
398
+ }
399
+ spawn_piece(p);
400
+ }
401
+
402
+ /* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
403
+ * (one cell per press), held DOWN soft-drops. A/B cycle the trio's colours
404
+ * — the classic trio "rotate". ── */
405
+ static void update_player(uint8_t p) {
406
+ uint8_t pad, newp, fd, t;
407
+ pad = pad_poll(p);
408
+ newp = (uint8_t)(pad & (uint8_t)~prev_pad[p]);
409
+ prev_pad[p] = pad;
410
+ if ((newp & PAD_LEFT) && can_place(p, (int8_t)(piece_x[p] - 1), piece_y[p]))
411
+ --piece_x[p];
412
+ if ((newp & PAD_RIGHT) && can_place(p, (int8_t)(piece_x[p] + 1), piece_y[p]))
413
+ ++piece_x[p];
414
+ if (newp & PAD_A) { /* cycle colours downward */
415
+ t = piece_col[p][2];
416
+ piece_col[p][2] = piece_col[p][1];
417
+ piece_col[p][1] = piece_col[p][0];
418
+ piece_col[p][0] = t;
419
+ sound_play_tone(1, 0x0A0, 3, 2);
420
+ }
421
+ if (newp & PAD_B) { /* cycle colours upward */
422
+ t = piece_col[p][0];
423
+ piece_col[p][0] = piece_col[p][1];
424
+ piece_col[p][1] = piece_col[p][2];
425
+ piece_col[p][2] = t;
426
+ sound_play_tone(1, 0x0C0, 3, 2);
427
+ }
428
+ if (pad & PAD_DOWN) fall_t[p] += 4; /* soft drop */
429
+ ++fall_t[p];
430
+ fd = two_player ? VS_FALL_DELAY
431
+ : (uint8_t)(32 - ((level << 1) + level)); /* 29..5 */
432
+ if (fall_t[p] >= fd) {
433
+ fall_t[p] = 0;
434
+ if (can_place(p, (int8_t)piece_x[p], (int8_t)(piece_y[p] + 1)))
435
+ ++piece_y[p];
436
+ else
437
+ lock_piece(p); /* may end the game */
438
+ }
439
+ }
440
+
441
+ /* Stage the falling trio's sprites (board gems are BG tiles, NOT sprites —
442
+ * only what moves every frame earns OAM slots). */
443
+ static void stage_piece(uint8_t p) {
444
+ uint8_t i;
445
+ int8_t y;
446
+ for (i = 0; i < 3; i++) {
447
+ y = (int8_t)(piece_y[p] + i);
448
+ if (y >= 0)
449
+ oam_spr((uint8_t)((well_tx[p] + piece_x[p]) << 3),
450
+ (uint8_t)((WELL_TY + (uint8_t)y) << 3),
451
+ piece_col[p][i], 0);
452
+ }
453
+ }
454
+
455
+ /* 5-digit number with the PPU off (the queued text_draw_u16 would deadlock
456
+ * before rendering is enabled — same rule as text_draw_unsafe). */
457
+ static void text_u16_unsafe(uint16_t addr, uint16_t v) {
458
+ uint8_t d[5], i;
459
+ for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
460
+ for (i = 0; i < 5; i++)
461
+ vram_unsafe_set((uint16_t)(addr + i), (uint8_t)(0x40 + d[4 - i]));
462
+ }
463
+
464
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
465
+ * Attribute table = palette per 16x16-PIXEL area (one 2-bit quadrant per
466
+ * 2x2 TILES). The wells use BG palette 0 (gem colours) and everything else
467
+ * palette 1 (text/frame/dither) — which only works because the wells are
468
+ * aligned to EVEN tile coordinates (WELL_TY=8; well columns 4/12/22), so
469
+ * every attribute quadrant is fully inside or fully outside a well. Move a
470
+ * well to an odd column and its edge quadrants straddle the boundary —
471
+ * half-recoloured gems. Keep wells 2-aligned (or budget palettes so
472
+ * neighbouring regions share one). */
473
+ static uint8_t quad_pal(uint8_t tc, uint8_t tr) {
474
+ if (tr >= WELL_TY && tr < WELL_TY + GRID_H) {
475
+ if (tc >= well_tx[0] && tc < well_tx[0] + GRID_W) return 0;
476
+ if (two_player && tc >= well_tx[1] && tc < well_tx[1] + GRID_W) return 0;
477
+ }
478
+ return 1;
479
+ }
480
+
481
+ static void paint_attributes(void) {
482
+ uint8_t ar, ac, b;
483
+ for (ar = 0; ar < 8; ar++) {
484
+ for (ac = 0; ac < 8; ac++) {
485
+ b = (uint8_t)( quad_pal((uint8_t)(ac * 4), (uint8_t)(ar * 4))
486
+ | (quad_pal((uint8_t)(ac * 4 + 2), (uint8_t)(ar * 4)) << 2)
487
+ | (quad_pal((uint8_t)(ac * 4), (uint8_t)(ar * 4 + 2)) << 4)
488
+ | (quad_pal((uint8_t)(ac * 4 + 2), (uint8_t)(ar * 4 + 2)) << 6));
489
+ vram_unsafe_set((uint16_t)(0x23C0 + ar * 8 + ac), b);
222
490
  }
223
491
  }
492
+ }
224
493
 
494
+ /* ── GAME LOGIC (clay) — the title screen ──────────────────────────────────
495
+ * Painted with the PPU OFF (text_draw_unsafe = raw VRAM writes; the queued
496
+ * variant would deadlock with rendering disabled — see TROUBLESHOOTING). */
497
+ static void paint_title(void) {
498
+ uint8_t r, c;
499
+ ppu_off();
500
+ for (r = 0; r < 30; r++)
501
+ for (c = 0; c < 32; c++)
502
+ vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + c),
503
+ (r < 2) ? 0 : BG_DITHER);
504
+ for (c = 0; c < 64; c++) /* whole screen → palette 1 */
505
+ vram_unsafe_set((uint16_t)(0x23C0 + c), 0x55);
506
+ text_draw_unsafe(0x2000 + 8 * 32 + ((32 - sizeof(GAME_TITLE) + 1) / 2), GAME_TITLE);
507
+ text_draw_unsafe(0x2000 + 13 * 32 + 10, "1P START - A");
508
+ text_draw_unsafe(0x2000 + 15 * 32 + 9, "2P VERSUS - B");
509
+ text_draw_unsafe(0x2000 + 20 * 32 + 10, "HI");
510
+ text_u16_unsafe((uint16_t)(0x2000 + 20 * 32 + 13), hiscore);
511
+ ppu_scroll(0, 0);
225
512
  oam_clear();
226
513
  ppu_on_all();
227
- sound_init();
514
+ }
228
515
 
516
+ /* ── GAME LOGIC (clay) — paint the playfield: cabinet dither, well frames,
517
+ * recessed interiors, HUD labels + starting numbers. PPU off throughout. ── */
518
+ static void paint_well(uint8_t p) {
519
+ uint8_t r, c, x0;
520
+ x0 = well_tx[p];
521
+ for (c = (uint8_t)(x0 - 1); c <= (uint8_t)(x0 + GRID_W); c++) {
522
+ vram_unsafe_set((uint16_t)(0x2000 + (WELL_TY - 1) * 32 + c), BG_WALL);
523
+ vram_unsafe_set((uint16_t)(0x2000 + (WELL_TY + GRID_H) * 32 + c), BG_WALL);
524
+ }
525
+ for (r = (uint8_t)(WELL_TY - 1); r <= (uint8_t)(WELL_TY + GRID_H); r++) {
526
+ vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + (x0 - 1)), BG_WALL);
527
+ vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + (x0 + GRID_W)), BG_WALL);
528
+ }
229
529
  for (r = 0; r < GRID_H; r++)
230
- for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
231
- spawn_piece();
530
+ for (c = 0; c < GRID_W; c++)
531
+ vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)(WELL_TY + r) * 32 + x0 + c),
532
+ BG_INNER);
533
+ }
534
+
535
+ static void paint_play(void) {
536
+ uint8_t r, c;
537
+ ppu_off();
538
+ /* Cabinet dither everywhere; rows 0-2 blank (row 0 = overscan-cropped,
539
+ * rows 1-2 = the HUD band — keep text on a clean background). */
540
+ for (r = 0; r < 30; r++)
541
+ for (c = 0; c < 32; c++)
542
+ vram_unsafe_set((uint16_t)(0x2000 + (uint16_t)r * 32 + c),
543
+ (r < 3) ? 0 : BG_DITHER);
544
+ paint_well(0);
545
+ if (two_player) paint_well(1);
546
+ paint_attributes();
547
+ /* HUD: labels row 1, numbers row 2 (row 0 NEVER — overscan). */
548
+ text_draw_unsafe(0x2000 + 32 + 4, two_player ? "P1" : "SC");
549
+ text_draw_unsafe(0x2000 + 32 + 14, "HI");
550
+ text_draw_unsafe(0x2000 + 32 + 24, two_player ? "P2" : "LV");
551
+ text_u16_unsafe(0x2000 + 64 + 2, 0);
552
+ text_u16_unsafe(0x2000 + 64 + 12, hiscore);
553
+ text_u16_unsafe(0x2000 + 64 + 22, two_player ? 0 : 1);
554
+ ppu_scroll(0, 0);
555
+ oam_clear();
556
+ ppu_on_all();
557
+ }
558
+
559
+ /* ── GAME LOGIC (clay) — start a run ── */
560
+ static void start_game(uint8_t versus) {
561
+ uint8_t p, r, c;
562
+ uint8_t (*g)[GRID_W];
563
+ two_player = versus;
564
+ well_tx[0] = versus ? WELL_VS_P1 : WELL_1P_TX;
565
+ well_tx[1] = WELL_VS_P2;
566
+ /* Stir the PRNG with time-spent-on-title so runs differ. */
567
+ rng ^= (uint16_t)(((uint16_t)nmi_counter << 7) | nmi_counter);
568
+ if (rng == 0) rng = 0xACE1;
569
+ for (p = 0; p < 2; p++) {
570
+ g = grid_of(p);
571
+ for (r = 0; r < GRID_H; r++)
572
+ for (c = 0; c < GRID_W; c++) g[r][c] = EMPTY;
573
+ fall_t[p] = 0;
574
+ score[p] = 0;
575
+ hud_dirty[p] = 0;
576
+ dirty_rows[p] = 0;
577
+ prev_pad[p] = 0xFF; /* the button that started the game
578
+ * shouldn't also rotate the first trio */
579
+ }
580
+ cleared_total = 0;
581
+ level = 1;
582
+ drain_turn = 0;
583
+ paint_play();
584
+ state = ST_PLAY;
585
+ spawn_piece(0);
586
+ if (versus) spawn_piece(1);
587
+ }
588
+
589
+ void main(void) {
590
+ uint8_t pad, newp;
591
+
592
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
593
+ * Init order: PPU off → CHR upload → palette → nametable (raw writes) →
594
+ * OAM clear → rendering on. CHR/palette/nametable writes REQUIRE the PPU
595
+ * off (raw $2007 traffic during rendering corrupts the address latch
596
+ * mid-frame). The runtime's ppu_off/ppu_on_all pair owns the PPUCTRL/
597
+ * PPUMASK bits — don't poke those registers directly alongside it. */
598
+ ppu_off();
599
+ chr_ram_upload(0x0000, tile_blank, 16); /* sprite table: trio gems */
600
+ chr_ram_upload(0x0010, tile_gem1, 16);
601
+ chr_ram_upload(0x0020, tile_gem2, 16);
602
+ chr_ram_upload(0x0030, tile_gem3, 16);
603
+ chr_ram_upload(0x1010, tile_wall, 16); /* BG table: furniture + gems */
604
+ chr_ram_upload(0x1020, tile_dither, 16);
605
+ chr_ram_upload(0x1030, tile_inner, 16);
606
+ chr_ram_upload(0x1040, tile_gem1, 16);
607
+ chr_ram_upload(0x1050, tile_gem2, 16);
608
+ chr_ram_upload(0x1060, tile_gem3, 16);
609
+ font_upload();
610
+ palette_load(palette);
611
+ sound_init();
612
+
613
+ hiscore = hiscore_load(); /* battery SRAM — 0 on first boot */
614
+ state = ST_TITLE;
615
+ prev_pad[0] = 0xFF;
616
+ paint_title();
232
617
 
233
618
  for (;;) {
234
- /* ── Stage sprites: every non-empty cell + the active piece ── */
235
- oam_clear();
236
- for (r = 0; r < GRID_H; r++) {
237
- for (c = 0; c < GRID_W; c++) {
238
- if (grid[r][c] != EMPTY) {
239
- oam_spr(
240
- ORIGIN_X + c * CELL_PX,
241
- ORIGIN_Y + r * CELL_PX,
242
- tile_for_color(grid[r][c]),
243
- 0);
244
- }
245
- }
619
+ if (state == ST_TITLE) {
620
+ /* ── GAME LOGIC (clay) — title: A/START = 1P, B = 2P versus ── */
621
+ oam_clear();
622
+ ppu_wait_nmi();
623
+ sound_music_tick();
624
+ pad = pad_poll(0);
625
+ newp = (uint8_t)(pad & (uint8_t)~prev_pad[0]);
626
+ prev_pad[0] = pad;
627
+ if (newp & PAD_A) start_game(0);
628
+ else if (newp & PAD_B) start_game(1);
629
+ else if (newp & PAD_START) start_game(0);
630
+ continue;
246
631
  }
247
- for (i = 0; i < 3; i++) {
248
- int8_t y = piece_y + i;
249
- if (y >= 0 && y < GRID_H) {
250
- oam_spr(
251
- ORIGIN_X + piece_x * CELL_PX,
252
- ORIGIN_Y + y * CELL_PX,
253
- tile_for_color(piece_col[i]),
254
- 0);
632
+
633
+ if (state == ST_OVER) {
634
+ /* Freeze the boards (trios hidden); finish trickling out any queued
635
+ * repaints; START or A returns to the title. */
636
+ oam_clear();
637
+ ppu_wait_nmi();
638
+ sound_music_tick();
639
+ drain_vram_budget();
640
+ pad = pad_poll(0);
641
+ newp = (uint8_t)(pad & (uint8_t)~prev_pad[0]);
642
+ prev_pad[0] = pad;
643
+ if (newp & (PAD_START | PAD_A)) {
644
+ /* Flush the queue BEFORE repainting: paint_title turns the PPU off,
645
+ * and any still-queued board writes would otherwise land on top of
646
+ * the freshly painted title when the NMI comes back. */
647
+ ppu_wait_nmi();
648
+ ppu_wait_nmi();
649
+ state = ST_TITLE;
650
+ prev_pad[0] = 0xFF;
651
+ paint_title();
255
652
  }
653
+ continue;
256
654
  }
257
655
 
656
+ /* ── ST_PLAY ─────────────────────────────────────────────────────── */
657
+
658
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
659
+ * Stage ALL sprites BEFORE ppu_wait_nmi(). The NMI DMAs shadow OAM →
660
+ * real OAM at the START of vblank, copying whatever shadow OAM holds AT
661
+ * THAT MOMENT. Stage-then-wait; flipping it shows stale/empty sprites. */
662
+ oam_clear();
663
+ stage_piece(0);
664
+ if (two_player) stage_piece(1);
665
+
258
666
  ppu_wait_nmi();
667
+ sound_music_tick();
259
668
 
260
- /* ── Input ──────────────────────────────────────────────── */
261
- pad = pad_poll(0);
262
- if ((pad & PAD_LEFT) && !(prev_pad & PAD_LEFT) && can_place(piece_x - 1, piece_y)) --piece_x;
263
- if ((pad & PAD_RIGHT) && !(prev_pad & PAD_RIGHT) && can_place(piece_x + 1, piece_y)) ++piece_x;
264
- /* A: rotate (cycle the 3 colors) */
265
- if ((pad & PAD_A) && !(prev_pad & PAD_A)) {
266
- uint8_t tmp = piece_col[0];
267
- piece_col[0] = piece_col[1];
268
- piece_col[1] = piece_col[2];
269
- piece_col[2] = tmp;
270
- }
271
- /* DOWN: soft-drop (faster fall) */
272
- if (pad & PAD_DOWN) fall_timer += 4;
273
- prev_pad = pad;
274
-
275
- /* ── Auto-fall ──────────────────────────────────────────── */
276
- ++fall_timer;
277
- if (fall_timer >= 30) {
278
- fall_timer = 0;
279
- if (can_place(piece_x, piece_y + 1)) {
280
- ++piece_y;
281
- } else {
282
- lock_piece();
283
- spawn_piece();
284
- /* Game over: piece can't be placed at spawn. Just reset grid. */
285
- if (!can_place(piece_x, piece_y)) {
286
- for (r = 0; r < GRID_H; r++)
287
- for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
288
- sound_play_noise(8, 8, 12); /* game-over buzz */
289
- }
290
- }
291
- }
669
+ /* ── GAME LOGIC (clay — reshape freely) ── */
670
+ update_player(0);
671
+ if (two_player && state == ST_PLAY) update_player(1);
672
+ if (state == ST_PLAY) drain_vram_budget();
292
673
  }
293
674
  }