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,208 +1,946 @@
1
- // ── puzzle.c — Commodore 64 match-3 falling-block puzzle ─────────────
2
- //
3
- // 6×12 grid rendered in the C64 screen-RAM character matrix (40×25).
4
- // 3 colors (R/G/B) via the COLOR_RAM at $D800. Active piece is 1×3
5
- // vertical. Joystick port 2: LEFT/RIGHT shift, FIRE rotates colors,
6
- // DOWN soft-drop, B2 hard-drop. Horizontal triples clear + score.
1
+ /* ── puzzle.c — C64 falling-trio versus puzzle (complete example game) ────────
2
+ *
3
+ * MAGMA MATCH a COMPLETE, working game: title screen, 1P MARATHON mode
4
+ * (levels speed the fall as you clear) and 2P SIMULTANEOUS VERSUS mode —
5
+ * two 6x12 wells side by side, P1 on CONTROL PORT 2, P2 on CONTROL PORT 1,
6
+ * both falling at once, where every cascade chain you score erupts garbage
7
+ * rows up from the bottom of your rival's well. Score + in-session hi-score
8
+ * behind the gated persistence seam, 2-voice SID music with the C64's
9
+ * signature filter sweep + SFX, and the C64's signature raster-IRQ split:
10
+ * a fixed HUD bar over the wells.
11
+ *
12
+ * The game: a falling-trio match-3. A vertical trio of blocks drops into a
13
+ * well; LEFT/RIGHT move it, UP cycles its three colours, FIRE hard-drops,
14
+ * DOWN soft-drops. When it lands, any straight run of 3+ same-coloured
15
+ * blocks (horizontal, vertical, or diagonal) clears; survivors fall and
16
+ * cascades chain for multiplied score. First stack to reach the rim loses.
17
+ *
18
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
19
+ * very different one. The markers tell you what's what:
20
+ * HARDWARE IDIOM (load-bearing) — dodges a documented C64 footgun; reshape
21
+ * your gameplay around it (see TROUBLESHOOTING before changing).
22
+ * GAME LOGIC (clay) — match rules, garbage, tuning, art: reshape freely.
23
+ *
24
+ * What depends on what:
25
+ * c64_registers.h — VIC-II / SID / CIA symbolic addresses (header only).
26
+ * c64_sfx.{h,c} — one-shot SID sound effects on voice 2.
27
+ * The BASIC stub + crt0 come from cc65's c64 target: the .prg loads at
28
+ * $0801, a tiny BASIC line does SYS into the C runtime, and the KERNAL
29
+ * stays banked in (we lean on that for the IRQ vector — see below).
30
+ *
31
+ * Memory map this file assumes (VIC bank 0 = $0000-$3FFF):
32
+ * $0400 screen RAM (40×25 chars) $D800 color RAM (per-cell color)
33
+ * $0801 this program (code+data grow up from here)
34
+ * Keep the program under ~14 KB. (No hardware sprites here — the whole
35
+ * board is screen-RAM CHARACTERS, so the classic $0800 / $1000 sprite-data
36
+ * trap doesn't even come up. The falling trio is drawn as chars too.)
37
+ *
38
+ * Frame budget (PAL, 50fps) — and a TEACHING POINT vs the NES version of
39
+ * this game (examples/nes/templates/puzzle.c): on the NES, board repaints
40
+ * squeeze through a ~16-entry vblank queue, so a full-board repaint is
41
+ * BUDGETED across 12 frames of dirty-row bitmask tricks. The C64 has NO
42
+ * VRAM port — screen RAM is plain memory, writable any time, mid-frame.
43
+ * But the C64's famine is CPU, not bandwidth: a full 880-cell repaint of
44
+ * cc65-generated C costs ~50 frames (a frozen second). So this game NEVER
45
+ * repaints the whole screen during play — it tracks the cells that actually
46
+ * changed and repaints ONLY those (see the cell-diff idiom). Same genre,
47
+ * a different scarcity to design around — fork accordingly.
48
+ */
7
49
 
8
50
  #include "c64_registers.h"
9
51
  #include "c64_sfx.h"
10
52
  #include <stdint.h>
11
53
 
54
+ /* cc65 KERNAL disk-I/O prototypes. We DON'T #include <cbm.h> — it drags in
55
+ * <c64.h>, whose VIC/SID/JOY macros collide with this project's
56
+ * c64_registers.h (cc65 errors "macro redefinition is not identical"). These
57
+ * four are the stable cc65 ABI; declaring them directly avoids the clash. */
58
+ unsigned char __fastcall__ cbm_open(unsigned char lfn, unsigned char device,
59
+ unsigned char sec_addr, const char *name);
60
+ void __fastcall__ cbm_close(unsigned char lfn);
61
+ int __fastcall__ cbm_read(unsigned char lfn, void *buffer, unsigned int size);
62
+ int __fastcall__ cbm_write(unsigned char lfn, const void *buffer, unsigned int size);
63
+
64
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
65
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
66
+ #define GAME_TITLE "MAGMA MATCH"
67
+
12
68
  #define POKE(addr, val) (*(volatile uint8_t*)(addr) = (val))
13
69
  #define PEEK(addr) (*(volatile uint8_t*)(addr))
14
70
 
15
71
  #define SCREEN ((volatile uint8_t*)0x0400)
16
72
  #define COLORS ((volatile uint8_t*)0xD800)
17
73
 
