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,141 +1,315 @@
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];
27
-
28
- #define COLS 6
29
- #define ROWS 12
30
-
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;
38
-
39
- static u32 xorshift(void) {
40
- rng ^= rng << 13;
41
- rng ^= rng >> 17;
42
- rng ^= rng << 5;
43
- return rng;
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 */
91
+
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 */
98
+
99
+ /* BG2 map entries select CGRAM palette block 1 (vhopppcc cccccccc). */
100
+ #define MAP_PAL1 0x0400
101
+
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);
91
- }
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
+ }
92
223
  }
93
224
 
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;
102
- }
103
- return 0;
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;
104
230
  }
105
231
 
106
- /* ── match / clear / gravity core (ported from the GBC reference puzzle).
107
- * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
108
- * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
109
- * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
110
- * 4 directions, clears them, applies per-column gravity, and loops so
111
- * cascades chain (score scales with chain depth). */
112
- static u8 matched[ROWS][COLS];
113
- static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
114
-
115
- static u8 mark_and_count(void) {
116
- u8 r, c, d, len, k, cnt;
117
- u8 col;
118
- s8 dr, dc;
119
- int sr, sc;
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);
120
294
  cnt = 0;
121
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
122
- for (r = 0; r < ROWS; r++) {
123
- for (c = 0; c < COLS; c++) {
124
- col = grid[r][c];
125
- if (col == 0) continue;
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;
126
300
  for (d = 0; d < 4; d++) {
127
- dr = DIRS4[d][0]; dc = DIRS4[d][1];
128
- sr = (int)r - dr; sc = (int)c - dc;
129
- if (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
130
- && grid[sr][sc] == col) continue; /* not the run's start */
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 */
131
305
  len = 1;
132
- sr = (int)r + dr; sc = (int)c + dc;
133
- while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
134
- && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
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; }
135
309
  if (len >= 3) {
136
310
  sr = r; sc = c;
137
311
  for (k = 0; k < len; k++) {
138
- if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
312
+ if (!matched[sr * GRID_W + sc]) { matched[sr * GRID_W + sc] = 1; cnt++; }
139
313
  sr += dr; sc += dc;
140
314
  }
141
315
  }
@@ -145,152 +319,357 @@ static u8 mark_and_count(void) {
145
319
  return cnt;
146
320
  }
147
321
 
148
- /* collapse each column so survivors rest on the floor (in place: walk
149
- * from the bottom, copying gems down to a write cursor, then zero above) */
150
- static void apply_gravity(void) {
151
- u8 c;
152
- int r, w;
153
- for (c = 0; c < COLS; c++) {
154
- w = ROWS - 1;
155
- for (r = ROWS - 1; r >= 0; r--) {
156
- if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
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--; }
157
331
  }
158
- for (; w >= 0; w--) grid[w][c] = 0;
332
+ for (; w >= 0; w--) g[w * GRID_W + c] = EMPTY;
333
+ }
334
+ }
335
+
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);
159
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;
160
351
  }
161
352
 
162
- static void resolve_board(void) {
163
- u8 n, r, c, chain;
164
- unsigned int amt;
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);
165
361
  chain = 0;
166
- while (1) {
167
- n = mark_and_count();
362
+ for (;;) {
363
+ n = mark_and_count(p);
168
364
  if (n == 0) break;
169
- chain++;
170
- for (r = 0; r < ROWS; r++)
171
- for (c = 0; c < COLS; c++)
172
- if (matched[r][c]) grid[r][c] = 0;
173
- amt = (unsigned int)n * 10u;
174
- if (chain > 1) amt = amt * chain;
175
- if (score < 65500) score += amt;
176
- sfx_play(2); /* clear chime */
177
- apply_gravity();
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
+ }
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;
178
422
  }
423
+ return 1;
179
424
  }
180
425
 
181
- static void lock_piece(void) {
182
- u16 i;
183
- s16 r;
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++) {
184
507
  for (i = 0; i < 3; i++) {
185
- r = piece_y + i;
186
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
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 */
187
516
  }
188
- resolve_board();
189
- draw_grid();
517
+ }
190
518
  }
191
519
 
192
- static void render_score(void) {
193
- char buf[6];
194
- u16 v;
195
- s8 i;
196
- buf[0]='0'; buf[1]='0'; buf[2]='0'; buf[3]='0'; buf[4]='0'; buf[5]=0;
197
- v = score;
198
- for (i = 4; i >= 0; i--) { buf[i] = '0' + (v % 10); v /= 10; }
199
- 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);
200
587
  }
