romdevtools 0.28.0 → 0.30.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 (179) hide show
  1. package/AGENTS.md +53 -43
  2. package/CHANGELOG.md +91 -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 +12 -12
  88. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  89. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  90. package/src/cores/wasm/fceumm_libretro.js +1 -1
  91. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  92. package/src/cores/wasm/gambatte_libretro.js +1 -1
  93. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  94. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  95. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  96. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  97. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  98. package/src/cores/wasm/handy_libretro.js +1 -1
  99. package/src/cores/wasm/handy_libretro.wasm +0 -0
  100. package/src/cores/wasm/mgba_libretro.js +1 -1
  101. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  102. package/src/cores/wasm/prosystem_libretro.js +1 -1
  103. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  104. package/src/cores/wasm/snes9x_libretro.js +1 -1
  105. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  106. package/src/cores/wasm/stella2014_libretro.js +1 -1
  107. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  108. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  109. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  110. package/src/host/LibretroHost.js +84 -8
  111. package/src/http/tool-registry.js +11 -11
  112. package/src/mcp/tools/cheats.js +2 -1
  113. package/src/mcp/tools/frame.js +3 -2
  114. package/src/mcp/tools/index.js +3 -3
  115. package/src/mcp/tools/input.js +5 -4
  116. package/src/mcp/tools/lifecycle.js +6 -4
  117. package/src/mcp/tools/memory.js +131 -24
  118. package/src/mcp/tools/platform-docs.js +1 -1
  119. package/src/mcp/tools/preview-tile.js +6 -2
  120. package/src/mcp/tools/project.js +1098 -130
  121. package/src/mcp/tools/record.js +6 -7
  122. package/src/mcp/tools/rom-id.js +5 -1
  123. package/src/mcp/tools/run-until.js +12 -4
  124. package/src/mcp/tools/snippets.js +6 -6
  125. package/src/mcp/tools/sprite-pipeline.js +14 -2
  126. package/src/mcp/tools/state.js +2 -1
  127. package/src/mcp/tools/tile-inspect.js +8 -1
  128. package/src/mcp/tools/toolchain.js +12 -1
  129. package/src/mcp/tools/watch-memory.js +53 -10
  130. package/src/observer/bus.js +73 -0
  131. package/src/observer/livestream.html +4 -2
  132. package/src/observer/tool-wrap.js +17 -14
  133. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +32 -3
  134. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  135. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  136. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  137. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  138. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  139. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  140. package/src/platforms/gb/lib/c/README.md +10 -11
  141. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  142. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  143. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  144. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  145. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  146. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  147. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  148. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  149. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  150. package/src/platforms/gbc/lib/c/README.md +10 -11
  151. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  152. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  153. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  154. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  155. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  156. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  157. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  158. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  159. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  160. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  161. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  162. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  163. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  164. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  165. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  166. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  167. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  168. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  169. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  170. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  171. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  172. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  173. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  174. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  175. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  176. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  177. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  178. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  179. package/src/toolchains/index.js +27 -11
@@ -1,370 +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];
50
-
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
+ };
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);
116
251
  }
117
252
 
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 */
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;
123
256
  }
124
257
 
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));
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));
128
262
  VDC_DATA_LO = (u8)(e & 0xFF);
129
263
  VDC_DATA_HI = (u8)(e >> 8);
130
264
  }
131
265
 
132
- /* fill the whole BAT with the cabinet background tile */
133
- static void clear_bat(void) {
266
+ static void put_tile(u8 col, u8 row, u16 e) {
267
+ vram_set_write_addr((u16)(BAT_VRAM + row * 32 + col));
268
+ VDC_DATA_LO = (u8)(e & 0xFF);
269
+ VDC_DATA_HI = (u8)(e >> 8);
270
+ }
271
+
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);
153
462
  }
154
463
  }
464
+ ++music_timer;
465
+ if (music_timer >= 9) music_timer = 0;
155
466
  }
156
467
 
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]));
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
+ }
519
+ }
520
+ }
521
+ return cnt;
162
522
  }
163
523
 
