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
@@ -0,0 +1,1056 @@
1
+ ; ── puzzle.asm — TILE TWINS — Atari 2600 memory match-pairs (complete game) ──
2
+ ;
3
+ ; A COMPLETE, working game — drawn title screen, a turn-based MEMORY puzzle
4
+ ; (flip two tiles, match the pair to clear them, clear the whole board to
5
+ ; win), a move counter + in-session best (fewest flips), TIA sound effects +
6
+ ; a title jingle, a win/game-over state with auto-return to the title, and
7
+ ; the 2600's signature feature: THE WHOLE MACHINE. There is no framebuffer,
8
+ ; no tilemap, no OS — every visible scanline is composed live by racing the
9
+ ; beam.
10
+ ;
11
+ ; WHY THIS IS A PUZZLE, NOT AN ACTION GAME: nothing moves on its own. The
12
+ ; board is static; the player THINKS, moves a cursor, and chooses which two
13
+ ; tiles to flip. The challenge is memory + deduction, not reflexes. That is
14
+ ; the honest "puzzle" idiom — and it suits the 2600 well, because a static,
15
+ ; turn-based board needs no per-frame motion and so the kernel is simple.
16
+ ;
17
+ ; THE BOARD: 8 tiles = 4 PAIRS, drawn as a vertical stack of 8 bands. Each
18
+ ; tile holds a hidden VALUE 0..3 (two of each, shuffled at game start). A
19
+ ; tile is in one of three display states:
20
+ ; FACE-DOWN — drawn in neutral gray (you don't know its value)
21
+ ; REVEALED — drawn in its VALUE's color (you flipped it this turn)
22
+ ; MATCHED — drawn dark/empty (cleared; it's out of play)
23
+ ; The cursor (the tile you're about to flip) gets a bright border line.
24
+ ;
25
+ ; TIA object roles:
26
+ ; PF = the 8 tile bands (full-width blocks; the only 2600 object wide
27
+ ; enough to read as a "tile"). COLUPF changes per band = per-
28
+ ; tile color, the easy honest way to show 4 distinct values.
29
+ ; COLUBK = the cursor highlight (the selected band's background brightens).
30
+ ;
31
+ ; CONTROLS: joystick UP/DOWN moves the cursor; FIRE flips the tile under it.
32
+ ; Flip one, then flip another: match → both clear + a chime; miss → both flip
33
+ ; back after a short pause. Match all 4 pairs to win.
34
+
35
+ processor 6502
36
+ org $F000
37
+
38
+ ; ── TIA write registers ───────────────────────────────────────────────
39
+ VSYNC = $00
40
+ VBLANK = $01
41
+ WSYNC = $02
42
+ NUSIZ0 = $04
43
+ NUSIZ1 = $05
44
+ COLUP0 = $06
45
+ COLUP1 = $07
46
+ COLUPF = $08
47
+ COLUBK = $09
48
+ CTRLPF = $0A
49
+ PF0 = $0D
50
+ PF1 = $0E
51
+ PF2 = $0F
52
+ RESP0 = $10
53
+ GRP0 = $1B
54
+ HMP0 = $20
55
+ HMOVE = $2A
56
+ HMCLR = $2B
57
+ CXCLR = $2C
58
+ ; ── TIA audio ─────────────────────────────────────────────────────────
59
+ AUDC0 = $15
60
+ AUDC1 = $16
61
+ AUDF0 = $17
62
+ AUDF1 = $18
63
+ AUDV0 = $19
64
+ AUDV1 = $1A
65
+ ; ── TIA READ registers ─────────────────────────────────────────────────
66
+ INPT4 = $0C ; joystick 0 fire (bit7, ACTIVE LOW)
67
+ ; ── RIOT ──────────────────────────────────────────────────────────────
68
+ SWCHA = $280 ; joysticks: P0 = high nibble (active LOW)
69
+ SWCHB = $282 ; console: bit0 RESET, bit1 SELECT (ACTIVE LOW)
70
+ INTIM = $284 ; timer read
71
+ TIM64T = $296 ; timer set, 64-cycle ticks
72
+
73
+ ; ── Zero-page state (the 2600's ENTIRE RAM is $80-$FF — 128 bytes; in
74
+ ; core memory dumps system_ram offset 0 = $80) ────────────────────────
75
+ STATE = $80 ; 0 = title, 1 = play, 2 = game over / win
76
+ CURSOR = $81 ; selected tile index 0..7
77
+ FIRST = $82 ; index of the first flipped tile this turn, or $FF none
78
+ MOVES = $83 ; flips taken this game, BCD (the score — LOWER is better)
79
+ MOVES_HI = $84 ; high byte of the move count, BCD
80
+ MATCHED = $85 ; bit i set = tile i is matched/cleared (8 bits)
81
+ REVEAL = $86 ; bit i set = tile i is currently face-UP (revealed)
82
+ PAIRS = $87 ; pairs found so far (win at 4)
83
+ MISS_T = $88 ; >0 = mismatch pause countdown (both tiles shown, then hide)
84
+ FRAME = $89
85
+ SFX_LEFT = $8A ; frames remaining on the voice-0 sound effect
86
+ TUNE_SEL = $8B ; 0 = title jingle, 1 = win/over tune (voice 1)
87
+ TUNE_POS = $8C
88
+ TUNE_LEFT = $8D ; frames left on current jingle note (0 = silent)
89
+ OVER_T = $8E ; game-over auto-return-to-title countdown
90
+ SWCHB_PRV = $8F ; previous SWCHB for RESET edge detect
91
+ FIRE_PRV = $90 ; previous fire level (bit7) for fire-edge detect
92
+ DPAD_PRV = $91 ; previous SWCHA for up/down edge detect
93
+ EDGEB = $92 ; this frame's RESET press-edge (bit0)
94
+ FIRE_EDG = $93 ; this frame's fire press-edge (bit7)
95
+ TMP = $94
96
+ TMP2 = $95
97
+ RNG = $96 ; pseudo-random state (LFSR), reseeded each idle frame
98
+ BOARD = $97 ; 8 bytes: hidden value 0..3 of each tile
99
+ ; (BOARD..BOARD+7 = $97..$9E)
100
+ S0BUF = $A0 ; 6 rows: packed move-count digits for the kernel
101
+ MOVES_BSV = $A6 ; SESSION best (fewest moves to clear), BCD low
102
+ MOVES_BSH = $A7 ; RAM only — real 2600 carts have no battery.
103
+ HSBUF = $A8 ; 6 rows: best, packed (for the title kernel)
104
+ SCRATCH = $AE ; 6 bytes general kernel/packer scratch
105
+
106
+ ; ── layout / tuning constants (clay — change to reshape the game) ──────
107
+ NTILES = 8 ; 4 pairs
108
+ NVALUES = 4 ; distinct tile values (two of each)
109
+ WIN_PAIRS = 4
110
+ BANDH = 18 ; scanlines per tile band (8 × 18 = 144)
111
+ BANDGAP = 4 ; black separator lines at the bottom of each band
112
+ ; (so the 8 tiles read as 8 distinct bars). The lit
113
+ ; tile occupies BANDH-BANDGAP lines.
114
+ MISS_HOLD = 45 ; frames a mismatched pair stays visible before hiding
115
+
116
+ COL_BG = $00 ; black gap behind the board
117
+ COL_DOWN = $06 ; neutral gray — a face-DOWN tile
118
+ COL_GONE = $02 ; near-black — a matched/cleared tile
119
+ COL_CUR = $0E ; cursor highlight — bright white separator bar
120
+ COL_HUD = $0E ; white move-counter digits
121
+
122
+ ; the four VALUE colors (revealed tiles). Distinct hues, all bright.
123
+ VAL_COL0 = $46 ; red
124
+ VAL_COL1 = $1E ; yellow
125
+ VAL_COL2 = $96 ; blue
126
+ VAL_COL3 = $C8 ; green
127
+
128
+ START:
129
+ SEI
130
+ CLD
131
+ LDX #$FF
132
+ TXS
133
+ LDA #0
134
+ .clr:
135
+ STA $00,X ; clears ALL of $00-$FF: zero page RAM AND the TIA
136
+ DEX ; write registers (GRP/audio all silenced — the
137
+ BNE .clr ; standard 2600 power-on hygiene)
138
+
139
+ ; single, full-width objects everywhere; the cursor sprite (P0) is one band
140
+ ; tall and we reposition it per frame.
141
+ LDA #%00000000
142
+ STA NUSIZ0
143
+ STA NUSIZ1
144
+ LDA #$FF
145
+ STA RNG ; nonzero LFSR seed
146
+
147
+ JSR enter_title
148
+
149
+ ; ──────────────────────────────────────────────────────────────────────
150
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this) ───────
151
+ ; THE FRAME LOOP. 262 scanlines, every frame, forever. VBLANK and overscan
152
+ ; are timed with the RIOT timer (TIM64T) instead of counted WSYNCs: set the
153
+ ; timer, run however much game logic the state needs, then spin on INTIM.
154
+ ; This kills the classic homebrew bug where adding one branch to the logic
155
+ ; emits a 263rd line and the TV loses vsync. The VISIBLE 192 lines are still
156
+ ; counted exactly by the kernels below.
157
+ ; ──────────────────────────────────────────────────────────────────────
158
+ MAIN:
159
+ LDA #2
160
+ STA VBLANK
161
+ STA VSYNC
162
+ STA WSYNC
163
+ STA WSYNC
164
+ STA WSYNC
165
+ LDA #0
166
+ STA VSYNC
167
+ LDA #43
168
+ STA TIM64T
169
+
170
+ JSR frame_logic ; all game thinking happens in the blanked region
171
+
172
+ .vbwait:
173
+ LDA INTIM
174
+ BNE .vbwait
175
+ STA WSYNC
176
+
177
+ LDA STATE
178
+ BNE .ingame
179
+ JMP title_kernel
180
+ .ingame:
181
+ JMP play_kernel
182
+
183
+ kernel_done:
184
+ LDA #2
185
+ STA VBLANK
186
+ LDA #35
187
+ STA TIM64T
188
+ .oswait:
189
+ LDA INTIM
190
+ BNE .oswait
191
+ STA WSYNC
192
+ JMP MAIN
193
+
194
+ ; ──────────────────────────────────────────────────────────────────────
195
+ ; Per-frame logic, dispatched by state. Runs entirely inside timed VBLANK.
196
+ ; ──────────────────────────────────────────────────────────────────────
197
+ frame_logic:
198
+ INC FRAME
199
+ JSR audio_tick
200
+
201
+ ; ── HARDWARE IDIOM (load-bearing) ──
202
+ ; Console RESET, fire, and the joystick are ACTIVE LOW and not debounced;
203
+ ; convert each to a press-EDGE once per frame (was-released AND pressed-now)
204
+ ; so one physical press = one action, not one-per-frame.
205
+ LDA SWCHB
206
+ TAX
207
+ EOR #$FF
208
+ AND SWCHB_PRV
209
+ STA EDGEB ; bit0 = RESET edge
210
+ STX SWCHB_PRV
211
+ ; fire button → edge in bit7
212
+ LDA #0
213
+ BIT INPT4
214
+ BMI .fup ; bit7 set = not pressed (active low)
215
+ ORA #$80
216
+ .fup:
217
+ TAY
218
+ LDA FIRE_PRV
219
+ EOR #$FF
220
+ STA TMP
221
+ TYA
222
+ AND TMP
223
+ STA FIRE_EDG ; bit7 = fire press-edge
224
+ STY FIRE_PRV
225
+
226
+ ; keep the RNG churning while we wait for input
227
+ JSR rng_step
228
+
229
+ LDA STATE
230
+ BEQ logic_title
231
+ CMP #1
232
+ BEQ logic_play_jmp
233
+ JMP logic_over
234
+ logic_play_jmp:
235
+ JMP logic_play
236
+
237
+ ; ── GAME LOGIC (clay — reshape freely) ── title-screen behavior ────────
238
+ logic_title:
239
+ ; fire or console RESET starts a new game.
240
+ LDA FIRE_EDG
241
+ BMI .start
242
+ LDA EDGEB
243
+ AND #$01
244
+ BNE .start
245
+ JMP .packtitle
246
+ .start:
247
+ JMP start_game
248
+ .packtitle:
249
+ ; Pack the session BEST into the title's display buffer (the kernel just
250
+ ; streams bytes — all per-frame thinking happens HERE, in VBLANK).
251
+ LDA MOVES_BSV
252
+ JSR pack_two_digits
253
+ LDY #0
254
+ .hst:
255
+ LDA SCRATCH,Y
256
+ STA HSBUF,Y
257
+ INY
258
+ CPY #6
259
+ BNE .hst
260
+ RTS
261
+
262
+ ; ── GAME LOGIC (clay — reshape freely) ── one turn of the puzzle ───────
263
+ ; All input is edge-triggered, so the board only changes on a deliberate
264
+ ; press. The mismatch pause (MISS_T) is the one timed element — it just
265
+ ; holds a wrong pair visible long enough to memorize before hiding it.
266
+ logic_play:
267
+ ; mismatch pause: if running, count it down; when it expires, hide BOTH
268
+ ; revealed tiles and end the turn. Ignore all input meanwhile.
269
+ LDA MISS_T
270
+ BEQ .noMiss
271
+ DEC MISS_T
272
+ BEQ .missEnd ; pause just expired → hide the pair below
273
+ JMP .ppack ; still pausing — show the pair, take no input
274
+ .missEnd:
275
+ LDA #0
276
+ STA REVEAL ; pause over: flip every revealed (unmatched) tile down
277
+ LDA #$FF
278
+ STA FIRST
279
+ JMP .ppack
280
+ .noMiss:
281
+
282
+ ; cursor up/down. SWCHA player-0 directions are active-LOW in the high
283
+ ; nibble: Up=bit4, Down=bit5, Left=bit6, Right=bit7 (verified empirically
284
+ ; against the host). A pressed direction reads 0, so invert to get a
285
+ ; "pressed-now" mask, then AND with last frame's pressed mask's complement
286
+ ; for a clean press-edge (one tap = one step).
287
+ LDA SWCHA
288
+ TAX ; X = raw current levels (active low)
289
+ EOR #$FF ; A = pressed-now (1 = held)
290
+ STA TMP2 ; TMP2 = pressed-now mask
291
+ EOR #$FF ; back to raw...
292
+ AND DPAD_PRV ; (unused path) — keep DPAD_PRV as the prev pressed mask
293
+ ; compute edge = pressed-now AND NOT pressed-last
294
+ LDA DPAD_PRV
295
+ EOR #$FF ; NOT(pressed-last)
296
+ AND TMP2 ; AND pressed-now → newly-pressed this frame
297
+ STA TMP ; TMP = press-edge mask
298
+ LDA TMP2
299
+ STA DPAD_PRV ; store pressed-now as next frame's "pressed-last"
300
+ LDA TMP
301
+ AND #%00010000 ; UP = bit4
302
+ BEQ .noUp
303
+ LDA CURSOR
304
+ BEQ .noUp ; already at top
305
+ DEC CURSOR
306
+ .noUp:
307
+ LDA TMP
308
+ AND #%00100000 ; DOWN = bit5
309
+ BEQ .noDown
310
+ LDA CURSOR
311
+ CMP #(NTILES-1)
312
+ BCS .noDown ; already at bottom
313
+ INC CURSOR
314
+ .noDown:
315
+
316
+ ; FIRE = flip the tile under the cursor (if it's legal to flip).
317
+ LDA FIRE_EDG
318
+ BPL .ppack ; no fire this frame
319
+ ; ignore if this tile is already matched or already revealed
320
+ LDX CURSOR
321
+ JSR tile_bit ; A = mask for CURSOR, X preserved as index
322
+ STA TMP2 ; TMP2 = cursor bit mask
323
+ AND MATCHED
324
+ BNE .ppack ; matched → can't flip
325
+ LDA TMP2
326
+ AND REVEAL
327
+ BNE .ppack ; already face-up → ignore
328
+
329
+ ; reveal this tile
330
+ LDA REVEAL
331
+ ORA TMP2
332
+ STA REVEAL
333
+ ; count the flip (a "move")
334
+ JSR add_move
335
+ ; SFX: a short blip on every flip
336
+ LDA #8
337
+ LDX #4
338
+ LDY #4
339
+ JSR sfx_play
340
+
341
+ ; is this the FIRST or the SECOND tile of the turn?
342
+ LDA FIRST
343
+ BPL .second
344
+ ; first: just remember it
345
+ LDA CURSOR
346
+ STA FIRST
347
+ JMP .ppack
348
+ .second:
349
+ ; second flip — compare values. FIRST holds the other tile's index.
350
+ LDX FIRST
351
+ LDA BOARD,X
352
+ STA TMP ; value of first tile
353
+ LDX CURSOR
354
+ LDA BOARD,X
355
+ CMP TMP
356
+ BNE .miss
357
+ ; MATCH! mark both matched, clear them from REVEAL, bump PAIRS.
358
+ LDA MATCHED
359
+ ORA TMP2 ; cursor's bit
360
+ STA TMP2 ; (reuse TMP2 to accumulate)
361
+ LDX FIRST
362
+ JSR tile_bit
363
+ ORA TMP2
364
+ STA MATCHED
365
+ LDA #0
366
+ STA REVEAL ; both were the only revealed tiles
367
+ LDA #$FF
368
+ STA FIRST
369
+ INC PAIRS
370
+ ; match chime (higher, longer)
371
+ LDA #20
372
+ LDX #12
373
+ LDY #14
374
+ JSR sfx_play
375
+ ; win?
376
+ LDA PAIRS
377
+ CMP #WIN_PAIRS
378
+ BCS .win
379
+ JMP .ppack
380
+ .miss:
381
+ ; mismatch: start the pause; both stay visible until MISS_T expires.
382
+ LDA #MISS_HOLD
383
+ STA MISS_T
384
+ ; low buzz
385
+ LDA #28
386
+ LDX #6
387
+ LDY #10
388
+ JSR sfx_play
389
+ JMP .ppack
390
+ .win:
391
+ ; record best (fewest moves) and go to the win/over state.
392
+ JSR record_best
393
+ JMP do_game_over
394
+
395
+ .ppack:
396
+ ; pack the live move count into the score buffer for the kernel.
397
+ JSR pack_moves
398
+ RTS
399
+
400
+ ; ── GAME LOGIC (clay — reshape freely) ── win / game-over freeze-frame ──
401
+ logic_over:
402
+ LDA EDGEB
403
+ AND #$01
404
+ BNE .toTitle
405
+ LDA FIRE_EDG
406
+ BMI .toTitle
407
+ DEC OVER_T
408
+ BNE .stay
409
+ .toTitle:
410
+ JMP enter_title
411
+ .stay:
412
+ JSR pack_moves
413
+ RTS
414
+
415
+ ; ── GAME LOGIC (clay — reshape freely) ── helpers ──────────────────────
416
+
417
+ ; tile_bit — A = the bit mask (1<<index) for tile index in X. X preserved.
418
+ tile_bit:
419
+ LDA #1
420
+ CPX #0
421
+ BEQ .tbdone
422
+ STX TMP
423
+ .sh:
424
+ ASL
425
+ DEC TMP
426
+ BNE .sh
427
+ .tbdone:
428
+ RTS
429
+
430
+ ; rng_step — 8-bit LFSR (taps 0xB8). Keeps RNG nonzero; cheap entropy for
431
+ ; the shuffle. Called every frame so the seed depends on how long the
432
+ ; player lingered on the title.
433
+ rng_step:
434
+ LDA RNG
435
+ LSR
436
+ BCC .noeor
437
+ EOR #$B8
438
+ .noeor:
439
+ STA RNG
440
+ RTS
441
+
442
+ ; TMP3PLUS1 — a scratch byte holding (i+1), the modulus for the shuffle's
443
+ ; "j = rng mod (i+1)" step (see shuffle_with_bounds below).
444
+ TMP3PLUS1 = SCRATCH+5
445
+
446
+ add_move: ; +1 flip, BCD, capped at 99 (then high byte)
447
+ SED
448
+ CLC
449
+ LDA MOVES
450
+ ADC #1
451
+ STA MOVES
452
+ LDA MOVES_HI
453
+ ADC #0
454
+ STA MOVES_HI
455
+ CLD
456
+ RTS
457
+
458
+ record_best: ; if MOVES < session best (or best is 0), store it
459
+ LDA MOVES_BSV
460
+ ORA MOVES_BSH
461
+ BEQ .store ; best still 0 = unset → first win always records
462
+ LDA MOVES_HI
463
+ CMP MOVES_BSH
464
+ BCC .store
465
+ BNE .rbdone
466
+ LDA MOVES
467
+ CMP MOVES_BSV
468
+ BCS .rbdone ; current >= best → keep old best
469
+ .store:
470
+ LDA MOVES
471
+ STA MOVES_BSV
472
+ LDA MOVES_HI
473
+ STA MOVES_BSH
474
+ .rbdone:
475
+ RTS
476
+
477
+ do_game_over:
478
+ LDA #2
479
+ STA STATE
480
+ LDA #180
481
+ STA OVER_T ; ~3s auto-return to title
482
+ LDA #1
483
+ STA TUNE_SEL ; win tune
484
+ JSR tune_start
485
+ RTS
486
+
487
+ start_game:
488
+ ; reset all per-game state, shuffle a fresh board, enter play.
489
+ LDA #0
490
+ STA MOVES
491
+ STA MOVES_HI
492
+ STA MATCHED
493
+ STA REVEAL
494
+ STA PAIRS
495
+ STA MISS_T
496
+ STA TUNE_LEFT ; silence the title jingle
497
+ LDA #$FF
498
+ STA FIRST
499
+ LDA #0
500
+ STA CURSOR
501
+ JSR shuffle_with_bounds ; fresh shuffled board (two each of 0..3)
502
+ LDA #1
503
+ STA STATE
504
+ JSR pack_moves
505
+ RTS
506
+
507
+ ; shuffle_with_bounds — wrapper that drives shuffle_board's mod bound (i+1)
508
+ ; as i descends. Kept separate so shuffle_board stays readable.
509
+ shuffle_with_bounds:
510
+ ; seed 0,0,1,1,2,2,3,3
511
+ LDX #0
512
+ .seed:
513
+ TXA
514
+ LSR
515
+ STA BOARD,X
516
+ INX
517
+ CPX #NTILES
518
+ BNE .seed
519
+ LDX #(NTILES-1)
520
+ .loop:
521
+ TXA
522
+ CLC
523
+ ADC #1
524
+ STA TMP3PLUS1 ; bound = i+1
525
+ JSR rng_step
526
+ LDA RNG
527
+ AND #$07
528
+ .fold:
529
+ CMP TMP3PLUS1
530
+ BCC .haveJ
531
+ SEC
532
+ SBC TMP3PLUS1
533
+ JMP .fold
534
+ .haveJ:
535
+ TAY ; Y = j
536
+ LDA BOARD,X
537
+ STA TMP2
538
+ LDA BOARD,Y
539
+ STA BOARD,X
540
+ LDA TMP2
541
+ STA BOARD,Y
542
+ DEX
543
+ BNE .loop
544
+ RTS
545
+
546
+ enter_title:
547
+ LDA #0
548
+ STA STATE
549
+ STA SFX_LEFT
550
+ STA TUNE_SEL ; title jingle
551
+ STA MISS_T
552
+ STA REVEAL
553
+ JSR tune_start
554
+ RTS
555
+
556
+ ; digit_times6 — A = digit 0-9 → A = digit*6 (DIGITS row index)
557
+ digit_times6:
558
+ STA TMP
559
+ ASL
560
+ ASL ; *4
561
+ CLC
562
+ ADC TMP ; *5
563
+ CLC
564
+ ADC TMP ; *6
565
+ RTS
566
+
567
+ ; pack_two_digits — render the two BCD digits of A into SCRATCH..SCRATCH+5
568
+ ; (6 font rows), low digit left, high digit right, for the title best line.
569
+ pack_two_digits:
570
+ PHA
571
+ AND #$0F ; low digit
572
+ JSR digit_times6
573
+ TAX
574
+ LDY #0
575
+ .lo:
576
+ LDA DIGITS,X
577
+ STA SCRATCH,Y
578
+ INX
579
+ INY
580
+ CPY #6
581
+ BNE .lo
582
+ PLA
583
+ LSR
584
+ LSR
585
+ LSR
586
+ LSR ; high digit
587
+ JSR digit_times6
588
+ TAX
589
+ LDY #0
590
+ .hi:
591
+ LDA DIGITS,X
592
+ ; merge: high digit occupies the right nibble columns (shift right 4)
593
+ LSR
594
+ LSR
595
+ LSR
596
+ LSR
597
+ ORA SCRATCH,Y
598
+ STA SCRATCH,Y
599
+ INX
600
+ INY
601
+ CPY #6
602
+ BNE .hi
603
+ RTS
604
+
605
+ ; pack_moves — render the low two MOVES digits into S0BUF (the live counter
606
+ ; the play/over kernel streams into the score bar).
607
+ pack_moves:
608
+ LDA MOVES
609
+ JSR pack_two_digits
610
+ LDY #0
611
+ .cp:
612
+ LDA SCRATCH,Y
613
+ STA S0BUF,Y
614
+ INY
615
+ CPY #6
616
+ BNE .cp
617
+ RTS
618
+
619
+ ; ── GAME LOGIC (clay — reshape freely) ── TIA sound ────────────────────
620
+ ; sfx_play — A = AUDF pitch, X = AUDC waveform, Y = frames. Voice 0.
621
+ sfx_play:
622
+ STA AUDF0
623
+ STX AUDC0
624
+ STY SFX_LEFT
625
+ LDA #8
626
+ STA AUDV0
627
+ RTS
628
+
629
+ ; tune_start — begin the jingle selected by TUNE_SEL (0 title, 1 win). V1.
630
+ tune_start:
631
+ LDA #0
632
+ STA TUNE_POS
633
+ JSR tune_note
634
+ RTS
635
+
636
+ ; tune_note — load AUDF1 from the selected table at TUNE_POS; returns Z set
637
+ ; (A=0) on the $FF terminator. Sets the note's duration into TUNE_LEFT.
638
+ tune_note:
639
+ LDX TUNE_POS
640
+ LDA TUNE_SEL
641
+ BEQ .title
642
+ LDA OVER_TUNE,X
643
+ JMP .got
644
+ .title:
645
+ LDA TITLE_TUNE,X
646
+ .got:
647
+ CMP #$FF
648
+ BEQ .end
649
+ STA AUDF1
650
+ LDA #12
651
+ STA AUDC1
652
+ LDA #8
653
+ STA AUDV1
654
+ LDA #16
655
+ STA TUNE_LEFT ; 16 frames per note
656
+ LDA #1 ; Z clear = not terminated
657
+ RTS
658
+ .end:
659
+ LDA #0
660
+ STA AUDV1 ; silence
661
+ STA TUNE_LEFT
662
+ RTS
663
+
664
+ ; audio_tick — once per frame, every state: age the SFX and advance the tune.
665
+ audio_tick:
666
+ LDA SFX_LEFT
667
+ BEQ .nosfx
668
+ DEC SFX_LEFT
669
+ BNE .nosfx
670
+ LDA #0
671
+ STA AUDV0 ; SFX finished → silence voice 0
672
+ .nosfx:
673
+ LDA TUNE_LEFT
674
+ BEQ .notune
675
+ DEC TUNE_LEFT
676
+ BNE .notune
677
+ INC TUNE_POS ; next note
678
+ JSR tune_note
679
+ .notune:
680
+ RTS
681
+
682
+ ; ──────────────────────────────────────────────────────────────────────
683
+ ; ── HARDWARE IDIOM (load-bearing) ──
684
+ ; OBJECT POSITIONING — the canonical SBC-#15 beam-race for P0 (the cursor
685
+ ; bracket). The object lands wherever the beam is when you strobe RESP0;
686
+ ; each SBC/BCS lap is 5 cycles = 15 pixels, and the remainder becomes the
687
+ ; fine HMOVE offset. We park P0 at the left margin so its bracket frames the
688
+ ; board's left edge on the cursor's band.
689
+ ; ──────────────────────────────────────────────────────────────────────
690
+ position_cursor:
691
+ STA WSYNC
692
+ STA HMCLR
693
+ LDA #16 ; cursor bracket X (left margin)
694
+ STA WSYNC
695
+ SEC
696
+ .d0:
697
+ SBC #15
698
+ BCS .d0
699
+ EOR #7
700
+ ASL
701
+ ASL
702
+ ASL
703
+ ASL
704
+ STA RESP0
705
+ STA HMP0
706
+ STA WSYNC
707
+ STA HMOVE
708
+ RTS
709
+
710
+ ; ──────────────────────────────────────────────────────────────────────
711
+ ; ── HARDWARE IDIOM (load-bearing) ──
712
+ ; THE PLAY/GAME-OVER KERNEL — 192 visible lines, fully accounted:
713
+ ; 24 = move-counter bar + 144 = board (8 bands × 18) + 24 = pad = 192
714
+ ;
715
+ ; MOVE BAR (SCORE mode): CTRLPF=$02 colors the left half with COLUP0; we
716
+ ; stream the packed counter digits into PF1, one font row / 4 lines.
717
+ ;
718
+ ; BOARD: 8 tile bands of BANDH lines. Per band we pick the tile's COLOR from
719
+ ; its state — matched (dark), revealed (its value color), or face-down (gray)
720
+ ; — and brighten COLUBK on the cursor's band. The whole band is one lit PF
721
+ ; block (PF0/PF1/PF2 = solid), so each tile reads as a fat horizontal bar.
722
+ ; ──────────────────────────────────────────────────────────────────────
723
+ play_kernel:
724
+ JSR position_cursor
725
+
726
+ LDA #COL_BG
727
+ STA COLUBK
728
+ LDA #0
729
+ STA PF0
730
+ STA PF1
731
+ STA PF2
732
+ STA GRP0
733
+ STA VBLANK ; beam on
734
+ ; SCORE mode colors the playfield halves by COLUP0 (left) / COLUP1 (right),
735
+ ; NOT COLUPF — set both white so the counter digits read on either half.
736
+ LDA #COL_HUD
737
+ STA COLUP0
738
+ STA COLUP1
739
+ STA COLUPF
740
+ LDA #$02
741
+ STA CTRLPF ; SCORE mode for the counter bar
742
+
743
+ ; ---- move-counter bar: 24 lines (6 font rows × 4) ----
744
+ LDX #0
745
+ .sbar:
746
+ STA WSYNC
747
+ TXA
748
+ LSR
749
+ LSR
750
+ TAY
751
+ LDA S0BUF,Y
752
+ STA PF1
753
+ INX
754
+ CPX #24
755
+ BNE .sbar
756
+
757
+ ; transition: clear the bar; switch to a solid full-width playfield for the
758
+ ; tile bands (no reflect needed — each band is a solid bar).
759
+ STA WSYNC
760
+ LDA #0
761
+ STA PF1
762
+ STA CTRLPF ; normal repeat, solid PF
763
+ LDA #$FF
764
+ STA PF0 ; PF0 uses bits 4-7 → solid left 4 px
765
+ STA PF1
766
+ STA PF2 ; all three solid = full 40-px-wide band
767
+
768
+ ; ---- board: NTILES bands, tile 0 at top ----
769
+ LDX #0 ; X = tile index
770
+ .bandLoop:
771
+ ; pick this tile's playfield color into A.
772
+ JSR tile_bit ; A = mask for tile X
773
+ STA TMP2
774
+ AND MATCHED
775
+ BNE .gone
776
+ LDA TMP2
777
+ AND REVEAL
778
+ BNE .revealed
779
+ ; face-down
780
+ LDA #COL_DOWN
781
+ JMP .haveCol
782
+ .gone:
783
+ LDA #COL_GONE
784
+ JMP .haveCol
785
+ .revealed:
786
+ ; color = VAL_COLn for BOARD[X]
787
+ LDA BOARD,X
788
+ TAY
789
+ LDA VALCOLS,Y
790
+ .haveCol:
791
+ STA TMP ; TMP = this tile's lit color
792
+
793
+ ; cursor highlight: the SELECTED band draws its separator gap as a bright
794
+ ; white bar (an unmistakable underline); other bands' gaps are black.
795
+ LDA #COL_BG
796
+ CPX CURSOR
797
+ BNE .noCur
798
+ LDA #COL_CUR
799
+ .noCur:
800
+ STA TMP2 ; TMP2 = this band's GAP color
801
+
802
+ ; ---- lit tile: BANDH-BANDGAP lines in the tile color ----
803
+ LDA TMP
804
+ STA COLUPF
805
+ LDA #$FF
806
+ STA PF0
807
+ STA PF1
808
+ STA PF2 ; ensure solid (the gap below clears it)
809
+ LDY #(BANDH-BANDGAP)
810
+ .tileLine:
811
+ STA WSYNC
812
+ DEY
813
+ BNE .tileLine
814
+
815
+ ; ---- separator gap: BANDGAP lines. PF colored by TMP2 (white on the
816
+ ; cursor band, black otherwise) so the selection reads as a bar. ----
817
+ LDA TMP2
818
+ STA COLUPF
819
+ LDY #BANDGAP
820
+ .gapLine:
821
+ STA WSYNC
822
+ DEY
823
+ BNE .gapLine
824
+
825
+ INX
826
+ CPX #NTILES
827
+ BNE .bandLoop
828
+
829
+ ; pad to 192 visible (24 bar + 144 board = 168 → +24 pad)
830
+ LDA #0
831
+ STA PF0
832
+ STA PF1
833
+ STA PF2
834
+ STA COLUBK
835
+ STA GRP0
836
+ LDX #24
837
+ .pad:
838
+ STA WSYNC
839
+ DEX
840
+ BNE .pad
841
+
842
+ JMP kernel_done
843
+
844
+ ; ──────────────────────────────────────────────────────────────────────
845
+ ; ── HARDWARE IDIOM (load-bearing) ──
846
+ ; THE TITLE KERNEL — 192 lines, banded:
847
+ ; 24 blank + 28 banner "TILE" + 8 gap + 28 banner "TWINS" + 16 gap +
848
+ ; 24 best + remainder pad = 192. The banner is an ASYMMETRIC PLAYFIELD,
849
+ ; the 2600's only way to draw full-width artwork: PF0/PF1/PF2 are reloaded
850
+ ; mid-line so the left copy (px 0..19) and right copy (px 20..39) carry
851
+ ; independent pixels. CTRLPF bit0 = 0 (repeat) is required.
852
+ ; ──────────────────────────────────────────────────────────────────────
853
+ title_kernel:
854
+ LDA #$84
855
+ STA COLUBK
856
+ LDA #0
857
+ STA PF0
858
+ STA PF1
859
+ STA PF2
860
+ STA GRP0
861
+ STA CTRLPF ; REPEAT mode — required by the banner
862
+ STA VBLANK
863
+
864
+ LDX #24
865
+ .tb1:
866
+ STA WSYNC
867
+ DEX
868
+ BNE .tb1
869
+
870
+ LDA #$3A ; word 1 in warm yellow
871
+ STA COLUPF
872
+ LDX #0
873
+ .ban1:
874
+ STA WSYNC
875
+ TXA
876
+ LSR
877
+ LSR
878
+ TAY
879
+ LDA R1_PF0L,Y
880
+ STA PF0
881
+ LDA R1_PF1L,Y
882
+ STA PF1
883
+ LDA R1_PF2L,Y
884
+ STA PF2
885
+ LDA R1_PF0R,Y
886
+ STA PF0
887
+ LDA R1_PF1R,Y
888
+ STA PF1
889
+ NOP
890
+ NOP
891
+ LDA R1_PF2R,Y
892
+ STA PF2
893
+ INX
894
+ CPX #28
895
+ BNE .ban1
896
+
897
+ STA WSYNC
898
+ LDA #0
899
+ STA PF0
900
+ STA PF1
901
+ STA PF2
902
+ LDX #7
903
+ .tb3:
904
+ STA WSYNC
905
+ DEX
906
+ BNE .tb3
907
+
908
+ LDA #$C8 ; word 2 in green
909
+ STA COLUPF
910
+ LDX #0
911
+ .ban2:
912
+ STA WSYNC
913
+ TXA
914
+ LSR
915
+ LSR
916
+ TAY
917
+ LDA R2_PF0L,Y
918
+ STA PF0
919
+ LDA R2_PF1L,Y
920
+ STA PF1
921
+ LDA R2_PF2L,Y
922
+ STA PF2
923
+ LDA R2_PF0R,Y
924
+ STA PF0
925
+ LDA R2_PF1R,Y
926
+ STA PF1
927
+ NOP
928
+ NOP
929
+ LDA R2_PF2R,Y
930
+ STA PF2
931
+ INX
932
+ CPX #28
933
+ BNE .ban2
934
+
935
+ STA WSYNC
936
+ LDA #0
937
+ STA PF0
938
+ STA PF1
939
+ STA PF2
940
+ LDA #$02
941
+ STA CTRLPF ; SCORE mode for the best band
942
+ LDX #15
943
+ .tb5:
944
+ STA WSYNC
945
+ DEX
946
+ BNE .tb5
947
+
948
+ ; ---- best line: 24 lines, the session best (fewest moves) ----
949
+ LDA #COL_HUD
950
+ STA COLUPF
951
+ LDX #0
952
+ .best:
953
+ STA WSYNC
954
+ TXA
955
+ LSR
956
+ LSR
957
+ TAY
958
+ LDA HSBUF,Y
959
+ STA PF1
960
+ INX
961
+ CPX #24
962
+ BNE .best
963
+
964
+ STA WSYNC
965
+ LDA #0
966
+ STA PF1
967
+ ; pad to 192 (24+28+8+28+16+24 = 128 → +64 pad)
968
+ LDX #64
969
+ .tpad:
970
+ STA WSYNC
971
+ DEX
972
+ BNE .tpad
973
+
974
+ JMP kernel_done
975
+
976
+ ; ──────────────────────────────────────────────────────────────────────
977
+ ; ── GAME LOGIC (clay — reshape freely) ── data tables ──────────────────
978
+ ; ──────────────────────────────────────────────────────────────────────
979
+
980
+ ; the four VALUE colors, indexed by BOARD[i] (0..3).
981
+ VALCOLS:
982
+ .byte VAL_COL0, VAL_COL1, VAL_COL2, VAL_COL3
983
+
984
+ ; DIGITS — 6 rows/glyph, 0..9. Each byte's high nibble (bits 4-7) is the lit
985
+ ; pattern; SCORE mode streams it through PF1 so a digit is 4 px wide.
986
+ DIGITS:
987
+ .byte %01100000,%10010000,%10010000,%10010000,%10010000,%01100000 ; 0
988
+ .byte %00100000,%01100000,%00100000,%00100000,%00100000,%01110000 ; 1
989
+ .byte %11100000,%00010000,%01100000,%10000000,%10000000,%11110000 ; 2
990
+ .byte %11100000,%00010000,%01100000,%00010000,%10010000,%01100000 ; 3
991
+ .byte %10010000,%10010000,%11110000,%00010000,%00010000,%00010000 ; 4
992
+ .byte %11110000,%10000000,%11100000,%00010000,%10010000,%01100000 ; 5
993
+ .byte %01100000,%10000000,%11100000,%10010000,%10010000,%01100000 ; 6
994
+ .byte %11110000,%00010000,%00100000,%01000000,%01000000,%01000000 ; 7
995
+ .byte %01100000,%10010000,%01100000,%10010000,%10010000,%01100000 ; 8
996
+ .byte %01100000,%10010000,%10010000,%01110000,%00010000,%01100000 ; 9
997
+
998
+ ; jingles — AUDF1 pitches, $FF terminates. (12 = pure tone waveform.)
999
+ TITLE_TUNE:
1000
+ .byte 20, 16, 12, 16, 20, 24, 20, $FF
1001
+ OVER_TUNE:
1002
+ .byte 12, 12, 16, 20, 24, 28, $FF
1003
+
1004
+ ; ── THE TITLE BANNER ──────────────────────────────────────────────────
1005
+ ; 40-px artwork, 7 rows/word, drawn by the asymmetric-playfield kernel.
1006
+ ; PF bit order is the 2600's prank — three registers, three orders:
1007
+ ; PF0: bits 4-7 used, bit4 = LEFTMOST PF1: bit7 = leftmost (normal)
1008
+ ; PF2: bit0 = leftmost. Tables generated from the ASCII art below.
1009
+ ;
1010
+ ; TILE (T-I-L-E, all in the left copy; +6px left pad to centre the word):
1011
+ ; #### ###. #... ####
1012
+ ; ..#. .#.. #... #...
1013
+ ; ..#. .#.. #... #...
1014
+ ; ..#. .#.. #... ###.
1015
+ ; ..#. .#.. #... #...
1016
+ ; ..#. .#.. #... #...
1017
+ ; ..#. ###. #### ####
1018
+ R1_PF0L:
1019
+ .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
1020
+ R1_PF1L:
1021
+ .byte %00111101, %00001000, %00001000, %00001000, %00001000, %00001000, %00001001
1022
+ R1_PF2L:
1023
+ .byte %00010011, %00010001, %00010001, %00010001, %00010001, %00010001, %11110011
1024
+ R1_PF0R:
1025
+ .byte %11100000, %00100000, %00100000, %11100000, %00100000, %00100000, %11100000
1026
+ R1_PF1R:
1027
+ .byte %10000000, %00000000, %00000000, %00000000, %00000000, %00000000, %10000000
1028
+ R1_PF2R:
1029
+ .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
1030
+
1031
+ ; TWINS (T-W-I-N in the left copy, S in the right):
1032
+ ; #### #..# ###. #..# .###
1033
+ ; ..#. #..# .#.. ##.# #...
1034
+ ; ..#. #..# .#.. ##.# #...
1035
+ ; ..#. #..# .#.. #.## ###.
1036
+ ; ..#. #.## .#.. #.## ...#
1037
+ ; ..#. ##.# .#.. #..# ...#
1038
+ ; ..#. #..# ###. #..# ###.
1039
+ R2_PF0L:
1040
+ .byte %11000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
1041
+ R2_PF1L:
1042
+ .byte %11010010, %10010010, %10010010, %10010010, %10010110, %10011010, %10010010
1043
+ R2_PF2L:
1044
+ .byte %00100111, %01100010, %01100010, %10100010, %10100010, %00100010, %00100111
1045
+ R2_PF0R:
1046
+ .byte %10010000, %01010000, %01010000, %11010000, %00010000, %00010000, %11010000
1047
+ R2_PF1R:
1048
+ .byte %11000000, %00000000, %00000000, %10000000, %01000000, %01000000, %10000000
1049
+ R2_PF2R:
1050
+ .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
1051
+
1052
+ ; ── Vector table ──────────────────────────────────────────────────────
1053
+ org $FFFA
1054
+ .word START
1055
+ .word START
1056
+ .word START