74
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
75
+ * Board geometry. Each cell is ONE 40×25 character. Wells are 6 wide × 12
76
+ * tall. In 1P a single well is centred; in 2P two wells split the screen.
77
+ * char row 0 — HUD text: SC / HI / LV / P2 score (FIXED, the raster split)
78
+ * char row 1 — solid divider line (FIXED)
79
+ * char row 2 — blank spacer: the split lands mid-row HERE (jitter-proof)
80
+ * char rows 3.. — the wells (frame at WELL_TOP-1 / WELL_TOP+GRID_H) */
81
+ #define GRID_W 6
82
+ #define GRID_H 12
83
+ #define WELL_TOP 5 /* top char ROW of a well's interior */
84
+ #define WELL_1P_X 17 /* 1P: single centred well (cols 17-22) */
85
+ #define WELL_VS_P1 6 /* 2P: P1 interior cols 6-11 ... */
86
+ #define WELL_VS_P2 28 /* P2 interior cols 28-33 (split board) */
87
+
88
+ #define EMPTY 0 /* cell colours 1..3 = magma / ember / ash */
89
+
90
+ /* Char codes + colours for the board cells. A filled cell is a reverse-space
91
+ * solid block tinted by its colour; an empty cell is a faint speck so the
92
+ * well reads as a recessed playfield instead of a black void. */
93
+ #define CH_BLOCK 0xA0 /* reverse-space solid block (the trio/locked) */
94
+ #define CH_DOT 0x2E /* '.' faint speck = empty well cell */
95
+ #define CH_FRAME 0xE6 /* checkered frame glyph = well border */
96
+ #define CH_BLANK 0x20
97
+ /* colour 1..3 → a C64 colour code (magma reds/oranges + ash grey). */
98
+ static const uint8_t cell_color[4] = {
99
+ COLOR_DARK_GRAY, /* 0 = empty speck (dim) */
100
+ COLOR_LIGHT_RED, /* 1 = magma */
101
+ COLOR_ORANGE, /* 2 = ember */
102
+ COLOR_LIGHT_GRAY, /* 3 = ash */
103
+ };
104
+
105
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
106
+ * THE RASTER-IRQ SPLIT — the C64's classic "fixed status bar over a moving
107
+ * world" trick (and the gateway drug to all raster effects). Here it pins a
108
+ * HUD bar at the top while the wells live below it. The VIC-II has ONE
109
+ * $D016 fine-scroll for the whole frame; we don't scroll the wells (a puzzle
110
+ * board holds still), but the split is STILL the idiomatic way to guarantee
111
+ * the HUD's first rows render in a known, fixed scroll state regardless of
112
+ * what the rest of the frame does — and it gives you the per-frame heartbeat
113
+ * the main loop paces on. Two IRQs ping-pong per frame:
114
+ *
115
+ * line 68 (inside the blank spacer row 2): assert the board's $D016
116
+ * → everything below the split renders in the board's scroll state
117
+ * line 251 (just past the text window): assert the bar's $D016
118
+ * → next frame's HUD rows render fixed; this IRQ is also the
119
+ * game's frame heartbeat (increments frame_count)
120
+ *
121
+ * The handshake, register by register:
122
+ * $D012 raster compare line (low 8 bits)
123
+ * $D011 b7 raster compare bit 8 — MUST be 0 here (both lines < 256).
124
+ * Forgetting this bit is the classic "my IRQ fires on the
125
+ * wrong line / twice" bug when lines ≥ 256 get involved.
126
+ * $D01A b0 raster IRQ enable
127
+ * $D019 IRQ latch. ACK by WRITING THE BITS BACK (write-1-to-clear).
128
+ * THE LOAD-BEARING LINE: skip the ack and the IRQ re-fires the
129
+ * instant it returns, forever — the main loop starves and the
130
+ * machine looks hung.
131
+ * $0314/15 the KERNAL's IRQ indirection. The hardware vector ($FFFE)
132
+ * points into KERNAL ROM, which saves A/X/Y and jumps through
133
+ * $0314 — so with the KERNAL banked in (cc65 default) we just
134
+ * repoint $0314. Exit via jmp $EA81 (KERNAL: restore regs +
135
+ * rti), SKIPPING $EA31's jiffy-clock/keyboard scan.
136
+ * $DC0D CIA1 interrupt control. The KERNAL leaves a 60Hz CIA timer
137
+ * IRQ running (the jiffy clock); disable it ($7F = clear all
138
+ * sources) and ack it (read $DC0D) or it shares the IRQ line
139
+ * with the raster and fires our handler at random lines.
140
+ *
141
+ * JITTER: an IRQ only starts after the current instruction finishes, so the
142
+ * handler begins 0-7 cycles late, plus the KERNAL thunk (~35 cycles) — the
143
+ * $D016 write lands one-to-two raster lines after SPLIT_LINE. We hide that
144
+ * by splitting inside a UNIFORM blank row, where shifting the (invisible)
145
+ * pixels mid-line changes nothing. Splits next to visible detail need
146
+ * cycle-exact stabilization (double-IRQ trick) — don't go there until you do.
147
+ *
148
+ * The handler is ASSEMBLY-IN-C on purpose: cc65's generated C uses shared
149
+ * zero-page scratch registers, so a C-level IRQ body would corrupt whatever
150
+ * the main loop was computing. These asm lines touch only A + the flags
151
+ * (which the KERNAL thunk already saved). requires: KERNAL banked in,
152
+ * frame_count file-scope NON-static (asm %v needs the symbol). */
153
+ #define SPLIT_LINE 68 /* inside spacer row 2 (67-74): jitter-proof */
154
+ #define BOTTOM_LINE 251 /* first line below the 25-row text window */
155
+ #define D016_BAR 0xC0 /* fine X = 0, 38-col mode for both halves */
156
+
157
+ volatile uint8_t frame_count; /* bumped by the bottom IRQ — frame heartbeat */
158
+
159
+ void raster_irq(void) {
160
+ asm("lda $d019"); /* read VIC IRQ latch... */
161
+ asm("sta $d019"); /* ...write it back = ACK (write-1-to-clear).
162
+ * THE line you must not lose (see above). */
163
+ asm("lda $d012"); /* which raster line woke us? (self-correcting
164
+ * dispatch — no phase variable to desync) */
165
+ asm("cmp #150");
166
+ asm("bcs %g", at_bottom); /* ≥150 → we're at BOTTOM_LINE */
167
+ /* — split point (line ~68, inside the blank spacer row) — */
168
+ asm("lda #$C0"); /* = D016_BAR — board holds still, same scroll */
169
+ asm("sta $d016");
170
+ asm("lda #251"); /* = BOTTOM_LINE (cc65's asm %b only takes */
171
+ asm("sta $d012"); /* signed bytes, so these are literals — the */
172
+ asm("jmp $ea81"); /* #if below keeps them honest) */
173
+ at_bottom:
174
+ asm("lda #$C0"); /* = D016_BAR */
175
+ asm("sta $d016"); /* bar scroll for the top of the NEXT frame */
176
+ asm("inc %v", frame_count);/* frame heartbeat for the main loop */
177
+ asm("lda #%b", SPLIT_LINE);
178
+ asm("sta $d012"); /* next stop: the split line */
179
+ asm("jmp $ea81"); /* KERNAL: pla/tay/pla/tax/pla/rti */
180
+ }
181
+ #if BOTTOM_LINE != 251 || D016_BAR != 0xC0
182
+ #error raster_irq's asm immediates are out of sync with BOTTOM_LINE / D016_BAR
183
+ #endif
184
+
185
+ static void install_raster_irq(void) {
186
+ asm("sei"); /* no IRQs while we rewire them */
187
+ POKE(CIA1_PRA + 0x0D, 0x7F); /* $DC0D: disable ALL CIA1 IRQ sources
188
+ * (kills the KERNAL jiffy/keyboard IRQ
189
+ * — we read the sticks ourselves) */
190
+ (void)PEEK(CIA1_PRA + 0x0D); /* reading $DC0D acks anything pending */
191
+ POKE(0x0314, (uint8_t)((unsigned)raster_irq & 0xFF));
192
+ POKE(0x0315, (uint8_t)((unsigned)raster_irq >> 8));
193
+ POKE(VIC_CTRL1, 0x1B); /* $D011 = power-on default: screen on,
194
+ * 25 rows, YSCROLL=3, and bit 7 (raster
195
+ * compare bit 8) = 0 — both lines < 256 */
196
+ POKE(VIC_RASTER, SPLIT_LINE); /* first stop */
197
+ POKE(VIC_IRQ_ENA, 0x01); /* raster IRQ on */
198
+ POKE(VIC_IRQ, 0xFF); /* clear any stale latch bits */
199
+ asm("cli");
200
+ }
201
+
202
+ /* Wait for the bottom IRQ's heartbeat. Replaces the usual poll-$D012 loop —
203
+ * the IRQ owns the raster now, the main loop just paces itself on it. */
204
+ static void wait_frame(void) {
205
+ uint8_t f = frame_count;
206
+ while (frame_count == f) { }
207
+ }
208
+
209
+ /* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ── reading BOTH
210
+ * joystick ports. CIA1 port A ($DC00) = control port 2, port B ($DC01) =
211
+ * control port 1. Active-low: a pressed switch reads 0, so invert and mask
212
+ * to bits 0-4 (up/down/left/right/fire).
213
+ *
214
+ * THE PORT-1 GOTCHA: $DC01 is ALSO the keyboard row register — the matrix
215
+ * hangs off the same CIA lines. Writing $FF to $DC00 first deselects every
216
+ * keyboard column, so held keys can't pull $DC01 rows low and ghost into
217
+ * the port-1 stick. That's also why "port 2 is the C64 game port": P1 lives
218
+ * there by convention, and this game puts the SECOND player on port 1.
219
+ * requires: install_raster_irq already disabled the KERNAL's keyboard scan,
220
+ * so nothing else rewrites $DC00. */
221
+ static uint8_t read_stick_port2(void) { /* player 1 */
222
+ POKE(CIA1_PRA, 0xFF);
223
+ return (uint8_t)(~PEEK(CIA1_PRA) & 0x1F);
224
+ }
225
+ static uint8_t read_stick_port1(void) { /* player 2 */
226
+ POKE(CIA1_PRA, 0xFF);
227
+ return (uint8_t)(~PEEK(CIA1_PRB) & 0x1F);
228
+ }
18
229
  #define JOY_UP 0x01
19
230
  #define JOY_DOWN 0x02
20
231
  #define JOY_LEFT 0x04
21
232
  #define JOY_RIGHT 0x08
22
233
  #define JOY_FIRE 0x10
23
234
 
