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,46 +1,81 @@
1
- ; ── platformer.asm — Atari 2600 PLATFORMER genre scaffold ─────────────
1
+ ; ── platformer.asm — PERCH PATROL — Atari 2600 single-screen platformer ──────
2
2
  ;
3
- ; SINGLE-SCREEN platformer. Pitfall!, Montezuma's Revenge, and Kangaroo
4
- ; are 2600 platformers, and they are single-screen-at-a-time: the 2600
5
- ; has NO hardware scroll, no tilemap, and 128 bytes of RAM, so a smooth
6
- ; side-scroller is not the honest 2600 idiom (you'd flip whole screens,
7
- ; Pitfall-style, instead). What IS idiomatic and what this scaffold
8
- ; shipsis gravity + a jump arc + land-on-top collision against a set
9
- ; of fixed platforms, all on one screen.
3
+ ; A COMPLETE, working game — drawn title screen, a single-SCREEN platformer
4
+ ; (you hop a P0 hero across PLAYFIELD ledges, grab the bouncing coin, dodge
5
+ ; the patrolling spike), score + in-session hi-score, TIA sound effects + a
6
+ ; title jingle, game-over with auto-return to the title, and the 2600's
7
+ ; signature feature: THE WHOLE MACHINE. There is no framebuffer, no tilemap,
8
+ ; no OS every visible scanline below is composed live by racing the beam,
9
+ ; and this file teaches the platformer's load-bearing TIA tricks while doing
10
+ ; it:
10
11
  ;
