romdevtools 0.27.0 → 0.29.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. package/AGENTS.md +56 -44
  2. package/CHANGELOG.md +355 -0
  3. package/README.md +4 -4
  4. package/examples/README.md +7 -7
  5. package/examples/atari2600/templates/platformer.asm +1227 -325
  6. package/examples/atari2600/templates/puzzle.asm +1056 -0
  7. package/examples/atari2600/templates/racing.asm +909 -257
  8. package/examples/atari2600/templates/shmup.asm +1035 -218
  9. package/examples/atari2600/templates/sports.asm +1143 -229
  10. package/examples/atari7800/templates/hello_sprite.c +8 -4
  11. package/examples/atari7800/templates/platformer.c +991 -152
  12. package/examples/atari7800/templates/puzzle.c +1091 -145
  13. package/examples/atari7800/templates/racing.c +949 -118
  14. package/examples/atari7800/templates/shmup.c +812 -130
  15. package/examples/atari7800/templates/sports.c +820 -181
  16. package/examples/c64/templates/platformer.c +876 -157
  17. package/examples/c64/templates/puzzle.c +881 -143
  18. package/examples/c64/templates/racing.c +873 -88
  19. package/examples/c64/templates/shmup.c +762 -154
  20. package/examples/c64/templates/sports.c +755 -95
  21. package/examples/gb/templates/platformer.c +841 -175
  22. package/examples/gb/templates/puzzle.c +1094 -176
  23. package/examples/gb/templates/racing.c +761 -169
  24. package/examples/gb/templates/shmup.c +679 -169
  25. package/examples/gb/templates/sports.c +790 -153
  26. package/examples/gba/templates/platformer.c +624 -169
  27. package/examples/gba/templates/puzzle.c +535 -207
  28. package/examples/gba/templates/racing.c +513 -196
  29. package/examples/gba/templates/shmup.c +565 -168
  30. package/examples/gba/templates/sports.c +454 -162
  31. package/examples/gbc/templates/platformer.c +944 -176
  32. package/examples/gbc/templates/puzzle.c +1131 -177
  33. package/examples/gbc/templates/racing.c +891 -175
  34. package/examples/gbc/templates/shmup.c +827 -179
  35. package/examples/gbc/templates/sports.c +870 -156
  36. package/examples/genesis/templates/platformer.c +747 -129
  37. package/examples/genesis/templates/puzzle.c +702 -208
  38. package/examples/genesis/templates/racing.c +728 -193
  39. package/examples/genesis/templates/shmup.c +535 -142
  40. package/examples/genesis/templates/shmup_2p.c +13 -1
  41. package/examples/genesis/templates/sports.c +495 -158
  42. package/examples/gg/templates/platformer.c +883 -214
  43. package/examples/gg/templates/puzzle.c +906 -181
  44. package/examples/gg/templates/racing.c +919 -160
  45. package/examples/gg/templates/shmup.c +716 -177
  46. package/examples/gg/templates/sports.c +735 -128
  47. package/examples/lynx/templates/platformer.c +604 -50
  48. package/examples/lynx/templates/puzzle.c +533 -130
  49. package/examples/lynx/templates/racing.c +538 -102
  50. package/examples/lynx/templates/shmup.c +461 -122
  51. package/examples/lynx/templates/sports.c +496 -69
  52. package/examples/msx/platformer/main.c +648 -159
  53. package/examples/msx/puzzle/main.c +750 -185
  54. package/examples/msx/racing/main.c +669 -177
  55. package/examples/msx/shmup/main.c +460 -177
  56. package/examples/msx/sports/main.c +591 -124
  57. package/examples/nes/templates/platformer.c +586 -160
  58. package/examples/nes/templates/puzzle.c +603 -222
  59. package/examples/nes/templates/racing.c +505 -197
  60. package/examples/nes/templates/shmup.c +339 -144
  61. package/examples/nes/templates/sports.c +341 -182
  62. package/examples/pce/platformer/main.c +875 -204
  63. package/examples/pce/puzzle/main.c +797 -216
  64. package/examples/pce/racing/main.c +782 -206
  65. package/examples/pce/shmup/main.c +638 -211
  66. package/examples/pce/sports/main.c +585 -167
  67. package/examples/porting-across-platforms/README.md +1 -1
  68. package/examples/sms/templates/platformer.c +765 -176
  69. package/examples/sms/templates/puzzle.c +783 -177
  70. package/examples/sms/templates/racing.c +812 -133
  71. package/examples/sms/templates/shmup.c +601 -148
  72. package/examples/sms/templates/shmup_2p.c +17 -1
  73. package/examples/sms/templates/sports.c +633 -121
  74. package/examples/snes/templates/music_demo.c +7 -0
  75. package/examples/snes/templates/platformer-data.asm +123 -24
  76. package/examples/snes/templates/platformer-hdr.asm +57 -0
  77. package/examples/snes/templates/platformer.c +587 -149
  78. package/examples/snes/templates/puzzle-data.asm +116 -21
  79. package/examples/snes/templates/puzzle-hdr.asm +57 -0
  80. package/examples/snes/templates/puzzle.c +632 -185
  81. package/examples/snes/templates/racing-data.asm +390 -32
  82. package/examples/snes/templates/racing-hdr.asm +57 -0
  83. package/examples/snes/templates/racing.c +807 -177
  84. package/examples/snes/templates/shmup-data.asm +87 -29
  85. package/examples/snes/templates/shmup-hdr.asm +57 -0
  86. package/examples/snes/templates/shmup.c +459 -180
  87. package/examples/snes/templates/sports-data.asm +48 -2
  88. package/examples/snes/templates/sports-hdr.asm +57 -0
  89. package/examples/snes/templates/sports.c +414 -156
  90. package/package.json +12 -12
  91. package/src/cores/wasm/bluemsx_libretro.js +1 -1
  92. package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
  93. package/src/cores/wasm/fceumm_libretro.js +1 -1
  94. package/src/cores/wasm/fceumm_libretro.wasm +0 -0
  95. package/src/cores/wasm/gambatte_libretro.js +1 -1
  96. package/src/cores/wasm/gambatte_libretro.wasm +0 -0
  97. package/src/cores/wasm/geargrafx_libretro.js +1 -1
  98. package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
  99. package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
  100. package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
  101. package/src/cores/wasm/handy_libretro.js +1 -1
  102. package/src/cores/wasm/handy_libretro.wasm +0 -0
  103. package/src/cores/wasm/mgba_libretro.js +1 -1
  104. package/src/cores/wasm/mgba_libretro.wasm +0 -0
  105. package/src/cores/wasm/prosystem_libretro.js +1 -1
  106. package/src/cores/wasm/prosystem_libretro.wasm +0 -0
  107. package/src/cores/wasm/snes9x_libretro.js +1 -1
  108. package/src/cores/wasm/snes9x_libretro.wasm +0 -0
  109. package/src/cores/wasm/stella2014_libretro.js +1 -1
  110. package/src/cores/wasm/stella2014_libretro.wasm +0 -0
  111. package/src/cores/wasm/vice_x64_libretro.js +1 -1
  112. package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
  113. package/src/host/LibretroHost.js +304 -11
  114. package/src/http/tool-registry.js +11 -11
  115. package/src/mcp/server.js +6 -0
  116. package/src/mcp/tools/cheats.js +2 -1
  117. package/src/mcp/tools/disasm-rebuild.js +315 -65
  118. package/src/mcp/tools/disasm.js +149 -28
  119. package/src/mcp/tools/find-references.js +216 -51
  120. package/src/mcp/tools/frame.js +14 -6
  121. package/src/mcp/tools/index.js +18 -4
  122. package/src/mcp/tools/input.js +31 -7
  123. package/src/mcp/tools/lifecycle.js +6 -4
  124. package/src/mcp/tools/memory.js +208 -39
  125. package/src/mcp/tools/platform-docs.js +1 -1
  126. package/src/mcp/tools/playtest.js +56 -4
  127. package/src/mcp/tools/preview-tile.js +6 -2
  128. package/src/mcp/tools/project.js +1114 -120
  129. package/src/mcp/tools/rom-id.js +5 -1
  130. package/src/mcp/tools/run-until.js +4 -2
  131. package/src/mcp/tools/snippets.js +6 -6
  132. package/src/mcp/tools/sprite-pipeline.js +14 -2
  133. package/src/mcp/tools/state.js +2 -1
  134. package/src/mcp/tools/tile-inspect.js +8 -1
  135. package/src/mcp/tools/toolchain.js +55 -11
  136. package/src/mcp/tools/watch-memory.js +145 -27
  137. package/src/observer/bus.js +73 -0
  138. package/src/observer/livestream.html +4 -2
  139. package/src/observer/tool-wrap.js +17 -14
  140. package/src/platforms/_guides/ROMHACKING_PLAYBOOK.md +64 -17
  141. package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
  142. package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
  143. package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
  144. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  145. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  146. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  147. package/src/platforms/gb/MENTAL_MODEL.md +19 -4
  148. package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
  149. package/src/platforms/gb/lib/c/README.md +10 -11
  150. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  151. package/src/platforms/gb/lib/c/patch-header.js +19 -6
  152. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  153. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  154. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  155. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  156. package/src/platforms/gbc/MENTAL_MODEL.md +16 -4
  157. package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
  158. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  159. package/src/platforms/gbc/lib/c/README.md +10 -11
  160. package/src/platforms/gbc/lib/c/font.h +43 -0
  161. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  162. package/src/platforms/gbc/lib/c/patch-header.js +19 -6
  163. package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
  164. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  165. package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
  166. package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
  167. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  168. package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
  169. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  170. package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
  171. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  172. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  173. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  174. package/src/platforms/lynx/lib/c/lynx_sfx.c +38 -2
  175. package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
  176. package/src/platforms/msx/MENTAL_MODEL.md +11 -5
  177. package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
  178. package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
  179. package/src/platforms/msx/lib/c/msx_hw.h +3 -0
  180. package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
  181. package/src/platforms/nes/MENTAL_MODEL.md +12 -5
  182. package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
  183. package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
  184. package/src/platforms/pce/MENTAL_MODEL.md +14 -5
  185. package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
  186. package/src/platforms/pce/lib/c/pce_hw.h +13 -1
  187. package/src/platforms/pce/lib/c/pce_sound.c +22 -0
  188. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  189. package/src/platforms/sms/MENTAL_MODEL.md +11 -6
  190. package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
  191. package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
  192. package/src/platforms/snes/MENTAL_MODEL.md +7 -2
  193. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  194. package/src/playtest/playtest.js +73 -3
  195. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  196. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  197. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  198. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  199. package/src/toolchains/index.js +64 -19
