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,41 +1,75 @@
|
|
|
1
|
-
; ── racing.asm — Atari 2600
|
|
1
|
+
; ── racing.asm — SWERVE STREAK — Atari 2600 road racer (complete game) ───────
|
|
2
2
|
;
|
|
3
|
-
;
|
|
4
|
-
;
|
|
5
|
-
;
|
|
6
|
-
;
|
|
7
|
-
;
|
|
8
|
-
;
|
|
3
|
+
; A COMPLETE, working game — drawn title screen, a forward-view lane racer
|
|
4
|
+
; (your car weaving up a road as traffic descends toward you), distance
|
|
5
|
+
; score + in-session hi-score, TIA sound effects + a title jingle, a crash
|
|
6
|
+
; → game over with auto-return to the title, and the 2600's signature
|
|
7
|
+
; feature: THE WHOLE MACHINE. There is no framebuffer, no tilemap, no OS —
|
|
8
|
+
; every visible scanline below is composed live by racing the beam, and this
|
|
9
|
+
; file teaches the road-racer's load-bearing TIA tricks while doing it:
|
|
9
10
|
;
|
|
10
|
-
;
|
|
11
|
-
;
|
|
12
|
-
;
|
|
13
|
-
;
|
|
14
|
-
;
|
|
15
|
-
;
|
|
16
|
-
;
|
|
17
|
-
;
|
|
18
|
-
;
|
|
11
|
+
; 1. THE ROAD IS PLAYFIELD, AND IT ANIMATES (the sense of motion) — the
|
|
12
|
+
; 2600 has NO hardware scroll and NO tilemap, so a road racer cannot
|
|
13
|
+
; "scroll" anything. The road is drawn from the PLAYFIELD registers
|
|
14
|
+
; (PF0/PF1/PF2) as two edges; the illusion of forward speed comes from
|
|
15
|
+
; animating a dashed CENTRE LINE that crawls DOWN the screen every
|
|
16
|
+
; frame (a per-frame phase offset, SCROLL), plus traffic cars that
|
|
17
|
+
; descend toward you. This is exactly how the era's forward-view road
|
|
18
|
+
; games faked motion — honest, period-correct, no scroll hardware.
|
|
19
|
+
; 2. RESP/HMOVE BEAM POSITIONING (the SBC-#15 idiom) — there is no sprite
|
|
20
|
+
; X register; you strobe RESPx/RESM0 WHERE THE BEAM IS, then nudge ±7px
|
|
21
|
+
; with HMOVE. Three objects (your car P0, a rival car P1, a hazard M0)
|
|
22
|
+
; positioned this way each frame, inside the timed VBLANK window.
|
|
23
|
+
; 3. TIA COLLISION LATCHES (the crash detect) — the TIA detects P0/P1 and
|
|
24
|
+
; M0/P0 pixel overlap in silicon as it draws; we read the latched
|
|
25
|
+
; result one frame later, free, instead of doing AABB math. Clear it
|
|
26
|
+
; every frame (CXCLR) or a stale hit crashes a car that isn't there.
|
|
27
|
+
; 4. TIM64T/INTIM FRAME TIMING — set the RIOT timer for VBLANK/overscan and
|
|
28
|
+
; let it absorb however much the game logic costs, instead of hand-
|
|
29
|
+
; counting WSYNCs (which rolls the picture the moment logic grows).
|
|
19
30
|
;
|
|
20
|
-
;
|
|
21
|
-
;
|
|
22
|
-
;
|
|
23
|
-
;
|
|
24
|
-
;
|
|
25
|
-
; hazard, a fuel gauge via a PF bar, or NUSIZ1 for two-abreast traffic.
|
|
31
|
+
; THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
32
|
+
; very different one. The markers tell you what's what:
|
|
33
|
+
; HARDWARE IDIOM (load-bearing) — cycle-counted / footgun-dodging code;
|
|
34
|
+
; reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
35
|
+
; GAME LOGIC (clay) — movement, scoring, tuning, art: reshape freely.
|
|
26
36
|
;
|
|
27
|
-
;
|
|
28
|
-
;
|
|
29
|
-
;
|
|
30
|
-
;
|
|
31
|
-
;
|
|
32
|
-
;
|
|
33
|
-
;
|
|
34
|
-
;
|
|
37
|
+
; GAME_TITLE: on the 2600 a title is DRAWN, not printed — there is no font
|
|
38
|
+
; hardware. The SWERVE/STREAK banner bitmaps near the bottom of this file ARE
|
|
39
|
+
; the title; redraw them for your game (the comment above each table shows
|
|
40
|
+
; the 40-pixel artwork and the PF0/PF1/PF2 bit-order encoding).
|
|
41
|
+
;
|
|
42
|
+
; CONTROLS (documented for players and for the fork README):
|
|
43
|
+
; Title: fire on JOYSTICK 0 (or console RESET) starts the game
|
|
44
|
+
; Play: joystick 0 LEFT/RIGHT steers your car across the road; survive
|
|
45
|
+
; the descending traffic. The longer you last the FASTER it gets
|
|
46
|
+
; (the centre dashes crawl faster, the traffic descends faster);
|
|
47
|
+
; console RESET returns to the title
|
|
48
|
+
; A crash flashes the screen and ends the run. Your DISTANCE this run is
|
|
49
|
+
; your score; your best DISTANCE this session is shown on the title screen.
|
|
50
|
+
;
|
|
51
|
+
; PLAYERS — 1P, honest. The 2600 has two joystick ports, but this genre's
|
|
52
|
+
; kernel is already spending its scanline budget on the road playfield, your
|
|
53
|
+
; car (P0), a rival car (P1) and a hazard (M0). A second human car would
|
|
54
|
+
; need its OWN positioned object competing for the SAME 76-cycle two-line
|
|
55
|
+
; passes the road + traffic already fill — and a split-screen second road has
|
|
56
|
+
; no spare PF registers. So like the era's single-driver road games, SWERVE
|
|
57
|
+
; STREAK is single-player. (To add a 2P "best distance, alternating runs"
|
|
58
|
+
; mode — cheap, no extra kernel objects — keep a second hi-score and swap on
|
|
59
|
+
; crash; left as an exercise.)
|
|
60
|
+
;
|
|
61
|
+
; HI-SCORE HONESTY: real 2600 cartridges had NO battery, NO SRAM, NO
|
|
62
|
+
; persistence of any kind. The hi-score here lives in RIOT RAM ($A4) and
|
|
63
|
+
; survives game → title cycles only WITHIN one power-on session — exactly
|
|
64
|
+
; like the arcade machines of the era. Power off and it is gone. Do not
|
|
65
|
+
; fake an EEPROM; state it honestly in your fork too.
|
|
66
|
+
;
|
|
67
|
+
; NTSC frame: 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan = 262 lines.
|
|
35
68
|
|
|
36
69
|
processor 6502
|
|
37
70
|
org $F000
|
|
38
71
|
|
|
72
|
+
; ── TIA write registers ───────────────────────────────────────────────
|
|
39
73
|
VSYNC = $00
|
|
40
74
|
VBLANK = $01
|
|
41
75
|
WSYNC = $02
|
|
@@ -60,30 +94,62 @@ HMP1 = $21
|
|
|
60
94
|
HMM0 = $22
|
|
61
95
|
HMOVE = $2A
|
|
62
96
|
HMCLR = $2B
|
|
63
|
-
CXPPMM = $07 ; READ: bit7 = P0/P1 collided
|
|
64
|
-
CXP0FB = $02 ; READ: bit6 = P0/missile-or-ball... we use CXM0P
|
|
65
|
-
CXM0P = $00 ; READ: bit6 = M0/P0 collided
|
|
66
97
|
CXCLR = $2C
|
|
67
|
-
|
|
68
|
-
INPT4 = $0C ; P0 fire (active-low, bit7) — unused here but handy
|
|
69
|
-
; TIA audio
|
|
98
|
+
; ── TIA audio ─────────────────────────────────────────────────────────
|
|
70
99
|
AUDC0 = $15
|
|
100
|
+
AUDC1 = $16
|
|
71
101
|
AUDF0 = $17
|
|
102
|
+
AUDF1 = $18
|
|
72
103
|
AUDV0 = $19
|
|
104
|
+
AUDV1 = $1A
|
|
105
|
+
; ── TIA READ registers (separate read map — the same addresses as some
|
|
106
|
+
; write strobes; e.g. CXPPMM reads $07 while STA $07 writes COLUP1) ─────
|
|
107
|
+
CXM0P = $00 ; bit6 = missile0 / player0 collision (latched)
|
|
108
|
+
CXPPMM = $07 ; bit7 = player0 / player1 collision (latched)
|
|
109
|
+
INPT4 = $0C ; joystick 0 fire (bit7, ACTIVE LOW)
|
|
110
|
+
; ── RIOT ──────────────────────────────────────────────────────────────
|
|
111
|
+
SWCHA = $280 ; joysticks: P0 = high nibble, P1 = LOW nibble
|
|
112
|
+
SWCHB = $282 ; console: bit0 RESET, bit1 SELECT (ACTIVE LOW)
|
|
113
|
+
INTIM = $284 ; timer read
|
|
114
|
+
TIM64T = $296 ; timer set, 64-cycle ticks
|
|
73
115
|
|
|
74
|
-
; ── Zero-page state
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
116
|
+
; ── Zero-page state (the 2600's ENTIRE RAM is $80-$FF — 128 bytes; in
|
|
117
|
+
; core memory dumps system_ram offset 0 = $80) ────────────────────────
|
|
118
|
+
STATE = $80 ; 0 = title, 1 = play, 2 = game over
|
|
119
|
+
P_X = $81 ; player car X column (visible 0..159)
|
|
120
|
+
E1_X = $82 ; rival car (P1) X column
|
|
121
|
+
E1_Y = $83 ; rival car TOP scanline (beam counts 192→1, so a
|
|
122
|
+
; SMALLER value = LOWER on screen = closer to you)
|
|
123
|
+
E2_X = $84 ; hazard (M0) X column
|
|
124
|
+
E2_Y = $85 ; hazard TOP scanline
|
|
125
|
+
SPEED = $86 ; current speed 1..6 — drives scroll + descent rate
|
|
126
|
+
SCROLL = $87 ; centre-line dash phase 0..7 (the road's "motion")
|
|
127
|
+
DIST = $88 ; distance score, BCD (digit nibbles fall out free)
|
|
128
|
+
DIST_HI = $89 ; distance score high byte, BCD (hundreds/thousands)
|
|
129
|
+
FRAME = $8A
|
|
130
|
+
SFX_LEFT = $8B ; frames remaining on the voice-0 sound effect
|
|
131
|
+
TUNE_SEL = $8C ; 0 = title jingle, 1 = game-over tune (voice 1)
|
|
132
|
+
TUNE_POS = $8D
|
|
133
|
+
TUNE_LEFT = $8E ; frames left on current jingle note (0 = silent)
|
|
134
|
+
OVER_T = $8F ; game-over auto-return-to-title countdown
|
|
135
|
+
FLASH = $90 ; >0 = crash-flash frames remaining
|
|
136
|
+
SWCHB_PRV = $91 ; previous SWCHB for RESET edge detect
|
|
137
|
+
FIRE_PRV = $92 ; previous fire level (bit7) for fire-edge detect
|
|
138
|
+
EDGEB = $93 ; this frame's RESET press-edge (bit0)
|
|
139
|
+
FIRE_EDG = $94 ; this frame's fire press-edge (bit7)
|
|
140
|
+
TMP = $95
|
|
141
|
+
TICK = $96 ; distance accumulator: +1 score every N frames
|
|
142
|
+
S0BUF = $97 ; 6 rows: packed score digits for the kernel
|
|
143
|
+
SCRATCH = $9D ; 6 bytes general kernel/packer scratch
|
|
144
|
+
DIST_HSV = $A4 ; SESSION hi-score (BCD low byte). RAM only — real
|
|
145
|
+
DIST_HSH = $A5 ; 2600 carts have no battery; honest by design.
|
|
146
|
+
HSBUF = $A6 ; 6 rows: hi-score, packed
|
|
147
|
+
|
|
148
|
+
COL_CAR = $1E ; yellow player car
|
|
149
|
+
COL_RIVAL = $46 ; red rival car (also colours the M0 hazard)
|
|
150
|
+
COL_ROAD = $0E ; white road markings (edges + centre dash)
|
|
151
|
+
COL_TARMAC = $00 ; black tarmac background
|
|
152
|
+
DIST_PERIOD = 8 ; frames per +1 distance at SPEED 1 (faster = more)
|
|
87
153
|
|
|
88
154
|
START:
|
|
89
155
|
SEI
|
|
@@ -92,163 +158,229 @@ START:
|
|
|
92
158
|
TXS
|
|
93
159
|
LDA #0
|
|
94
160
|
.clr:
|
|
95
|
-
STA $00,X
|
|
96
|
-
DEX
|
|
97
|
-
BNE .clr
|
|
161
|
+
STA $00,X ; clears ALL of $00-$FF: zero page RAM AND the TIA
|
|
162
|
+
DEX ; write registers (GRP/ENAxx/HMxx/audio all silenced
|
|
163
|
+
BNE .clr ; — the standard 2600 power-on hygiene)
|
|
98
164
|
|
|
99
|
-
;
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
LDA #50
|
|
103
|
-
STA E1_X
|
|
104
|
-
LDA #170
|
|
105
|
-
STA E1_Y ; enemy car starts up top
|
|
106
|
-
LDA #104
|
|
107
|
-
STA E2_X
|
|
108
|
-
LDA #150
|
|
109
|
-
STA E2_Y
|
|
110
|
-
LDA #1
|
|
111
|
-
STA SPEED
|
|
112
|
-
|
|
113
|
-
; Colours
|
|
114
|
-
LDA #$00 ; black "tarmac" background
|
|
115
|
-
STA COLUBK
|
|
116
|
-
LDA #$1E ; yellow player car
|
|
165
|
+
; Fixed identity colors (the kernels rewrite COLUPF/COLUBK per band, but
|
|
166
|
+
; the car colors are constant all session).
|
|
167
|
+
LDA #COL_CAR
|
|
117
168
|
STA COLUP0
|
|
118
|
-
LDA
|
|
169
|
+
LDA #COL_RIVAL
|
|
119
170
|
STA COLUP1
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
; M0 hazard shares P0 colour normally; we want it to read as debris.
|
|
124
|
-
; Make it 2px wide.
|
|
125
|
-
LDA #%00010000 ; NUSIZ0: missile 2x wide (bits 4-5), P0 single
|
|
171
|
+
; NUSIZ0: single-width car, but make MISSILE 0 (the hazard) 2px wide so it
|
|
172
|
+
; reads as debris, not a hairline. NUSIZ1: single-width rival car.
|
|
173
|
+
LDA #%00010000 ; M0 width = 2px (bits 4-5 = 01); P0 single
|
|
126
174
|
STA NUSIZ0
|
|
175
|
+
LDA #%00000000
|
|
176
|
+
STA NUSIZ1
|
|
127
177
|
|
|
128
|
-
|
|
129
|
-
; SCORE_COLOR priority not needed. CTRLPF bit0 = reflect.
|
|
130
|
-
LDA #%00000001
|
|
131
|
-
STA CTRLPF
|
|
132
|
-
|
|
133
|
-
; Boot chime — confirms TIA audio is wired (engine "rev").
|
|
134
|
-
LDA #$03
|
|
135
|
-
STA AUDC0
|
|
136
|
-
LDA #$0A
|
|
137
|
-
STA AUDF0
|
|
138
|
-
LDA #$0C
|
|
139
|
-
STA AUDV0
|
|
140
|
-
LDA #18
|
|
141
|
-
STA SFX_LEFT
|
|
178
|
+
JSR enter_title
|
|
142
179
|
|
|
180
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
181
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
182
|
+
; THE FRAME LOOP. 262 scanlines, every frame, forever. VBLANK and overscan
|
|
183
|
+
; are timed with the RIOT timer (TIM64T) instead of counted WSYNCs: set the
|
|
184
|
+
; timer, run however much game logic the state needs, then spin on INTIM.
|
|
185
|
+
; This is how shipped 2600 games did it, and it kills the classic homebrew
|
|
186
|
+
; bug class where adding one branch to the game logic emits a 263rd line
|
|
187
|
+
; and the TV loses vsync (rolling picture). The VISIBLE 192 lines are still
|
|
188
|
+
; counted exactly — every STA WSYNC below is one scanline, and each state's
|
|
189
|
+
; kernel accounts for all 192.
|
|
190
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
143
191
|
MAIN:
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
; ── VSYNC (3 lines) ──
|
|
192
|
+
; VSYNC: 3 lines
|
|
147
193
|
LDA #2
|
|
194
|
+
STA VBLANK
|
|
148
195
|
STA VSYNC
|
|
149
196
|
STA WSYNC
|
|
150
197
|
STA WSYNC
|
|
151
198
|
STA WSYNC
|
|
152
199
|
LDA #0
|
|
153
200
|
STA VSYNC
|
|
201
|
+
; 37 lines of VBLANK = 2812 cycles ≈ 43 × 64-cycle timer ticks.
|
|
202
|
+
LDA #43
|
|
203
|
+
STA TIM64T
|
|
204
|
+
|
|
205
|
+
JSR frame_logic ; all game thinking happens in the blanked region
|
|
206
|
+
|
|
207
|
+
; burn whatever VBLANK time the logic didn't use
|
|
208
|
+
.vbwait:
|
|
209
|
+
LDA INTIM
|
|
210
|
+
BNE .vbwait
|
|
211
|
+
STA WSYNC
|
|
212
|
+
|
|
213
|
+
; kernel dispatch — title has its own kernel; play and game-over share one
|
|
214
|
+
LDA STATE
|
|
215
|
+
BNE .ingame
|
|
216
|
+
JMP title_kernel
|
|
217
|
+
.ingame:
|
|
218
|
+
JMP play_kernel
|
|
154
219
|
|
|
155
|
-
|
|
220
|
+
kernel_done:
|
|
221
|
+
; overscan: 30 lines, timer-paced like VBLANK
|
|
156
222
|
LDA #2
|
|
157
223
|
STA VBLANK
|
|
158
|
-
|
|
159
|
-
|
|
224
|
+
LDA #35
|
|
225
|
+
STA TIM64T
|
|
226
|
+
.oswait:
|
|
227
|
+
LDA INTIM
|
|
228
|
+
BNE .oswait
|
|
160
229
|
STA WSYNC
|
|
161
|
-
|
|
162
|
-
BNE .vb
|
|
230
|
+
JMP MAIN
|
|
163
231
|
|
|
164
|
-
|
|
165
|
-
|
|
232
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
233
|
+
; Per-frame logic, dispatched by state. Runs entirely inside the timed
|
|
234
|
+
; VBLANK window (~2800 cycles — an eternity next to the kernel's 76/line).
|
|
235
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
236
|
+
frame_logic:
|
|
237
|
+
INC FRAME
|
|
238
|
+
JSR audio_tick
|
|
239
|
+
|
|
240
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
241
|
+
; Console RESET + fire button are ACTIVE LOW and not debounced; a held
|
|
242
|
+
; RESET would restart every frame. Convert to press-EDGES once per frame:
|
|
243
|
+
; edge = was-released-last-frame AND pressed-now.
|
|
244
|
+
LDA SWCHB
|
|
245
|
+
TAX ; X = current switch levels
|
|
246
|
+
EOR #$FF ; A = pressed-now mask (1 = held)
|
|
247
|
+
AND SWCHB_PRV ; ...that were RELEASED (1) last frame
|
|
248
|
+
STA EDGEB ; bit0 = RESET edge
|
|
249
|
+
STX SWCHB_PRV
|
|
250
|
+
; fire button → same edge treatment in bit7
|
|
251
|
+
LDA #0
|
|
252
|
+
BIT INPT4
|
|
253
|
+
BMI .fup ; bit7 set = not pressed (active low)
|
|
254
|
+
ORA #$80
|
|
255
|
+
.fup:
|
|
256
|
+
TAY ; Y = pressed-now (bit7)
|
|
257
|
+
LDA FIRE_PRV
|
|
258
|
+
EOR #$FF
|
|
259
|
+
STA TMP ; released-last-frame mask
|
|
260
|
+
TYA
|
|
261
|
+
AND TMP
|
|
262
|
+
STA FIRE_EDG ; bit7 = fire press-edge
|
|
263
|
+
STY FIRE_PRV
|
|
264
|
+
|
|
265
|
+
LDA STATE
|
|
266
|
+
BEQ logic_title
|
|
267
|
+
CMP #1
|
|
268
|
+
BEQ logic_play_jmp
|
|
269
|
+
JMP logic_over
|
|
270
|
+
logic_play_jmp:
|
|
271
|
+
JMP logic_play
|
|
272
|
+
|
|
273
|
+
; ── GAME LOGIC (clay — reshape freely) ── title-screen behavior ────────
|
|
274
|
+
logic_title:
|
|
275
|
+
; fire 0 or console RESET starts the game.
|
|
276
|
+
LDA FIRE_EDG
|
|
277
|
+
BMI .start ; bit7 set = fire edge
|
|
278
|
+
LDA EDGEB
|
|
166
279
|
AND #$01
|
|
167
|
-
BNE .
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
;
|
|
280
|
+
BNE .start
|
|
281
|
+
JMP .packtitle
|
|
282
|
+
.start:
|
|
283
|
+
JMP start_game
|
|
284
|
+
.packtitle:
|
|
285
|
+
; Pack the hi-score into the title's display buffer (the kernel just
|
|
286
|
+
; streams bytes — all per-frame thinking happens HERE, in VBLANK, never
|
|
287
|
+
; inside a kernel). We show the low TWO digits of the best distance.
|
|
288
|
+
LDA DIST_HSV
|
|
289
|
+
JSR pack_two_digits
|
|
290
|
+
LDY #0
|
|
291
|
+
.hst:
|
|
292
|
+
LDA SCRATCH,Y ; pack_two_digits left 6 rows in SCRATCH..SCRATCH+5
|
|
293
|
+
STA HSBUF,Y
|
|
294
|
+
INY
|
|
295
|
+
CPY #6
|
|
296
|
+
BNE .hst
|
|
297
|
+
|
|
298
|
+
; title shows no moving objects
|
|
299
|
+
LDA #0
|
|
300
|
+
STA GRP0
|
|
301
|
+
STA GRP1
|
|
302
|
+
STA ENAM0
|
|
303
|
+
RTS
|
|
304
|
+
|
|
305
|
+
; ── GAME LOGIC (clay — reshape freely) ── one frame of the racer ───────
|
|
306
|
+
logic_play:
|
|
307
|
+
LDA EDGEB
|
|
308
|
+
AND #$01 ; console RESET → back to title
|
|
309
|
+
BEQ .noquit
|
|
310
|
+
JMP enter_title
|
|
311
|
+
.noquit:
|
|
312
|
+
|
|
313
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
314
|
+
; SWCHA is ACTIVE LOW (0 = pressed) and must be RE-LOADED for every
|
|
315
|
+
; direction check. The classic bug: caching it in A and chaining ASLs,
|
|
316
|
+
; then clobbering A with game state between shifts — "right works once,
|
|
317
|
+
; left never steers". Fresh LDA SWCHA + AND #mask per check is immune.
|
|
318
|
+
; Joystick 0 lives in the HIGH nibble: bit7 right, bit6 left.
|
|
173
319
|
LDA SWCHA
|
|
174
|
-
AND #$80 ;
|
|
320
|
+
AND #$80 ; joy0 right
|
|
175
321
|
BNE .nr
|
|
176
322
|
LDA P_X
|
|
177
|
-
CMP #128
|
|
323
|
+
CMP #128 ; right road edge
|
|
178
324
|
BCS .nr
|
|
179
325
|
INC P_X
|
|
180
326
|
INC P_X
|
|
181
327
|
.nr:
|
|
182
|
-
LDA SWCHA
|
|
183
|
-
AND #$40 ;
|
|
328
|
+
LDA SWCHA ; RE-LOAD — never trust A to still hold SWCHA
|
|
329
|
+
AND #$40 ; joy0 left
|
|
184
330
|
BNE .nl
|
|
185
331
|
LDA P_X
|
|
186
|
-
CMP #28
|
|
332
|
+
CMP #28 ; left road edge
|
|
187
333
|
BCC .nl
|
|
188
334
|
DEC P_X
|
|
189
335
|
DEC P_X
|
|
190
336
|
.nl:
|
|
191
|
-
.skipmove:
|
|
192
|
-
|
|
193
|
-
; ── Crash flash countdown ──
|
|
194
|
-
LDA FLASH
|
|
195
|
-
BEQ .noflash
|
|
196
|
-
DEC FLASH
|
|
197
|
-
.noflash:
|
|
198
337
|
|
|
199
|
-
; ──
|
|
200
|
-
;
|
|
338
|
+
; ── GAME LOGIC (clay) — road MOTION. No scroll hardware exists, so the
|
|
339
|
+
; centre-line dash phase crawls every frame: subtract SPEED from SCROLL
|
|
340
|
+
; and wrap mod 8. The kernel reads (Y + SCROLL) & 8 to decide whether a
|
|
341
|
+
; dash is lit on each line — so as SCROLL counts down, the lit bands
|
|
342
|
+
; appear to march DOWN the screen toward you = forward speed.
|
|
201
343
|
LDA SCROLL
|
|
202
344
|
SEC
|
|
203
345
|
SBC SPEED
|
|
204
346
|
AND #$07
|
|
205
347
|
STA SCROLL
|
|
206
348
|
|
|
207
|
-
;
|
|
208
|
-
|
|
349
|
+
; crash-flash countdown (cosmetic; the crash itself ends the run below)
|
|
350
|
+
LDA FLASH
|
|
351
|
+
BEQ .noflash
|
|
352
|
+
DEC FLASH
|
|
353
|
+
.noflash:
|
|
354
|
+
|
|
355
|
+
; ── GAME LOGIC (clay) — descend the rival car. Beam-Y counts 192→1 going
|
|
356
|
+
; DOWN the screen, so "moving down toward the player" = SUBTRACT from Y.
|
|
357
|
+
; When it passes the bottom, recycle it to the top in a new (deterministic)
|
|
358
|
+
; lane and bump distance — you survived a car.
|
|
209
359
|
LDA E1_Y
|
|
210
360
|
SEC
|
|
211
361
|
SBC SPEED
|
|
212
362
|
STA E1_Y
|
|
213
|
-
CMP #20 ; passed the bottom?
|
|
363
|
+
CMP #20 ; passed the bottom of the road?
|
|
214
364
|
BCS .e1ok
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
STA E1_Y
|
|
365
|
+
LDA #180
|
|
366
|
+
STA E1_Y ; back to the top
|
|
218
367
|
LDA FRAME
|
|
219
368
|
AND #$3F
|
|
220
369
|
CLC
|
|
221
370
|
ADC #40
|
|
222
|
-
STA E1_X
|
|
223
|
-
INC SCORE ; survived a car
|
|
224
|
-
; ramp speed every time SCORE crosses a multiple of 4 (cap at 6)
|
|
225
|
-
LDA SCORE
|
|
226
|
-
AND #$03
|
|
227
|
-
BNE .e1ok
|
|
228
|
-
LDA SPEED
|
|
229
|
-
CMP #6
|
|
230
|
-
BCS .e1ok
|
|
231
|
-
INC SPEED
|
|
232
|
-
; speed-up "rev" sfx
|
|
233
|
-
LDA #$03
|
|
234
|
-
STA AUDC0
|
|
235
|
-
LDA #$08
|
|
236
|
-
STA AUDF0
|
|
237
|
-
LDA #$0C
|
|
238
|
-
STA AUDV0
|
|
239
|
-
LDA #10
|
|
240
|
-
STA SFX_LEFT
|
|
371
|
+
STA E1_X ; new deterministic lane from FRAME
|
|
241
372
|
.e1ok:
|
|
242
373
|
|
|
243
|
-
;
|
|
374
|
+
; hazard M0 descends a touch faster (SPEED + 1) and recycles likewise.
|
|
244
375
|
LDA E2_Y
|
|
245
376
|
SEC
|
|
246
377
|
SBC SPEED
|
|
247
|
-
|
|
378
|
+
SEC
|
|
379
|
+
SBC #1
|
|
248
380
|
STA E2_Y
|
|
249
381
|
CMP #18
|
|
250
382
|
BCS .e2ok
|
|
251
|
-
LDA #
|
|
383
|
+
LDA #176
|
|
252
384
|
STA E2_Y
|
|
253
385
|
LDA FRAME
|
|
254
386
|
EOR #$5A
|
|
@@ -258,197 +390,478 @@ MAIN:
|
|
|
258
390
|
STA E2_X
|
|
259
391
|
.e2ok:
|
|
260
392
|
|
|
261
|
-
; ──
|
|
262
|
-
;
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
393
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
394
|
+
; CRASH via the TIA's hardware collision LATCHES — the 2600 detects P0/P1
|
|
395
|
+
; and M0/P0 pixel overlap in silicon while it draws; we read the latched
|
|
396
|
+
; result here, one frame late, for free (no AABB math). Rules:
|
|
397
|
+
; * latches accumulate until CXCLR — clear them EVERY frame, or a stale
|
|
398
|
+
; hit from 10 frames ago crashes a car that isn't there;
|
|
399
|
+
; * P0/P1 = CXPPMM bit7 (N flag after BIT); M0/P0 = CXM0P bit6 (V flag).
|
|
400
|
+
BIT CXPPMM ; bit7 (N) = player car overlapped the rival car
|
|
401
|
+
BMI do_crash
|
|
402
|
+
BIT CXM0P ; bit6 (V) = the hazard overlapped the player car
|
|
403
|
+
BVS do_crash
|
|
267
404
|
JMP .nocrash
|
|
268
|
-
|
|
269
|
-
;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
STA
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
;
|
|
405
|
+
do_crash:
|
|
406
|
+
STA CXCLR ; clear the latch BEFORE leaving (next frame is title)
|
|
407
|
+
JMP do_game_over
|
|
408
|
+
.nocrash:
|
|
409
|
+
STA CXCLR ; arm the latches fresh for the frame we're about to draw
|
|
410
|
+
|
|
411
|
+
; ── GAME LOGIC (clay) — distance score. +1 every DIST_PERIOD/SPEED frames
|
|
412
|
+
; (faster speed scores faster). When DIST crosses a multiple of $20 in
|
|
413
|
+
; BCD, ramp SPEED (cap 6): the longer you survive, the harder it gets.
|
|
414
|
+
INC TICK
|
|
415
|
+
LDA SPEED
|
|
416
|
+
STA TMP
|
|
417
|
+
LDA #DIST_PERIOD
|
|
418
|
+
SEC
|
|
419
|
+
SBC TMP ; period = 8 - SPEED → faster cars score faster
|
|
420
|
+
CMP TICK
|
|
421
|
+
BCS .notick
|
|
422
|
+
LDA #0
|
|
423
|
+
STA TICK
|
|
424
|
+
JSR add_distance
|
|
425
|
+
; ramp speed when the tens digit rolls (every 16 distance, capped at 6)
|
|
426
|
+
LDA DIST
|
|
427
|
+
AND #$0F
|
|
428
|
+
BNE .notick
|
|
429
|
+
LDA SPEED
|
|
430
|
+
CMP #6
|
|
431
|
+
BCS .notick
|
|
432
|
+
INC SPEED
|
|
433
|
+
LDA #$08 ; "rev" speed-up blip
|
|
434
|
+
LDX #$03
|
|
435
|
+
LDY #10
|
|
436
|
+
JSR sfx_play
|
|
437
|
+
.notick:
|
|
438
|
+
|
|
439
|
+
JMP pack_score ; render DIST into the kernel's row buffer (tail-RTS)
|
|
440
|
+
|
|
441
|
+
; ── GAME LOGIC (clay — reshape freely) ── game-over freeze-frame ───────
|
|
442
|
+
logic_over:
|
|
443
|
+
LDA EDGEB
|
|
444
|
+
AND #$01
|
|
445
|
+
BNE .toTitle
|
|
446
|
+
LDA FIRE_EDG
|
|
447
|
+
BMI .toTitle
|
|
448
|
+
DEC OVER_T
|
|
449
|
+
BNE .stay
|
|
450
|
+
.toTitle:
|
|
451
|
+
JMP enter_title
|
|
452
|
+
.stay:
|
|
453
|
+
; freeze: hold the traffic where it crashed; flash the tarmac red/black
|
|
283
454
|
LDA FRAME
|
|
284
|
-
AND #$
|
|
455
|
+
AND #$08
|
|
456
|
+
BEQ .flBlack
|
|
457
|
+
LDA #$42 ; dark red
|
|
458
|
+
JMP .flSet
|
|
459
|
+
.flBlack:
|
|
460
|
+
LDA #COL_TARMAC
|
|
461
|
+
.flSet:
|
|
462
|
+
STA FLASH ; reuse FLASH as the freeze-flash color carrier
|
|
463
|
+
RTS
|
|
464
|
+
|
|
465
|
+
; ── GAME LOGIC (clay — reshape freely) ── helpers ──────────────────────
|
|
466
|
+
|
|
467
|
+
add_distance: ; +1 distance, BCD, capped at 9999
|
|
468
|
+
SED
|
|
469
|
+
LDA DIST
|
|
285
470
|
CLC
|
|
286
|
-
ADC
|
|
471
|
+
ADC #$01
|
|
472
|
+
STA DIST
|
|
473
|
+
LDA DIST_HI
|
|
474
|
+
ADC #0 ; carry into the high byte
|
|
475
|
+
STA DIST_HI
|
|
476
|
+
CLD
|
|
477
|
+
; keep the running session hi-score (best distance)
|
|
478
|
+
LDA DIST_HI
|
|
479
|
+
CMP DIST_HSH
|
|
480
|
+
BCC .nohs
|
|
481
|
+
BNE .seths
|
|
482
|
+
LDA DIST
|
|
483
|
+
CMP DIST_HSV
|
|
484
|
+
BCC .nohs
|
|
485
|
+
.seths:
|
|
486
|
+
LDA DIST
|
|
487
|
+
STA DIST_HSV
|
|
488
|
+
LDA DIST_HI
|
|
489
|
+
STA DIST_HSH
|
|
490
|
+
.nohs:
|
|
491
|
+
RTS
|
|
492
|
+
|
|
493
|
+
do_game_over:
|
|
494
|
+
LDA #2
|
|
495
|
+
STA STATE
|
|
496
|
+
LDA #200 ; ~3.3 s freeze, then auto-return to title
|
|
497
|
+
STA OVER_T
|
|
498
|
+
LDA #12
|
|
499
|
+
STA FLASH
|
|
500
|
+
LDA #1
|
|
501
|
+
STA TUNE_SEL
|
|
502
|
+
; crash noise on voice 0 (over the game-over tune on voice 1)
|
|
503
|
+
LDA #$1F
|
|
504
|
+
LDX #$08
|
|
505
|
+
LDY #16
|
|
506
|
+
JSR sfx_play
|
|
507
|
+
JMP tune_start ; game-over tune on voice 1
|
|
508
|
+
|
|
509
|
+
start_game:
|
|
510
|
+
LDA #0
|
|
511
|
+
STA DIST
|
|
512
|
+
STA DIST_HI
|
|
513
|
+
STA TICK
|
|
514
|
+
STA SCROLL
|
|
515
|
+
STA FLASH
|
|
516
|
+
STA TUNE_LEFT ; silence the title jingle
|
|
517
|
+
STA AUDV1
|
|
518
|
+
LDA #1
|
|
519
|
+
STA SPEED
|
|
520
|
+
LDA #76
|
|
521
|
+
STA P_X ; player mid-road, near the bottom
|
|
522
|
+
LDA #50
|
|
287
523
|
STA E1_X
|
|
288
|
-
LDA
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
CLC
|
|
292
|
-
ADC #44
|
|
524
|
+
LDA #150
|
|
525
|
+
STA E1_Y ; rival car starts up top
|
|
526
|
+
LDA #104
|
|
293
527
|
STA E2_X
|
|
294
|
-
LDA
|
|
295
|
-
STA
|
|
296
|
-
LDA
|
|
297
|
-
STA
|
|
298
|
-
LDA #$
|
|
528
|
+
LDA #130
|
|
529
|
+
STA E2_Y
|
|
530
|
+
LDA #1
|
|
531
|
+
STA STATE
|
|
532
|
+
LDA #$06 ; start blip
|
|
533
|
+
LDX #$04
|
|
534
|
+
LDY #10
|
|
535
|
+
JMP sfx_play
|
|
536
|
+
|
|
537
|
+
enter_title:
|
|
538
|
+
LDA #0
|
|
539
|
+
STA STATE
|
|
540
|
+
STA GRP0
|
|
541
|
+
STA GRP1
|
|
542
|
+
STA ENAM0
|
|
299
543
|
STA AUDV0
|
|
300
|
-
LDA #14
|
|
301
544
|
STA SFX_LEFT
|
|
302
|
-
|
|
303
|
-
STA
|
|
545
|
+
STA FLASH
|
|
546
|
+
STA TUNE_SEL ; title jingle
|
|
547
|
+
JMP tune_start
|
|
304
548
|
|
|
305
|
-
|
|
549
|
+
digit_times6: ; A = digit 0-9 → A = digit*6 (DIGITS row index)
|
|
550
|
+
STA TMP
|
|
551
|
+
ASL
|
|
552
|
+
CLC
|
|
553
|
+
ADC TMP ; *3
|
|
554
|
+
ASL ; *6
|
|
555
|
+
RTS
|
|
556
|
+
|
|
557
|
+
; pack_two_digits — A = a BCD byte (two digits). Writes 6 rows into SCRATCH,
|
|
558
|
+
; left digit (high nibble) in PF1 high nibble, right digit (low nibble) in
|
|
559
|
+
; PF1 low nibble. In SCORE mode the byte draws twice (two colors) — the
|
|
560
|
+
; classic dual-score look — but here both halves carry the SAME packed pair.
|
|
561
|
+
pack_two_digits:
|
|
562
|
+
PHA
|
|
563
|
+
LSR
|
|
564
|
+
LSR
|
|
565
|
+
LSR
|
|
566
|
+
LSR ; high (tens) digit
|
|
567
|
+
JSR digit_times6
|
|
568
|
+
TAX
|
|
569
|
+
LDY #0
|
|
570
|
+
.pd0:
|
|
571
|
+
LDA DIGITS,X
|
|
572
|
+
STA SCRATCH,Y ; high nibble of font = left digit
|
|
573
|
+
INX
|
|
574
|
+
INY
|
|
575
|
+
CPY #6
|
|
576
|
+
BNE .pd0
|
|
577
|
+
PLA
|
|
578
|
+
AND #$0F ; low (ones) digit
|
|
579
|
+
JSR digit_times6
|
|
580
|
+
TAX
|
|
581
|
+
LDY #0
|
|
582
|
+
.pd1:
|
|
583
|
+
LDA DIGITS,X
|
|
584
|
+
LSR
|
|
585
|
+
LSR
|
|
586
|
+
LSR
|
|
587
|
+
LSR ; ones in the LOW nibble
|
|
588
|
+
ORA SCRATCH,Y
|
|
589
|
+
STA SCRATCH,Y
|
|
590
|
+
INX
|
|
591
|
+
INY
|
|
592
|
+
CPY #6
|
|
593
|
+
BNE .pd1
|
|
594
|
+
RTS
|
|
595
|
+
|
|
596
|
+
pack_score: ; render the low two DIST digits into S0BUF
|
|
597
|
+
LDA DIST
|
|
598
|
+
JSR pack_two_digits
|
|
599
|
+
LDY #0
|
|
600
|
+
.pks:
|
|
601
|
+
LDA SCRATCH,Y
|
|
602
|
+
STA S0BUF,Y
|
|
603
|
+
INY
|
|
604
|
+
CPY #6
|
|
605
|
+
BNE .pks
|
|
606
|
+
RTS
|
|
607
|
+
|
|
608
|
+
; ── GAME LOGIC (clay — reshape freely) ── TIA sound ────────────────────
|
|
609
|
+
; Voice 0 = one-shot sound effects (engine revs + crash); voice 1 = the
|
|
610
|
+
; jingle player. Separate voices means a rev blip never cuts the tune off.
|
|
611
|
+
sfx_play: ; A = AUDF pitch, X = AUDC waveform, Y = frames
|
|
612
|
+
STA AUDF0
|
|
613
|
+
STX AUDC0
|
|
614
|
+
STY SFX_LEFT
|
|
615
|
+
LDA #$0C
|
|
616
|
+
STA AUDV0
|
|
617
|
+
RTS
|
|
618
|
+
|
|
619
|
+
tune_start: ; TUNE_SEL chosen by caller (0 title, 1 game over)
|
|
620
|
+
LDA #0
|
|
621
|
+
STA TUNE_POS
|
|
622
|
+
JSR tune_note
|
|
623
|
+
LDA #$04 ; pure square wave
|
|
624
|
+
STA AUDC1
|
|
625
|
+
LDA #$06
|
|
626
|
+
STA AUDV1
|
|
627
|
+
LDA #10
|
|
628
|
+
STA TUNE_LEFT
|
|
629
|
+
RTS
|
|
630
|
+
|
|
631
|
+
tune_note: ; load AUDF1 from the selected table at TUNE_POS;
|
|
632
|
+
LDX TUNE_POS ; returns Z set (A=0) on the $FF terminator
|
|
633
|
+
LDA TUNE_SEL
|
|
634
|
+
BNE .tn1
|
|
635
|
+
LDA TITLE_TUNE,X
|
|
636
|
+
JMP .tn2
|
|
637
|
+
.tn1:
|
|
638
|
+
LDA OVER_TUNE,X
|
|
639
|
+
.tn2:
|
|
640
|
+
CMP #$FF
|
|
641
|
+
BEQ .tnEnd
|
|
642
|
+
STA AUDF1
|
|
643
|
+
LDA #1
|
|
644
|
+
RTS
|
|
645
|
+
.tnEnd:
|
|
646
|
+
LDA #0
|
|
647
|
+
STA AUDV1
|
|
648
|
+
RTS
|
|
649
|
+
|
|
650
|
+
audio_tick: ; called once per frame, every state
|
|
306
651
|
LDA SFX_LEFT
|
|
307
|
-
BEQ .
|
|
652
|
+
BEQ .at1
|
|
308
653
|
DEC SFX_LEFT
|
|
309
|
-
BNE .
|
|
654
|
+
BNE .at1
|
|
310
655
|
LDA #0
|
|
311
|
-
STA AUDV0
|
|
312
|
-
.
|
|
656
|
+
STA AUDV0 ; sfx finished → silence voice 0
|
|
657
|
+
.at1:
|
|
658
|
+
LDA TUNE_LEFT
|
|
659
|
+
BEQ .at2
|
|
660
|
+
DEC TUNE_LEFT
|
|
661
|
+
BNE .at2
|
|
662
|
+
INC TUNE_POS
|
|
663
|
+
JSR tune_note
|
|
664
|
+
BEQ .at2 ; hit the terminator → tune stays off
|
|
665
|
+
LDA #10
|
|
666
|
+
STA TUNE_LEFT
|
|
667
|
+
.at2:
|
|
668
|
+
RTS
|
|
313
669
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
670
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
671
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
672
|
+
; OBJECT POSITIONING — the canonical SBC-#15 beam-race. There is no "X
|
|
673
|
+
; register" for sprites: you strobe RESPx/RESM0 and the object lands
|
|
674
|
+
; WHEREVER THE BEAM IS. Each SBC/BCS lap is 5 CPU cycles = 15 beam pixels,
|
|
675
|
+
; so when the subtraction underflows the beam has crossed x/15 coarse
|
|
676
|
+
; columns; the remainder, EOR #7 shifted to the high nibble, becomes the
|
|
677
|
+
; ±7px fine offset HMOVE applies on the next line. The naive "divide first,
|
|
678
|
+
; then burn a delay loop" version lands in the WRONG column. Three objects =
|
|
679
|
+
; three WSYNC lines + one shared HMOVE line, all inside timed VBLANK.
|
|
680
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
681
|
+
position_objects:
|
|
317
682
|
STA WSYNC
|
|
318
683
|
STA HMCLR
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
.p0pos:
|
|
322
|
-
CPX #15
|
|
323
|
-
BCC .p0done
|
|
684
|
+
LDA P_X ; player car → P0
|
|
685
|
+
STA WSYNC
|
|
324
686
|
SEC
|
|
687
|
+
.d0:
|
|
325
688
|
SBC #15
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
689
|
+
BCS .d0
|
|
690
|
+
EOR #7
|
|
691
|
+
ASL
|
|
692
|
+
ASL
|
|
693
|
+
ASL
|
|
694
|
+
ASL
|
|
329
695
|
STA RESP0
|
|
330
|
-
|
|
696
|
+
STA HMP0
|
|
697
|
+
LDA E1_X ; rival car → P1
|
|
331
698
|
STA WSYNC
|
|
332
|
-
LDX E1_X
|
|
333
|
-
LDA #0
|
|
334
|
-
.p1pos:
|
|
335
|
-
CPX #15
|
|
336
|
-
BCC .p1done
|
|
337
699
|
SEC
|
|
700
|
+
.d1:
|
|
338
701
|
SBC #15
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
702
|
+
BCS .d1
|
|
703
|
+
EOR #7
|
|
704
|
+
ASL
|
|
705
|
+
ASL
|
|
706
|
+
ASL
|
|
707
|
+
ASL
|
|
342
708
|
STA RESP1
|
|
343
|
-
|
|
709
|
+
STA HMP1
|
|
710
|
+
LDA E2_X ; hazard → M0
|
|
344
711
|
STA WSYNC
|
|
345
|
-
LDX E2_X
|
|
346
|
-
LDA #0
|
|
347
|
-
.m0pos:
|
|
348
|
-
CPX #15
|
|
349
|
-
BCC .m0done
|
|
350
712
|
SEC
|
|
713
|
+
.d2:
|
|
351
714
|
SBC #15
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
715
|
+
BCS .d2
|
|
716
|
+
EOR #7
|
|
717
|
+
ASL
|
|
718
|
+
ASL
|
|
719
|
+
ASL
|
|
720
|
+
ASL
|
|
355
721
|
STA RESM0
|
|
356
|
-
STA
|
|
722
|
+
STA HMM0
|
|
723
|
+
STA WSYNC
|
|
724
|
+
STA HMOVE ; one HMOVE applies ALL the fine offsets; it must
|
|
725
|
+
RTS ; come fresh after a WSYNC (mid-line HMOVE combs)
|
|
357
726
|
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
727
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
728
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
729
|
+
; THE PLAY/GAME-OVER KERNEL — 192 visible lines, fully accounted:
|
|
730
|
+
; 24 = score bar + 168 = road (84 two-line passes)
|
|
731
|
+
;
|
|
732
|
+
; SCORE BAR (SCORE mode): CTRLPF = $02 colors the LEFT playfield half with
|
|
733
|
+
; COLUP0 and the RIGHT half with COLUP1 — a two-color scoreboard with zero
|
|
734
|
+
; sprites. We stream the packed distance digits into PF1, one font row per
|
|
735
|
+
; 4 scanlines.
|
|
736
|
+
;
|
|
737
|
+
; ROAD (two-line kernel): one line of road work — road edges (PF0/PF2) +
|
|
738
|
+
; scrolling centre dash (PF) + ONE car test — is ~80+ cycles, more than a
|
|
739
|
+
; single 76-cycle scanline allows. So each loop pass spans TWO scanlines:
|
|
740
|
+
; line A draws the road playfield (rails + centre dash) + the player car;
|
|
741
|
+
; line B draws the rival car (P1) + the hazard (M0).
|
|
742
|
+
; 84 passes × 2 = 168 lines; objects move in 2-px steps (invisible on 1977
|
|
743
|
+
; televisions). The road's MOTION is in the dash phase (SCROLL), updated in
|
|
744
|
+
; VBLANK — the kernel only READS it; never animate inside a kernel.
|
|
745
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
746
|
+
play_kernel:
|
|
747
|
+
; positioning runs first, inside the still-blanked region
|
|
748
|
+
JSR position_objects
|
|
368
749
|
|
|
750
|
+
LDA #COL_TARMAC
|
|
751
|
+
STA COLUBK
|
|
369
752
|
LDA #0
|
|
370
|
-
STA
|
|
753
|
+
STA PF0
|
|
754
|
+
STA PF1
|
|
755
|
+
STA PF2
|
|
756
|
+
STA GRP0
|
|
757
|
+
STA GRP1
|
|
758
|
+
STA ENAM0
|
|
759
|
+
STA VBLANK ; beam on
|
|
760
|
+
LDA #$0E
|
|
761
|
+
STA COLUPF ; score digits bright
|
|
762
|
+
LDA #$02
|
|
763
|
+
STA CTRLPF ; SCORE mode for the score bar
|
|
764
|
+
|
|
765
|
+
; ---- score bar: 24 lines (6 font rows × 4) ----
|
|
766
|
+
; S0BUF was packed in logic_play (VBLANK); stream it here. Two visible
|
|
767
|
+
; digits (tens/ones of DIST), doubled by SCORE mode into two colors.
|
|
768
|
+
LDX #0
|
|
769
|
+
.sbar:
|
|
770
|
+
STA WSYNC
|
|
771
|
+
TXA
|
|
772
|
+
LSR
|
|
773
|
+
LSR
|
|
774
|
+
TAY ; row = line/4
|
|
775
|
+
LDA S0BUF,Y
|
|
776
|
+
STA PF1
|
|
777
|
+
INX
|
|
778
|
+
CPX #24
|
|
779
|
+
BNE .sbar
|
|
780
|
+
|
|
781
|
+
; transition: clear the bar, switch the TIA to the road. CTRLPF bit0
|
|
782
|
+
; (reflect) MIRRORS the 20-pixel playfield so the left rail draws a
|
|
783
|
+
; matching right rail for free — the road is symmetric. COLUPF = the
|
|
784
|
+
; white road markings; COLUBK = black tarmac.
|
|
785
|
+
STA WSYNC
|
|
786
|
+
LDA #$01
|
|
787
|
+
STA CTRLPF ; reflect mode: symmetric rails
|
|
788
|
+
LDA #COL_ROAD
|
|
789
|
+
STA COLUPF
|
|
790
|
+
LDA #COL_TARMAC
|
|
791
|
+
STA COLUBK
|
|
371
792
|
|
|
372
|
-
;
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
;
|
|
376
|
-
; Y counts 192 -> 2 in steps of 2. 96 passes = 192 lines.
|
|
377
|
-
LDY #192
|
|
378
|
-
.draw:
|
|
379
|
-
; ---- line A: road + player car ----
|
|
793
|
+
; ---- road: Y from 168 down to 1 (84 two-line passes) ----
|
|
794
|
+
LDY #168
|
|
795
|
+
.road:
|
|
796
|
+
; ============ line A: road playfield + player car ============
|
|
380
797
|
STA WSYNC
|
|
381
|
-
; Road
|
|
382
|
-
;
|
|
383
|
-
|
|
384
|
-
; the right edge by CTRLPF reflect. A constant rail every line.
|
|
385
|
-
LDA #%00010000 ; one rail bar on each side
|
|
798
|
+
; Road EDGES: PF0 high nibble bit4 is the leftmost visible PF pixel; one
|
|
799
|
+
; rail bar there, mirrored by reflect to the right edge. Constant rails.
|
|
800
|
+
LDA #%00010000
|
|
386
801
|
STA PF0
|
|
387
|
-
; Centre
|
|
388
|
-
;
|
|
802
|
+
; Centre DASH via PF2 — lit on some line groups, phased by SCROLL so the
|
|
803
|
+
; lit bands crawl DOWN the screen (forward motion). (Y + SCROLL) & 8.
|
|
389
804
|
TYA
|
|
390
805
|
CLC
|
|
391
806
|
ADC SCROLL
|
|
392
807
|
AND #%00001000
|
|
393
808
|
BEQ .nodash
|
|
394
|
-
LDA #%00011000 ; centre pixels
|
|
809
|
+
LDA #%00011000 ; centre-ish PF2 pixels = the lane dash
|
|
395
810
|
STA PF2
|
|
396
811
|
JMP .dashdone
|
|
397
812
|
.nodash:
|
|
398
813
|
LDA #0
|
|
399
814
|
STA PF2
|
|
400
815
|
.dashdone:
|
|
401
|
-
; Player car: 8 rows
|
|
402
|
-
; Window: (Y - 22) in [0..7] → index CAR bitmap.
|
|
816
|
+
; Player car: 8 rows in the bottom band (Y in [22,30)).
|
|
403
817
|
TYA
|
|
404
818
|
SEC
|
|
405
819
|
SBC #22
|
|
406
820
|
CMP #8
|
|
407
|
-
BCS .
|
|
821
|
+
BCS .noPlayer
|
|
408
822
|
TAX
|
|
409
823
|
LDA CAR,X
|
|
410
824
|
STA GRP0
|
|
411
|
-
JMP .
|
|
412
|
-
.
|
|
825
|
+
JMP .playerDone
|
|
826
|
+
.noPlayer:
|
|
413
827
|
LDA #0
|
|
414
828
|
STA GRP0
|
|
415
|
-
.
|
|
416
|
-
|
|
417
|
-
; ---- line B: enemy car + hazard ----
|
|
829
|
+
.playerDone:
|
|
830
|
+
; ============ line B: rival car + hazard ============
|
|
418
831
|
STA WSYNC
|
|
419
|
-
;
|
|
832
|
+
; Rival car P1: 8 rows starting at E1_Y.
|
|
420
833
|
TYA
|
|
421
834
|
SEC
|
|
422
835
|
SBC E1_Y
|
|
423
836
|
CMP #8
|
|
424
|
-
BCS .
|
|
837
|
+
BCS .noRival
|
|
425
838
|
TAX
|
|
426
839
|
LDA CAR,X
|
|
427
840
|
STA GRP1
|
|
428
|
-
JMP .
|
|
429
|
-
.
|
|
841
|
+
JMP .rivalDone
|
|
842
|
+
.noRival:
|
|
430
843
|
LDA #0
|
|
431
844
|
STA GRP1
|
|
432
|
-
.
|
|
433
|
-
; Hazard M0:
|
|
845
|
+
.rivalDone:
|
|
846
|
+
; Hazard M0: enabled for 4 rows at E2_Y (0..3 from its top).
|
|
434
847
|
TYA
|
|
435
848
|
SEC
|
|
436
849
|
SBC E2_Y
|
|
437
850
|
CMP #4
|
|
438
|
-
BCS .
|
|
851
|
+
BCS .noHaz
|
|
439
852
|
LDA #2
|
|
440
853
|
STA ENAM0
|
|
441
|
-
JMP .
|
|
442
|
-
.
|
|
854
|
+
JMP .hazDone
|
|
855
|
+
.noHaz:
|
|
443
856
|
LDA #0
|
|
444
857
|
STA ENAM0
|
|
445
|
-
.
|
|
858
|
+
.hazDone:
|
|
446
859
|
|
|
447
860
|
DEY
|
|
448
861
|
DEY
|
|
449
|
-
BNE .
|
|
862
|
+
BNE .road
|
|
450
863
|
|
|
451
|
-
;
|
|
864
|
+
; clear the playfield so it doesn't bleed into overscan
|
|
452
865
|
LDA #0
|
|
453
866
|
STA PF0
|
|
454
867
|
STA PF1
|
|
@@ -456,17 +869,180 @@ MAIN:
|
|
|
456
869
|
STA GRP0
|
|
457
870
|
STA GRP1
|
|
458
871
|
STA ENAM0
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
872
|
+
JMP kernel_done
|
|
873
|
+
|
|
874
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
875
|
+
; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
876
|
+
; THE TITLE KERNEL — 192 lines, banded:
|
|
877
|
+
; 24 blank + 28 banner "SWERVE" + 8 gap + 28 banner "STREAK" + 16 gap +
|
|
878
|
+
; 24 hi-score + remainder pad = 192
|
|
879
|
+
;
|
|
880
|
+
; The banner is an ASYMMETRIC PLAYFIELD — the 2600's only way to draw
|
|
881
|
+
; full-width artwork. The playfield registers hold just 20 pixels; the TIA
|
|
882
|
+
; replays them for the right half of the line (CTRLPF bit0 chooses repeat
|
|
883
|
+
; or mirror). For 40 INDEPENDENT pixels you rewrite all three registers
|
|
884
|
+
; mid-line, each inside its window (CPU cycle = 3 color clocks; left copy
|
|
885
|
+
; reads at clocks 68-147, right copy at 148-227):
|
|
886
|
+
; PF0 again after cycle ~28 (left copy drawn) before ~49 (right copy reads)
|
|
887
|
+
; PF1 again after cycle ~39 before ~54
|
|
888
|
+
; PF2 again after cycle ~50 before ~65
|
|
889
|
+
; The code below hits those windows by instruction order alone — count
|
|
890
|
+
; cycles before you reorder ANYTHING between the WSYNC and the last STA.
|
|
891
|
+
; REQUIRES: CTRLPF bit0 = 0 (repeat mode). In mirror mode the right half
|
|
892
|
+
; reads the registers in REVERSE order and every window above is wrong.
|
|
893
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
894
|
+
title_kernel:
|
|
895
|
+
LDA #$94 ; deep slate-blue backdrop (the night road)
|
|
896
|
+
STA COLUBK
|
|
897
|
+
LDA #0
|
|
898
|
+
STA PF0
|
|
899
|
+
STA PF1
|
|
900
|
+
STA PF2
|
|
901
|
+
STA GRP0
|
|
902
|
+
STA GRP1
|
|
903
|
+
STA ENAM0
|
|
904
|
+
STA CTRLPF ; REPEAT mode — required by the banner (see above)
|
|
905
|
+
STA VBLANK ; beam on
|
|
906
|
+
|
|
907
|
+
LDX #24 ; band 1: 24 blank lines
|
|
908
|
+
.tb1:
|
|
463
909
|
STA WSYNC
|
|
464
910
|
DEX
|
|
465
|
-
BNE .
|
|
911
|
+
BNE .tb1
|
|
466
912
|
|
|
467
|
-
|
|
913
|
+
LDA #$9E ; word 1 in light blue
|
|
914
|
+
STA COLUPF
|
|
915
|
+
LDX #0 ; band 2: 28 banner lines (7 rows × 4)
|
|
916
|
+
.ban1:
|
|
917
|
+
STA WSYNC
|
|
918
|
+
TXA ; row = line/4
|
|
919
|
+
LSR
|
|
920
|
+
LSR
|
|
921
|
+
TAY
|
|
922
|
+
LDA R1_PF0L,Y
|
|
923
|
+
STA PF0
|
|
924
|
+
LDA R1_PF1L,Y
|
|
925
|
+
STA PF1
|
|
926
|
+
LDA R1_PF2L,Y
|
|
927
|
+
STA PF2
|
|
928
|
+
LDA R1_PF0R,Y
|
|
929
|
+
STA PF0
|
|
930
|
+
LDA R1_PF1R,Y
|
|
931
|
+
STA PF1
|
|
932
|
+
NOP
|
|
933
|
+
NOP
|
|
934
|
+
LDA R1_PF2R,Y
|
|
935
|
+
STA PF2
|
|
936
|
+
INX
|
|
937
|
+
CPX #28
|
|
938
|
+
BNE .ban1
|
|
939
|
+
|
|
940
|
+
STA WSYNC ; band 3: clear + 7 gap lines
|
|
941
|
+
LDA #0
|
|
942
|
+
STA PF0
|
|
943
|
+
STA PF1
|
|
944
|
+
STA PF2
|
|
945
|
+
LDX #7
|
|
946
|
+
.tb3:
|
|
947
|
+
STA WSYNC
|
|
948
|
+
DEX
|
|
949
|
+
BNE .tb3
|
|
468
950
|
|
|
469
|
-
;
|
|
951
|
+
LDA #$1E ; word 2 in yellow (the headlights)
|
|
952
|
+
STA COLUPF
|
|
953
|
+
LDX #0 ; band 4: 28 banner lines, word 2
|
|
954
|
+
.ban2:
|
|
955
|
+
STA WSYNC
|
|
956
|
+
TXA
|
|
957
|
+
LSR
|
|
958
|
+
LSR
|
|
959
|
+
TAY
|
|
960
|
+
LDA R2_PF0L,Y
|
|
961
|
+
STA PF0
|
|
962
|
+
LDA R2_PF1L,Y
|
|
963
|
+
STA PF1
|
|
964
|
+
LDA R2_PF2L,Y
|
|
965
|
+
STA PF2
|
|
966
|
+
LDA R2_PF0R,Y
|
|
967
|
+
STA PF0
|
|
968
|
+
LDA R2_PF1R,Y
|
|
969
|
+
STA PF1
|
|
970
|
+
NOP
|
|
971
|
+
NOP
|
|
972
|
+
LDA R2_PF2R,Y
|
|
973
|
+
STA PF2
|
|
974
|
+
INX
|
|
975
|
+
CPX #28
|
|
976
|
+
BNE .ban2
|
|
977
|
+
|
|
978
|
+
STA WSYNC ; band 5: clear + 15 gap lines
|
|
979
|
+
LDA #0
|
|
980
|
+
STA PF0
|
|
981
|
+
STA PF1
|
|
982
|
+
STA PF2
|
|
983
|
+
LDA #$02
|
|
984
|
+
STA CTRLPF ; SCORE mode for the hi-score band
|
|
985
|
+
LDX #15
|
|
986
|
+
.tb5:
|
|
987
|
+
STA WSYNC
|
|
988
|
+
DEX
|
|
989
|
+
BNE .tb5
|
|
990
|
+
|
|
991
|
+
; band 6: hi-score, 24 lines (6 rows × 4). Packed digits stream into PF1;
|
|
992
|
+
; SCORE mode draws them twice in the two player colors. In-session best
|
|
993
|
+
; DISTANCE; honest: there is no battery — gone at power-off, like the
|
|
994
|
+
; arcades.
|
|
995
|
+
LDX #0
|
|
996
|
+
.hsb:
|
|
997
|
+
STA WSYNC
|
|
998
|
+
TXA
|
|
999
|
+
LSR
|
|
1000
|
+
LSR
|
|
1001
|
+
TAY
|
|
1002
|
+
LDA HSBUF,Y
|
|
1003
|
+
STA PF1
|
|
1004
|
+
INX
|
|
1005
|
+
CPX #24
|
|
1006
|
+
BNE .hsb
|
|
1007
|
+
|
|
1008
|
+
STA WSYNC ; band 7: clear + pad to exactly 192
|
|
1009
|
+
LDA #0
|
|
1010
|
+
STA PF1
|
|
1011
|
+
LDX #65
|
|
1012
|
+
.tb7:
|
|
1013
|
+
STA WSYNC
|
|
1014
|
+
DEX
|
|
1015
|
+
BNE .tb7
|
|
1016
|
+
|
|
1017
|
+
JMP kernel_done
|
|
1018
|
+
|
|
1019
|
+
; ──────────────────────────────────────────────────────────────────────
|
|
1020
|
+
; ── GAME LOGIC (clay — reshape freely) ── data tables ──────────────────
|
|
1021
|
+
; Digit font: 4 pixels wide × 6 rows, stored in the HIGH nibble (PF1 bit7
|
|
1022
|
+
; is the LEFTMOST pixel of the left playfield half — high nibble = left).
|
|
1023
|
+
DIGITS:
|
|
1024
|
+
.byte $60,$90,$90,$90,$90,$60 ; 0
|
|
1025
|
+
.byte $20,$60,$20,$20,$20,$70 ; 1
|
|
1026
|
+
.byte $60,$90,$10,$20,$40,$F0 ; 2
|
|
1027
|
+
.byte $E0,$10,$60,$10,$10,$E0 ; 3
|
|
1028
|
+
.byte $90,$90,$F0,$10,$10,$10 ; 4
|
|
1029
|
+
.byte $F0,$80,$E0,$10,$10,$E0 ; 5
|
|
1030
|
+
.byte $60,$80,$E0,$90,$90,$60 ; 6
|
|
1031
|
+
.byte $F0,$10,$20,$40,$40,$40 ; 7
|
|
1032
|
+
.byte $60,$90,$60,$90,$90,$60 ; 8
|
|
1033
|
+
.byte $60,$90,$90,$70,$10,$60 ; 9
|
|
1034
|
+
|
|
1035
|
+
; Title jingle (voice 1, AUDC $04 square; AUDF divider — LOWER = higher
|
|
1036
|
+
; pitch; 10 frames per note; $FF terminates). The table IS the song.
|
|
1037
|
+
TITLE_TUNE:
|
|
1038
|
+
.byte $0F,$0C,$09,$0C,$0F,$13,$0F,$0C,$FF
|
|
1039
|
+
; Game-over tune: a falling figure.
|
|
1040
|
+
OVER_TUNE:
|
|
1041
|
+
.byte $09,$0C,$0F,$13,$17,$1B,$FF
|
|
1042
|
+
|
|
1043
|
+
; ── THE CAR SPRITE ────────────────────────────────────────────────────
|
|
1044
|
+
; 8 rows: a forward-view car silhouette (cabin + body + wheels), drawn for
|
|
1045
|
+
; the player via P0 and for the rival via P1 (same bitmap, different color).
|
|
470
1046
|
CAR:
|
|
471
1047
|
.byte %00111100
|
|
472
1048
|
.byte %01111110
|
|
@@ -477,7 +1053,62 @@ CAR:
|
|
|
477
1053
|
.byte %01011010
|
|
478
1054
|
.byte %01111110
|
|
479
1055
|
|
|
480
|
-
|
|
1056
|
+
; ── THE TITLE BANNER ──────────────────────────────────────────────────
|
|
1057
|
+
; 40-pixel-wide artwork, 7 rows per word, drawn by the asymmetric-playfield
|
|
1058
|
+
; kernel above. Each row is six bytes across six tables (left PF0/PF1/PF2,
|
|
1059
|
+
; right PF0/PF1/PF2). PF bit order is the 2600's great prank — three
|
|
1060
|
+
; registers, three different orders:
|
|
1061
|
+
; PF0: only bits 4-7 used, bit 4 = LEFTMOST pixel (reversed)
|
|
1062
|
+
; PF1: bit 7 = leftmost (normal)
|
|
1063
|
+
; PF2: bit 0 = leftmost (reversed again)
|
|
1064
|
+
;
|
|
1065
|
+
; The 40-px art for each row is the comment ASCII above each table; the
|
|
1066
|
+
; bytes are mechanically encoded from it (left half = pixels 0..19 → PF0
|
|
1067
|
+
; bits4-7 / PF1 bits7-0 / PF2 bits0-7; right half = pixels 20..39 likewise).
|
|
1068
|
+
;
|
|
1069
|
+
; SWERVE:
|
|
1070
|
+
; .####..#...#.####.####..#...#.####.......
|
|
1071
|
+
; .#.....#...#.#....#...#.#...#.#...........
|
|
1072
|
+
; .#.....#...#.#....#...#.#...#.#...........
|
|
1073
|
+
; .####..#.#.#.###..####..#.#..####........
|
|
1074
|
+
; ....#..#.#.#.#....#.#...#.#..#............
|
|
1075
|
+
; .#..#..#.#.#.#....#..#...#...#............
|
|
1076
|
+
; .####...#.#..####.#...#..#...####........
|
|
1077
|
+
R1_PF0L:
|
|
1078
|
+
.byte %11100000, %00100000, %00100000, %11100000, %00000000, %00100000, %11100000
|
|
1079
|
+
R1_PF1L:
|
|
1080
|
+
.byte %10010001, %00010001, %00010001, %10010101, %10010101, %10010101, %10001010
|
|
1081
|
+
R1_PF2L:
|
|
1082
|
+
.byte %11011110, %01000010, %01000010, %11001110, %01000010, %01000010, %01011110
|
|
1083
|
+
R1_PF0R:
|
|
1084
|
+
.byte %00110000, %01000000, %01000000, %00110000, %00010000, %00100000, %01000000
|
|
1085
|
+
R1_PF1R:
|
|
1086
|
+
.byte %10001011, %10001010, %10001010, %10100111, %10100100, %01000100, %01000111
|
|
1087
|
+
R1_PF2R:
|
|
1088
|
+
.byte %00000011, %00000000, %00000000, %00000001, %00000000, %00000000, %00000001
|
|
1089
|
+
|
|
1090
|
+
; STREAK:
|
|
1091
|
+
; .####..#####.####..####..###..#...#......
|
|
1092
|
+
; .#.......#...#...#.#.....#...#.#..#.......
|
|
1093
|
+
; .#.......#...#...#.#.....#...#.#.#........
|
|
1094
|
+
; .####....#...####..###...#####.##........
|
|
1095
|
+
; ....#....#...#.#...#.....#...#.#.#........
|
|
1096
|
+
; .#..#....#...#..#..#.....#...#.#..#.......
|
|
1097
|
+
; .####....#...#...#.####..#...#.#...#......
|
|
1098
|
+
R2_PF0L:
|
|
1099
|
+
.byte %11100000, %00100000, %00100000, %11100000, %00000000, %00100000, %11100000
|
|
1100
|
+
R2_PF1L:
|
|
1101
|
+
.byte %10011111, %00000100, %00000100, %10000100, %10000100, %10000100, %10000100
|
|
1102
|
+
R2_PF2L:
|
|
1103
|
+
.byte %10011110, %10100010, %10100010, %10011110, %10001010, %10010010, %10100010
|
|
1104
|
+
R2_PF0R:
|
|
1105
|
+
.byte %01110000, %00000000, %00000000, %00110000, %00000000, %00000000, %01110000
|
|
1106
|
+
R2_PF1R:
|
|
1107
|
+
.byte %01110010, %01000101, %01000101, %01111101, %01000101, %01000101, %01000101
|
|
1108
|
+
R2_PF2R:
|
|
1109
|
+
.byte %00000100, %00000100, %00000010, %00000001, %00000010, %00000100, %00001000
|
|
1110
|
+
|
|
1111
|
+
; ── Vector table ──────────────────────────────────────────────────────
|
|
481
1112
|
org $FFFA
|
|
482
1113
|
.word START
|
|
483
1114
|
.word START
|