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.
- package/AGENTS.md +53 -43
- package/CHANGELOG.md +91 -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 +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +84 -8
- 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/memory.js +131 -24
- 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/record.js +6 -7
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +12 -4
- 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 +53 -10
- 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/_guides/ROMHACKING_PLAYBOOK.md +32 -3
- 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
|
@@ -1,36 +1,68 @@
|
|
|
1
|
-
; ── sports.asm — Atari 2600
|
|
1
|
+
; ── sports.asm — RAPID RALLY — Atari 2600 sports (complete example game) ─────
|
|
2
2
|
;
|
|
3
|
-
;
|
|
4
|
-
;
|
|
5
|
-
;
|
|
6
|
-
;
|
|
3
|
+
; A COMPLETE, working game — title screen, 1P (vs AI) and 2P head-to-head
|
|
4
|
+
; modes, scoring to 7, in-session hi-score (best rally), TIA sound effects +
|
|
5
|
+
; a title jingle, and the 2600's signature feature: THE WHOLE MACHINE.
|
|
6
|
+
; There is no framebuffer, no tilemap, no OS — every visible scanline below
|
|
7
|
+
; is composed live by racing the beam, and this file teaches the four
|
|
8
|
+
; classic per-line kernel tricks while doing it:
|
|
7
9
|
;
|
|
8
|
-
;
|
|
9
|
-
;
|
|
10
|
-
;
|
|
10
|
+
; 1. ASYMMETRIC PLAYFIELD (the title banner) — the playfield registers
|
|
11
|
+
; cover only HALF the screen; rewriting PF0/PF1/PF2 mid-scanline,
|
|
12
|
+
; inside strict cycle windows, paints 40 independent pixels per line.
|
|
13
|
+
; 2. SCORE MODE + MID-LINE PF1 REWRITE (the score bar) — CTRLPF bit 1
|
|
14
|
+
; colors the left playfield half with COLUP0 and the right half with
|
|
15
|
+
; COLUP1 for free; a second PF1 write mid-line puts a DIFFERENT digit
|
|
16
|
+
; on each side. Two-color scoreboard, zero sprites used.
|
|
17
|
+
; 3. THE TWO-LINE KERNEL (the court) — a full line of render work does
|
|
18
|
+
; not fit in one 76-cycle scanline, so each loop pass paints TWO.
|
|
19
|
+
; 4. TIM64T/INTIM FRAME TIMING — instead of hand-counting every VBLANK
|
|
20
|
+
; scanline (and rolling the picture when game logic grows), set the
|
|
21
|
+
; RIOT timer and let it absorb whatever the logic costs.
|
|
11
22
|
;
|
|
12
|
-
;
|
|
13
|
-
;
|
|
14
|
-
;
|
|
15
|
-
;
|
|
16
|
-
;
|
|
23
|
+
; THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
24
|
+
; very different one. The markers tell you what's what:
|
|
25
|
+
; HARDWARE IDIOM (load-bearing) — cycle-counted / footgun-dodging code;
|
|
26
|
+
; reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
27
|
+
; GAME LOGIC (clay) — ball physics, AI, scoring, tuning, art: reshape freely.
|
|
17
28
|
;
|
|
18
|
-
;
|
|
19
|
-
;
|
|
20
|
-
;
|
|
21
|
-
;
|
|
22
|
-
;
|
|
29
|
+
; GAME_TITLE: on the 2600 a title is DRAWN, not printed — there is no font
|
|
30
|
+
; hardware. The RAPID/RALLY banner bitmaps near the bottom of this file ARE
|
|
31
|
+
; the title; redraw them for your game (the comment above each table shows
|
|
32
|
+
; the 40-pixel artwork and the PF0/PF1/PF2 bit-order encoding).
|
|
33
|
+
;
|
|
34
|
+
; CONTROLS (documented for players and for the fork README):
|
|
35
|
+
; Title: fire on JOYSTICK 0 starts 1P (vs AI)
|
|
36
|
+
; fire on JOYSTICK 1 starts 2P (both paddles human)
|
|
37
|
+
; console SELECT toggles the 1P/2P digit; console RESET starts
|
|
38
|
+
; the selected mode
|
|
39
|
+
; Play: joystick up/down moves your paddle (P0 = left, P1 = right);
|
|
40
|
+
; console RESET returns to the title
|
|
41
|
+
; First to 7 points wins. Your BEST RALLY (consecutive paddle hits in one
|
|
42
|
+
; volley) is the hi-score shown on the title screen.
|
|
43
|
+
;
|
|
44
|
+
; HI-SCORE HONESTY: real 2600 cartridges had NO battery, NO SRAM, NO
|
|
45
|
+
; persistence of any kind. The hi-score here lives in RIOT RAM ($8B) and
|
|
46
|
+
; survives game → title cycles only WITHIN one power-on session — exactly
|
|
47
|
+
; like the arcade machines of the era. Power off and it is gone. Do not
|
|
48
|
+
; fake an EEPROM; state it honestly in your fork too.
|
|
49
|
+
;
|
|
50
|
+
; NTSC frame: 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan = 262 lines.
|
|
23
51
|
|
|
24
52
|
processor 6502
|
|
25
53
|
org $F000
|
|
26
54
|
|
|
55
|
+
; ── TIA write registers ───────────────────────────────────────────────
|
|
27
56
|
VSYNC = $00
|
|
28
57
|
VBLANK = $01
|
|
29
58
|
WSYNC = $02
|
|
59
|
+
NUSIZ0 = $04
|
|
60
|
+
NUSIZ1 = $05
|
|
30
61
|
COLUP0 = $06
|
|
31
62
|
COLUP1 = $07
|
|
32
63
|
COLUPF = $08
|
|
33
64
|
COLUBK = $09
|
|
65
|
+
CTRLPF = $0A
|
|
34
66
|
PF0 = $0D
|
|
35
67
|
PF1 = $0E
|
|
36
68
|
PF2 = $0F
|
|
@@ -39,27 +71,74 @@ RESP1 = $11
|
|
|
39
71
|
RESBL = $14
|
|
40
72
|
GRP0 = $1B
|
|
41
73
|
GRP1 = $1C
|
|
74
|
+
ENAM0 = $1D
|
|
75
|
+
ENAM1 = $1E
|
|
42
76
|
ENABL = $1F
|
|
43
77
|
HMP0 = $20
|
|
44
78
|
HMP1 = $21
|
|
45
79
|
HMBL = $24
|
|
46
80
|
HMOVE = $2A
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
; ── TIA audio
|
|
81
|
+
HMCLR = $2B
|
|
82
|
+
CXCLR = $2C
|
|
83
|
+
; ── TIA audio ─────────────────────────────────────────────────────────
|
|
50
84
|
AUDC0 = $15
|
|
85
|
+
AUDC1 = $16
|
|
51
86
|
AUDF0 = $17
|
|
87
|
+
AUDF1 = $18
|
|
52
88
|
AUDV0 = $19
|
|
89
|
+
AUDV1 = $1A
|
|
90
|
+
; ── TIA READ registers (separate read map — the same addresses as some
|
|
91
|
+
; write strobes; e.g. CXP0FB reads $02 while STA $02 strobes WSYNC) ────
|
|
92
|
+
CXP0FB = $02 ; bit6 = player 0 / ball collision (latched)
|
|
93
|
+
CXP1FB = $03 ; bit6 = player 1 / ball collision (latched)
|
|
94
|
+
INPT4 = $0C ; joystick 0 fire (bit7, ACTIVE LOW)
|
|
95
|
+
INPT5 = $0D ; joystick 1 fire (bit7, ACTIVE LOW)
|
|
96
|
+
; ── RIOT ──────────────────────────────────────────────────────────────
|
|
97
|
+
SWCHA = $280 ; joysticks: P0 = high nibble, P1 = LOW nibble
|
|
98
|
+
SWCHB = $282 ; console: bit0 RESET, bit1 SELECT (ACTIVE LOW)
|
|
99
|
+
INTIM = $284 ; timer read
|
|
100
|
+
TIM64T = $296 ; timer set, 64-cycle ticks
|
|
53
101
|
|
|
54
|
-
; ── Zero-page state
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
102
|
+
; ── Zero-page state (the 2600's ENTIRE RAM is $80-$FF — 128 bytes; in
|
|
103
|
+
; core memory dumps system_ram offset 0 = $80) ────────────────────────
|
|
104
|
+
STATE = $80 ; 0 = title, 1 = play, 2 = game over
|
|
105
|
+
MODE2P = $81 ; 0 = 1P vs AI, 1 = 2P head-to-head
|
|
106
|
+
P0_Y = $82 ; left paddle BOTTOM scanline (court Y, larger = higher)
|
|
107
|
+
P1_Y = $83 ; right paddle bottom scanline
|
|
108
|
+
BALL_X = $84 ; ball column 0..159 (kept in 4..150 — see PosBall)
|
|
109
|
+
BALL_Y = $85 ; ball bottom scanline (200 = parked off-court/hidden)
|
|
110
|
+
BALL_DX = $86 ; +2 or -2 (signed)
|
|
111
|
+
BALL_DY = $87 ; +1 or -1 (signed)
|
|
112
|
+
SCORE0 = $88 ; left player points (0..7)
|
|
113
|
+
SCORE1 = $89 ; right player points (0..7)
|
|
114
|
+
RALLY = $8A ; current volley's paddle hits — BCD, so the digit
|
|
115
|
+
; nibbles fall out for free in the score kernel
|
|
116
|
+
HISCORE = $8B ; best rally this SESSION (BCD). RAM only — real
|
|
117
|
+
; 2600 carts have no battery; honest by design.
|
|
118
|
+
FRAME = $8C
|
|
119
|
+
SFX_LEFT = $8D ; frames remaining on the voice-0 sound effect
|
|
120
|
+
TUNE_SEL = $8E ; 0 = title jingle, 1 = game-over tune (voice 1)
|
|
121
|
+
TUNE_POS = $8F
|
|
122
|
+
TUNE_LEFT = $90 ; frames left on current jingle note (0 = silent)
|
|
123
|
+
SERVE_T = $91 ; serve pause countdown (ball hidden while > 0)
|
|
124
|
+
OVER_T = $92 ; game-over auto-return-to-title countdown
|
|
125
|
+
WINNER = $93 ; 0 = left player won, 1 = right
|
|
126
|
+
SWCHB_PRV = $94 ; previous SWCHB for RESET/SELECT edge detect
|
|
127
|
+
FIRE_PRV = $95 ; previous fire bits (bit7 = joy0, bit6 = joy1)
|
|
128
|
+
EDGEB = $96 ; this frame's switch press-edges (bit0/bit1)
|
|
129
|
+
FIRE_EDG = $97 ; this frame's fire press-edges (bit7/bit6)
|
|
130
|
+
TMP = $98
|
|
131
|
+
COURTBK = $99 ; court background color (game-over flashes it)
|
|
132
|
+
S0BUF = $9A ; 5 rows: left score digit, PF1 high nibble
|
|
133
|
+
S1BUF = $9F ; 5 rows: right score digit
|
|
134
|
+
HSBUF = $A4 ; 5 rows: hi-score, BOTH digits packed in one byte
|
|
135
|
+
INDBUF = $A9 ; 5 rows: title mode digit (1 or 2)
|
|
136
|
+
|
|
137
|
+
PADGFX = %00111100 ; paddle: 4-px-wide bar (GRP bits, 8px sprite)
|
|
138
|
+
PADH = 14 ; paddle height in scanlines
|
|
139
|
+
COL_COURT = $C4 ; court green
|
|
140
|
+
COL_P0 = $9A ; left player blue (paddle + their score digits)
|
|
141
|
+
COL_P1 = $44 ; right player red
|
|
63
142
|
|
|
64
143
|
START:
|
|
65
144
|
SEI
|
|
@@ -68,280 +147,868 @@ START:
|
|
|
68
147
|
TXS
|
|
69
148
|
LDA #0
|
|
70
149
|
.clr:
|
|
71
|
-
STA $00,X
|
|
72
|
-
DEX
|
|
73
|
-
BNE .clr
|
|
150
|
+
STA $00,X ; clears ALL of $00-$FF: zero page RAM AND the TIA
|
|
151
|
+
DEX ; write registers (GRP/ENAxx/HMxx/audio all silenced
|
|
152
|
+
BNE .clr ; — the standard 2600 power-on hygiene)
|
|
74
153
|
|
|
75
|
-
|
|
154
|
+
; Fixed identity colors — used by the score bar (SCORE mode), the court
|
|
155
|
+
; paddles, and the title hi-score band alike.
|
|
156
|
+
LDA #COL_P0
|
|
157
|
+
STA COLUP0
|
|
158
|
+
LDA #COL_P1
|
|
159
|
+
STA COLUP1
|
|
160
|
+
LDA #80
|
|
76
161
|
STA P0_Y
|
|
77
162
|
STA P1_Y
|
|
78
|
-
LDA #
|
|
163
|
+
LDA #78
|
|
79
164
|
STA BALL_X
|
|
80
|
-
LDA #
|
|
81
|
-
STA BALL_Y
|
|
82
|
-
|
|
83
|
-
STA BALL_DX
|
|
84
|
-
STA BALL_DY
|
|
85
|
-
|
|
86
|
-
; Boot chime — confirms TIA audio is wired.
|
|
87
|
-
LDA #$04
|
|
88
|
-
STA AUDC0
|
|
89
|
-
LDA #$0C
|
|
90
|
-
STA AUDF0
|
|
91
|
-
LDA #$0F
|
|
92
|
-
STA AUDV0
|
|
93
|
-
LDA #20
|
|
94
|
-
STA SFX_LEFT
|
|
95
|
-
|
|
96
|
-
LDA #$80 ; blue background
|
|
97
|
-
STA COLUBK
|
|
98
|
-
LDA #$0F ; white paddles
|
|
99
|
-
STA COLUP0
|
|
100
|
-
STA COLUP1
|
|
101
|
-
LDA #$48 ; cyan playfield
|
|
102
|
-
STA COLUPF
|
|
103
|
-
LDA #$05 ; PF symmetric reflect + ball priority
|
|
104
|
-
STA CTRLPF
|
|
165
|
+
LDA #200
|
|
166
|
+
STA BALL_Y ; ball parked off-court until a game starts
|
|
167
|
+
JSR enter_title
|
|
105
168
|
|
|
169
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
170
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
171
|
+
; THE FRAME LOOP. 262 scanlines, every frame, forever. VBLANK and overscan
|
|
172
|
+
; are timed with the RIOT timer (TIM64T) instead of counted WSYNCs: set the
|
|
173
|
+
; timer, run however much game logic the state needs, then spin on INTIM.
|
|
174
|
+
; This is how shipped 2600 games did it, and it kills the classic homebrew
|
|
175
|
+
; bug class where adding one branch to the game logic emits a 263rd line
|
|
176
|
+
; and the TV loses vsync (rolling picture). The VISIBLE 192 lines are still
|
|
177
|
+
; counted exactly — every STA WSYNC below is one scanline, and each state's
|
|
178
|
+
; kernel accounts for all 192.
|
|
179
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
106
180
|
MAIN:
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
; ── VSYNC ─────────────────────────────────────────────────────────
|
|
181
|
+
; VSYNC: 3 lines
|
|
110
182
|
LDA #2
|
|
183
|
+
STA VBLANK
|
|
111
184
|
STA VSYNC
|
|
112
185
|
STA WSYNC
|
|
113
186
|
STA WSYNC
|
|
114
187
|
STA WSYNC
|
|
115
188
|
LDA #0
|
|
116
189
|
STA VSYNC
|
|
190
|
+
; 37 lines of VBLANK = 2812 cycles ≈ 43 × 64-cycle timer ticks.
|
|
191
|
+
LDA #43
|
|
192
|
+
STA TIM64T
|
|
193
|
+
|
|
194
|
+
JSR frame_logic ; all game thinking happens in the blanked region
|
|
195
|
+
|
|
196
|
+
; burn whatever VBLANK time the logic didn't use
|
|
197
|
+
.vbwait:
|
|
198
|
+
LDA INTIM
|
|
199
|
+
BNE .vbwait
|
|
200
|
+
STA WSYNC
|
|
201
|
+
|
|
202
|
+
; kernel dispatch — title has its own kernel; play and game-over share one
|
|
203
|
+
LDA STATE
|
|
204
|
+
BNE .ingame
|
|
205
|
+
JMP title_kernel
|
|
206
|
+
.ingame:
|
|
207
|
+
JMP play_kernel
|
|
117
208
|
|
|
118
|
-
|
|
119
|
-
;
|
|
120
|
-
; HMOVE, ball, ball-HMOVE) = 37 VBLANK lines total. (Bug fix history:
|
|
121
|
-
; this loop used to be 37 AND the positioning added more → >262
|
|
122
|
-
; scanlines/frame → the TV/emulator can't lock vsync → rolling /
|
|
123
|
-
; black picture. Exactly 262 lines = 3 VSYNC + 37 VBLANK + 192 visible
|
|
124
|
-
; + 30 overscan; every positioning WSYNC MUST be counted against the 37.)
|
|
209
|
+
kernel_done:
|
|
210
|
+
; overscan: 30 lines, timer-paced like VBLANK
|
|
125
211
|
LDA #2
|
|
126
212
|
STA VBLANK
|
|
127
|
-
|
|
128
|
-
|
|
213
|
+
LDA #35
|
|
214
|
+
STA TIM64T
|
|
215
|
+
.oswait:
|
|
216
|
+
LDA INTIM
|
|
217
|
+
BNE .oswait
|
|
129
218
|
STA WSYNC
|
|
130
|
-
|
|
131
|
-
BNE .vb
|
|
219
|
+
JMP MAIN
|
|
132
220
|
|
|
133
|
-
|
|
134
|
-
|
|
221
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
222
|
+
; Per-frame logic, dispatched by state. Runs entirely inside the timed
|
|
223
|
+
; VBLANK window (~2800 cycles — an eternity next to the kernel's 76/line).
|
|
224
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
225
|
+
frame_logic:
|
|
226
|
+
INC FRAME
|
|
227
|
+
JSR audio_tick
|
|
228
|
+
|
|
229
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
230
|
+
; Console switches + fire buttons are ACTIVE LOW and not debounced; a held
|
|
231
|
+
; RESET would restart every frame. Convert to press-EDGES once per frame:
|
|
232
|
+
; edge = was-released-last-frame AND pressed-now.
|
|
233
|
+
LDA SWCHB
|
|
234
|
+
TAX ; X = current switch levels
|
|
235
|
+
EOR #$FF ; A = pressed-now mask (1 = held)
|
|
236
|
+
AND SWCHB_PRV ; ...that were RELEASED (1) last frame
|
|
237
|
+
STA EDGEB ; bit0 = RESET edge, bit1 = SELECT edge
|
|
238
|
+
STX SWCHB_PRV
|
|
239
|
+
; fire buttons → same edge treatment, packed bit7 = joy0, bit6 = joy1
|
|
240
|
+
LDA #0
|
|
241
|
+
BIT INPT4
|
|
242
|
+
BMI .f0up ; bit7 set = not pressed (active low)
|
|
243
|
+
ORA #$80
|
|
244
|
+
.f0up:
|
|
245
|
+
BIT INPT5
|
|
246
|
+
BMI .f1up
|
|
247
|
+
ORA #$40
|
|
248
|
+
.f1up:
|
|
249
|
+
TAY ; Y = pressed-now bits
|
|
250
|
+
LDA FIRE_PRV
|
|
251
|
+
EOR #$FF
|
|
252
|
+
STA TMP ; released-last-frame mask
|
|
253
|
+
TYA
|
|
254
|
+
AND TMP
|
|
255
|
+
STA FIRE_EDG ; bit7 = joy0 fire edge, bit6 = joy1 fire edge
|
|
256
|
+
STY FIRE_PRV
|
|
257
|
+
|
|
258
|
+
LDA STATE
|
|
259
|
+
BEQ logic_title
|
|
260
|
+
CMP #1
|
|
261
|
+
BEQ logic_play_jmp
|
|
262
|
+
JMP logic_over
|
|
263
|
+
logic_play_jmp:
|
|
264
|
+
JMP logic_play
|
|
265
|
+
|
|
266
|
+
; ── GAME LOGIC (clay — reshape freely) ── title-screen behavior ────────
|
|
267
|
+
logic_title:
|
|
268
|
+
; SELECT toggles the mode digit; fire 0 = start 1P; fire 1 = start 2P;
|
|
269
|
+
; RESET starts whatever the digit shows.
|
|
270
|
+
LDA EDGEB
|
|
271
|
+
AND #$02
|
|
272
|
+
BEQ .nosel
|
|
273
|
+
LDA MODE2P
|
|
274
|
+
EOR #$01
|
|
275
|
+
STA MODE2P
|
|
276
|
+
LDA #$0E ; tick sfx on toggle
|
|
277
|
+
LDX #$04
|
|
278
|
+
LDY #4
|
|
279
|
+
JSR sfx_play
|
|
280
|
+
.nosel:
|
|
281
|
+
LDA FIRE_EDG
|
|
282
|
+
BPL .nof0 ; bit7 clear = no joy0 fire edge
|
|
283
|
+
LDA #0
|
|
284
|
+
STA MODE2P
|
|
285
|
+
JMP start_game
|
|
286
|
+
.nof0:
|
|
287
|
+
LDA FIRE_EDG
|
|
288
|
+
AND #$40
|
|
289
|
+
BEQ .nof1
|
|
290
|
+
LDA #1
|
|
291
|
+
STA MODE2P
|
|
292
|
+
JMP start_game
|
|
293
|
+
.nof1:
|
|
294
|
+
LDA EDGEB
|
|
135
295
|
AND #$01
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
; the
|
|
296
|
+
BEQ .nores
|
|
297
|
+
JMP start_game
|
|
298
|
+
.nores:
|
|
299
|
+
|
|
300
|
+
; Pack the title's two display buffers (the kernels just stream bytes —
|
|
301
|
+
; all per-frame thinking happens HERE, in VBLANK, never inside a kernel):
|
|
302
|
+
; HSBUF — hi-score, tens+ones nibbles packed into ONE PF1 byte/row.
|
|
303
|
+
; INDBUF — the mode digit (1 or 2), blinking.
|
|
304
|
+
LDA HISCORE
|
|
305
|
+
LSR
|
|
306
|
+
LSR
|
|
307
|
+
LSR
|
|
308
|
+
LSR ; tens digit (BCD → no divide needed: the nibble IS it)
|
|
309
|
+
JSR digit_times5
|
|
310
|
+
TAX
|
|
311
|
+
LDY #0
|
|
312
|
+
.hst:
|
|
313
|
+
LDA DIGITS,X
|
|
314
|
+
STA HSBUF,Y
|
|
315
|
+
INX
|
|
316
|
+
INY
|
|
317
|
+
CPY #5
|
|
318
|
+
BNE .hst
|
|
319
|
+
LDA HISCORE
|
|
320
|
+
AND #$0F ; ones digit
|
|
321
|
+
JSR digit_times5
|
|
322
|
+
TAX
|
|
323
|
+
LDY #0
|
|
324
|
+
.hso:
|
|
325
|
+
LDA DIGITS,X
|
|
326
|
+
LSR
|
|
327
|
+
LSR
|
|
328
|
+
LSR
|
|
329
|
+
LSR ; ones go in the LOW nibble (PF1 bit7 = leftmost,
|
|
330
|
+
ORA HSBUF,Y ; so the HIGH nibble is the LEFT digit = tens)
|
|
331
|
+
STA HSBUF,Y
|
|
332
|
+
INX
|
|
333
|
+
INY
|
|
334
|
+
CPY #5
|
|
335
|
+
BNE .hso
|
|
336
|
+
|
|
337
|
+
; mode digit: 1 or 2, blinked by FRAME bit 5 (~1 Hz)
|
|
338
|
+
LDA MODE2P
|
|
339
|
+
CLC
|
|
340
|
+
ADC #1
|
|
341
|
+
JSR digit_times5
|
|
342
|
+
TAX
|
|
343
|
+
LDY #0
|
|
344
|
+
.ind:
|
|
345
|
+
LDA FRAME
|
|
346
|
+
AND #$20
|
|
347
|
+
BEQ .indblank
|
|
348
|
+
LDA DIGITS,X
|
|
349
|
+
JMP .indstore
|
|
350
|
+
.indblank:
|
|
351
|
+
LDA #0
|
|
352
|
+
.indstore:
|
|
353
|
+
STA INDBUF,Y
|
|
354
|
+
INX
|
|
355
|
+
INY
|
|
356
|
+
CPY #5
|
|
357
|
+
BNE .ind
|
|
358
|
+
|
|
359
|
+
; title shows no moving objects
|
|
360
|
+
LDA #0
|
|
361
|
+
STA GRP0
|
|
362
|
+
STA GRP1
|
|
363
|
+
STA ENABL
|
|
364
|
+
RTS
|
|
365
|
+
|
|
366
|
+
; ── GAME LOGIC (clay — reshape freely) ── one frame of pong ────────────
|
|
367
|
+
logic_play:
|
|
368
|
+
LDA EDGEB
|
|
369
|
+
AND #$01 ; console RESET → back to title
|
|
370
|
+
BEQ .noquit
|
|
371
|
+
JMP enter_title
|
|
372
|
+
.noquit:
|
|
373
|
+
|
|
374
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
375
|
+
; SWCHA is ACTIVE LOW (0 = pressed) and must be RE-LOADED for every
|
|
376
|
+
; direction check. The classic bug: caching it in A and chaining ASLs,
|
|
377
|
+
; then clobbering A with game state between shifts — "up works once,
|
|
378
|
+
; down never moves". Fresh LDA SWCHA + AND #mask per check is immune.
|
|
379
|
+
; Joystick 0 = HIGH nibble (bit4 up, bit5 down); joystick 1 = LOW nibble
|
|
380
|
+
; (bit0 up, bit1 down) — both sticks arrive in this ONE register.
|
|
141
381
|
LDA SWCHA
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
382
|
+
AND #$10 ; joy0 up
|
|
383
|
+
BNE .p0nup
|
|
384
|
+
INC P0_Y ; court Y grows UPWARD (the kernel's line counter
|
|
385
|
+
INC P0_Y ; runs 170 → 2 as the beam moves DOWN the screen)
|
|
386
|
+
.p0nup:
|
|
387
|
+
LDA SWCHA ; RE-LOAD — never trust A to still hold SWCHA
|
|
388
|
+
AND #$20 ; joy0 down
|
|
389
|
+
BNE .p0ndn
|
|
146
390
|
DEC P0_Y
|
|
147
391
|
DEC P0_Y
|
|
148
|
-
.
|
|
149
|
-
ASL ; up
|
|
150
|
-
BCS .nu
|
|
151
|
-
INC P0_Y
|
|
152
|
-
INC P0_Y
|
|
153
|
-
.nu:
|
|
154
|
-
; Clamp paddle within bounds
|
|
392
|
+
.p0ndn:
|
|
155
393
|
LDA P0_Y
|
|
156
|
-
|
|
157
|
-
BCS .nopaddmin
|
|
158
|
-
LDA #16
|
|
394
|
+
JSR clamp_paddle
|
|
159
395
|
STA P0_Y
|
|
160
|
-
.nopaddmin:
|
|
161
|
-
CMP #168
|
|
162
|
-
BCC .nopaddmax
|
|
163
|
-
LDA #168
|
|
164
|
-
STA P0_Y
|
|
165
|
-
.nopaddmax:
|
|
166
|
-
.skip_pad:
|
|
167
396
|
|
|
168
|
-
|
|
397
|
+
LDA MODE2P
|
|
398
|
+
BEQ .ai
|
|
399
|
+
; 2P: the second human, same idiom, low nibble
|
|
400
|
+
LDA SWCHA
|
|
401
|
+
AND #$01 ; joy1 up
|
|
402
|
+
BNE .p1nup
|
|
403
|
+
INC P1_Y
|
|
404
|
+
INC P1_Y
|
|
405
|
+
.p1nup:
|
|
406
|
+
LDA SWCHA ; RE-LOAD (same footgun, other nibble)
|
|
407
|
+
AND #$02 ; joy1 down
|
|
408
|
+
BNE .p1ndn
|
|
409
|
+
DEC P1_Y
|
|
410
|
+
DEC P1_Y
|
|
411
|
+
.p1ndn:
|
|
412
|
+
JMP .p1clamp
|
|
413
|
+
.ai:
|
|
414
|
+
; ── GAME LOGIC (clay) — the AI is deliberately beatable: it moves 1px
|
|
415
|
+
; on only 3 of every 4 frames (0.75 px/f) while the ball climbs/dives at
|
|
416
|
+
; 1 px/f — edge hits (which re-angle the ball) out-run it.
|
|
417
|
+
LDA FRAME
|
|
418
|
+
AND #$03
|
|
419
|
+
BEQ .p1clamp ; skip every 4th frame
|
|
169
420
|
LDA BALL_Y
|
|
421
|
+
SEC
|
|
422
|
+
SBC #5 ; aim paddle center at ball center
|
|
170
423
|
CMP P1_Y
|
|
171
|
-
|
|
424
|
+
BEQ .p1clamp
|
|
425
|
+
BCC .aidn
|
|
172
426
|
INC P1_Y
|
|
173
|
-
JMP .
|
|
174
|
-
.
|
|
427
|
+
JMP .p1clamp
|
|
428
|
+
.aidn:
|
|
175
429
|
DEC P1_Y
|
|
176
|
-
.
|
|
430
|
+
.p1clamp:
|
|
431
|
+
LDA P1_Y
|
|
432
|
+
JSR clamp_paddle
|
|
433
|
+
STA P1_Y
|
|
177
434
|
|
|
178
|
-
;
|
|
179
|
-
LDA
|
|
435
|
+
; serve pause: ball hidden, then released from center court
|
|
436
|
+
LDA SERVE_T
|
|
437
|
+
BEQ .ballmove
|
|
438
|
+
DEC SERVE_T
|
|
439
|
+
BNE .nopack_jmp
|
|
440
|
+
LDA #90
|
|
441
|
+
STA BALL_Y ; release the serve
|
|
442
|
+
LDA #78
|
|
443
|
+
STA BALL_X
|
|
444
|
+
.nopack_jmp:
|
|
445
|
+
JMP .pack
|
|
446
|
+
.ballmove:
|
|
447
|
+
LDA BALL_X
|
|
180
448
|
CLC
|
|
181
|
-
ADC
|
|
449
|
+
ADC BALL_DX
|
|
182
450
|
STA BALL_X
|
|
183
|
-
LDA
|
|
451
|
+
LDA BALL_Y
|
|
184
452
|
CLC
|
|
185
|
-
ADC
|
|
453
|
+
ADC BALL_DY
|
|
186
454
|
STA BALL_Y
|
|
187
455
|
|
|
188
|
-
;
|
|
456
|
+
; wall bounce (the court walls live at lines 170-164 and 8-2)
|
|
189
457
|
LDA BALL_Y
|
|
190
|
-
CMP #
|
|
191
|
-
|
|
192
|
-
LDA
|
|
458
|
+
CMP #159
|
|
459
|
+
BCC .nwtop
|
|
460
|
+
LDA #$FF
|
|
193
461
|
STA BALL_DY
|
|
194
462
|
JSR sfx_wall
|
|
195
|
-
.
|
|
463
|
+
.nwtop:
|
|
196
464
|
LDA BALL_Y
|
|
197
|
-
CMP #
|
|
198
|
-
|
|
199
|
-
LDA
|
|
465
|
+
CMP #12
|
|
466
|
+
BCS .nwbot
|
|
467
|
+
LDA #1
|
|
200
468
|
STA BALL_DY
|
|
201
469
|
JSR sfx_wall
|
|
202
|
-
.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
470
|
+
.nwbot:
|
|
471
|
+
|
|
472
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
473
|
+
; Paddle/ball collision via the TIA's hardware collision LATCHES — the
|
|
474
|
+
; 2600 detects overlap per-pixel in silicon while it draws; you read the
|
|
475
|
+
; latched result here, one frame later, for free (no AABB math). Rules:
|
|
476
|
+
; * latches accumulate until CXCLR — clear them EVERY frame, or a stale
|
|
477
|
+
; hit from 10 frames ago bounces a ball that isn't there;
|
|
478
|
+
; * gate on travel direction, or the ball re-bounces every frame while
|
|
479
|
+
; it overlaps the paddle (the "ball glued to paddle" classic).
|
|
480
|
+
BIT CXP0FB ; bit6 (V flag) = P0/ball overlapped last frame
|
|
481
|
+
BVC .nohit0
|
|
482
|
+
LDA BALL_DX
|
|
483
|
+
BPL .nohit0 ; only when moving LEFT (toward P0)
|
|
484
|
+
LDA #2
|
|
208
485
|
STA BALL_DX
|
|
209
|
-
LDA #80
|
|
210
|
-
STA BALL_X
|
|
211
|
-
JSR sfx_score
|
|
212
|
-
.nb_l:
|
|
213
486
|
LDA BALL_X
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
487
|
+
CLC
|
|
488
|
+
ADC #2
|
|
489
|
+
STA BALL_X
|
|
490
|
+
LDA P0_Y
|
|
491
|
+
JSR paddle_english
|
|
492
|
+
JSR rally_hit
|
|
493
|
+
.nohit0:
|
|
494
|
+
BIT CXP1FB
|
|
495
|
+
BVC .nohit1
|
|
496
|
+
LDA BALL_DX
|
|
497
|
+
BMI .nohit1 ; only when moving RIGHT (toward P1)
|
|
498
|
+
LDA #$FE ; -2
|
|
217
499
|
STA BALL_DX
|
|
218
|
-
LDA
|
|
500
|
+
LDA BALL_X
|
|
501
|
+
SEC
|
|
502
|
+
SBC #2
|
|
219
503
|
STA BALL_X
|
|
220
|
-
|
|
221
|
-
|
|
504
|
+
LDA P1_Y
|
|
505
|
+
JSR paddle_english
|
|
506
|
+
JSR rally_hit
|
|
507
|
+
.nohit1:
|
|
508
|
+
STA CXCLR ; arm the latches fresh for the frame we're about to draw
|
|
222
509
|
|
|
223
|
-
;
|
|
224
|
-
LDA
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
510
|
+
; scoring: ball escaped past a paddle
|
|
511
|
+
LDA BALL_X
|
|
512
|
+
CMP #4
|
|
513
|
+
BCS .nptL
|
|
514
|
+
LDX #1 ; right player scores
|
|
515
|
+
JSR point_scored
|
|
516
|
+
JMP .pack
|
|
517
|
+
.nptL:
|
|
518
|
+
CMP #149
|
|
519
|
+
BCC .pack
|
|
520
|
+
LDX #0 ; left player scores
|
|
521
|
+
JSR point_scored
|
|
522
|
+
|
|
523
|
+
.pack:
|
|
524
|
+
JSR pack_scores
|
|
525
|
+
JMP position_objects ; (tail-call; RTS from there ends frame_logic)
|
|
526
|
+
|
|
527
|
+
; ── GAME LOGIC (clay — reshape freely) ── game-over freeze-frame ───────
|
|
528
|
+
logic_over:
|
|
529
|
+
LDA EDGEB
|
|
530
|
+
AND #$01
|
|
531
|
+
BNE .toTitle
|
|
532
|
+
LDA FIRE_EDG
|
|
533
|
+
AND #$C0 ; either fire button
|
|
534
|
+
BNE .toTitle
|
|
535
|
+
DEC OVER_T
|
|
536
|
+
BNE .stay
|
|
537
|
+
.toTitle:
|
|
538
|
+
JMP enter_title
|
|
539
|
+
.stay:
|
|
540
|
+
; flash the court between green and dark red while frozen
|
|
541
|
+
LDA FRAME
|
|
542
|
+
AND #$10
|
|
543
|
+
BEQ .flashB
|
|
544
|
+
LDA #COL_COURT
|
|
545
|
+
JMP .flashSet
|
|
546
|
+
.flashB:
|
|
547
|
+
LDA #$42
|
|
548
|
+
.flashSet:
|
|
549
|
+
STA COURTBK
|
|
550
|
+
JSR pack_scores
|
|
551
|
+
; blink the WINNER's digit (clearly decodable evidence of who won)
|
|
552
|
+
LDA FRAME
|
|
553
|
+
AND #$10
|
|
554
|
+
BNE .noblink
|
|
555
|
+
LDX #4
|
|
556
|
+
LDA WINNER
|
|
557
|
+
BNE .blink1
|
|
558
|
+
.blink0:
|
|
228
559
|
LDA #0
|
|
229
|
-
STA
|
|
230
|
-
.sfx_done:
|
|
231
|
-
|
|
232
|
-
; ── Position P0 / P1 / HMOVE — exactly 3 WSYNC-bounded lines ───────
|
|
233
|
-
; CRITICAL: every RESPx write AND the STA HMOVE must complete inside
|
|
234
|
-
; the 76-cycle scanline that began with its STA WSYNC. A DEX/BNE delay
|
|
235
|
-
; loop costs 5 cycles/iteration, so the loop count must be small enough
|
|
236
|
-
; that RESPx still lands before the line ends. The old code used
|
|
237
|
-
; LDX #38 (~189 cycles = 2.5 scanlines!) with no WSYNC before RESP1/
|
|
238
|
-
; HMOVE, so it emitted ~2-3 UNCOUNTED scanlines past the 262 budget →
|
|
239
|
-
; ~265 lines/frame → vsync never locks (rolling magenta band). HMOVE
|
|
240
|
-
; was also issued mid-line; it must follow a fresh WSYNC.
|
|
241
|
-
|
|
242
|
-
; Line 1 of 3: coarse-position P0 (left, ~column 16)
|
|
243
|
-
STA WSYNC
|
|
244
|
-
LDX #5
|
|
245
|
-
.p0d:
|
|
560
|
+
STA S0BUF,X
|
|
246
561
|
DEX
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
.p1d:
|
|
562
|
+
BPL .blink0
|
|
563
|
+
JMP .noblink
|
|
564
|
+
.blink1:
|
|
565
|
+
LDA #0
|
|
566
|
+
STA S1BUF,X
|
|
253
567
|
DEX
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
STA WSYNC
|
|
258
|
-
STA HMOVE
|
|
568
|
+
BPL .blink1
|
|
569
|
+
.noblink:
|
|
570
|
+
JMP position_objects
|
|
259
571
|
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
572
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
573
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
574
|
+
; HORIZONTAL POSITIONING — the canonical SBC-#15 beam-race. There is no
|
|
575
|
+
; "X register" for sprites: you strobe RESPx/RESBL and the object lands
|
|
576
|
+
; WHEREVER THE BEAM IS. Each SBC/BCS lap is 5 CPU cycles = 15 beam pixels,
|
|
577
|
+
; so when the subtraction underflows the beam has crossed x/15 coarse
|
|
578
|
+
; columns; the remainder (-15..-1), EOR #7 and shifted to the high nibble,
|
|
579
|
+
; becomes the ±7px fine offset HMOVE applies on the next line. The naive
|
|
580
|
+
; "divide first, then burn a delay loop" version lands in the WRONG column
|
|
581
|
+
; — RESP must fire AT the beam position, not after computed time.
|
|
582
|
+
; Three objects = three WSYNC lines + one shared HMOVE line, all inside
|
|
583
|
+
; the timed VBLANK window. Paddles are at fixed columns but are re-strobed
|
|
584
|
+
; every frame anyway: one proven code path, no special cases.
|
|
585
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
586
|
+
position_objects:
|
|
587
|
+
LDA #16 ; left paddle column
|
|
265
588
|
STA WSYNC
|
|
266
|
-
LDA BALL_X
|
|
267
|
-
CLC
|
|
268
|
-
ADC #14 ; +14: compensate the loop's minimum latency
|
|
269
589
|
SEC
|
|
270
|
-
.
|
|
590
|
+
.d0:
|
|
591
|
+
SBC #15
|
|
592
|
+
BCS .d0
|
|
593
|
+
EOR #7
|
|
594
|
+
ASL
|
|
595
|
+
ASL
|
|
596
|
+
ASL
|
|
597
|
+
ASL
|
|
598
|
+
STA RESP0 ; coarse: lands at the beam's current column
|
|
599
|
+
STA HMP0 ; fine: remainder → signed nibble
|
|
600
|
+
LDA #140 ; right paddle column
|
|
601
|
+
STA WSYNC
|
|
602
|
+
SEC
|
|
603
|
+
.d1:
|
|
604
|
+
SBC #15
|
|
605
|
+
BCS .d1
|
|
606
|
+
EOR #7
|
|
607
|
+
ASL
|
|
608
|
+
ASL
|
|
609
|
+
ASL
|
|
610
|
+
ASL
|
|
611
|
+
STA RESP1
|
|
612
|
+
STA HMP1
|
|
613
|
+
LDA BALL_X ; ball (logic clamps it to 4..150 — past ~155 the
|
|
614
|
+
STA WSYNC ; divide loop wouldn't finish inside the line)
|
|
615
|
+
SEC
|
|
616
|
+
.d2:
|
|
271
617
|
SBC #15
|
|
272
|
-
BCS .
|
|
273
|
-
|
|
274
|
-
EOR #$FF ; fine: remainder -> HMBL nibble
|
|
618
|
+
BCS .d2
|
|
619
|
+
EOR #7
|
|
275
620
|
ASL
|
|
276
621
|
ASL
|
|
277
622
|
ASL
|
|
278
623
|
ASL
|
|
624
|
+
STA RESBL
|
|
279
625
|
STA HMBL
|
|
280
626
|
STA WSYNC
|
|
281
|
-
STA HMOVE
|
|
627
|
+
STA HMOVE ; one HMOVE applies ALL the fine offsets; it must
|
|
628
|
+
RTS ; come fresh after a WSYNC (mid-line HMOVE shifts
|
|
629
|
+
; the line's pixels — the "comb" artifact)
|
|
630
|
+
|
|
631
|
+
; ── GAME LOGIC (clay — reshape freely) ── helpers ──────────────────────
|
|
632
|
+
clamp_paddle: ; A = paddle Y → clamped to the court
|
|
633
|
+
CMP #12
|
|
634
|
+
BCS .cl1
|
|
635
|
+
LDA #12
|
|
636
|
+
.cl1:
|
|
637
|
+
CMP #148
|
|
638
|
+
BCC .cl2
|
|
639
|
+
LDA #148
|
|
640
|
+
.cl2:
|
|
641
|
+
RTS
|
|
642
|
+
|
|
643
|
+
paddle_english: ; A = paddle bottom Y; deflect by hit point:
|
|
644
|
+
STA TMP ; top third sends the ball up, bottom third down
|
|
645
|
+
LDA BALL_Y
|
|
646
|
+
SEC
|
|
647
|
+
SBC TMP ; 0..13 = where on the paddle we struck
|
|
648
|
+
CMP #10
|
|
649
|
+
BCC .eng1
|
|
650
|
+
LDA #1 ; struck the top → deflect upward
|
|
651
|
+
STA BALL_DY
|
|
652
|
+
RTS
|
|
653
|
+
.eng1:
|
|
654
|
+
CMP #4
|
|
655
|
+
BCS .eng2
|
|
656
|
+
LDA #$FF ; struck the bottom → deflect downward
|
|
657
|
+
STA BALL_DY
|
|
658
|
+
.eng2:
|
|
659
|
+
RTS
|
|
660
|
+
|
|
661
|
+
rally_hit: ; one more paddle hit this volley (BCD, capped at 99)
|
|
662
|
+
LDA RALLY
|
|
663
|
+
CMP #$99
|
|
664
|
+
BEQ .rdone
|
|
665
|
+
SED ; BCD mode: $09 + 1 = $10, nibbles stay decimal —
|
|
666
|
+
CLC ; the score kernel reads digits straight out of the
|
|
667
|
+
ADC #1 ; nibbles, no divide-by-10 anywhere
|
|
668
|
+
STA RALLY
|
|
669
|
+
CLD ; ALWAYS clear decimal mode immediately
|
|
670
|
+
.rdone:
|
|
671
|
+
LDA #$0A ; paddle blip
|
|
672
|
+
LDX #$04
|
|
673
|
+
LDY #4
|
|
674
|
+
JMP sfx_play
|
|
675
|
+
|
|
676
|
+
rally_end: ; volley over: keep the best rally as the session
|
|
677
|
+
LDA RALLY ; hi-score. RAM ONLY — no battery exists on a real
|
|
678
|
+
CMP HISCORE ; 2600 cart, so this honestly resets at power-off.
|
|
679
|
+
BCC .rkeep
|
|
680
|
+
STA HISCORE
|
|
681
|
+
.rkeep:
|
|
682
|
+
LDA #0
|
|
683
|
+
STA RALLY
|
|
684
|
+
RTS
|
|
685
|
+
|
|
686
|
+
point_scored: ; X = scorer (0 = left, 1 = right)
|
|
687
|
+
JSR rally_end
|
|
688
|
+
LDA SCORE0,X ; SCORE0/SCORE1 are adjacent — indexed access
|
|
689
|
+
CLC
|
|
690
|
+
ADC #1
|
|
691
|
+
STA SCORE0,X
|
|
692
|
+
CMP #7
|
|
693
|
+
BCS game_over
|
|
694
|
+
; serve again, toward the side that just conceded
|
|
695
|
+
LDA #50
|
|
696
|
+
STA SERVE_T
|
|
697
|
+
LDA #200
|
|
698
|
+
STA BALL_Y ; hide the ball during the serve pause
|
|
699
|
+
LDA #78
|
|
700
|
+
STA BALL_X
|
|
701
|
+
; Serve TOWARD the player who just conceded — an idle player keeps
|
|
702
|
+
; conceding, so an unattended match always ends (no stalemates).
|
|
703
|
+
TXA
|
|
704
|
+
BNE .srvL
|
|
705
|
+
LDA #2 ; left scored → conceder is on the right → serve right
|
|
706
|
+
JMP .srvSet
|
|
707
|
+
.srvL:
|
|
708
|
+
LDA #$FE ; right scored → serve left (-2)
|
|
709
|
+
.srvSet:
|
|
710
|
+
STA BALL_DX
|
|
711
|
+
LDA FRAME
|
|
712
|
+
AND #$01 ; pseudo-random up/down serve angle
|
|
713
|
+
BNE .srvUp
|
|
714
|
+
LDA #$FF
|
|
715
|
+
STA BALL_DY
|
|
716
|
+
JMP .srvSnd
|
|
717
|
+
.srvUp:
|
|
718
|
+
LDA #1
|
|
719
|
+
STA BALL_DY
|
|
720
|
+
.srvSnd:
|
|
721
|
+
LDA #$06 ; point chime
|
|
722
|
+
LDX #$04
|
|
723
|
+
LDY #20
|
|
724
|
+
JMP sfx_play
|
|
725
|
+
|
|
726
|
+
game_over:
|
|
727
|
+
STX WINNER
|
|
728
|
+
LDA #2
|
|
729
|
+
STA STATE
|
|
730
|
+
LDA #240 ; ~4 s freeze, then auto-return to title
|
|
731
|
+
STA OVER_T
|
|
732
|
+
LDA #200
|
|
733
|
+
STA BALL_Y ; HIDE the ball — a frozen game must not render a
|
|
734
|
+
LDA #0 ; stale object floating mid-court (looks broken)
|
|
735
|
+
STA ENABL
|
|
736
|
+
LDA #1
|
|
737
|
+
STA TUNE_SEL
|
|
738
|
+
JMP tune_start ; game-over tune on voice 1
|
|
739
|
+
|
|
740
|
+
start_game:
|
|
282
741
|
LDA #0
|
|
283
|
-
STA
|
|
742
|
+
STA SCORE0
|
|
743
|
+
STA SCORE1
|
|
744
|
+
STA RALLY
|
|
745
|
+
STA TUNE_LEFT ; silence the title jingle
|
|
746
|
+
STA AUDV1
|
|
747
|
+
LDA #80
|
|
748
|
+
STA P0_Y
|
|
749
|
+
STA P1_Y
|
|
750
|
+
LDA #1
|
|
751
|
+
STA STATE
|
|
752
|
+
LDA #50
|
|
753
|
+
STA SERVE_T
|
|
754
|
+
LDA #200
|
|
755
|
+
STA BALL_Y
|
|
756
|
+
LDA #78
|
|
757
|
+
STA BALL_X
|
|
758
|
+
LDA #$FE ; first serve toward the left player
|
|
759
|
+
STA BALL_DX
|
|
760
|
+
LDA #1
|
|
761
|
+
STA BALL_DY
|
|
762
|
+
LDA #COL_COURT
|
|
763
|
+
STA COURTBK
|
|
764
|
+
LDA #$08 ; start blip
|
|
765
|
+
LDX #$04
|
|
766
|
+
LDY #10
|
|
767
|
+
JMP sfx_play
|
|
284
768
|
|
|
769
|
+
enter_title:
|
|
285
770
|
LDA #0
|
|
286
|
-
STA
|
|
771
|
+
STA STATE
|
|
772
|
+
STA GRP0
|
|
773
|
+
STA GRP1
|
|
774
|
+
STA ENABL
|
|
775
|
+
STA AUDV0
|
|
776
|
+
STA SFX_LEFT
|
|
777
|
+
STA TUNE_SEL ; title jingle
|
|
778
|
+
JMP tune_start
|
|
779
|
+
|
|
780
|
+
digit_times5: ; A = digit 0-9 → A = digit*5 (DIGITS row index)
|
|
781
|
+
STA TMP
|
|
782
|
+
ASL
|
|
783
|
+
ASL
|
|
784
|
+
CLC
|
|
785
|
+
ADC TMP
|
|
786
|
+
RTS
|
|
787
|
+
|
|
788
|
+
pack_scores: ; render SCORE0/SCORE1 into the kernel's row buffers
|
|
789
|
+
LDA SCORE0
|
|
790
|
+
JSR digit_times5
|
|
791
|
+
TAX
|
|
792
|
+
LDY #0
|
|
793
|
+
.ps0:
|
|
794
|
+
LDA DIGITS,X
|
|
795
|
+
STA S0BUF,Y
|
|
796
|
+
INX
|
|
797
|
+
INY
|
|
798
|
+
CPY #5
|
|
799
|
+
BNE .ps0
|
|
800
|
+
LDA SCORE1
|
|
801
|
+
JSR digit_times5
|
|
802
|
+
TAX
|
|
803
|
+
LDY #0
|
|
804
|
+
.ps1:
|
|
805
|
+
LDA DIGITS,X
|
|
806
|
+
STA S1BUF,Y
|
|
807
|
+
INX
|
|
808
|
+
INY
|
|
809
|
+
CPY #5
|
|
810
|
+
BNE .ps1
|
|
811
|
+
RTS
|
|
287
812
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
LDY #
|
|
303
|
-
|
|
304
|
-
|
|
813
|
+
; ── GAME LOGIC (clay — reshape freely) ── TIA sound ────────────────────
|
|
814
|
+
; Voice 0 = one-shot sound effects; voice 1 = the jingle player. Keeping
|
|
815
|
+
; them on separate voices means a wall blip never cuts the tune off.
|
|
816
|
+
sfx_play: ; A = AUDF pitch, X = AUDC waveform, Y = frames
|
|
817
|
+
STA AUDF0
|
|
818
|
+
STX AUDC0
|
|
819
|
+
STY SFX_LEFT
|
|
820
|
+
LDA #$0C
|
|
821
|
+
STA AUDV0
|
|
822
|
+
RTS
|
|
823
|
+
|
|
824
|
+
sfx_wall:
|
|
825
|
+
LDA #$13
|
|
826
|
+
LDX #$04
|
|
827
|
+
LDY #4
|
|
828
|
+
JMP sfx_play
|
|
829
|
+
|
|
830
|
+
tune_start: ; TUNE_SEL chosen by caller (0 title, 1 game over)
|
|
831
|
+
LDA #0
|
|
832
|
+
STA TUNE_POS
|
|
833
|
+
JSR tune_note
|
|
834
|
+
LDA #$04 ; pure square wave
|
|
835
|
+
STA AUDC1
|
|
836
|
+
LDA #$06
|
|
837
|
+
STA AUDV1
|
|
838
|
+
LDA #8
|
|
839
|
+
STA TUNE_LEFT
|
|
840
|
+
RTS
|
|
841
|
+
|
|
842
|
+
tune_note: ; load AUDF1 from the selected table at TUNE_POS;
|
|
843
|
+
LDX TUNE_POS ; returns Z set (A=0) on the $FF terminator
|
|
844
|
+
LDA TUNE_SEL
|
|
845
|
+
BNE .tn1
|
|
846
|
+
LDA TITLE_TUNE,X
|
|
847
|
+
JMP .tn2
|
|
848
|
+
.tn1:
|
|
849
|
+
LDA OVER_TUNE,X
|
|
850
|
+
.tn2:
|
|
851
|
+
CMP #$FF
|
|
852
|
+
BEQ .tnEnd
|
|
853
|
+
STA AUDF1
|
|
854
|
+
LDA #1
|
|
855
|
+
RTS
|
|
856
|
+
.tnEnd:
|
|
857
|
+
LDA #0
|
|
858
|
+
STA AUDV1
|
|
859
|
+
RTS
|
|
860
|
+
|
|
861
|
+
audio_tick: ; called once per frame, every state
|
|
862
|
+
LDA SFX_LEFT
|
|
863
|
+
BEQ .at1
|
|
864
|
+
DEC SFX_LEFT
|
|
865
|
+
BNE .at1
|
|
866
|
+
LDA #0
|
|
867
|
+
STA AUDV0 ; sfx finished → silence voice 0
|
|
868
|
+
.at1:
|
|
869
|
+
LDA TUNE_LEFT
|
|
870
|
+
BEQ .at2
|
|
871
|
+
DEC TUNE_LEFT
|
|
872
|
+
BNE .at2
|
|
873
|
+
INC TUNE_POS
|
|
874
|
+
JSR tune_note
|
|
875
|
+
BEQ .at2 ; hit the terminator → tune stays off
|
|
876
|
+
LDA #8
|
|
877
|
+
STA TUNE_LEFT
|
|
878
|
+
.at2:
|
|
879
|
+
RTS
|
|
880
|
+
|
|
881
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
882
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
883
|
+
; THE PLAY/GAME-OVER KERNEL — 192 visible lines, fully accounted:
|
|
884
|
+
; 22 = score bar (20 digit lines + 2 transition) + 170 = court
|
|
885
|
+
; Two per-line tricks live here:
|
|
886
|
+
;
|
|
887
|
+
; SCORE BAR (SCORE mode + mid-line PF1 rewrite): CTRLPF = $02 puts the TIA
|
|
888
|
+
; in SCORE mode — the LEFT playfield half draws in COLUP0's color and the
|
|
889
|
+
; RIGHT half in COLUP1's. The TIA reads PF1 twice per line (left copy at
|
|
890
|
+
; color clocks 84-115, right copy at 164-195, with the CPU at 3 clocks per
|
|
891
|
+
; cycle). Write the LEFT player's digit row early in the line, then write
|
|
892
|
+
; the RIGHT player's row in the window AFTER the left copy is fully drawn
|
|
893
|
+
; (cycle 39) and BEFORE the right copy starts (cycle 54) — one register,
|
|
894
|
+
; two different digits, two colors. The NOPs below are not padding sloth:
|
|
895
|
+
; each is 2 cycles = 6 beam pixels of deliberate waiting for that window.
|
|
896
|
+
; (Without the rewrite you'd see the same byte twice — the classic
|
|
897
|
+
; "10 10" dual-score look, which the title screen embraces deliberately.)
|
|
898
|
+
;
|
|
899
|
+
; COURT (two-line kernel): one line of work here (walls + net + paddle +
|
|
900
|
+
; ball, each a compare-and-store) is ~90 cycles — more than the 76 a single
|
|
901
|
+
; scanline allows. The standard fix: each loop pass spans TWO scanlines and
|
|
902
|
+
; splits the work — line A draws playfield + left paddle, line B draws
|
|
903
|
+
; right paddle + ball. 85 passes × 2 = 170 lines; objects move in 2-px
|
|
904
|
+
; steps, which 1977 televisions made invisible.
|
|
905
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
906
|
+
play_kernel:
|
|
907
|
+
; band setup runs in the last blanked line — registers are live before
|
|
908
|
+
; the first visible WSYNC
|
|
909
|
+
LDA #0
|
|
910
|
+
STA COLUBK ; score bar band is black
|
|
911
|
+
STA PF0
|
|
912
|
+
STA PF1
|
|
913
|
+
STA PF2
|
|
914
|
+
STA GRP0 ; scrub objects left over from last frame's court
|
|
915
|
+
STA GRP1 ; (TIA registers persist; the bar would re-render
|
|
916
|
+
STA ENABL ; a stale ball pixel on every bar line otherwise)
|
|
917
|
+
STA VBLANK ; beam on
|
|
918
|
+
LDA #$0E
|
|
919
|
+
STA COLUPF ; walls + net in white (the title kernel leaves its
|
|
920
|
+
; last banner color in COLUPF — registers persist!)
|
|
921
|
+
LDA #$02
|
|
922
|
+
STA CTRLPF ; SCORE mode: PF left half = COLUP0, right = COLUP1
|
|
923
|
+
|
|
924
|
+
LDX #0 ; 20 score-bar lines, 5 digit rows × 4 lines each
|
|
925
|
+
.sbar:
|
|
926
|
+
STA WSYNC
|
|
927
|
+
TXA
|
|
928
|
+
LSR
|
|
929
|
+
LSR
|
|
930
|
+
TAY ; row = line/4
|
|
931
|
+
LDA S0BUF,Y
|
|
932
|
+
STA PF1 ; cycle ~15 → left copy gets the LEFT digit
|
|
933
|
+
NOP ; ── now wait for the beam ──
|
|
934
|
+
NOP ; 9 NOPs = 18 cycles = 54 beam pixels: parks the
|
|
935
|
+
NOP ; CPU until the TIA has FINISHED drawing the left
|
|
936
|
+
NOP ; copy of PF1 (ends cycle ~38) but hasn't started
|
|
937
|
+
NOP ; the right copy (cycle ~55)
|
|
938
|
+
NOP
|
|
939
|
+
NOP
|
|
940
|
+
NOP
|
|
941
|
+
NOP
|
|
942
|
+
LDA S1BUF,Y
|
|
943
|
+
STA PF1 ; cycle ~40 → right copy gets the RIGHT digit
|
|
944
|
+
INX
|
|
945
|
+
CPX #20
|
|
946
|
+
BNE .sbar
|
|
947
|
+
|
|
948
|
+
; 2 transition lines: clear the bar, re-program the TIA for the court.
|
|
949
|
+
; (The TIA has no concept of "regions" — CTRLPF/COLUBK are simply
|
|
950
|
+
; rewritten mid-frame. EVERY banded 2600 screen is built this way.)
|
|
305
951
|
STA WSYNC
|
|
306
|
-
; Top + bottom walls: full-width PF on the outer rows (Y>=189 / Y<5)
|
|
307
952
|
LDA #0
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
953
|
+
STA PF1
|
|
954
|
+
LDA #$11 ; court CTRLPF: REFLECT (bit0, symmetric walls) +
|
|
955
|
+
STA CTRLPF ; 2-px ball (bits 4-5 = 01)
|
|
956
|
+
LDA COURTBK
|
|
957
|
+
STA COLUBK
|
|
958
|
+
STA WSYNC
|
|
959
|
+
|
|
960
|
+
; court: Y = 170 down to 2, step 2 (85 two-line passes)
|
|
961
|
+
LDY #170
|
|
962
|
+
.court:
|
|
963
|
+
; ---- line A: walls + center net (playfield) + left paddle ----
|
|
964
|
+
STA WSYNC
|
|
965
|
+
LDA #0
|
|
966
|
+
CPY #164
|
|
967
|
+
BCC .ckbot ; Y >= 164 → top wall band (8 lines)
|
|
313
968
|
LDA #$FF
|
|
314
|
-
.
|
|
969
|
+
BNE .wallSet
|
|
970
|
+
.ckbot:
|
|
971
|
+
CPY #10
|
|
972
|
+
BCS .wallSet ; Y < 10 → bottom wall band (8 lines)
|
|
973
|
+
LDA #$FF
|
|
974
|
+
.wallSet:
|
|
315
975
|
STA PF0
|
|
316
976
|
STA PF1
|
|
317
|
-
STA PF2
|
|
318
|
-
; Left paddle: 8 lines starting at P0_Y
|
|
977
|
+
STA TMP ; remember wall byte — PF2 also carries the net
|
|
319
978
|
TYA
|
|
979
|
+
AND #$08 ; dashed center line, 8 lines on / 8 off
|
|
980
|
+
BNE .dashOff
|
|
981
|
+
LDA #$80 ; PF2 bit7 = the center-most playfield pixel; the
|
|
982
|
+
JMP .dashSet ; REFLECT bit mirrors it to make a 2-px net
|
|
983
|
+
.dashOff:
|
|
984
|
+
LDA #0
|
|
985
|
+
.dashSet:
|
|
986
|
+
ORA TMP
|
|
987
|
+
STA PF2
|
|
988
|
+
TYA ; left paddle: drawn when (Y - P0_Y) in [0,14)
|
|
320
989
|
SEC
|
|
321
990
|
SBC P0_Y
|
|
322
|
-
CMP #
|
|
323
|
-
LDA #0
|
|
991
|
+
CMP #PADH
|
|
992
|
+
LDA #0 ; branchless on/off: A = 0, overwritten if in range
|
|
324
993
|
BCS .p0off
|
|
325
|
-
LDA
|
|
994
|
+
LDA #PADGFX
|
|
326
995
|
.p0off:
|
|
327
996
|
STA GRP0
|
|
328
|
-
; ----
|
|
997
|
+
; ---- line B: right paddle + ball ----
|
|
329
998
|
STA WSYNC
|
|
330
|
-
; Right paddle: 8 lines starting at P1_Y
|
|
331
999
|
TYA
|
|
332
1000
|
SEC
|
|
333
1001
|
SBC P1_Y
|
|
334
|
-
CMP #
|
|
1002
|
+
CMP #PADH
|
|
335
1003
|
LDA #0
|
|
336
1004
|
BCS .p1off
|
|
337
|
-
LDA
|
|
1005
|
+
LDA #PADGFX
|
|
338
1006
|
.p1off:
|
|
339
1007
|
STA GRP1
|
|
340
|
-
;
|
|
341
|
-
|
|
342
|
-
SEC
|
|
1008
|
+
TYA ; ball: 4 lines tall at BALL_Y (200 = parked
|
|
1009
|
+
SEC ; off-court → never matches → hidden)
|
|
343
1010
|
SBC BALL_Y
|
|
344
|
-
CMP #
|
|
1011
|
+
CMP #4
|
|
345
1012
|
LDA #0
|
|
346
1013
|
BCS .bloff
|
|
347
1014
|
LDA #2
|
|
@@ -349,45 +1016,260 @@ MAIN:
|
|
|
349
1016
|
STA ENABL
|
|
350
1017
|
DEY
|
|
351
1018
|
DEY
|
|
352
|
-
BNE .
|
|
1019
|
+
BNE .court
|
|
353
1020
|
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
1021
|
+
JMP kernel_done
|
|
1022
|
+
|
|
1023
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1024
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
1025
|
+
; THE TITLE KERNEL — 192 lines, banded:
|
|
1026
|
+
; 16 blank + 28 banner "RAPID" + 8 gap + 28 banner "RALLY" + 16 gap +
|
|
1027
|
+
; 20 hi-score + 12 gap + 10 mode digit + 54 bottom pad = 192
|
|
1028
|
+
;
|
|
1029
|
+
; The banner is an ASYMMETRIC PLAYFIELD — the 2600's only way to draw
|
|
1030
|
+
; full-width artwork. The playfield registers hold just 20 pixels; the TIA
|
|
1031
|
+
; replays them for the right half of the line (CTRLPF bit0 chooses repeat
|
|
1032
|
+
; or mirror). For 40 INDEPENDENT pixels you rewrite all three registers
|
|
1033
|
+
; mid-line, each inside its window (CPU cycle = 3 color clocks; left copy
|
|
1034
|
+
; reads at clocks 68-147, right copy at 148-227):
|
|
1035
|
+
; PF0 again after cycle ~28 (left copy drawn) before ~49 (right copy reads)
|
|
1036
|
+
; PF1 again after cycle ~39 before ~54
|
|
1037
|
+
; PF2 again after cycle ~50 before ~65
|
|
1038
|
+
; The code below hits those windows by instruction order alone — count
|
|
1039
|
+
; cycles before you reorder ANYTHING between the WSYNC and the last STA.
|
|
1040
|
+
; REQUIRES: CTRLPF bit0 = 0 (repeat mode). In mirror mode the right half
|
|
1041
|
+
; reads the registers in REVERSE order and every window above is wrong.
|
|
1042
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1043
|
+
title_kernel:
|
|
1044
|
+
LDA #$A2 ; deep blue backdrop
|
|
1045
|
+
STA COLUBK
|
|
1046
|
+
LDA #0
|
|
1047
|
+
STA PF0
|
|
1048
|
+
STA PF1
|
|
1049
|
+
STA PF2
|
|
1050
|
+
STA GRP0
|
|
1051
|
+
STA GRP1
|
|
1052
|
+
STA ENABL
|
|
1053
|
+
STA CTRLPF ; REPEAT mode — required by the banner (see above)
|
|
1054
|
+
STA VBLANK ; beam on
|
|
1055
|
+
|
|
1056
|
+
LDX #16 ; band 1: 16 blank lines
|
|
1057
|
+
.tb1:
|
|
359
1058
|
STA WSYNC
|
|
360
1059
|
DEX
|
|
361
|
-
BNE .
|
|
1060
|
+
BNE .tb1
|
|
362
1061
|
|
|
363
|
-
|
|
1062
|
+
LDA #$9E ; word 1 in light blue
|
|
1063
|
+
STA COLUPF
|
|
1064
|
+
LDX #0 ; band 2: 28 banner lines (7 rows × 4)
|
|
1065
|
+
.ban1:
|
|
1066
|
+
STA WSYNC
|
|
1067
|
+
TXA ; row = line/4 (cycle 8)
|
|
1068
|
+
LSR
|
|
1069
|
+
LSR
|
|
1070
|
+
TAY
|
|
1071
|
+
LDA R1_PF0L,Y ; left third of the banner row
|
|
1072
|
+
STA PF0 ; c15 — beam at clock 45, PF0 reads at 68: in time
|
|
1073
|
+
LDA R1_PF1L,Y
|
|
1074
|
+
STA PF1 ; c22 (clock 66 < 84)
|
|
1075
|
+
LDA R1_PF2L,Y
|
|
1076
|
+
STA PF2 ; c29 (clock 87 < 116)
|
|
1077
|
+
LDA R1_PF0R,Y ; ── now RE-write the same registers for the
|
|
1078
|
+
STA PF0 ; right half: c36, clock 108 — left PF0 long since
|
|
1079
|
+
LDA R1_PF1R,Y ; drawn (83), right read still ahead (148)
|
|
1080
|
+
STA PF1 ; c43 (clock 129: left done 115, right at 164)
|
|
1081
|
+
NOP ; 2 cycles of deliberate beam-waiting: left PF2
|
|
1082
|
+
NOP ; finishes at clock 147; don't clobber it early
|
|
1083
|
+
LDA R1_PF2R,Y
|
|
1084
|
+
STA PF2 ; c54 (clock 162: after 147, before 196)
|
|
1085
|
+
INX
|
|
1086
|
+
CPX #28
|
|
1087
|
+
BNE .ban1
|
|
364
1088
|
|
|
365
|
-
;
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
STA
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
STA SFX_LEFT
|
|
376
|
-
RTS
|
|
1089
|
+
STA WSYNC ; band 3: clear + 7 gap lines
|
|
1090
|
+
LDA #0
|
|
1091
|
+
STA PF0
|
|
1092
|
+
STA PF1
|
|
1093
|
+
STA PF2
|
|
1094
|
+
LDX #7
|
|
1095
|
+
.tb3:
|
|
1096
|
+
STA WSYNC
|
|
1097
|
+
DEX
|
|
1098
|
+
BNE .tb3
|
|
377
1099
|
|
|
378
|
-
;
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
1100
|
+
LDA #$4A ; word 2 in warm red
|
|
1101
|
+
STA COLUPF
|
|
1102
|
+
LDX #0 ; band 4: 28 banner lines, word 2
|
|
1103
|
+
.ban2:
|
|
1104
|
+
STA WSYNC
|
|
1105
|
+
TXA
|
|
1106
|
+
LSR
|
|
1107
|
+
LSR
|
|
1108
|
+
TAY
|
|
1109
|
+
LDA R2_PF0L,Y
|
|
1110
|
+
STA PF0
|
|
1111
|
+
LDA R2_PF1L,Y
|
|
1112
|
+
STA PF1
|
|
1113
|
+
LDA R2_PF2L,Y
|
|
1114
|
+
STA PF2
|
|
1115
|
+
LDA R2_PF0R,Y
|
|
1116
|
+
STA PF0
|
|
1117
|
+
LDA R2_PF1R,Y
|
|
1118
|
+
STA PF1
|
|
1119
|
+
NOP
|
|
1120
|
+
NOP
|
|
1121
|
+
LDA R2_PF2R,Y
|
|
1122
|
+
STA PF2
|
|
1123
|
+
INX
|
|
1124
|
+
CPX #28
|
|
1125
|
+
BNE .ban2
|
|
1126
|
+
|
|
1127
|
+
STA WSYNC ; band 5: clear + 15 gap lines
|
|
1128
|
+
LDA #0
|
|
1129
|
+
STA PF0
|
|
1130
|
+
STA PF1
|
|
1131
|
+
STA PF2
|
|
1132
|
+
LDA #$02
|
|
1133
|
+
STA CTRLPF ; SCORE mode for the hi-score + mode-digit bands
|
|
1134
|
+
LDX #15
|
|
1135
|
+
.tb5:
|
|
1136
|
+
STA WSYNC
|
|
1137
|
+
DEX
|
|
1138
|
+
BNE .tb5
|
|
1139
|
+
|
|
1140
|
+
; band 6: hi-score, 20 lines (5 rows × 4). Both digits are packed into
|
|
1141
|
+
; ONE PF1 byte (tens = high nibble = left, ones = low). In SCORE mode
|
|
1142
|
+
; with no reflect the byte draws TWICE — left copy in COLUP0's blue,
|
|
1143
|
+
; right copy in COLUP1's red. That doubled "NN NN" is the classic 2600
|
|
1144
|
+
; dual-score aesthetic (think launch-era tank/plane games): embraced
|
|
1145
|
+
; here, not fixed. In-session best rally; honest comment: there is no
|
|
1146
|
+
; battery — this number is gone at power-off, like the arcades.
|
|
1147
|
+
LDX #0
|
|
1148
|
+
.hsb:
|
|
1149
|
+
STA WSYNC
|
|
1150
|
+
TXA
|
|
1151
|
+
LSR
|
|
1152
|
+
LSR
|
|
1153
|
+
TAY
|
|
1154
|
+
LDA HSBUF,Y
|
|
1155
|
+
STA PF1
|
|
1156
|
+
INX
|
|
1157
|
+
CPX #20
|
|
1158
|
+
BNE .hsb
|
|
1159
|
+
|
|
1160
|
+
STA WSYNC ; band 7: clear + 11 gap lines
|
|
1161
|
+
LDA #0
|
|
1162
|
+
STA PF1
|
|
1163
|
+
LDX #11
|
|
1164
|
+
.tb7:
|
|
1165
|
+
STA WSYNC
|
|
1166
|
+
DEX
|
|
1167
|
+
BNE .tb7
|
|
1168
|
+
|
|
1169
|
+
; band 8: mode digit (1 or 2), 10 lines (5 rows × 2), blinking. Also
|
|
1170
|
+
; doubled by SCORE mode — "1 1" / "2 2" in the two player colors reads
|
|
1171
|
+
; as "this many players". SELECT toggles it; fire 0/1 overrides it.
|
|
1172
|
+
LDX #0
|
|
1173
|
+
.modeb:
|
|
1174
|
+
STA WSYNC
|
|
1175
|
+
TXA
|
|
1176
|
+
LSR
|
|
1177
|
+
TAY
|
|
1178
|
+
LDA INDBUF,Y
|
|
1179
|
+
STA PF1
|
|
1180
|
+
INX
|
|
1181
|
+
CPX #10
|
|
1182
|
+
BNE .modeb
|
|
1183
|
+
|
|
1184
|
+
STA WSYNC ; band 9: clear + 53 pad lines to reach exactly 192
|
|
1185
|
+
LDA #0
|
|
1186
|
+
STA PF1
|
|
1187
|
+
LDX #53
|
|
1188
|
+
.tb9:
|
|
1189
|
+
STA WSYNC
|
|
1190
|
+
DEX
|
|
1191
|
+
BNE .tb9
|
|
1192
|
+
|
|
1193
|
+
JMP kernel_done
|
|
1194
|
+
|
|
1195
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1196
|
+
; ── GAME LOGIC (clay — reshape freely) ── data tables ──────────────────
|
|
1197
|
+
; Digit font: 4 pixels wide × 5 rows, stored in the HIGH nibble (PF1 bit7
|
|
1198
|
+
; is the LEFTMOST pixel of the left playfield half — high nibble = left).
|
|
1199
|
+
DIGITS:
|
|
1200
|
+
.byte $60,$90,$90,$90,$60 ; 0
|
|
1201
|
+
.byte $20,$60,$20,$20,$70 ; 1
|
|
1202
|
+
.byte $60,$90,$20,$40,$F0 ; 2
|
|
1203
|
+
.byte $E0,$10,$60,$10,$E0 ; 3
|
|
1204
|
+
.byte $90,$90,$F0,$10,$10 ; 4
|
|
1205
|
+
.byte $F0,$80,$E0,$10,$E0 ; 5
|
|
1206
|
+
.byte $60,$80,$E0,$90,$60 ; 6
|
|
1207
|
+
.byte $F0,$10,$20,$40,$40 ; 7
|
|
1208
|
+
.byte $60,$90,$60,$90,$60 ; 8
|
|
1209
|
+
.byte $60,$90,$70,$10,$60 ; 9
|
|
1210
|
+
|
|
1211
|
+
; Title jingle (voice 1, AUDC $04 square; AUDF divider — LOWER = higher
|
|
1212
|
+
; pitch; 8 frames per note; $FF terminates). The table IS the song.
|
|
1213
|
+
TITLE_TUNE:
|
|
1214
|
+
.byte $13,$0F,$0C,$09,$0C,$09,$07,$09,$FF
|
|
1215
|
+
; Game-over tune: a falling figure.
|
|
1216
|
+
OVER_TUNE:
|
|
1217
|
+
.byte $07,$09,$0C,$0F,$13,$17,$FF
|
|
1218
|
+
|
|
1219
|
+
; ── THE TITLE BANNER ──────────────────────────────────────────────────
|
|
1220
|
+
; 40-pixel-wide artwork, 7 rows per word, drawn by the asymmetric-playfield
|
|
1221
|
+
; kernel above. Each row is six bytes across six tables (left PF0/PF1/PF2,
|
|
1222
|
+
; right PF0/PF1/PF2). PF bit order is the 2600's great prank — three
|
|
1223
|
+
; registers, three different orders:
|
|
1224
|
+
; PF0: only bits 4-7 used, bit 4 = LEFTMOST pixel (reversed)
|
|
1225
|
+
; PF1: bit 7 = leftmost (normal)
|
|
1226
|
+
; PF2: bit 0 = leftmost (reversed again)
|
|
1227
|
+
; The art below each header is the row layout; regenerate the bytes by
|
|
1228
|
+
; hand or with any 40-column bitmap-to-PF script honoring that order.
|
|
1229
|
+
;
|
|
1230
|
+
; RAPID:
|
|
1231
|
+
; .####.....###....####....#####...####...
|
|
1232
|
+
; .#...#...#...#...#...#.....#.....#...#..
|
|
1233
|
+
; .#...#...#...#...#...#.....#.....#...#..
|
|
1234
|
+
; .####....#####...####......#.....#...#..
|
|
1235
|
+
; .#.#.....#...#...#.........#.....#...#..
|
|
1236
|
+
; .#..#....#...#...#.........#.....#...#..
|
|
1237
|
+
; .#...#...#...#...#.......#####...####...
|
|
1238
|
+
R1_PF0L:
|
|
1239
|
+
.byte %11100000, %00100000, %00100000, %11100000, %10100000, %00100000, %00100000
|
|
1240
|
+
R1_PF1L:
|
|
1241
|
+
.byte %10000011, %01000100, %01000100, %10000111, %00000100, %10000100, %01000100
|
|
1242
|
+
R1_PF2L:
|
|
1243
|
+
.byte %11100001, %00100010, %00100010, %11100011, %00100010, %00100010, %00100010
|
|
1244
|
+
R1_PF0R:
|
|
1245
|
+
.byte %00010000, %00100000, %00100000, %00010000, %00000000, %00000000, %00000000
|
|
1246
|
+
R1_PF1R:
|
|
1247
|
+
.byte %01111100, %00010000, %00010000, %00010000, %00010000, %00010000, %01111100
|
|
1248
|
+
R1_PF2R:
|
|
1249
|
+
.byte %00011110, %00100010, %00100010, %00100010, %00100010, %00100010, %00011110
|
|
1250
|
+
|
|
1251
|
+
; RALLY:
|
|
1252
|
+
; .####.....###....#.......#.......#...#..
|
|
1253
|
+
; .#...#...#...#...#.......#........#.#...
|
|
1254
|
+
; .#...#...#...#...#.......#.........#....
|
|
1255
|
+
; .####....#####...#.......#.........#....
|
|
1256
|
+
; .#.#.....#...#...#.......#.........#....
|
|
1257
|
+
; .#..#....#...#...#.......#.........#....
|
|
1258
|
+
; .#...#...#...#...#####...#####.....#....
|
|
1259
|
+
R2_PF0L:
|
|
1260
|
+
.byte %11100000, %00100000, %00100000, %11100000, %10100000, %00100000, %00100000
|
|
1261
|
+
R2_PF1L:
|
|
1262
|
+
.byte %10000011, %01000100, %01000100, %10000111, %00000100, %10000100, %01000100
|
|
1263
|
+
R2_PF2L:
|
|
1264
|
+
.byte %00100001, %00100010, %00100010, %00100011, %00100010, %00100010, %11100010
|
|
1265
|
+
R2_PF0R:
|
|
1266
|
+
.byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00110000
|
|
1267
|
+
R2_PF1R:
|
|
1268
|
+
.byte %01000000, %01000000, %01000000, %01000000, %01000000, %01000000, %01111100
|
|
1269
|
+
R2_PF2R:
|
|
1270
|
+
.byte %00100010, %00010100, %00001000, %00001000, %00001000, %00001000, %00001000
|
|
389
1271
|
|
|
390
|
-
|
|
1272
|
+
; ── Vector table ──────────────────────────────────────────────────────
|
|
391
1273
|
org $FFFA
|
|
392
1274
|
.word START
|
|
393
1275
|
.word START
|