24
- #define COLS 6
25
- #define ROWS 12
26
- #define GRID_C 17 /* center grid in the 40-col matrix */
27
- #define GRID_R 6
235
+ /* ── HARDWARE IDIOM (load-bearing) — hi-score persistence: DISK SAVE ─────────
236
+ * The C64 has no battery SRAM — the honest save medium is the FLOPPY. A game
237
+ * persists by writing a file to drive 8; VICE commits it into the live 1541
238
+ * disk image (true-drive GCR write-back), so a save survives a power cycle
239
+ * exactly as it did on real hardware. (To capture it headlessly the host does
240
+ * state({op:'exportDisk', path}); re-loading that .d64 restores the save.)
241
+ *
242
+ * REQUIRES THE GAME RUN FROM A DISK: build/package it as a .d64 and load THAT
243
+ * (loadMedia autostarts it). A bare .prg injected straight into RAM has no
244
+ * mounted disk to save to, so the save is a silent no-op — still honest (the
245
+ * value just stays in-session), it simply has nowhere to persist.
246
+ *
247
+ * We keep a 2-byte record in a SEQ file "HI" on drive 8. These are the STABLE
248
+ * SEAM: the game calls hiscore_load at boot and hiscore_save on a new record;
249
+ * reshape the record format freely, just keep the two function signatures. */
250
+ #define SAVE_NAME "@0:HI,S,W" /* @ = replace-if-exists; S=SEQ, W=write */
251
+ #define LOAD_NAME "0:HI,S,R"
252
+
253
+ static uint16_t hiscore_load(void) {
254
+ uint16_t v = 0;
255
+ uint8_t buf[2];
256
+ if (cbm_open(2, 8, 2, LOAD_NAME) == 0) {
257
+ if (cbm_read(2, buf, 2) == 2) v = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
258
+ cbm_close(2);
259
+ }
260
+ return v; /* 0 if the file isn't there yet (first ever boot) */
261
+ }
262
+
263
+ static void hiscore_save(uint16_t v) {
264
+ uint8_t buf[2];
265
+ buf[0] = (uint8_t)(v & 0xFF);
266
+ buf[1] = (uint8_t)(v >> 8);
267
+ if (cbm_open(2, 8, 2, SAVE_NAME) == 0) {
268
+ cbm_write(2, buf, 2);
269
+ cbm_close(2);
270
+ }
271
+ /* No disk mounted (ran as a bare .prg) -> cbm_open fails -> silent no-op. */
272
+ }
273
+
274
+ /* ── GAME LOGIC (clay) — SID music: 2 voices + THE filter sweep ─────────────
275
+ * Voice 0 = melody (pulse), voice 1 = bass (sawtooth THROUGH THE FILTER),
276
+ * voice 2 is reserved for sound effects (c64_sfx). Each voice walks a
277
+ * (freq, frames) note table once per frame; end wraps → continuous loop.
278
+ *
279
+ * THE SID FILTER — the C64's sonic signature, and the part most "music
280
+ * drivers ported from other chips" miss. One analog-modeled filter, shared
281
+ * by all voices, four registers:
282
+ * $D415 cutoff low 3 bits $D416 cutoff high 8 bits (11-bit total)
283
+ * $D417 high nibble = resonance 0-15; low 3 bits ROUTE voices into the
284
+ * filter (bit0=voice0, bit1=voice1, bit2=voice2)
285
+ * $D418 bit4=lowpass bit5=bandpass bit6=highpass — AND master volume in
286
+ * bits 0-3. Volume and filter mode share a register: any "set
287
+ * volume" helper that writes plain $0F silently turns the filter
288
+ * OFF (c64_sfx's sfx_init does exactly that, so music_init runs
289
+ * AFTER it and re-asserts the mode bits).
290
+ * FOOTGUN: bit 7 of $D418 is "3OFF" — it MUTES voice 3 entirely.
291
+ * Set it by accident and all your sound effects vanish.
292
+ * The sweep: a triangle LFO walks the cutoff up and down each frame over
293
+ * the resonant lowpass — the bass goes from muffled to snarling and back,
294
+ * the "wah" that screams Commodore. Hear it change: that IS the chip. */
295
+ #define N_C3 0x1199u
296
+ #define N_D3 0x13EEu
297
+ #define N_E3 0x1666u
298
+ #define N_F3 0x1798u
299
+ #define N_G3 0x1AE6u
300
+ #define N_A3 0x1E78u
301
+ #define N_B3 0x2253u
302
+ #define N_C4 0x2333u
303
+ #define N_D4 0x27DDu
304
+ #define N_E4 0x2CCCu
305
+ #define N_F4 0x2F30u
306
+ #define N_G4 0x35CCu
307
+ #define N_A4 0x3CF1u
308
+ #define N_B4 0x44A7u
309
+ #define N_C5 0x4666u
310
+ #define N_D5 0x4FBAu
311
+ #define N_E5 0x5998u
312
+ #define N_REST 0u
313
+ #define STEP 9 /* frames per melodic eighth-note (~140 BPM PAL) */
314
+
315
+ typedef struct { uint16_t freq; uint8_t len; } Note;
28
316
 
29
- #define CELL_CHAR 0xA0 /* solid block */
317
+ /* The table IS the song — edit these to rescore your fork. A brooding minor
318
+ * line that suits a magma well. */
319
+ static const Note melody[] = {
320
+ { N_A4, STEP*2 }, { N_C5, STEP }, { N_B4, STEP }, { N_A4, STEP*2 }, { N_E4, STEP*2 },
321
+ { N_F4, STEP*2 }, { N_A4, STEP }, { N_C5, STEP }, { N_A4, STEP*2 }, { N_REST, STEP },
322
+ { N_G4, STEP }, { N_B4, STEP }, { N_D5, STEP*2 }, { N_B4, STEP }, { N_G4, STEP*2 }, { N_D5, STEP },
323
+ { N_E5, STEP }, { N_D5, STEP }, { N_C5, STEP }, { N_B4, STEP }, { N_A4, STEP*2 }, { N_REST, STEP },
324
+ { N_C5, STEP }, { N_B4, STEP }, { N_A4, STEP }, { N_G4, STEP }, { N_F4, STEP*2 }, { N_E4, STEP*2 },
325
+ { N_A4, STEP }, { N_C5, STEP }, { N_E5, STEP*2 }, { N_C5, STEP }, { N_A4, STEP*2 }, { N_REST, STEP },
326
+ };
327
+ static const Note bassline[] = {
328
+ /* Octave-pumping bass — the filter sweep chews on this. */
329
+ { N_A3, STEP*3 }, { N_A3, STEP }, { N_E3, STEP*2 }, { N_A3, STEP*2 },
330
+ { N_F3, STEP*3 }, { N_C3, STEP }, { N_F3, STEP*2 }, { N_C4, STEP*2 },
331
+ { N_G3, STEP*3 }, { N_D3, STEP }, { N_G3, STEP*2 }, { N_B3, STEP*2 },
332
+ { N_A3, STEP*3 }, { N_E3, STEP }, { N_A3, STEP*2 }, { N_C4, STEP*2 },
333
+ };
334
+ #define MELODY_LEN (sizeof(melody) / sizeof(melody[0]))
335
+ #define BASS_LEN (sizeof(bassline) / sizeof(bassline[0]))
30
336
 
31
- static uint8_t grid[ROWS][COLS];
32
- static uint8_t piece[3];
33
- static int8_t piece_x, piece_y;
34
- static uint8_t fall_timer;
35
- static uint16_t score;
36
- static uint32_t rng = 1;
337
+ static uint8_t m_pos[2], m_left[2];
338
+ static uint16_t filter_cut; /* 11-bit cutoff, 0-2047 */
339
+ static uint8_t filter_up;
37
340
 
