romdevtools 0.28.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 (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -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 +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -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;
103
147
 
148
+ #define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
149
+ #define GARBAGE_CAP 4 /* max garbage rows per attack */
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,61 +158,93 @@ 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;
169
+ }
170
+ static void mark_all_dirty(uint8_t p) {
171
+ dirty_rows[p] = 0x0FFF; /* all 12 rows */
126
172
  }
127
173
 
128
- static void spawn_piece(void) {
129
- piece_x = GRID_W / 2;
130
- piece_y = -3;
131
- random_piece_colors();
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
+ }
206
+ }
207
+ }
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
+ }
132
218
  }
133
219
 
134
- /* Lock current piece into the grid + check for match-3 horizontals. */
135
- /* ── match / clear / gravity core (ported from the GBC reference puzzle).
136
- * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
137
- * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
138
- * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
139
- * 4 directions, clears them, applies per-column gravity, and loops so
140
- * cascades chain (score scales with chain depth). */
141
- /* matched[] lives at $0500 — OUTSIDE the linker's RAM area ($0300-$04FF,
142
- * which grid+runtime statics nearly fill; 72 more BSS bytes overflow it).
143
- * $0500-$07FF is real, unused NES work RAM (hw stack is $0100, shadow OAM
144
- * $0200), so an absolute pointer there is free. */
145
- #define matched ((uint8_t (*)[GRID_W])0x0500)
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. */
146
225
  static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
147
226
 
