romdevtools 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -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,157 +158,229 @@ START:
92
158
  TXS
93
159
  LDA #0
94
160
  .clr:
95
- STA $00,X
96
- DEX
97
- BNE .clr
98
-
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
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)
112
164
 
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
154
206
 
155
- ; ── VBLANK (37 lines: 34 here + 3 positioning WSYNCs below) ──
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
219
+
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
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.
168
319
  LDA SWCHA
169
- ASL ; bit7 = P0 Right
170
- BCS .nr
320
+ AND #$80 ; joy0 right
321
+ BNE .nr
171
322
  LDA P_X
172
- CMP #128
323
+ CMP #128 ; right road edge
173
324
  BCS .nr
174
325
  INC P_X
175
326
  INC P_X
176
327
  .nr:
177
- ASL ; bit6 = P0 Left
178
- BCS .nl
328
+ LDA SWCHA ; RE-LOAD never trust A to still hold SWCHA
329
+ AND #$40 ; joy0 left
330
+ BNE .nl
179
331
  LDA P_X
180
- CMP #28
332
+ CMP #28 ; left road edge
181
333
  BCC .nl
182
334
  DEC P_X
183
335
  DEC P_X
184
336
  .nl:
185
- .skipmove:
186
-
187
- ; ── Crash flash countdown ──
188
- LDA FLASH
189
- BEQ .noflash
190
- DEC FLASH
191
- .noflash:
192
337
 
193
- ; ── Scroll the dashed centre line upward at SPEED px/frame ──
194
- ; 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.
195
343
  LDA SCROLL
196
344
  SEC
197
345
  SBC SPEED
198
346
  AND #$07
199
347
  STA SCROLL
200
348
 
