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,228 +1,675 @@
1
- /* ── puzzle.c — SNES PVSnesLib match-3 falling-block scaffold ──────
1
+ /* ── puzzle.c — SNES falling-jewel versus puzzle (complete example game) ──────
2
2
  *
3
- * 6-wide × 12-tall grid drawn entirely via text-mode characters
4
- * (R = red, G = green, B = blue). Active piece is 1×3 vertical;
5
- * LEFT/RIGHT shifts, A rotates colour order, DOWN soft-drops, START
6
- * hard-drops. Horizontal triples clear and score.
3
+ * A COMPLETE, working game title screen, 1P marathon (levels speed the
4
+ * fall) and 2P SIMULTANEOUS split-board versus with garbage attacks,
5
+ * score + persistent hi-score (battery SRAM, survives power cycles),
6
+ * SPC music + SFX, and the board rendered the SNES way: a WRAM shadow
7
+ * tilemap blasted to VRAM by DMA every single frame.
7
8
  *
8
- * Why text-mode? PVSnesLib's consoleDrawText is the simplest
9
- * "draw something to a cell" path on SNES. Real puzzle games use
10
- * BG-tile sprite cells with proper graphics that's the natural
11
- * next step from this scaffold.
9
+ * The game: a falling-trio match-3. A trio of jewels falls into a 6x12 well;
10
+ * LEFT/RIGHT move it, A/B cycle its three colours, DOWN soft-drops. When it
11
+ * lands, any straight run of 3+ same-coloured jewels (horizontal, vertical,
12
+ * or diagonal) clears; survivors fall and cascades chain for multiplied score.
13
+ *
14
+ * 2P VERSUS design (simultaneous, split board): two 6x12 wells side by side —
15
+ * P1 left on controller 1, P2 right on controller 2 (padsCurrent(1) — that's
16
+ * the entire 2P wiring), both falling at once. Clears ATTACK: every chain
17
+ * step you score sends one garbage row (random jewels with one gap, capped
18
+ * at 4 per attack) rising from the bottom of the opponent's well. First
19
+ * player whose stack reaches the top loses.
20
+ *
21
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
22
+ * very different one. The markers tell you what's what:
23
+ * HARDWARE IDIOM (load-bearing) — dodges a documented SNES footgun; reshape
24
+ * your gameplay around it (see TROUBLESHOOTING before changing).
25
+ * GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
26
+ *
27
+ * What depends on what:
28
+ * data.asm — console font, the 8-tile board/jewel tileset + palette
29
+ * (shared by BG2 and the OBJ sprites), and sram_read16/write16.
30
+ * Load-bearing.
31
+ * hdr.asm — THIS PROJECT OVERRIDES the stock header to declare battery
32
+ * SRAM (CARTRIDGETYPE $02 + SRAMSIZE $01). Delete that file and saves
33
+ * silently stop existing — the build still succeeds.
34
+ * snes_sfx.{h,c} + snes_sfx_data.asm + apu_blob.bin — the SPC700 sound
35
+ * driver (music + 2 one-shot samples). #include'd, not separately built.
36
+ *
37
+ * ── SNES vs NES: THE SAME GAME, TWO RENDER BUDGETS (teaching note) ──────────
38
+ * The NES build of this exact game (examples/nes puzzle) has to DRIP board
39
+ * repaints through a queued-VRAM path: the NMI drains at most 16 queue bytes
40
+ * per vblank, so a cascade repaints ONE dirty row per frame and a full-board
41
+ * sweep takes 12 frames. On the SNES none of that machinery exists: the whole
42
+ * 32x32 board tilemap lives in WRAM (board_map below) and general-purpose DMA
43
+ * copies all 2 KB of it to VRAM EVERY frame inside vblank (~12 scanlines of
44
+ * the ~38 available — bus speed makes the budget problem evaporate). Game
45
+ * logic just rewrites WRAM whenever it likes, with zero dirty-row tracking
46
+ * toward the PPU; a 12-row double-cascade lands on screen in ONE frame.
47
+ *
48
+ * Frame budget: input + gravity for two trios is nothing; the spike is
49
+ * resolve_board() at lock time (full 4-direction match scan over 72 cells in
50
+ * tcc-compiled C). It can spill a frame past vblank — that shows as (at
51
+ * most) a one-frame hitch on the falling pieces, never corruption, because
52
+ * the shadow map is only DMA'd after WaitForVBlank.
53
+ *
54
+ * VRAM BUDGET (word addresses):
55
+ * $2000- board tileset (8 tiles) $3000- console font (96 glyphs)
56
+ * $4000- board map (BG2, 32x32) $6000- OBJ tiles (same 8 tiles)
57
+ * $6800- console text map (BG1)
12
58
  */
13
59
 
14
60
  #include <snes.h>
15
61
  #include "snes_sfx.c"
16
62
 
17
- extern char tilfont, palfont;
18
- extern char tilbg, palbg; /* wallpaper tile + palette (data.asm) */
63
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
64
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
65
+ #define GAME_TITLE "JEWEL JOUST"
66
+
67
+ extern char tilfont, palfont; /* console font + text palette (data.asm) */
68
+ extern char tilboard, palboard; /* board/jewel tiles + palette */
19
69
 
20
70
  /* consoleVblank() copies the dirty text tilemap to VRAM during VBlank.
21
71
  * No public prototype in console.h, so declare it; call once per frame. */
22
72
  extern void consoleVblank(void);
23
73
 
