romdevtools 0.28.0 → 0.30.0

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