201
- ; ── Descend traffic at SPEED px/frame (smaller Y = lower on screen,
202
- ; 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.
203
359
  LDA E1_Y
204
360
  SEC
205
361
  SBC SPEED
206
362
  STA E1_Y
207
- CMP #20 ; passed the bottom?
363
+ CMP #20 ; passed the bottom of the road?
208
364
  BCS .e1ok
209
- ; recycle to top, new deterministic lane from FRAME
210
- LDA #186
211
- STA E1_Y
365
+ LDA #180
366
+ STA E1_Y ; back to the top
212
367
  LDA FRAME
213
368
  AND #$3F
214
369
  CLC
215
370
  ADC #40
216
- STA E1_X
217
- INC SCORE ; survived a car
218
- ; ramp speed every time SCORE crosses a multiple of 4 (cap at 6)
219
- LDA SCORE
220
- AND #$03
221
- BNE .e1ok
222
- LDA SPEED
223
- CMP #6
224
- BCS .e1ok
225
- INC SPEED
226
- ; speed-up "rev" sfx
227
- LDA #$03
228
- STA AUDC0
229
- LDA #$08
230
- STA AUDF0
231
- LDA #$0C
232
- STA AUDV0
233
- LDA #10
234
- STA SFX_LEFT
371
+ STA E1_X ; new deterministic lane from FRAME
235
372
  .e1ok:
236
373
 
237
- ; Hazard M0 descends a touch faster (SPEED+1).
374
+ ; hazard M0 descends a touch faster (SPEED + 1) and recycles likewise.
238
375
  LDA E2_Y
239
376
  SEC
240
377
  SBC SPEED
241
- SBC #0 ; (placeholder; SPEED already applied)
378
+ SEC
379
+ SBC #1
242
380
  STA E2_Y
243
381
  CMP #18
244
382
  BCS .e2ok
245
- LDA #182
383
+ LDA #176
246
384
  STA E2_Y
247
385
  LDA FRAME
248
386
  EOR #$5A
@@ -252,182 +390,478 @@ MAIN:
252
390
  STA E2_X
253
391
  .e2ok:
254
392
 
255
- ; ── Collision check (read TIA collision latches from LAST frame) ──
256
- ; P0 vs P1 CXPPMM bit7. M0 vs P0 CXM0P bit6.
257
- BIT CXPPMM
258
- BMI .crash ; bit7 set = P0/P1 overlapped
259
- BIT CXM0P
260
- 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
261
404
  JMP .nocrash
262
- .crash:
263
- ; Reset speed, flash the screen, recycle the offending traffic up top,
264
- ; play a crash tone.
265
- LDA #1
266
- STA SPEED
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
454
+ LDA FRAME
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
470
+ CLC
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
267
498
  LDA #12
268
499
  STA FLASH
269
- LDA #186
270
- STA E1_Y
271
- LDA #182
272
- STA E2_Y
273
- LDA #$08 ; noisy crash
274
- STA AUDC0
500
+ LDA #1
501
+ STA TUNE_SEL
502
+ ; crash noise on voice 0 (over the game-over tune on voice 1)
275
503
  LDA #$1F
276
- STA AUDF0
277
- LDA #$0F
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
523
+ STA E1_X
524
+ LDA #150
525
+ STA E1_Y ; rival car starts up top
526
+ LDA #104
527
+ STA E2_X
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
278
543
  STA AUDV0
279
- LDA #14
280
544
  STA SFX_LEFT
281
- .nocrash:
282
- STA CXCLR ; clear collision latches for the next frame
545
+ STA FLASH
546
+ STA TUNE_SEL ; title jingle
547
+ JMP tune_start
548
+
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
283
618
 
284
- ; ── sfx countdown ──
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
285
651
  LDA SFX_LEFT
286
- BEQ .sfxdone
652
+ BEQ .at1
287
653
  DEC SFX_LEFT
288
- BNE .sfxdone
654
+ BNE .at1
289
655
  LDA #0
290
- STA AUDV0
291
- .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
292
669
 
293
- ; ── Position objects (race-the-beam) — 3 WSYNC-bounded lines ──
294
- ; Use the divide-by-15 coarse approach (good enough for a starter; the
295
- ; 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:
296
682
  STA WSYNC
297
683
  STA HMCLR
298
- LDX P_X
299
- LDA #0
300
- .p0pos:
301
- CPX #15
302
- BCC .p0done
684
+ LDA P_X ; player car → P0
685
+ STA WSYNC
303
686
  SEC
687
+ .d0:
304
688
  SBC #15
305
- TAX
306
- JMP .p0pos
307
- .p0done:
689
+ BCS .d0
690
+ EOR #7
691
+ ASL
692
+ ASL
693
+ ASL
694
+ ASL
308
695
  STA RESP0
309
- ; P1 (enemy car)
696
+ STA HMP0
697
+ LDA E1_X ; rival car → P1
310
698
  STA WSYNC
311
- LDX E1_X
312
- LDA #0
313
- .p1pos:
314
- CPX #15
315
- BCC .p1done
316
699
  SEC
700
+ .d1:
317
701
  SBC #15
318
- TAX
319
- JMP .p1pos
320
- .p1done:
702
+ BCS .d1
703
+ EOR #7
704
+ ASL
705
+ ASL
706
+ ASL
707
+ ASL
321
708
  STA RESP1
322
- ; M0 (hazard)
709
+ STA HMP1
710
+ LDA E2_X ; hazard → M0
323
711
  STA WSYNC
324
- LDX E2_X
325
- LDA #0
326
- .m0pos:
327
- CPX #15
328
- BCC .m0done
329
712
  SEC
713
+ .d2:
330
714
  SBC #15
331
- TAX
332
- JMP .m0pos
333
- .m0done:
715
+ BCS .d2
716
+ EOR #7
717
+ ASL
718
+ ASL
719
+ ASL
720
+ ASL
334
721
  STA RESM0
335
- 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)
336
726
 
337
- ; Crash flash: paint background dark-red while FLASH active.
338
- LDA FLASH
339
- BEQ .bgblack
340
- LDA #$42 ; dark red
341
- STA COLUBK
342
- JMP .bgdone
343
- .bgblack:
344
- LDA #$00
345
- STA COLUBK
346
- .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
347
749
 
750
+ LDA #COL_TARMAC
751
+ STA COLUBK
348
752
  LDA #0
349
- 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
350
792
 
351
- ; ── Visible (192 lines) TWO-LINE KERNEL ──
352
- ; Each pass renders TWO scanlines:
353
- ; line A: road edges (PF) + scrolling centre dash (PF) + player car.
354
- ; line B: enemy car (P1) + hazard (M0).
355
- ; Y counts 192 -> 2 in steps of 2. 96 passes = 192 lines.
356
- LDY #192
357
- .draw:
358
- ; ---- 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 ============
359
797
  STA WSYNC
360
- ; Road rails via PF0 (reflected left+right rails). The rails are the
361
- ; OUTER playfield pixels; PF0's high nibble shows on screen pixels
362
- ; 4..7 (the leftmost visible chunk after the 4-px PF0 gap), mirrored to
363
- ; the right edge by CTRLPF reflect. A constant rail every line.
364
- 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
365
801
  STA PF0
366
- ; Centre dash via PF2 — a dash that appears on some scanline groups,
367
- ; 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.
368
804
  TYA
369
805
  CLC
370
806
  ADC SCROLL
371
807
  AND #%00001000
372
808
  BEQ .nodash
373
- LDA #%00011000 ; centre pixels of PF2 (maps near screen middle)
809
+ LDA #%00011000 ; centre-ish PF2 pixels = the lane dash
374
810
  STA PF2
375
811
  JMP .dashdone
376
812
  .nodash:
377
813
  LDA #0
378
814
  STA PF2
379
815
  .dashdone:
380
- ; Player car: 8 rows starting at P_Y region near the bottom (~Y 30..22).
381
- ; Window: (Y - 22) in [0..7] → index CAR bitmap.
816
+ ; Player car: 8 rows in the bottom band (Y in [22,30)).
382
817
  TYA
383
818
  SEC
384
819
  SBC #22
385
820
  CMP #8
386
- BCS .pblank
821
+ BCS .noPlayer
387
822
  TAX
388
823
  LDA CAR,X
389
824
  STA GRP0
390
- JMP .pdone
391
- .pblank:
825
+ JMP .playerDone
826
+ .noPlayer:
392
827
  LDA #0
393
828
  STA GRP0
394
- .pdone:
395
-
396
- ; ---- line B: enemy car + hazard ----
829
+ .playerDone:
830
+ ; ============ line B: rival car + hazard ============
397
831
  STA WSYNC
398
- ; Enemy car P1: 8 rows starting at E1_Y.
832
+ ; Rival car P1: 8 rows starting at E1_Y.
399
833
  TYA
400
834
  SEC
401
835
  SBC E1_Y
402
836
  CMP #8
403
- BCS .eblank
837
+ BCS .noRival
404
838
  TAX
405
839
  LDA CAR,X
406
840
  STA GRP1
407
- JMP .edone
408
- .eblank:
841
+ JMP .rivalDone
842
+ .noRival:
409
843
  LDA #0
410
844
  STA GRP1
411
- .edone:
412
- ; 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).
413
847
  TYA