11
- ; TIA object roles:
12
- ; P0 = the player (8-px sprite) that walks + jumps.
13
- ; PF = the platforms (and the floor): three horizontal playfield bars
14
- ; at fixed Y bands. The playfield is the only 2600 object wide
15
- ; enough to be a platform; players/missiles are too narrow.
12
+ ; 1. PLAYFIELD-AS-LEVEL (the ledges + floor + pit) — the 2600 has NO
13
+ ; tilemap and NO hardware scroll, so the level IS the playfield. PF0/
14
+ ; PF1/PF2 are reloaded per scanline band from a per-row table; a lit PF
15
+ ; pixel is solid ground, a gap is a pit. This is exactly how the era's
16
+ ; single-screen platformers drew their arenas: the honest 2600
17
+ ; platformer is a FIXED screen (those games flip whole new screens at
18
+ ; the edges — they never scroll), so this one is too.
19
+ ; 2. CODE COLLISION, NOT TIA LATCHES (land-on-ledge) — a shooter reads the
20
+ ; TIA's hardware overlap latch (FLAK FRENZY does), but a platformer must
21
+ ; know WHICH surface it's standing on to stop the fall there. So ground
22
+ ; contact is tested in CODE: sample the level's PF bit directly under
23
+ ; the hero's column at his feet's Y. (TIA latches are still used — for
24
+ ; the coin pickup and the spike death, where "did I touch it" is enough.)
25
+ ; 3. RESP/HMOVE BEAM POSITIONING (the SBC-#15 idiom) — there is no sprite X
26
+ ; register; you strobe RESPx/RESBL/RESM0 WHERE THE BEAM IS, then nudge
27
+ ; ±7px with HMOVE. Hero P0, coin BL and spike M0 are positioned this way
28
+ ; every frame, inside the timed VBLANK window.
29
+ ; 4. TIM64T/INTIM FRAME TIMING — set the RIOT timer for VBLANK/overscan and
30
+ ; let it absorb however much the game logic costs, instead of hand-
31
+ ; counting WSYNCs (which rolls the picture the moment logic grows).
16
32
  ;
17
- ; Physics (fixed-point Y, 1 sub-pixel bit):
18
- ; * Gravity pulls the player down every frame (velocity += g).
19
- ; * Pressing FIRE while standing on a surface launches a jump
20
- ; (velocity = -jump).
21
- ; * After moving, we test the player's FEET against each platform's
22
- ; (Y, x-span) in CODE not TIA collisionbecause we need to know
23
- ; WHICH surface to stand on and TIA only gives a yes/no overlap.
24
- ; * Walk left/right with the joystick; you can't walk off the screen.
33
+ ; THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
34
+ ; very different one. The markers tell you what's what:
35
+ ; HARDWARE IDIOM (load-bearing) cycle-counted / footgun-dodging code;
36
+ ; reshape your gameplay around it (see TROUBLESHOOTING before changing).
37
+ ; GAME LOGIC (clay) physics, level layout, scoring, tuning, art: reshape
38
+ ; freely (the LEVEL table near the bottom is pure clay redraw it).
25
39
  ;
26
- ; This is the jump/gravity/collision CORE. Extend it with: a second
27
- ; sprite (P1) as a pickup or enemy, M0 as a thrown rock, ladders (let
28
- ; UP/DOWN move Y when overlapping a ladder x-span), or Pitfall-style
29
- ; screen flipping when the player exits the left/right edge.
40
+ ; GAME_TITLE: on the 2600 a title is DRAWN, not printed — there is no font
41
+ ; hardware. The PERCH/PATROL banner bitmaps near the bottom of this file ARE
42
+ ; the title; redraw them for your game (the comment above each table shows
43
+ ; the 40-pixel artwork and the PF0/PF1/PF2 bit-order encoding).
30
44
  ;
31
- ; TIMING: 262 lines = 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan.
32
- ; One positioning WSYNC is counted against VBLANK (loop = #36). The
33
- ; visible region is a TWO-LINE KERNEL so the per-pass work (platform PF
34
- ; lookup + player-sprite test) fits the cycle budget.
45
+ ; CONTROLS (documented for players and for the fork README):
46
+ ; Title: fire on JOYSTICK 0 (or console RESET) starts the game
47
+ ; Play: joystick 0 LEFT/RIGHT walks the hero; fire (or UP) JUMPS when
48
+ ; standing on a ledge; console RESET returns to the title
49
+ ; Grab the bouncing coin to score; touch the patrolling spike and it's
50
+ ; game over. Your best SCORE this session is shown on the title screen.
51
+ ;
52
+ ; PLAYERS — 1P, honest. The 2600 has two joystick ports, but this single-
53
+ ; screen kernel is already spending its scanline budget on the PLAYFIELD
54
+ ; level (per-row PF reload), the hero (P0), the coin (BL) and the spike
55
+ ; (M0). A second human hero would need its own positioned object competing
56
+ ; for the SAME 76-cycle lines the level reload already fills. To add 2P
57
+ ; alternating TURNS instead — cheap, no extra kernel objects — keep a second
58
+ ; score/lives pair and swap on death; left as an exercise.
59
+ ;
60
+ ; HI-SCORE HONESTY: real 2600 cartridges had NO battery, NO SRAM, NO
61
+ ; persistence of any kind. The hi-score here lives in RIOT RAM ($A0) and
62
+ ; survives game → title cycles only WITHIN one power-on session — exactly
63
+ ; like the arcade machines of the era. Power off and it is gone. Do not
64
+ ; fake an EEPROM; state it honestly in your fork too.
65
+ ;
66
+ ; NTSC frame: 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan = 262 lines.
35
67
 
36
68
  processor 6502
37
69
  org $F000
38
70
 
71
+ ; ── TIA write registers ───────────────────────────────────────────────
39
72
  VSYNC = $00
40
73
  VBLANK = $01
41
74
  WSYNC = $02
42
75
  NUSIZ0 = $04
76
+ NUSIZ1 = $05
43
77
  COLUP0 = $06
78
+ COLUP1 = $07
44
79
  COLUPF = $08
45
80
  COLUBK = $09
46
81
  CTRLPF = $0A
@@ -48,49 +83,95 @@ PF0 = $0D
48
83
  PF1 = $0E
49
84
  PF2 = $0F
50
85
  RESP0 = $10
86
+ RESBL = $14
87
+ RESM0 = $12
51
88
  GRP0 = $1B
89
+ GRP1 = $1C
90
+ ENABL = $1F
91
+ ENAM0 = $1D
52
92
  HMP0 = $20
93
+ HMBL = $24
94
+ HMM0 = $22
53
95
  HMOVE = $2A
54
96
  HMCLR = $2B
55
- SWCHA = $280
56
- INPT4 = $0C ; fire button (active-low, bit7) = JUMP
57
- ; TIA audio
97
+ CXCLR = $2C
98
+ ; ── TIA audio ─────────────────────────────────────────────────────────
58
99
  AUDC0 = $15
100
+ AUDC1 = $16
59
101
  AUDF0 = $17
102
+ AUDF1 = $18
60
103
  AUDV0 = $19
104
+ AUDV1 = $1A
105
+ ; ── TIA READ registers (separate read map — the same addresses as some
106
+ ; write strobes; e.g. CXP0FB reads $02 while STA $02 strobes WSYNC) ─────
107
+ CXP0FB = $02 ; bit6 = player0 / ball collision (latched)
108
+ CXM0P = $00 ; bit7 = missile0 / player0 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
115
+
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 ; hero X column (visible 0..159; kept 12..148)
120
+ P_Y = $82 ; hero FEET scanline in level space (0=floor .. up)
121
+ P_VY = $83 ; vertical velocity, signed (jump up = +, fall = -)
122
+ ON_GND = $84 ; 1 = standing on a ledge/floor this frame, 0 = airborne
123
+ COIN_X = $85 ; coin (BL) X column
124
+ COIN_Y = $86 ; coin Y in level space
125
+ COIN_VY = $87 ; coin bounce velocity (signed)
126
+ SPK_X = $88 ; spike (M0) X column
127
+ SPK_Y = $89 ; spike Y in level space (which ledge it patrols)
128
+ SPK_DIR = $8A ; +1 marching right, $FF marching left
129
+ SCORE = $8B ; current score, BCD (digit nibbles fall out free)
130
+ SCORE_HI = $8C ; current score high byte, BCD
131
+ FRAME = $8D
132
+ SFX_LEFT = $8E ; frames remaining on the voice-0 sound effect
133
+ TUNE_SEL = $8F ; 0 = title jingle, 1 = game-over tune (voice 1)
134
+ TUNE_POS = $90
135
+ TUNE_LEFT = $91 ; frames left on current jingle note (0 = silent)
136
+ OVER_T = $92 ; game-over auto-return-to-title countdown
137
+ SWCHB_PRV = $93 ; previous SWCHB for RESET edge detect
138
+ FIRE_PRV = $94 ; previous fire level (bit7) for fire-edge detect
139
+ EDGEB = $95 ; this frame's RESET press-edge (bit0)
140
+ FIRE_EDG = $96 ; this frame's fire press-edge (bit7)
141
+ TMP = $97
142
+ TMP2 = $98
143
+ P_ROW = $99 ; hero's current level ROW (Y/16) — picked in logic,
144
+ ; reused by the kernel to draw the hero band
145
+ COIN_ROW = $9A ; coin's level row (for the kernel)
146
+ SPK_ROW = $9B ; spike's level row (for the kernel)
147
+ GFXIDX = $9C ; hero sprite frame base (0 = idle, 6 = walk)
148
+ SCORE_HSV = $A0 ; SESSION hi-score (BCD low byte). RAM only — real
149
+ SCORE_HSH = $A1 ; 2600 carts have no battery; honest by design.
150
+ S0BUF = $A2 ; 6 rows: packed score digits for the play kernel
151
+ HSBUF = $A8 ; 6 rows: hi-score, packed (for the title kernel)
152
+ SCRATCH = $AE ; 6 bytes general kernel/packer scratch
153
+
154
+ ; ── level geometry constants (clay — change to reshape the arena) ──────
155
+ ; The level is NROWS bands of 16 scanlines. Each row carries a PF0/PF1/PF2
156
+ ; triple (LEVEL table) = where the ground/ledges are on that band. P_Y is
157
+ ; the hero's FEET measured 0 (bottom of the arena) upward; row = P_Y/16.
158
+ NROWS = 9 ; 9 rows × 16 = 144 visible level lines (+24 score bar)
159
+ ROWH = 16
160
+ GRAVITY = 1 ; downward pull per frame
161
+ JUMP_VY = 9 ; initial jump impulse
162
+ WALK_LO = 12 ; hero X clamp
163
+ WALK_HI = 148
61
164
 
62
- ; ── Zero-page state ───────────────────────────────────────────────────
63
- P_X = $80 ; player X (visible column)
64
- P_Y = $81 ; player top scanline (integer part). Y counts with
65
- ; the beam 192->0, so SMALLER = LOWER on screen.
66
- P_VY = $82 ; vertical velocity, signed, in half-pixels (8.1)
67
- P_YSUB = $83 ; (spare physics is integer-pixel; kept for layout)
68
- ON_GND = $84 ; 1 = standing on a surface (can jump)
69
- FRAME = $85
70
- SFX_LEFT = $86
71
- TMP = $87
72
- LANDY = $88 ; the Y we snap to when we land
73
- ; PFROW: 96-byte playfield row buffer (one entry per 2-line kernel row),
74
- ; built ONCE at boot from the platform table. $89..$E8. Stack ($FF down)
75
- ; has $E9..$FF free (23 bytes) — ample, since the only JSR is the one-shot
76
- ; build_pfrow and the kernel itself calls nothing.
77
- PFROW = $89
78
-
79
- ; Player height (sprite rows).
80
- PH = 8
81
-
82
- ; ── Platform table ────────────────────────────────────────────────────
83
- ; Each platform = (top-band scanline Y, x-left, x-right). The visuals are
84
- ; FULL-WIDTH horizontal bars (cheap + reads cleanly), so the x-spans below
85
- ; are the whole screen and the land-on-top test lets you stand anywhere on
86
- ; a platform. Beam Y counts 192(top)->1(bottom): a LARGER Y value sits
87
- ; HIGHER on the screen, so PLAT_Y=18 is the bottom FLOOR and PLAT_Y=150 is
88
- ; the top ledge.
89
- ; floor : Y=18 (bottom, full width)
90
- ; ledge : Y=70
91
- ; ledge : Y=110
92
- ; ledge : Y=150 (highest)
93
- NUM_PLAT = 4
165
+ HEROH = 6 ; hero sprite height in scanlines
166
+ COIN_FLR = 8 ; coin's bounce floor (level Y)
167
+ COIN_CEIL = 124 ; coin's bounce ceiling
168
+
169
+ COL_SKY = $00 ; black space behind the arena
170
+ COL_HERO = $3A ; warm yellow hero
171
+ COL_LEDGE = $C6 ; green ledges/floor
172
+ COL_COIN = $1E ; bright yellow coin
173
+ COL_SPIKE = $44 ; red spike
174
+ COL_HUD = $0E ; white score digits
94
175
 
95
176
  START:
96
177
  SEI
@@ -99,370 +180,1182 @@ START:
99
180
  TXS
100
181
  LDA #0
101
182
  .clr:
102
- STA $00,X
103
- DEX
104
- BNE .clr
105
-
106
- ; Start the player standing on the floor.
107
- LDA #76
108
- STA P_X
109
- LDA #26 ; just above the floor band (floor top = 18, +PH)
110
- STA P_Y
111
- LDA #1
112
- STA ON_GND
183
+ STA $00,X ; clears ALL of $00-$FF: zero page RAM AND the TIA
184
+ DEX ; write registers (GRP/ENAxx/HMxx/audio all silenced
185
+ BNE .clr ; — the standard 2600 power-on hygiene)
113
186
 
114
- ; Colours
115
- LDA #$84 ; blue sky background
116
- STA COLUBK
117
- LDA #$1E ; yellow player
187
+ ; Fixed identity colors (the kernels rewrite COLUPF per band, but the
188
+ ; object colors are constant all session).
189
+ LDA #COL_HERO
118
190
  STA COLUP0
119
- LDA #$28 ; brown/orange platforms
120
- STA COLUPF
191
+ ; single-width hero. NUSIZ left at 0 = single objects everywhere.
192
+ LDA #%00000000
193
+ STA NUSIZ0
194
+ STA NUSIZ1
121
195
 
122
- ; Reflected playfield (harmless for full-width bars; left set so that if
123
- ; you switch ledges to partial patterns later they mirror symmetrically).
124
- LDA #%00000001
125
- STA CTRLPF
126
-
127
- ; Build the static playfield row buffer ONCE (platforms never move).
128
- JSR build_pfrow
129
-
130
- ; Boot chime.
131
- LDA #$04
132
- STA AUDC0
133
- LDA #$0C
134
- STA AUDF0
135
- LDA #$0F
136
- STA AUDV0
137
- LDA #15
138
- STA SFX_LEFT
196
+ JSR enter_title
139
197
 
198
+ ; ──────────────────────────────────────────────────────────────────────
199
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
200
+ ; THE FRAME LOOP. 262 scanlines, every frame, forever. VBLANK and overscan
201
+ ; are timed with the RIOT timer (TIM64T) instead of counted WSYNCs: set the
202
+ ; timer, run however much game logic the state needs, then spin on INTIM.
203
+ ; This is how shipped 2600 games did it, and it kills the classic homebrew
204
+ ; bug class where adding one branch to the game logic emits a 263rd line
205
+ ; and the TV loses vsync (rolling picture). The VISIBLE 192 lines are still
206
+ ; counted exactly — every STA WSYNC below is one scanline, and each state's
207
+ ; kernel accounts for all 192.
208
+ ; ──────────────────────────────────────────────────────────────────────
140
209
  MAIN:
141
- INC FRAME
142
-
143
- ; ── VSYNC (3 lines) ──
210
+ ; VSYNC: 3 lines
144
211
  LDA #2
212
+ STA VBLANK
145
213
  STA VSYNC
146
214
  STA WSYNC
147
215
  STA WSYNC
148
216
  STA WSYNC
149
217
  LDA #0
150
218
  STA VSYNC
219
+ ; 37 lines of VBLANK = 2812 cycles ≈ 43 × 64-cycle timer ticks.
220
+ LDA #43
221
+ STA TIM64T
151
222
 
152
- ; ── VBLANK (37 lines: 36 here + 1 positioning WSYNC below) ──
223
+ JSR frame_logic ; all game thinking happens in the blanked region
224
+
225
+ ; burn whatever VBLANK time the logic didn't use
226
+ .vbwait:
227
+ LDA INTIM
228
+ BNE .vbwait
229
+ STA WSYNC
230
+
231
+ ; kernel dispatch — title has its own kernel; play and game-over share one
232
+ LDA STATE
233
+ BNE .ingame
234
+ JMP title_kernel
235
+ .ingame:
236
+ JMP play_kernel
237
+
238
+ kernel_done:
239
+ ; overscan: 30 lines, timer-paced like VBLANK
153
240
  LDA #2
154
241
  STA VBLANK
155
- LDX #36
156
- .vb:
242
+ LDA #35
243
+ STA TIM64T
244
+ .oswait:
245
+ LDA INTIM
246
+ BNE .oswait
157
247
  STA WSYNC
158
- DEX
159
- BNE .vb
248
+ JMP MAIN
160
249
 
161
- ; ── Horizontal move (every 2nd frame to throttle) ──
162
- LDA FRAME
250
+ ; ──────────────────────────────────────────────────────────────────────
251
+ ; Per-frame logic, dispatched by state. Runs entirely inside the timed
252
+ ; VBLANK window (~2800 cycles — an eternity next to the kernel's 76/line).
253
+ ; ──────────────────────────────────────────────────────────────────────
254
+ frame_logic:
255
+ INC FRAME
256
+ JSR audio_tick
257
+
258
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
259
+ ; Console RESET + fire button are ACTIVE LOW and not debounced; a held
260
+ ; RESET would restart every frame. Convert to press-EDGES once per frame:
261
+ ; edge = was-released-last-frame AND pressed-now.
262
+ LDA SWCHB
263
+ TAX ; X = current switch levels
264
+ EOR #$FF ; A = pressed-now mask (1 = held)
265
+ AND SWCHB_PRV ; ...that were RELEASED (1) last frame
266
+ STA EDGEB ; bit0 = RESET edge
267
+ STX SWCHB_PRV
268
+ ; fire button → same edge treatment in bit7
269
+ LDA #0
270
+ BIT INPT4
271
+ BMI .fup ; bit7 set = not pressed (active low)
272
+ ORA #$80
273
+ .fup:
274
+ TAY ; Y = pressed-now (bit7)
275
+ LDA FIRE_PRV
276
+ EOR #$FF
277
+ STA TMP ; released-last-frame mask
278
+ TYA
279
+ AND TMP
280
+ STA FIRE_EDG ; bit7 = fire press-edge
281
+ STY FIRE_PRV
282
+
283
+ LDA STATE
284
+ BEQ logic_title
285
+ CMP #1
286
+ BEQ logic_play_jmp
287
+ JMP logic_over
288
+ logic_play_jmp:
289
+ JMP logic_play
290
+
291
+ ; ── GAME LOGIC (clay — reshape freely) ── title-screen behavior ────────
292
+ logic_title:
293
+ ; fire 0 or console RESET starts the game.
294
+ LDA FIRE_EDG
295
+ BMI .start ; bit7 set = fire edge
296
+ LDA EDGEB
163
297
  AND #$01
164
- BNE .skipmove
165
- ; SWCHA is active-LOW; RE-LOAD per direction (the old ASL carry-chain
166
- ; clobbered A with LDA P_X between shifts → RIGHT also triggered LEFT
167
- ; and the moves cancelled — the player couldn't move).
298
+ BNE .start
299
+ JMP .packtitle
300
+ .start:
301
+ JMP start_game
302
+ .packtitle:
303
+ ; Pack the hi-score into the title's display buffer (the kernel just
304
+ ; streams bytes — all per-frame thinking happens HERE, in VBLANK, never
305
+ ; inside a kernel). Two visible digits, packed two-per-PF1-row.
306
+ LDA SCORE_HSV
307
+ JSR pack_two_digits
308
+ LDY #0
309
+ .hst:
310
+ LDA SCRATCH,Y ; pack_two_digits left 6 rows in SCRATCH..SCRATCH+5
311
+ STA HSBUF,Y
312
+ INY
313
+ CPY #6
314
+ BNE .hst
315
+
316
+ ; title shows no moving objects
317
+ LDA #0
318
+ STA GRP0
319
+ STA ENABL
320
+ STA ENAM0
321
+ RTS
322
+
323
+ ; ── GAME LOGIC (clay — reshape freely) ── one frame of the platformer ──
324
+ logic_play:
325
+ LDA EDGEB
326
+ AND #$01 ; console RESET → back to title
327
+ BEQ .noquit
328
+ JMP enter_title
329
+ .noquit:
330
+
331
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
332
+ ; SWCHA is ACTIVE LOW (0 = pressed) and must be RE-LOADED for every
333
+ ; direction check. The classic bug: caching it in A and chaining ASLs,
334
+ ; then clobbering A with game state between shifts — "right works once,
335
+ ; left never moves". Fresh LDA SWCHA + AND #mask per check is immune.
336
+ ; Joystick 0 lives in the HIGH nibble: bit7 right, bit6 left, bit4 up.
168
337
  LDA SWCHA
169
- AND #$80 ; bit7 = Right (0 = pressed)
338
+ AND #$80 ; joy0 right
170
339
  BNE .nr
171
340
  LDA P_X
172
- CMP #140
341
+ CMP #WALK_HI
173
342
  BCS .nr
174
343
  INC P_X
175
344
  INC P_X
345
+ LDA #6
346
+ STA GFXIDX ; walk frame while moving
176
347
  .nr:
177
- LDA SWCHA
178
- AND #$40 ; bit6 = Left (0 = pressed)
348
+ LDA SWCHA ; RE-LOAD — never trust A to still hold SWCHA
349
+ AND #$40 ; joy0 left
179
350
  BNE .nl
180
351
  LDA P_X
181
- CMP #16
352
+ CMP #WALK_LO
182
353
  BCC .nl
183
354
  DEC P_X
184
355
  DEC P_X
356
+ LDA #6
357
+ STA GFXIDX
185
358
  .nl:
186
- .skipmove:
187
-
188
- ; ── Jump: FIRE while on the ground launches an upward velocity ──
189
- ; Coordinate reminder: Y is the BEAM scanline; the top of the screen is
190
- ; the LARGER Y. So "up" = INCREASING P_Y, and a positive P_VY rises.
191
- ; P_VY is signed WHOLE PIXELS/frame (no sub-pixel integer motion looks
192
- ; perfectly fine for a 2600 jump and avoids a fractional-carry bug where
193
- ; small half-pixel velocities never accumulate a whole pixel).
359
+
360
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
361
+ ; JUMP / GRAVITY a fixed-point vertical-velocity counter, the heart of
362
+ ; any platformer. P_VY is signed: positive = rising, negative = falling.
363
+ ; JUMP is only allowed when ON_GND was true (set by the ground test BELOW,
364
+ ; from last frame's footing) the canonical "no mid-air double jump" gate.
365
+ ; Beam-Y runs 192→1 top-to-bottom, but we keep P_Y in LEVEL space (0 =
366
+ ; arena floor, growing UP) so the physics reads naturally; the kernel maps
367
+ ; it back to a scanline.
194
368
  LDA ON_GND
195
- BEQ .nojump
196
- BIT INPT4
197
- BMI .nojump ; bit7 set = button released
198
- LDA #6 ; initial jump speed (pixels/frame, upward)
369
+ BEQ .noJump
370
+ ; jump on fire edge OR joystick up
371
+ LDA FIRE_EDG
372
+ BMI .doJump
373
+ LDA SWCHA
374
+ AND #$10 ; joy0 up
375
+ BNE .noJump
376
+ .doJump:
377
+ LDA #JUMP_VY
199
378
  STA P_VY
200
379
  LDA #0
201
- STA ON_GND
202
- ; jump sfx
203
- LDA #$0C
204
- STA AUDC0
205
- LDA #$14
206
- STA AUDF0
207
- LDA #$0F
208
- STA AUDV0
209
- LDA #6
210
- STA SFX_LEFT
211
- .nojump:
380
+ STA ON_GND ; we leave the ground this frame
381
+ LDA #$0C ; jump sfx
382
+ LDX #$04
383
+ LDY #6
384
+ JSR sfx_play
385
+ .noJump:
212
386
 
213
- ; ── Gravity + integrate vertical velocity (only while airborne) ──
214
- ; Standing still on a platform we DON'T apply gravity (otherwise the
215
- ; player drops 1px every frame and the landing snap fights it → jitter).
216
- LDA ON_GND
217
- BNE .skipgrav
218
- DEC P_VY ; gravity: velocity drifts toward falling each frame
219
- ; Clamp terminal FALL speed to -8 px/frame — but ONLY while falling.
220
- ; The old unsigned compare (CMP #$F8 / BCS keep) also caught every
221
- ; POSITIVE velocity (5 < $F8 unsigned!), so the instant you jumped the
222
- ; clamp slammed P_VY from +6 to -8: the whole "jump" rose 0 frames,
223
- ; fell 8px and re-landed within ONE frame — jump sfx played, screen
224
- ; blipped, player never left the ground.
387
+ ; apply gravity to velocity, then velocity to position (signed integrate)
225
388
  LDA P_VY
226
- BPL .vyok ; rising (positive) → terminal clamp doesn't apply
227
- CMP #$F8
228
- BCS .vyok ; -8..-1 → within terminal speed, keep
229
- LDA #$F8 ; -128..-9 → clamp to -8
389
+ SEC
390
+ SBC #GRAVITY
230
391
  STA P_VY
231
- .vyok:
232
- ; P_Y += P_VY (signed add: sign-extend P_VY into the add)
233
- LDA P_VY
234
392
  CLC
235
- ADC P_Y
393
+ LDA P_Y
394
+ ADC P_VY
236
395
  STA P_Y
237
- .skipgrav:
238
-
239
- ; ── Land-on-top collision against the platform table ──
240
- ; Only while DESCENDING (P_VY negative = moving down the screen). For
241
- ; each platform, the stand-line = PLAT_Y + PH (the player's feet rest
242
- ; just above the band's top edge). If the player's feet (P_Y) have
243
- ; reached or just dropped through that line from above, and X is within
244
- ; the platform's span, snap onto it.
396
+ ; ceiling clamp (top of arena = NROWS*16 - HEROH)
397
+ CMP #(NROWS*ROWH - HEROH)
398
+ BCC .nceil
399
+ ; only clamp when RISING into the ceiling (large value, not a fall-wrap)
245
400
  LDA P_VY
246
- BPL .noland ; rising or stationary → can't land this frame
247
- LDX #0
248
- .landloop:
249
- LDA PLAT_Y,X
250
- CLC
251
- ADC #PH ; stand-line for this platform
252
- STA LANDY
253
- ; player at/below the stand-line? (P_Y <= LANDY, i.e. NOT P_Y > LANDY)
254
- LDA P_Y
255
- CMP LANDY
256
- BEQ .ydepth ; exactly on it
257
- BCS .nextplat ; P_Y > LANDY → still above the surface → no land
258
- .ydepth:
259
- ; not fallen WAY past it (avoid grabbing a platform from underneath):
260
- ; require LANDY - P_Y <= 12.
261
- LDA LANDY
262
- SEC
263
- SBC P_Y
264
- CMP #13
265
- BCS .nextplat ; dropped >12px below → ignore
266
- ; x-span test: PLAT_XL <= P_X <= PLAT_XR
267
- LDA P_X
268
- CMP PLAT_XL,X
269
- BCC .nextplat
270
- CMP PLAT_XR,X
271
- BCS .nextplat ; (XR=159 + the +1 makes the whole row standable)
272
- ; LAND!
273
- LDA LANDY
401
+ BMI .nceil
402
+ LDA #(NROWS*ROWH - HEROH)
274
403
  STA P_Y
275
404
  LDA #0
405
+ STA P_VY ; bonk the head → stop rising
406
+ .nceil:
407
+
408
+ ; floor / fall-through clamp FIRST: P_Y is unsigned; a downward step past 0
409
+ ; wraps to a large value. Detect that and snap to the floor (row 0 top).
410
+ LDA P_Y
411
+ CMP #(NROWS*ROWH)
412
+ BCC .nfloor
413
+ LDA #0 ; wrapped (fell below the floor) → clamp to floor
414
+ STA P_Y
276
415
  STA P_VY
277
416
  LDA #1
278
417
  STA ON_GND
279
- JMP .landdone
280
- .nextplat:
281
- INX
282
- CPX #NUM_PLAT
283
- BNE .landloop
284
- ; matched nothing still airborne
418
+ JMP .landDone
419
+ .nfloor:
420
+
421
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
422
+ ; LAND-ON-LEDGE collision — done in CODE, not via a TIA latch. A shooter
423
+ ; can ask the TIA "did anything overlap?", but a platformer must know
424
+ ; WHICH surface stopped the fall, to settle the hero exactly on top of it.
425
+ ; Rule: only when FALLING (P_VY <= 0), look up the LEVEL row at the hero's
426
+ ; FEET and test whether a solid PF pixel sits under his X column. If yes,
427
+ ; snap his feet to that row's top, zero the velocity, and mark ON_GND for
428
+ ; next frame's jump gate.
285
429
  LDA #0
286
430
  STA ON_GND
287
- .landdone:
288
- .noland:
289
-
290
- ; Safety floor: never let the player fall off the bottom of the world.
431
+ LDA P_VY
432
+ BPL .landDone ; rising (P_VY > 0) → can't land
433
+ ; falling. Which row are the feet in? row = P_Y / 16.
291
434
  LDA P_Y
292
- CMP #18
293
- BCS .floorok
294
- LDA #26
435
+ LSR
436
+ LSR
437
+ LSR
438
+ LSR
439
+ CMP #NROWS
440
+ BCC .rowok
441
+ LDA #(NROWS-1)
442
+ .rowok:
443
+ STA TMP ; TMP = row index
444
+ JSR ground_under_hero ; C=1 if solid PF bit under P_X in row TMP
445
+ BCC .landDone
446
+ ; snap feet to the TOP of this row (row*16)
447
+ LDA TMP
448
+ ASL
449
+ ASL
450
+ ASL
451
+ ASL ; row*16
295
452
  STA P_Y
296
453
  LDA #0
297
454
  STA P_VY
298
455
  LDA #1
299
456
  STA ON_GND
300
- .floorok:
457
+ .landDone:
458
+
459
+ ; ── GAME LOGIC (clay) — the COIN bounces in place; grab it to score ────
460
+ ; The coin (TIA ball, BL) bounces vertically between COIN_FLR and
461
+ ; COIN_CEIL. Touch it (P0/BL collision latch) → score + respawn it at a
462
+ ; new pseudo-random column.
463
+ LDA COIN_Y
464
+ CLC
465
+ ADC COIN_VY
466
+ STA COIN_Y
467
+ CMP #COIN_CEIL
468
+ BCC .ncoinTop
469
+ LDA #$FF ; reverse to falling
470
+ STA COIN_VY
471
+ .ncoinTop:
472
+ LDA COIN_Y
473
+ CMP #COIN_FLR
474
+ BCS .ncoinBot
475
+ LDA #1 ; reverse to rising
476
+ STA COIN_VY
477
+ .ncoinBot:
301
478
 
302
- ; ── sfx countdown ──
479
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
480
+ ; Coin pickup AND spike death both use the TIA's hardware collision
481
+ ; LATCHES (P0/ball, P0/missile0). The TIA detects pixel overlap in silicon
482
+ ; as it draws; we read the latched result here, one frame later, free.
483
+ ; Rules: latches accumulate until CXCLR — clear them EVERY frame (we do at
484
+ ; the end of this block), or a stale hit fires phantom pickups/deaths long
485
+ ; after the object moved.
486
+ BIT CXP0FB ; bit6 (V flag) = P0/ball (hero/coin) overlapped
487
+ BVC .noCoin
488
+ JSR add_score ; +10
489
+ JSR respawn_coin
490
+ LDA #$08 ; coin chime
491
+ LDX #$0C
492
+ LDY #8
493
+ JSR sfx_play
494
+ .noCoin:
495
+
496
+ ; ── GAME LOGIC (clay) — the SPIKE patrols a ledge left/right ───────────
497
+ LDA SPK_X
498
+ CLC
499
+ ADC SPK_DIR
500
+ STA SPK_X
501
+ CMP #WALK_HI
502
+ BCC .nspkR
503
+ LDA #$FF
504
+ STA SPK_DIR
505
+ .nspkR:
506
+ LDA SPK_X
507
+ CMP #WALK_LO
508
+ BCS .nspkL
509
+ LDA #1
510
+ STA SPK_DIR
511
+ .nspkL:
512
+
513
+ BIT CXM0P ; bit7 (N flag) = missile0/P0 (spike/hero) overlapped
514
+ BPL .noDeath
515
+ JMP do_game_over
516
+ .noDeath:
517
+ STA CXCLR ; arm BOTH latches fresh for the frame we draw next
518
+
519
+ ; pick the rows for the kernel (Y/16) so the kernel doesn't divide per line
520
+ LDA P_Y
521
+ LSR
522
+ LSR
523
+ LSR
524
+ LSR
525
+ STA P_ROW
526
+ LDA COIN_Y
527
+ LSR
528
+ LSR
529
+ LSR
530
+ LSR
531
+ STA COIN_ROW
532
+ LDA SPK_Y
533
+ LSR
534
+ LSR
535
+ LSR
536
+ LSR
537
+ STA SPK_ROW
538
+
539
+ ; decay the walk-animation frame back to idle when not pressing
540
+ LDA GFXIDX
541
+ BEQ .pk
542
+ DEC GFXIDX
543
+ .pk:
544
+ JMP pack_score ; render SCORE into S0BUF (tail-RTS ends frame_logic)
545
+
546
+ ; ── GAME LOGIC (clay — reshape freely) ── game-over freeze-frame ───────
547
+ logic_over:
548
+ LDA EDGEB
549
+ AND #$01
550
+ BNE .toTitle
551
+ LDA FIRE_EDG
552
+ BMI .toTitle
553
+ DEC OVER_T
554
+ BNE .stay
555
+ .toTitle:
556
+ JMP enter_title
557
+ .stay:
558
+ RTS
559
+
560
+ ; ── GAME LOGIC (clay — reshape freely) ── helpers ──────────────────────
561
+
562
+ ; ground_under_hero — is there a solid LEVEL pixel under the hero's column
563
+ ; in row TMP? Returns C=1 if solid (can stand), C=0 if pit/gap.
564
+ ; The hero stands at a coarse 1-of-40 column: PF pixel = P_X/4 across the 40
565
+ ; playfield pixels of a (reflect-mode) symmetric arena. We fold the right
566
+ ; half back, then test the right PF register+bit. Code reads the SAME LEVEL
567
+ ; table the kernel draws, so picture and physics never disagree.
568
+ ground_under_hero:
569
+ LDA TMP
570
+ CMP #NROWS
571
+ BCC .lok
572
+ LDA #(NROWS-1)
573
+ .lok:
574
+ STA TMP2
575
+ ASL
576
+ CLC
577
+ ADC TMP2 ; row*3 = LEVEL byte offset (PF0,PF1,PF2 per row)
578
+ TAX ; X = LEVEL offset
579
+ ; coarse playfield pixel index 0..39 from P_X (160 visible px / 4 = 40)
580
+ LDA P_X
581
+ LSR
582
+ LSR ; P_X/4
583
+ STA TMP2
584
+ ; In REFLECT mode the right half (px 20..39) mirrors the left, so fold any
585
+ ; index >= 20 down to 0..19 reading from the right edge inward.
586
+ CMP #20
587
+ BCC .left
588
+ LDA #39
589
+ SEC
590
+ SBC TMP2 ; 39 - idx → 0..19
591
+ STA TMP2
592
+ .left:
593
+ ; TMP2 = 0..19 into the 20-pixel half. Which register/bit?
594
+ ; px 0..3 → PF0 bits 4..7 (bit4 = leftmost)
595
+ ; px 4..11 → PF1 bits 7..0 (bit7 = leftmost)
596
+ ; px 12..19 → PF2 bits 0..7 (bit0 = leftmost)
597
+ LDA TMP2
598
+ CMP #4
599
+ BCS .notPF0
600
+ ; PF0: pixel n (0..3) → bit (4+n)
601
+ CLC
602
+ ADC #4
603
+ TAY ; Y = bit index in PF0
604
+ LDA LEVEL,X ; PF0 byte
605
+ JMP .testBit
606
+ .notPF0:
607
+ CMP #12
608
+ BCS .pf2
609
+ ; PF1: pixel n (4..11) → bit (11-n)
610
+ STA TMP2
611
+ LDA #11
612
+ SEC
613
+ SBC TMP2
614
+ TAY ; Y = bit index in PF1
615
+ LDA LEVEL+1,X ; PF1 byte
616
+ JMP .testBit
617
+ .pf2:
618
+ ; PF2: pixel n (12..19) → bit (n-12) (bit0 = leftmost)
619
+ SEC
620
+ SBC #12
621
+ TAY ; Y = bit index in PF2
622
+ LDA LEVEL+2,X ; PF2 byte
623
+ .testBit:
624
+ ; shift the chosen bit (Y) down to bit0, return C = that bit
625
+ CPY #0
626
+ BEQ .haveBit
627
+ .shloop:
628
+ LSR
629
+ DEY
630
+ BNE .shloop
631
+ .haveBit:
632
+ AND #$01
633
+ CMP #$01 ; sets C if the bit was 1 (solid ground)
634
+ RTS
635
+
636
+ respawn_coin: ; place the coin at a new pseudo-random column + reset Y
637
+ LDA FRAME
638
+ AND #$7F
639
+ CLC
640
+ ADC #20 ; 20..147 column
641
+ CMP #WALK_HI
642
+ BCC .cok
643
+ LDA #100
644
+ .cok:
645
+ STA COIN_X
646
+ LDA #COIN_CEIL
647
+ STA COIN_Y
648
+ LDA #$FF
649
+ STA COIN_VY ; start it falling
650
+ RTS
651
+
652
+ add_score: ; +10 points, BCD, capped at 9990, tracks session hi
653
+ SED
654
+ LDA SCORE
655
+ CLC
656
+ ADC #$10 ; tens place +1 → +10 points
657
+ STA SCORE
658
+ LDA SCORE_HI
659
+ ADC #0 ; carry into the high byte
660
+ STA SCORE_HI
661
+ CLD
662
+ ; keep the running session hi-score
663
+ LDA SCORE_HI
664
+ CMP SCORE_HSH
665
+ BCC .nohs
666
+ BNE .seths
667
+ LDA SCORE
668
+ CMP SCORE_HSV
669
+ BCC .nohs
670
+ .seths:
671
+ LDA SCORE
672
+ STA SCORE_HSV
673
+ LDA SCORE_HI
674
+ STA SCORE_HSH
675
+ .nohs:
676
+ RTS
677
+
678
+ do_game_over:
679
+ LDA #2
680
+ STA STATE
681
+ LDA #200 ; ~3.3 s freeze, then auto-return to title
682
+ STA OVER_T
683
+ LDA #0
684
+ STA ENABL
685
+ STA ENAM0
686
+ STA GRP0
687
+ LDA #1
688
+ STA TUNE_SEL
689
+ JMP tune_start ; game-over tune on voice 1
690
+
691
+ start_game:
692
+ LDA #0
693
+ STA SCORE
694
+ STA SCORE_HI
695
+ STA TUNE_LEFT ; silence the title jingle
696
+ STA AUDV1
697
+ STA P_VY
698
+ STA GFXIDX
699
+ LDA #1
700
+ STA ON_GND
701
+ LDA #76
702
+ STA P_X
703
+ LDA #(2*ROWH) ; start standing on row 2's ledge
704
+ STA P_Y
705
+ ; coin
706
+ LDA #110
707
+ STA COIN_X
708
+ LDA #COIN_CEIL
709
+ STA COIN_Y
710
+ LDA #$FF
711
+ STA COIN_VY
712
+ ; spike patrols row 1
713
+ LDA #40
714
+ STA SPK_X
715
+ LDA #(1*ROWH)
716
+ STA SPK_Y
717
+ LDA #1
718
+ STA SPK_DIR
719
+ LDA #1
720
+ STA STATE
721
+ LDA #$06 ; start blip
722
+ LDX #$04
723
+ LDY #10
724
+ JMP sfx_play
725
+
726
+ enter_title:
727
+ LDA #0
728
+ STA STATE
729
+ STA GRP0
730
+ STA ENABL
731
+ STA ENAM0
732
+ STA AUDV0
733
+ STA SFX_LEFT
734
+ STA TUNE_SEL ; title jingle
735
+ JMP tune_start
736
+
737
+ digit_times6: ; A = digit 0-9 → A = digit*6 (DIGITS row index)
738
+ STA TMP
739
+ ASL
740
+ CLC
741
+ ADC TMP ; *3
742
+ ASL ; *6
743
+ RTS
744
+
745
+ ; pack_two_digits — A = a BCD byte (two digits). Writes 6 rows into SCRATCH,
746
+ ; left digit (high nibble) in PF1 high nibble, right digit (low nibble) in
747
+ ; PF1 low nibble. In SCORE mode the byte draws twice (two colors) — the
748
+ ; classic dual-score look — but here both halves carry the SAME packed pair.
749
+ pack_two_digits:
750
+ PHA
751
+ LSR
752
+ LSR
753
+ LSR
754
+ LSR ; high (tens) digit
755
+ JSR digit_times6
756
+ TAX
757
+ LDY #0
758
+ .pd0:
759
+ LDA DIGITS,X
760
+ STA SCRATCH,Y ; high nibble of font = left digit
761
+ INX
762
+ INY
763
+ CPY #6
764
+ BNE .pd0
765
+ PLA
766
+ AND #$0F ; low (ones) digit
767
+ JSR digit_times6
768
+ TAX
769
+ LDY #0
770
+ .pd1:
771
+ LDA DIGITS,X
772
+ LSR
773
+ LSR
774
+ LSR
775
+ LSR ; ones in the LOW nibble
776
+ ORA SCRATCH,Y
777
+ STA SCRATCH,Y
778
+ INX
779
+ INY
780
+ CPY #6
781
+ BNE .pd1
782
+ RTS
783
+
784
+ pack_score: ; render the low two SCORE digits into S0BUF
785
+ LDA SCORE
786
+ JSR pack_two_digits
787
+ LDY #0
788
+ .pks:
789
+ LDA SCRATCH,Y
790
+ STA S0BUF,Y
791
+ INY
792
+ CPY #6
793
+ BNE .pks
794
+ RTS
795
+
796
+ ; ── GAME LOGIC (clay — reshape freely) ── TIA sound ────────────────────
797
+ ; Voice 0 = one-shot sound effects; voice 1 = the jingle player. Keeping
798
+ ; them on separate voices means a jump blip never cuts the tune off.
799
+ sfx_play: ; A = AUDF pitch, X = AUDC waveform, Y = frames
800
+ STA AUDF0
801
+ STX AUDC0
802
+ STY SFX_LEFT
803
+ LDA #$0C
804
+ STA AUDV0
805
+ RTS
806
+
807
+ tune_start: ; TUNE_SEL chosen by caller (0 title, 1 game over)
808
+ LDA #0
809
+ STA TUNE_POS
810
+ JSR tune_note
811
+ LDA #$04 ; pure square wave
812
+ STA AUDC1
813
+ LDA #$06
814
+ STA AUDV1
815
+ LDA #10
816
+ STA TUNE_LEFT
817
+ RTS
818
+
819
+ tune_note: ; load AUDF1 from the selected table at TUNE_POS;
820
+ LDX TUNE_POS ; returns Z set (A=0) on the $FF terminator
821
+ LDA TUNE_SEL
822
+ BNE .tn1
823
+ LDA TITLE_TUNE,X
824
+ JMP .tn2
825
+ .tn1:
826
+ LDA OVER_TUNE,X
827
+ .tn2:
828
+ CMP #$FF
829
+ BEQ .tnEnd
830
+ STA AUDF1
831
+ LDA #1
832
+ RTS
833
+ .tnEnd:
834
+ LDA #0
835
+ STA AUDV1
836
+ RTS
837
+
838
+ audio_tick: ; called once per frame, every state
303
839
  LDA SFX_LEFT
304
- BEQ .sfxdone
840
+ BEQ .at1
305
841
  DEC SFX_LEFT
306
- BNE .sfxdone
842
+ BNE .at1
307
843
  LDA #0
308
- STA AUDV0
309
- .sfxdone:
844
+ STA AUDV0 ; sfx finished → silence voice 0
845
+ .at1:
846
+ LDA TUNE_LEFT
847
+ BEQ .at2
848
+ DEC TUNE_LEFT
849
+ BNE .at2
850
+ INC TUNE_POS
851
+ JSR tune_note
852
+ BEQ .at2 ; hit the terminator → tune stays off
853
+ LDA #10
854
+ STA TUNE_LEFT
855
+ .at2:
856
+ RTS
310
857
 
311
- ; ── Position P0 at column P_X (1 WSYNC, counted in VBLANK) ──
858
+ ; ──────────────────────────────────────────────────────────────────────
859
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
860
+ ; OBJECT POSITIONING — the canonical SBC-#15 beam-race. There is no "X
861
+ ; register" for sprites: you strobe RESP0/RESBL/RESM0 and the object lands
862
+ ; WHEREVER THE BEAM IS. Each SBC/BCS lap is 5 CPU cycles = 15 beam pixels,
863
+ ; so when the subtraction underflows the beam has crossed x/15 coarse
864
+ ; columns; the remainder, EOR #7 shifted to the high nibble, becomes the
865
+ ; ±7px fine offset HMOVE applies on the next line. The naive "divide first,
866
+ ; then burn a delay loop" version lands in the WRONG column. Three objects =
867
+ ; three WSYNC lines + one shared HMOVE line, all inside timed VBLANK.
868
+ ; ──────────────────────────────────────────────────────────────────────
869
+ position_objects:
312
870
  STA WSYNC
313
871
  STA HMCLR
314
- LDX P_X
315
- LDA #0
316
- .p0pos:
317
- CPX #15
318
- BCC .p0done
872
+ LDA P_X ; hero → P0
873
+ STA WSYNC
319
874
  SEC
875
+ .d0:
320
876
  SBC #15
321
- TAX
322
- JMP .p0pos
323
- .p0done:
877
+ BCS .d0
878
+ EOR #7
879
+ ASL
880
+ ASL
881
+ ASL
882
+ ASL
324
883
  STA RESP0
325
- STA HMOVE
884
+ STA HMP0
885
+ LDA COIN_X ; coin → BL
886
+ STA WSYNC
887
+ SEC
888
+ .d1:
889
+ SBC #15
890
+ BCS .d1
891
+ EOR #7
892
+ ASL
893
+ ASL
894
+ ASL
895
+ ASL
896
+ STA RESBL
897
+ STA HMBL
898
+ LDA SPK_X ; spike → M0
899
+ STA WSYNC
900
+ SEC
901
+ .d2:
902
+ SBC #15
903
+ BCS .d2
904
+ EOR #7
905
+ ASL
906
+ ASL
907
+ ASL
908
+ ASL
909
+ STA RESM0
910
+ STA HMM0
911
+ STA WSYNC
912
+ STA HMOVE ; one HMOVE applies ALL the fine offsets; it must
913
+ RTS ; come fresh after a WSYNC (mid-line HMOVE combs)
326
914
 
915
+ ; ──────────────────────────────────────────────────────────────────────
916
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
917
+ ; THE PLAY/GAME-OVER KERNEL — 192 visible lines, fully accounted:
918
+ ; 24 = score bar + 144 = arena (9 rows × 16) + 24 = pad = 192
919
+ ;
920
+ ; SCORE BAR (SCORE mode): CTRLPF = $02 colors the LEFT playfield half with
921
+ ; COLUP0 and the RIGHT half with COLUP1 — a two-color scoreboard with zero
922
+ ; sprites. We stream the packed score digits into PF1, one font row / 4 lines.
923
+ ;
924
+ ; ARENA: the level is NROWS bands of 16 lines. Each band reloads PF0/PF1/PF2
925
+ ; from the LEVEL table ONCE (the ledges/floor/pit for that row), in REFLECT
926
+ ; mode so 20 stored pixels mirror into a symmetric 40-pixel arena. Per
927
+ ; scanline we also test, from the row counter, whether the hero (P0), coin
928
+ ; (BL) or spike (M0) band is here and enable/disable that object's graphic.
929
+ ; Each test is a compare-and-store (no multiply) so the work fits 76 cycles.
930
+ ; Beam Y counts DOWN; level rows are drawn TOP first (row NROWS-1 → 0), so a
931
+ ; lit pixel in a HIGH row index draws nearer the top, matching level space.
932
+ ; ──────────────────────────────────────────────────────────────────────
933
+ play_kernel:
934
+ ; positioning runs first, inside the still-blanked region
935
+ JSR position_objects
936
+
937
+ LDA #COL_SKY
938
+ STA COLUBK
327
939
  LDA #0
328
- STA VBLANK
940
+ STA PF0
941
+ STA PF1
942
+ STA PF2
943
+ STA GRP0
944
+ STA ENABL
945
+ STA ENAM0
946
+ STA VBLANK ; beam on
947
+ LDA #COL_HUD
948
+ STA COLUPF ; score digits bright
949
+ LDA #$02
950
+ STA CTRLPF ; SCORE mode for the score bar
329
951
 
330
- ; ── Visible (192 lines) SINGLE-LINE KERNEL reading a PF row buffer ──
331
- ; CRITICAL CYCLE NOTE: the platforms are STATIC, so we DON'T recompute
332
- ; them per scanline (a per-line JSR over the platform table overflowed
333
- ; the 76-cycle budget → frames grew to ~250 lines → no vsync lock →
334
- ; black rolling screen — the bug this kernel fixes). Instead PFROW[] is
335
- ; a 96-byte buffer (one entry per 2-line row) filled ONCE at boot from
336
- ; the platform table: $FF = platform here, $00 = open air. The kernel
337
- ; just LDA PFROW,X / STA PF1 / STA PF2 (cheap) + a single player-sprite
338
- ; test. That comfortably fits one scanline.
339
- ;
340
- ; X = row index 0..95 (top→bottom in buffer order). Y = beam scanline
341
- ; 192→1. We draw two scanlines per buffer row.
342
- LDX #0 ; PFROW index
343
- LDY #192
344
- .draw:
952
+ ; ---- score bar: 24 lines (6 font rows × 4) ----
953
+ LDX #0
954
+ .sbar:
345
955
  STA WSYNC
346
- ; --- playfield for this row (full-width bars) ---
347
- LDA PFROW,X
956
+ TXA
957
+ LSR
958
+ LSR
959
+ TAY ; row = line/4
960
+ LDA S0BUF,Y
961
+ STA PF1
962
+ INX
963
+ CPX #24
964
+ BNE .sbar
965
+
966
+ ; transition: clear the bar, switch the TIA to the arena (REFLECT mode so
967
+ ; the 20-pixel level mirrors symmetric), ledges in green.
968
+ STA WSYNC
969
+ LDA #0
970
+ STA PF1
971
+ LDA #$11 ; REFLECT (bit0) + 2px ball (bits 4-5 = 01)
972
+ STA CTRLPF
973
+ LDA #COL_LEDGE
974
+ STA COLUPF
975
+
976
+ ; ---- arena: NROWS rows × 16 lines, top row (NROWS-1) drawn first ----
977
+ LDX #(NROWS-1) ; X = current level row
978
+ .rowLoop:
979
+ ; reload PF for this row (once per 16 lines). Row table offset = X*3.
980
+ TXA
981
+ STA TMP ; save row index for the per-line object tests
982
+ ASL
983
+ CLC
984
+ ADC TMP ; X*3
985
+ TAY
986
+ LDA LEVEL,Y
348
987
  STA PF0
988
+ LDA LEVEL+1,Y
349
989
  STA PF1
990
+ LDA LEVEL+2,Y
350
991
  STA PF2
351
- ; --- player sprite test ---
992
+
993
+ LDY #ROWH ; 16 scanlines for this row (Y = 16..1)
994
+ .lineLoop:
995
+ STA WSYNC
996
+ ; sub-line within the row, 0 (top) .. 15 (bottom) = ROWH - Y
997
+ ; hero: drawn if this row == P_ROW and sub < HEROH
998
+ LDA P_ROW
999
+ CMP TMP
1000
+ BNE .noHero
352
1001
  TYA
353
- SEC
354
- SBC P_Y
355
- CMP #PH
356
- BCS .pblank
357
- STY TMP ; save beam line
358
- TAY
359
- LDA PLAYER,Y
1002
+ EOR #$FF
1003
+ CLC
1004
+ ADC #(ROWH+1) ; sub = ROWH - Y
1005
+ CMP #HEROH
1006
+ BCS .noHero
1007
+ CLC
1008
+ ADC GFXIDX ; pick idle (0) or walk (6) frame base
1009
+ TAX
1010
+ LDA HERO,X
360
1011
  STA GRP0
361
- LDY TMP
362
- JMP .pdone
363
- .pblank:
1012
+ LDX TMP ; restore row index
1013
+ JMP .heroDone
1014
+ .noHero:
364
1015
  LDA #0
365
1016
  STA GRP0
366
- .pdone:
1017
+ .heroDone:
1018
+
1019
+ ; coin (BL): enabled if this row == COIN_ROW and sub-line < 4
1020
+ LDA COIN_ROW
1021
+ CMP TMP
1022
+ BNE .noCoinK
1023
+ TYA
1024
+ EOR #$FF
1025
+ CLC
1026
+ ADC #(ROWH+1)
1027
+ CMP #4
1028
+ BCS .noCoinK
1029
+ LDA #2
1030
+ STA ENABL
1031
+ JMP .coinDone
1032
+ .noCoinK:
1033
+ LDA #0
1034
+ STA ENABL
1035
+ .coinDone:
1036
+
1037
+ ; spike (M0): enabled if this row == SPK_ROW and sub-line < 6
1038
+ LDA SPK_ROW
1039
+ CMP TMP
1040
+ BNE .noSpikeK
1041
+ TYA
1042
+ EOR #$FF
1043
+ CLC
1044
+ ADC #(ROWH+1)
1045
+ CMP #6
1046
+ BCS .noSpikeK
1047
+ LDA #2
1048
+ STA ENAM0
1049
+ JMP .spikeDone
1050
+ .noSpikeK:
1051
+ LDA #0
1052
+ STA ENAM0
1053
+ .spikeDone:
1054
+
367
1055
  DEY
368
- ; second scanline of this row — reuse same PF, re-test the sprite.
1056
+ BNE .lineLoop
1057
+
1058
+ LDX TMP ; restore row counter (clobbered by the hero pick)
1059
+ DEX
1060
+ BPL .rowLoop
1061
+
1062
+ ; pad to reach exactly 192 visible (24 bar + 144 arena = 168 → +24 pad)
1063
+ LDA #0
1064
+ STA GRP0
1065
+ STA ENABL
1066
+ STA ENAM0
1067
+ STA PF0
1068
+ STA PF1
1069
+ STA PF2
1070
+ LDX #24
1071
+ .pad:
369
1072
  STA WSYNC
370
- STA GRP0 ; (A still holds the sprite/blank byte from above —
371
- ; good enough; sprite is effectively 2px tall rows)
372
- DEY
373
- INX
374
- CPX #96
375
- BNE .draw
1073
+ DEX
1074
+ BNE .pad
1075
+
1076
+ JMP kernel_done
376
1077
 
377
- ; ── Overscan (30 lines) ──
1078
+ ; ──────────────────────────────────────────────────────────────────────
1079
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
1080
+ ; THE TITLE KERNEL — 192 lines, banded:
1081
+ ; 24 blank + 28 banner "PERCH" + 8 gap + 28 banner "PATROL" + 16 gap +
1082
+ ; 24 hi-score + remainder pad = 192
1083
+ ;
1084
+ ; The banner is an ASYMMETRIC PLAYFIELD — the 2600's only way to draw
1085
+ ; full-width artwork. The playfield registers hold just 20 pixels; the TIA
1086
+ ; replays them for the right half of the line (CTRLPF bit0 chooses repeat
1087
+ ; or mirror). For 40 INDEPENDENT pixels you rewrite all three registers
1088
+ ; mid-line, each inside its window (CPU cycle = 3 color clocks; left copy
1089
+ ; reads at clocks 68-147, right copy at 148-227):
1090
+ ; PF0 again after cycle ~28 (left copy drawn) before ~49 (right copy reads)
1091
+ ; PF1 again after cycle ~39 before ~54
1092
+ ; PF2 again after cycle ~50 before ~65
1093
+ ; The code below hits those windows by instruction order alone — count
1094
+ ; cycles before you reorder ANYTHING between the WSYNC and the last STA.
1095
+ ; REQUIRES: CTRLPF bit0 = 0 (repeat mode). In mirror mode the right half
1096
+ ; reads the registers in REVERSE order and every window above is wrong.
1097
+ ; ──────────────────────────────────────────────────────────────────────
1098
+ title_kernel:
1099
+ LDA #$84 ; deep blue backdrop
1100
+ STA COLUBK
378
1101
  LDA #0
379
1102
  STA PF0
380
1103
  STA PF1
381
1104
  STA PF2
382
1105
  STA GRP0
383
- LDA #2
384
- STA VBLANK
385
- LDX #30
386
- .os:
1106
+ STA ENABL
1107
+ STA ENAM0
1108
+ STA CTRLPF ; REPEAT mode — required by the banner (see above)
1109
+ STA VBLANK ; beam on
1110
+
1111
+ LDX #24 ; band 1: 24 blank lines
1112
+ .tb1:
387
1113
  STA WSYNC
388
1114
  DEX
389
- BNE .os
1115
+ BNE .tb1
390
1116
 
391
- JMP MAIN
1117
+ LDA #$3A ; word 1 in warm yellow
1118
+ STA COLUPF
1119
+ LDX #0 ; band 2: 28 banner lines (7 rows × 4)
1120
+ .ban1:
1121
+ STA WSYNC
1122
+ TXA ; row = line/4
1123
+ LSR
1124
+ LSR
1125
+ TAY
1126
+ LDA R1_PF0L,Y
1127
+ STA PF0
1128
+ LDA R1_PF1L,Y
1129
+ STA PF1
1130
+ LDA R1_PF2L,Y
1131
+ STA PF2
1132
+ LDA R1_PF0R,Y
1133
+ STA PF0
1134
+ LDA R1_PF1R,Y
1135
+ STA PF1
1136
+ NOP
1137
+ NOP
1138
+ LDA R1_PF2R,Y
1139
+ STA PF2
1140
+ INX
1141
+ CPX #28
1142
+ BNE .ban1
1143
+
1144
+ STA WSYNC ; band 3: clear + 7 gap lines
1145
+ LDA #0
1146
+ STA PF0
1147
+ STA PF1
1148
+ STA PF2
1149
+ LDX #7
1150
+ .tb3:
1151
+ STA WSYNC
1152
+ DEX
1153
+ BNE .tb3
392
1154
 
393
- ; ── build_pfrow: fill the 96-byte PFROW buffer from the platform table.
394
- ; Called ONCE at boot. Row r covers beam scanlines (192 - 2*r) down to
395
- ; (191 - 2*r). A row is a platform if its top scanline falls within any
396
- ; platform's PLAT_Y..PLAT_Y+(bandHeight) window. ──
397
- PF_BAND = 8 ; platform visual thickness in scanlines
398
- build_pfrow:
399
- LDX #0 ; row index 0..95
400
- .brow:
401
- ; beam scanline for this row = 192 - 2*X
1155
+ LDA #$C6 ; word 2 in green
1156
+ STA COLUPF
1157
+ LDX #0 ; band 4: 28 banner lines, word 2
1158
+ .ban2:
1159
+ STA WSYNC
402
1160
  TXA
403
- ASL
404
- STA TMP ; TMP = 2*X
405
- LDA #192
406
- SEC
407
- SBC TMP
408
- STA LANDY ; LANDY reused as "this row's scanline"
409
- ; test against each platform
410
- LDY #0
1161
+ LSR
1162
+ LSR
1163
+ TAY
1164
+ LDA R2_PF0L,Y
1165
+ STA PF0
1166
+ LDA R2_PF1L,Y
1167
+ STA PF1
1168
+ LDA R2_PF2L,Y
1169
+ STA PF2
1170
+ LDA R2_PF0R,Y
1171
+ STA PF0
1172
+ LDA R2_PF1R,Y
1173
+ STA PF1
1174
+ NOP
1175
+ NOP
1176
+ LDA R2_PF2R,Y
1177
+ STA PF2
1178
+ INX
1179
+ CPX #28
1180
+ BNE .ban2
1181
+
1182
+ STA WSYNC ; band 5: clear + 15 gap lines
411
1183
  LDA #0
412
- STA PFROW,X ; default open air
413
- .bplat:
414
- LDA LANDY
415
- SEC
416
- SBC PLAT_Y,Y
417
- CMP #PF_BAND
418
- BCS .bnext
419
- ; within a platform band → mark solid
420
- LDA #$FF
421
- STA PFROW,X
422
- .bnext:
423
- INY
424
- CPY #NUM_PLAT
425
- BNE .bplat
1184
+ STA PF0
1185
+ STA PF1
1186
+ STA PF2
1187
+ LDA #$02
1188
+ STA CTRLPF ; SCORE mode for the hi-score band
1189
+ LDX #15
1190
+ .tb5:
1191
+ STA WSYNC
1192
+ DEX
1193
+ BNE .tb5
1194
+
1195
+ ; band 6: hi-score, 24 lines (6 rows × 4). Packed digits stream into PF1;
1196
+ ; SCORE mode draws them twice in the two player colors. In-session best;
1197
+ ; honest: there is no battery — gone at power-off, like the arcades.
1198
+ LDA #COL_HUD
1199
+ STA COLUPF
1200
+ LDX #0
1201
+ .hsb:
1202
+ STA WSYNC
1203
+ TXA
1204
+ LSR
1205
+ LSR
1206
+ TAY
1207
+ LDA HSBUF,Y
1208
+ STA PF1
426
1209
  INX
427
- CPX #96
428
- BNE .brow
429
- RTS
1210
+ CPX #24
1211
+ BNE .hsb
430
1212
 
431
- ; ── Player sprite (8 rows) a little explorer ──
432
- PLAYER:
433
- .byte %00111100
1213
+ STA WSYNC ; band 7: clear + pad to exactly 192
1214
+ LDA #0
1215
+ STA PF1
1216
+ LDX #65
1217
+ .tb7:
1218
+ STA WSYNC
1219
+ DEX
1220
+ BNE .tb7
1221
+
1222
+ JMP kernel_done
1223
+
1224
+ ; ──────────────────────────────────────────────────────────────────────
1225
+ ; ── GAME LOGIC (clay — reshape freely) ── data tables ──────────────────
1226
+
1227
+ ; Digit font: 4 pixels wide × 6 rows, stored in the HIGH nibble (PF1 bit7
1228
+ ; is the LEFTMOST pixel of the left playfield half — high nibble = left).
1229
+ DIGITS:
1230
+ .byte $60,$90,$90,$90,$90,$60 ; 0
1231
+ .byte $20,$60,$20,$20,$20,$70 ; 1
1232
+ .byte $60,$90,$10,$20,$40,$F0 ; 2
1233
+ .byte $E0,$10,$60,$10,$10,$E0 ; 3
1234
+ .byte $90,$90,$F0,$10,$10,$10 ; 4
1235
+ .byte $F0,$80,$E0,$10,$10,$E0 ; 5
1236
+ .byte $60,$80,$E0,$90,$90,$60 ; 6
1237
+ .byte $F0,$10,$20,$40,$40,$40 ; 7
1238
+ .byte $60,$90,$60,$90,$90,$60 ; 8
1239
+ .byte $60,$90,$90,$70,$10,$60 ; 9
1240
+
1241
+ ; ── THE HERO SPRITE ───────────────────────────────────────────────────
1242
+ ; 6 rows tall, P0. Two frames stacked: idle (base 0) and a walk pose
1243
+ ; (base 6) selected by GFXIDX in the kernel — a cheap 1977-style 2-frame
1244
+ ; animation. Drawn TOP-row first (the kernel scans the band downward).
1245
+ HERO:
1246
+ ; idle (GFXIDX = 0)
434
1247
  .byte %00111100
1248
+ .byte %01111110
435
1249
  .byte %00011000
1250
+ .byte %00111100
1251
+ .byte %01100110
1252
+ .byte %01000010
1253
+ ; walk (GFXIDX = 6)
1254
+ .byte %00111100
436
1255
  .byte %01111110
437
- .byte %10111101
1256
+ .byte %00011000
438
1257
  .byte %00111100
439
1258
  .byte %00100100
440
- .byte %01100110
1259
+ .byte %01000010
1260
+
1261
+ ; Title jingle (voice 1, AUDC $04 square; AUDF divider — LOWER = higher
1262
+ ; pitch; 10 frames per note; $FF terminates). The table IS the song.
1263
+ TITLE_TUNE:
1264
+ .byte $1B,$17,$13,$0F,$13,$17,$13,$0F,$FF
1265
+ ; Game-over tune: a falling figure.
1266
+ OVER_TUNE:
1267
+ .byte $0F,$13,$17,$1B,$1F,$23,$FF
1268
+
1269
+ ; ── THE LEVEL ─────────────────────────────────────────────────────────
1270
+ ; NROWS rows × (PF0, PF1, PF2). A lit pixel = solid ground; a gap = pit.
1271
+ ; REFLECT mode mirrors the 20 stored pixels into a symmetric 40-px arena,
1272
+ ; so you only author the LEFT half — the arena is naturally left/right
1273
+ ; symmetric (for an asymmetric arena, switch the kernel to a per-line
1274
+ ; asymmetric reload like the title banner). Row 0 = BOTTOM (the floor),
1275
+ ; row NROWS-1 = TOP. PF bit order, as ever on the 2600:
1276
+ ; PF0: bits 4..7 used, bit4 = leftmost (reversed)
1277
+ ; PF1: bit7 = leftmost (normal)
1278
+ ; PF2: bit0 = leftmost (reversed)
1279
+ ; This is the workhorse "clay" of the file — every ledge, pit and the floor
1280
+ ; lives here; ground_under_hero reads the SAME table so code and picture
1281
+ ; never disagree. (The hero spawns on row 2; row 0 is a solid landing floor
1282
+ ; so a fall always ends on ground.)
1283
+ LEVEL:
1284
+ ; row 0 — solid floor (full left half → mirrored = full floor)
1285
+ .byte %11110000, %11111111, %11111111
1286
+ ; row 1 — a low ledge on the left (the spike's patrol band)
1287
+ .byte %11110000, %11110000, %00000000
1288
+ ; row 2 — a mid ledge (the hero's start perch)
1289
+ .byte %00000000, %00001111, %11110000
1290
+ ; row 3 — open (air)
1291
+ .byte %00000000, %00000000, %00000000
1292
+ ; row 4 — a high ledge near the wall
1293
+ .byte %11110000, %00000000, %00000000
1294
+ ; row 5 — open
1295
+ .byte %00000000, %00000000, %00000000
1296
+ ; row 6 — a small floating ledge centre-left
1297
+ .byte %00000000, %00111100, %00000000
1298
+ ; row 7 — open
1299
+ .byte %00000000, %00000000, %00000000
1300
+ ; row 8 — top cap ledge (a landing under the HUD)
1301
+ .byte %11110000, %00000011, %00000000
1302
+
1303
+ ; ── THE TITLE BANNER ──────────────────────────────────────────────────
1304
+ ; 40-pixel-wide artwork, 7 rows per word, drawn by the asymmetric-playfield
1305
+ ; kernel above. Each row is six bytes across six tables (left PF0/PF1/PF2,
1306
+ ; right PF0/PF1/PF2). PF bit order is the 2600's great prank — three
1307
+ ; registers, three different orders:
1308
+ ; PF0: only bits 4-7 used, bit 4 = LEFTMOST pixel (reversed)
1309
+ ; PF1: bit 7 = leftmost (normal)
1310
+ ; PF2: bit 0 = leftmost (reversed again)
1311
+ ;
1312
+ ; The 40-px art for each row is the comment ASCII above each table; the
1313
+ ; bytes are mechanically encoded from it (left half = pixels 0..19 → PF0
1314
+ ; bits4-7 / PF1 bits7-0 / PF2 bits0-7; right half = pixels 20..39 likewise).
1315
+ ;
1316
+ ; PERCH (P-E-R-C in the left copy, H in the right; +2px left pad to centre):
1317
+ ; #### #### #### .### #..#
1318
+ ; #..# #... #..# #... #..#
1319
+ ; #..# #... #..# #... #..#
1320
+ ; #### ###. ###. #... ####
1321
+ ; #... #... #.#. #... #..#
1322
+ ; #... #... #..# #... #..#
1323
+ ; #... #### #..# .### #..#
1324
+ R1_PF0L:
1325
+ .byte %11000000, %01000000, %01000000, %11000000, %01000000, %01000000, %01000000
1326
+ R1_PF1L:
1327
+ .byte %11011110, %01010000, %01010000, %11011100, %00010000, %00010000, %00011110
1328
+ R1_PF2L:
1329
+ .byte %11001111, %00101001, %00101001, %00100111, %00100101, %00101001, %11001001
1330
+ R1_PF0R:
1331
+ .byte %01010000, %01000000, %01000000, %11000000, %01000000, %01000000, %01010000
1332
+ R1_PF1R:
1333
+ .byte %01000000, %01000000, %01000000, %11000000, %01000000, %01000000, %01000000
1334
+ R1_PF2R:
1335
+ .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
1336
+
1337
+ ; PATROL (P-A-T-R in the left copy, O-L in the right):
1338
+ ; #### .##. #### #### .##. #...
1339
+ ; #..# #..# ..#. #..# #..# #...
1340
+ ; #..# #..# ..#. #..# #..# #...
1341
+ ; #### #### ..#. ###. #..# #...
1342
+ ; #... #..# ..#. #.#. #..# #...
1343
+ ; #... #..# ..#. #..# #..# #...
1344
+ ; #... #..# ..#. #..# .##. ####
1345
+ R2_PF0L:
1346
+ .byte %11110000, %10010000, %10010000, %11110000, %00010000, %00010000, %00010000
1347
+ R2_PF1L:
1348
+ .byte %00110011, %01001000, %01001000, %01111000, %01001000, %01001000, %01001000
1349
+ R2_PF2L:
1350
+ .byte %01111011, %01001001, %01001001, %00111001, %00101001, %01001001, %01001001
1351
+ R2_PF0R:
1352
+ .byte %01100000, %10010000, %10010000, %10010000, %10010000, %10010000, %01100000
1353
+ R2_PF1R:
1354
+ .byte %01000000, %01000000, %01000000, %01000000, %01000000, %01000000, %01111000
1355
+ R2_PF2R:
1356
+ .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
441
1357
 
442
- ; ── Platform table ────────────────────────────────────────────────────
443
- ; Parallel arrays indexed 0..NUM_PLAT-1. Y = band top scanline (beam
444
- ; coords: bigger Y = higher on screen). XL/XR = the column span for the
445
- ; land-on-top test. The bars render FULL WIDTH, so every span is the whole
446
- ; screen (you can stand anywhere on a platform — visual == collision). To
447
- ; make narrower ledges, give a platform a partial PFROW pattern AND shrink
448
- ; its XL/XR here so the two stay in sync.
449
- PLAT_Y:
450
- .byte 18 ; floor (bottom)
451
- .byte 70 ; ledge
452
- .byte 110 ; ledge
453
- .byte 150 ; ledge (top)
454
- PLAT_XL:
455
- .byte 0
456
- .byte 0
457
- .byte 0
458
- .byte 0
459
- PLAT_XR:
460
- .byte 159
461
- .byte 159
462
- .byte 159
463
- .byte 159
464
-
465
- ; ── Vector table ──
1358
+ ; ── Vector table ──────────────────────────────────────────────────────
466
1359
  org $FFFA
467
1360
  .word START
468
1361
  .word START