@@ -1,174 +1,1005 @@
1
- /* racing.c — Atari 7800 top-down racing (minimal).
1
+ /* ── racing.c — Atari 7800 top-down road racer (complete example game) ────────
2
2
  *
3
- * SCAFFOLD CAVEAT: the original "3-lane racer with 3 enemy cars" is
4
- * deferred. Multi-object scenes on 7800 require careful per-scanline
5
- * DL pool sizing within the 2 KB RAM1 budget (see sports.c for a 3-
6
- * object working example, MENTAL_MODEL.md for the MARIA constraints).
3
+ * PISTON PINCH a COMPLETE, working game: title screen, 1P endless race with
4
+ * speed control, and 2P SIMULTANEOUS split-lane VERSUS (both cars on the same
5
+ * road at once, P2 on JOYSTICK PORT 1), a vertically-"scrolling" road, dense
6
+ * descending traffic, crash/lives rules, in-session best distance, TIA music +
7
+ * SFX, and the 7800's signature feature: MARIA OBJECT QUANTITY. The player
8
+ * car(s) + up to 10 traffic cars are all just display-list entries MARIA DMAs
9
+ * per scanline — a thick stream of traffic no 2600 (5 hardware objects) draws
10
+ * comfortably. On the 7800 there is no sprite table; every car IS a DL entry,
11
+ * and quantity is the whole point of the chip.
7
12
  *
8
- * This minimal scaffold demonstrates: player car (one 16x8 sprite)
9
- * that moves LEFT/RIGHT between 3 lanes. Extend by reading sports.c
10
- * for the per-scanline pool pattern and add 1-3 enemy cars within
11
- * the RAM budget.
13
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
14
+ * very different one. The markers tell you what's what:
15
+ * HARDWARE IDIOM (load-bearing) dodges a documented 7800/MARIA footgun;
16
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
17
+ * GAME LOGIC (clay) — traffic patterns, speeds, tuning, art: reshape freely.
18
+ *
19
+ * What depends on what:
20
+ * atari7800_sfx.{h,c} — TIA one-shot effects (we give it voice 1; the
21
+ * inline music player below owns voice 0 — TIA only HAS two voices).
22
+ * cc65's atari7800 target crt0 + atari7800.cfg — boot, BSS in RAM1
23
+ * ($1800-$203F), C parameter stack at the TOP of RAM3 growing DOWN
24
+ * ($2800 →). This game claims the BOTTOM of RAM3 ($2200-$25FD) for its
25
+ * display-list pool — see the RAM MAP below before moving anything.
26
+ *
27
+ * ════════════════════════════════════════════════════════════════════════
28
+ * NO HARDWARE SCROLL — the load-bearing design fact of a 7800 racer. MARIA
29
+ * has NO scroll register (unlike the NES racer's BG Y-scroll, the SMS/GG
30
+ * VDP, or the Genesis VSRAM). The road cannot be scrolled; it can only be
31
+ * REDRAWN. A top-down racer therefore FAKES vertical road motion two ways,
32
+ * both used here:
33
+ * 1. The lane DASHES march downward — each frame the dash pattern's phase
34
+ * advances, so the on-off rhythm of the centre/lane lines slides toward
35
+ * the player. This is the whole illusion of "the road is moving"; it is
36
+ * a CHEAP per-frame swap of which dash-drawable each road zone points at
37
+ * (no DLL teardown — see the dash-bank idiom), NOT a scroll.
38
+ * 2. The TRAFFIC descends — cars are display-list objects with their own Y,
39
+ * moving down the screen at road speed (they read as slower cars you are
40
+ * overtaking). This is where the MARIA object-quantity signature lives:
41
+ * a thick stream of independent traffic objects.
42
+ * The asphalt itself (the solid road band + roadside grass) is STATIC — it is
43
+ * a single colour either way, so redrawing it would buy nothing. Documented
44
+ * honestly so a fork doesn't go hunting for a scroll register that isn't there.
45
+ * ════════════════════════════════════════════════════════════════════════
46
+ *
47
+ * PERSISTENCE — honest note: the canonical 7800 save path is the High Score
48
+ * Cart (HSC): a pass-through cartridge with 2KB battery RAM at $1000-$17FF
49
+ * plus a directory ROM. The bundled prosystem core does NOT implement HSC
50
+ * (probed 2026-06: retro_get_memory(SAVE_RAM) size = 0, and the core binary
51
+ * has no HSC code at all), so this game keeps BEST DISTANCE IN-SESSION ONLY
52
+ * (it survives play → title → play, dies on power-off). Do not fake
53
+ * persistence the hardware path can't back — if a future core round adds
54
+ * HSC, wire best into $1000-$17FF and it becomes real.
55
+ *
56
+ * Frame budget (NTSC): the per-tick update (steer + speed + ≤10 traffic ×
57
+ * ≤2 cars AABB + the dash phase step + HUD redraw) fits in one 60Hz frame,
58
+ * dipping to two on heavy frames — vblank_wait() paces the sim, the classic
59
+ * 8-bit pattern. MARIA does not care — it re-walks the same DLs every frame,
60
+ * so a slow CPU loop never blanks or tears the whole screen. That budget only
61
+ * holds because of the #pragma optimize(on) right below — read its comment
62
+ * before deleting it.
12
63
  */
64
+
13
65
  #include <stdint.h>
66
+ #include <string.h>
14
67
  #include "atari7800_sfx.h"
15
68
 
69
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
70
+ * cc65 SHIPS WITH ITS OPTIMIZER OFF, and this toolchain does not pass -O —
71
+ * each translation unit must opt in. Without this pragma the unoptimized
72
+ * emit pass made the main loop take ~9 frames per sim tick instead of 1-2
73
+ * (measured on the 7800 shmup: 8.8 → 1.7 frames/tick on prosystem), and
74
+ * every TICK-DENOMINATED timer silently stretched 4-5x in wall-clock terms:
75
+ * the crash-blink grace, the spawn cadence, the marching-dash phase — all
76
+ * ~4.5x too slow, so the road "scroll" crawled and traffic oozed down. That
77
+ * presents as "broken game feel / sprite vanishing" (a synchronized blink
78
+ * keeps an object off screen for ~600ms at a time) — but the DLL, the zone
79
+ * pointers, and every pool slot were byte-perfect when read back from RAM.
80
+ * The footgun generalizes: on a 1.79MHz 6502 the C optimizer is not a nicety,
81
+ * it IS the frame budget, and a too-slow loop shows up as broken GAME RULES
82
+ * (stretched timers, missed 1-frame input edges), not as a slow-looking
83
+ * screen — MARIA keeps repainting the same display lists at a rock-steady
84
+ * 60Hz no matter how far behind the CPU falls. If your fork feels like
85
+ * molasses or "ignores" short button taps, check this pragma is still here
86
+ * before debugging the display lists. */
87
+ #pragma optimize(on)
88
+
89
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
90
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
91
+ #define GAME_TITLE "PISTON PINCH"
92
+
93
+ /* ── MARIA + TIA + RIOT registers (full list in MENTAL_MODEL.md) ── */
16
94
  #define BACKGRND (*(volatile uint8_t*)0x20)
17
95
  #define P0C1 (*(volatile uint8_t*)0x21)
18
96
  #define P0C2 (*(volatile uint8_t*)0x22)
19
97
  #define P0C3 (*(volatile uint8_t*)0x23)
20
98
  #define P1C1 (*(volatile uint8_t*)0x25)
21
- #define P2C1 (*(volatile uint8_t*)0x29)
99
+ #define P1C2 (*(volatile uint8_t*)0x26)
100
+ #define P1C3 (*(volatile uint8_t*)0x27)
22
101
  #define MSTAT (*(volatile uint8_t*)0x28)
102
+ #define P2C1 (*(volatile uint8_t*)0x29)
103
+ #define P2C2 (*(volatile uint8_t*)0x2A)
104
+ #define P2C3 (*(volatile uint8_t*)0x2B)
23
105
  #define DPPH (*(volatile uint8_t*)0x2C)
106
+ #define P3C1 (*(volatile uint8_t*)0x2D)
107
+ #define P3C2 (*(volatile uint8_t*)0x2E)
108
+ #define P3C3 (*(volatile uint8_t*)0x2F)
24
109
  #define DPPL (*(volatile uint8_t*)0x30)
110
+ #define P4C1 (*(volatile uint8_t*)0x31)
111
+ #define P4C2 (*(volatile uint8_t*)0x32)
112
+ #define P4C3 (*(volatile uint8_t*)0x33)
25
113
  #define CHARBASE (*(volatile uint8_t*)0x34)
114
+ #define P5C1 (*(volatile uint8_t*)0x35)
26
115
  #define OFFSET (*(volatile uint8_t*)0x38)
116
+ #define P6C1 (*(volatile uint8_t*)0x39)
27
117
  #define CTRL (*(volatile uint8_t*)0x3C)
28
- #define SWCHA (*(volatile uint8_t*)0x280)
118
+ #define P7C1 (*(volatile uint8_t*)0x3D)
29
119
 
30
- #define JOY_LEFT 0x20
31
- #define JOY_RIGHT 0x10
120
+ /* TIA audio (shared with the music player below; atari7800_sfx.c has the
121
+ * same defines — the chip is tiny enough that duplicating 6 lines beats a
122
+ * header dependency the fork machinery would have to carry). */
123
+ #define AUDC0 (*(volatile uint8_t*)0x15)
124
+ #define AUDC1 (*(volatile uint8_t*)0x16)
125
+ #define AUDF0 (*(volatile uint8_t*)0x17)
126
+ #define AUDF1 (*(volatile uint8_t*)0x18)
127
+ #define AUDV0 (*(volatile uint8_t*)0x19)
128
+ #define AUDV1 (*(volatile uint8_t*)0x1A)
32
129
 