148
- static uint8_t mark_and_count(void) {
149
- uint8_t r, c, d, len, k, cnt;
150
- uint8_t col;
227
+ static uint8_t mark_and_count(uint8_t p) {
228
+ uint8_t r, c, d, len, k, cnt, col;
151
229
  int8_t dr, dc;
152
230
  int sr, sc;
231
+ uint8_t (*g)[GRID_W] = grid_of(p);
153
232
  cnt = 0;
154
- for (r = 0; r < GRID_H; r++) for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
233
+ for (r = 0; r < GRID_H; r++)
234
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
155
235
  for (r = 0; r < GRID_H; r++) {
156
236
  for (c = 0; c < GRID_W; c++) {
157
- col = grid[r][c];
237
+ col = g[r][c];
158
238
  if (col == EMPTY) continue;
159
239
  for (d = 0; d < 4; d++) {
160
240
  dr = DIRS4[d][0]; dc = DIRS4[d][1];
161
241
  sr = (int)r - dr; sc = (int)c - dc;
162
242
  if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
163
- && grid[sr][sc] == col) continue; /* not the run's start */
243
+ && g[sr][sc] == col) continue; /* not the run's start */
164
244
  len = 1;
165
245
  sr = (int)r + dr; sc = (int)c + dc;
166
246
  while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
167
- && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
247
+ && g[sr][sc] == col) { len++; sr += dr; sc += dc; }
168
248
  if (len >= 3) {
169
249
  sr = r; sc = c;
170
250
  for (k = 0; k < len; k++) {
@@ -178,176 +258,417 @@ static uint8_t mark_and_count(void) {
178
258
  return cnt;
179
259
  }
180
260
 
181
- /* collapse each column so survivors rest on the floor (in place: walk
182
- * from the bottom, copying gems down to a write cursor, then zero above) */
183
- static void apply_gravity(void) {
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) {
184
264
  uint8_t c;
185
- int r, w;
265
+ int8_t r, w;
266
+ uint8_t (*g)[GRID_W] = grid_of(p);
186
267
  for (c = 0; c < GRID_W; c++) {
187
268
  w = GRID_H - 1;
188
269
  for (r = GRID_H - 1; r >= 0; r--) {
189
- if (grid[r][c] != EMPTY) { grid[w][c] = grid[r][c]; w--; }
270
+ if (g[r][c] != EMPTY) { g[w][c] = g[r][c]; w--; }
190
271
  }
191
- for (; w >= 0; w--) grid[w][c] = EMPTY;
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);
192
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;
193
294
  }
194
295
 
195
- static void resolve_board(void) {
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) {
196
300
  uint8_t n, r, c, chain;
197
- unsigned int amt;
301
+ uint16_t amt;
302
+ uint8_t (*g)[GRID_W] = grid_of(p);
198
303
  chain = 0;
199
- while (1) {
200
- n = mark_and_count();
304
+ for (;;) {
305
+ n = mark_and_count(p);
201
306
  if (n == 0) break;
202
- chain++;
307
+ ++chain;
203
308
  for (r = 0; r < GRID_H; r++)
204
309
  for (c = 0; c < GRID_W; c++)
205
- if (matched[r][c]) grid[r][c] = EMPTY;
206
- amt = (unsigned int)n * 10u;
207
- if (chain > 1) amt = amt * chain;
208
- (void)amt;
209
- sound_play_tone(0, 0xC0, 6, 4); /* clear chime */
210
- apply_gravity();
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
+ }
211
326
  }
327
+ return chain;
212
328
  }
213
329
 
214
- static void lock_piece(void) {
215
- int8_t i;
216
- /* Drop the 3 cells into the grid. */
217
- for (i = 0; i < 3; i++) {
218
- int8_t y = piece_y + i;
219
- if (y >= 0 && y < GRID_H) {
220
- grid[y][piece_x] = piece_col[i];
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; }
221
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 */
222
351
  }
223
- resolve_board();
352
+ mark_all_dirty(v);
224
353
  }
225
354
 
226
- /* Can the piece occupy (x, y..y+2) given the current grid? */
227
- static uint8_t can_place(int8_t x, int8_t y) {
228
- 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);
229
360
  if (x < 0 || x >= GRID_W) return 0;
230
361
  for (i = 0; i < 3; i++) {
231
- int8_t cy = y + i;
232
- if (cy < 0) continue; /* above the well — allowed */
233
- if (cy >= GRID_H) return 0; /* below the well — blocked */
234
- 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;
235
366
  }
236
367
  return 1;
237
368
  }
238
369
 
239
- void main(void) {
240
- uint8_t pad, prev_pad = 0;
241
- uint8_t r, c;
242
- int8_t i;
243
-
244
- ppu_off();
245
- chr_ram_upload(0x0000, tile_blank, 16); /* sprite slot 0 */
246
- chr_ram_upload(0x0010, tile_block_r, 16); /* sprite slots 1..3 */
247
- chr_ram_upload(0x0020, tile_block_g, 16);
248
- chr_ram_upload(0x0030, tile_block_b, 16);
249
- chr_ram_upload(0x1010, tile_wall, 16); /* BG slot 1 (background table) */
250
- chr_ram_upload(0x1020, tile_brick, 16); /* BG slot 2 (cabinet wall) */
251
- chr_ram_upload(0x1030, tile_inner, 16); /* BG slot 3 (well interior) */
252
- 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
+ }
253
378
 
254
- /* Draw the cabinet + well into the nametable while rendering is off
255
- * (vram_unsafe_set = raw PPU write; the friendly tile_set queue would
256
- * deadlock before ppu_on). The grid is 6 wide × 12 tall at pixel origin
257
- * (80,16) → tile cols 10..15, rows 2..13; frame it one cell out. We first
258
- * tile the WHOLE screen with brick (the machine cabinet), then carve the
259
- * recessed well interior, then stamp the steel-blue frame on top — so the
260
- * screen is fully covered instead of sprites floating on black. */
261
- {
262
- uint16_t gx0 = ORIGIN_X / 8, gy0 = ORIGIN_Y / 8; /* 10, 2 */
263
- uint16_t gx1 = gx0 + GRID_W, gy1 = gy0 + GRID_H; /* 16, 14 (exclusive) */
264
- uint16_t cc, rr;
265
- /* whole-screen cabinet brick */
266
- for (rr = 0; rr < 30; rr++)
267
- for (cc = 0; cc < 32; cc++)
268
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_BRICK);
269
- /* recessed well interior (inside the frame) */
270
- for (rr = gy0; rr < gy1; rr++)
271
- for (cc = gx0; cc < gx1; cc++)
272
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + cc), BG_INNER);
273
- /* steel frame one cell out around the well */
274
- for (cc = gx0 - 1; cc <= gx1; cc++) {
275
- vram_unsafe_set((uint16_t)(0x2000 + (gy0 - 1) * 32 + cc), BG_WALL); /* top */
276
- 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);
277
389
  }