164
- static u16 next_rand(void) {
165
- rng = (u16)(rng * 25173u + 13849u);
166
- return rng;
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
+ }
167
535
  }
168
- static u8 rand_color(void) { return (u8)(1 + (next_rand() >> 8) % 3); }
169
536
 
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;
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);
176
566
  }
177
567
 
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;
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;
186
593
  }
187
- return 0;
594
+ return chain;
188
595
  }
189
596
 
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));
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 */
617
+ }
618
+ board_dirty[v] = 1;
619
+ }
620
+
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;
630
+ }
631
+ return 1;
632
+ }
633
+
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 */
198
703
  }
199
704
  }
200
705
 
201
- /* ── match / clear / gravity core (ported from the GBC reference puzzle).
202
- * The old scan was horizontal-only AND cleared cells mid-scan, so vertical
203
- * and diagonal runs never cleared, 4+ runs half-cleared, and nothing ever
204
- * fell afterwards ("rows don't shift down"). This marks every 3+ run in all
205
- * 4 directions, clears them, applies per-column gravity, and loops so
206
- * cascades chain (score scales with chain depth). */
207
- static u8 matched[ROWS][COLS];
208
- /* H + V only on PCE the stock cc65 pce.cfg boot bank is 8KB and the
209
- * two diagonal passes don't fit; add them back if you free up ROM. */
210
- static const int8_t DIRS4[2][2] = { {0,1}, {1,0} };
211
-
212
- static u8 mark_and_count(void) {
213
- u8 r, c, d, len, k, cnt;
214
- u8 col;
215
- int8_t dr, dc;
216
- int sr, sc;
217
- cnt = 0;
218
- for (r = 0; r < ROWS; r++) for (c = 0; c < COLS; c++) matched[r][c] = 0;
219
- for (r = 0; r < ROWS; r++) {
220
- for (c = 0; c < COLS; c++) {
221
- col = grid[r][c];
222
- if (col == 0) continue;
223
- for (d = 0; d < 2; d++) {
224
- dr = DIRS4[d][0]; dc = DIRS4[d][1];
225
- /* (no run-start check: a mid-run scan only re-marks already-
226
- * marked cells, so skipping the predecessor test is pure
227
- * code-size savings on the 8KB PCE boot bank) */
228
- len = 1;
229
- sr = (int)r + dr; sc = (int)c + dc;
230
- while (sr >= 0 && sr < ROWS && sc >= 0 && sc < COLS
231
- && grid[sr][sc] == col) { len++; sr += dr; sc += dc; }
232
- if (len >= 3) {
233
- sr = r; sc = c;
234
- for (k = 0; k < len; k++) {
235
- if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
236
- sr += dr; sc += dc;
237
- }
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));
724
+ }
238
725
  }
239
- }
240
726
  }
241
- }
242
- return cnt;
243
- }
244
-
245
- /* collapse each column so survivors rest on the floor (in place: walk
246
- * from the bottom, copying gems down to a write cursor, then zero above) */
247
- static void apply_gravity(void) {
248
- u8 c;
249
- int r, w;
250
- for (c = 0; c < COLS; c++) {
251
- w = ROWS - 1;
252
- for (r = ROWS - 1; r >= 0; r--) {
253
- if (grid[r][c] != 0) { grid[w][c] = grid[r][c]; w--; }
727
+ }
728
+
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");
254
748
  }
255
- for (; w >= 0; w--) grid[w][c] = 0;
256
- }
257
- }
258
-
259
- static void resolve_board(void) {
260
- u8 n, r, c, chain;
261
- unsigned int amt;
262
- chain = 0;
263
- while (1) {
264
- n = mark_and_count();
265
- if (n == 0) break;
266
- chain++;
267
- for (r = 0; r < ROWS; r++)
268
- for (c = 0; c < COLS; c++)
269
- if (matched[r][c]) grid[r][c] = 0;
270
- amt = (unsigned int)n * 10u;
271
- if (chain > 1) amt = amt * chain;
272
- if (score < 9999) score += amt;
273
- psg_tone(0, 0x180, 24); /* clear chime */
274
- apply_gravity();
275
- }
276
- }
277
-
278
- static void clear_triples(void) {
279
- resolve_board();
280
- }
281
-
282
- static void lock_piece(void) {
283
- u8 i;
284
- int8_t r;
285
- for (i = 0; i < 3; ++i) {
286
- r = (int8_t)(piece_y + i);
287
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
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 */
288
766
  }
289
- clear_triples();
290
- draw_grid();
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;
291
797
  }
