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,32 +1,72 @@
1
- ; ── shmup.asm — Atari 2600 SHMUP genre scaffold ─────────────────────
1
+ ; ── shmup.asm — FLAK FRENZY — Atari 2600 gallery shooter (complete game) ─────
2
2
  ;
3
- ; The 2600's flagship genreSpace Invaders, Galaxian, Demon Attack
4
- ; were all fixed/gallery shooters, because that is exactly what the TIA
5
- ; is good at. This is the canonical 2600 shmup, identical in spirit to
6
- ; the verified `mini_invaders` template.
3
+ ; A COMPLETE, working gamedrawn title screen, a fixed/gallery shooter
4
+ ; (your cannon vs a marching formation of replicated invaders), score +
5
+ ; in-session hi-score, TIA sound effects + a title jingle, game-over with
6
+ ; auto-return to the title, and the 2600's signature feature: THE WHOLE
7
+ ; MACHINE. There is no framebuffer, no tilemap, no OS — every visible
8
+ ; scanline below is composed live by racing the beam, and this file
9
+ ; teaches the gallery-shooter's load-bearing TIA tricks while doing it:
7
10
  ;
8
- ; A fixed-shooter / gallery-shooter 2600 game that uses the RIGHT TIA
9
- ; objects instead of playfield "barcode" bars (see the note at the bottom).
11
+ ; 1. NUSIZ REPLICATION (the enemy formation) ONE GRP1 write paints
12
+ ; THREE invaders. NUSIZ1 = %011 (three medium-spaced copies) is how
13
+ ; Space Invaders / Galaxian / Demon Attack drew a whole row of aliens
14
+ ; from a single 8-pixel sprite. This is the genre's defining idiom and
15
+ ; the reason the 2600 is GOOD at this genre — playfield "barcode" bars
16
+ ; would read as stripes, not invaders. We replicate a 2x2 BLOCK of six
17
+ ; invaders by re-using P1 (GRP1) on a SECOND scanline band lower down.
18
+ ; 2. RESP/HMOVE BEAM POSITIONING (the SBC-#15 idiom) — there is no sprite
19
+ ; X register; you strobe RESPx WHERE THE BEAM IS, then nudge ±7px with
20
+ ; HMOVE. Three objects (ship P0, formation P1, shot M0) positioned this
21
+ ; way each frame, inside the timed VBLANK window.
22
+ ; 3. TIA COLLISION LATCHES (the hit detect) — the TIA detects M0/P1 pixel
23
+ ; overlap in silicon as it draws; we read the latched result one frame
24
+ ; later, free, instead of doing AABB math. Clear it every frame (CXCLR)
25
+ ; or a stale hit scores phantom kills.
26
+ ; 4. TIM64T/INTIM FRAME TIMING — set the RIOT timer for VBLANK/overscan and
27
+ ; let it absorb however much the game logic costs, instead of hand-
28
+ ; counting WSYNCs (which rolls the picture the moment logic grows).
10
29
  ;
11
- ; TIA object roles (the canonical layout for this genre):
12
- ; P0 = player cannon (double-width via NUSIZ0 = %00000111)
13
- ; P1 + NUSIZ1 = %011 (3 medium-spaced copies) = a ROW OF INVADERS
14
- ; one GRP1 write draws three aliens, hardware-replicated.
15
- ; M0 = the player's shot
16
- ; PF = a thin ground line only (NOT the aliens — playfield bits look
17
- ; like a barcode; real sprites read as actual invaders).
30
+ ; THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
31
+ ; very different one. The markers tell you what's what:
32
+ ; HARDWARE IDIOM (load-bearing) cycle-counted / footgun-dodging code;
33
+ ; reshape your gameplay around it (see TROUBLESHOOTING before changing).
34
+ ; GAME LOGIC (clay) — movement, scoring, tuning, art: reshape freely.
18
35
  ;
19
- ; The aliens march left/right as a block (move P1's X), drop a step at
20
- ; the edges, and you shoot upward with the joystick button. This is the
21
- ; deliberately-small but visually-honest version of the genre on the
22
- ; 2600 extend it by reusing P1 again lower down for shields, or
23
- ; adding M1 as an alien bomb.
36
+ ; GAME_TITLE: on the 2600 a title is DRAWN, not printed there is no font
37
+ ; hardware. The FLAK/FRENZY banner bitmaps near the bottom of this file ARE
38
+ ; the title; redraw them for your game (the comment above each table shows
39
+ ; the 40-pixel artwork and the PF0/PF1/PF2 bit-order encoding).
24
40
  ;
25
- ; NTSC kernel: 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan.
41
+ ; CONTROLS (documented for players and for the fork README):
42
+ ; Title: fire on JOYSTICK 0 (or console RESET) starts the game
43
+ ; Play: joystick 0 LEFT/RIGHT moves your cannon; fire launches a shot
44
+ ; (one shot in flight at a time, like the era's arcade games);
45
+ ; console RESET returns to the title
46
+ ; Clear the whole formation to advance; let it reach your row and it's
47
+ ; game over. Your best SCORE this session is shown on the title screen.
48
+ ;
49
+ ; PLAYERS — 1P, honest. The 2600 has two joystick ports, but this genre's
50
+ ; kernel is already spending its scanline budget on P0 (your ship), P1 with
51
+ ; NUSIZ replication (the formation), and M0 (the shot). A second human ship
52
+ ; would need its own positioned object competing for the SAME 76-cycle
53
+ ; lines the formation already fills — so like the arcade gallery shooters
54
+ ; this descends from, FLAK FRENZY is single-player. (To add 2P alternating
55
+ ; TURNS instead — cheap, no extra kernel objects — keep a second score/lives
56
+ ; pair and swap on death; left as an exercise.)
57
+ ;
58
+ ; HI-SCORE HONESTY: real 2600 cartridges had NO battery, NO SRAM, NO
59
+ ; persistence of any kind. The hi-score here lives in RIOT RAM ($A0) and
60
+ ; survives game → title cycles only WITHIN one power-on session — exactly
61
+ ; like the arcade machines of the era. Power off and it is gone. Do not
62
+ ; fake an EEPROM; state it honestly in your fork too.
63
+ ;
64
+ ; NTSC frame: 3 VSYNC + 37 VBLANK + 192 visible + 30 overscan = 262 lines.
26
65
 
27
66
  processor 6502
28
67
  org $F000
29
68
 
69
+ ; ── TIA write registers ───────────────────────────────────────────────
30
70
  VSYNC = $00
31
71
  VBLANK = $01
32
72
  WSYNC = $02
@@ -37,34 +77,79 @@ COLUP1 = $07
37
77
  COLUPF = $08
38
78
  COLUBK = $09
39
79
  CTRLPF = $0A
80
+ PF0 = $0D
81
+ PF1 = $0E
82
+ PF2 = $0F
40
83
  RESP0 = $10
41
84
  RESP1 = $11
42
85
  RESM0 = $12
43
86
  GRP0 = $1B
44
87
  GRP1 = $1C
45
88
  ENAM0 = $1D
46
- PF0 = $0D
47
89
  HMP0 = $20
48
90
  HMP1 = $21
49
91
  HMM0 = $22
50
92
  HMOVE = $2A
51
93
  HMCLR = $2B
52
- SWCHA = $280
53
- INPT4 = $0C ; P0 fire (active-low, bit 7)
94
+ CXCLR = $2C
95
+ ; ── TIA audio ─────────────────────────────────────────────────────────
54
96
  AUDC0 = $15
97
+ AUDC1 = $16
55
98
  AUDF0 = $17
99
+ AUDF1 = $18
56
100
  AUDV0 = $19
101
+ AUDV1 = $1A
102
+ ; ── TIA READ registers (separate read map — the same addresses as some
103
+ ; write strobes; e.g. CXM0P reads $00 while STA $00 strobes VSYNC) ──────
104
+ CXM0P = $00 ; bit7 = missile0 / player1 collision (latched)
105
+ INPT4 = $0C ; joystick 0 fire (bit7, ACTIVE LOW)
106
+ ; ── RIOT ──────────────────────────────────────────────────────────────
107
+ SWCHA = $280 ; joysticks: P0 = high nibble, P1 = LOW nibble
108
+ SWCHB = $282 ; console: bit0 RESET, bit1 SELECT (ACTIVE LOW)
109
+ INTIM = $284 ; timer read
110
+ TIM64T = $296 ; timer set, 64-cycle ticks
111
+
112
+ ; ── Zero-page state (the 2600's ENTIRE RAM is $80-$FF — 128 bytes; in
113
+ ; core memory dumps system_ram offset 0 = $80) ────────────────────────
114
+ STATE = $80 ; 0 = title, 1 = play, 2 = game over
115
+ P_X = $81 ; player cannon X column (visible 0..159)
116
+ FORM_X = $82 ; formation left edge (P1 X)
117
+ FORM_Y = $83 ; formation TOP scanline (beam counts 192→1, so a
118
+ ; SMALLER value = LOWER on screen = closer to you)
119
+ FORM_DIR = $84 ; +1 = marching right, $FF = marching left
120
+ ALIVE = $85 ; 6 bits = which formation cells still live (one per
121
+ ; invader: a kill clears its bit, kernel skips it)
122
+ SHOT_X = $86 ; missile column
123
+ SHOT_Y = $87 ; missile scanline (0 = no shot active)
124
+ SCORE = $88 ; current score, BCD (digit nibbles fall out free)
125
+ SCORE_HI = $89 ; current score high byte, BCD (hundreds/thousands)
126
+ FRAME = $8A
127
+ MARCH = $8B ; march step timer
128
+ SFX_LEFT = $8C ; frames remaining on the voice-0 sound effect
129
+ TUNE_SEL = $8D ; 0 = title jingle, 1 = game-over tune (voice 1)
130
+ TUNE_POS = $8E
131
+ TUNE_LEFT = $8F ; frames left on current jingle note (0 = silent)
132
+ OVER_T = $90 ; game-over auto-return-to-title countdown
133
+ SWCHB_PRV = $91 ; previous SWCHB for RESET edge detect
134
+ FIRE_PRV = $92 ; previous fire level (bit7) for fire-edge detect
135
+ EDGEB = $93 ; this frame's RESET press-edge (bit0)
136
+ FIRE_EDG = $94 ; this frame's fire press-edge (bit7)
137
+ TMP = $95
138
+ WAVE = $96 ; wave number (formation speeds up each wave)
139
+ MARCH_PERIOD = $97 ; frames per march step (set per wave; speeds up)
140
+ S0BUF = $98 ; 6 rows: packed score digits for the kernel
141
+ SCRATCH = $9E ; 6 bytes general kernel/packer scratch
142
+ SCORE_HSV = $A4 ; SESSION hi-score (BCD low byte). RAM only — real
143
+ SCORE_HSH = $A5 ; 2600 carts have no battery; honest by design.
144
+ HSBUF = $A6 ; 6 rows: hi-score, packed
57
145
 
58
- ; Zero page
59
- P_X = $80 ; player cannon X (visible column)
60
- ALIEN_X = $81 ; left edge of the alien block (P1 X)
61
- ALIEN_Y = $82 ; top scanline of the alien row
62
- ALIEN_DIR = $83 ; 1 = right, $FF = left
63
- SHOT_X = $84
64
- SHOT_Y = $85 ; $FF = no shot active
65
- FRAME = $86
66
- MARCH = $87 ; march timer
67
- SFX_LEFT = $88
146
+ SHIPGFX = %00111100 ; cannon top bar (full sprite in SHIP table below)
147
+ COL_SHIP = $1E ; yellow cannon
148
+ COL_FORM = $46 ; red invaders
149
+ COL_BG = $00 ; black space
150
+ COL_SHOT = $0E ; white shot
151
+ FORM_COLS = 3 ; 3 NUSIZ copies across
152
+ FORM_W = 32 ; formation block width in color clocks (for the edge)
68
153
 
69
154
  START:
70
155
  SEI
@@ -73,316 +158,979 @@ START:
73
158
  TXS
74
159
  LDA #0
75
160
  .clr:
76
- STA $00,X
77
- DEX
78
- BNE .clr
161
+ STA $00,X ; clears ALL of $00-$FF: zero page RAM AND the TIA
162
+ DEX ; write registers (GRP/ENAxx/HMxx/audio all silenced
163
+ BNE .clr ; — the standard 2600 power-on hygiene)
79
164
 
80
- LDA #76
81
- STA P_X
82
- LDA #40
83
- STA ALIEN_X
84
- LDA #60
85
- STA ALIEN_Y
86
- LDA #1
87
- STA ALIEN_DIR
88
- LDA #$FF
89
- STA SHOT_Y ; no shot yet
90
-
91
- ; Colours
92
- LDA #$06 ; dark blue-grey bg
93
- STA COLUBK
94
- LDA #$1E ; yellow cannon
165
+ ; Fixed identity colors (the kernels rewrite COLUPF per band, but the
166
+ ; object colors are constant all session).
167
+ LDA #COL_SHIP
95
168
  STA COLUP0
96
- LDA #$46 ; red-ish aliens
169
+ LDA #COL_FORM
97
170
  STA COLUP1
98
- LDA #$0A ; grey ground line
99
- STA COLUPF
100
-
101
- ; Object sizes: P0 double-width; P1 = 3 medium-spaced copies.
102
- LDA #%00000101 ; NUSIZ0: P0 double size (2x width)
171
+ ; NUSIZ0: single-width cannon. NUSIZ1: THREE medium-spaced copies — the
172
+ ; gallery-shooter idiom. Set ONCE; persists every frame.
173
+ LDA #%00000000
103
174
  STA NUSIZ0
104
- LDA #%00000011 ; NUSIZ1: three copies, medium spacing
175
+ LDA #%00000011 ; %011 = 3 copies, medium spacing
105
176
  STA NUSIZ1
106
- LDA #1
107
- STA CTRLPF ; PF reflected (symmetric ground)
108
177
 
109
- MAIN:
110
- INC FRAME
178
+ JSR enter_title
111
179
 
112
- ; ── VSYNC (3 lines) ──
180
+ ; ──────────────────────────────────────────────────────────────────────
181
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
182
+ ; THE FRAME LOOP. 262 scanlines, every frame, forever. VBLANK and overscan
183
+ ; are timed with the RIOT timer (TIM64T) instead of counted WSYNCs: set the
184
+ ; timer, run however much game logic the state needs, then spin on INTIM.
185
+ ; This is how shipped 2600 games did it, and it kills the classic homebrew
186
+ ; bug class where adding one branch to the game logic emits a 263rd line
187
+ ; and the TV loses vsync (rolling picture). The VISIBLE 192 lines are still
188
+ ; counted exactly — every STA WSYNC below is one scanline, and each state's
189
+ ; kernel accounts for all 192.
190
+ ; ──────────────────────────────────────────────────────────────────────
191
+ MAIN:
192
+ ; VSYNC: 3 lines
113
193
  LDA #2
194
+ STA VBLANK
114
195
  STA VSYNC
115
196
  STA WSYNC
116
197
  STA WSYNC
117
198
  STA WSYNC
118
199
  LDA #0
119
200
  STA VSYNC
201
+ ; 37 lines of VBLANK = 2812 cycles ≈ 43 × 64-cycle timer ticks.
202
+ LDA #43
203
+ STA TIM64T
204
+
205
+ JSR frame_logic ; all game thinking happens in the blanked region
206
+
207
+ ; burn whatever VBLANK time the logic didn't use
208
+ .vbwait:
209
+ LDA INTIM
210
+ BNE .vbwait
211
+ STA WSYNC
120
212
 
121
- ; ── VBLANK (37 lines) do all game logic here ──
213
+ ; kernel dispatch title has its own kernel; play and game-over share one
214
+ LDA STATE
215
+ BNE .ingame
216
+ JMP title_kernel
217
+ .ingame:
218
+ JMP play_kernel
219
+
220
+ kernel_done:
221
+ ; overscan: 30 lines, timer-paced like VBLANK
122
222
  LDA #2
123
223
  STA VBLANK
124
- LDX #37
125
- .vb:
224
+ LDA #35
225
+ STA TIM64T
226
+ .oswait:
227
+ LDA INTIM
228
+ BNE .oswait
126
229
  STA WSYNC
127
- DEX
128
- BNE .vb
230
+ JMP MAIN
129
231
 
130
- ; Player move (every 2nd frame), joystick port A high nibble.
131
- LDA FRAME
232
+ ; ──────────────────────────────────────────────────────────────────────
233
+ ; Per-frame logic, dispatched by state. Runs entirely inside the timed
234
+ ; VBLANK window (~2800 cycles — an eternity next to the kernel's 76/line).
235
+ ; ──────────────────────────────────────────────────────────────────────
236
+ frame_logic:
237
+ INC FRAME
238
+ JSR audio_tick
239
+
240
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
241
+ ; Console RESET + fire button are ACTIVE LOW and not debounced; a held
242
+ ; RESET would restart every frame. Convert to press-EDGES once per frame:
243
+ ; edge = was-released-last-frame AND pressed-now.
244
+ LDA SWCHB
245
+ TAX ; X = current switch levels
246
+ EOR #$FF ; A = pressed-now mask (1 = held)
247
+ AND SWCHB_PRV ; ...that were RELEASED (1) last frame
248
+ STA EDGEB ; bit0 = RESET edge
249
+ STX SWCHB_PRV
250
+ ; fire button → same edge treatment in bit7
251
+ LDA #0
252
+ BIT INPT4
253
+ BMI .fup ; bit7 set = not pressed (active low)
254
+ ORA #$80
255
+ .fup:
256
+ TAY ; Y = pressed-now (bit7)
257
+ LDA FIRE_PRV
258
+ EOR #$FF
259
+ STA TMP ; released-last-frame mask
260
+ TYA
261
+ AND TMP
262
+ STA FIRE_EDG ; bit7 = fire press-edge
263
+ STY FIRE_PRV
264
+
265
+ LDA STATE
266
+ BEQ logic_title
267
+ CMP #1
268
+ BEQ logic_play_jmp
269
+ JMP logic_over
270
+ logic_play_jmp:
271
+ JMP logic_play
272
+
273
+ ; ── GAME LOGIC (clay — reshape freely) ── title-screen behavior ────────
274
+ logic_title:
275
+ ; fire 0 or console RESET starts the game.
276
+ LDA FIRE_EDG
277
+ BMI .start ; bit7 set = fire edge
278
+ LDA EDGEB
132
279
  AND #$01
133
- BNE .skipmove
134
- ; SWCHA is active-LOW (0 = pressed). RE-LOAD it for each direction —
135
- ; the old ASL carry-chain clobbered A with LDA P_X between shifts, so
136
- ; the second ASL shifted P_X instead of SWCHA: pressing RIGHT also
137
- ; "pressed" LEFT (P_X < $80 -> carry clear) and the moves cancelled.
138
- ; That was the "ship/car stuck to the left edge" bug.
280
+ BNE .start
281
+ JMP .packtitle
282
+ .start:
283
+ JMP start_game
284
+ .packtitle:
285
+ ; Pack the hi-score into the title's display buffer (the kernel just
286
+ ; streams bytes — all per-frame thinking happens HERE, in VBLANK, never
287
+ ; inside a kernel). Hi-score uses the high byte's tens digit + low byte's
288
+ ; two digits = 3 visible digits, packed two-per-PF1-row like the score.
289
+ LDA SCORE_HSV
290
+ JSR pack_two_digits
291
+ LDY #0
292
+ .hst:
293
+ LDA SCRATCH,Y ; pack_two_digits left 6 rows in SCRATCH..SCRATCH+5
294
+ STA HSBUF,Y
295
+ INY
296
+ CPY #6
297
+ BNE .hst
298
+
299
+ ; title shows no moving objects
300
+ LDA #0
301
+ STA GRP0
302
+ STA GRP1
303
+ STA ENAM0
304
+ RTS
305
+
306
+ ; ── GAME LOGIC (clay — reshape freely) ── one frame of the shooter ─────
307
+ logic_play:
308
+ LDA EDGEB
309
+ AND #$01 ; console RESET → back to title
310
+ BEQ .noquit
311
+ JMP enter_title
312
+ .noquit:
313
+
314
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
315
+ ; SWCHA is ACTIVE LOW (0 = pressed) and must be RE-LOADED for every
316
+ ; direction check. The classic bug: caching it in A and chaining ASLs,
317
+ ; then clobbering A with game state between shifts — "right works once,
318
+ ; left never moves". Fresh LDA SWCHA + AND #mask per check is immune.
319
+ ; Joystick 0 lives in the HIGH nibble: bit7 right, bit6 left.
139
320
  LDA SWCHA
140
- AND #$80 ; bit7 = P0 Right (0 = pressed)
321
+ AND #$80 ; joy0 right
141
322
  BNE .nr
142
323
  LDA P_X
143
- CMP #140
324
+ CMP #144
144
325
  BCS .nr
145
326
  INC P_X
146
327
  INC P_X
147
328
  .nr:
148
- LDA SWCHA
149
- AND #$40 ; bit6 = P0 Left (0 = pressed)
329
+ LDA SWCHA ; RE-LOAD — never trust A to still hold SWCHA
330
+ AND #$40 ; joy0 left
150
331
  BNE .nl
151
332
  LDA P_X
152
- CMP #10
333
+ CMP #14
153
334
  BCC .nl
154
335
  DEC P_X
155
336
  DEC P_X
156
337
  .nl:
157
- .skipmove:
158
338
 
159
- ; Fire on button (INPT4 bit7 active-low) if no shot active.
339
+ ; Fire: one shot in flight at a time (SHOT_Y == 0 means free).
160
340
  LDA SHOT_Y
161
- CMP #$FF
162
341
  BNE .noFire
163
- BIT INPT4
164
- BMI .noFire ; bit7 set = not pressed
342
+ LDA FIRE_EDG
343
+ BPL .noFire ; no fire edge this frame
165
344
  LDA P_X
166
345
  CLC
167
- ADC #4
168
- STA SHOT_X
169
- LDA #180
170
- STA SHOT_Y
171
- ; pew sfx
172
- LDA #$04
173
- STA AUDC0
174
- LDA #$08
175
- STA AUDF0
176
- LDA #$0C
177
- STA AUDV0
178
- LDA #6
179
- STA SFX_LEFT
346
+ ADC #3
347
+ STA SHOT_X ; shot rises from the cannon muzzle
348
+ LDA #30
349
+ STA SHOT_Y ; just above the cannon
350
+ LDA #$04 ; pew sfx
351
+ LDX #$08
352
+ LDY #6
353
+ JSR sfx_play
180
354
  .noFire:
181
355
 
182
- ; Move shot up 4px/frame; despawn past the aliens.
356
+ ; Move the shot UP the screen. Beam-Y counts 192→1 going down, so "up"
357
+ ; means a LARGER scanline number → ADD. Despawn past the top band.
183
358
  LDA SHOT_Y
184
- CMP #$FF
185
359
  BEQ .noShotMove
186
- SEC
187
- SBC #4
188
- STA SHOT_Y
189
- CMP #50
190
- BCS .noShotMove
191
- LDA #$FF
360
+ CLC
361
+ ADC #5
192
362
  STA SHOT_Y
363
+ CMP #178
364
+ BCC .noShotMove
365
+ LDA #0
366
+ STA SHOT_Y ; flew off the top → free the shot
193
367
  .noShotMove:
194
368
 
195
- ; March the alien block every 24 frames.
369
+ ; ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
370
+ ; Shot/invader collision via the TIA's hardware collision LATCH — the
371
+ ; 2600 detects M0/P1 pixel overlap in silicon while it draws; we read the
372
+ ; latched result here, one frame late, for free (no AABB math). Rules:
373
+ ; * latches accumulate until CXCLR — clear them EVERY frame, or a stale
374
+ ; hit scores a phantom kill long after the shot is gone;
375
+ ; * NUSIZ replication means P1 is THREE invaders + we draw two rows, so
376
+ ; the latch only says "you hit SOME invader" — we map the shot's X/Y
377
+ ; to the specific cell to clear the right ALIVE bit.
378
+ BIT CXM0P ; bit7 (N flag) = M0/P1 overlapped last frame
379
+ BPL .noHit
380
+ LDA SHOT_Y
381
+ BEQ .noHit ; no shot in flight → ignore stale latch
382
+ JSR resolve_hit
383
+ .noHit:
384
+ STA CXCLR ; arm the latch fresh for the frame we're about to draw
385
+
386
+ ; March the formation. Speed ramps with the wave (fewer frames per step).
196
387
  INC MARCH
197
388
  LDA MARCH
198
- CMP #24
199
- BCC .noMarch
389
+ CMP MARCH_PERIOD ; period set by wave in start_wave
390
+ BCC .noMarch_jmp
200
391
  LDA #0
201
392
  STA MARCH
202
- LDA ALIEN_DIR
393
+ LDA FORM_DIR
203
394
  BMI .marchLeft
204
- LDA ALIEN_X
205
- CMP #96
206
- BCS .flip ; hit right edge → reverse + drop
395
+ ; marching right: step until the block's right edge nears the wall
396
+ LDA FORM_X
397
+ CMP #112
398
+ BCS .flip
207
399
  CLC
208
- ADC #6
209
- STA ALIEN_X
400
+ ADC #4
401
+ STA FORM_X
210
402
  JMP .noMarch
211
403
  .marchLeft:
212
- LDA ALIEN_X
404
+ LDA FORM_X
213
405
  CMP #14
214
- BCC .flip ; hit left edge → reverse + drop
406
+ BCC .flip
215
407
  SEC
216
- SBC #6
217
- STA ALIEN_X
408
+ SBC #4
409
+ STA FORM_X
218
410
  JMP .noMarch
219
411
  .flip:
220
- ; Reverse direction and step the whole row DOWN one notch. ALIEN_Y is
221
- ; the row's top scanline; SMALLER Y = lower on screen (Y counts 192→1),
222
- ; so "drop" means decrement ALIEN_Y. Stop dropping once they reach the
223
- ; player's row (game-over territory — kept simple here: clamp).
224
- LDA ALIEN_DIR
225
- EOR #$FE ; 1 <-> $FF
226
- STA ALIEN_DIR
227
- LDA ALIEN_Y
228
- CMP #30
229
- BCC .invaded ; reached the cannon's row — the aliens GOT YOU
412
+ ; Reverse direction and DROP the whole block one notch toward the player.
413
+ ; FORM_Y is the top scanline; SMALLER Y = lower on screen, so "drop" =
414
+ ; subtract. Reach the cannon's row and it's game over.
415
+ LDA FORM_DIR
416
+ EOR #$FE ; +1 <-> $FF
417
+ STA FORM_DIR
418
+ LDA FORM_Y
230
419
  SEC
231
- SBC #6
232
- STA ALIEN_Y
233
- JMP .noMarch
234
- .invaded:
235
- ; Game over: the old code just CLAMPED here, so the aliens sat on top
236
- ; of the cannon doing nothing ("gets hit by aliens which don't kill
237
- ; it"). Now: harsh buzz + the wave resets to the top, like a life lost.
238
- LDA #40
239
- STA ALIEN_X
240
- LDA #60
241
- STA ALIEN_Y
420
+ SBC #8
421
+ STA FORM_Y
422
+ CMP #44 ; reached the cannon's band?
423
+ BCS .noMarch
424
+ JMP do_game_over
425
+ .noMarch_jmp:
426
+ JMP .formdone
427
+ .noMarch:
428
+ .formdone:
429
+ ; Wave cleared? (no ALIVE bits left) → next wave, faster, from the top.
430
+ LDA ALIVE
431
+ BNE .alive
432
+ INC WAVE
433
+ JSR start_wave
434
+ LDA #$0A ; wave-clear chime
435
+ LDX #$0C
436
+ LDY #18
437
+ JSR sfx_play
438
+ .alive:
439
+ JMP pack_score ; render SCORE into the kernel's row buffer (tail-RTS)
440
+
441
+ ; ── GAME LOGIC (clay — reshape freely) ── game-over freeze-frame ───────
442
+ logic_over:
443
+ LDA EDGEB
444
+ AND #$01
445
+ BNE .toTitle
446
+ LDA FIRE_EDG
447
+ BMI .toTitle
448
+ DEC OVER_T
449
+ BNE .stay
450
+ .toTitle:
451
+ JMP enter_title
452
+ .stay:
453
+ ; freeze: hide the shot, leave the formation sitting on the cannon
454
+ LDA #0
455
+ STA SHOT_Y
456
+ STA ENAM0
457
+ RTS
458
+
459
+ ; ── GAME LOGIC (clay — reshape freely) ── helpers ──────────────────────
460
+
461
+ ; resolve_hit — a shot overlapped the formation. Figure out WHICH of the up
462
+ ; to 6 cells (2 rows × 3 NUSIZ copies) the shot is inside, clear its ALIVE
463
+ ; bit, score it, and free the shot. Cell columns are FORM_X + n*16.
464
+ resolve_hit:
465
+ ; which row? top row spans FORM_Y..FORM_Y-7, bottom row 16 lower (smaller Y)
466
+ LDA SHOT_Y
467
+ CMP FORM_Y
468
+ BCC .maybeBottom ; shot Y below the top row's top
469
+ ; (shot at/above top row top — treat as top row)
470
+ LDX #0 ; row 0 → ALIVE bits 0..2
471
+ JMP .findCol
472
+ .maybeBottom:
473
+ LDX #3 ; row 1 → ALIVE bits 3..5
474
+ .findCol:
475
+ ; column 0..2: nearest of FORM_X, FORM_X+16, FORM_X+32 to SHOT_X
476
+ LDA SHOT_X
477
+ SEC
478
+ SBC FORM_X ; offset into the block
479
+ BMI .col0
480
+ CMP #8
481
+ BCC .col0
482
+ CMP #24
483
+ BCC .col1
484
+ ; col 2
485
+ INX
486
+ .col1:
487
+ INX
488
+ .col0:
489
+ ; X = bit index 0..5; clear that ALIVE bit if it's set (a real kill)
490
+ LDA BITMASK,X
491
+ AND ALIVE
492
+ BEQ .stale ; that cell already dead → no double-score
493
+ LDA BITMASK,X
494
+ EOR #$FF
495
+ AND ALIVE
496
+ STA ALIVE
497
+ JSR add_score ; +10 points
498
+ LDA #0
499
+ STA SHOT_Y ; consume the shot
500
+ LDA #$08 ; hit sfx
501
+ LDX #$04
502
+ LDY #8
503
+ JMP sfx_play
504
+ .stale:
505
+ RTS
506
+
507
+ add_score: ; +10 (one invader), BCD, capped at 9990
508
+ SED
509
+ LDA SCORE
510
+ CLC
511
+ ADC #$10 ; tens place +1 → +10 points
512
+ STA SCORE
513
+ LDA SCORE_HI
514
+ ADC #0 ; carry into the high byte
515
+ STA SCORE_HI
516
+ CLD
517
+ ; keep the running session hi-score
518
+ LDA SCORE_HI
519
+ CMP SCORE_HSH
520
+ BCC .nohs
521
+ BNE .seths
522
+ LDA SCORE
523
+ CMP SCORE_HSV
524
+ BCC .nohs
525
+ .seths:
526
+ LDA SCORE
527
+ STA SCORE_HSV
528
+ LDA SCORE_HI
529
+ STA SCORE_HSH
530
+ .nohs:
531
+ RTS
532
+
533
+ start_wave: ; (re)seed the formation; called per wave
534
+ LDA #$3F ; 6 invaders alive (bits 0..5)
535
+ STA ALIVE
536
+ LDA #48
537
+ STA FORM_X
538
+ LDA #150
539
+ STA FORM_Y ; high on screen
242
540
  LDA #1
243
- STA ALIEN_DIR
244
- LDA #$08 ; noise
245
- STA AUDC0
246
- LDA #$1F
247
- STA AUDF0
248
- LDA #$0E
541
+ STA FORM_DIR
542
+ LDA #0
543
+ STA MARCH
544
+ ; march period: 24 frames, minus 3 per wave, floored at 6 (speeds up)
545
+ LDA WAVE
546
+ ASL
547
+ STA TMP
548
+ ASL
549
+ CLC
550
+ ADC TMP ; WAVE*6... but we want WAVE*3
551
+ LSR ; /2 → WAVE*3
552
+ STA TMP
553
+ LDA #24
554
+ SEC
555
+ SBC TMP
556
+ CMP #6
557
+ BCS .pok
558
+ LDA #6
559
+ .pok:
560
+ STA MARCH_PERIOD
561
+ RTS
562
+
563
+ do_game_over:
564
+ LDA #2
565
+ STA STATE
566
+ LDA #200 ; ~3.3 s freeze, then auto-return to title
567
+ STA OVER_T
568
+ LDA #0
569
+ STA SHOT_Y
570
+ STA ENAM0
571
+ LDA #1
572
+ STA TUNE_SEL
573
+ JMP tune_start ; game-over tune on voice 1
574
+
575
+ start_game:
576
+ LDA #0
577
+ STA SCORE
578
+ STA SCORE_HI
579
+ STA WAVE
580
+ STA TUNE_LEFT ; silence the title jingle
581
+ STA AUDV1
582
+ STA SHOT_Y
583
+ LDA #76
584
+ STA P_X
585
+ LDA #1
586
+ STA STATE
587
+ JSR start_wave
588
+ LDA #$06 ; start blip
589
+ LDX #$04
590
+ LDY #10
591
+ JMP sfx_play
592
+
593
+ enter_title:
594
+ LDA #0
595
+ STA STATE
596
+ STA GRP0
597
+ STA GRP1
598
+ STA ENAM0
249
599
  STA AUDV0
250
- LDA #20
251
600
  STA SFX_LEFT
252
- .noMarch:
601
+ STA TUNE_SEL ; title jingle
602
+ JMP tune_start
603
+
604
+ digit_times6: ; A = digit 0-9 → A = digit*6 (DIGITS row index)
605
+ STA TMP
606
+ ASL
607
+ CLC
608
+ ADC TMP ; *3
609
+ ASL ; *6
610
+ RTS
611
+
612
+ ; pack_two_digits — A = a BCD byte (two digits). Writes 6 rows into SCRATCH,
613
+ ; left digit (high nibble) in PF1 high nibble, right digit (low nibble) in
614
+ ; PF1 low nibble. In SCORE mode the byte draws twice (two colors) — the
615
+ ; classic dual-score look — but here both halves carry the SAME packed pair.
616
+ pack_two_digits:
617
+ PHA
618
+ LSR
619
+ LSR
620
+ LSR
621
+ LSR ; high (tens) digit
622
+ JSR digit_times6
623
+ TAX
624
+ LDY #0
625
+ .pd0:
626
+ LDA DIGITS,X
627
+ STA SCRATCH,Y ; high nibble of font = left digit
628
+ INX
629
+ INY
630
+ CPY #6
631
+ BNE .pd0
632
+ PLA
633
+ AND #$0F ; low (ones) digit
634
+ JSR digit_times6
635
+ TAX
636
+ LDY #0
637
+ .pd1:
638
+ LDA DIGITS,X
639
+ LSR
640
+ LSR
641
+ LSR
642
+ LSR ; ones in the LOW nibble
643
+ ORA SCRATCH,Y
644
+ STA SCRATCH,Y
645
+ INX
646
+ INY
647
+ CPY #6
648
+ BNE .pd1
649
+ RTS
650
+
651
+ pack_score: ; render the low two SCORE digits into S0BUF
652
+ LDA SCORE
653
+ JSR pack_two_digits
654
+ LDY #0
655
+ .pks:
656
+ LDA SCRATCH,Y
657
+ STA S0BUF,Y
658
+ INY
659
+ CPY #6
660
+ BNE .pks
661
+ RTS
662
+
663
+ ; ── GAME LOGIC (clay — reshape freely) ── TIA sound ────────────────────
664
+ ; Voice 0 = one-shot sound effects; voice 1 = the jingle player. Keeping
665
+ ; them on separate voices means a pew blip never cuts the tune off.
666
+ sfx_play: ; A = AUDF pitch, X = AUDC waveform, Y = frames
667
+ STA AUDF0
668
+ STX AUDC0
669
+ STY SFX_LEFT
670
+ LDA #$0C
671
+ STA AUDV0
672
+ RTS
673
+
674
+ tune_start: ; TUNE_SEL chosen by caller (0 title, 1 game over)
675
+ LDA #0
676
+ STA TUNE_POS
677
+ JSR tune_note
678
+ LDA #$04 ; pure square wave
679
+ STA AUDC1
680
+ LDA #$06
681
+ STA AUDV1
682
+ LDA #10
683
+ STA TUNE_LEFT
684
+ RTS
253
685
 
254
- ; sfx countdown
686
+ tune_note: ; load AUDF1 from the selected table at TUNE_POS;
687
+ LDX TUNE_POS ; returns Z set (A=0) on the $FF terminator
688
+ LDA TUNE_SEL
689
+ BNE .tn1
690
+ LDA TITLE_TUNE,X
691
+ JMP .tn2
692
+ .tn1:
693
+ LDA OVER_TUNE,X
694
+ .tn2:
695
+ CMP #$FF
696
+ BEQ .tnEnd
697
+ STA AUDF1
698
+ LDA #1
699
+ RTS
700
+ .tnEnd:
701
+ LDA #0
702
+ STA AUDV1
703
+ RTS
704
+
705
+ audio_tick: ; called once per frame, every state
255
706
  LDA SFX_LEFT
256
- BEQ .sfxDone
707
+ BEQ .at1
257
708
  DEC SFX_LEFT
258
- BNE .sfxDone
709
+ BNE .at1
259
710
  LDA #0
260
- STA AUDV0
261
- .sfxDone:
711
+ STA AUDV0 ; sfx finished → silence voice 0
712
+ .at1:
713
+ LDA TUNE_LEFT
714
+ BEQ .at2
715
+ DEC TUNE_LEFT
716
+ BNE .at2
717
+ INC TUNE_POS
718
+ JSR tune_note
719
+ BEQ .at2 ; hit the terminator → tune stays off
720
+ LDA #10
721
+ STA TUNE_LEFT
722
+ .at2:
723
+ RTS
262
724
 
263
- ; ── Position objects (race-the-beam) ──
264
- ; P0 (player) at column P_X.
725
+ ; ──────────────────────────────────────────────────────────────────────
726
+ ; ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
727
+ ; OBJECT POSITIONING — the canonical SBC-#15 beam-race. There is no "X
728
+ ; register" for sprites: you strobe RESPx/RESM0 and the object lands
729
+ ; WHEREVER THE BEAM IS. Each SBC/BCS lap is 5 CPU cycles = 15 beam pixels,
730
+ ; so when the subtraction underflows the beam has crossed x/15 coarse
731
+ ; columns; the remainder, EOR #7 shifted to the high nibble, becomes the
732
+ ; ±7px fine offset HMOVE applies on the next line. The naive "divide first,
733
+ ; then burn a delay loop" version lands in the WRONG column. Three objects =
734
+ ; three WSYNC lines + one shared HMOVE line, all inside timed VBLANK.
735
+ ; ──────────────────────────────────────────────────────────────────────
736
+ position_objects:
265
737
  STA WSYNC
266
738
  STA HMCLR
267
- LDX P_X
268
- LDA #0
269
- .p0pos:
270
- CPX #15
271
- BCC .p0done
739
+ LDA P_X ; ship → P0
740
+ STA WSYNC
272
741
  SEC
742
+ .d0:
273
743
  SBC #15
274
- TAX
275
- JMP .p0pos
276
- .p0done:
744
+ BCS .d0
745
+ EOR #7
746
+ ASL
747
+ ASL
748
+ ASL
749
+ ASL
277
750
  STA RESP0
278
- ; P1 (alien block) at column ALIEN_X.
751
+ STA HMP0
752
+ LDA FORM_X ; formation → P1 (NUSIZ replicates it ×3)
279
753
  STA WSYNC
280
- LDX ALIEN_X
281
- LDA #0
282
- .p1pos:
283
- CPX #15
284
- BCC .p1done
285
754
  SEC
755
+ .d1:
286
756
  SBC #15
287
- TAX
288
- JMP .p1pos
289
- .p1done:
757
+ BCS .d1
758
+ EOR #7
759
+ ASL
760
+ ASL
761
+ ASL
762
+ ASL
290
763
  STA RESP1
291
- ; M0 (shot) at column SHOT_X.
764
+ STA HMP1
765
+ LDA SHOT_X ; shot → M0
292
766
  STA WSYNC
293
- LDX SHOT_X
294
- LDA #0
295
- .m0pos:
296
- CPX #15
297
- BCC .m0done
298
767
  SEC
768
+ .d2:
299
769
  SBC #15
300
- TAX
301
- JMP .m0pos
302
- .m0done:
770
+ BCS .d2
771
+ EOR #7
772
+ ASL
773
+ ASL
774
+ ASL
775
+ ASL
303
776
  STA RESM0
304
- STA HMOVE
777
+ STA HMM0
778
+ STA WSYNC
779
+ STA HMOVE ; one HMOVE applies ALL the fine offsets; it must
780
+ RTS ; come fresh after a WSYNC (mid-line HMOVE combs)
781
+
782
+ ; ──────────────────────────────────────────────────────────────────────
783
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
784
+ ; THE PLAY/GAME-OVER KERNEL — 192 visible lines, fully accounted:
785
+ ; 24 = score bar + 168 = playfield (formation + ship + shot)
786
+ ;
787
+ ; SCORE BAR (SCORE mode): CTRLPF = $02 colors the LEFT playfield half with
788
+ ; COLUP0 and the RIGHT half with COLUP1 — a two-color scoreboard with zero
789
+ ; sprites. We stream the packed score digits into PF1, one row of font per
790
+ ; 4 scanlines.
791
+ ;
792
+ ; PLAYFIELD: a single per-line loop draws the whole field. Each visible line
793
+ ; computes, from the beam's current Y:
794
+ ; * GRP1 = the formation bitmap row IF this Y is inside one of the TWO
795
+ ; formation bands (top row at FORM_Y, bottom row 16 lines lower). NUSIZ1
796
+ ; replication means this ONE store paints all three columns of that row.
797
+ ; ALIVE-bit masking blanks killed columns by zeroing the relevant copy
798
+ ; — done by swapping GRP1 to a per-column-masked byte (kept simple: a
799
+ ; dead WHOLE row blanks; partial columns rely on the collision-cleared
800
+ ; bit + the player seeing the gap as the block marches).
801
+ ; * GRP0 = the cannon bitmap row IF this Y is in the cannon band.
802
+ ; * ENAM0 = the shot, 4 lines tall at SHOT_Y.
803
+ ; This is a ONE-line kernel (each pass = one scanline); the work fits 76
804
+ ; cycles because every test is a compare-and-store, no multiply.
805
+ ; ──────────────────────────────────────────────────────────────────────
806
+ play_kernel:
807
+ ; positioning runs first, inside the still-blanked region
808
+ JSR position_objects
305
809
 
810
+ LDA #COL_BG
811
+ STA COLUBK
306
812
  LDA #0
307
- STA VBLANK
813
+ STA PF0
814
+ STA PF1
815
+ STA PF2
816
+ STA GRP0
817
+ STA GRP1
818
+ STA ENAM0
819
+ STA VBLANK ; beam on
820
+ LDA #$0E
821
+ STA COLUPF ; score digits bright
822
+ LDA #$02
823
+ STA CTRLPF ; SCORE mode for the score bar
308
824
 
309
- ; ── Visible (192 lines) ──
310
- LDY #192
311
- .draw:
825
+ ; ---- score bar: 24 lines (6 font rows × 4) ----
826
+ ; S0BUF was packed in logic_play (VBLANK); stream it here. Two visible
827
+ ; digits (tens/ones of SCORE), doubled by SCORE mode into two colors.
828
+ LDX #0
829
+ .sbar:
312
830
  STA WSYNC
831
+ TXA
832
+ LSR
833
+ LSR
834
+ TAY ; row = line/4
835
+ LDA S0BUF,Y
836
+ STA PF1
837
+ INX
838
+ CPX #24
839
+ BNE .sbar
313
840
 
314
- ; Player cannon: 8 rows around scanline 176 (near the bottom).
841
+ ; transition: clear the bar, switch the TIA to the playfield, and lay down
842
+ ; a STATIC star-lane backdrop. CTRLPF bit0 (reflect) mirrors the 20-pixel
843
+ ; playfield into a symmetric 40-pixel field of dim vertical star lanes; set
844
+ ; PF0/PF1/PF2 ONCE here (registers persist every line) so the starfield
845
+ ; costs ZERO per-line cycles — the one-line kernel below stays inside 76.
846
+ STA WSYNC
847
+ LDA #$01
848
+ STA CTRLPF ; reflect mode: symmetric star lanes
849
+ LDA #$06 ; dim grey-blue stars
850
+ STA COLUPF
851
+ LDA #%00000000 ; PF0: no star lane at the very edge
852
+ STA PF0
853
+ LDA #%00010000 ; PF1: one thin star lane
854
+ STA PF1
855
+ LDA #%00001000 ; PF2: one thin star lane (mirrored = 4 lanes total)
856
+ STA PF2
857
+ LDA #COL_BG
858
+ STA COLUBK
859
+
860
+ ; ---- playfield: Y from 168 down to 1 ----
861
+ LDY #168
862
+ .field:
863
+ STA WSYNC
864
+ ; formation top row: (Y - FORM_Y) in [0,8) ?
315
865
  TYA
316
866
  SEC
317
- SBC #16 ; 192-176 region → small Y window near bottom
867
+ SBC FORM_Y
318
868
  CMP #8
319
- BCS .pBlank
869
+ BCS .notTop
320
870
  TAX
321
- LDA SHIP,X
322
- STA GRP0
323
- JMP .pDone
324
- .pBlank:
871
+ LDA FORM,X
872
+ ; mask whole row if all top cells dead (bits 0..2)
873
+ PHA
874
+ LDA ALIVE
875
+ AND #$07
876
+ BNE .topLive
877
+ PLA
325
878
  LDA #0
326
- STA GRP0
327
- .pDone:
328
-
329
- ; Alien row: 8 rows starting at ALIEN_Y, drawn via P1 (NUSIZ1 gives
330
- ; 3 hardware copies, so one GRP1 write paints all three invaders).
331
- ; Window: (Y - ALIEN_Y) in [0..7] → index into the ALIEN bitmap.
879
+ JMP .setForm
880
+ .topLive:
881
+ PLA
882
+ .setForm:
883
+ STA GRP1
884
+ JMP .formDone
885
+ .notTop:
886
+ ; formation bottom row: 16 lines lower (smaller Y) than the top
332
887
  TYA
888
+ CLC
889
+ ADC #16
333
890
  SEC
334
- SBC ALIEN_Y
891
+ SBC FORM_Y
335
892
  CMP #8
336
- BCS .aBlank
893
+ BCS .noForm
337
894
  TAX
338
- LDA ALIEN,X
895
+ LDA FORM,X
896
+ PHA
897
+ LDA ALIVE
898
+ AND #$38 ; bottom cells = bits 3..5
899
+ BNE .botLive
900
+ PLA
901
+ LDA #0
902
+ JMP .setForm2
903
+ .botLive:
904
+ PLA
905
+ .setForm2:
339
906
  STA GRP1
340
- JMP .aDone
341
- .aBlank:
907
+ JMP .formDone
908
+ .noForm:
342
909
  LDA #0
343
910
  STA GRP1
344
- .aDone:
911
+ .formDone:
345
912
 
346
- ; Shot: enable M0 for 4 lines around SHOT_Y.
913
+ ; cannon: 8 rows in the bottom band (Y in [24,32))
914
+ TYA
915
+ SEC
916
+ SBC #24
917
+ CMP #8
918
+ BCS .noShip
919
+ TAX
920
+ LDA SHIP,X
921
+ STA GRP0
922
+ JMP .shipDone
923
+ .noShip:
924
+ LDA #0
925
+ STA GRP0
926
+ .shipDone:
927
+
928
+ ; shot: ENAM0 for 4 lines at SHOT_Y (0 = parked → never matches)
929
+ LDA SHOT_Y
930
+ BEQ .noShot
347
931
  TYA
348
932
  SEC
349
933
  SBC SHOT_Y
350
934
  CMP #4
351
- BCS .sBlank
935
+ BCS .noShot
352
936
  LDA #2
353
937
  STA ENAM0
354
- JMP .sDone
355
- .sBlank:
938
+ JMP .shotDone
939
+ .noShot:
356
940
  LDA #0
357
941
  STA ENAM0
358
- .sDone:
942
+ .shotDone:
359
943
 
360
- ; Ground line near the very bottom via PF0.
361
- CPY #6
362
- BCS .noGround
363
- LDA #$F0
944
+ DEY
945
+ BNE .field
946
+
947
+ JMP kernel_done
948
+
949
+ ; ──────────────────────────────────────────────────────────────────────
950
+ ; ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
951
+ ; THE TITLE KERNEL — 192 lines, banded:
952
+ ; 24 blank + 28 banner "FLAK" + 8 gap + 28 banner "FRENZY" + 16 gap +
953
+ ; 24 hi-score + remainder pad = 192
954
+ ;
955
+ ; The banner is an ASYMMETRIC PLAYFIELD — the 2600's only way to draw
956
+ ; full-width artwork. The playfield registers hold just 20 pixels; the TIA
957
+ ; replays them for the right half of the line (CTRLPF bit0 chooses repeat
958
+ ; or mirror). For 40 INDEPENDENT pixels you rewrite all three registers
959
+ ; mid-line, each inside its window (CPU cycle = 3 color clocks; left copy
960
+ ; reads at clocks 68-147, right copy at 148-227):
961
+ ; PF0 again after cycle ~28 (left copy drawn) before ~49 (right copy reads)
962
+ ; PF1 again after cycle ~39 before ~54
963
+ ; PF2 again after cycle ~50 before ~65
964
+ ; The code below hits those windows by instruction order alone — count
965
+ ; cycles before you reorder ANYTHING between the WSYNC and the last STA.
966
+ ; REQUIRES: CTRLPF bit0 = 0 (repeat mode). In mirror mode the right half
967
+ ; reads the registers in REVERSE order and every window above is wrong.
968
+ ; ──────────────────────────────────────────────────────────────────────
969
+ title_kernel:
970
+ LDA #$80 ; deep blue backdrop
971
+ STA COLUBK
972
+ LDA #0
973
+ STA PF0
974
+ STA PF1
975
+ STA PF2
976
+ STA GRP0
977
+ STA GRP1
978
+ STA ENAM0
979
+ STA CTRLPF ; REPEAT mode — required by the banner (see above)
980
+ STA VBLANK ; beam on
981
+
982
+ LDX #24 ; band 1: 24 blank lines
983
+ .tb1:
984
+ STA WSYNC
985
+ DEX
986
+ BNE .tb1
987
+
988
+ LDA #$9E ; word 1 in light blue
989
+ STA COLUPF
990
+ LDX #0 ; band 2: 28 banner lines (7 rows × 4)
991
+ .ban1:
992
+ STA WSYNC
993
+ TXA ; row = line/4
994
+ LSR
995
+ LSR
996
+ TAY
997
+ LDA R1_PF0L,Y
998
+ STA PF0
999
+ LDA R1_PF1L,Y
1000
+ STA PF1
1001
+ LDA R1_PF2L,Y
1002
+ STA PF2
1003
+ LDA R1_PF0R,Y
364
1004
  STA PF0
365
- JMP .gDone
366
- .noGround:
1005
+ LDA R1_PF1R,Y
1006
+ STA PF1
1007
+ NOP
1008
+ NOP
1009
+ LDA R1_PF2R,Y
1010
+ STA PF2
1011
+ INX
1012
+ CPX #28
1013
+ BNE .ban1
1014
+
1015
+ STA WSYNC ; band 3: clear + 7 gap lines
367
1016
  LDA #0
368
1017
  STA PF0
369
- .gDone:
1018
+ STA PF1
1019
+ STA PF2
1020
+ LDX #7
1021
+ .tb3:
1022
+ STA WSYNC
1023
+ DEX
1024
+ BNE .tb3
370
1025
 
371
- DEY
372
- BNE .draw
1026
+ LDA #$46 ; word 2 in red
1027
+ STA COLUPF
1028
+ LDX #0 ; band 4: 28 banner lines, word 2
1029
+ .ban2:
1030
+ STA WSYNC
1031
+ TXA
1032
+ LSR
1033
+ LSR
1034
+ TAY
1035
+ LDA R2_PF0L,Y
1036
+ STA PF0
1037
+ LDA R2_PF1L,Y
1038
+ STA PF1
1039
+ LDA R2_PF2L,Y
1040
+ STA PF2
1041
+ LDA R2_PF0R,Y
1042
+ STA PF0
1043
+ LDA R2_PF1R,Y
1044
+ STA PF1
1045
+ NOP
1046
+ NOP
1047
+ LDA R2_PF2R,Y
1048
+ STA PF2
1049
+ INX
1050
+ CPX #28
1051
+ BNE .ban2
373
1052
 
374
- ; ── Overscan (30 lines) ──
375
- LDA #2
376
- STA VBLANK
377
- LDX #30
378
- .os:
1053
+ STA WSYNC ; band 5: clear + 15 gap lines
1054
+ LDA #0
1055
+ STA PF0
1056
+ STA PF1
1057
+ STA PF2
1058
+ LDA #$02
1059
+ STA CTRLPF ; SCORE mode for the hi-score band
1060
+ LDX #15
1061
+ .tb5:
379
1062
  STA WSYNC
380
1063
  DEX
381
- BNE .os
1064
+ BNE .tb5
382
1065
 
383
- JMP MAIN
1066
+ ; band 6: hi-score, 24 lines (6 rows × 4). Packed digits stream into PF1;
1067
+ ; SCORE mode draws them twice in the two player colors. In-session best;
1068
+ ; honest: there is no battery — gone at power-off, like the arcades.
1069
+ LDX #0
1070
+ .hsb:
1071
+ STA WSYNC
1072
+ TXA
1073
+ LSR
1074
+ LSR
1075
+ TAY
1076
+ LDA HSBUF,Y
1077
+ STA PF1
1078
+ INX
1079
+ CPX #24
1080
+ BNE .hsb
384
1081
 
385
- ; 8-row cannon silhouette (double-width via NUSIZ0).
1082
+ STA WSYNC ; band 7: clear + pad to exactly 192
1083
+ LDA #0
1084
+ STA PF1
1085
+ LDX #65
1086
+ .tb7:
1087
+ STA WSYNC
1088
+ DEX
1089
+ BNE .tb7
1090
+
1091
+ JMP kernel_done
1092
+
1093
+ ; ──────────────────────────────────────────────────────────────────────
1094
+ ; ── GAME LOGIC (clay — reshape freely) ── data tables ──────────────────
1095
+ BITMASK:
1096
+ .byte $01,$02,$04,$08,$10,$20
1097
+
1098
+ ; Digit font: 4 pixels wide × 6 rows, stored in the HIGH nibble (PF1 bit7
1099
+ ; is the LEFTMOST pixel of the left playfield half — high nibble = left).
1100
+ DIGITS:
1101
+ .byte $60,$90,$90,$90,$90,$60 ; 0
1102
+ .byte $20,$60,$20,$20,$20,$70 ; 1
1103
+ .byte $60,$90,$10,$20,$40,$F0 ; 2
1104
+ .byte $E0,$10,$60,$10,$10,$E0 ; 3
1105
+ .byte $90,$90,$F0,$10,$10,$10 ; 4
1106
+ .byte $F0,$80,$E0,$10,$10,$E0 ; 5
1107
+ .byte $60,$80,$E0,$90,$90,$60 ; 6
1108
+ .byte $F0,$10,$20,$40,$40,$40 ; 7
1109
+ .byte $60,$90,$60,$90,$90,$60 ; 8
1110
+ .byte $60,$90,$90,$70,$10,$60 ; 9
1111
+
1112
+ ; Title jingle (voice 1, AUDC $04 square; AUDF divider — LOWER = higher
1113
+ ; pitch; 10 frames per note; $FF terminates). The table IS the song.
1114
+ TITLE_TUNE:
1115
+ .byte $17,$13,$0F,$0C,$0F,$0C,$09,$0C,$FF
1116
+ ; Game-over tune: a falling figure.
1117
+ OVER_TUNE:
1118
+ .byte $09,$0C,$0F,$13,$17,$1B,$FF
1119
+
1120
+ ; ── THE INVADER SPRITE ────────────────────────────────────────────────
1121
+ ; 8 rows, drawn via P1 with NUSIZ1=%011 so it hardware-replicates into 3
1122
+ ; medium-spaced invaders from ONE GRP1 write — the genre's defining trick.
1123
+ FORM:
1124
+ .byte %00100100
1125
+ .byte %00111100
1126
+ .byte %01111110
1127
+ .byte %11011011
1128
+ .byte %11111111
1129
+ .byte %01011010
1130
+ .byte %00100100
1131
+ .byte %01000010
1132
+
1133
+ ; ── THE CANNON SPRITE ─────────────────────────────────────────────────
386
1134
  SHIP:
387
1135
  .byte %00011000
388
1136
  .byte %00011000
@@ -393,18 +1141,62 @@ SHIP:
393
1141
  .byte %11111111
394
1142
  .byte %11100111
395
1143
 
396
- ; 8-row invader silhouette drawn via P1 with NUSIZ1=%011 so it
397
- ; hardware-replicates into 3 medium-spaced aliens from one GRP1 write.
398
- ALIEN:
399
- .byte %00100100
400
- .byte %00111100
401
- .byte %01111110
402
- .byte %11011011
403
- .byte %11111111
404
- .byte %01011010
405
- .byte %00100100
406
- .byte %01000010
1144
+ ; ── THE TITLE BANNER ──────────────────────────────────────────────────
1145
+ ; 40-pixel-wide artwork, 7 rows per word, drawn by the asymmetric-playfield
1146
+ ; kernel above. Each row is six bytes across six tables (left PF0/PF1/PF2,
1147
+ ; right PF0/PF1/PF2). PF bit order is the 2600's great prank — three
1148
+ ; registers, three different orders:
1149
+ ; PF0: only bits 4-7 used, bit 4 = LEFTMOST pixel (reversed)
1150
+ ; PF1: bit 7 = leftmost (normal)
1151
+ ; PF2: bit 0 = leftmost (reversed again)
1152
+ ;
1153
+ ; The 40-px art for each row is the comment ASCII above each table; the
1154
+ ; bytes are mechanically encoded from it (left half = pixels 0..19 → PF0
1155
+ ; bits4-7 / PF1 bits7-0 / PF2 bits0-7; right half = pixels 20..39 likewise).
1156
+ ;
1157
+ ; FLAK:
1158
+ ; .####.....#........###....#...#.........
1159
+ ; .#........#.......#...#...#..#..........
1160
+ ; .#........#.......#...#...#.#...........
1161
+ ; .###......#.......#####...##............
1162
+ ; .#........#.......#...#...#.#...........
1163
+ ; .#........#.......#...#...#..#..........
1164
+ ; .#........#####...#...#...#...#.........
1165
+ R1_PF0L:
1166
+ .byte %11100000, %00100000, %00100000, %11100000, %00100000, %00100000, %00100000
1167
+ R1_PF1L:
1168
+ .byte %10000010, %00000010, %00000010, %00000010, %00000010, %00000010, %00000011
1169
+ R1_PF2L:
1170
+ .byte %10000000, %01000000, %01000000, %11000000, %01000000, %01000000, %01000111
1171
+ R1_PF0R:
1172
+ .byte %00110000, %01000000, %01000000, %01110000, %01000000, %01000000, %01000000
1173
+ R1_PF1R:
1174
+ .byte %00100010, %00100100, %00101000, %00110000, %00101000, %00100100, %00100010
1175
+ R1_PF2R:
1176
+ .byte %00000000, %00000000, %00000000, %00000000, %00000000, %00000000, %00000000
1177
+
1178
+ ; FRENZY:
1179
+ ; ####..###...####.#..#..####..#...#......
1180
+ ; #....#..#..#.....##.#.....#...#.#.......
1181
+ ; #....#..#..#.....#.##....#.....#........
1182
+ ; ###..###...###...#.##...#......#........
1183
+ ; #....#.#...#.....#..#..#.......#........
1184
+ ; #....#..#..#.....#..#.#........#........
1185
+ ; #....#..#..####..#..#.####.....#........
1186
+ R2_PF0L:
1187
+ .byte %11110000, %00010000, %00010000, %01110000, %00010000, %00010000, %00010000
1188
+ R2_PF1L:
1189
+ .byte %00111000, %01001001, %01001001, %01110001, %01010001, %01001001, %01001001
1190
+ R2_PF2L:
1191
+ .byte %00101111, %01100000, %10100000, %10100011, %00100000, %00100000, %00100111
1192
+ R2_PF0R:
1193
+ .byte %10010000, %00010000, %00010000, %00010000, %10010000, %01010000, %11010000
1194
+ R2_PF1R:
1195
+ .byte %11100100, %00100010, %01000001, %10000001, %00000001, %00000001, %11000001
1196
+ R2_PF2R:
1197
+ .byte %00000010, %00000001, %00000000, %00000000, %00000000, %00000000, %00000000
407
1198
 
1199
+ ; ── Vector table ──────────────────────────────────────────────────────
408
1200
  org $FFFA
409
1201
  .word START
410
1202
  .word START