romdevtools 0.28.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +51 -41
- package/CHANGELOG.md +46 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +1 -1
- package/src/host/LibretroHost.js +59 -1
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +4 -3
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- 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
|