romdevtools 0.28.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -0
  3. package/README.md +3 -3
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1225 -332
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +906 -275
  8. package/examples/atari2600/templates/shmup.asm +1031 -239
  9. package/examples/atari2600/templates/sports.asm +1135 -253
  10. package/examples/atari7800/templates/platformer.c +991 -156
  11. package/examples/atari7800/templates/puzzle.c +1091 -148
  12. package/examples/atari7800/templates/racing.c +952 -124
  13. package/examples/atari7800/templates/shmup.c +812 -134
  14. package/examples/atari7800/templates/sports.c +820 -184
  15. package/examples/c64/templates/platformer.c +879 -164
  16. package/examples/c64/templates/puzzle.c +855 -178
  17. package/examples/c64/templates/racing.c +873 -97
  18. package/examples/c64/templates/shmup.c +757 -161
  19. package/examples/c64/templates/sports.c +755 -100
  20. package/examples/gb/templates/platformer.c +841 -179
  21. package/examples/gb/templates/puzzle.c +986 -246
  22. package/examples/gb/templates/racing.c +754 -174
  23. package/examples/gb/templates/shmup.c +673 -175
  24. package/examples/gb/templates/sports.c +790 -159
  25. package/examples/gba/templates/platformer.c +626 -165
  26. package/examples/gba/templates/puzzle.c +519 -269
  27. package/examples/gba/templates/racing.c +511 -206
  28. package/examples/gba/templates/shmup.c +564 -179
  29. package/examples/gba/templates/sports.c +454 -174
  30. package/examples/gbc/templates/platformer.c +944 -180
  31. package/examples/gbc/templates/puzzle.c +363 -109
  32. package/examples/gbc/templates/racing.c +884 -180
  33. package/examples/gbc/templates/shmup.c +821 -185
  34. package/examples/gbc/templates/sports.c +870 -162
  35. package/examples/genesis/templates/platformer.c +747 -129
  36. package/examples/genesis/templates/puzzle.c +694 -261
  37. package/examples/genesis/templates/racing.c +726 -203
  38. package/examples/genesis/templates/shmup.c +535 -142
  39. package/examples/genesis/templates/sports.c +495 -158
  40. package/examples/gg/templates/platformer.c +880 -215
  41. package/examples/gg/templates/puzzle.c +875 -216
  42. package/examples/gg/templates/racing.c +915 -172
  43. package/examples/gg/templates/shmup.c +714 -191
  44. package/examples/gg/templates/sports.c +732 -129
  45. package/examples/lynx/templates/platformer.c +604 -69
  46. package/examples/lynx/templates/puzzle.c +498 -158
  47. package/examples/lynx/templates/racing.c +538 -102
  48. package/examples/lynx/templates/shmup.c +458 -131
  49. package/examples/lynx/templates/sports.c +496 -72
  50. package/examples/msx/platformer/main.c +649 -162
  51. package/examples/msx/puzzle/main.c +742 -240
  52. package/examples/msx/racing/main.c +669 -178
  53. package/examples/msx/shmup/main.c +460 -178
  54. package/examples/msx/sports/main.c +592 -126
  55. package/examples/nes/templates/platformer.c +589 -171
  56. package/examples/nes/templates/puzzle.c +563 -242
  57. package/examples/nes/templates/racing.c +502 -208
  58. package/examples/nes/templates/shmup.c +339 -145
  59. package/examples/nes/templates/sports.c +341 -183
  60. package/examples/pce/platformer/main.c +874 -205
  61. package/examples/pce/puzzle/main.c +802 -287
  62. package/examples/pce/racing/main.c +783 -208
  63. package/examples/pce/shmup/main.c +638 -212
  64. package/examples/pce/sports/main.c +586 -169
  65. package/examples/porting-across-platforms/README.md +1 -1
  66. package/examples/sms/templates/platformer.c +762 -177
  67. package/examples/sms/templates/puzzle.c +752 -212
  68. package/examples/sms/templates/racing.c +808 -145
  69. package/examples/sms/templates/shmup.c +599 -162
  70. package/examples/sms/templates/sports.c +630 -122
  71. package/examples/snes/templates/music_demo.c +7 -0
  72. package/examples/snes/templates/platformer-data.asm +123 -24
  73. package/examples/snes/templates/platformer-hdr.asm +57 -0
  74. package/examples/snes/templates/platformer.c +586 -165
  75. package/examples/snes/templates/puzzle-data.asm +116 -21
  76. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  77. package/examples/snes/templates/puzzle.c +614 -235
  78. package/examples/snes/templates/racing-data.asm +390 -32
  79. package/examples/snes/templates/racing-hdr.asm +57 -0
  80. package/examples/snes/templates/racing.c +807 -196
  81. package/examples/snes/templates/shmup-data.asm +87 -29
  82. package/examples/snes/templates/shmup-hdr.asm +57 -0
  83. package/examples/snes/templates/shmup.c +459 -198
  84. package/examples/snes/templates/sports-data.asm +48 -2
  85. package/examples/snes/templates/sports-hdr.asm +57 -0
  86. package/examples/snes/templates/sports.c +414 -163
  87. package/package.json +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -1,41 +1,75 @@