278
- for (rr = gy0 - 1; rr <= gy1; rr++) {
279
- vram_unsafe_set((uint16_t)(0x2000 + rr * 32 + (gx0 - 1)), BG_WALL); /* left */
280
- 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);
281
490
  }
282
491
  }
492
+ }
283
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);
284
512
  oam_clear();
285
513
  ppu_on_all();
286
- sound_init();
514
+ }
287
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
+ }
288
529
  for (r = 0; r < GRID_H; r++)
289
- for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
290
- 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();
291
617
 
292
618
  for (;;) {
293
- /* ── Stage sprites: every non-empty cell + the active piece ── */
294
- oam_clear();
295
- for (r = 0; r < GRID_H; r++) {
296
- for (c = 0; c < GRID_W; c++) {
297
- if (grid[r][c] != EMPTY) {
298
- oam_spr(
299
- ORIGIN_X + c * CELL_PX,
300
- ORIGIN_Y + r * CELL_PX,
301
- tile_for_color(grid[r][c]),
302
- 0);
303
- }
304
- }
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;
305
631
  }
306
- for (i = 0; i < 3; i++) {
307
- int8_t y = piece_y + i;
308
- if (y >= 0 && y < GRID_H) {
309
- oam_spr(
310
- ORIGIN_X + piece_x * CELL_PX,
311
- ORIGIN_Y + y * CELL_PX,
312
- tile_for_color(piece_col[i]),
313
- 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();
314
652
  }
653
+ continue;
315
654
  }
316
655
 
317
- ppu_wait_nmi();
656
+ /* ── ST_PLAY ─────────────────────────────────────────────────────── */
318
657
 
319
- /* ── Input ──────────────────────────────────────────────── */
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
+
666
+ ppu_wait_nmi();
320
667
  sound_music_tick();
321
- pad = pad_poll(0);
322
- if ((pad & PAD_LEFT) && !(prev_pad & PAD_LEFT) && can_place(piece_x - 1, piece_y)) --piece_x;
323
- if ((pad & PAD_RIGHT) && !(prev_pad & PAD_RIGHT) && can_place(piece_x + 1, piece_y)) ++piece_x;
324
- /* A: rotate (cycle the 3 colors) */
325
- if ((pad & PAD_A) && !(prev_pad & PAD_A)) {
326
- uint8_t tmp = piece_col[0];
327
- piece_col[0] = piece_col[1];
328
- piece_col[1] = piece_col[2];
329
- piece_col[2] = tmp;
330
- }
331
- /* DOWN: soft-drop (faster fall) */
332
- if (pad & PAD_DOWN) fall_timer += 4;
333
- prev_pad = pad;
334
-
335
- /* ── Auto-fall ──────────────────────────────────────────── */
336
- ++fall_timer;
337
- if (fall_timer >= 30) {
338
- fall_timer = 0;
339
- if (can_place(piece_x, piece_y + 1)) {
340
- ++piece_y;
341
- } else {
342
- lock_piece();
343
- spawn_piece();
344
- /* Game over: piece can't be placed at spawn. Just reset grid. */
345
- if (!can_place(piece_x, piece_y)) {
346
- for (r = 0; r < GRID_H; r++)
347
- for (c = 0; c < GRID_W; c++) grid[r][c] = EMPTY;
348
- sound_play_noise(8, 8, 12); /* game-over buzz */
349
- }
350
- }
351
- }
668
+
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();
352
673
  }
353
674
  }