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,304 +1,885 @@
1
- /*
2
- * PC Engine "puzzle" — a match-3 falling-block scaffold.
1
+ /* ── main.c — PC Engine falling-trio versus puzzle (complete example game) ─────
3
2
  *
4
- * A 1x3 column of coloured blocks falls into a 6-wide x 12-tall well drawn with
5
- * background tiles. LEFT/RIGHT slide the piece, button I rotates the three
6
- * colours, DOWN soft-drops, button II hard-drops. When a piece locks, any
7
- * horizontal run of three same-colour cells clears and scores. Mirrors the
8
- * NES/Genesis/SNES/GB/SMS puzzle scaffolds, translated to the PCE helper API.
3
+ * TUMBLE TIDE a COMPLETE, working game: title screen, 1P MARATHON mode
4
+ * (levels speed the fall as you clear) and 2P SIMULTANEOUS VERSUS mode — two
5
+ * 6x12 wells side by side, P1 on the stock pad, P2 on the TurboTap's second
6
+ * pad, both falling at once, where every cascade chain you score sends a TIDE
7
+ * of garbage rows rising from the bottom of your rival's well. Score +
8
+ * in-session hi-score (a bare HuCard can't save — see the hi-score note
9
+ * below), PSG music + SFX.
9
10
  *
10
- * The whole field is drawn from BG tiles (no sprites needed) a grey wall
11
- * frame around a dim field interior, with R/G/B block tiles for the cells, so
12
- * the screen is clearly a populated playfield (clears the verify gate).
11
+ * The game: a falling-trio match-3. A vertical trio of pieces drops into a
12
+ * well; LEFT/RIGHT move it, I/II cycle its three colours, DOWN soft-drops,
13
+ * RUN hard-drops. When it lands, any straight run of 3+ same-coloured cells
14
+ * (horizontal, vertical, or diagonal) clears; survivors fall and cascades
15
+ * chain for multiplied score. First stack to reach the rim loses.
13
16
  *
14
- * PCE notes (see pce_hw.h / MENTAL_MODEL.md):
15
- * - bg_enable() turns on the BG plane + the VBlank IRQ (waitvsync needs it).
16
- * - .bss must be non-empty (pce_video.c's _pce_keep[] covers it).
17
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
18
+ * very different one. The markers tell you what's what:
19
+ * HARDWARE IDIOM (load-bearing) dodges a documented PCE footgun; reshape
20
+ * your gameplay around it (see TROUBLESHOOTING before changing).
21
+ * GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
17
22
  *
18
- * cc65 is C89 — declare locals at the top of a block.
23
+ * What depends on what:
24
+ * pce_hw.h / pce_video.c / pce_input.c / pce_sound.c — the helper lib
25
+ * (VDC/VCE/PSG register dances + joypad). The HARDWARE IDIOM markers in
26
+ * pce_video.c say which parts are load-bearing.
27
+ * cc65's pce crt0 + pce.lib are auto-linked; the 'rom32k' linker preset
28
+ * (applied automatically to example projects) gives a 32KB HuCard.
29
+ *
30
+ * 2P, honestly: the stock PC Engine has ONE controller port; 2P needs a
31
+ * TurboTap. The geargrafx core implements the TurboTap and the romdev host
32
+ * now force-ENABLES it (PLATFORM_CORE_OPTIONS pce: geargrafx_turbotap), so a
33
+ * second pad's input reaches the game on pad slot 2 — verified by driving
34
+ * port-1 input and seeing P2 move. So this game ships REAL simultaneous 2P
35
+ * versus. (On real hardware the player plugs a TurboTap and a second pad.)
36
+ *
37
+ * Frame budget (NTSC, 60fps) — and a TEACHING POINT vs the NES version of
38
+ * this game (examples/nes/templates/puzzle.c): on the NES, board repaints
39
+ * squeeze through a ~16-entry vblank queue, so a full-board repaint is
40
+ * BUDGETED across ~12 frames of dirty-row bitmask tricks. The PC Engine has
41
+ * no such famine: the VDC's VRAM write port streams words back-to-back, and a
42
+ * whole well is 24 tile rows x 12 tile cols = 288 BAT words. Two wells + the
43
+ * 6-entry SATB + the HUD all stream inside one vblank with budget to spare —
44
+ * so this version just REPAINTS THE WHOLE DIRTY WELL each time it changes (no
45
+ * dirty-row machinery at all). Same genre, two bandwidth worlds — fork
46
+ * accordingly.
19
47
  */
20
48
  #include <pce.h>
21
- #include <stdint.h> /* int8_t for signed grid coordinates */
49
+ #include <joystick.h> /* JOY_2 + joy_read for the 2nd pad (TurboTap port 1) */
22
50
  #include "pce_hw.h"
23
51
 