414
848
  SEC
415
849
  SBC E2_Y
416
850
  CMP #4
417
- BCS .hblank
851
+ BCS .noHaz
418
852
  LDA #2
419
853
  STA ENAM0
420
- JMP .hdone
421
- .hblank:
854
+ JMP .hazDone
855
+ .noHaz:
422
856
  LDA #0
423
857
  STA ENAM0
424
- .hdone:
858
+ .hazDone:
425
859
 
426
860
  DEY
427
861
  DEY
428
- BNE .draw
862
+ BNE .road
429
863
 
430
- ; ── Overscan (30 lines) — clear the playfield so it doesn't bleed ──
864
+ ; clear the playfield so it doesn't bleed into overscan
431
865
  LDA #0
432
866
  STA PF0
433
867
  STA PF1
@@ -435,17 +869,180 @@ MAIN:
435
869
  STA GRP0
436
870
  STA GRP1
437
871
  STA ENAM0
438
- LDA #2
439
- STA VBLANK
440
- LDX #30
441
- .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:
442
909
  STA WSYNC
443
910
  DEX
444
- BNE .os
911
+ BNE .tb1
445
912
 
446
- 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
950
+
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
447
1016
 
448
- ; ── 8-row top-down car silhouette (windshield + body) ──
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).
449
1046
  CAR:
450
1047
  .byte %00111100
451
1048
  .byte %01111110
@@ -456,7 +1053,62 @@ CAR:
456
1053
  .byte %01011010
457
1054
  .byte %01111110
458
1055
 
459
- ; ── 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 ──────────────────────────────────────────────────────
460
1112
  org $FFFA
461
1113
  .word START
462
1114
  .word START