romdevtools 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- 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 +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- 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 -177
- 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 -180
- 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 -156
- 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 +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- 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 +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- 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 +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- 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 +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- 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 +19 -6
- 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 +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- 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/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- 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 +64 -19
|
@@ -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,248 +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
|
-
; lines total. (Bug fix: this loop used to be 37 AND the positioning added 3
|
|
121
|
-
; more → 265 scanlines/frame → the TV/emulator can't lock vsync → rolling /
|
|
122
|
-
; black picture. Exactly 262 lines = 3 VSYNC + 37 VBLANK + 192 visible + 30
|
|
123
|
-
; overscan; the positioning WSYNCs MUST be counted against the 37.)
|
|
209
|
+
kernel_done:
|
|
210
|
+
; overscan: 30 lines, timer-paced like VBLANK
|
|
124
211
|
LDA #2
|
|
125
212
|
STA VBLANK
|
|
126
|
-
|
|
127
|
-
|
|
213
|
+
LDA #35
|
|
214
|
+
STA TIM64T
|
|
215
|
+
.oswait:
|
|
216
|
+
LDA INTIM
|
|
217
|
+
BNE .oswait
|
|
128
218
|
STA WSYNC
|
|
129
|
-
|
|
130
|
-
BNE .vb
|
|
219
|
+
JMP MAIN
|
|
131
220
|
|
|
132
|
-
|
|
133
|
-
|
|
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
|
|
134
295
|
AND #$01
|
|
135
|
-
|
|
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.
|
|
136
381
|
LDA SWCHA
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
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
|
|
390
|
+
DEC P0_Y
|
|
145
391
|
DEC P0_Y
|
|
146
|
-
.
|
|
147
|
-
; Clamp paddle within bounds
|
|
392
|
+
.p0ndn:
|
|
148
393
|
LDA P0_Y
|
|
149
|
-
|
|
150
|
-
BCS .nopaddmin
|
|
151
|
-
LDA #16
|
|
394
|
+
JSR clamp_paddle
|
|
152
395
|
STA P0_Y
|
|
153
|
-
.nopaddmin:
|
|
154
|
-
CMP #168
|
|
155
|
-
BCC .nopaddmax
|
|
156
|
-
LDA #168
|
|
157
|
-
STA P0_Y
|
|
158
|
-
.nopaddmax:
|
|
159
|
-
.skip_pad:
|
|
160
396
|
|
|
161
|
-
|
|
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
|
|
162
420
|
LDA BALL_Y
|
|
421
|
+
SEC
|
|
422
|
+
SBC #5 ; aim paddle center at ball center
|
|
163
423
|
CMP P1_Y
|
|
164
|
-
|
|
424
|
+
BEQ .p1clamp
|
|
425
|
+
BCC .aidn
|
|
165
426
|
INC P1_Y
|
|
166
|
-
JMP .
|
|
167
|
-
.
|
|
427
|
+
JMP .p1clamp
|
|
428
|
+
.aidn:
|
|
168
429
|
DEC P1_Y
|
|
169
|
-
.
|
|
430
|
+
.p1clamp:
|
|
431
|
+
LDA P1_Y
|
|
432
|
+
JSR clamp_paddle
|
|
433
|
+
STA P1_Y
|
|
170
434
|
|
|
171
|
-
;
|
|
172
|
-
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
|
|
173
448
|
CLC
|
|
174
|
-
ADC
|
|
449
|
+
ADC BALL_DX
|
|
175
450
|
STA BALL_X
|
|
176
|
-
LDA
|
|
451
|
+
LDA BALL_Y
|
|
177
452
|
CLC
|
|
178
|
-
ADC
|
|
453
|
+
ADC BALL_DY
|
|
179
454
|
STA BALL_Y
|
|
180
455
|
|
|
181
|
-
;
|
|
456
|
+
; wall bounce (the court walls live at lines 170-164 and 8-2)
|
|
182
457
|
LDA BALL_Y
|
|
183
|
-
CMP #
|
|
184
|
-
|
|
185
|
-
LDA
|
|
458
|
+
CMP #159
|
|
459
|
+
BCC .nwtop
|
|
460
|
+
LDA #$FF
|
|
186
461
|
STA BALL_DY
|
|
187
462
|
JSR sfx_wall
|
|
188
|
-
.
|
|
463
|
+
.nwtop:
|
|
189
464
|
LDA BALL_Y
|
|
190
|
-
CMP #
|
|
191
|
-
|
|
192
|
-
LDA
|
|
465
|
+
CMP #12
|
|
466
|
+
BCS .nwbot
|
|
467
|
+
LDA #1
|
|
193
468
|
STA BALL_DY
|
|
194
469
|
JSR sfx_wall
|
|
195
|
-
.
|
|
196
|
-
|
|
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
|
|
485
|
+
STA BALL_DX
|
|
197
486
|
LDA BALL_X
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
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
|
|
201
499
|
STA BALL_DX
|
|
202
|
-
LDA
|
|
500
|
+
LDA BALL_X
|
|
501
|
+
SEC
|
|
502
|
+
SBC #2
|
|
203
503
|
STA BALL_X
|
|
204
|
-
|
|
205
|
-
|
|
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
|
|
509
|
+
|
|
510
|
+
; scoring: ball escaped past a paddle
|
|
206
511
|
LDA BALL_X
|
|
207
|
-
CMP #
|
|
208
|
-
|
|
209
|
-
|
|
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:
|
|
559
|
+
LDA #0
|
|
560
|
+
STA S0BUF,X
|
|
561
|
+
DEX
|
|
562
|
+
BPL .blink0
|
|
563
|
+
JMP .noblink
|
|
564
|
+
.blink1:
|
|
565
|
+
LDA #0
|
|
566
|
+
STA S1BUF,X
|
|
567
|
+
DEX
|
|
568
|
+
BPL .blink1
|
|
569
|
+
.noblink:
|
|
570
|
+
JMP position_objects
|
|
571
|
+
|
|
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
|
|
588
|
+
STA WSYNC
|
|
589
|
+
SEC
|
|
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:
|
|
617
|
+
SBC #15
|
|
618
|
+
BCS .d2
|
|
619
|
+
EOR #7
|
|
620
|
+
ASL
|
|
621
|
+
ASL
|
|
622
|
+
ASL
|
|
623
|
+
ASL
|
|
624
|
+
STA RESBL
|
|
625
|
+
STA HMBL
|
|
626
|
+
STA WSYNC
|
|
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:
|
|
210
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:
|
|
741
|
+
LDA #0
|
|
742
|
+
STA SCORE0
|
|
743
|
+
STA SCORE1
|
|
744
|
+
STA RALLY
|
|
745
|
+
STA TUNE_LEFT ; silence the title jingle
|
|
746
|
+
STA AUDV1
|
|
211
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
|
|
212
757
|
STA BALL_X
|
|
213
|
-
|
|
214
|
-
|
|
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
|
|
215
768
|
|
|
216
|
-
|
|
769
|
+
enter_title:
|
|
770
|
+
LDA #0
|
|
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
|
|
812
|
+
|
|
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
|
|
217
862
|
LDA SFX_LEFT
|
|
218
|
-
BEQ .
|
|
863
|
+
BEQ .at1
|
|
219
864
|
DEC SFX_LEFT
|
|
220
|
-
BNE .
|
|
865
|
+
BNE .at1
|
|
221
866
|
LDA #0
|
|
222
|
-
STA AUDV0
|
|
223
|
-
.
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
;
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
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:
|
|
250
926
|
STA WSYNC
|
|
251
|
-
|
|
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
|
|
252
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.)
|
|
951
|
+
STA WSYNC
|
|
253
952
|
LDA #0
|
|
254
|
-
STA
|
|
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
|
|
255
959
|
|
|
256
|
-
;
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
;
|
|
260
|
-
; 1-line kernel each WSYNC iteration then spills past the line boundary,
|
|
261
|
-
; so 192 iterations stretch to ~232 emitted lines → ~250-line frame →
|
|
262
|
-
; vsync never locks (rolling magenta band — THE bug).
|
|
263
|
-
;
|
|
264
|
-
; The fix is the standard 2600 "2-line kernel": each loop pass renders
|
|
265
|
-
; TWO scanlines and splits the work across two WSYNCs, doubling the
|
|
266
|
-
; budget to ~152 cycles. 96 passes × 2 lines = 192 visible lines.
|
|
267
|
-
; Y counts 192→2 in steps of 2; paddles/ball move in 2px steps (fine
|
|
268
|
-
; for Pong). The branchless "LDA #off / CMP / BCS skip / LDA #on" form
|
|
269
|
-
; also drops the JMPs the old code paid every line.
|
|
270
|
-
LDY #192
|
|
271
|
-
.draw:
|
|
272
|
-
; ---- first line of the pair: playfield walls + left paddle ----
|
|
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 ----
|
|
273
964
|
STA WSYNC
|
|
274
|
-
; Top + bottom walls: full-width PF on the outer rows (Y>=189 / Y<5)
|
|
275
965
|
LDA #0
|
|
276
|
-
CPY #
|
|
277
|
-
|
|
278
|
-
CPY #5
|
|
279
|
-
BCS .nowall
|
|
280
|
-
.wall:
|
|
966
|
+
CPY #164
|
|
967
|
+
BCC .ckbot ; Y >= 164 → top wall band (8 lines)
|
|
281
968
|
LDA #$FF
|
|
282
|
-
.
|
|
969
|
+
BNE .wallSet
|
|
970
|
+
.ckbot:
|
|
971
|
+
CPY #10
|
|
972
|
+
BCS .wallSet ; Y < 10 → bottom wall band (8 lines)
|
|
973
|
+
LDA #$FF
|
|
974
|
+
.wallSet:
|
|
283
975
|
STA PF0
|
|
284
976
|
STA PF1
|
|
285
|
-
STA PF2
|
|
286
|
-
; Left paddle: 8 lines starting at P0_Y
|
|
977
|
+
STA TMP ; remember wall byte — PF2 also carries the net
|
|
287
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)
|
|
288
989
|
SEC
|
|
289
990
|
SBC P0_Y
|
|
290
|
-
CMP #
|
|
291
|
-
LDA #0
|
|
991
|
+
CMP #PADH
|
|
992
|
+
LDA #0 ; branchless on/off: A = 0, overwritten if in range
|
|
292
993
|
BCS .p0off
|
|
293
|
-
LDA
|
|
994
|
+
LDA #PADGFX
|
|
294
995
|
.p0off:
|
|
295
996
|
STA GRP0
|
|
296
|
-
; ----
|
|
997
|
+
; ---- line B: right paddle + ball ----
|
|
297
998
|
STA WSYNC
|
|
298
|
-
; Right paddle: 8 lines starting at P1_Y
|
|
299
999
|
TYA
|
|
300
1000
|
SEC
|
|
301
1001
|
SBC P1_Y
|
|
302
|
-
CMP #
|
|
1002
|
+
CMP #PADH
|
|
303
1003
|
LDA #0
|
|
304
1004
|
BCS .p1off
|
|
305
|
-
LDA
|
|
1005
|
+
LDA #PADGFX
|
|
306
1006
|
.p1off:
|
|
307
1007
|
STA GRP1
|
|
308
|
-
;
|
|
309
|
-
|
|
310
|
-
SEC
|
|
1008
|
+
TYA ; ball: 4 lines tall at BALL_Y (200 = parked
|
|
1009
|
+
SEC ; off-court → never matches → hidden)
|
|
311
1010
|
SBC BALL_Y
|
|
312
|
-
CMP #
|
|
1011
|
+
CMP #4
|
|
313
1012
|
LDA #0
|
|
314
1013
|
BCS .bloff
|
|
315
1014
|
LDA #2
|
|
@@ -317,45 +1016,260 @@ MAIN:
|
|
|
317
1016
|
STA ENABL
|
|
318
1017
|
DEY
|
|
319
1018
|
DEY
|
|
320
|
-
BNE .
|
|
1019
|
+
BNE .court
|
|
321
1020
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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:
|
|
327
1058
|
STA WSYNC
|
|
328
1059
|
DEX
|
|
329
|
-
BNE .
|
|
1060
|
+
BNE .tb1
|
|
330
1061
|
|
|
331
|
-
|
|
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
|
|
332
1088
|
|
|
333
|
-
;
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
STA
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
STA SFX_LEFT
|
|
344
|
-
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
|
|
345
1099
|
|
|
346
|
-
;
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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
|
|
357
1271
|
|
|
358
|
-
|
|
1272
|
+
; ── Vector table ──────────────────────────────────────────────────────
|
|
359
1273
|
org $FFFA
|
|
360
1274
|
.word START
|
|
361
1275
|
.word START
|