292
798
 
293
799
  void main(void) {
294
- u8 r, c;
295
- u8 sfx_timer;
296
-
297
- _pce_keep[0] = 0;
298
-
299
- /* palette: BG sub-pal 0 holds field/wall + R/G/B blocks + frame */
300
- vce_set_color(0, PCE_RGB(0, 0, 1)); /* backdrop navy */
301
- vce_set_color(1, PCE_RGB(7, 1, 1)); /* c1 red block */
302
- vce_set_color(2, PCE_RGB(1, 6, 1)); /* c2 green block */
303
- vce_set_color(3, PCE_RGB(2, 3, 7)); /* c3 blue block */
304
- vce_set_color(4, PCE_RGB(5, 5, 5)); /* c4 wall grey */
305
- vce_set_color(5, PCE_RGB(1, 2, 4)); /* c5 field blue (clearly *
306
- * distinct from backdrop) */
307
- vce_set_color(6, PCE_RGB(1, 0, 2)); /* c6 cabinet purple base + *
308
- * block frame */
309
- 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 */
310
826
 
311
827
  upload_art();
312
- clear_bat();
313
- draw_well();
314
-
315
- for (r = 0; r < ROWS; ++r) for (c = 0; c < COLS; ++c) grid[r][c] = 0;
316
- score = 0;
317
- fall_timer = 0;
318
- rng = 0x1357;
319
- prev_pad = 0;
320
- sfx_timer = 0;
321
- new_piece();
322
- 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);
323
833
 
324
834
  pce_joy_init();
325
- bg_enable();
835
+ disp_enable();
326
836
 
327
837
  for (;;) {
328
- u8 fall_rate;
329
838
  waitvsync();
330
- psg_music_tick();
331
-
332
- draw_piece(1); /* erase old piece footprint */
333
-
334
- pad = pce_joy_read();
335
- if ((pad & PCE_JOY_LEFT) && !(prev_pad & PCE_JOY_LEFT)
336
- && !collides((int8_t)(piece_x - 1), piece_y)) piece_x--;
337
- if ((pad & PCE_JOY_RIGHT) && !(prev_pad & PCE_JOY_RIGHT)
338
- && !collides((int8_t)(piece_x + 1), piece_y)) piece_x++;
339
- if ((pad & PCE_JOY_I) && !(prev_pad & PCE_JOY_I)) {
340
- u8 t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
341
- psg_tone(1, 0x300, 18); sfx_timer = 3;
839
+
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();
848
+
849
+ music_tick();
850
+ if (sfx_timer) {
851
+ --sfx_timer;
852
+ if (sfx_timer == 0) { psg_off(2); psg_off(3); }
342
853
  }
343
- if ((pad & PCE_JOY_II) && !(prev_pad & PCE_JOY_II)) {
344
- while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
345
- lock_piece();
346
- new_piece();
347
- psg_tone(1, 0x140, 22); sfx_timer = 4;
348
- prev_pad = pad;
349
- 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);
350
866
  continue;
351
867
  }
352
- prev_pad = pad;
353
-
354
- fall_rate = (pad & PCE_JOY_DOWN) ? 4 : 30;
355
- fall_timer++;
356
- if (fall_timer >= fall_rate) {
357
- fall_timer = 0;
358
- if (collides(piece_x, (int8_t)(piece_y + 1))) {
359
- lock_piece();
360
- new_piece();
361
- } else {
362
- 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);
363
875
  }
876
+ continue;
364
877
  }
365
878
 
366
- draw_piece(0); /* draw piece at new position */
367
-
368
- 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);
369
884
  }
370
885
  }