24
- /* BG1 wallpaper map: a full 32x32 screen of the 4-colour tile so the
25
- * playfield reads as a real backdrop, not flat blank. Filled at runtime. */
26
- static u16 bg_map[32 * 32];
74
+ /* data.asm exports battery SRAM accessors ($70:0000 long addressing). */
75
+ extern u16 sram_read16(u16 offset);
76
+ extern void sram_write16(u16 offset, u16 value);
77
+
78
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
79
+ * Board geometry. Tile coordinates are free on the SNES: unlike the NES there
80
+ * is NO attribute table — every 4bpp map entry carries its own palette bits —
81
+ * so wells can sit at ANY column (the NES version must keep them 2-aligned). */
82
+ #define GRID_W 6
83
+ #define GRID_H 12
84
+ #define GRID_CELLS (GRID_W * GRID_H)
85
+ #define WELL_TY 8 /* top tile row of the well interior */
86
+ #define WELL_1P_TX 13 /* 1P: single centered well (cols 13-18) */
87
+ #define WELL_VS_P1 4 /* 2P: P1 well cols 4-9 ... */
88
+ #define WELL_VS_P2 22 /* P2 well cols 22-27 (split board) */
89
+
90
+ #define EMPTY 0 /* cell colours 1..3 = ruby/emerald/amber */
27
91
 
28
- #define COLS 6
29
- #define ROWS 12
92
+ /* board tileset indices — MUST match the tile order in data.asm */
93
+ #define BG_BLANK 0
94
+ #define BG_WALL 1
95
+ #define BG_DITHER 2
96
+ #define BG_INNER 3
97
+ #define BG_GEM_BASE 4 /* tiles 4/5/6 = jewel colours 1/2/3 */
30
98
 
31
- static u8 grid[ROWS][COLS];
32
- static u8 piece[3];
33
- static s16 piece_x;
34
- static s16 piece_y;
35
- static u16 fall_timer;
36
- static u16 score;
37
- static u32 rng = 1;
99
+ /* BG2 map entries select CGRAM palette block 1 (vhopppcc cccccccc). */
100
+ #define MAP_PAL1 0x0400
38
101
 