201
588
 
202
589
  int main(void) {
203
- s16 r, c;
204
- u16 pad, prev = 0, fall_rate;
205
- u16 i;
206
- u8 t;
207
-
208
- consoleSetTextMapPtr(0x6800);
209
- consoleSetTextGfxPtr(0x3000);
210
- consoleSetTextOffset(0x0000); /* tile index = (char-0x20); font is at the BG char base */
211
- consoleInitText(0, 16 * 2, &tilfont, &palfont);
212
- setMode(BG_MODE1, 0);
213
- /* consoleInitText DMAs the font but does NOT set the PPU BG base
214
- * registers — point BG0 at the same font ($3000) + map ($6800). */
215
- bgSetGfxPtr(0, 0x3000);
216
- bgSetMapPtr(0, 0x6800, SC_32x32);
217
-
218
- /* BG1 = full-screen wallpaper so the playfield never reads as blank.
219
- * Tiles -> VRAM $2000, map -> VRAM $4000 (clear of sprites $0000 and
220
- * the console gfx $3000 / map $6800). Map entries use palette block 1
221
- * (0x0400) so the wallpaper palette doesn't disturb the console font
222
- * palette in block 0 (HUD/grid text stays legible). */
223
- bgInitTileSet(1, (u8 *)&tilbg, (u8 *)&palbg, 1,
224
- 32, 32, BG_16COLORS, 0x2000);
225
-
226
- /* Per-genre backdrop tint every SNES scaffold used to ship the same
227
- * blue checkered wallpaper ('no variety'). Recolor the wallpaper's
228
- * CGRAM entries (block 1 = entries 16+) to a deep violet scheme. */
229
- setPaletteColor(0, RGB5(4,2,8));
230
- setPaletteColor(17, RGB5(9,5,15));
231
- setPaletteColor(18, RGB5(6,3,11));
232
- for (i = 0; i < 32 * 32; i++) bg_map[i] = 0x0400;
233
- bgInitMapSet(1, (u8 *)bg_map, sizeof(bg_map), SC_32x32, 0x4000);
234
- bgSetEnable(1);
235
- bgSetDisable(2);
236
-
237
- for (r = 0; r < ROWS; r++)
238
- for (c = 0; c < COLS; c++)
239
- grid[r][c] = 0;
240
-
241
- score = 0;
242
- fall_timer = 0;
243
- new_piece();
244
-
245
- consoleDrawText(14, 2, "SCORE");
246
- consoleDrawText(2, 26, "LR MOVE A ROT START DROP");
247
- draw_grid();
248
-
249
- /* Screen ON first, THEN sound. sfx_init() must run AFTER setScreenOn()
250
- * (snes_sfx.h:63) — if the SPC stalls before the screen is on you get a
251
- * black/forced-blank screen forever. */
252
- setScreenOn();
253
- sfx_init();
254
-
255
- while (1) {
256
- pad = padsCurrent(0);
257
- draw_piece(1);
258
-
259
- if ((pad & KEY_LEFT) && !(prev & KEY_LEFT)
260
- && !collides(piece_x - 1, piece_y)) piece_x--;
261
- if ((pad & KEY_RIGHT) && !(prev & KEY_RIGHT)
262
- && !collides(piece_x + 1, piece_y)) piece_x++;
263
- if ((pad & KEY_A) && !(prev & KEY_A)) {
264
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
265
- sfx_play(1); /* rotate click */
266
- }
267
- if ((pad & KEY_START) && !(prev & KEY_START)) {
268
- while (!collides(piece_x, piece_y + 1)) piece_y++;
269
- lock_piece();
270
- new_piece();
271
- prev = pad;
272
- render_score();
273
- WaitForVBlank();
274
- consoleVblank();
275
- continue;
276
- }
277
- prev = pad;
278
-
279
- fall_rate = (pad & KEY_DOWN) ? 4 : 30;
280
- if (++fall_timer >= fall_rate) {
281
- fall_timer = 0;
282
- if (collides(piece_x, piece_y + 1)) {
283
- lock_piece();
284
- new_piece();
285
- } else {
286
- piece_y++;
287
- }
288
- }
590
+ u16 pad, newp;
591
+ u8 i;
592
+
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();
289
641
 
290
- draw_piece(0);
291
- render_score();
292
- WaitForVBlank();
293
- consoleVblank();
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();
294
661
  }
295
- 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;
296
675
  }