38
- static void wait_vblank(void) {
39
- while (PEEK(VIC_RASTER) < 250) { }
40
- while (PEEK(VIC_RASTER) >= 250) { }
341
+ static void music_trigger(uint8_t v, uint16_t freq, uint8_t wave) {
342
+ if (freq == N_REST) {
343
+ POKE(SID_CTRL(v), wave); /* gate off: release tail plays */
344
+ return;
345
+ }
346
+ POKE(SID_FREQ_LO(v), (uint8_t)(freq & 0xFF));
347
+ POKE(SID_FREQ_HI(v), (uint8_t)(freq >> 8));
348
+ POKE(SID_CTRL(v), wave); /* gate OFF then ON — the 6581/8580 */
349
+ POKE(SID_CTRL(v), wave | SID_GATE); /* envelope only retriggers on the
350
+ * 0→1 gate edge */
41
351
  }
42
352
 
43
- /* Paint the playfield surround so the board reads as a real puzzle screen
44
- * instead of a tiny well floating in a black void: a dithered backdrop fills
45
- * the whole 40x25 matrix (two dark blues, so two colours share the screen
46
- * and neither dominates), then a bright frame is drawn one cell outside the
47
- * 6x12 well, and the well interior is cleared to black so the falling blocks
48
- * pop. Call ONCE before draw_grid(); draw_grid() owns the interior after. */
49
- static void draw_field(void) {
50
- uint16_t i;
51
- uint8_t r, c;
52
- int8_t fr, fc;
53
- for (i = 0; i < 1000; i++) {
54
- SCREEN[i] = 0xA0; /* solid block backdrop */
55
- COLORS[i] = ((i ^ (i >> 5)) & 1) ? 0x06 : 0x0E; /* blue / light blue */
353
+ static void music_init(void) {
354
+ /* Melody: pulse at 50% duty, snappy envelope. */
355
+ POKE(SID_PW_LO(0), 0x00); POKE(SID_PW_HI(0), 0x08);
356
+ POKE(SID_AD(0), 0x07); /* attack 0, decay 7 */
357
+ POKE(SID_SR(0), 0x84); /* sustain 8, release 4 */
358
+ /* Bass: sawtooth (harmonically rich gives the filter teeth to chew). */
359
+ POKE(SID_AD(1), 0x06);
360
+ POKE(SID_SR(1), 0xA5);
361
+ /* Filter: route VOICE 1 ONLY into it (bit 1 of $D417), resonance 13/15. */
362
+ POKE(SID_RES_FILT, 0xD2);
363
+ /* Lowpass mode + master volume 15. NOTE bits shared with volume, and bit
364
+ * 7 (3OFF) stays 0 or voice-2 sound effects go silent — see block doc. */
365
+ POKE(SID_VOL_MODE, 0x1F);
366
+ filter_cut = 0x180; filter_up = 1;
367
+ m_pos[0] = m_pos[1] = 0;
368
+ m_left[0] = m_left[1] = 1; /* triggers both voices on the next update */
369
+ }
370
+
371
+ static void music_update(void) {
372
+ /* Note sequencing, one table per voice. */
373
+ if (--m_left[0] == 0) {
374
+ music_trigger(0, melody[m_pos[0]].freq, SID_PULSE);
375
+ m_left[0] = melody[m_pos[0]].len;
376
+ if (++m_pos[0] >= MELODY_LEN) m_pos[0] = 0;
56
377
  }
57
- /* Bright frame one cell outside the well. */
58
- for (fc = -1; fc <= COLS; fc++) {
59
- r = (uint8_t)(GRID_R - 1); SCREEN[r * 40 + GRID_C + fc] = 0xA0; COLORS[r * 40 + GRID_C + fc] = 0x01;
60
- r = (uint8_t)(GRID_R + ROWS); SCREEN[r * 40 + GRID_C + fc] = 0xA0; COLORS[r * 40 + GRID_C + fc] = 0x01;
378
+ if (--m_left[1] == 0) {
379
+ music_trigger(1, bassline[m_pos[1]].freq, SID_SAWTOOTH);
380
+ m_left[1] = bassline[m_pos[1]].len;
381
+ if (++m_pos[1] >= BASS_LEN) m_pos[1] = 0;
61
382
  }
62
- for (fr = -1; fr <= ROWS; fr++) {
63
- r = (uint8_t)(GRID_R + fr);
64
- SCREEN[r * 40 + GRID_C - 1] = 0xA0; COLORS[r * 40 + GRID_C - 1] = 0x01;
65
- SCREEN[r * 40 + GRID_C + COLS] = 0xA0; COLORS[r * 40 + GRID_C + COLS] = 0x01;
383
+ /* THE FILTER SWEEP triangle LFO on the cutoff, ~10s round trip.
384
+ * 11-bit value split across two registers: low 3 bits in $D415,
385
+ * high 8 in $D416. */
386
+ if (filter_up) { filter_cut += 6; if (filter_cut >= 0x700) filter_up = 0; }
387
+ else { filter_cut -= 6; if (filter_cut <= 0x180) filter_up = 1; }
388
+ POKE(SID_FILTER_LO, (uint8_t)(filter_cut & 0x07));
389
+ POKE(SID_FILTER_HI, (uint8_t)(filter_cut >> 3));
390
+ }
391
+
392
+ /* ── GAME LOGIC (clay) — screen text. The C64 has NO VRAM port: screen RAM
393
+ * is plain memory, writable any time, mid-frame, no vblank dance. The only
394
+ * translation is ASCII → SCREEN CODES (not PETSCII!): A-Z land at 1-26;
395
+ * space through '?' (incl. digits) keep their ASCII values. ── */
396
+ static void draw_text(uint8_t row, uint8_t col, const char *s) {
397
+ uint16_t off = (uint16_t)row * 40 + col;
398
+ uint8_t ch;
399
+ while ((ch = (uint8_t)*s++) != 0) {
400
+ if (ch >= 'A' && ch <= 'Z') ch -= 64; /* A-Z → screen codes 1-26 */
401
+ SCREEN[off] = ch; /* 32-63 map straight through */
402
+ COLORS[off] = COLOR_WHITE;
403
+ ++off;
66
404
  }
67
- /* Clear the well interior to black so colored blocks stand out. */
68
- for (r = 0; r < ROWS; r++)
69
- for (c = 0; c < COLS; c++) SCREEN[(GRID_R + r) * 40 + GRID_C + c] = ' ';
70
405
  }
406
+ /* Blank the whole 40-col row, then draw `s` on it — a clean text BAND, so
407
+ * message text reads cleanly over whatever the board left behind. */
408
+ static void draw_text_band(uint8_t row, uint8_t col, const char *s) {
409
+ uint8_t c;
410
+ volatile uint8_t *p = SCREEN + (uint16_t)row * 40;
411
+ for (c = 0; c < 40; c++) p[c] = CH_BLANK;
412
+ draw_text(row, col, s);
413
+ }
414
+
415
+ static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
416
+ uint8_t i, d[5];
417
+ uint16_t off = (uint16_t)row * 40 + col;
418
+ for (i = 0; i < 5; i++) { d[i] = v % 10; v /= 10; }
419
+ for (i = 0; i < 5; i++) {
420
+ SCREEN[off + i] = (uint8_t)('0' + d[4 - i]); /* digit screen code = ASCII */
421
+ COLORS[off + i] = COLOR_WHITE;
422
+ }
423
+ }
424
+
425
+ /* ── GAME LOGIC (clay) — xorshift16 PRNG (a few instructions) ── */
426
+ static uint16_t rng = 0xACE1;
427
+ static uint8_t random8(void) {
428
+ uint16_t r = rng;
429
+ r ^= r << 7;
430
+ r ^= r >> 9;
431
+ r ^= r << 8;
432
+ rng = r;
433
+ return (uint8_t)r;
434
+ }
435
+
436
+ /* ── GAME LOGIC (clay — reshape freely) ── game state.
437
+ * Boards are PLAIN STATIC ARRAYS — the C64 has 38 KB of BASIC RAM free, so
438
+ * none of the NES version's absolute-address scratch-page gymnastics. The
439
+ * hot ones are file-scope NON-static so they land in the cc65 link map
440
+ * (build symbols) — a headless agent can resolve them by name and read/poke
441
+ * live state. */
442
+ uint8_t grid[2][GRID_H][GRID_W]; /* the two wells (P2's unused in 1P) */
443
+ int8_t piece_x[2]; /* falling trio: column 0..5 */
444
+ int8_t piece_y[2]; /* row of its TOP cell (<0 above rim) */
445
+ uint8_t piece_col[2][3]; /* trio colours, top to bottom */
446
+ uint16_t score[2];
447
+ uint16_t hiscore;
448
+ uint8_t level; /* 1P: 1..9, speeds up the fall */
449
+ uint8_t state; /* ST_TITLE / ST_PLAY / ST_OVER */
450
+ uint8_t two_player;
451
+
452
+ static uint8_t matched[GRID_H][GRID_W];
453
+ static uint8_t well_x[2]; /* left interior char column per well */
454
+ static uint8_t fall_t[2]; /* frames until next gravity step */
455
+ static uint8_t prev0, prev1; /* edge-triggered input per port */
456
+ static uint16_t cleared_total; /* 1P: cells cleared, drives the level */
457
+
458
+ #define ST_TITLE 0
459
+ #define ST_PLAY 1
460
+ #define ST_OVER 2
71
461
 
72
- static uint8_t rng_pick(void) {
73
- rng = rng * 1103515245u + 12345u;
74
- return (uint8_t)(1 + (rng >> 16) % 3);
462
+ #define VS_FALL_DELAY 26 /* 2P: fixed gravity (frames per row) */
463
+ #define GARBAGE_CAP 4 /* max garbage rows per attack */
464
+
465
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
466
+ * THE CELL-DIFF REPAINT — the C64's "queued VRAM" equivalent, inverted.
467
+ * Screen RAM is plain memory, so there's no vblank queue to budget against
468
+ * (the NES version's whole drain_vram_budget machinery is moot). The C64's
469
+ * scarcity is CPU: a naive "repaint the whole 6x12 well every frame" is 72
470
+ * cells × (a colour write + a char write) of cc65 C, and a WHOLE-SCREEN
471
+ * 880-cell repaint costs ~50 frames — a frozen second (the TALUS TROT
472
+ * platformer hit exactly this; see its paint_level note).
473
+ *
474
+ * So we keep a SHADOW of what's on screen and repaint ONLY cells that
475
+ * changed. set_cell() compares against shadow[] and writes screen+color RAM
476
+ * only on a difference — most frames touch 0-3 cells (the trio that moved).
477
+ * A full cascade dirties the whole well, but spread across the cells that
478
+ * actually changed it's still a few dozen writes, not 880. THE RULE: never
479
+ * blit the board wholesale during play; always go through set_cell so the
480
+ * diff does the work. (Static screens — title, game-over — CAN repaint
481
+ * freely; they're not in the per-frame path.) */
482
+ static uint8_t shadow[25][40]; /* mirror of screen RAM char codes */
483
+ static uint8_t shadow_c[25][40]; /* mirror of color RAM (so a colour-only
484
+ * change still repaints — empty cells share
485
+ * CH_DOT with the backdrop but want their
486
+ * own well colour) */
487
+
488
+ static void set_cell(uint8_t row, uint8_t col, uint8_t ch, uint8_t color) {
489
+ if (shadow[row][col] == ch && shadow_c[row][col] == color) return; /* unchanged */
490
+ shadow[row][col] = ch;
491
+ shadow_c[row][col] = color;
492
+ {
493
+ uint16_t off = (uint16_t)row * 40 + col;
494
+ SCREEN[off] = ch;
495
+ COLORS[off] = color;
496
+ }
75
497
  }
76
498
 
77
- static void draw_cell(int8_t r, int8_t c) {
78
- uint16_t sx, sy;
79
- uint8_t col_chr;
80
- if (r < 0 || r >= ROWS) return;
81
- sx = GRID_C + c;
82
- sy = GRID_R + r;
83
- col_chr = grid[r][c];
84
- if (col_chr == 0) {
85
- SCREEN[sy * 40 + sx] = ' ';
86
- } else {
87
- SCREEN[sy * 40 + sx] = CELL_CHAR;
88
- COLORS[sy * 40 + sx] = col_chr; /* col is the C64 colour id */
499
+ /* Paint ONE board cell at (grid r,c) for player p, honoring the falling trio
500
+ * overlaid on top of the locked grid. Empty = faint speck; filled = tinted
501
+ * solid block. */
502
+ static void draw_board_cell(uint8_t p, uint8_t r, uint8_t c) {
503
+ uint8_t v = grid[p][r][c];
504
+ /* Is the falling trio occupying this cell? (only for the active well) */
505
+ if ((p == 0 || two_player) && piece_x[p] == (int8_t)c) {
506
+ int8_t rel = (int8_t)((int8_t)r - piece_y[p]);
507
+ if (rel >= 0 && rel < 3) v = piece_col[p][rel];
89
508
  }
509
+ if (v) set_cell((uint8_t)(WELL_TOP + r), (uint8_t)(well_x[p] + c),
510
+ CH_BLOCK, cell_color[v]);
511
+ else set_cell((uint8_t)(WELL_TOP + r), (uint8_t)(well_x[p] + c),
512
+ CH_DOT, cell_color[0]);
90
513
  }
91
514
 
92
- static void draw_grid(void) {
515
+ /* Repaint a whole well through the cell-diff (used on board changes — the
516
+ * diff means only the cells that really moved cost anything). */
517
+ static void draw_well(uint8_t p) {
93
518
  uint8_t r, c;
94
- for (r = 0; r < ROWS; r++)
95
- for (c = 0; c < COLS; c++) draw_cell((int8_t)r, (int8_t)c);
519
+ for (r = 0; r < GRID_H; r++)
520
+ for (c = 0; c < GRID_W; c++) draw_board_cell(p, r, c);
96
521
  }
97
522
 
98
- static void draw_piece(uint8_t clear) {
99
- uint8_t i;
100
- int8_t r;
101
- for (i = 0; i < 3; i++) {
102
- r = (int8_t)(piece_y + i);
103
- if (r < 0 || r >= ROWS) continue;
104
- if (clear) {
105
- grid[r][piece_x] = 0;
106
- draw_cell(r, (int8_t)piece_x);
107
- } else {
108
- uint8_t saved = grid[r][piece_x];
109
- grid[r][piece_x] = piece[i];
110
- draw_cell(r, (int8_t)piece_x);
111
- grid[r][piece_x] = saved;
523
+ /* ── GAME LOGIC (clay) — the HUD bar (rows 0-1, the fixed split) ── */
524
+ static void draw_bar_labels(void) {
525
+ uint8_t c;
526
+ for (c = 0; c < 40; c++) { /* row 1: solid divider line */
527
+ SCREEN[40 + c] = CH_BLOCK;
528
+ COLORS[40 + c] = COLOR_DARK_GRAY;
529
+ SCREEN[80 + c] = CH_BLANK; /* row 2: the blank spacer the
530
+ * raster split hides in */
531
+ SCREEN[c] = CH_BLANK;
532
+ }
533
+ draw_text(0, 1, "SC");
534
+ draw_text(0, 12, "HI");
535
+ if (two_player) draw_text(0, 30, "P2");
536
+ else draw_text(0, 30, "LV");
537
+ }
538
+ static void draw_bar_stats(void) {
539
+ draw_u16(0, 4, score[0]);
540
+ draw_u16(0, 15, hiscore);
541
+ if (two_player) draw_u16(0, 33, score[1]);
542
+ else {
543
+ SCREEN[33] = (uint8_t)('0' + level);
544
+ COLORS[33] = COLOR_WHITE;
545
+ }
546
+ }
547
+
548
+ /* ── GAME LOGIC (clay) — paint the well frame (one cell outside the interior).
549
+ * Runs on state changes only (a static screen), so it may write directly. ── */
550
+ static void paint_frame(uint8_t p) {
551
+ uint8_t r, c, x0 = well_x[p];
552
+ for (c = (uint8_t)(x0 - 1); c <= (uint8_t)(x0 + GRID_W); c++) {
553
+ set_cell(WELL_TOP - 1, c, CH_FRAME, COLOR_BROWN);
554
+ set_cell((uint8_t)(WELL_TOP + GRID_H), c, CH_FRAME, COLOR_BROWN);
555
+ }
556
+ for (r = (uint8_t)(WELL_TOP - 1); r <= (uint8_t)(WELL_TOP + GRID_H); r++) {
557
+ set_cell(r, (uint8_t)(x0 - 1), CH_FRAME, COLOR_BROWN);
558
+ set_cell(r, (uint8_t)(x0 + GRID_W), CH_FRAME, COLOR_BROWN);
559
+ }
560
+ }
561
+
562
+ /* Clear the whole 25-row screen to blanks (and sync the shadow). Used on
563
+ * state changes — a static-screen operation, cheap enough once. */
564
+ static void clear_screen(void) {
565
+ uint16_t i;
566
+ for (i = 0; i < 1000; i++) { SCREEN[i] = CH_BLANK; COLORS[i] = COLOR_BLACK; }
567
+ for (i = 0; i < 25 * 40; i++) { ((uint8_t*)shadow)[i] = CH_BLANK; ((uint8_t*)shadow_c)[i] = COLOR_BLACK; }
568
+ }
569
+
570
+ /* ── GAME LOGIC (clay) — a thin ember speck band along the screen edges for
571
+ * the static screens (title / game-over). Just the top + bottom playfield
572
+ * rows get a sparse colour texture — enough to read as "alive" without the
573
+ * ~3-second full-screen 880-cell repaint freeze (the C64's documented
574
+ * full-repaint footgun; see the cell-diff idiom). The coloured BORDER (set
575
+ * once in main) does the heavy lifting for screen liveliness; this is garnish.
576
+ * Static-screen only — never per frame. ── */
577
+ static void paint_edge_band(void) {
578
+ uint8_t c;
579
+ volatile uint8_t *top = SCREEN + 3 * 40, *bot = SCREEN + 24 * 40;
580
+ volatile uint8_t *tcol = COLORS + 3 * 40, *bcol = COLORS + 24 * 40;
581
+ for (c = 0; c < 40; c++) {
582
+ uint8_t lit = (uint8_t)((c & 1) == 0);
583
+ top[c] = bot[c] = lit ? CH_DOT : CH_BLANK;
584
+ tcol[c] = bcol[c] = lit ? COLOR_BROWN : COLOR_DARK_GRAY;
585
+ shadow[3][c] = shadow[24][c] = top[c];
586
+ shadow_c[3][c] = shadow_c[24][c] = tcol[c];
587
+ }
588
+ }
589
+
590
+ /* ── GAME LOGIC (clay) — the title screen (static, free to repaint) ── */
591
+ static void paint_title(void) {
592
+ clear_screen();
593
+ paint_edge_band();
594
+ two_player = 0;
595
+ draw_bar_labels();
596
+ draw_bar_stats();
597
+ draw_text_band(8, (40 - (sizeof(GAME_TITLE) - 1)) / 2, GAME_TITLE);
598
+ draw_text_band(13, 12, "PORT 2 FIRE - 1P");
599
+ draw_text_band(15, 9, "PORT 1 FIRE - 2P VERSUS");
600
+ draw_text_band(18, 7, "UP ROTATE - FIRE DROP");
601
+ draw_text_band(20, 6, "CHAINS ERUPT ON YOUR RIVAL");
602
+ draw_text_band(23, 16, "HI");
603
+ draw_u16(23, 19, hiscore);
604
+ state = ST_TITLE;
605
+ }
606
+
607
+ /* ── GAME LOGIC (clay) — paint the playfield (wells + HUD), static. ── */
608
+ static void paint_play(void) {
609
+ clear_screen();
610
+ paint_edge_band(); /* thin ember edge garnish (static) */
611
+ draw_bar_labels();
612
+ draw_bar_stats();
613
+ paint_frame(0);
614
+ draw_well(0);
615
+ if (two_player) {
616
+ paint_frame(1);
617
+ draw_well(1);
618
+ draw_text(11, 19, "VS");
619
+ }
620
+ }
621
+
622
+ /* ── GAME LOGIC (clay) — game-over / results (static screen). ── */
623
+ static void paint_over(uint8_t loser) {
624
+ clear_screen();
625
+ paint_edge_band();
626
+ draw_bar_labels();
627
+ if (two_player)
628
+ draw_text_band(8, 16, loser ? "P1 WINS" : "P2 WINS");
629
+ else
630
+ draw_text_band(8, 15, "GAME OVER");
631
+ draw_text_band(12, 13, "P1");
632
+ draw_u16(12, 17, score[0]);
633
+ if (two_player) {
634
+ draw_text_band(14, 13, "P2");
635
+ draw_u16(14, 17, score[1]);
636
+ }
637
+ draw_text_band(17, 13, "HI");
638
+ draw_u16(17, 17, hiscore);
639
+ draw_text_band(21, 12, "FIRE - TITLE");
640
+ }
641
+
642
+ /* ── GAME LOGIC (clay) — end of game (top-out). `loser` topped out. ── */
643
+ static void game_end(uint8_t loser) {
644
+ uint16_t best = score[0];
645
+ if (two_player && score[1] > best) best = score[1];
646
+ if (best > hiscore) {
647
+ hiscore = best;
648
+ hiscore_save(hiscore); /* the persistence seam — see its block doc */
649
+ }
650
+ sfx_noise(24); /* game-over rumble */
651
+ state = ST_OVER;
652
+ prev0 = prev1 = 0x1F; /* swallow the held FIRE */
653
+ paint_over(loser);
654
+ }
655
+
656
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
657
+ * Match scan: mark every straight run of 3+ same-coloured blocks in all 4
658
+ * directions (a cell can belong to several runs — the mask de-dupes), and
659
+ * return how many cells matched. Same routine as every other platform's
660
+ * version of this game. */
661
+ static const int8_t DIRS4[4][2] = { {0,1}, {1,0}, {1,1}, {1,-1} };
662
+
663
+ static uint8_t mark_and_count(uint8_t p) {
664
+ uint8_t r, c, d, len, k, cnt, col;
665
+ int8_t dr, dc;
666
+ int sr, sc;
667
+ cnt = 0;
668
+ for (r = 0; r < GRID_H; r++)
669
+ for (c = 0; c < GRID_W; c++) matched[r][c] = 0;
670
+ for (r = 0; r < GRID_H; r++) {
671
+ for (c = 0; c < GRID_W; c++) {
672
+ col = grid[p][r][c];
673
+ if (col == EMPTY) continue;
674
+ for (d = 0; d < 4; d++) {
675
+ dr = DIRS4[d][0]; dc = DIRS4[d][1];
676
+ sr = (int)r - dr; sc = (int)c - dc;
677
+ if (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
678
+ && grid[p][sr][sc] == col) continue; /* not the run's start */
679
+ len = 1;
680
+ sr = (int)r + dr; sc = (int)c + dc;
681
+ while (sr >= 0 && sr < GRID_H && sc >= 0 && sc < GRID_W
682
+ && grid[p][sr][sc] == col) { len++; sr += dr; sc += dc; }
683
+ if (len >= 3) {
684
+ sr = r; sc = c;
685
+ for (k = 0; k < len; k++) {
686
+ if (!matched[sr][sc]) { matched[sr][sc] = 1; cnt++; }
687
+ sr += dr; sc += dc;
688
+ }
689
+ }
690
+ }
691
+ }
692
+ }
693
+ return cnt;
694
+ }
695
+
696
+ /* Collapse each column so survivors rest on the floor (walk from the bottom,
697
+ * copying blocks down to a write cursor, then zero everything above it). */
698
+ static void apply_gravity(uint8_t p) {
699
+ uint8_t c;
700
+ int8_t r, w;
701
+ for (c = 0; c < GRID_W; c++) {
702
+ w = GRID_H - 1;
703
+ for (r = GRID_H - 1; r >= 0; r--) {
704
+ if (grid[p][r][c] != EMPTY) { grid[p][w][c] = grid[p][r][c]; w--; }
112
705
  }
706
+ for (; w >= 0; w--) grid[p][w][c] = EMPTY;
113
707
  }
114
708
  }
115
709
 
116
- static uint8_t collides(int8_t x, int8_t y) {
117
- uint8_t i;
710
+ /* ── GAME LOGIC (clay) — clear matches, drop survivors, chain cascades.
711
+ * Returns the chain depth (0 = the lock matched nothing). Repaints go
712
+ * through the cell-diff via draw_well. */
713
+ static uint8_t resolve_board(uint8_t p) {
714
+ uint8_t n, r, c, chain;
715
+ uint16_t amt;
716
+ chain = 0;
717
+ for (;;) {
718
+ n = mark_and_count(p);
719
+ if (n == 0) break;
720
+ ++chain;
721
+ for (r = 0; r < GRID_H; r++)
722
+ for (c = 0; c < GRID_W; c++)
723
+ if (matched[r][c]) grid[p][r][c] = EMPTY;
724
+ amt = (uint16_t)n * 10;
725
+ if (chain > 1) amt *= chain; /* cascades pay multiplied */
726
+ if (score[p] < 65000) score[p] += amt;
727
+ /* clear chime — pitch rises with chain depth (higher freq_hi byte). */
728
+ sfx_tone(2, 0x00, (uint8_t)(0x30 + (chain << 2)), 8);
729
+ apply_gravity(p);
730
+ draw_well(p);
731
+ if (!two_player) {
732
+ cleared_total += n;
733
+ while (level < 9 && cleared_total >= (uint16_t)level * 10) ++level;
734
+ }
735
+ draw_bar_stats();
736
+ }
737
+ return chain;
738
+ }
739
+
740
+ /* ── GAME LOGIC (clay) — VERSUS attack: garbage rows ERUPT up from the bottom
741
+ * of the victim's well (random blocks with one gap — matchable, so a skilled
742
+ * victim digs out). The victim's stack rising means the falling trio shifts
743
+ * up one to stay board-aligned; if the top row is already occupied, the
744
+ * victim tops out and loses. ── */
745
+ static void garbage_insert(uint8_t v, uint8_t nrows) {
746
+ uint8_t k, c, gap;
118
747
  int8_t r;
119
- if (x < 0 || x >= COLS) return 1;
748
+ sfx_noise(8); /* incoming-garbage thud */
749
+ for (k = 0; k < nrows; k++) {
750
+ for (c = 0; c < GRID_W; c++)
751
+ if (grid[v][0][c] != EMPTY) { game_end(v); return; }
752
+ for (r = 0; r < GRID_H - 1; r++)
753
+ for (c = 0; c < GRID_W; c++)
754
+ grid[v][r][c] = grid[v][r + 1][c];
755
+ gap = random8() % GRID_W;
756
+ for (c = 0; c < GRID_W; c++)
757
+ grid[v][GRID_H - 1][c] = (c == gap) ? EMPTY : (uint8_t)(1 + random8() % 3);
758
+ if (piece_y[v] > -3) --piece_y[v]; /* keep the trio aligned */
759
+ }
760
+ draw_well(v);
761
+ }
762
+
763
+ /* Can the trio occupy column x, rows y..y+2? Cells above the rim are fine
764
+ * (pieces enter from above); below the floor or on a block is not. */
765
+ static uint8_t can_place(uint8_t p, int8_t x, int8_t y) {
766
+ int8_t i, cy;
767
+ if (x < 0 || x >= GRID_W) return 0;
120
768
  for (i = 0; i < 3; i++) {
121
- r = (int8_t)(y + i);
122
- if (r >= ROWS) return 1;
123
- if (r >= 0 && grid[r][x] != 0) return 1;
769
+ cy = (int8_t)(y + i);
770
+ if (cy < 0) continue;
771
+ if (cy >= GRID_H) return 0;
772
+ if (grid[p][cy][x] != EMPTY) return 0;
124
773
  }
125
- return 0;
774
+ return 1;
126
775
  }
127
776
 
128
- static void new_piece(void) {
129
- piece[0] = (uint8_t)(2 + rng_pick()); /* C64 colors 3,4,5: red, mauve, green */
130
- piece[1] = (uint8_t)(2 + rng_pick());
131
- piece[2] = (uint8_t)(2 + rng_pick());
132
- piece_x = COLS / 2 - 1;
133
- piece_y = -3;
777
+ static void spawn_piece(uint8_t p) {
778
+ piece_x[p] = GRID_W / 2;
779
+ piece_y[p] = -2;
780
+ piece_col[p][0] = (uint8_t)(1 + random8() % 3);
781
+ piece_col[p][1] = (uint8_t)(1 + random8() % 3);
782
+ piece_col[p][2] = (uint8_t)(1 + random8() % 3);
783
+ if (!can_place(p, piece_x[p], piece_y[p])) game_end(p);
134
784
  }
135
785
 
136
- static void lock_piece(void) {
137
- uint8_t i, c;
138
- int8_t r;
139
- uint8_t a, b, d;
786
+ /* ── GAME LOGIC (clay) — land the trio, resolve, attack, respawn. ── */
787
+ static void lock_piece(uint8_t p) {
788
+ int8_t i, y;
789
+ uint8_t chain;
140
790
  for (i = 0; i < 3; i++) {
141
- r = (int8_t)(piece_y + i);
142
- if (r >= 0 && r < ROWS) grid[r][piece_x] = piece[i];
791
+ y = (int8_t)(piece_y[p] + i);
792
+ if (y >= 0) grid[p][y][piece_x[p]] = piece_col[p][i];
793
+ }
794
+ sfx_tone(2, 0x00, 0x18, 4); /* lock thunk */
795
+ draw_well(p);
796
+ if (piece_y[p] < 0) { game_end(p); return; } /* locked above the rim */
797
+ chain = resolve_board(p);
798
+ if (state != ST_PLAY) return;
799
+ if (chain && two_player) {
800
+ garbage_insert((uint8_t)(p ^ 1), chain > GARBAGE_CAP ? GARBAGE_CAP : chain);
801
+ if (state != ST_PLAY) return; /* garbage topped them out */
143
802
  }
803
+ spawn_piece(p);
804
+ }
805
+
806
+ /* ── GAME LOGIC (clay) — per-player input + gravity. Edge-triggered moves
807
+ * (one cell per press), held DOWN soft-drops, UP cycles the trio's colours
808
+ * (the classic trio "rotate"), FIRE hard-drops. P2 reads control PORT 1. ──
809
+ * The board cells the trio used to occupy are repainted via draw_board_cell
810
+ * before/after the move, so the cell-diff erases its trail and stamps its
811
+ * new spot — never a whole-well blit. */
812
+ static void erase_trio(uint8_t p) {
813
+ int8_t i, y;
144
814
  for (i = 0; i < 3; i++) {
145
- r = (int8_t)(piece_y + i);
146
- if (r < 0 || r >= ROWS) continue;
147
- for (c = 0; c <= COLS - 3; c++) {
148
- a = grid[r][c]; b = grid[r][c+1]; d = grid[r][c+2];
149
- if (a != 0 && a == b && b == d) {
150
- grid[r][c] = 0;
151
- grid[r][c+1] = 0;
152
- grid[r][c+2] = 0;
153
- if (score < 65500u) score += 30;
154
- sfx_tone(0, 0x80, 0x10, 12);
155
- }
156
- }
815
+ y = (int8_t)(piece_y[p] + i);
816
+ if (y >= 0 && y < GRID_H) draw_board_cell(p, (uint8_t)y, (uint8_t)piece_x[p]);
157
817
  }
158
- draw_grid();
818
+ }
819
+
820
+ static void stamp_trio(uint8_t p) {
821
+ int8_t i, y;
822
+ for (i = 0; i < 3; i++) {
823
+ y = (int8_t)(piece_y[p] + i);
824
+ if (y >= 0 && y < GRID_H) draw_board_cell(p, (uint8_t)y, (uint8_t)piece_x[p]);
825
+ }
826
+ }
827
+
828
+ static void update_player(uint8_t p, uint8_t pad, uint8_t prev) {
829
+ uint8_t fresh = (uint8_t)(pad & ~prev);
830
+ uint8_t t, fd;
831
+ erase_trio(p); /* lift the trio off the board */
832
+ if ((fresh & JOY_LEFT) && can_place(p, (int8_t)(piece_x[p] - 1), piece_y[p]))
833
+ --piece_x[p];
834
+ if ((fresh & JOY_RIGHT) && can_place(p, (int8_t)(piece_x[p] + 1), piece_y[p]))
835
+ ++piece_x[p];
836
+ if (fresh & JOY_UP) { /* cycle colours downward */
837
+ t = piece_col[p][2];
838
+ piece_col[p][2] = piece_col[p][1];
839
+ piece_col[p][1] = piece_col[p][0];
840
+ piece_col[p][0] = t;
841
+ sfx_tone(2, 0x00, 0x28, 3);
842
+ }
843
+ if (fresh & JOY_FIRE) { /* hard drop */
844
+ while (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1))) ++piece_y[p];
845
+ lock_piece(p); /* may end the game */
846
+ return;
847
+ }
848
+ if (pad & JOY_DOWN) fall_t[p] += 4; /* soft drop */
849
+ ++fall_t[p];
850
+ fd = two_player ? VS_FALL_DELAY
851
+ : (uint8_t)(34 - ((level << 1) + level)); /* 31..7 */
852
+ if (fall_t[p] >= fd) {
853
+ fall_t[p] = 0;
854
+ if (can_place(p, piece_x[p], (int8_t)(piece_y[p] + 1)))
855
+ ++piece_y[p];
856
+ else { lock_piece(p); return; } /* may end the game */
857
+ }
858
+ if (state == ST_PLAY) stamp_trio(p); /* re-stamp the trio's new spot */
859
+ }
860
+
861
+ /* ── GAME LOGIC (clay) — start a run ── */
862
+ static void start_game(uint8_t versus) {
863
+ uint8_t p, r, c;
864
+ two_player = versus;
865
+ well_x[0] = versus ? WELL_VS_P1 : WELL_1P_X;
866
+ well_x[1] = WELL_VS_P2;
867
+ /* Stir the PRNG with time-spent-on-title so runs differ. */
868
+ rng ^= (uint16_t)frame_count ^ ((uint16_t)frame_count << 7);
869
+ if (rng == 0) rng = 0xACE1;
870
+ for (p = 0; p < 2; p++) {
871
+ for (r = 0; r < GRID_H; r++)
872
+ for (c = 0; c < GRID_W; c++) grid[p][r][c] = EMPTY;
873
+ fall_t[p] = 0;
874
+ score[p] = 0;
875
+ }
876
+ cleared_total = 0;
877
+ level = 1;
878
+ state = ST_PLAY;
879
+ prev0 = prev1 = 0x1F; /* the button that started the game
880
+ * shouldn't also rotate the first trio */
881
+ paint_play();
882
+ spawn_piece(0);
883
+ if (versus) spawn_piece(1);
884
+ draw_well(0);
885
+ if (versus) draw_well(1);
886
+ sfx_tone(2, 0x00, 0x20, 10); /* start jingle */
159
887
  }
160
888
 
161
889
  void main(void) {
162
- uint8_t r, c, pad, prev = 0, fall_rate, t;
163
- POKE(VIC_BORDER, 0x06); /* blue border frames the playfield */
164
- POKE(VIC_BG0, 0x00); /* black well interior so blocks pop */
890
+ uint8_t pad0, pad1;
165
891
 
166
- for (r = 0; r < ROWS; r++)
167
- for (c = 0; c < COLS; c++) grid[r][c] = 0;
892
+ /* ── HARDWARE IDIOM (load-bearing) boot order. VIC + SID config before
893
+ * the IRQ goes live; sfx_init BEFORE music_init (sfx_init writes a plain
894
+ * volume to $D418, music_init re-asserts the filter-mode bits on top). ── */
895
+ POKE(VIC_SPR_ENA, 0); /* no hardware sprites — board is chars */
896
+ /* A coloured BORDER (one register, zero per-frame cost) keeps the screen
897
+ * visibly alive even though the board itself is small over a black BG —
898
+ * the border is ~40% of the framebuffer, so no single colour dominates the
899
+ * render-health pixel scan. (Compare the platformer/shmup, which instead
900
+ * fill the field with a scrolling starfield; a puzzle board doesn't.) */
901
+ POKE(VIC_BORDER, COLOR_BROWN);
902
+ POKE(VIC_BG0, COLOR_BLACK);
903
+ POKE(VIC_CTRL2, D016_BAR); /* 38-col mode from the start */
904
+ POKE(CIA1_DDRA, 0xFF); /* port A drives keyboard columns */
905
+ POKE(CIA1_DDRB, 0x00); /* port B reads rows / stick 1 */
168
906
 
169
- score = 0; fall_timer = 0;
170
907
  sfx_init();
171
- draw_field(); /* paint the textured surround + well frame */
172
- new_piece();
173
- draw_grid();
908
+ music_init();
909
+ hiscore = hiscore_load(); /* 0 until the core save round lands */
910
+
911
+ clear_screen();
912
+ install_raster_irq(); /* the split + heartbeat go live */
913
+ paint_title();
174
914
 
175
915
  for (;;) {
176
- pad = (uint8_t)(~PEEK(CIA1_PRA) & 0x1F);
177
- wait_vblank();
916
+ wait_frame(); /* the line-251 IRQ paces everything */
917
+
918
+ music_update();
178
919
  sfx_update();
179
- draw_piece(1);
920
+ pad0 = read_stick_port2(); /* P1 — control port 2 (convention) */
921
+ pad1 = read_stick_port1(); /* P2 — control port 1 */
180
922
 
181
- if ((pad & JOY_LEFT) && !(prev & JOY_LEFT) && !collides(piece_x - 1, piece_y)) piece_x--;
182
- if ((pad & JOY_RIGHT) && !(prev & JOY_RIGHT) && !collides(piece_x + 1, piece_y)) piece_x++;
183
- if ((pad & JOY_FIRE) && !(prev & JOY_FIRE)) {
184
- t = piece[0]; piece[0] = piece[1]; piece[1] = piece[2]; piece[2] = t;
185
- sfx_tone(1, 0x20, 0x14, 2);
186
- }
187
- if ((pad & JOY_UP) && !(prev & JOY_UP)) {
188
- while (!collides(piece_x, (int8_t)(piece_y + 1))) piece_y++;
189
- lock_piece();
190
- new_piece();
191
- prev = pad;
923
+ if (state == ST_TITLE) {
924
+ /* Mode select doubles as a controls demo: the stick that presses FIRE
925
+ * picks the mode port 2 starts 1P, port 1 starts 2P versus. */
926
+ if ((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) start_game(0);
927
+ else if ((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE)) start_game(1);
928
+ prev0 = pad0; prev1 = pad1;
192
929
  continue;
193
930
  }
194
- prev = pad;
195
-
196
- fall_rate = (pad & JOY_DOWN) ? 4 : 30;
197
- if (++fall_timer >= fall_rate) {
198
- fall_timer = 0;
199
- if (collides(piece_x, (int8_t)(piece_y + 1))) {
200
- lock_piece();
201
- new_piece();
202
- } else {
203
- piece_y++;
204
- }
931
+
932
+ if (state == ST_OVER) {
933
+ if (((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) ||
934
+ ((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE))) paint_title();
935
+ prev0 = pad0; prev1 = pad1;
936
+ continue;
205
937
  }
206
- draw_piece(0);
938
+
939
+ /* ── ST_PLAY ─────────────────────────────────────────────────────────
940
+ * Both players update EVERY frame (simultaneous versus, not alternating
941
+ * turns). Any update can end the game, so re-check state between them. */
942
+ update_player(0, pad0, prev0);
943
+ if (two_player && state == ST_PLAY) update_player(1, pad1, prev1);
944
+ prev0 = pad0; prev1 = pad1;
207
945
  }
208
946
  }