33
- /* 16-pixel-wide (= 4 bytes in 160A) × 8 row car sprite. */
34
- static const uint8_t car_row0[4] = { 0x05, 0x55, 0x55, 0x50 };
35
- static const uint8_t car_row1[4] = { 0x5A, 0x5A, 0xA5, 0xA5 };
36
- static const uint8_t car_row2[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
37
- static const uint8_t car_row3[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
38
- static const uint8_t car_row4[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
39
- static const uint8_t car_row5[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
40
- static const uint8_t car_row6[4] = { 0x5A, 0x5A, 0xA5, 0xA5 };
41
- static const uint8_t car_row7[4] = { 0x05, 0x55, 0x55, 0x50 };
130
+ #define SWCHA (*(volatile uint8_t*)0x280)
131
+ #define INPT4 (*(volatile uint8_t*)0x0C) /* P1 fire, active low (bit 7) */
132
+ #define INPT5 (*(volatile uint8_t*)0x0D) /* P2 fire, active low (bit 7) */
42
133
 
43
- #define MK_DL(name) static uint8_t name[7] = { 0, 0x40, 0, 0x1C, 80, 0, 0 }
44
- MK_DL(dl_row0); MK_DL(dl_row1); MK_DL(dl_row2); MK_DL(dl_row3);
45
- MK_DL(dl_row4); MK_DL(dl_row5); MK_DL(dl_row6); MK_DL(dl_row7);
134
+ /* ── HARDWARE IDIOM (load-bearing reshape gameplay around this; see TROUBLESHOOTING) ──
135
+ * SWCHA joystick bit order — the #1 7800 input footgun. After the ~SWCHA
136
+ * invert, port 0 (left jack) lives in the HIGH nibble as
137
+ * Right($80) Left($40) Down($20) Up($10), and port 1 (right jack) in the
138
+ * LOW nibble as Right($08) Left($04) Down($02) Up($01). Writing the masks
139
+ * in "natural reading order" (UP=0x80…) is exactly REVERSED and makes the
140
+ * stick's vertical axis steer horizontally — a bug weird enough to
141
+ * misdiagnose as a core problem. Verified bit-by-bit against prosystem.
142
+ * 2P versus uses BOTH ports: player 0 reads the high nibble + INPT4 fire,
143
+ * player 1 the low nibble + INPT5 fire. */
144
+ #define J1_RIGHT 0x80
145
+ #define J1_LEFT 0x40
146
+ #define J1_DOWN 0x20
147
+ #define J1_UP 0x10
148
+ #define J2_RIGHT 0x08
149
+ #define J2_LEFT 0x04
150
+ #define J2_DOWN 0x02
151
+ #define J2_UP 0x01
46
152
 
47
- static uint8_t dl_empty[2] = { 0, 0 };
153
+ /* ════════════════════════════════════════════════════════════════════════
154
+ * RAM MAP — the 7800 gives you 4KB ($1800-$27FF) and the stock cc65 config
155
+ * only hands the linker the first 2112 bytes of it:
156
+ *
157
+ * $1800-$203F RAM1 — cc65 DATA + BSS (everything `static` below)
158
+ * $2040-$20FF (gap the cc65 cfg skips — unused here)
159
+ * $2100-$213F RAM2 — unused here
160
+ * $2200-$25FD RAM3 bottom — OUR display-list pool/canvas arena (POOLB):
161
+ * raw pointer, invisible to the linker, 1022 bytes
162
+ * $25FE-$27FF RAM3 top — cc65 C parameter stack (crt0 starts it at $2800
163
+ * growing DOWN; ~510 bytes is plenty for these call depths,
164
+ * but if you add deep recursion, shrink POOLB_LINES first)
165
+ * ════════════════════════════════════════════════════════════════════════ */
166
+ #define POOLB ((uint8_t*)0x2200)
167
+
168
+ /* ── Screen layout (243 NTSC zone-lines; the visible frame is ~lines 9-232) ──
169
+ * lines 0- 15 blank (top overscan) 1 DLL entry, 16 tall
170
+ * lines 16- 23 HUD text row (RAM canvas) 8 entries, 1 tall each
171
+ * lines 24- 25 divider band 1 entry, 2 tall
172
+ * lines 26-145 THE ROAD — 120 one-line zones 120 entries (the pool)
173
+ * lines 146-147 guard band 1 entry, 2 tall
174
+ * lines 148-242 decor stripes (horizon glow) 12 entries, 8/7 tall
175
+ * Total: 143 DLL entries = 429 bytes (vs 729 for the naive all-1-line DLL —
176
+ * mixed zone heights are how real 7800 games keep the DLL small).
177
+ * The ROAD pool holds every moving object: both player cars AND the descending
178
+ * traffic. The asphalt + roadside grass + marching dashes are STANDING road
179
+ * drawables the field zones point at when no car is on that line. */
180
+ #define FIELD_LINES 120
181
+ #define FIELD_DLL_OFF 30 /* byte offset of road entry 0 in dll[] */
182
+
183
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
184
+ * Object art. 160A mode: 1 byte = 4 pixels of 2 bits each; pixel value
185
+ * 1/2/3 = colour 1/2/3 of the palette the DL entry names, 0 = transparent.
186
+ * Rows are stored top-down, consecutive (the 1-scanline-zone pattern below
187
+ * means NO page-alignment dance — see "offset addressing quirk" in
188
+ * MENTAL_MODEL.md for what multi-line zones would demand instead). */
189
+
190
+ /* Player car, 12px wide (3 bytes) x 10 rows — nose up. Colours: 1 body,
191
+ * 2 window/shade, 3 highlight. Drawn with palette 1 (P1) or 2 (P2). */
192
+ static const uint8_t GFX_CAR[10 * 3] = {
193
+ 0x01, 0x55, 0x40, /* 1111111 (roof) */
194
+ 0x05, 0x55, 0x50, /* 111111111 */
195
+ 0x06, 0xAA, 0x90, /* 1222222 1 (glass) */
196
+ 0x06, 0xAA, 0x90, /* 1222222 1 */
197
+ 0x15, 0x55, 0x54, /* 11111111111 (hood) */
198
+ 0x15, 0x55, 0x54, /* 11111111111 */
199
+ 0x36, 0xAA, 0x9C, /* 31222222 13 (mirrr) */
200
+ 0x05, 0x55, 0x50, /* 111111111 */
201
+ 0x05, 0x55, 0x50, /* 111111111 */
202
+ 0x14, 0x00, 0x14, /* 11 11 (wheels)*/
203
+ };
204
+
205
+ /* Traffic car, 12px wide (3 bytes) x 8 rows — tail up (you overtake it).
206
+ * Drawn with palette 3 (rival red). */
207
+ static const uint8_t GFX_TRAFFIC[8 * 3] = {
208
+ 0x14, 0x00, 0x14, /* 11 11 (wheels)*/
209
+ 0x05, 0x55, 0x50, /* 111111111 */
210
+ 0x36, 0xAA, 0x9C, /* 31222222 13 */
211
+ 0x15, 0x55, 0x54, /* 11111111111 (hood) */
212
+ 0x15, 0x55, 0x54, /* 11111111111 */
213
+ 0x06, 0xAA, 0x90, /* 1222222 1 (glass) */
214
+ 0x05, 0x55, 0x50, /* 111111111 */
215
+ 0x01, 0x55, 0x40, /* 1111111 (tail) */
216
+ };
217
+
218
+ /* DL mode bytes for the 4-byte (direct) entry form: palette in bits 5-7,
219
+ * width as (32 - width_bytes) in bits 0-4 (must be non-zero — a zero low
220
+ * 5 bits would make MARIA parse a 5-byte entry instead). */
221
+ #define MODE_CAR1 ((1u << 5) | (32 - 3)) /* palette 1, 3 bytes wide */
222
+ #define MODE_CAR2 ((2u << 5) | (32 - 3)) /* palette 2 */
223
+ #define MODE_TRAFFIC ((3u << 5) | (32 - 3)) /* palette 3, 3 bytes wide */
48
224
 
49
- /* ── Background road ──────────────────────────────────────────────
50
- * Without a full-screen drawable the display list emits only the car
51
- * and ~99% of the screen stays the flat BACKGRND colour (reads as
52
- * "blank"). Each road zone draws three full-width segments: grass on
53
- * the left (palette 1), the grey road down the centre (palette 2),
54
- * grass on the right (palette 1). Width = byte[3] low 5 bits (32-n);
55
- * high 3 bits = palette. */
56
- static const uint8_t band_pix[16] = {
57
- 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,
58
- 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
225
+ /* ── GAME LOGIC (clay) — 8x8 text font, 1 bit per pixel, 7px glyphs.
226
+ * The 7800 has NO text mode and no tilemap; text is just more objects.
227
+ * The text path here: expand glyphs into a 32-byte-wide RAM canvas
228
+ * (= 128px, 16 characters), then show the canvas with ONE wide DL entry
229
+ * per scanline. One drawable per line beats one-DL-entry-per-character
230
+ * by 16x in MARIA DMA time. Index order: 0-9 A-Z dash space. */
231
+ static const uint8_t FONT[38 * 8] = {
232
+ 0x70,0x88,0x98,0xA8,0xC8,0x88,0x70,0x00, /* 0 */
233
+ 0x20,0x60,0x20,0x20,0x20,0x20,0x70,0x00, /* 1 */
234
+ 0x70,0x88,0x08,0x30,0x40,0x80,0xF8,0x00, /* 2 */
235
+ 0x70,0x88,0x08,0x30,0x08,0x88,0x70,0x00, /* 3 */
236
+ 0x10,0x30,0x50,0x90,0xF8,0x10,0x10,0x00, /* 4 */
237
+ 0xF8,0x80,0xF0,0x08,0x08,0x88,0x70,0x00, /* 5 */
238
+ 0x30,0x40,0x80,0xF0,0x88,0x88,0x70,0x00, /* 6 */
239
+ 0xF8,0x08,0x10,0x20,0x40,0x40,0x40,0x00, /* 7 */
240
+ 0x70,0x88,0x88,0x70,0x88,0x88,0x70,0x00, /* 8 */
241
+ 0x70,0x88,0x88,0x78,0x08,0x10,0x60,0x00, /* 9 */
242
+ 0x20,0x50,0x88,0x88,0xF8,0x88,0x88,0x00, /* A */
243
+ 0xF0,0x88,0x88,0xF0,0x88,0x88,0xF0,0x00, /* B */
244
+ 0x70,0x88,0x80,0x80,0x80,0x88,0x70,0x00, /* C */
245
+ 0xF0,0x88,0x88,0x88,0x88,0x88,0xF0,0x00, /* D */
246
+ 0xF8,0x80,0x80,0xF0,0x80,0x80,0xF8,0x00, /* E */
247
+ 0xF8,0x80,0x80,0xF0,0x80,0x80,0x80,0x00, /* F */
248
+ 0x70,0x88,0x80,0xB8,0x88,0x88,0x70,0x00, /* G */
249
+ 0x88,0x88,0x88,0xF8,0x88,0x88,0x88,0x00, /* H */
250
+ 0x70,0x20,0x20,0x20,0x20,0x20,0x70,0x00, /* I */
251
+ 0x38,0x10,0x10,0x10,0x10,0x90,0x60,0x00, /* J */
252
+ 0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x00, /* K */
253
+ 0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00, /* L */
254
+ 0x88,0xD8,0xA8,0xA8,0x88,0x88,0x88,0x00, /* M */
255
+ 0x88,0xC8,0xA8,0x98,0x88,0x88,0x88,0x00, /* N */
256
+ 0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00, /* O */
257
+ 0xF0,0x88,0x88,0xF0,0x80,0x80,0x80,0x00, /* P */
258
+ 0x70,0x88,0x88,0x88,0xA8,0x90,0x68,0x00, /* Q */
259
+ 0xF0,0x88,0x88,0xF0,0xA0,0x90,0x88,0x00, /* R */
260
+ 0x78,0x80,0x80,0x70,0x08,0x08,0xF0,0x00, /* S */
261
+ 0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0x00, /* T */
262
+ 0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00, /* U */
263
+ 0x88,0x88,0x88,0x88,0x88,0x50,0x20,0x00, /* V */
264
+ 0x88,0x88,0x88,0xA8,0xA8,0xD8,0x88,0x00, /* W */
265
+ 0x88,0x88,0x50,0x20,0x50,0x88,0x88,0x00, /* X */
266
+ 0x88,0x88,0x50,0x20,0x20,0x20,0x20,0x00, /* Y */
267
+ 0xF8,0x08,0x10,0x20,0x40,0x80,0xF8,0x00, /* Z */
268
+ 0x00,0x00,0x00,0x78,0x00,0x00,0x00,0x00, /* - */
269
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* space */
59
270
  };
60
- /* 8 bytes (32 px) grass @ x0, 16 bytes (64 px) road @ x32,
61
- * 8 bytes (32 px) grass @ x96, terminator. */
62
- static uint8_t dl_road[16] = {
63
- 0, 0x40, 0, (1 << 5) | 24, 0,
64
- 0, 0x40, 0, (2 << 5) | 16, 32,
65
- 0, 0x40, 0, (1 << 5) | 24, 96,
66
- 0
271
+ /* nibble 2bpp expansion: each 1 bit becomes pixel value 1 (palette c1) */
272
+ static const uint8_t NIB2[16] = {
273
+ 0x00,0x01,0x04,0x05,0x10,0x11,0x14,0x15,
274
+ 0x40,0x41,0x44,0x45,0x50,0x51,0x54,0x55,
67
275
  };
68
276
 
69
- static void set_road_addr(void) {
70
- uint16_t a = (uint16_t)(uintptr_t)band_pix;
71
- dl_road[0] = dl_road[5] = dl_road[10] = (uint8_t)(a & 0xFF);
72
- dl_road[2] = dl_road[7] = dl_road[12] = (uint8_t)(a >> 8);
277
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
278
+ * Solid band drawable for multi-line zones AND the static asphalt. Inside a
279
+ * zone of height H, MARIA fetches scanline l's pixels from ADDR + (H-1-l)*256
280
+ * the "offset addressing quirk". A multi-line drawable therefore needs valid
281
+ * data at the SAME low-byte offset across H consecutive 256-byte pages. For
282
+ * solid colour bands we sidestep alignment entirely: a 2KB ROM run of 0x55
283
+ * means ANY address inside the first page works for zones up to 8 tall (8
284
+ * pages × 256). Costs 2KB of a 32KB cart — ROM is the cheap resource here. The
285
+ * road's grass + asphalt rails reuse SOLID8: each is a wide colour-1 object
286
+ * drawn into the one-line road zones it spans (1-line zones ⇒ the quirk
287
+ * vanishes, any SOLID8 address works). */
288
+ #define S16 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
289
+ #define S256 S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16
290
+ static const uint8_t SOLID8[2048] = { S256,S256,S256,S256,S256,S256,S256,S256 };
291
+
292
+ /* Full-width band DL: a DL drawable is at most 32 bytes (128px), so a
293
+ * 160px line takes TWO 5-byte entries + terminator = 11 bytes. 5-byte
294
+ * form: lo, $40 (extended, write-mode 0 = 160A), hi, palette|width, X.
295
+ * Width 32 encodes as 0 in the low 5 bits — legal ONLY in 5-byte form. */
296
+ #define MK_BAND(name, pal) static uint8_t name[11] = { \
297
+ 0, 0x40, 0, ((pal) << 5) | 0, 0, /* 128px @ x=0 */ \
298
+ 0, 0x40, 0, ((pal) << 5) | 24, 128, /* 32px @ x=128 */ \
299
+ 0 }
300
+ MK_BAND(dl_band_a, 6);
301
+ MK_BAND(dl_band_b, 7);
302
+ static uint8_t dl_empty[2] = { 0, 0 };
303
+
304
+ /* ════════════════════════════════════════════════════════════════════════
305
+ * ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
306
+ * THE ROAD as STANDING drawables + the MARCHING-DASH "scroll" — the 7800
307
+ * answer to "there is no scroll register".
308
+ *
309
+ * MARIA hierarchy refresher: DPP → DLL (one entry per ZONE: height + DL
310
+ * pointer) → DL (one 4/5-byte entry per OBJECT crossing that zone) → pixel
311
+ * bytes. There is no sprite table; "an object" IS a DL entry.
312
+ *
313
+ * The road is 120 one-scanline zones. Each zone's STANDING image is a short
314
+ * pre-built DL (road_dl[bank][...]) holding: a wide grey asphalt band, the two
315
+ * white shoulder rails, the solid centre divider, and — on the lines a lane
316
+ * DASH falls — a short white dash object. Every zone points at one of two
317
+ * pre-built DASH BANKS that differ only in WHERE the dash on-segments sit:
318
+ *
319
+ * MARCHING DASH (the fake scroll) — we don't move pixels, we re-point each
320
+ * road zone at the dash bank whose on/off phase matches that line's current
321
+ * offset. Advancing a single global `dash_phase` each frame slides the dash
322
+ * rhythm DOWNWARD with zero per-pixel work — just 120 one-byte DLL writes
323
+ * choosing bank A vs B per line. Cheap enough to do every frame inside the
324
+ * budget; reads as the road rushing toward you. The asphalt + rails are the
325
+ * SAME in both banks, so only the dashes appear to move.
326
+ *
327
+ * The per-line DL slot is the 14-byte road pool: the standing road object(s)
328
+ * are built ONCE per bank, and each frame we only repoint the DLL zone at the
329
+ * right bank — UNLESS a car sits on that line, in which case we emit the car
330
+ * INTO that line's pool slot after the standing road bytes (cars-as-objects).
331
+ *
332
+ * WHY ≤3 OBJECTS PER LINE — the MARIA DMA budget, the dial this game turns:
333
+ * MARIA steals the bus per scanline (~113 DMA cycles before a line runs out).
334
+ * The standing road is at most 2 wide band objects + 1 dash; we keep cars to
335
+ * ≤1 extra per line by spacing traffic vertically, so even a busy road line
336
+ * stays inside budget. When a 4th object-row would land on a line we DROP it
337
+ * for that frame — a one-line flicker, the artifact real dense 7800 games show.
338
+ *
339
+ * Rebuild-vs-patch doctrine (MENTAL_MODEL.md): the DLL is built ONCE and only
340
+ * its 3-byte road entries are repointed (dash phase + cars), with car emits
341
+ * writing only bytes INSIDE existing 14-byte slots. Tearing down the DLL
342
+ * itself mid-game races MARIA's walker — the classic "works one frame then the
343
+ * screen falls apart" 7800 bug.
344
+ * ════════════════════════════════════════════════════════════════════════ */
345
+ /* Per-line DL slot is 14 bytes (same as the shmup). The standing road is two
346
+ * 4-byte DIRECT objects (asphalt + dash = 8 bytes), then room for ONE 4-byte
347
+ * car entry, then the terminator — 8+4+1 = 13 ≤ 14. (Asphalt fits the 4-byte
348
+ * direct form because its 16-byte width encodes as a non-zero low-5-bits 32-16;
349
+ * the 5-byte extended form is only needed for the full-32-byte bands.)
350
+ * LINE_FULL gates car emits so the terminator never spills into the next slot. */
351
+ #define LINE_BYTES 14
352
+ #define LINE_FULL 12 /* stop emitting cars once a line is this full */
353
+ #define POOLA_LINES 47 /* 47 lines in BSS; the rest in RAM3 (POOLB) */
354
+ static uint8_t pool_a[POOLA_LINES * LINE_BYTES];
355
+ static uint8_t* line_dl[FIELD_LINES];
356
+ static uint8_t line_used[FIELD_LINES];
357
+
358
+ static uint8_t dll[143 * 3];
359
+ static uint8_t hud_canvas[8 * 32]; /* 16-char text row, lives in BSS */
360
+ static uint8_t hud_dls[8 * 7]; /* one 5-byte DL + term per row */
361
+
362
+ /* ── HARDWARE IDIOM (load-bearing) — the ROAD BANKS. Two pre-built standing
363
+ * road DLs: bank 0 draws the lane dashes on a line, bank 1 leaves the dash
364
+ * gap. A road zone alternates banks every DASH_RUN lines, and the marching
365
+ * "scroll" shifts which lines are on which bank by `dash_phase`. The asphalt
366
+ * band + shoulder rails + centre divider are identical in both banks (only the
367
+ * dash differs), so the road never appears to change except for the dashes
368
+ * sliding downward. Each bank DL is at most: asphalt(5) + dash(4) + term(1) =
369
+ * 10 bytes ≤ 14. We build it into a tiny per-bank ROM-pointing RAM DL once. */
370
+ #define ROAD_W_BYTES 16 /* 64px asphalt centred on a 160px field */
371
+ #define ROAD_X 48 /* asphalt left edge (px) — 64px road */
372
+ #define DASH_RUN 8 /* dash on for 8 lines, off for 8 */
373
+ /* Every road line shares the SAME asphalt object and the SAME dash object
374
+ * (the dash only differs in WHETHER it appears on a line, chosen by phase —
375
+ * not in its bytes), so one template of each suffices (5-byte asphalt, 4-byte
376
+ * dash). Per-line copies would waste ~2KB of the 2KB RAM1 budget for nothing. */
377
+ static uint8_t road_band[4]; /* the 64px asphalt object (4-byte direct) */
378
+ static uint8_t road_dash[4]; /* the 4px centre dash object (built once)*/
379
+ #define DASH_L_X 78 /* centre-of-road dash column (px) */
380
+
381
+ /* Emit one object: a 4-byte direct DL entry into every road line one of its
382
+ * rows crosses. gfx rows are consecutive (stride = width in bytes). Callers
383
+ * keep y in [0, FIELD_LINES - h] so no clipping is needed — keep that
384
+ * invariant if you change movement code, or add clipping here. */
385
+ static void emit_object(uint8_t y, uint8_t h, const uint8_t* gfx,
386
+ uint8_t stride, uint8_t mode, uint8_t x) {
387
+ uint8_t r, off;
388
+ uint8_t* dl;
389
+ for (r = 0; r < h; ++r) {
390
+ off = line_used[y];
391
+ if (off < LINE_FULL) { /* line full ⇒ drop row (flicker) */
392
+ dl = line_dl[y] + off;
393
+ dl[0] = (uint8_t)((uint16_t)(uintptr_t)gfx & 0xFF);
394
+ dl[1] = mode;
395
+ dl[2] = (uint8_t)((uint16_t)(uintptr_t)gfx >> 8);
396
+ dl[3] = x;
397
+ line_used[y] = off + 4;
398
+ }
399
+ ++y;
400
+ gfx += stride;
401
+ }
402
+ }
403
+
404
+ static void field_close(void) { /* terminate every line after emits */
405
+ uint8_t i;
406
+ for (i = 0; i < FIELD_LINES; ++i)
407
+ line_dl[i][line_used[i] + 1] = 0; /* next entry's MODE byte = 0 */
408
+ }
409
+
410
+ /* ── HARDWARE IDIOM (load-bearing) — DLL construction + zone repointing.
411
+ * Built once at boot; dll_zone appends one 3-byte entry (offset byte =
412
+ * height-1; DLI/holey bits stay 0 — no NMI handler, no holey DMA here). */
413
+ static uint8_t* dllp;
414
+ static void dll_zone(uint8_t height, uint16_t dl) {
415
+ dllp[0] = height - 1;
416
+ dllp[1] = (uint8_t)(dl >> 8);
417
+ dllp[2] = (uint8_t)(dl & 0xFF);
418
+ dllp += 3;
419
+ }
420
+
421
+ /* Repoint ONE road line's DLL entry (title/menu/game-over text overlays
422
+ * borrow road zones; play repoints them back at the pool slots). */
423
+ static void point_field_zone(uint8_t fline, uint16_t dl) {
424
+ uint8_t* e = dll + FIELD_DLL_OFF + (uint16_t)fline * 3;
425
+ e[0] = 0;
426
+ e[1] = (uint8_t)(dl >> 8);
427
+ e[2] = (uint8_t)(dl & 0xFF);
428
+ }
429
+
430
+ /* ── GAME LOGIC (clay) — text rendering into a 32-byte-wide RAM canvas ── */
431
+ static uint8_t glyph_index(char c) {
432
+ if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
433
+ if (c >= 'A' && c <= 'Z') return (uint8_t)(10 + c - 'A');
434
+ if (c == '-') return 36;
435
+ return 37; /* space */
436
+ }
437
+
438
+ static void draw_text(uint8_t* canvas, uint8_t col, const char* s) {
439
+ uint8_t r, b;
440
+ const uint8_t* g;
441
+ uint8_t* dst;
442
+ while (*s && col < 16) {
443
+ g = FONT + ((uint16_t)glyph_index(*s) << 3);
444
+ dst = canvas + ((uint16_t)col << 1);
445
+ for (r = 0; r < 8; ++r) {
446
+ b = g[r];
447
+ dst[0] = NIB2[b >> 4];
448
+ dst[1] = NIB2[b & 0x0F];
449
+ dst += 32;
450
+ }
451
+ ++s;
452
+ ++col;
453
+ }
454
+ }
455
+
456
+ static void digits5(char* d, uint16_t v) {
457
+ uint8_t i;
458
+ for (i = 0; i < 5; ++i) { d[4 - i] = (char)('0' + v % 10); v /= 10; }
459
+ }
460
+
461
+ /* Build the 8 one-line DLs that display an arbitrary RAM canvas at x=16
462
+ * (centered 128px). pal picks the text colour palette. dls = 8*7 bytes. */
463
+ static void canvas_dls(uint8_t* dls, const uint8_t* canvas, uint8_t pal) {
464
+ uint8_t r;
465
+ uint16_t a;
466
+ for (r = 0; r < 8; ++r) {
467
+ a = (uint16_t)(uintptr_t)canvas + ((uint16_t)r << 5);
468
+ dls[0] = (uint8_t)(a & 0xFF);
469
+ dls[1] = 0x40; /* 5-byte form, 160A write mode */
470
+ dls[2] = (uint8_t)(a >> 8);
471
+ dls[3] = (uint8_t)((pal << 5) | 0); /* width 32 bytes encodes as 0 */
472
+ dls[4] = 16;
473
+ dls[5] = 0;
474
+ dls[6] = 0; /* terminator for the next read */
475
+ dls += 7;
476
+ }
73
477
  }
74
478
 
75
- static uint16_t bg_zone_dl(int zone) {
76
- if (zone >= 16 && zone < 220) return (uint16_t)(uintptr_t)dl_road;
77
- return (uint16_t)(uintptr_t)dl_empty;
479
+ /* ── GAME LOGIC (clay) — the music. Two-voice TIA tune loop. ─────────────────
480
+ * The TIA's frequency divider is 5 bits — ~32 pitches TOTAL, none of them
481
+ * in tune with each other. Don't fight it: write the melody IN the TIA's
482
+ * crooked scale and it reads as "gritty 7800", fight it and it reads as
483
+ * "wrong". The note tables ARE the song — edit them to recompose.
484
+ * Voice 0 = melody (AUDC 4, square-ish). Voice 1 = bass (AUDC 6, deep
485
+ * buzz) — and voice 1 is SHARED with sound effects (TIA has only two
486
+ * voices): when the game fires an effect, sfx_hold mutes the bass for the
487
+ * effect's length, then the bass re-enters on its next note. That
488
+ * steal-and-return is the standard 2-voice arbitration trick. */
489
+ static const uint8_t MEL_F[16] = { 12,13,15,13, 12,15,17,255, 13,15,17,15, 13,12,10,255 };
490
+ static const uint8_t MEL_L[16] = { 8, 8, 8, 8, 8, 8,16, 8, 8, 8, 8, 8, 8, 8,16, 8 };
491
+ static const uint8_t BAS_F[8] = { 25,25,29,29, 23,23,27,25 };
492
+ static uint8_t mel_i, mel_t, bas_i, bas_t, sfx_hold;
493
+
494
+ static void music_tick(void) {
495
+ if (mel_t) --mel_t;
496
+ if (mel_t == 0) {
497
+ mel_i = (uint8_t)((mel_i + 1) & 15);
498
+ mel_t = MEL_L[mel_i];
499
+ if (MEL_F[mel_i] == 255) {
500
+ AUDV0 = 0; /* 255 = rest */
501
+ } else {
502
+ AUDC0 = 4; AUDF0 = MEL_F[mel_i]; AUDV0 = 6;
503
+ }
504
+ }
505
+ if (sfx_hold) { /* an effect owns voice 1 */
506
+ --sfx_hold;
507
+ if (sfx_hold == 0) bas_t = 1; /* bass re-enters next tick */
508
+ return;
509
+ }
510
+ if (bas_t) --bas_t;
511
+ if (bas_t == 0) {
512
+ bas_i = (uint8_t)((bas_i + 1) & 7);
513
+ bas_t = 16;
514
+ AUDC1 = 6; AUDF1 = BAS_F[bas_i]; AUDV1 = 5;
515
+ }
78
516
  }
79
517
 
80
- #define DLL_ZONES 243
81
- static uint8_t dll[DLL_ZONES * 3];
518
+ /* Effects (voice 1 via atari7800_sfx; sfx_hold keeps the bass out). */
519
+ static void fx_lane(void) { sfx_tone(1, 18, 3); sfx_hold = 4; }
520
+ static void fx_gas(void) { sfx_tone(1, 10, 4); sfx_hold = 5; }
521
+ static void fx_brake(void) { sfx_tone(1, 24, 3); sfx_hold = 4; }
522
+ static void fx_pass(void) { sfx_tone(1, 14, 2); sfx_hold = 3; }
523
+ static void fx_crash(void) { sfx_noise(22); sfx_hold = 23; }
524
+ static void fx_start(void) { sfx_tone(1, 8, 6); sfx_hold = 7; }
525
+
526
+ /* ── GAME LOGIC (clay — reshape freely) — ROAD GEOMETRY ──────────────────────
527
+ * Four lanes between the shoulders on a 64px-wide road. Lane centres (left
528
+ * pixel of the 12px car). The centre divider sits between lane 1 and lane 2;
529
+ * in 2P that line splits the territories (P1 lanes 0-1, P2 lanes 2-3). */
530
+ #define LANES 4
531
+ static const uint8_t LANE_X[LANES] = { 52, 66, 84, 98 };
532
+ #define CAR_Y 96 /* both players' fixed road-line Y */
533
+ #define CAR_H 10
534
+ #define TRAFFIC_H 8
535
+ #define SPAWN_Y 2 /* traffic enters at the top road line */
536
+ #define DESPAWN_Y 112 /* recycle past the bottom (keeps emit in-bounds) */
537
+
538
+ /* ── GAME LOGIC (clay) — traffic pool (fixed slots, no allocation). MORE
539
+ * traffic than lanes so the MARIA object-quantity signature shows: a thick
540
+ * descending stream. */
541
+ #define TRAFFIC 10
542
+ static uint8_t tr_lane[TRAFFIC], tr_y[TRAFFIC], tr_act[TRAFFIC];
82
543
 
83
- #define PLAYER_Y 170
84
- #define LANES 3
85
- static int lane;
544
+ /* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────────
545
+ * Fixed object pools, no allocation (1.79MHz CPU, 4KB RAM — a heap is a cost
546
+ * with no payer). Players: 0 = P1 (port 0), 1 = P2 (port 1, versus only). */
547
+ #define LIVES_START 3
548
+ static uint8_t car_lane[2], car_act[2], crashes[2], invuln[2];
549
+ static uint8_t lane_min[2], lane_max[2]; /* 2P split territories */
550
+ static uint8_t two_p, winner;
551
+ static uint8_t speed; /* road px/8-per-tick, 1..4 */
552
+ static uint16_t dist, best; /* 1P distance + session best */
553
+ static uint8_t dist_frac;
554
+ static uint8_t dash_phase; /* marching-dash scroll offset */
555
+ static uint8_t dash_acc; /* sub-line dash accumulator */
556
+ static uint8_t spawn_t;
557
+ static uint8_t dirty, over_lock;
558
+ static uint8_t prev_pad0, prev_pad1, pf0, pf1;
559
+ static uint16_t rng = 0xC0DE;
86
560
 
87
- static const uint8_t lane_xs[LANES] = { 40, 80, 120 };
561
+ #define ST_TITLE 0
562
+ #define ST_PLAY 1
563
+ #define ST_OVER 2
564
+ static uint8_t state;
88
565
 
89
- static void set_dl_addr(uint8_t* dl, const uint8_t* row) {
90
- uint16_t a = (uint16_t)(uintptr_t)row;
91
- dl[0] = (uint8_t)(a & 0xFF);
92
- dl[2] = (uint8_t)(a >> 8);
566
+ static uint8_t random8(void) { /* xorshift16 cheap + fine */
567
+ uint16_t r = rng;
568
+ r ^= r << 7;
569
+ r ^= r >> 9;
570
+ r ^= r << 8;
571
+ rng = r;
572
+ return (uint8_t)r;
93
573
  }
94
574
 
95
- static void set_dll_entry(int idx, uint16_t dl_ptr) {
96
- dll[idx * 3 + 0] = 0;
97
- dll[idx * 3 + 1] = (uint8_t)(dl_ptr >> 8);
98
- dll[idx * 3 + 2] = (uint8_t)(dl_ptr & 0xFF);
575
+ /* AABB on the road: both boxes ~12px wide, ~10 tall, in road-line space. */
576
+ static uint8_t hits(uint8_t ax, uint8_t ay, uint8_t bx, uint8_t by) {
577
+ uint8_t dx = (ax > bx) ? (ax - bx) : (bx - ax);
578
+ uint8_t dy = (ay > by) ? (ay - by) : (by - ay);
579
+ return (dx < 11) && (dy < 9);
99
580
  }
100
581
 
101
- static void set_x(uint8_t x) {
102
- dl_row0[4] = x; dl_row1[4] = x; dl_row2[4] = x; dl_row3[4] = x;
103
- dl_row4[4] = x; dl_row5[4] = x; dl_row6[4] = x; dl_row7[4] = x;
582
+ /* ── HARDWARE IDIOM (load-bearing) — build the STANDING road. For each road
583
+ * line, road_band[] holds the asphalt band object (grey, 64px) + a centre
584
+ * divider object; road_dash[] holds a short white dash object placed on the
585
+ * lines where the marching pattern is "on". emit_road() points each line's
586
+ * pool slot at its standing bytes; the dash on/off is chosen by the line's
587
+ * phase. Called every frame (it's cheap: ~120 short memcpys) so the dash
588
+ * march is just a changing phase — no DLL teardown. */
589
+ static void build_road_drawables(void) {
590
+ uint16_t sa = (uint16_t)(uintptr_t)SOLID8;
591
+ /* asphalt band: one 16-byte (64px) grey object @ ROAD_X (palette 5), 4-byte
592
+ * DIRECT form [lo, mode, hi, x] — width 16 ⇒ mode low5 = 32-16 = 16 (≠0). */
593
+ road_band[0] = (uint8_t)(sa & 0xFF);
594
+ road_band[1] = (uint8_t)((5u << 5) | (32 - ROAD_W_BYTES));
595
+ road_band[2] = (uint8_t)(sa >> 8);
596
+ road_band[3] = ROAD_X;
597
+ /* dash object: a 4px white tick @ the centre lane line (palette 6, 4-byte) */
598
+ road_dash[0] = (uint8_t)(sa & 0xFF);
599
+ road_dash[1] = (uint8_t)((6u << 5) | (32 - 1)); /* 1 byte = 4px */
600
+ road_dash[2] = (uint8_t)(sa >> 8);
601
+ road_dash[3] = DASH_L_X;
104
602
  }
105
603
 
106
- static void build_dll(void) {
107
- int i;
108
- for (i = 0; i < DLL_ZONES; i++) {
109
- uint16_t dl;
110
- int d = i - PLAYER_Y;
111
- switch (d) {
112
- case 0: dl = (uint16_t)(uintptr_t)dl_row0; break;
113
- case 1: dl = (uint16_t)(uintptr_t)dl_row1; break;
114
- case 2: dl = (uint16_t)(uintptr_t)dl_row2; break;
115
- case 3: dl = (uint16_t)(uintptr_t)dl_row3; break;
116
- case 4: dl = (uint16_t)(uintptr_t)dl_row4; break;
117
- case 5: dl = (uint16_t)(uintptr_t)dl_row5; break;
118
- case 6: dl = (uint16_t)(uintptr_t)dl_row6; break;
119
- case 7: dl = (uint16_t)(uintptr_t)dl_row7; break;
120
- default: dl = bg_zone_dl(i); break;
604
+ /* Compose every road line's pool slot: asphalt band, then (on dash-on lines)
605
+ * the marching dash, then the terminator. dash_phase slides the pattern. */
606
+ static void compose_road(void) {
607
+ uint8_t i, off, phase, on;
608
+ for (i = 0; i < FIELD_LINES; ++i) {
609
+ uint8_t* dl = line_dl[i];
610
+ /* the 64px asphalt as a 4-byte direct standing object */
611
+ dl[0] = road_band[0];
612
+ dl[1] = road_band[1];
613
+ dl[2] = road_band[2];
614
+ dl[3] = road_band[3];
615
+ off = 4;
616
+ /* marching dash: on for DASH_RUN lines, off for DASH_RUN, sliding by
617
+ * dash_phase so the rhythm scrolls DOWNWARD (the fake road motion). */
618
+ phase = (uint8_t)((i + dash_phase) & ((DASH_RUN << 1) - 1));
619
+ on = (phase < DASH_RUN) ? 1 : 0;
620
+ if (on) {
621
+ dl[off + 0] = road_dash[0];
622
+ dl[off + 1] = road_dash[1];
623
+ dl[off + 2] = road_dash[2];
624
+ dl[off + 3] = road_dash[3];
625
+ off += 4;
121
626
  }
122
- set_dll_entry(i, dl);
627
+ line_used[i] = off; /* cars emit AFTER the road bytes */
123
628
  }
124
629
  }
125
630
 
631
+ /* ── GAME LOGIC (clay) — HUD: "DIST 00000 BEST 0" / "P1 0 - P2 0" composed ── */
632
+ static void draw_hud(void) {
633
+ if (two_p) {
634
+ static char vbuf[17] = "P1 3 VS P2 3";
635
+ vbuf[3] = (char)('0' + crashes[0]);
636
+ vbuf[15] = (char)('0' + crashes[1]);
637
+ memset(hud_canvas, 0, sizeof(hud_canvas));
638
+ draw_text(hud_canvas, 0, vbuf);
639
+ } else {
640
+ static char buf[17] = "D00000 B00000 C0";
641
+ digits5(buf + 1, dist);
642
+ digits5(buf + 8, best);
643
+ buf[15] = (char)('0' + crashes[0]);
644
+ memset(hud_canvas, 0, sizeof(hud_canvas));
645
+ draw_text(hud_canvas, 0, buf);
646
+ }
647
+ dirty = 0;
648
+ }
649
+
650
+ static void draw_hud_title(void) {
651
+ static char buf[9] = "BEST00000";
652
+ digits5(buf + 4, best);
653
+ memset(hud_canvas, 0, sizeof(hud_canvas));
654
+ draw_text(hud_canvas, 3, buf);
655
+ }
656
+
657
+ /* ── HARDWARE IDIOM (load-bearing) — paint functions bracket structural
658
+ * display-list changes with MARIA DMA OFF ($7F) / ON ($40), the 7800's
659
+ * version of the NES "rendering off before nametable writes" rule: MARIA
660
+ * may be mid-walk through the very lists being rewritten, and repointing
661
+ * dozens of zones under it glitches (or with bad luck hangs) the frame.
662
+ * CTRL $40 = DMA on, 160A read mode, colour burst on — forget to restore
663
+ * it and the screen stays the flat BACKGRND colour forever. ── */
664
+
665
+ /* Title screen: borrow road zones for three text overlays composed in POOLB
666
+ * (the pool isn't drawing the road on the title, so its RAM is free — 4KB
667
+ * machines make you reuse like this). Title is double-height by pointing TWO
668
+ * consecutive 1-line zones at each canvas row — zero extra RAM, pure DLL
669
+ * trickery. */
670
+ static void paint_title(void) {
671
+ uint8_t i;
672
+ uint8_t* c0 = POOLB; /* title canvas (256 bytes) */
673
+ uint8_t* c1 = POOLB + 256; /* menu line 1 (256 bytes) */
674
+ uint8_t* c2 = POOLB + 512; /* menu line 2 (256 bytes) */
675
+ uint8_t* td = POOLB + 768; /* 3 lines * 8 row-DLs * 7 */
676
+ CTRL = 0x7F; /* DMA off */
677
+ memset(POOLB, 0, 768);
678
+ draw_text(c0, (uint8_t)((16 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
679
+ draw_text(c1, 1, "1P - FIRE RACE");
680
+ draw_text(c2, 1, "2P PAD2 VERSUS");
681
+ canvas_dls(td, c0, 0); /* white */
682
+ canvas_dls(td + 56, c1, 5); /* HUD green */
683
+ canvas_dls(td + 112, c2, 5);
684
+ for (i = 0; i < FIELD_LINES; ++i)
685
+ point_field_zone(i, (uint16_t)(uintptr_t)dl_empty);
686
+ for (i = 0; i < 16; ++i) /* double-height title rows */
687
+ point_field_zone((uint8_t)(8 + i),
688
+ (uint16_t)(uintptr_t)(td + ((i >> 1) * 7)));
689
+ for (i = 0; i < 8; ++i) {
690
+ point_field_zone((uint8_t)(56 + i), (uint16_t)(uintptr_t)(td + 56 + i * 7));
691
+ point_field_zone((uint8_t)(76 + i), (uint16_t)(uintptr_t)(td + 112 + i * 7));
692
+ }
693
+ draw_hud_title();
694
+ state = ST_TITLE;
695
+ CTRL = 0x40; /* DMA back on */
696
+ }
697
+
698
+ /* Game over: the pool RAM becomes the message overlay (same reuse trick as
699
+ * the title), the rest of the road goes blank. */
700
+ static void paint_gameover(void) {
701
+ uint8_t i;
702
+ uint8_t* c0 = POOLB;
703
+ uint8_t* c1 = POOLB + 256;
704
+ uint8_t* td = POOLB + 768;
705
+ static char buf[12] = "DIST 00000";
706
+ CTRL = 0x7F;
707
+ memset(POOLB, 0, 768);
708
+ if (two_p) draw_text(c0, 4, winner ? "P2 WINS" : "P1 WINS");
709
+ else draw_text(c0, 3, "WRECKED");
710
+ if (two_p) {
711
+ draw_text(c1, 3, "RIVAL OUT");
712
+ } else {
713
+ digits5(buf + 5, dist);
714
+ draw_text(c1, 3, buf);
715
+ }
716
+ canvas_dls(td, c0, 0);
717
+ canvas_dls(td + 56, c1, 5);
718
+ for (i = 0; i < FIELD_LINES; ++i)
719
+ point_field_zone(i, (uint16_t)(uintptr_t)dl_empty);
720
+ for (i = 0; i < 8; ++i) {
721
+ point_field_zone((uint8_t)(40 + i), (uint16_t)(uintptr_t)(td + i * 7));
722
+ point_field_zone((uint8_t)(60 + i), (uint16_t)(uintptr_t)(td + 56 + i * 7));
723
+ }
724
+ over_lock = 30; /* swallow the held fire button */
725
+ state = ST_OVER;
726
+ CTRL = 0x40;
727
+ }
728
+
729
+ /* ── GAME LOGIC (clay) — spawn one traffic car in a free slot ── */
730
+ static void spawn_traffic(void) {
731
+ uint8_t i;
732
+ for (i = 0; i < TRAFFIC; ++i) {
733
+ if (!tr_act[i]) {
734
+ tr_act[i] = 1;
735
+ tr_lane[i] = (uint8_t)(random8() & 3);
736
+ tr_y[i] = SPAWN_Y;
737
+ return;
738
+ }
739
+ }
740
+ }
741
+
742
+ /* ── GAME LOGIC (clay) — start a run ── */
743
+ static void start_game(uint8_t players) {
744
+ uint8_t i;
745
+ CTRL = 0x7F;
746
+ two_p = players;
747
+ for (i = 0; i < FIELD_LINES; ++i) /* road zones → pool slots */
748
+ point_field_zone(i, (uint16_t)(uintptr_t)line_dl[i]);
749
+ for (i = 0; i < TRAFFIC; ++i) tr_act[i] = 0;
750
+ for (i = 0; i < 2; ++i) { crashes[i] = LIVES_START; invuln[i] = 0; }
751
+ if (players) {
752
+ car_act[0] = car_act[1] = 1;
753
+ lane_min[0] = 0; lane_max[0] = 1; car_lane[0] = 0; /* P1: left half */
754
+ lane_min[1] = 2; lane_max[1] = 3; car_lane[1] = 3; /* P2: right half */
755
+ speed = 2; /* shared road, fixed (one DLL) */
756
+ } else {
757
+ car_act[0] = 1; car_act[1] = 0;
758
+ lane_min[0] = 0; lane_max[0] = 3; car_lane[0] = 1; /* whole road */
759
+ speed = 1;
760
+ }
761
+ dist = 0; dist_frac = 0; dash_phase = 0; spawn_t = 0; winner = 0;
762
+ rng ^= (uint16_t)(best * 251) ^ 0x1234;
763
+ compose_road();
764
+ field_close();
765
+ draw_hud();
766
+ fx_start();
767
+ state = ST_PLAY;
768
+ CTRL = 0x40;
769
+ }
770
+
771
+ static void game_over(void) {
772
+ if (!two_p && dist > best) {
773
+ best = dist;
774
+ /* HSC NOTE (see file header): on real hardware with a High Score Cart you
775
+ * would write the record into HSC RAM ($1000-$17FF) here. The bundled
776
+ * prosystem core has no HSC support and exposes no SAVE_RAM, so the record
777
+ * honestly lives only as long as the session. */
778
+ }
779
+ paint_gameover();
780
+ }
781
+
782
+ static void crash(uint8_t p) {
783
+ fx_crash();
784
+ invuln[p] = 60; /* blink + no-collide grace */
785
+ if (!two_p) { speed = 1; } /* a wreck kills your momentum */
786
+ if (crashes[p] > 0) --crashes[p];
787
+ dirty = 1;
788
+ if (crashes[p] == 0) {
789
+ winner = (uint8_t)(p ^ 1); /* versus: the OTHER player wins */
790
+ game_over();
791
+ }
792
+ }
793
+
794
+ /* ── GAME LOGIC (clay) — per-player input. LEFT/RIGHT steer between lanes
795
+ * (edge-detected — held d-pad shouldn't machine-gun across the road). 1P
796
+ * only: UP/A accelerate, DOWN/B brake (speed 1-4). ── */
797
+ static void update_player(uint8_t p, uint8_t fire, uint8_t pressed) {
798
+ uint8_t lf, rt, up, dn;
799
+ if (!car_act[p]) return;
800
+ if (p == 0) { rt = pressed & J1_RIGHT; lf = pressed & J1_LEFT; up = pressed & J1_UP; dn = pressed & J1_DOWN; }
801
+ else { rt = pressed & J2_RIGHT; lf = pressed & J2_LEFT; up = pressed & J2_UP; dn = pressed & J2_DOWN; }
802
+ if (lf && car_lane[p] > lane_min[p]) { --car_lane[p]; fx_lane(); }
803
+ if (rt && car_lane[p] < lane_max[p]) { ++car_lane[p]; fx_lane(); }
804
+ if (!two_p) { /* speed is shared — 1P only */
805
+ if ((up || fire) && speed < 4) { ++speed; fx_gas(); }
806
+ if (dn && speed > 1) { --speed; fx_brake(); }
807
+ }
808
+ if (invuln[p]) --invuln[p];
809
+ }
810
+
126
811
  static void vblank_wait(void) {
127
- while (MSTAT & 0x80) { }
128
- while (!(MSTAT & 0x80)) { }
812
+ while (MSTAT & 0x80) { } /* leave the current vblank */
813
+ while (!(MSTAT & 0x80)) { } /* catch the next one starting */
129
814
  }
130
815
 
131
816
  void main(void) {
132
- uint16_t dll_addr;
133
- uint8_t prev_pad = 0;
134
-
135
- set_dl_addr(dl_row0, car_row0);
136
- set_dl_addr(dl_row1, car_row1);
137
- set_dl_addr(dl_row2, car_row2);
138
- set_dl_addr(dl_row3, car_row3);
139
- set_dl_addr(dl_row4, car_row4);
140
- set_dl_addr(dl_row5, car_row5);
141
- set_dl_addr(dl_row6, car_row6);
142
- set_dl_addr(dl_row7, car_row7);
143
-
144
- lane = 1;
145
- set_road_addr();
146
- set_x(lane_xs[lane]);
147
- build_dll();
148
-
149
- BACKGRND = 0x88; /* sky/horizon */
150
- P0C1 = 0x46; /* car */
151
- P0C2 = 0x0F;
152
- P0C3 = 0x36;
153
- P1C1 = 0xC8; /* roadside grass (green) */
154
- P2C1 = 0x06; /* road surface (grey) */
817
+ uint8_t i;
818
+ uint16_t a;
819
+
820
+ /* ── HARDWARE IDIOM (load-bearing) — boot order: build EVERYTHING the DLL
821
+ * will reference, then point DPP at it, THEN enable DMA. Enabling DMA over
822
+ * a half-built DLL is the 7800 black-screen classic. ── */
823
+
824
+ /* Resolve the pool split: road line → 14-byte DL slot. */
825
+ for (i = 0; i < POOLA_LINES; ++i)
826
+ line_dl[i] = pool_a + (uint16_t)i * LINE_BYTES;
827
+ for (i = POOLA_LINES; i < FIELD_LINES; ++i)
828
+ line_dl[i] = POOLB + (uint16_t)(i - POOLA_LINES) * LINE_BYTES;
829
+
830
+ /* Patch the ROM band drawables' data pointers (SOLID8). */
831
+ a = (uint16_t)(uintptr_t)SOLID8;
832
+ dl_band_a[0] = dl_band_a[5] = (uint8_t)(a & 0xFF);
833
+ dl_band_a[2] = dl_band_a[7] = (uint8_t)(a >> 8);
834
+ dl_band_b[0] = dl_band_b[5] = (uint8_t)(a & 0xFF);
835
+ dl_band_b[2] = dl_band_b[7] = (uint8_t)(a >> 8);
836
+
837
+ build_road_drawables();
838
+ canvas_dls(hud_dls, hud_canvas, 5);
839
+
840
+ /* The DLL — the screen layout, built once (see the layout table above).
841
+ * 143 entries, mixed zone heights; only the 120 road entries are ever
842
+ * repointed after this. */
843
+ dllp = dll;
844
+ dll_zone(16, (uint16_t)(uintptr_t)dl_empty); /* lines 0-15 */
845
+ for (i = 0; i < 8; ++i) /* HUD 16-23 */
846
+ dll_zone(1, (uint16_t)(uintptr_t)(hud_dls + i * 7));
847
+ dll_zone(2, (uint16_t)(uintptr_t)dl_band_a); /* divider */
848
+ for (i = 0; i < FIELD_LINES; ++i) /* road 26-145 */
849
+ dll_zone(1, (uint16_t)(uintptr_t)line_dl[i]);
850
+ dll_zone(2, (uint16_t)(uintptr_t)dl_band_a); /* guard band */
851
+ /* Horizon decor stripes — also our anti-blank-screen ballast: with DMA
852
+ * fetching only objects, everything else is the single flat BACKGRND
853
+ * colour, and a mostly-one-colour frame reads as "dead". */
854
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
855
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
856
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_b);
857
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
858
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
859
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
860
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_b);
861
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
862
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
863
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
864
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_b); /* …through 235 */
865
+ dll_zone(7, (uint16_t)(uintptr_t)dl_empty); /* 236-242 */
866
+
867
+ /* Palettes (Atari colour byte = hue<<4 | luminance). */
868
+ BACKGRND = 0xC4; /* roadside grass green */
869
+ P0C1 = 0x0F; /* title text white */
870
+ P1C1 = 0x94; P1C2 = 0x0F; P1C3 = 0x9C; /* P1 car blues */
871
+ P2C1 = 0x46; P2C2 = 0x0F; P2C3 = 0x4C; /* P2 car golds */
872
+ P3C1 = 0x36; P3C2 = 0x0F; P3C3 = 0x3C; /* traffic reds */
873
+ P4C1 = 0x0F; /* (spare) */
874
+ P5C1 = 0x06; /* road asphalt grey */
875
+ P6C1 = 0x0E; /* lane dash / HUD white-ish */
876
+ P7C1 = 0x0A; /* horizon decor band */
155
877
  CHARBASE = 0;
156
- OFFSET = 0;
878
+ OFFSET = 0; /* must stay 0 (7800 standard) */
879
+
880
+ a = (uint16_t)(uintptr_t)dll;
881
+ DPPL = (uint8_t)(a & 0xFF);
882
+ DPPH = (uint8_t)(a >> 8);
157
883
 
158
- dll_addr = (uint16_t)(uintptr_t)dll;
159
- DPPL = (uint8_t)(dll_addr & 0xFF);
160
- DPPH = (uint8_t)(dll_addr >> 8);
161
- CTRL = 0x40;
162
884
  sfx_init();
885
+ best = 0; /* in-session only — see header */
886
+ paint_title(); /* …turns DMA on */
163
887
 
164
888
  for (;;) {
165
- uint8_t pad;
889
+ uint8_t pad, f1, f2, pr0, pr1;
166
890
  vblank_wait();
167
891
  sfx_update();
892
+ music_tick();
893
+
894
+ pad = (uint8_t)~SWCHA;
895
+ f1 = (uint8_t)(!(INPT4 & 0x80));
896
+ f2 = (uint8_t)(!(INPT5 & 0x80));
897
+
898
+ if (state == ST_TITLE) {
899
+ /* ── GAME LOGIC (clay) — title: P1 fire = 1P race, P2 fire = 2P ── */
900
+ if (f1 && !pf0) start_game(0);
901
+ else if (f2 && !pf1) start_game(1);
902
+ pf0 = f1; pf1 = f2;
903
+ continue;
904
+ }
905
+
906
+ if (state == ST_OVER) {
907
+ if (over_lock) { --over_lock; pf0 = f1; pf1 = f2; continue; }
908
+ if ((f1 || f2) && !pf0 && !pf1) paint_title();
909
+ pf0 = f1; pf1 = f2;
910
+ continue;
911
+ }
912
+
913
+ /* ── ST_PLAY ───────────────────────────────────────────────────── */
914
+ pr0 = (uint8_t)(pad & ~prev_pad0); /* port-0 newly-pressed edges */
915
+ pr1 = (uint8_t)(pad & ~prev_pad1); /* port-1 newly-pressed edges */
916
+ prev_pad0 = pad; prev_pad1 = pad;
917
+ update_player(0, f1, pr0);
918
+ if (two_p) update_player(1, f2, pr1);
919
+ if (state != ST_PLAY) { pf0 = f1; pf1 = f2; continue; } /* a crash ended it */
920
+
921
+ /* ── HARDWARE IDIOM (load-bearing) — the marching-dash "scroll": advance
922
+ * the phase by the road speed, then re-compose the road into the pool
923
+ * slots. compose_road() points each road zone at the dash bank matching
924
+ * its (line + dash_phase) — the dashes slide downward with no per-pixel
925
+ * work. This IS the fake road motion (MARIA has no scroll register).
926
+ * dash_acc accumulates speed so the march speeds up with the throttle but
927
+ * never skips so far it strobes; the actual compose happens in the draw
928
+ * pass below (compose_road), which reads dash_phase. ── */
929
+ dash_acc = (uint8_t)(dash_acc + speed);
930
+ while (dash_acc >= 2) { dash_acc -= 2; dash_phase = (uint8_t)(dash_phase + 1); }
931
+ if (dash_phase >= (DASH_RUN << 1)) dash_phase -= (DASH_RUN << 1);
932
+
933
+ /* ── GAME LOGIC (clay) — traffic flows DOWN at road speed (reads as cars
934
+ * you overtake); recycle past the bottom with a little pass tick. ── */
935
+ for (i = 0; i < TRAFFIC; ++i) {
936
+ if (!tr_act[i]) continue;
937
+ if (tr_y[i] >= (uint8_t)(DESPAWN_Y - speed)) {
938
+ tr_act[i] = 0;
939
+ fx_pass();
940
+ } else {
941
+ tr_y[i] = (uint8_t)(tr_y[i] + speed);
942
+ }
943
+ }
944
+ if (++spawn_t >= (uint8_t)(40 - (speed << 2))) { /* faster ⇒ denser */
945
+ spawn_t = 0;
946
+ spawn_traffic();
947
+ }
948
+
949
+ /* Distance (1P stat): 1 unit per 16 "scrolled" px. */
950
+ if (!two_p) {
951
+ dist_frac = (uint8_t)(dist_frac + speed);
952
+ if (dist_frac >= 16) {
953
+ dist_frac -= 16;
954
+ if (dist < 65535u) ++dist;
955
+ dirty = 1;
956
+ }
957
+ }
958
+
959
+ /* ── GAME LOGIC (clay) — traffic × cars. Crash grace: a just-wrecked car
960
+ * blinks and can't collide for 60 frames. ── */
961
+ for (i = 0; i < TRAFFIC; ++i) {
962
+ uint8_t p;
963
+ if (!tr_act[i]) continue;
964
+ for (p = 0; p < 2; ++p) {
965
+ if (!car_act[p] || invuln[p]) continue;
966
+ if (hits(LANE_X[tr_lane[i]], tr_y[i], LANE_X[car_lane[p]], CAR_Y)) {
967
+ tr_act[i] = 0;
968
+ crash(p);
969
+ if (state != ST_PLAY) break;
970
+ }
971
+ }
972
+ if (state != ST_PLAY) break;
973
+ }
974
+ if (state != ST_PLAY) { pf0 = f1; pf1 = f2; continue; }
975
+
976
+ /* ── HARDWARE IDIOM (load-bearing) — the per-frame draw pass:
977
+ * compose the road (sets line_used past the standing road bytes) → emit
978
+ * every car INTO the remaining room of each line's slot → terminate.
979
+ * Cars go last so the road is always present even if a line fills; a
980
+ * dropped car-row is a one-line flicker, never a missing road. ── */
981
+ compose_road();
982
+ /* traffic first (so the player's own car wins the 3-object budget on a
983
+ * shared line — the player car should never be the one that flickers). */
984
+ for (i = 0; i < TRAFFIC; ++i)
985
+ if (tr_act[i]) emit_object(tr_y[i], TRAFFIC_H, GFX_TRAFFIC, 3,
986
+ MODE_TRAFFIC, LANE_X[tr_lane[i]]);
987
+ for (i = 0; i < 2; ++i) {
988
+ if (!car_act[i]) continue;
989
+ /* crash blink = SHIMMER, never vanish: on blink ticks draw only the
990
+ * car's bottom half instead of skipping it (a fully-skipped sprite
991
+ * reads as "gone" in any single sampled frame — the spawn-blink
992
+ * footgun from the gold round). */
993
+ if (invuln[i] && (invuln[i] & 4))
994
+ emit_object((uint8_t)(CAR_Y + 5), 5, GFX_CAR + 15, 3,
995
+ i ? MODE_CAR2 : MODE_CAR1, LANE_X[car_lane[i]]);
996
+ else
997
+ emit_object(CAR_Y, CAR_H, GFX_CAR, 3,
998
+ i ? MODE_CAR2 : MODE_CAR1, LANE_X[car_lane[i]]);
999
+ }
1000
+ field_close();
168
1001
 
169
- pad = ~SWCHA;
170
- if ((pad & JOY_LEFT) && !(prev_pad & JOY_LEFT) && lane > 0) { lane--; set_x(lane_xs[lane]); sfx_tone(0, 6, 4); }
171
- if ((pad & JOY_RIGHT) && !(prev_pad & JOY_RIGHT) && lane < LANES - 1) { lane++; set_x(lane_xs[lane]); sfx_tone(0, 6, 4); }
172
- prev_pad = pad;
1002
+ if (dirty) draw_hud();
1003
+ pf0 = f1; pf1 = f2;
173
1004
  }
174
1005
  }