39
- static u32 xorshift(void) {
40
- rng ^= rng << 13;
41
- rng ^= rng >> 17;
42
- rng ^= rng << 5;
43
- return rng;
102
+ #define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
103
+ #define GARBAGE_CAP 4 /* max garbage rows per attack */
104
+
105
+ /* SRAM layout: [0]=magic "JJ", [2]=hi-score, [4]=hi ^ 0xA5C3.
106
+ * Magic is written LAST in hi_save so a torn write never validates. */
107
+ #define SRAM_MAGIC 0x4A4Au
108
+
109
+ /* Game states — the shell every example shares: title → play → game over. */
110
+ #define ST_TITLE 0
111
+ #define ST_PLAY 1
112
+ #define ST_OVER 2
113
+
114
+ static u8 state;
115
+ static u8 two_player; /* mode chosen on the title screen */
116
+ static u8 sound_ok;
117
+ static u8 well_tx[2]; /* left tile column of each well */
118
+ static u8 piece_x[2]; /* falling trio: column 0..5 */
119
+ static s8 piece_y[2]; /* row of its TOP cell (<0 = above rim) */
120
+ static u8 piece_col[2][3]; /* trio colours, top to bottom */
121
+ static u8 fall_t[2]; /* frames until next gravity step */
122
+ static u16 prev_pad[2]; /* per-player edge-triggered input */
123
+ static u16 prev_pad0; /* shell (title/game-over) edge detect */
124
+ static u16 score[2];
125
+ static u16 hiscore;
126
+ static u16 cleared_total; /* 1P: jewels cleared, drives the level */
127
+ static u8 level; /* 1P: 1..9, speeds up the fall */
128
+ static u8 board_dirty[2]; /* well cells changed → recompose shadow map */
129
+ static u8 garb_rows[2]; /* garbage rows RECEIVED (telemetry + tuning) */
130
+ static u16 frames; /* free-running frame counter (PRNG stir) */
131
+ static u16 rng = 0xACE1;
132
+ static char tbuf[8]; /* 5-digit number formatter output */
133
+
134
+ /* the two boards, flattened (row*GRID_W+col); P2's right after P1's */
135
+ static u8 grid[2 * GRID_CELLS];
136
+ static u8 matched[GRID_CELLS];
137
+ #define GRIDOF(p) (grid + ((p) ? GRID_CELLS : 0))
138
+
139
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
140
+ * The board's WRAM shadow tilemap. This 2 KB array IS the screen: game code
141
+ * writes map entries here whenever it likes (any time, mid-frame, mid-logic),
142
+ * and the main loop DMAs the whole thing to VRAM word $4000 right after
143
+ * WaitForVBlank — full repaint, every frame, no queue, no dirty-row budget
144
+ * (see the NES-contrast note in the header). The ONLY rule is the DMA's:
145
+ * VRAM writes land correctly ONLY during vblank/forced blank, so the
146
+ * dmaCopyVram call must stay where it is, between WaitForVBlank and the
147
+ * frame's logic. Writing board_map itself is always safe. */
148
+ static u16 board_map[32 * 32];
149
+
150
+ /* headless-test telemetry — magic "JW"+0xBD; a test harness scans WRAM for
151
+ * it and plays the game from real state instead of parsing pixels. Costs a
152
+ * few byte-writes per frame; delete freely. */
153
+ static u8 telem[24];
154
+
155
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (~tens of cycles per call) ── */
156
+ static u8 random8(void) {
157
+ u16 r = rng;
158
+ r ^= r << 7;
159
+ r ^= r >> 9;
160
+ r ^= r << 8;
161
+ rng = r;
162
+ return (u8)r;
44
163
  }
45
164
 
46
- static u8 random_colour(void) { return 1 + (xorshift() % 3); }
165
+ /* cell colour → board tile (empty cells show the faint speck, not raw
166
+ * backdrop, so the well reads as a recessed playfield). */
167
+ static u16 cell_entry(u8 col) {
168
+ return (u16)(col ? (u8)(BG_GEM_BASE - 1 + col) : BG_INNER) | MAP_PAL1;
169
+ }
47
170
 
48
- static char glyph_for(u8 v) {
49
- switch (v) {
50
- case 1: return 'R';
51
- case 2: return 'G';
52
- case 3: return 'B';
53
- default: return '.';
54
- }
171
+ /* ── GAME LOGIC (clay) — shadow-map painters ─────────────────────────────────
172
+ * All of these only touch board_map (WRAM); the per-frame DMA makes them
173
+ * visible. paint_board is the per-change repaint: ~72 u16 stores, cheap
174
+ * enough to run whole-board whenever anything locked/cleared/shifted. */
175
+ static void map_fill(u8 tile) {
176
+ u16 i, e;
177
+ e = (u16)tile | MAP_PAL1;
178
+ for (i = 0; i < 32 * 32; i++) board_map[i] = e;
55
179
  }
56
180
 
57
- static void new_piece(void) {
58
- piece[0] = random_colour();
59
- piece[1] = random_colour();
60
- piece[2] = random_colour();
61
- piece_x = COLS / 2 - 1;
62
- piece_y = -3;
181
+ static void map_row_fill(u8 row, u8 tile) {
182
+ u16 i, base;
183
+ base = (u16)row << 5;
184
+ for (i = 0; i < 32; i++) board_map[base + i] = (u16)tile | MAP_PAL1;
63
185
  }
64
186
 
65
- static void draw_cell(s16 col, s16 row) {
66
- char s[2];
67
- if (row < 0 || row >= ROWS) return;
68
- s[0] = glyph_for(grid[row][col]);
69
- s[1] = 0;
70
- consoleDrawText(col + 12, row + 4, s);
187
+ static void paint_board(u8 p) {
188
+ u8 r, c;
189
+ u16 base;
190
+ u8 *g = GRIDOF(p);
191
+ for (r = 0; r < GRID_H; r++) {
192
+ base = ((u16)(WELL_TY + r) << 5) + well_tx[p];
193
+ for (c = 0; c < GRID_W; c++)
194
+ board_map[base + c] = cell_entry(*g++);
195
+ }
71
196
  }
72
197
 
73
- static void draw_grid(void) {
74
- s16 r, c;
75
- for (r = 0; r < ROWS; r++)
76
- for (c = 0; c < COLS; c++)
77
- draw_cell(c, r);
198
+ static void paint_well_frame(u8 p) {
199
+ u8 r, c, x0;
200
+ u16 e;
201
+ x0 = well_tx[p];
202
+ e = (u16)BG_WALL | MAP_PAL1;
203
+ for (c = (u8)(x0 - 1); c <= (u8)(x0 + GRID_W); c++) {
204
+ board_map[((u16)(WELL_TY - 1) << 5) + c] = e;
205
+ board_map[((u16)(WELL_TY + GRID_H) << 5) + c] = e;
206
+ }
207
+ for (r = (u8)(WELL_TY - 1); r <= (u8)(WELL_TY + GRID_H); r++) {
208
+ board_map[((u16)r << 5) + (u16)(x0 - 1)] = e;
209
+ board_map[((u16)r << 5) + (u16)(x0 + GRID_W)] = e;
210
+ }
78
211
  }
79
212
 
80
- static void draw_piece(u8 clear) {
81
- u16 i;
82
- s16 r;
83
- char s[2];
84
- for (i = 0; i < 3; i++) {
85
- r = piece_y + i;
86
- if (r < 0 || r >= ROWS) continue;
87
- s[0] = clear ? glyph_for(grid[r][piece_x])
88
- : glyph_for(piece[i]);
89
- s[1] = 0;
90
- consoleDrawText(piece_x + 12, r + 4, s);
213
+ /* title backdrop: dither cabinet, a clear band for the menu text, and a
214
+ * jewel stripe under the logo (the attract twist below scrolls its hues). */
215
+ static void paint_title_map(void) {
216
+ u8 r, c;
217
+ map_fill(BG_DITHER);
218
+ for (r = 2; r <= 6; r++) map_row_fill(r, BG_BLANK);
219
+ for (r = 13; r <= 17; r++) map_row_fill(r, BG_BLANK);
220
+ for (c = 0; c < 32; c++) {
221
+ board_map[(25u << 5) + c] = (u16)BG_WALL | MAP_PAL1;
222
+ }
223
+ }
224
+
225
+ static void paint_title_stripe(u8 phase) {
226
+ u8 c;
227
+ for (c = 10; c < 22; c++)
228
+ board_map[(7u << 5) + c] =
229
+ (u16)(BG_GEM_BASE + (u8)((c + phase) % 3)) | MAP_PAL1;
230
+ }
231
+
232
+ static void paint_play_map(void) {
233
+ u8 r;
234
+ map_fill(BG_DITHER);
235
+ for (r = 0; r < 3; r++) map_row_fill(r, BG_BLANK); /* clean HUD band */
236
+ paint_well_frame(0);
237
+ paint_board(0);
238
+ if (two_player) { paint_well_frame(1); paint_board(1); }
239
+ }
240
+
241
+ /* ── GAME LOGIC (clay) — text helpers (console BG1, queued via consoleVblank) */
242
+ static void fmt5(u16 v) {
243
+ s8 i;
244
+ for (i = 4; i >= 0; i--) { tbuf[i] = (char)('0' + (v % 10)); v /= 10; }
245
+ tbuf[5] = 0;
246
+ }
247
+
248
+ static void clear_rows(u8 a, u8 b) {
249
+ u8 y;
250
+ for (y = a; y <= b; y++)
251
+ consoleDrawText(0, y, " ");
252
+ }
253
+
254
+ static void draw_hud_num(u8 p) {
255
+ if (p == 0) { fmt5(score[0]); consoleDrawText(2, 2, tbuf); }
256
+ else {
257
+ if (two_player) fmt5(score[1]); else fmt5(level);
258
+ consoleDrawText(24, 2, tbuf);
259
+ }
260
+ }
261
+
262
+ static void draw_hi(u8 x, u8 y) {
263
+ fmt5(hiscore);
264
+ consoleDrawText(x, y, tbuf);
265
+ }
266
+
267
+ /* ── GAME LOGIC (clay) — hi-score in battery SRAM (see sram_* in data.asm) ── */
268
+ static u16 hi_load(void) {
269
+ u16 v;
270
+ if (sram_read16(0) != SRAM_MAGIC) return 0;
271
+ v = sram_read16(2);
272
+ if (sram_read16(4) != (u16)(v ^ 0xA5C3u)) return 0;
273
+ return v;
274
+ }
275
+
276
+ static void hi_save(u16 v) {
277
+ sram_write16(2, v);
278
+ sram_write16(4, (u16)(v ^ 0xA5C3u));
279
+ sram_write16(0, SRAM_MAGIC); /* magic LAST — torn write = no record */
280
+ }
281
+
282
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
283
+ * Match scan: mark every straight run of 3+ same-coloured jewels in all 4
284
+ * directions (a cell can belong to several runs — the mask de-dupes), and
285
+ * return how many cells matched. This is the resolve-time spike the header's
286
+ * frame-budget note talks about. */
287
+ static const s8 DR4[4] = { 0, 1, 1, 1 };
288
+ static const s8 DC4[4] = { 1, 0, 1, -1 };
289
+
290
+ static u8 mark_and_count(u8 p) {
291
+ u8 d, len, k, cnt, col;
292
+ s16 r, c, sr, sc, dr, dc;
293
+ u8 *g = GRIDOF(p);
294
+ cnt = 0;
295
+ for (k = 0; k < GRID_CELLS; k++) matched[k] = 0;
296
+ for (r = 0; r < GRID_H; r++) {
297
+ for (c = 0; c < GRID_W; c++) {
298
+ col = g[r * GRID_W + c];
299
+ if (col == EMPTY) continue;
300
+ for (d = 0; d < 4; d++) {
301
+ dr = DR4[d]; dc = DC4[d];
302
+ sr = r - dr; sc = c - dc;
303
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
304
+ && g[sr * GRID_W + sc] == col) continue; /* not the run's start */
305
+ len = 1;
306
+ sr = r + dr; sc = c + dc;
307
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
308
+ && g[sr * GRID_W + sc] == col) { len++; sr += dr; sc += dc; }
309
+ if (len >= 3) {
310
+ sr = r; sc = c;
311
+ for (k = 0; k < len; k++) {
312
+ if (!matched[sr * GRID_W + sc]) { matched[sr * GRID_W + sc] = 1; cnt++; }
313
+ sr += dr; sc += dc;
314
+ }
315
+ }
316
+ }
91
317
  }
318
+ }
319
+ return cnt;
92
320
  }
93
321
 
94
- static u8 collides(s16 col, s16 row) {
95
- u16 i;
96
- s16 r;
97
- if (col < 0 || col >= COLS) return 1;
98
- for (i = 0; i < 3; i++) {
99
- r = row + i;
100
- if (r >= ROWS) return 1;
101
- if (r >= 0 && grid[r][col] != 0) return 1;
322
+ /* Collapse each column so survivors rest on the floor (walk from the bottom,
323
+ * copying jewels down to a write cursor, then zero everything above it). */
324
+ static void apply_gravity(u8 p) {
325
+ s16 c, r, w;
326
+ u8 *g = GRIDOF(p);
327
+ for (c = 0; c < GRID_W; c++) {
328
+ w = GRID_H - 1;
329
+ for (r = GRID_H - 1; r >= 0; r--) {
330
+ if (g[r * GRID_W + c] != EMPTY) { g[w * GRID_W + c] = g[r * GRID_W + c]; w--; }
102
331
  }
103
- return 0;
332
+ for (; w >= 0; w--) g[w * GRID_W + c] = EMPTY;
333
+ }
104
334
  }
105
335
 
106
- static void lock_piece(void) {
107
- u16 i;
108
- s16 r, c;
109
- u8 a, b, d;
110
- for (i = 0; i < 3; i++) {
111
- r = piece_y + i;
112
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
336
+ /* ── GAME LOGIC (clay) — end of game (top-out). `loser` topped out. ── */
337
+ static void game_end(u8 loser) {
338
+ u16 best = score[0];
339
+ if (two_player && score[1] > best) best = score[1];
340
+ if (best > hiscore) {
341
+ hiscore = best;
342
+ hi_save(hiscore); /* battery SRAM survives power-off */
343
+ draw_hi(13, 2);
344
+ }
345
+ if (sound_ok) sfx_play(2); /* game-over thud */
346
+ if (two_player) consoleDrawText(12, 22, loser ? "P1 WINS" : "P2 WINS");
347
+ else consoleDrawText(11, 22, "GAME OVER");
348
+ consoleDrawText(9, 24, "START - TITLE");
349
+ prev_pad0 = 0xFFFF; /* require a fresh press */
350
+ state = ST_OVER;
351
+ }
352
+
353
+ /* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
354
+ * Returns the chain depth (0 = the lock matched nothing). The repaint is
355
+ * just board_dirty=1: the whole well redraws into the shadow map this frame
356
+ * and the next vblank's DMA shows it — chains land instantly on screen. */
357
+ static u8 resolve_board(u8 p) {
358
+ u8 n, k, chain;
359
+ u16 amt;
360
+ u8 *g = GRIDOF(p);
361
+ chain = 0;
362
+ for (;;) {
363
+ n = mark_and_count(p);
364
+ if (n == 0) break;
365
+ ++chain;
366
+ for (k = 0; k < GRID_CELLS; k++)
367
+ if (matched[k]) g[k] = EMPTY;
368
+ amt = (u16)n * 10;
369
+ if (chain > 1) amt *= chain; /* cascades pay multiplied */
370
+ score[p] += amt;
371
+ draw_hud_num(p);
372
+ if (sound_ok) sfx_play(2); /* clear chime */
373
+ apply_gravity(p);
374
+ board_dirty[p] = 1;
375
+ if (!two_player) {
376
+ cleared_total += n;
377
+ while (level < 9 && cleared_total >= (u16)level * 10) {
378
+ ++level;
379
+ draw_hud_num(1); /* 1P: slot 1 shows the level */
380
+ }
113
381
  }
382
+ }
383
+ return chain;
384
+ }
385
+
386
+ /* ── GAME LOGIC (clay) — VERSUS attack: garbage rows rise from the bottom of
387
+ * the victim's well (random jewels with one gap — matchable, so a skilled
388
+ * victim digs out). The victim's stack rising means the falling trio shifts
389
+ * up one to stay board-relative; if the top row is already occupied, the
390
+ * victim tops out and loses. ── */
391
+ static void garbage_insert(u8 v, u8 nrows) {
392
+ u8 k, c, gap;
393
+ s16 r;
394
+ u8 *g = GRIDOF(v);
395
+ if (sound_ok) sfx_play(2); /* incoming-garbage thud */
396
+ for (k = 0; k < nrows; k++) {
397
+ for (c = 0; c < GRID_W; c++)
398
+ if (g[c] != EMPTY) { board_dirty[v] = 1; game_end(v); return; }
399
+ for (r = 0; r < GRID_H - 1; r++)
400
+ for (c = 0; c < GRID_W; c++)
401
+ g[r * GRID_W + c] = g[(r + 1) * GRID_W + c];
402
+ gap = random8() % GRID_W;
403
+ for (c = 0; c < GRID_W; c++)
404
+ g[(GRID_H - 1) * GRID_W + c] = (c == gap) ? EMPTY : (u8)(1 + random8() % 3);
405
+ if (piece_y[v] > -3) --piece_y[v]; /* keep the trio board-relative */
406
+ ++garb_rows[v];
407
+ }
408
+ board_dirty[v] = 1;
409
+ }
410
+
411
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
412
+ * (pieces enter from above); below the floor or on a jewel is not. */
413
+ static u8 can_place(u8 p, s16 x, s16 y) {
414
+ s16 i, cy;
415
+ u8 *g = GRIDOF(p);
416
+ if (x < 0 || x >= GRID_W) return 0;
417
+ for (i = 0; i < 3; i++) {
418
+ cy = y + i;
419
+ if (cy < 0) continue;
420
+ if (cy >= GRID_H) return 0;
421
+ if (g[cy * GRID_W + x] != EMPTY) return 0;
422
+ }
423
+ return 1;
424
+ }
425
+
426
+ static void spawn_piece(u8 p) {
427
+ piece_x[p] = GRID_W / 2;
428
+ piece_y[p] = -2;
429
+ piece_col[p][0] = (u8)(1 + random8() % 3);
430
+ piece_col[p][1] = (u8)(1 + random8() % 3);
431
+ piece_col[p][2] = (u8)(1 + random8() % 3);
432
+ if (!can_place(p, (s16)piece_x[p], (s16)piece_y[p])) game_end(p);
433
+ }
434
+
435
+ /* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
436
+ static void lock_piece(u8 p) {
437
+ s16 i, y;
438
+ u8 chain;
439
+ u8 *g = GRIDOF(p);
440
+ for (i = 0; i < 3; i++) {
441
+ y = piece_y[p] + i;
442
+ if (y >= 0) g[y * GRID_W + piece_x[p]] = piece_col[p][i];
443
+ }
444
+ board_dirty[p] = 1;
445
+ if (sound_ok) sfx_play(1); /* lock click */
446
+ if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
447
+ chain = resolve_board(p);
448
+ if (state != ST_PLAY) return;
449
+ if (chain && two_player) {
450
+ garbage_insert(p ^ 1, chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
451
+ if (state != ST_PLAY) return; /* garbage topped them out */
452
+ }
453
+ spawn_piece(p);
454
+ }
455
+
456
+ /* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
457
+ * (one cell per press), held DOWN soft-drops. A/B cycle the trio's colours
458
+ * — the classic trio "rotate". P2's pad is just padsCurrent(1). ── */
459
+ static void update_player(u8 p) {
460
+ u16 pad, newp;
461
+ u8 fd, t;
462
+ pad = padsCurrent(p);
463
+ newp = pad & (u16)~prev_pad[p];
464
+ prev_pad[p] = pad;
465
+ if ((newp & KEY_LEFT) && can_place(p, (s16)(piece_x[p] - 1), (s16)piece_y[p]))
466
+ --piece_x[p];
467
+ if ((newp & KEY_RIGHT) && can_place(p, (s16)(piece_x[p] + 1), (s16)piece_y[p]))
468
+ ++piece_x[p];
469
+ if (newp & KEY_A) { /* cycle colours downward */
470
+ t = piece_col[p][2];
471
+ piece_col[p][2] = piece_col[p][1];
472
+ piece_col[p][1] = piece_col[p][0];
473
+ piece_col[p][0] = t;
474
+ if (sound_ok) sfx_play(1);
475
+ }
476
+ if (newp & KEY_B) { /* cycle colours upward */
477
+ t = piece_col[p][0];
478
+ piece_col[p][0] = piece_col[p][1];
479
+ piece_col[p][1] = piece_col[p][2];
480
+ piece_col[p][2] = t;
481
+ if (sound_ok) sfx_play(1);
482
+ }
483
+ if (pad & KEY_DOWN) fall_t[p] += 4; /* soft drop */
484
+ ++fall_t[p];
485
+ fd = two_player ? VS_FALL_DELAY
486
+ : (u8)(32 - level * 3); /* 1P: 29..5 frames per row */
487
+ if (fall_t[p] >= fd) {
488
+ fall_t[p] = 0;
489
+ if (can_place(p, (s16)piece_x[p], (s16)(piece_y[p] + 1)))
490
+ ++piece_y[p];
491
+ else
492
+ lock_piece(p); /* may end the game */
493
+ }
494
+ }
495
+
496
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
497
+ * The falling trios are the ONLY sprites (board jewels are BG tiles — only
498
+ * what moves every frame earns OAM slots). oamSet's first arg is a BYTE
499
+ * OFFSET into OAM (slot*4), its gfxoffset is a tile INDEX into the OBJ page.
500
+ * Hiding = parking at y=240 (no oamSetEx churn). oamUpdate() queues the
501
+ * shadow table; PVSnesLib's VBlank ISR DMAs it to hardware on channel 7
502
+ * every NMI — so stage sprites BEFORE WaitForVBlank, never after. */
503
+ static void stage_pieces(void) {
504
+ u8 p, i, n;
505
+ s8 y;
506
+ for (p = 0; p < 2; p++) {
114
507
  for (i = 0; i < 3; i++) {
115
- r = piece_y + i;
116
- if (r < 0 || r >= ROWS) continue;
117
- for (c = 0; c <= COLS - 3; c++) {
118
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
119
- if (a != 0 && a == b && b == d) {
120
- grid[r][c] = 0;
121
- grid[r][c + 1] = 0;
122
- grid[r][c + 2] = 0;
123
- if (score < 65500) score += 30;
124
- sfx_play(2); /* triple-clear chime */
125
- }
126
- }
508
+ n = (u8)((p * 3 + i) << 2);
509
+ y = (s8)(piece_y[p] + i);
510
+ if (state == ST_PLAY && y >= 0 && (p == 0 || two_player))
511
+ oamSet(n, (u16)((well_tx[p] + piece_x[p]) << 3),
512
+ (u16)((WELL_TY + (u8)y) << 3), 3, 0, 0,
513
+ (u16)(BG_GEM_BASE - 1 + piece_col[p][i]), 0);
514
+ else
515
+ oamSet(n, 0, 240, 3, 0, 0, 0, 0); /* y=240 = hidden */
127
516
  }
128
- draw_grid();
517
+ }
129
518
  }
130
519
 
131
- static void render_score(void) {
132
- char buf[6];
133
- u16 v;
134
- s8 i;
135
- buf[0]='0'; buf[1]='0'; buf[2]='0'; buf[3]='0'; buf[4]='0'; buf[5]=0;
136
- v = score;
137
- for (i = 4; i >= 0; i--) { buf[i] = '0' + (v % 10); v /= 10; }
138
- consoleDrawText(20, 2, buf);
520
+ /* ── GAME LOGIC (clay) — state entries ─────────────────────────────────────── */
521
+ static void title_enter(void) {
522
+ clear_rows(0, 27);
523
+ consoleDrawText(10, 3, GAME_TITLE);
524
+ consoleDrawText(11, 5, "HI"); draw_hi(14, 5);
525
+ consoleDrawText(8, 14, "A - 1P MARATHON");
526
+ consoleDrawText(8, 16, "B - 2P VERSUS");
527
+ consoleDrawText(2, 26, "LR MOVE A B SPIN DOWN DROP");
528
+ paint_title_map();
529
+ paint_title_stripe(0);
530
+ prev_pad0 = 0xFFFF; /* swallow the press that ENTERED this state — without
531
+ * this, the START that left the game-over screen
532
+ * instantly starts a new 1P run (classic edge-detect
533
+ * reuse bug) */
534
+ state = ST_TITLE;
535
+ }
536
+
537
+ static void start_game(u8 versus) {
538
+ u8 p, k;
539
+ two_player = versus;
540
+ well_tx[0] = versus ? WELL_VS_P1 : WELL_1P_TX;
541
+ well_tx[1] = WELL_VS_P2;
542
+ /* Stir the PRNG with time-spent-on-title so runs differ. */
543
+ rng ^= (u16)((frames << 7) | frames);
544
+ if (rng == 0) rng = 0xACE1;
545
+ for (p = 0; p < 2; p++) {
546
+ u8 *g = GRIDOF(p);
547
+ for (k = 0; k < GRID_CELLS; k++) g[k] = EMPTY;
548
+ fall_t[p] = 0;
549
+ score[p] = 0;
550
+ garb_rows[p] = 0;
551
+ board_dirty[p] = 0;
552
+ prev_pad[p] = 0xFFFF; /* the button that started the game
553
+ * shouldn't also spin the first trio */
554
+ }
555
+ cleared_total = 0;
556
+ level = 1;
557
+ clear_rows(0, 27);
558
+ /* HUD: labels row 1, numbers row 2 */
559
+ consoleDrawText(2, 1, versus ? "P1" : "SC");
560
+ consoleDrawText(13, 1, "HI");
561
+ consoleDrawText(24, 1, versus ? "P2" : "LV");
562
+ draw_hud_num(0);
563
+ draw_hi(13, 2);
564
+ draw_hud_num(1);
565
+ paint_play_map();
566
+ state = ST_PLAY;
567
+ spawn_piece(0);
568
+ if (versus) spawn_piece(1);
569
+ }
570
+
571
+ /* Headless-test telemetry — see the static block's comment. */
572
+ static void telem_update(void) {
573
+ telem[0] = 'J'; telem[1] = 'W'; telem[2] = 0xBD;
574
+ telem[3] = state;
575
+ telem[4] = (u8)((sound_ok << 7) | two_player);
576
+ telem[5] = level;
577
+ telem[6] = (u8)score[0]; telem[7] = (u8)(score[0] >> 8);
578
+ telem[8] = (u8)score[1]; telem[9] = (u8)(score[1] >> 8);
579
+ telem[10] = piece_x[0]; telem[11] = (u8)piece_y[0];
580
+ telem[12] = piece_x[1]; telem[13] = (u8)piece_y[1];
581
+ telem[14] = (u8)hiscore; telem[15] = (u8)(hiscore >> 8);
582
+ telem[16] = garb_rows[0]; telem[17] = garb_rows[1];
583
+ telem[18] = (u8)(piece_col[0][0] | (piece_col[0][1] << 2) | (piece_col[0][2] << 4));
584
+ telem[19] = (u8)(piece_col[1][0] | (piece_col[1][1] << 2) | (piece_col[1][2] << 4));
585
+ telem[20] = (u8)((u16)grid); telem[21] = (u8)((u16)grid >> 8);
586
+ telem[22] = (u8)cleared_total; telem[23] = (u8)(cleared_total >> 8);
139
587
  }
140
588
 
141
589
  int main(void) {
142
- s16 r, c;
143
- u16 pad, prev = 0, fall_rate;
144
- u16 i;
145
- u8 t;
146
-
147
- consoleSetTextMapPtr(0x6800);
148
- consoleSetTextGfxPtr(0x3000);
149
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
150
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
151
- setMode(BG_MODE1, 0);
152
- /* consoleInitText DMAs the font but does NOT set the PPU BG base
153
- * registers — point BG0 at the same font ($3000) + map ($6800). */
154
- bgSetGfxPtr(0, 0x3000);
155
- bgSetMapPtr(0, 0x6800, SC_32x32);
156
-
157
- /* BG1 = full-screen wallpaper so the playfield never reads as blank.
158
- * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
159
- * the console gfx $3000 / map $6800). Map entries use palette block 1
160
- * (0x0400) so the wallpaper palette doesn't disturb the console font
161
- * palette in block 0 (HUD/grid text stays legible). */
162
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
163
- 32, 32, BG_16COLORS, 0x2000);
164
- for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
165
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
166
- bgSetEnable(1);
167
- bgSetDisable(2);
168
-
169
- for (r = 0; r < ROWS; r++)
170
- for (c = 0; c < COLS; c++)
171
- grid[r][c] = 0;
172
-
173
- score = 0;
174
- fall_timer = 0;
175
- new_piece();
176
-
177
- consoleDrawText(14, 2, "SCORE");
178
- consoleDrawText(2, 26, "LR MOVE A ROT START DROP");
179
- draw_grid();
180
-
181
- /* Screen ON first, THEN sound. sfx_init() must run AFTER setScreenOn()
182
- * (snes_sfx.h:63) — if the SPC stalls before the screen is on you get a
183
- * black/forced-blank screen forever. */
184
- setScreenOn();
185
- sfx_init();
186
-
187
- while (1) {
188
- pad = padsCurrent(0);
189
- draw_piece(1);
190
-
191
- if ((pad & KEY_LEFT) && !(prev & KEY_LEFT)
192
- && !collides(piece_x - 1, piece_y)) piece_x--;
193
- if ((pad & KEY_RIGHT) && !(prev & KEY_RIGHT)
194
- && !collides(piece_x + 1, piece_y)) piece_x++;
195
- if ((pad & KEY_A) && !(prev & KEY_A)) {
196
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
197
- sfx_play(1); /* rotate click */
198
- }
199
- if ((pad & KEY_START) && !(prev & KEY_START)) {
200
- while (!collides(piece_x, piece_y + 1)) piece_y++;
201
- lock_piece();
202
- new_piece();
203
- prev = pad;
204
- render_score();
205
- WaitForVBlank();
206
- consoleVblank();
207
- continue;
208
- }
209
- prev = pad;
210
-
211
- fall_rate = (pad & KEY_DOWN) ? 4 : 30;
212
- if (++fall_timer >= fall_rate) {
213
- fall_timer = 0;
214
- if (collides(piece_x, piece_y + 1)) {
215
- lock_piece();
216
- new_piece();
217
- } else {
218
- piece_y++;
219
- }
220
- }
590
+ u16 pad, newp;
591
+ u8 i;
221
592
 
222
- draw_piece(0);
223
- render_score();
224
- WaitForVBlank();
225
- consoleVblank();
593
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
594
+ * Init order: console text pointers FIRST, then mode, then VRAM uploads
595
+ * while the screen is still off (forced blank = unrestricted VRAM access;
596
+ * once the screen is on, only the vblank DMA path below may touch VRAM).
597
+ * consoleInitText DMAs the font but does NOT set the PPU BG base registers
598
+ * — point BG1 at the same font/map yourself. */
599
+ consoleSetTextMapPtr(0x6800);
600
+ consoleSetTextGfxPtr(0x3000);
601
+ consoleSetTextOffset(0x0000);
602
+ consoleInitText(0, 16 * 2, &tilfont, &palfont);
603
+ setMode(BG_MODE1, 0);
604
+ bgSetGfxPtr(0, 0x3000);
605
+ bgSetMapPtr(0, 0x6800, SC_32x32);
606
+
607
+ /* BG2 = the board layer: 8-tile set → VRAM $2000, palette → CGRAM block 1
608
+ * (map entries carry MAP_PAL1 so the console font palette in block 0 stays
609
+ * untouched), shadow map → VRAM $4000. */
610
+ bgInitTileSet(1, (u8 *)&tilboard, (u8 *)&palboard, 1,
611
+ 256, 32, BG_16COLORS, 0x2000);
612
+ paint_title_map();
613
+ bgInitMapSet(1, (u8 *)board_map, sizeof(board_map), SC_32x32, 0x4000);
614
+ bgSetEnable(1);
615
+ bgSetDisable(2); /* BG3 carries garbage in mode 1 */
616
+ setPaletteColor(0, RGB5(2, 2, 6)); /* backdrop: near-black indigo */
617
+
618
+ /* OBJ: the SAME 8 board tiles → OBJ base $6000 + palette → OBJ pal 0, so
619
+ * falling jewels match locked jewels exactly. 8x8 sprites (OBJ_SIZE8_L16,
620
+ * size bit stays small). */
621
+ oamInitGfxSet((u8 *)&tilboard, 256, (u8 *)&palboard, 32, 0, 0x6000,
622
+ OBJ_SIZE8_L16);
623
+ for (i = 0; i < 6; i++) oamSet((u8)(i << 2), 0, 240, 3, 0, 0, 0, 0);
624
+
625
+ setScreenOn();
626
+
627
+ /* ── HARDWARE IDIOM (load-bearing) — sfx_init AFTER setScreenOn, and CHECK
628
+ * the return: a wedged SPC700 must not take the video down with it. ── */
629
+ sound_ok = (sfx_init() == 0);
630
+ /* ── HARDWARE IDIOM (load-bearing) — one frame between init and the first
631
+ * command. sfx_init returns the instant the SPC echoes the jump command,
632
+ * but the driver then spends ~50 port writes initialising the DSP BEFORE
633
+ * it seeds its command edge-detector from $2140. Send a command in that
634
+ * window and the seed swallows it — music silently never starts. A
635
+ * WaitForVBlank is thousands of SPC cycles — deterministic cure. ── */
636
+ WaitForVBlank();
637
+ if (sound_ok) sfx_music_play();
638
+
639
+ hiscore = hi_load(); /* battery SRAM — 0 on first boot */
640
+ title_enter();
641
+
642
+ while (1) {
643
+ pad = padsCurrent(0);
644
+ newp = pad & (u16)~prev_pad0;
645
+ prev_pad0 = pad;
646
+
647
+ if (state == ST_TITLE) {
648
+ /* ── GAME LOGIC (clay) — title: A/START = 1P, B = 2P versus; the jewel
649
+ * stripe cycles its hues (board_map is live every frame — free juice) */
650
+ if ((frames & 31) == 0) paint_title_stripe((u8)(frames >> 5));
651
+ if (newp & (KEY_A | KEY_START)) start_game(0);
652
+ else if (newp & KEY_B) start_game(1);
653
+ } else if (state == ST_PLAY) {
654
+ /* ── GAME LOGIC (clay — reshape freely) ── */
655
+ update_player(0);
656
+ if (two_player && state == ST_PLAY) update_player(1);
657
+ if (board_dirty[0]) { paint_board(0); board_dirty[0] = 0; }
658
+ if (board_dirty[1]) { paint_board(1); board_dirty[1] = 0; }
659
+ } else { /* ST_OVER — boards stay frozen on screen */
660
+ if (newp & (KEY_START | KEY_A)) title_enter();
226
661
  }
227
- return 0;
662
+
663
+ stage_pieces(); /* sprites staged BEFORE the vblank wait */
664
+ telem_update();
665
+ frames++;
666
+ oamUpdate();
667
+
668
+ WaitForVBlank();
669
+ /* vblank-only writes — FIRST after the wait: the full-board DMA (see the
670
+ * shadow-map idiom above + the NES-contrast note in the header). */
671
+ dmaCopyVram((u8 *)board_map, 0x4000, sizeof(board_map));
672
+ consoleVblank();
673
+ }
674
+ return 0;
228
675
  }