1
- ; ── racing.asm — Atari 2600 RACING genre scaffold (top-down) ──────────
1
+ ; ── racing.asm — SWERVE STREAK — Atari 2600 road racer (complete game) ───────
2
2
  ;
3
- ; The 2600 had a deep racing catalogue — Enduro, Indy 500, Night Driver,
4
- ; Pole Position, Grand Prix. This scaffold is the HONEST 2600 racer: a
5
- ; TOP-DOWN, vertically-scrolling lane racer (the same idiom Enduro uses),
6
- ; NOT a pseudo-3D road projecting a 3D road needs a per-line table the
7
- ; 4 KB/76-cycle budget can't spare in a starter, and a top-down racer is
8
- ; a fully period-correct, recognizable racing game on its own.
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
- ; TIA object roles:
11
- ; P0 = the player's car (8-px sprite) near the bottom, steers L/R.
12
- ; PF = the road: reflected playfield draws the two ROAD EDGES (left
13
- ; rail mirrors to the right rail) plus a dashed CENTRE LINE that
14
- ; scrolls upward every frame to convey forward speed.
15
- ; P1 = an oncoming/lead car you must avoid (8-px sprite) that drifts
16
- ; down the road; reused each time it passes the bottom, dropped
17
- ; back to the top in a (deterministic) new lane.
18
- ; M0 = a second smaller hazard (a cone / debris) in another lane.
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
- ; Gameplay: hold LEFT/RIGHT on the joystick to weave between the rails;
21
- ; survive the descending traffic. Your SPEED (and the score) ramps up
22
- ; the longer you last the centre-line dashes scroll faster and the
23
- ; traffic descends faster. A collision (TIA P0-vs-P1 / P0-vs-M0) flashes
24
- ; the screen red and resets your speed. Extend it with M1 as a 3rd
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
- ; TIMING DISCIPLINE (learned the hard way in paddle.asm):
28
- ; * 262 lines EXACTLY = 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan.
29
- ; * The 3 object-positioning WSYNCs are COUNTED against the 37 VBLANK
30
- ; lines (so the VBLANK delay loop is #34, not #37).
31
- ; * The visible region is a TWO-LINE KERNEL: one scanline of render
32
- ; work (road edges + centre line + one car test) is ~80+ cycles and
33
- ; does NOT fit in 76; splitting across two WSYNCs doubles the budget.
34
- ; 96 passes x 2 lines = 192 visible lines.
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
- SWCHA = $280
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
- P_X = $80 ; player car X (visible column 0..159)
76
- E1_X = $81 ; enemy car P1 X
77
- E1_Y = $82 ; enemy car P1 top scanline (counts with the beam)
78
- E2_X = $83 ; hazard M0 X
79
- E2_Y = $84 ; hazard M0 top scanline
80
- SPEED = $85 ; current speed (1..6) drives scroll + descent
81
- SCROLL = $86 ; centre-line dash phase (0..7)
82
- FRAME = $87
83
- SCORE = $88 ; distance survived / ramps speed
84
- SFX_LEFT = $89 ; frames remaining on active sfx
85
- FLASH = $8A ; >0 = crash flash frames remaining
86
- TMP = $8B
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
- ; Initial positions
100
- LDA #76
101
- STA P_X ; player mid-road, near bottom
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 #$36 ; pink/red oncoming car (also colours M0 hazard)
169
+ LDA #COL_RIVAL
119
170
  STA COLUP1
120
- LDA #$0E ; white road markings
121
- STA COLUPF
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
- ; Playfield: reflected so the left rail mirrors to a right rail, and
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
- INC FRAME
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
- ; ── VBLANK (37 lines: 34 here + 3 positioning WSYNCs below) ──
220
+ kernel_done:
221
+ ; overscan: 30 lines, timer-paced like VBLANK
156
222
  LDA #2
157
223
  STA VBLANK
158
- LDX #34
159
- .vb:
224
+ LDA #35
225
+ STA TIM64T
226
+ .oswait:
227
+ LDA INTIM
228
+ BNE .oswait
160
229
  STA WSYNC
161
- DEX
162
- BNE .vb
230
+ JMP MAIN
163
231
 
164
- ; ── Steering: joystick port A left/right, every 2nd frame ──
165
- LDA FRAME
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 .skipmove
168
- ; SWCHA is active-LOW (0 = pressed). RE-LOAD it for each direction —
169
- ; the old ASL carry-chain clobbered A with LDA P_X between shifts, so
170
- ; the second ASL shifted P_X instead of SWCHA: pressing RIGHT also
171
- ; "pressed" LEFT (P_X < $80 -> carry clear) and the moves cancelled.
172
- ; That was the "ship/car stuck to the left edge" bug.
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 ; bit7 = P0 Right (0 = pressed)
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 ; bit6 = P0 Left (0 = pressed)
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
- ; ── Scroll the dashed centre line upward at SPEED px/frame ──
200
- ; SCROLL is the phase 0..7; subtract SPEED, wrap mod 8.
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
- ; ── Descend traffic at SPEED px/frame (smaller Y = lower on screen,
208
- ; because Y counts 192->1 with the beam). So descending = DEC Y. ──
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
- ; recycle to top, new deterministic lane from FRAME
216
- LDA #186
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
- ; Hazard M0 descends a touch faster (SPEED+1).
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
- SBC #0 ; (placeholder; SPEED already applied)
378
+ SEC
379
+ SBC #1
248
380
  STA E2_Y
249
381
  CMP #18
250
382
  BCS .e2ok
251
- LDA #182
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
- ; ── Collision check (read TIA collision latches from LAST frame) ──
262
- ; P0 vs P1 CXPPMM bit7. M0 vs P0 CXM0P bit6.
263
- BIT CXPPMM
264
- BMI .crash ; bit7 set = P0/P1 overlapped
265
- BIT CXM0P
266
- BVS .crash ; bit6 set = M0/P0 overlapped
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
- .crash:
269
- ; Reset speed, flash the screen, recycle the offending traffic up top,
270
- ; play a crash tone.
271
- LDA #1
272
- STA SPEED
273
- LDA #12
274
- STA FLASH
275
- LDA #186
276
- STA E1_Y
277
- LDA #182
278
- STA E2_Y
279
- ; Re-randomize BOTH lanes on crash (FRAME-derived). The old code only
280
- ; reset Y, so after a crash the enemy kept its old X — crash into it
281
- ; once near the left edge and it respawned in the same column forever
282
- ; ("enemy car stuck to the left edge").
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 #$3F
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 #40
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 FRAME
289
- EOR #$2A
290
- AND #$3F
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 #$08 ; noisy crash
295
- STA AUDC0
296
- LDA #$1F
297
- STA AUDF0
298
- LDA #$0F
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
- .nocrash:
303
- STA CXCLR ; clear collision latches for the next frame
545
+ STA FLASH
546
+ STA TUNE_SEL ; title jingle
547
+ JMP tune_start
304
548
 
305
- ; ── sfx countdown ──
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 .sfxdone
652
+ BEQ .at1
308
653
  DEC SFX_LEFT
309
- BNE .sfxdone
654
+ BNE .at1
310
655
  LDA #0
311
- STA AUDV0
312
- .sfxdone:
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
- ; ── Position objects (race-the-beam) — 3 WSYNC-bounded lines ──
315
- ; Use the divide-by-15 coarse approach (good enough for a starter; the
316
- ; cars sit on lane centres). HMCLR first so stale HM values don't drift.
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
- LDX P_X
320
- LDA #0
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
- TAX
327
- JMP .p0pos
328
- .p0done:
689
+ BCS .d0
690
+ EOR #7
691
+ ASL
692
+ ASL
693
+ ASL
694
+ ASL
329
695
  STA RESP0
330
- ; P1 (enemy car)
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
- TAX
340
- JMP .p1pos
341
- .p1done:
702
+ BCS .d1
703
+ EOR #7
704
+ ASL
705
+ ASL
706
+ ASL
707
+ ASL
342
708
  STA RESP1
343
- ; M0 (hazard)
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
- TAX
353
- JMP .m0pos
354
- .m0done:
715
+ BCS .d2
716
+ EOR #7
717
+ ASL
718
+ ASL
719
+ ASL
720
+ ASL
355
721
  STA RESM0
356
- STA HMOVE
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
- ; Crash flash: paint background dark-red while FLASH active.
359
- LDA FLASH
360
- BEQ .bgblack
361
- LDA #$42 ; dark red
362
- STA COLUBK
363
- JMP .bgdone
364
- .bgblack:
365
- LDA #$00
366
- STA COLUBK
367
- .bgdone:
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 VBLANK
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
- ; ── Visible (192 lines) TWO-LINE KERNEL ──
373
- ; Each pass renders TWO scanlines:
374
- ; line A: road edges (PF) + scrolling centre dash (PF) + player car.
375
- ; line B: enemy car (P1) + hazard (M0).
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 rails via PF0 (reflected left+right rails). The rails are the
382
- ; OUTER playfield pixels; PF0's high nibble shows on screen pixels
383
- ; 4..7 (the leftmost visible chunk after the 4-px PF0 gap), mirrored to
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 dash via PF2 — a dash that appears on some scanline groups,
388
- ; phased by SCROLL so it crawls upward. (Y+SCROLL)&8 picks dash on/off.
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 of PF2 (maps near screen middle)
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 starting at P_Y region near the bottom (~Y 30..22).
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 .pblank
821
+ BCS .noPlayer
408
822
  TAX
409
823
  LDA CAR,X
410
824
  STA GRP0
411
- JMP .pdone
412
- .pblank:
825
+ JMP .playerDone
826
+ .noPlayer:
413
827
  LDA #0
414
828
  STA GRP0
415
- .pdone:
416
-
417
- ; ---- line B: enemy car + hazard ----
829
+ .playerDone:
830
+ ; ============ line B: rival car + hazard ============
418
831
  STA WSYNC
419
- ; Enemy car P1: 8 rows starting at E1_Y.
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 .eblank
837
+ BCS .noRival
425
838
  TAX
426
839
  LDA CAR,X
427
840
  STA GRP1
428
- JMP .edone
429
- .eblank:
841
+ JMP .rivalDone
842
+ .noRival:
430
843
  LDA #0
431
844
  STA GRP1
432
- .edone:
433
- ; Hazard M0: enable for 4 rows around E2_Y.
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 .hblank
851
+ BCS .noHaz
439
852
  LDA #2
440
853
  STA ENAM0
441
- JMP .hdone
442
- .hblank:
854
+ JMP .hazDone
855
+ .noHaz:
443
856
  LDA #0
444
857
  STA ENAM0
445
- .hdone:
858
+ .hazDone:
446
859
 
447
860
  DEY
448
861
  DEY
449
- BNE .draw
862
+ BNE .road
450
863
 
451
- ; ── Overscan (30 lines) — clear the playfield so it doesn't bleed ──
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
- LDA #2
460
- STA VBLANK
461
- LDX #30
462
- .os:
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 .os
911
+ BNE .tb1
466
912
 
467
- JMP MAIN
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
- ; ── 8-row top-down car silhouette (windshield + body) ──
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
- ; ── Vector table ──
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