24
- /* ---- VRAM layout (word addresses) --------------------------------------- */
25
- #define BAT_VRAM 0x0000
26
- #define BG_VRAM 0x1000 /* cabinet background (dotted, colour 6/7) */
27
- #define RED_VRAM 0x1010 /* block colour 1 */
28
- #define GRN_VRAM 0x1020 /* block colour 2 */
29
- #define BLU_VRAM 0x1030 /* block colour 3 */
30
- #define WALL_VRAM 0x1040 /* well border (colour 4) */
31
- #define FIELD_VRAM 0x1050 /* dim empty field (colour 5) */
52
+ /* pce_hw.h gives us u8/u16; the match-scan + piece coords need signed types
53
+ * (cells can sit above the rim at negative rows). cc65's int is 16-bit. */
54
+ typedef signed char s8;
55
+ typedef int s16;
56
+
57
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
58
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
59
+ #define GAME_TITLE "TUMBLE TIDE"
60
+
61
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
62
+ * VRAM map (WORD addresses — the VDC is a 16-bit-word machine; an 8x8 tile is
63
+ * 16 words, a 16x16 sprite cell is 64). Sprites and BG tiles share one 64KB
64
+ * VRAM, so lay it out ONCE and keep the SATB out of pattern space:
65
+ * $0000 BAT (32x32 background map — matches vdc_init's VDC_MWR setting)
66
+ * $1000 font glyphs (38 tiles: blank, 0-9, A-Z, dash)
67
+ * $1400 board furniture tiles (backdrop, HUD band, frame, empty cell)
68
+ * $1500 CELL tiles: 3 colours, each its own 8x8 tile (a 16x16 cell is 2x2)
69
+ * $1800 16x16 trio SPRITE cells: 3 colours
70
+ * $7F00 shadow SATB destination (satb_dma copies it here, VDC reads it) */
71
+ #define BAT_VRAM 0x0000
72
+ #define FONT_VRAM 0x1000
73
+ #define BACK_VRAM 0x1400 /* solid colour 1 — cabinet backdrop */
74
+ #define BAND_VRAM 0x1410 /* solid colour 2 — band behind the HUD text */
75
+ #define FRAME_VRAM 0x1420 /* solid colour 3 — well border */
76
+ #define INNER_VRAM 0x1430 /* near-black well interior + faint speck */
77
+ #define CELL0_VRAM 0x1500 /* locked-cell BG tile, colour A (8x8) */
78
+ #define CELL1_VRAM 0x1510 /* locked-cell BG tile, colour B */
79
+ #define CELL2_VRAM 0x1520 /* locked-cell BG tile, colour C */
80
+ #define SPR0_VRAM 0x1800 /* 16x16 falling-trio sprite, colour A */
81
+ #define SPR1_VRAM 0x1840 /* 16x16 falling-trio sprite, colour B */
82
+ #define SPR2_VRAM 0x1880 /* 16x16 falling-trio sprite, colour C */
32
83
 
33
84
  #define BAT_ENTRY(pal, vram) ((u16)(((pal) << 12) | ((vram) >> 4)))
34
85
 
35
- #define COLS 6
36
- #define ROWS 12
37
- #define GRID_COL0 13 /* well's left BAT column (centres the 6-wide field) */
38
- #define GRID_ROW0 4 /* well's top BAT row */
39
-
40
- /* ---- state -------------------------------------------------------------- */
41
- static u8 grid[ROWS][COLS]; /* 0 = empty, 1..3 = colour */
42
- static u8 piece[3]; /* three stacked colours */
43
- static int8_t piece_x; /* column 0..COLS-1 */
44
- static int8_t piece_y; /* row of top cell (can be negative) */
45
- static u8 fall_timer;
46
- static u16 score;
47
- static u16 rng;
48
- static u8 pad, prev_pad;
49
- static u16 tile_buf[16];
86
+ /* Sprite pattern codes = VRAM >> 6 (the 16x16 cell index). */
87
+ #define SPR_PAT(c) ((u16)((SPR0_VRAM >> 6) + (c))) /* colour 0..2 */
88
+
89
+ /* ── GAME LOGIC (clay reshape freely) ──────────────────────────────────────
90
+ * Board geometry. Cells are 16x16 px (2x2 BAT tiles) — the PCE 256x224 screen
91
+ * has room to spare; chunky cells read better than 8-px ones. The BAT is
92
+ * 32 tiles wide; a 12-wide well (6 cells x 2 tiles) fits twice for the split
93
+ * board. Tile rows 0-1 sit under the HUD band; well interiors start at row 3. */
94
+ #define GRID_W 6
95
+ #define GRID_H 12
96
+ #define WELL_TR 3 /* top TILE row of the well interior */
97
+ #define WELL_1P_TC 10 /* 1P: single centered well (tiles 10-21) */
98
+ #define WELL_VS_P1 2 /* 2P: P1 interior tiles 2-13 ... */
99
+ #define WELL_VS_P2 18 /* P2 interior tiles 18-29 (split board) */
100
+ #define HUD_ROWS 2 /* BAT rows reserved for the HUD band */
101
+
102
+ #define EMPTY 0 /* cell colours 1..3 = amber/teal/magenta */
103
+
104
+ /* SATB slot plan: 3 trio sprites per player (slot order = priority). */
105
+ #define SLOT_TRIO(p, i) (u8)((p) * 3 + (i))
106
+ #define OFFSCREEN_Y 0x1F0 /* park hidden sprites below the display */
107
+ /* Each trio colour gets its OWN sprite sub-palette (1/2/3) so the falling
108
+ * pieces show their three distinct hues, matching the locked-board cells. */
109
+ #define PAL_TRIO(col) (u8)(col) /* colour 1..3 -> sprite sub-palette 1..3 */
110
+
111
+ /* ── GAME LOGIC (clay — reshape freely) ── game state ── */
112
+ static u8 grid[2][GRID_H][GRID_W]; /* the two wells (P2's unused in 1P) */
113
+ static s16 piece_x[2]; /* falling trio: column 0..5 */
114
+ static s16 piece_y[2]; /* row of its TOP cell (<0 above rim) */
115
+ static u8 piece_col[2][3]; /* trio colours, top to bottom */
116
+ static u16 score[2];
117
+ static u16 hiscore;
118
+ static u8 level; /* 1P: 1..9, speeds up the fall */
119
+ static u8 state; /* ST_TITLE / ST_PLAY / ST_OVER */
120
+ static u8 two_player;
121
+
122
+ static u8 matched[GRID_H][GRID_W];
123
+ static u8 well_tc[2]; /* left interior TILE column per well */
124
+ static u8 fall_t[2]; /* frames until next gravity step */
125
+ static u8 prev_pad[2]; /* for edge-triggered input */
126
+ static u16 cleared_total; /* 1P: cells cleared, drives the level */
127
+ static u8 board_dirty[2]; /* well needs a repaint this frame */
128
+ static u8 loser; /* who topped out (2P result text) */
129
+ static u16 rng = 0xACE1;
130
+ static u8 sfx_timer;
131
+ static u8 hud_dirty;
132
+
133
+ /* Game states — the shell every example shares: title → play → game over. */
134
+ #define ST_TITLE 0
135
+ #define ST_PLAY 1
136
+ #define ST_OVER 2
137
+
138
+ #define VS_FALL_DELAY 24 /* 2P: fixed gravity (frames per row) */
139
+ #define GARBAGE_CAP 4 /* max garbage rows per attack */
140
+
141
+ static u16 tile_buf[16]; /* scratch for one 8x8 tile */
142
+ static u16 spr_buf[64]; /* scratch for one 16x16 sprite cell */
143
+
144
+ /* ── GAME LOGIC (clay) — 5x7 glyph font: blank, 0-9, A-Z, dash ──────────────
145
+ * Each glyph is 7 rows of 5 bits (bit4 = leftmost). upload_font() expands
146
+ * them into 8x8 1-plane tiles; drawn with BG sub-palette 1 (white). */
147
+ #define G_BLANK 0
148
+ #define G_DIGIT 1 /* '0'..'9' -> glyphs 1..10 */
149
+ #define G_ALPHA 11 /* 'A'..'Z' -> glyphs 11..36 */
150
+ #define G_DASH 37
151
+ #define NUM_GLYPHS 38
152
+
153
+ static const u8 FONT5x7[NUM_GLYPHS][7] = {
154
+ {0,0,0,0,0,0,0},
155
+ {0x0E,0x11,0x13,0x15,0x19,0x11,0x0E}, {0x04,0x0C,0x04,0x04,0x04,0x04,0x0E},
156
+ {0x0E,0x11,0x01,0x02,0x04,0x08,0x1F}, {0x1F,0x02,0x04,0x02,0x01,0x11,0x0E},
157
+ {0x02,0x06,0x0A,0x12,0x1F,0x02,0x02}, {0x1F,0x10,0x1E,0x01,0x01,0x11,0x0E},
158
+ {0x06,0x08,0x10,0x1E,0x11,0x11,0x0E}, {0x1F,0x01,0x02,0x04,0x08,0x08,0x08},
159
+ {0x0E,0x11,0x11,0x0E,0x11,0x11,0x0E}, {0x0E,0x11,0x11,0x0F,0x01,0x02,0x0C},
160
+ {0x0E,0x11,0x11,0x1F,0x11,0x11,0x11}, {0x1E,0x11,0x11,0x1E,0x11,0x11,0x1E},
161
+ {0x0E,0x11,0x10,0x10,0x10,0x11,0x0E}, {0x1E,0x11,0x11,0x11,0x11,0x11,0x1E},
162
+ {0x1F,0x10,0x10,0x1E,0x10,0x10,0x1F}, {0x1F,0x10,0x10,0x1E,0x10,0x10,0x10},
163
+ {0x0E,0x11,0x10,0x17,0x11,0x11,0x0F}, {0x11,0x11,0x11,0x1F,0x11,0x11,0x11},
164
+ {0x0E,0x04,0x04,0x04,0x04,0x04,0x0E}, {0x07,0x02,0x02,0x02,0x02,0x12,0x0C},
165
+ {0x11,0x12,0x14,0x18,0x14,0x12,0x11}, {0x10,0x10,0x10,0x10,0x10,0x10,0x1F},
166
+ {0x11,0x1B,0x15,0x15,0x11,0x11,0x11}, {0x11,0x19,0x15,0x13,0x11,0x11,0x11},
167
+ {0x0E,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x1E,0x11,0x11,0x1E,0x10,0x10,0x10},
168
+ {0x0E,0x11,0x11,0x11,0x15,0x12,0x0D}, {0x1E,0x11,0x11,0x1E,0x14,0x12,0x11},
169
+ {0x0F,0x10,0x10,0x0E,0x01,0x01,0x1E}, {0x1F,0x04,0x04,0x04,0x04,0x04,0x04},
170
+ {0x11,0x11,0x11,0x11,0x11,0x11,0x0E}, {0x11,0x11,0x11,0x11,0x11,0x0A,0x04},
171
+ {0x11,0x11,0x11,0x15,0x15,0x15,0x0A}, {0x11,0x11,0x0A,0x04,0x0A,0x11,0x11},
172
+ {0x11,0x11,0x0A,0x04,0x04,0x04,0x04}, {0x1F,0x01,0x02,0x04,0x08,0x10,0x1F},
173
+ {0x00,0x00,0x00,0x1F,0x00,0x00,0x00},
174
+ };
50
175
 
176
+ /* ── GAME LOGIC (clay) — a 16x16 round-cell mask (16 rows × 16 bits, bit15
177
+ * leftmost). The falling-trio sprites use this whole; one piece of art, three
178
+ * colours (the colour is the PALETTE, not the bits). */
179
+ static const u16 cell_mask[16] = {
180
+ 0x07E0, 0x1FF8, 0x3FFC, 0x7E7E, 0x7C3E, 0xFC3F, 0xFFFF, 0xFFFF,
181
+ 0xFFFF, 0xFFFF, 0xFC3F, 0x7C3E, 0x7E7E, 0x3FFC, 0x1FF8, 0x07E0
182
+ };
183
+
184
+ /* ── GAME LOGIC (clay) — tile/sprite builders ────────────────────────────── */
51
185
  static void make_solid_tile(u16 *t, u8 ci) {
52
186
  u8 r;
53
187
  u8 p0 = (ci & 1) ? 0xFF : 0x00;
54
188
  u8 p1 = (ci & 2) ? 0xFF : 0x00;
55
- u8 p2 = (ci & 4) ? 0xFF : 0x00;
56
- u8 p3 = (ci & 8) ? 0xFF : 0x00;
57
189
  for (r = 0; r < 8; ++r) {
58
190
  t[r] = (u16)(p0 | (p1 << 8));
59
- t[r + 8] = (u16)(p2 | (p3 << 8));
191
+ t[r + 8] = 0;
60
192
  }
61
193
  }
62
194
 
63
- /* A block tile: a solid `ci`-colour body with a 1px `frame`-colour border on
64
- * all four edges, so adjacent same-colour blocks still read as distinct cells.
65
- * For each of the 8 rows we pick a per-plane mask: border rows (0,7) are all
66
- * `frame`; interior rows are `ci` body with the left/right edge pixels framed. */
67
- static void make_block_tile(u16 *t, u8 ci, u8 frame) {
195
+ /* one-colour 16x16 sprite cell from a 16-row mask (colour = plane0 index 1) */
196
+ static void make_sprite16(u16 vram, const u16 *mask) {
68
197
  u8 r;
69
- for (r = 0; r < 8; ++r) {
70
- u8 edge_row = (r == 0 || r == 7);
71
- /* body colour planes (fill the whole row) */
72
- u8 b0 = (ci & 1) ? 0xFF : 0x00, b1 = (ci & 2) ? 0xFF : 0x00;
73
- u8 b2 = (ci & 4) ? 0xFF : 0x00, b3 = (ci & 8) ? 0xFF : 0x00;
74
- /* frame colour planes */
75
- u8 f0 = (frame & 1) ? 0xFF : 0x00, f1 = (frame & 2) ? 0xFF : 0x00;
76
- u8 f2 = (frame & 4) ? 0xFF : 0x00, f3 = (frame & 8) ? 0xFF : 0x00;
77
- u8 p0, p1, p2, p3;
78
- if (edge_row) {
79
- p0 = f0; p1 = f1; p2 = f2; p3 = f3; /* whole row framed */
80
- } else {
81
- /* body fill, but pixels 0 and 7 (mask 0x81) use the frame colour */
82
- p0 = (u8)((b0 & 0x7E) | (f0 & 0x81));
83
- p1 = (u8)((b1 & 0x7E) | (f1 & 0x81));
84
- p2 = (u8)((b2 & 0x7E) | (f2 & 0x81));
85
- p3 = (u8)((b3 & 0x7E) | (f3 & 0x81));
86
- }
87
- t[r] = (u16)(p0 | (p1 << 8));
88
- t[r + 8] = (u16)(p2 | (p3 << 8));
89
- }
198
+ for (r = 0; r < 64; ++r) spr_buf[r] = 0;
199
+ for (r = 0; r < 16; ++r) spr_buf[r] = mask[r]; /* plane 0 → colour 1 */
200
+ load_tiles(vram, spr_buf, 64);
90
201
  }
91
202
 
92
- /* Cabinet background tile: every pixel is colour 6, with a colour-7 dot on a
93
- * sparse lattice, so the whole screen reads as an intentional textured backdrop
94
- * rather than the flat hardware backdrop. Colour 6 = planes 1+2; colour 7 adds
95
- * plane 0 (so dots = planes 0+1+2). Build per-plane row bytes:
96
- * plane0 (low byte words 0..7) = dot mask (only dot pixels)
97
- * plane1 (high byte words 0..7) = 0xFF (colour 6 base, all pixels)
98
- * plane2 (low byte words 8..15) = 0xFF (colour 6 base, all pixels)
99
- * plane3 (high byte words 8..15)= 0 */
100
- static void make_dots_tile(u16 *t) {
203
+ /* A locked-board cell is a chunky 8x8 "pip" tile (a 16x16 cell is 2x2 of it):
204
+ * a filled colour-1 square with a dark 1-px rim on every edge so adjacent
205
+ * cells read as separate pieces. The colour is the PALETTE, not the bits. */
206
+ static void make_cell_tile(u16 *t) {
101
207
  u8 r;
102
208
  for (r = 0; r < 8; ++r) {
103
- u8 dot = ((r & 3) == 0) ? 0x22 : 0x00; /* dot columns every 4 px */
104
- t[r] = (u16)(dot | 0xFF00u); /* plane0=dots, plane1=base */
105
- t[r + 8] = (u16)0x00FFu; /* plane2=base, plane3=0 */
209
+ u16 fill = 0x00FF; /* plane0 all set colour 1 */
210
+ if (r == 0 || r == 7) fill = 0; /* clear top/bottom rim rows */
211
+ else fill &= 0x007E; /* clear left+right edge columns */
212
+ t[r] = fill;
213
+ t[r + 8] = 0;
214
+ }
215
+ }
216
+
217
+ static void upload_font(void) {
218
+ u8 g, row, bits, px;
219
+ for (g = 0; g < NUM_GLYPHS; ++g) {
220
+ for (row = 0; row < 16; ++row) tile_buf[row] = 0;
221
+ for (row = 0; row < 7; ++row) {
222
+ bits = FONT5x7[g][row];
223
+ px = 0;
224
+ if (bits & 0x10) px |= 0x40;
225
+ if (bits & 0x08) px |= 0x20;
226
+ if (bits & 0x04) px |= 0x10;
227
+ if (bits & 0x02) px |= 0x08;
228
+ if (bits & 0x01) px |= 0x04;
229
+ tile_buf[row] = (u16)px;
230
+ }
231
+ load_tiles((u16)(FONT_VRAM + g * 16), tile_buf, 16);
106
232
  }
107
233
  }
108
234
 
109
235
  static void upload_art(void) {
110
- make_dots_tile(tile_buf); load_tiles(BG_VRAM, tile_buf, 16);
111
- make_block_tile(tile_buf, 1, 6); load_tiles(RED_VRAM, tile_buf, 16);
112
- make_block_tile(tile_buf, 2, 6); load_tiles(GRN_VRAM, tile_buf, 16);
113
- make_block_tile(tile_buf, 3, 6); load_tiles(BLU_VRAM, tile_buf, 16);
114
- make_solid_tile(tile_buf, 4); load_tiles(WALL_VRAM, tile_buf, 16);
115
- make_solid_tile(tile_buf, 5); load_tiles(FIELD_VRAM, tile_buf, 16);
236
+ upload_font();
237
+ make_solid_tile(tile_buf, 1); load_tiles(BACK_VRAM, tile_buf, 16);
238
+ make_solid_tile(tile_buf, 2); load_tiles(BAND_VRAM, tile_buf, 16);
239
+ make_solid_tile(tile_buf, 3); load_tiles(FRAME_VRAM, tile_buf, 16);
240
+ /* well interior: near-black colour-1 with one faint colour-3 speck */
241
+ make_solid_tile(tile_buf, 1); tile_buf[4] |= 0x1000; tile_buf[4 + 8] = 0x1000;
242
+ load_tiles(INNER_VRAM, tile_buf, 16);
243
+ /* one cell tile shape, reused for all three colours (palette gives hue) */
244
+ make_cell_tile(tile_buf);
245
+ load_tiles(CELL0_VRAM, tile_buf, 16);
246
+ load_tiles(CELL1_VRAM, tile_buf, 16);
247
+ load_tiles(CELL2_VRAM, tile_buf, 16);
248
+ make_sprite16(SPR0_VRAM, cell_mask);
249
+ make_sprite16(SPR1_VRAM, cell_mask);
250
+ make_sprite16(SPR2_VRAM, cell_mask);
251
+ }
252
+
253
+ /* cell colour 1..3 → its locked-board BG tile VRAM */
254
+ static u16 cell_vram(u8 col) {
255
+ return (col == 1) ? CELL0_VRAM : (col == 2) ? CELL1_VRAM : CELL2_VRAM;
116
256
  }
117
257
 
118
- static u16 vram_for(u8 cell) {
119
- if (cell == 1) return RED_VRAM;
120
- if (cell == 2) return GRN_VRAM;
121
- if (cell == 3) return BLU_VRAM;
122
- return FIELD_VRAM; /* empty -> dim field interior */
258
+ /* ── GAME LOGIC (clay) — BAT text + board paint ──────────────────────────── */
259
+ static void put_glyph(u8 col, u8 row, u8 glyph) {
260
+ u16 e = BAT_ENTRY(1, (u16)(FONT_VRAM + glyph * 16)); /* pal 1 = white */
261
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
262
+ VDC_DATA_LO = (u8)(e & 0xFF);
263
+ VDC_DATA_HI = (u8)(e >> 8);
123
264
  }
124
265
 
125
- static void put_cell(u8 batCol, u8 batRow, u16 vram) {
126
- u16 e = BAT_ENTRY(0, vram);
127
- vram_set_write_addr((u16)(BAT_VRAM + batRow * 32 + batCol));
266
+ static void put_tile(u8 col, u8 row, u16 e) {
267
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
128
268
  VDC_DATA_LO = (u8)(e & 0xFF);
129
269
  VDC_DATA_HI = (u8)(e >> 8);
130
270
  }
131
271
 
132
- /* fill the whole BAT with the cabinet background tile */
133
- static void clear_bat(void) {
272
+ static void draw_text(u8 col, u8 row, const char *s) {
273
+ u8 c;
274
+ while ((c = (u8)*s++) != 0) {
275
+ u8 g = G_BLANK;
276
+ if (c >= '0' && c <= '9') g = (u8)(G_DIGIT + c - '0');
277
+ else if (c >= 'A' && c <= 'Z') g = (u8)(G_ALPHA + c - 'A');
278
+ else if (c == '-') g = G_DASH;
279
+ put_glyph(col++, row, g);
280
+ }
281
+ }
282
+
283
+ static void draw_num5(u8 col, u8 row, u16 v) {
284
+ u8 i, d[5];
285
+ for (i = 0; i < 5; ++i) { d[i] = (u8)(v % 10); v /= 10; }
286
+ for (i = 0; i < 5; ++i) put_glyph((u8)(col + i), row, (u8)(G_DIGIT + d[4 - i]));
287
+ }
288
+
289
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
290
+ * WHOLE-BOARD BAT REPAINT — the PCE's puzzle bandwidth, the inverse of the
291
+ * NES famine. A locked cell is two-by-two of one BG tile (on its own colour
292
+ * sub-palette); an empty cell is two-by-two of the INNER tile. When a board
293
+ * changes we simply rewrite ALL 24x12 of its BAT entries — 288 word writes
294
+ * straight at the VDC's VWR port (vram_set_write_addr arms the auto-
295
+ * incrementing address, then we stream). The whole well streams in well under
296
+ * a vblank; both wells + the SATB + HUD fit one frame. The NES version of THIS
297
+ * GAME budgets the same repaint across ~12 frames through a 16-entry queue —
298
+ * the PCE just blasts it. Two rules:
299
+ * - do the streaming inside the vblank window (we repaint just after
300
+ * waitvsync()), so the VDC isn't fetching the BAT for display mid-write;
301
+ * - keep the SATB-DMA after the BAT writes — both share the VDC and the DMA
302
+ * wants the address latch left where it expects it.
303
+ *
304
+ * requires: BAT 32x32 (vdc_init's MWR); well within the 32-wide BAT (it is:
305
+ * the split board uses tiles 2-13 and 18-29). */
306
+ static void paint_board(u8 p) {
307
+ u8 r, c, tr, tc;
308
+ u16 e_inner = BAT_ENTRY(0, INNER_VRAM);
309
+ u8 left = well_tc[p];
310
+ for (r = 0; r < GRID_H; r++) {
311
+ for (c = 0; c < GRID_W; c++) {
312
+ u8 col = grid[p][r][c];
313
+ /* each locked colour gets its own BG sub-palette (3/4/5) so the
314
+ * one cell-tile shape (all pixels colour index 1) renders three
315
+ * distinct hues; empty interior uses sub-palette 0 (backdrop). */
316
+ u16 e = col ? BAT_ENTRY(2 + col, cell_vram(col)) : e_inner;
317
+ tr = (u8)(WELL_TR + r * 2);
318
+ tc = (u8)(left + c * 2);
319
+ put_tile(tc, tr, e); /* 2x2 of the cell tile */
320
+ put_tile((u8)(tc + 1), tr, e);
321
+ put_tile(tc, (u8)(tr + 1), e);
322
+ put_tile((u8)(tc + 1), (u8)(tr + 1), e);
323
+ }
324
+ }
325
+ }
326
+
327
+ static void paint_frame(u8 p) {
328
+ u8 r, tr, c;
329
+ u16 e = BAT_ENTRY(0, FRAME_VRAM);
330
+ u8 x0 = (u8)(well_tc[p] - 1);
331
+ u8 w = (u8)(GRID_W * 2 + 2);
332
+ /* top + bottom rails */
333
+ for (c = 0; c < w; c++) {
334
+ put_tile((u8)(x0 + c), (u8)(WELL_TR - 1), e);
335
+ put_tile((u8)(x0 + c), (u8)(WELL_TR + GRID_H * 2), e);
336
+ }
337
+ /* side rails */
338
+ for (r = 0; r < GRID_H * 2; r++) {
339
+ tr = (u8)(WELL_TR + r);
340
+ put_tile(x0, tr, e);
341
+ put_tile((u8)(x0 + GRID_W * 2 + 1), tr, e);
342
+ }
343
+ }
344
+
345
+ /* Fill the whole 32x32 BAT: HUD band on the top rows, backdrop below. */
346
+ static void paint_backdrop(void) {
134
347
  u8 r, c;
135
- u16 e = BAT_ENTRY(0, BG_VRAM);
136
- for (r = 0; r < 32; ++r) {
348
+ u16 band = BAT_ENTRY(0, BAND_VRAM);
349
+ u16 back = BAT_ENTRY(0, BACK_VRAM);
350
+ for (r = 0; r < 32; r++) {
137
351
  vram_set_write_addr((u16)(BAT_VRAM + r * 32));
138
- for (c = 0; c < 32; ++c) {
352
+ for (c = 0; c < 32; c++) {
353
+ u16 e = (r < HUD_ROWS) ? band : back;
139
354
  VDC_DATA_LO = (u8)(e & 0xFF);
140
355
  VDC_DATA_HI = (u8)(e >> 8);
141
356
  }
142
357
  }
143
358
  }
144
359
 
145
- /* draw the well frame + dim interior */
146
- static void draw_well(void) {
147
- int8_t r, c;
148
- for (r = -1; r <= ROWS; ++r) {
149
- for (c = -1; c <= COLS; ++c) {
150
- u16 vram = (r == -1 || r == ROWS || c == -1 || c == COLS)
151
- ? WALL_VRAM : FIELD_VRAM;
152
- put_cell((u8)(GRID_COL0 + c), (u8)(GRID_ROW0 + r), vram);
360
+ /* HUD: row 0 = "SC 00000 HI 00000 LV 1" (1P) or "P1 .. HI .. P2 .." (2P). */
361
+ static void draw_hud(void) {
362
+ u8 i;
363
+ /* clear the HUD text row before repainting (band tile under the glyphs) */
364
+ for (i = 0; i < 32; i++) put_tile(i, 0, BAT_ENTRY(0, BAND_VRAM));
365
+ if (state == ST_TITLE) {
366
+ draw_text(13, 0, "HI");
367
+ draw_num5(16, 0, hiscore);
368
+ return;
369
+ }
370
+ if (two_player) {
371
+ draw_text(1, 0, "P1");
372
+ draw_num5(4, 0, score[0]);
373
+ draw_text(12, 0, "HI");
374
+ draw_num5(15, 0, hiscore);
375
+ draw_text(24, 0, "P2");
376
+ draw_num5(27, 0, score[1]);
377
+ } else {
378
+ draw_text(1, 0, "SC");
379
+ draw_num5(4, 0, score[0]);
380
+ draw_text(12, 0, "HI");
381
+ draw_num5(15, 0, hiscore);
382
+ draw_text(24, 0, "LV");
383
+ put_glyph(27, 0, (u8)(G_DIGIT + level));
384
+ }
385
+ }
386
+
387
+ /* ── HARDWARE TRUTH: a bare HuCard CANNOT save a hi-score (in-session only) ──
388
+ * This was researched and corrected: earlier versions wrote the hi-score to
389
+ * BRAM ("backup RAM", bank $F7) and claimed it persisted across power cycles.
390
+ * That is NOT honest for a HuCard game. On REAL hardware a plain HuCard plugged
391
+ * into a base PC Engine / TurboGrafx-16 has NO backup RAM at all — BRAM exists
392
+ * ONLY when a peripheral is attached: the CD-ROM² System (2KB kept by a
393
+ * supercapacitor), the Tennokoe Bank HuCard, or the Memory Base 128. No
394
+ * commercial HuCard self-saved; they used PASSWORDS. (The often-cited Populous
395
+ * "ROMRAM" SRAM was the game's own working RAM, not a battery save.) An
396
+ * emulator like geargrafx exposes BRAM unconditionally, so the old code
397
+ * "worked" in emulation in a way the real machine never would.
398
+ *
399
+ * So this game keeps an IN-SESSION hi-score only (like the honest 2600/Lynx
400
+ * examples) — it survives game-overs within a power-on, resets to 0 on a cold
401
+ * boot. To make it ACTUALLY persist on real hardware you would target a
402
+ * peripheral: write to BRAM only after detecting one (and go through the System
403
+ * Card BIOS's 'HUBM' directory for CD saves), or move the game to a CD-ROM²
404
+ * build. Either is a real-hardware feature, not a property of the cartridge. */
405
+ static u16 hiscore_load(void) {
406
+ return 0; /* cold boot: no persistence on a bare HuCard */
407
+ }
408
+
409
+ static void hiscore_save(u16 v) {
410
+ (void)v; /* in-session only — nowhere to persist on real HW */
411
+ }
412
+
413
+ /* ── GAME LOGIC (clay) — music: a 2-channel tune ticked once per frame ──────
414
+ * PSG channel plan: 5 = melody, 4 = bass, 2/3 = SFX (tones cut by sfx_timer).
415
+ * PCE frequency regs are DIVIDERS: pitch ≈ 3.58MHz / (32 × value), so a
416
+ * BIGGER number is a LOWER note. Note indices into NOTE_DIV below. */
417
+ enum { R = 0, A2N, C3, F3, G3, A3, B3, C4, D4, E4, F4, G4, A4, B4, C5, D5, E5 };
418
+ static const u16 NOTE_DIV[17] = {
419
+ 0, 1017, 854, 641, 571, 508, 453, 427, 381, 339, 320, 285, 254, 226, 214, 190, 170
420
+ };
421
+ /* 16 melody steps + 8 bass steps (one bass note per 2 melody steps) */
422
+ static const u8 MEL_TITLE[16] = { A3,C4,E4,A4, G4,E4,C4,E4, F4,A4,C5,A4, G4,E4,D4,C4 };
423
+ static const u8 BAS_TITLE[8] = { A2N,A2N, F3,F3, C3,C3, G3,G3 };
424
+ static const u8 MEL_PLAY[16] = { C4,E4,G4,E4, D4,F4,A4,F4, E4,G4,C5,G4, A4,G4,E4,R };
425
+ static const u8 BAS_PLAY[8] = { C3,C3, F3,F3, A2N,A2N, G3,G3 };
426
+ static const u8 MEL_OVER[16] = { C5,R,A4,R, F4,R,E4,R, D4,R,C4,R, A2N,R,R,R };
427
+
428
+ static u8 music_song; /* reuses the ST_* ids */
429
+ static u8 music_step, music_timer, music_done;
430
+
431
+ static void music_set(u8 song) {
432
+ music_song = song;
433
+ music_step = 0;
434
+ music_timer = 0;
435
+ music_done = 0;
436
+ psg_off(4);
437
+ psg_off(5);
438
+ }
439
+
440
+ static void music_tick(void) {
441
+ const u8 *mel;
442
+ u8 n;
443
+ if (music_done) return;
444
+ if (music_timer == 0) {
445
+ mel = (music_song == ST_PLAY) ? MEL_PLAY
446
+ : (music_song == ST_OVER) ? MEL_OVER : MEL_TITLE;
447
+ n = mel[music_step & 15];
448
+ if (n != R) psg_tone(5, NOTE_DIV[n], 26);
449
+ else psg_off(5);
450
+ if (music_song != ST_OVER) { /* the game-over jingle has no bass */
451
+ n = ((music_step & 1) == 0)
452
+ ? ((music_song == ST_PLAY) ? BAS_PLAY[(music_step >> 1) & 7]
453
+ : BAS_TITLE[(music_step >> 1) & 7])
454
+ : R;
455
+ if (n != R) psg_tone(4, NOTE_DIV[n], 20);
456
+ }
457
+ ++music_step;
458
+ if (music_song == ST_OVER && music_step >= 16) { /* play once, stop */
459
+ music_done = 1;
460
+ psg_off(4);
461
+ psg_off(5);
462
+ }
463
+ }
464
+ ++music_timer;
465
+ if (music_timer >= 9) music_timer = 0;
466
+ }
467
+
468
+ /* short SFX on channels 2/3, auto-cut by sfx_timer */
469
+ static void sfx(u8 chan, u16 freq, u8 frames) {
470
+ psg_tone(chan, freq, 31);
471
+ if (frames > sfx_timer) sfx_timer = frames;
472
+ }
473
+
474
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG ── */
475
+ static u8 random8(void) {
476
+ u16 r = rng;
477
+ r ^= r << 7;
478
+ r ^= r >> 9;
479
+ r ^= r << 8;
480
+ rng = r;
481
+ return (u8)r;
482
+ }
483
+
484
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
485
+ * Match scan: mark every straight run of 3+ same-coloured cells in all 4
486
+ * directions (a cell can belong to several runs — the mask de-dupes), and
487
+ * return how many cells matched. Runs flat-out on the HuC6280 — no need to
488
+ * smear it across frames like the cc65 NES version's queue dance. */
489
+ static const s8 DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
490
+
491
+ static u8 mark_and_count(u8 p) {
492
+ u8 r, c, d, len, k, cnt, col;
493
+ s8 dr, dc;
494
+ s16 sr, sc;
495
+ cnt = 0;
496
+ for (r = 0; r < GRID_H; r++)
497
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
498
+ for (r = 0; r < GRID_H; r++) {
499
+ for (c = 0; c < GRID_W; c++) {
500
+ col = grid[p][r][c];
501
+ if (col == EMPTY) continue;
502
+ for (d = 0; d < 4; d++) {
503
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
504
+ sr = (s16)r - dr; sc = (s16)c - dc;
505
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
506
+ && grid[p][sr][sc] == col) continue; /* not the run's start */
507
+ len = 1;
508
+ sr = (s16)r + dr; sc = (s16)c + dc;
509
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
510
+ && grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
511
+ if (len >= 3) {
512
+ sr = r; sc = c;
513
+ for (k = 0; k < len; k++) {
514
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
515
+ sr += dr; sc += dc;
516
+ }
517
+ }
518
+ }
153
519
  }
154
520
  }
521
+ return cnt;
155
522
  }
156
523
 
157
- static void draw_grid(void) {
158
- u8 r, c;
159
- for (r = 0; r < ROWS; ++r)
160
- for (c = 0; c < COLS; ++c)
161
- put_cell((u8)(GRID_COL0 + c), (u8)(GRID_ROW0 + r), vram_for(grid[r][c]));
524
+ /* Collapse each column so survivors rest on the floor. */
525
+ static void apply_gravity(u8 p) {
526
+ u8 c;
527
+ s16 r, w;
528
+ for (c = 0; c < GRID_W; c++) {
529
+ w = GRID_H - 1;
530
+ for (r = GRID_H - 1; r >= 0; r--) {
531
+ if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
532
+ }
533
+ for (; w >= 0; w--) grid[p][w][c] = EMPTY;
534
+ }
162
535
  }
163
536
 
164
- static u16 next_rand(void) {
165
- rng = (u16)(rng * 25173u + 13849u);
166
- return rng;
537
+ /* ── GAME LOGIC (clay) — end of game (top-out). `who` topped out. ── */
538
+ static void game_end(u8 who) {
539
+ u16 best = score[0];
540
+ if (two_player && score[1] > best) best = score[1];
541
+ if (best > hiscore) {
542
+ hiscore = best;
543
+ hiscore_save(hiscore); /* in-session only (no save on a bare HuCard) */
544
+ }
545
+ loser = who;
546
+ sfx(3, 0x500, 24); /* game-over rumble */
547
+ state = ST_OVER;
548
+ board_dirty[0] = board_dirty[1] = 0;
549
+ prev_pad[0] = prev_pad[1] = 0xFF; /* require a fresh press */
550
+ /* paint the result screen onto the BAT */
551
+ paint_backdrop();
552
+ if (two_player)
553
+ draw_text(13, 8, loser ? "P1 WINS" : "P2 WINS");
554
+ else
555
+ draw_text(12, 8, "GAME OVER");
556
+ draw_text(11, 12, "P1");
557
+ draw_num5(15, 12, score[0]);
558
+ if (two_player) {
559
+ draw_text(11, 14, "P2");
560
+ draw_num5(15, 14, score[1]);
561
+ }
562
+ draw_text(11, 17, "HI");
563
+ draw_num5(15, 17, hiscore);
564
+ draw_text(9, 21, "RUN - TITLE");
565
+ music_set(ST_OVER);
167
566
  }
168
- static u8 rand_color(void) { return (u8)(1 + (next_rand() >> 8) % 3); }
169
567
 
170
- static void new_piece(void) {
171
- piece[0] = rand_color();
172
- piece[1] = rand_color();
173
- piece[2] = rand_color();
174
- piece_x = COLS / 2 - 1;
175
- piece_y = -3;
568
+ /* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
569
+ * Returns the chain depth (0 = the lock matched nothing). */
570
+ static u8 resolve_board(u8 p) {
571
+ u8 n, r, c, chain;
572
+ u16 amt;
573
+ chain = 0;
574
+ for (;;) {
575
+ n = mark_and_count(p);
576
+ if (n == 0) break;
577
+ ++chain;
578
+ for (r = 0; r < GRID_H; r++)
579
+ for (c = 0; c < GRID_W; c++)
580
+ if (matched[r][c]) grid[p][r][c] = EMPTY;
581
+ amt = (u16)n * 10;
582
+ if (chain > 1) amt *= chain; /* cascades pay multiplied */
583
+ if (score[p] < 65000u) score[p] += amt;
584
+ /* clear chime — pitch rises with chain depth (smaller divider) */
585
+ sfx(2, (u16)(0x140 - ((u16)chain << 4)), 8);
586
+ apply_gravity(p);
587
+ board_dirty[p] = 1;
588
+ if (!two_player) {
589
+ cleared_total += n;
590
+ while (level < 9 && cleared_total >= (u16)level * 10) ++level;
591
+ }
592
+ hud_dirty = 1;
593
+ }
594
+ return chain;
176
595
  }
177
596
 
178
- static u8 collides(int8_t col, int8_t row) {
179
- u8 i;
180
- int8_t r;
181
- if (col < 0 || col >= COLS) return 1;
182
- for (i = 0; i < 3; ++i) {
183
- r = (int8_t)(row + i);
184
- if (r >= ROWS) return 1;
185
- if (r >= 0 && grid[r][col] != 0) return 1;
597
+ /* ── GAME LOGIC (clay) VERSUS attack: garbage rows rise from the bottom of
598
+ * the victim's well (random cells with one gap — matchable, so a skilled
599
+ * victim digs out). The victim's stack rising means the falling trio shifts
600
+ * up one to stay board-aligned; if the top row is already occupied, the
601
+ * victim tops out and loses. ── */
602
+ static void garbage_insert(u8 v, u8 nrows) {
603
+ u8 k, c, gap;
604
+ s16 r;
605
+ sfx(3, 0x300, 8); /* incoming-garbage thud */
606
+ for (k = 0; k < nrows; k++) {
607
+ for (c = 0; c < GRID_W; c++) {
608
+ if (grid[v][0][c] != EMPTY) { game_end(v); return; }
609
+ }
610
+ for (r = 0; r < GRID_H - 1; r++)
611
+ for (c = 0; c < GRID_W; c++)
612
+ grid[v][r][c] = grid[v][r + 1][c];
613
+ gap = random8() % GRID_W;
614
+ for (c = 0; c < GRID_W; c++)
615
+ grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (u8)(1 + random8() % 3);
616
+ if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
186
617
  }
187
- return 0;
618
+ board_dirty[v] = 1;
188
619
  }
189
620
 
190
- static void draw_piece(u8 clear) {
191
- u8 i;
192
- for (i = 0; i < 3; ++i) {
193
- int8_t r = (int8_t)(piece_y + i);
194
- u8 v;
195
- if (r < 0 || r >= ROWS) continue;
196
- v = clear ? grid[r][piece_x] : piece[i];
197
- put_cell((u8)(GRID_COL0 + piece_x), (u8)(GRID_ROW0 + r), vram_for(v));
621
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine. */
622
+ static u8 can_place(u8 p, s16 x, s16 y) {
623
+ s16 i, cy;
624
+ if (x < 0 || x >= GRID_W) return 0;
625
+ for (i = 0; i < 3; i++) {
626
+ cy = y + i;
627
+ if (cy < 0) continue;
628
+ if (cy >= GRID_H) return 0;
629
+ if (grid[p][cy][x] != EMPTY) return 0;
198
630
  }
631
+ return 1;
199
632
  }
200
633
 
201
- static void clear_triples(void) {
202
- u8 r;
203
- int8_t c;
204
- u8 a, b, d;
205
- for (r = 0; r < ROWS; ++r) {
206
- for (c = 0; c <= COLS - 3; ++c) {
207
- a = grid[r][c]; b = grid[r][c + 1]; d = grid[r][c + 2];
208
- if (a != 0 && a == b && b == d) {
209
- grid[r][c] = 0; grid[r][c + 1] = 0; grid[r][c + 2] = 0;
210
- if (score < 9999) score += 30;
211
- psg_tone(0, 0x180, 24);
634
+ static void spawn_piece(u8 p) {
635
+ piece_x[p] = GRID_W / 2;
636
+ piece_y[p] = -2;
637
+ piece_col[p][0] = (u8)(1 + random8() % 3);
638
+ piece_col[p][1] = (u8)(1 + random8() % 3);
639
+ piece_col[p][2] = (u8)(1 + random8() % 3);
640
+ if (!can_place(p, piece_x[p], piece_y[p])) game_end(p);
641
+ }
642
+
643
+ /* ── GAME LOGIC (clay) land the trio, resolve, attack, respawn. ── */
644
+ static void lock_piece(u8 p) {
645
+ s16 i, y;
646
+ u8 chain;
647
+ for (i = 0; i < 3; i++) {
648
+ y = piece_y[p] + i;
649
+ if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
650
+ }
651
+ board_dirty[p] = 1;
652
+ sfx(2, 0x300, 4); /* lock thunk */
653
+ if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
654
+ chain = resolve_board(p);
655
+ if (state != ST_PLAY) return;
656
+ if (chain && two_player) {
657
+ garbage_insert((u8)(p ^ 1), chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
658
+ if (state != ST_PLAY) return; /* garbage topped them out */
659
+ }
660
+ spawn_piece(p);
661
+ }
662
+
663
+ /* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
664
+ * (one cell per press), held DOWN soft-drops, I/II cycle the trio's colours
665
+ * (the classic trio "rotate"), RUN hard-drops. ── */
666
+ static void update_player(u8 p, u8 pad) {
667
+ u8 fresh, fd, t;
668
+ fresh = (u8)(pad & ~prev_pad[p]);
669
+ prev_pad[p] = pad;
670
+ if ((fresh & PCE_JOY_LEFT) && can_place(p, piece_x[p] - 1, piece_y[p]))
671
+ --piece_x[p];
672
+ if ((fresh & PCE_JOY_RIGHT) && can_place(p, piece_x[p] + 1, piece_y[p]))
673
+ ++piece_x[p];
674
+ if (fresh & PCE_JOY_I) { /* cycle colours downward */
675
+ t = piece_col[p][2];
676
+ piece_col[p][2] = piece_col[p][1];
677
+ piece_col[p][1] = piece_col[p][0];
678
+ piece_col[p][0] = t;
679
+ sfx(2, 0x140, 3);
680
+ }
681
+ if (fresh & PCE_JOY_II) { /* cycle colours upward */
682
+ t = piece_col[p][0];
683
+ piece_col[p][0] = piece_col[p][1];
684
+ piece_col[p][1] = piece_col[p][2];
685
+ piece_col[p][2] = t;
686
+ sfx(2, 0x120, 3);
687
+ }
688
+ if (fresh & PCE_JOY_RUN) { /* hard drop */
689
+ while (can_place(p, piece_x[p], piece_y[p] + 1)) ++piece_y[p];
690
+ lock_piece(p); /* may end the game */
691
+ return;
692
+ }
693
+ if (pad & PCE_JOY_DOWN) fall_t[p] += 4; /* soft drop */
694
+ ++fall_t[p];
695
+ fd = two_player ? VS_FALL_DELAY
696
+ : (u8)(32 - ((level << 1) + level)); /* 29..5 */
697
+ if (fall_t[p] >= fd) {
698
+ fall_t[p] = 0;
699
+ if (can_place(p, piece_x[p], piece_y[p] + 1))
700
+ ++piece_y[p];
701
+ else
702
+ lock_piece(p); /* may end the game */
703
+ }
704
+ }
705
+
706
+ /* ── GAME LOGIC (clay) — stage this frame's sprites ─────────────────────────
707
+ * Only the falling trios are sprites (locked cells are BAT tiles): 3 SATB
708
+ * slots per player, 16x16 each. Cells above the rim aren't drawn — they'd
709
+ * poke out from under the HUD band. */
710
+ static void push_sprites(void) {
711
+ u8 p, i;
712
+ for (p = 0; p < 2; p++) {
713
+ u8 active = (state == ST_PLAY) && (p == 0 || two_player);
714
+ for (i = 0; i < 3; i++) {
715
+ s16 r = piece_y[p] + (s16)i;
716
+ u8 col = piece_col[p][i] ? piece_col[p][i] : 1;
717
+ u8 slot = SLOT_TRIO(p, i);
718
+ if (active && r >= 0) {
719
+ u16 x = (u16)((well_tc[p] + piece_x[p] * 2) * 8);
720
+ u16 y = (u16)((WELL_TR + r * 2) * 8);
721
+ set_sprite(slot, x, y, SPR_PAT((u16)(col - 1)), PAL_TRIO(col));
722
+ } else {
723
+ set_sprite(slot, 0, OFFSCREEN_Y, SPR_PAT(0), PAL_TRIO(1));
212
724
  }
213
725
  }
214
726
  }
215
727
  }
216
728
 
217
- static void lock_piece(void) {
218
- u8 i;
219
- int8_t r;
220
- for (i = 0; i < 3; ++i) {
221
- r = (int8_t)(piece_y + i);
222
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
729
+ /* ── GAME LOGIC (clay) — screen painters (full BAT repaint per state change) ── */
730
+ static void paint_title(void) {
731
+ paint_backdrop();
732
+ draw_text((u8)((32 - (sizeof(GAME_TITLE) - 1)) / 2), 8, GAME_TITLE);
733
+ draw_text(10, 13, "1P RUN - I");
734
+ draw_text(10, 15, "2P VS - II");
735
+ draw_text(8, 19, "I II ROTATE RUN DROP");
736
+ draw_text(6, 22, "CHAINS FLOOD YOUR RIVAL");
737
+ draw_hud();
738
+ }
739
+
740
+ static void paint_play(void) {
741
+ paint_backdrop();
742
+ paint_frame(0);
743
+ paint_board(0);
744
+ if (two_player) {
745
+ paint_frame(1);
746
+ paint_board(1);
747
+ draw_text(15, 14, "VS");
223
748
  }
224
- clear_triples();
225
- draw_grid();
749
+ draw_hud();
750
+ }
751
+
752
+ /* ── GAME LOGIC (clay) — start a run ── */
753
+ static void start_game(u8 versus) {
754
+ u8 p, r, c;
755
+ two_player = versus;
756
+ well_tc[0] = versus ? WELL_VS_P1 : WELL_1P_TC;
757
+ well_tc[1] = WELL_VS_P2;
758
+ if (rng == 0) rng = 0xACE1;
759
+ for (p = 0; p < 2; p++) {
760
+ for (r = 0; r < GRID_H; r++)
761
+ for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
762
+ fall_t[p] = 0;
763
+ score[p] = 0;
764
+ prev_pad[p] = 0xFF; /* the button that started the game
765
+ * shouldn't also rotate the first trio */
766
+ }
767
+ cleared_total = 0;
768
+ level = 1;
769
+ state = ST_PLAY;
770
+ board_dirty[0] = 1;
771
+ board_dirty[1] = versus;
772
+ paint_play();
773
+ music_set(ST_PLAY);
774
+ sfx(2, 0x180, 6); /* start blip */
775
+ spawn_piece(0);
776
+ if (versus) spawn_piece(1);
777
+ }
778
+
779
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
780
+ * 2P INPUT via the TurboTap. pce_joy_read() reads pad 1 (slot 0). For pad 2 we
781
+ * read cc65's JOY_2 directly and translate it to the same clean PCE bitmask
782
+ * pce_input.c builds for pad 1. The host force-enables the TurboTap core
783
+ * option, so JOY_2 carries real port-1 input; without that override port 1 is
784
+ * dead and this would silently fall back to 1P. ── */
785
+ static u8 read_pad2(void) {
786
+ u8 raw = joy_read(JOY_2);
787
+ u8 m = 0;
788
+ if (JOY_UP(raw)) m |= PCE_JOY_UP;
789
+ if (JOY_DOWN(raw)) m |= PCE_JOY_DOWN;
790
+ if (JOY_LEFT(raw)) m |= PCE_JOY_LEFT;
791
+ if (JOY_RIGHT(raw)) m |= PCE_JOY_RIGHT;
792
+ if (JOY_BTN_1(raw)) m |= PCE_JOY_I;
793
+ if (JOY_BTN_2(raw)) m |= PCE_JOY_II;
794
+ if (JOY_BTN_3(raw)) m |= PCE_JOY_SELECT;
795
+ if (JOY_BTN_4(raw)) m |= PCE_JOY_RUN;
796
+ return m;
226
797
  }
227
798
 
228
799
  void main(void) {
229
- u8 r, c;
230
- u8 sfx_timer;
231
-
232
- _pce_keep[0] = 0;
233
-
234
- /* palette: BG sub-pal 0 holds field/wall + R/G/B blocks + frame */
235
- vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop navy */
236
- vce_set_color(1, PCE_RGB(7, 1, 1)); /* c1 red block */
237
- vce_set_color(2, PCE_RGB(1, 6, 1)); /* c2 green block */
238
- vce_set_color(3, PCE_RGB(2, 3, 7)); /* c3 blue block */
239
- vce_set_color(4, PCE_RGB(5, 5, 5)); /* c4 wall grey */
240
- vce_set_color(5, PCE_RGB(1, 2, 4)); /* c5 field blue (clearly *
241
- * distinct from backdrop) */
242
- vce_set_color(6, PCE_RGB(1, 0, 2)); /* c6 cabinet purple base + *
243
- * block frame */
244
- vce_set_color(7, PCE_RGB(2, 1, 4)); /* c7 cabinet dot */
800
+ u8 pad1, pad2, newpad;
801
+
802
+ _pce_keep[0] = 0; /* see the EMPTY-BSS TRAP note in pce_hw.h */
803
+
804
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ──
805
+ * Init order: palette VRAM uploads BAT paint joypad display ON.
806
+ * disp_enable() also sets the VBlank IRQ bit — without it waitvsync()
807
+ * never returns and the game freezes on its first frame. */
808
+ /* BG sub-pal 0: backdrop/frame/interior + text-on-band. BG sub-pal 1:
809
+ * HUD/text (white). BG sub-pal 3: the three locked-cell hues. */
810
+ vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop: near-black blue */
811
+ vce_set_color(1, PCE_RGB(1, 1, 2)); /* cabinet block */
812
+ vce_set_color(2, PCE_RGB(1, 1, 1)); /* HUD band: dark grey */
813
+ vce_set_color(3, PCE_RGB(4, 4, 5)); /* well frame: steel */
814
+ vce_set_color(17, PCE_RGB(7, 7, 7)); /* pal1 text: white */
815
+ /* locked cells: one tile shape (colour index 1) on three BG sub-palettes
816
+ * (3/4/5) → three hues. Entry = sub-palette*16 + 1. */
817
+ vce_set_color(3 * 16 + 1, PCE_RGB(7, 5, 0)); /* pal3 c1: amber */
818
+ vce_set_color(4 * 16 + 1, PCE_RGB(0, 6, 5)); /* pal4 c1: teal */
819
+ vce_set_color(5 * 16 + 1, PCE_RGB(7, 1, 6)); /* pal5 c1: magenta */
820
+ /* sprite sub-palettes (256 + pal*16 + index) — the falling trio mirrors
821
+ * the locked-cell hues, one sub-palette per colour so all three trio
822
+ * colours are visible (push_sprites selects PAL_TRIO(col) per cell). */
823
+ vce_set_color(256 + 1 * 16 + 1, PCE_RGB(7, 5, 0)); /* spr pal1 c1: amber */
824
+ vce_set_color(256 + 2 * 16 + 1, PCE_RGB(0, 6, 5)); /* spr pal2 c1: teal */
825
+ vce_set_color(256 + 3 * 16 + 1, PCE_RGB(7, 1, 6)); /* spr pal3 c1: magenta */
245
826
 
246
827
  upload_art();
247
- clear_bat();
248
- draw_well();
249
-
250
- for (r = 0; r < ROWS; ++r) for (c = 0; c < COLS; ++c) grid[r][c] = 0;
251
- score = 0;
252
- fall_timer = 0;
253
- rng = 0x1357;
254
- prev_pad = 0;
255
- sfx_timer = 0;
256
- new_piece();
257
- draw_grid();
828
+
829
+ hiscore = hiscore_load(); /* always 0 — no persistence on a bare HuCard */
830
+ state = ST_TITLE;
831
+ paint_title();
832
+ music_set(ST_TITLE);
258
833
 
259
834
  pce_joy_init();
260
- bg_enable();
835
+ disp_enable();
261
836
 
262
837
  for (;;) {
263
- u8 fall_rate;
264
838
  waitvsync();
265
839
 
266
- draw_piece(1); /* erase old piece footprint */
840
+ /* ── vblank work first: BAT repaints + sprites + SATB DMA ──
841
+ * Whole-board BAT repaint (see the WHOLE-BOARD REPAINT idiom) — both
842
+ * dirty wells stream in this one vblank, then the SATB DMA. */
843
+ if (board_dirty[0]) { paint_board(0); board_dirty[0] = 0; }
844
+ if (two_player && board_dirty[1]) { paint_board(1); board_dirty[1] = 0; }
845
+ if (hud_dirty) { draw_hud(); hud_dirty = 0; }
846
+ push_sprites();
847
+ satb_dma();
267
848
 
268
- pad = pce_joy_read();
269
- if ((pad & PCE_JOY_LEFT) && !(prev_pad & PCE_JOY_LEFT)
270
- && !collides((int8_t)(piece_x - 1), piece_y)) piece_x--;
271
- if ((pad & PCE_JOY_RIGHT) && !(prev_pad & PCE_JOY_RIGHT)
272
- && !collides((int8_t)(piece_x + 1), piece_y)) piece_x++;
273
- if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I)) {
274
- u8 t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
275
- psg_tone(1, 0x300, 18); sfx_timer = 3;
849
+ music_tick();
850
+ if (sfx_timer) {
851
+ --sfx_timer;
852
+ if (sfx_timer == 0) { psg_off(2); psg_off(3); }
276
853
  }
277
- if ((pad & PCE_JOY_II) && !(prev_pad & PCE_JOY_II)) {
278
- while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
279
- lock_piece();
280
- new_piece();
281
- psg_tone(1, 0x140, 22); sfx_timer = 4;
282
- prev_pad = pad;
283
- if (sfx_timer) { --sfx_timer; }
854
+
855
+ /* ── 2P input via the TurboTap (see read_pad2's idiom note). In 2P
856
+ * versus BOTH play simultaneously, so we read BOTH pads every frame;
857
+ * on the menus only pad 1 matters. ── */
858
+ pad1 = pce_joy_read();
859
+ pad2 = (state == ST_PLAY && two_player) ? read_pad2() : 0;
860
+
861
+ if (state == ST_TITLE) {
862
+ newpad = (u8)(pad1 & ~prev_pad[0]);
863
+ prev_pad[0] = pad1;
864
+ if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) start_game(0);
865
+ else if (newpad & PCE_JOY_II) start_game(1);
284
866
  continue;
285
867
  }
286
- prev_pad = pad;
287
-
288
- fall_rate = (pad & PCE_JOY_DOWN) ? 4 : 30;
289
- fall_timer++;
290
- if (fall_timer >= fall_rate) {
291
- fall_timer = 0;
292
- if (collides(piece_x, (int8_t)(piece_y + 1))) {
293
- lock_piece();
294
- new_piece();
295
- } else {
296
- piece_y++;
868
+ if (state == ST_OVER) {
869
+ newpad = (u8)(pad1 & ~prev_pad[0]);
870
+ prev_pad[0] = pad1;
871
+ if (newpad & (PCE_JOY_RUN | PCE_JOY_I)) {
872
+ state = ST_TITLE;
873
+ paint_title();
874
+ music_set(ST_TITLE);
297
875
  }
876
+ continue;
298
877
  }
299
878
 
300
- draw_piece(0); /* draw piece at new position */
301
-
302
- if (sfx_timer) { --sfx_timer; if (sfx_timer == 0) { psg_off(0); psg_off(1); } }
879
+ /* ── ST_PLAY both players update every frame (simultaneous versus,
880
+ * not alternating turns). Any update can end the game, so re-check
881
+ * state between them. ── */
882
+ update_player(0, pad1);
883
+ if (two_player && state == ST_PLAY) update_player(1, pad2);
303
884
  }
304
885
  }