romdevtools 0.28.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 (154) hide show
  1. package/AGENTS.md +51 -41
  2. package/CHANGELOG.md +46 -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 +1 -1
  88. package/src/host/LibretroHost.js +59 -1
  89. package/src/http/tool-registry.js +11 -11
  90. package/src/mcp/tools/cheats.js +2 -1
  91. package/src/mcp/tools/frame.js +3 -2
  92. package/src/mcp/tools/index.js +3 -3
  93. package/src/mcp/tools/input.js +5 -4
  94. package/src/mcp/tools/lifecycle.js +6 -4
  95. package/src/mcp/tools/platform-docs.js +1 -1
  96. package/src/mcp/tools/preview-tile.js +6 -2
  97. package/src/mcp/tools/project.js +1098 -130
  98. package/src/mcp/tools/rom-id.js +5 -1
  99. package/src/mcp/tools/run-until.js +4 -2
  100. package/src/mcp/tools/snippets.js +6 -6
  101. package/src/mcp/tools/sprite-pipeline.js +14 -2
  102. package/src/mcp/tools/state.js +2 -1
  103. package/src/mcp/tools/tile-inspect.js +8 -1
  104. package/src/mcp/tools/toolchain.js +12 -1
  105. package/src/mcp/tools/watch-memory.js +4 -3
  106. package/src/observer/bus.js +73 -0
  107. package/src/observer/livestream.html +4 -2
  108. package/src/observer/tool-wrap.js +17 -14
  109. package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
  110. package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
  111. package/src/platforms/c64/MENTAL_MODEL.md +11 -4
  112. package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
  113. package/src/platforms/gb/MENTAL_MODEL.md +3 -3
  114. package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
  115. package/src/platforms/gb/lib/c/README.md +10 -11
  116. package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
  117. package/src/platforms/gb/lib/c/patch-header.js +13 -3
  118. package/src/platforms/gba/MENTAL_MODEL.md +4 -4
  119. package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
  120. package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
  121. package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
  122. package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
  123. package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
  124. package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
  125. package/src/platforms/gbc/lib/c/README.md +10 -11
  126. package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
  127. package/src/platforms/gbc/lib/c/patch-header.js +13 -3
  128. package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
  129. package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
  130. package/src/platforms/gg/MENTAL_MODEL.md +4 -4
  131. package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
  132. package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
  133. package/src/platforms/gg/lib/c/joypad_read.c +29 -0
  134. package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
  135. package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
  136. package/src/platforms/msx/MENTAL_MODEL.md +5 -5
  137. package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
  138. package/src/platforms/msx/lib/c/msx_hw.h +1 -0
  139. package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
  140. package/src/platforms/nes/MENTAL_MODEL.md +2 -2
  141. package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
  142. package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
  143. package/src/platforms/pce/MENTAL_MODEL.md +5 -5
  144. package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
  145. package/src/platforms/pce/lib/c/pce_hw.h +11 -0
  146. package/src/platforms/pce/lib/c/pce_video.c +32 -0
  147. package/src/platforms/sms/MENTAL_MODEL.md +6 -6
  148. package/src/platforms/snes/MENTAL_MODEL.md +2 -2
  149. package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
  150. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
  151. package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
  152. package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
  153. package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
  154. package/src/toolchains/index.js +27 -11
@@ -1,202 +1,1037 @@
1
- /* platformer.c — Atari 7800 single-screen platformer scaffold.
1
+ /* ── platformer.c — Atari 7800 single-screen platformer (complete example) ───
2
2
  *
3
- * Subpixel gravity + jump physics + grounded check. Player is one
4
- * 16x8 sprite; ground is a fixed Y coordinate. Same MARIA pattern
5
- * as hello_sprite.c (per-scanline zones; see MENTAL_MODEL.md).
3
+ * STRATA STRIDE a COMPLETE, working game: title screen, 1P mode and 2P
4
+ * ALTERNATING-TURNS mode (arcade-classic: players swap on death; each player
5
+ * has their own score and own lives, P2 plays on JOYSTICK PORT 2), gravity +
6
+ * sub-pixel jump physics over a multi-tier arena of one-way ledges, coins to
7
+ * collect, spikes + a lethal floor-pit to avoid, in-session hi-score, music +
8
+ * SFX, and the 7800's signature feature: MARIA OBJECT QUANTITY. The hero +
9
+ * up to 6 coins + 5 spikes + every ledge band are all just display-list
10
+ * entries MARIA DMAs per scanline — a populated single screen of independent
11
+ * objects no 2600 (5 hardware objects) draws comfortably. On the 7800 there
12
+ * is no tilemap and no hardware scroll; "the level" IS a stack of display
13
+ * lists, and quantity is the whole point of the chip.
6
14
  *
7
- * Static platforms aren't included extend by rebuilding the DLL
8
- * with sprite-row DLs at each platform's Y range.
15
+ * THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
16
+ * very different one. The markers tell you what's what:
17
+ * HARDWARE IDIOM (load-bearing) — dodges a documented 7800/MARIA footgun;
18
+ * reshape your gameplay around it (see TROUBLESHOOTING before changing).
19
+ * GAME LOGIC (clay) — ledge layout, physics tuning, scoring, art: reshape
20
+ * freely.
21
+ *
22
+ * What depends on what:
23
+ * atari7800_sfx.{h,c} — TIA one-shot effects (we give it voice 1; the
24
+ * inline music player below owns voice 0 — TIA only HAS two voices).
25
+ * cc65's atari7800 target crt0 + atari7800.cfg — boot, BSS in RAM1
26
+ * ($1800-$203F), C parameter stack at the TOP of RAM3 growing DOWN
27
+ * ($2800 →). This game claims the BOTTOM of RAM3 ($2200-$25FD) for its
28
+ * display-list pool — see the RAM MAP below before moving anything.
29
+ *
30
+ * NO HARDWARE SCROLL — honest note: MARIA has no scroll register; a scrolling
31
+ * platformer rebuilds/repoints the display lists every frame (expensive on a
32
+ * 1.79MHz 6502). This example is a FIXED single-screen arena (the form most
33
+ * 7800 platformers actually shipped in). To scroll, you would re-emit the
34
+ * ledge bands at shifted Y/X each frame under the same pool — see the
35
+ * "scrolling a 7800 field" note in TROUBLESHOOTING.
36
+ *
37
+ * PERSISTENCE — honest note: the canonical 7800 save path is the High Score
38
+ * Cart (HSC): a pass-through cartridge with 2KB battery RAM at $1000-$17FF
39
+ * plus a directory ROM. The bundled prosystem core does NOT implement HSC
40
+ * (probed 2026-06: retro_get_memory(SAVE_RAM) size = 0, and the core binary
41
+ * has no HSC code at all), so this game keeps the hi-score IN-SESSION ONLY
42
+ * (it survives play → title → play, dies on power-off). Do not fake
43
+ * persistence the hardware path can't back — if a future core round adds
44
+ * HSC, wire hiscore into $1000-$17FF and it becomes real.
45
+ *
46
+ * Frame budget (NTSC): the per-tick update (player physics + a handful of
47
+ * AABB checks + HUD redraw) fits in one 60Hz frame, dipping to two on heavy
48
+ * frames — vblank_wait() paces the sim, the classic 8-bit pattern. MARIA does
49
+ * not care — it re-walks the same DLs every frame, so a slow CPU loop never
50
+ * blanks or tears the whole screen. That budget only holds because of the
51
+ * #pragma optimize(on) right below — read its comment before deleting it.
9
52
  */
53
+
10
54
  #include <stdint.h>
55
+ #include <string.h>
11
56
  #include "atari7800_sfx.h"
12
57
 
58
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
59
+ * cc65 SHIPS WITH ITS OPTIMIZER OFF, and this toolchain does not pass -O —
60
+ * each translation unit must opt in. Without this pragma the unoptimized
61
+ * emit pass made the main loop take ~9 frames per sim tick instead of 1-2
62
+ * (measured on the 7800 shmup: 8.8 → 1.7 frames/tick on prosystem), and
63
+ * every TICK-DENOMINATED timer silently stretched 4-5x in wall-clock terms:
64
+ * the turn-change blink, the jump arc, the spike timers — all ~4.5x too slow.
65
+ * That presents as "broken game rules / sprite vanishing" (a synchronized
66
+ * blink keeps an object off screen for ~600ms at a time) — but the DLL, the
67
+ * zone pointers, and every pool slot were byte-perfect when read back from
68
+ * RAM. The footgun generalizes: on a 1.79MHz 6502 the C optimizer is not a
69
+ * nicety, it IS the frame budget, and a too-slow loop shows up as broken GAME
70
+ * RULES (stretched timers, missed 1-frame input edges), not as a slow-looking
71
+ * screen — MARIA keeps repainting the same display lists at a rock-steady
72
+ * 60Hz no matter how far behind the CPU falls. If your fork feels like
73
+ * molasses or "ignores" short button taps, check this pragma is still here
74
+ * before debugging the display lists. */
75
+ #pragma optimize(on)
76
+
77
+ /* The title screen renders this — examples({op:'fork'}) stamps your game's
78
+ * name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
79
+ #define GAME_TITLE "STRATA STRIDE"
80
+
81
+ /* ── MARIA + TIA + RIOT registers (full list in MENTAL_MODEL.md) ── */
13
82
  #define BACKGRND (*(volatile uint8_t*)0x20)
14
83
  #define P0C1 (*(volatile uint8_t*)0x21)
15
84
  #define P0C2 (*(volatile uint8_t*)0x22)
16
85
  #define P0C3 (*(volatile uint8_t*)0x23)
17
86
  #define P1C1 (*(volatile uint8_t*)0x25)
18
- #define P2C1 (*(volatile uint8_t*)0x29)
87
+ #define P1C2 (*(volatile uint8_t*)0x26)
88
+ #define P1C3 (*(volatile uint8_t*)0x27)
19
89
  #define MSTAT (*(volatile uint8_t*)0x28)
90
+ #define P2C1 (*(volatile uint8_t*)0x29)
91
+ #define P2C2 (*(volatile uint8_t*)0x2A)
92
+ #define P2C3 (*(volatile uint8_t*)0x2B)
20
93
  #define DPPH (*(volatile uint8_t*)0x2C)
94
+ #define P3C1 (*(volatile uint8_t*)0x2D)
95
+ #define P3C2 (*(volatile uint8_t*)0x2E)
96
+ #define P3C3 (*(volatile uint8_t*)0x2F)
21
97
  #define DPPL (*(volatile uint8_t*)0x30)
98
+ #define P4C1 (*(volatile uint8_t*)0x31)
99
+ #define P4C2 (*(volatile uint8_t*)0x32)
100
+ #define P4C3 (*(volatile uint8_t*)0x33)
22
101
  #define CHARBASE (*(volatile uint8_t*)0x34)
102
+ #define P5C1 (*(volatile uint8_t*)0x35)
23
103
  #define OFFSET (*(volatile uint8_t*)0x38)
104
+ #define P6C1 (*(volatile uint8_t*)0x39)
24
105
  #define CTRL (*(volatile uint8_t*)0x3C)
25
- #define SWCHA (*(volatile uint8_t*)0x280)
26
- #define INPT4 (*(volatile uint8_t*)0x0C)
27
-
28
- /* SWCHA P0 nibble, active-low after the ~SWCHA invert. The bit order is
29
- * Right/Left/Down/Up from bit7 down — the OLD defines here were exactly
30
- * REVERSED (UP=0x80 etc.), which made up/down move the sprite left/right
31
- * on every 7800 scaffold. */
32
- #define JOY_RIGHT 0x80
33
- #define JOY_LEFT 0x40
34
- #define JOY_DOWN 0x20
35
- #define JOY_UP 0x10
36
-
37
- /* 16-pixel-wide (= 4 bytes in 160A), 8 rows tall player ball. */
38
- static const uint8_t player_row0[4] = { 0x05, 0x55, 0x55, 0x50 };
39
- static const uint8_t player_row1[4] = { 0x55, 0xAA, 0xAA, 0x55 };
40
- static const uint8_t player_row2[4] = { 0x5A, 0xFF, 0xFF, 0xA5 };
41
- static const uint8_t player_row3[4] = { 0x5A, 0xFF, 0xFF, 0xA5 };
42
- static const uint8_t player_row4[4] = { 0x5A, 0xFF, 0xFF, 0xA5 };
43
- static const uint8_t player_row5[4] = { 0x5A, 0xFF, 0xFF, 0xA5 };
44
- static const uint8_t player_row6[4] = { 0x55, 0xAA, 0xAA, 0x55 };
45
- static const uint8_t player_row7[4] = { 0x05, 0x55, 0x55, 0x50 };
46
-
47
- #define MK_DL(name) static uint8_t name[7] = { 0, 0x40, 0, 0x1C, 80, 0, 0 }
48
- MK_DL(dl_row0); MK_DL(dl_row1); MK_DL(dl_row2); MK_DL(dl_row3);
49
- MK_DL(dl_row4); MK_DL(dl_row5); MK_DL(dl_row6); MK_DL(dl_row7);
106
+ #define P7C1 (*(volatile uint8_t*)0x3D)
50
107
 
51
- static uint8_t dl_empty[2] = { 0, 0 };
108
+ /* TIA audio (shared with the music player below; atari7800_sfx.c has the
109
+ * same defines — the chip is tiny enough that duplicating 6 lines beats a
110
+ * header dependency the fork machinery would have to carry). */
111
+ #define AUDC0 (*(volatile uint8_t*)0x15)
112
+ #define AUDC1 (*(volatile uint8_t*)0x16)
113
+ #define AUDF0 (*(volatile uint8_t*)0x17)
114
+ #define AUDF1 (*(volatile uint8_t*)0x18)
115
+ #define AUDV0 (*(volatile uint8_t*)0x19)
116
+ #define AUDV1 (*(volatile uint8_t*)0x1A)
117
+
118
+ #define SWCHA (*(volatile uint8_t*)0x280)
119
+ #define INPT4 (*(volatile uint8_t*)0x0C) /* P1 fire, active low (bit 7) */
120
+ #define INPT5 (*(volatile uint8_t*)0x0D) /* P2 fire, active low (bit 7) */
121
+
122
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
123
+ * SWCHA joystick bit order — the #1 7800 input footgun. After the ~SWCHA
124
+ * invert, port 0 (left jack) lives in the HIGH nibble as
125
+ * Right($80) Left($40) Down($20) Up($10), and port 1 (right jack) in the
126
+ * LOW nibble as Right($08) Left($04) Down($02) Up($01). Writing the masks
127
+ * in "natural reading order" (UP=0x80…) is exactly REVERSED and makes the
128
+ * stick's vertical axis steer horizontally — a bug weird enough to
129
+ * misdiagnose as a core problem. Verified bit-by-bit against prosystem.
130
+ * 2P alternating turns uses BOTH ports: player 1 reads the high nibble +
131
+ * INPT4 fire, player 2 the low nibble + INPT5 fire. */
132
+ #define J1_RIGHT 0x80
133
+ #define J1_LEFT 0x40
134
+ #define J1_DOWN 0x20
135
+ #define J1_UP 0x10
136
+ #define J2_RIGHT 0x08
137
+ #define J2_LEFT 0x04
138
+ #define J2_DOWN 0x02
139
+ #define J2_UP 0x01
52
140
 
53
- /* ── Background playfield ─────────────────────────────────────────
54
- * Without a full-screen drawable the display list emits only the
55
- * player and ~99% of the screen stays the flat BACKGRND colour
56
- * (reads as "blank"). These full-width bands give the level a sky,
57
- * a field, and a solid ground strip the player stands on.
141
+ /* ════════════════════════════════════════════════════════════════════════
142
+ * RAM MAP the 7800 gives you 4KB ($1800-$27FF) and the stock cc65 config
143
+ * only hands the linker the first 2112 bytes of it:
58
144
  *
59
- * A single DL drawable is at most 32 bytes = 128 px wide, so a full
60
- * 160-px line needs TWO drawables. Width = byte[3] low 5 bits (32-n);
61
- * high 3 bits = palette. */
62
- static const uint8_t band_pix[32] = {
63
- 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,
64
- 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
145
+ * $1800-$203F RAM1 — cc65 DATA + BSS (everything `static` below)
146
+ * $2040-$20FF (gap the cc65 cfg skips unused here)
147
+ * $2100-$213F RAM2 — unused here
148
+ * $2200-$25FD RAM3 bottom OUR display-list pool/canvas arena (POOLB):
149
+ * raw pointer, invisible to the linker, 1022 bytes
150
+ * $25FE-$27FF RAM3 top — cc65 C parameter stack (crt0 starts it at $2800
151
+ * growing DOWN; ~510 bytes is plenty for these call depths,
152
+ * but if you add deep recursion, shrink POOLB_LINES first)
153
+ * ════════════════════════════════════════════════════════════════════════ */
154
+ #define POOLB ((uint8_t*)0x2200)
155
+
156
+ /* ── Screen layout (243 NTSC zone-lines; the visible frame is ~lines 9-232) ──
157
+ * lines 0- 15 blank (top overscan) 1 DLL entry, 16 tall
158
+ * lines 16- 23 HUD text row (RAM canvas) 8 entries, 1 tall each
159
+ * lines 24- 25 divider band 1 entry, 2 tall
160
+ * lines 26-145 THE ARENA — 120 one-line zones 120 entries (the pool)
161
+ * lines 146-147 ground band (the floor surface) 1 entry, 2 tall
162
+ * lines 148-242 pit/decor stripes (below floor) 12 entries, 8/7 tall
163
+ * Total: 143 DLL entries = 429 bytes (vs 729 for the naive all-1-line DLL —
164
+ * mixed zone heights are how real 7800 games keep the DLL small).
165
+ * The ARENA pool holds every moving/placed object: the hero, coins, spikes,
166
+ * AND the one-way ledge bands (a ledge is just a wide colour-1 object drawn
167
+ * across the lines it occupies). */
168
+ #define FIELD_LINES 120
169
+ #define FIELD_DLL_OFF 30 /* byte offset of arena entry 0 in dll[] */
170
+ #define ARENA_TOP 26 /* zone line of arena line 0 (for Y math) */
171
+
172
+ /* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
173
+ * Object art. 160A mode: 1 byte = 4 pixels of 2 bits each; pixel value
174
+ * 1/2/3 = colour 1/2/3 of the palette the DL entry names, 0 = transparent.
175
+ * Rows are stored top-down, consecutive (the 1-scanline-zone pattern below
176
+ * means NO page-alignment dance — see "offset addressing quirk" in
177
+ * MENTAL_MODEL.md for what multi-line zones would demand instead). */
178
+
179
+ /* Hero, 8px wide (2 bytes) x 12 rows — a little climber. Colours: 1 body,
180
+ * 2 shade, 3 highlight. Two poses: idle and jump (arms up). Drawn with
181
+ * palette 1 (P1) or 2 (P2). */
182
+ static const uint8_t GFX_HERO_IDLE[12 * 2] = {
183
+ 0x05, 0x40, /* 1 1 (head) */
184
+ 0x16, 0x90, /* 11 21 1 */
185
+ 0x16, 0x90, /* 11 21 1 */
186
+ 0x05, 0x40, /* 1 1 */
187
+ 0x1A, 0xA4, /* 122221 1 (body) */
188
+ 0x6A, 0xA9, /*122222221 */
189
+ 0x6A, 0xA9, /*122222221 */
190
+ 0x6A, 0xA9, /*122222221 */
191
+ 0x1A, 0xA4, /* 1222221 */
192
+ 0x14, 0x10, /* 11 1 (legs) */
193
+ 0x14, 0x10, /* 11 1 */
194
+ 0x34, 0x30, /* 31 31 */
195
+ };
196
+ static const uint8_t GFX_HERO_JUMP[12 * 2] = {
197
+ 0x40, 0x01, /*1 1 (arms up) */
198
+ 0x50, 0x05, /*11 11 */
199
+ 0x15, 0x40, /* 11 11 */
200
+ 0x05, 0x40, /* 1 1 (head) */
201
+ 0x1A, 0xA4, /* 122221 */
202
+ 0x6A, 0xA9, /*122222221 (body) */
203
+ 0x6A, 0xA9, /*122222221 */
204
+ 0x1A, 0xA4, /* 122221 */
205
+ 0x16, 0x90, /* 11 21 1 */
206
+ 0x24, 0x12, /* 2 1 2 (legs out)*/
207
+ 0x60, 0x06, /*2 2 */
208
+ 0x40, 0x01, /*3 3 */
209
+ };
210
+
211
+ /* Coin, 8px wide (2 bytes) x 6 rows — colour 3 disc, colour 2 emboss. */
212
+ static const uint8_t GFX_COIN[6 * 2] = {
213
+ 0x3C, 0xF0, /* 333 33 */
214
+ 0xFB, 0xFC, /* 3322333 3 */
215
+ 0xFB, 0xFC, /* 3322333 3 */
216
+ 0xFB, 0xFC, /* 3322333 3 */
217
+ 0x3F, 0xF0, /* 333333 */
218
+ 0x0F, 0xC0, /* 3333 */
65
219
  };
220
+
221
+ /* Spike, 8px wide (2 bytes) x 5 rows — a colour-1 hazard with colour-3 tip. */
222
+ static const uint8_t GFX_SPIKE[5 * 2] = {
223
+ 0x00, 0x30, /* 3 */
224
+ 0x03, 0x30, /* 1 13 */
225
+ 0x0F, 0x3C, /* 111 33 */
226
+ 0x3F, 0xFC, /* 11111133 */
227
+ 0xFF, 0xFF, /*1111111111 */
228
+ };
229
+
230
+ /* DL mode bytes for the 4-byte (direct) entry form: palette in bits 5-7,
231
+ * width as (32 - width_bytes) in bits 0-4 (must be non-zero — a zero low
232
+ * 5 bits would make MARIA parse a 5-byte entry instead). */
233
+ #define MODE_HERO1 ((1u << 5) | (32 - 2)) /* palette 1, 2 bytes wide */
234
+ #define MODE_HERO2 ((2u << 5) | (32 - 2)) /* palette 2 */
235
+ #define MODE_SPIKE ((3u << 5) | (32 - 2)) /* palette 3, 2 bytes wide */
236
+ #define MODE_COIN ((4u << 5) | (32 - 2)) /* palette 4, 2 bytes wide */
237
+
238
+ /* ── GAME LOGIC (clay) — 8x8 text font, 1 bit per pixel, 7px glyphs.
239
+ * The 7800 has NO text mode and no tilemap; text is just more objects.
240
+ * The text path here: expand glyphs into a 32-byte-wide RAM canvas
241
+ * (= 128px, 16 characters), then show the canvas with ONE wide DL entry
242
+ * per scanline. One drawable per line beats one-DL-entry-per-character
243
+ * by 16x in MARIA DMA time. Index order: 0-9 A-Z dash space. */
244
+ static const uint8_t FONT[38 * 8] = {
245
+ 0x70,0x88,0x98,0xA8,0xC8,0x88,0x70,0x00, /* 0 */
246
+ 0x20,0x60,0x20,0x20,0x20,0x20,0x70,0x00, /* 1 */
247
+ 0x70,0x88,0x08,0x30,0x40,0x80,0xF8,0x00, /* 2 */
248
+ 0x70,0x88,0x08,0x30,0x08,0x88,0x70,0x00, /* 3 */
249
+ 0x10,0x30,0x50,0x90,0xF8,0x10,0x10,0x00, /* 4 */
250
+ 0xF8,0x80,0xF0,0x08,0x08,0x88,0x70,0x00, /* 5 */
251
+ 0x30,0x40,0x80,0xF0,0x88,0x88,0x70,0x00, /* 6 */
252
+ 0xF8,0x08,0x10,0x20,0x40,0x40,0x40,0x00, /* 7 */
253
+ 0x70,0x88,0x88,0x70,0x88,0x88,0x70,0x00, /* 8 */
254
+ 0x70,0x88,0x88,0x78,0x08,0x10,0x60,0x00, /* 9 */
255
+ 0x20,0x50,0x88,0x88,0xF8,0x88,0x88,0x00, /* A */
256
+ 0xF0,0x88,0x88,0xF0,0x88,0x88,0xF0,0x00, /* B */
257
+ 0x70,0x88,0x80,0x80,0x80,0x88,0x70,0x00, /* C */
258
+ 0xF0,0x88,0x88,0x88,0x88,0x88,0xF0,0x00, /* D */
259
+ 0xF8,0x80,0x80,0xF0,0x80,0x80,0xF8,0x00, /* E */
260
+ 0xF8,0x80,0x80,0xF0,0x80,0x80,0x80,0x00, /* F */
261
+ 0x70,0x88,0x80,0xB8,0x88,0x88,0x70,0x00, /* G */
262
+ 0x88,0x88,0x88,0xF8,0x88,0x88,0x88,0x00, /* H */
263
+ 0x70,0x20,0x20,0x20,0x20,0x20,0x70,0x00, /* I */
264
+ 0x38,0x10,0x10,0x10,0x10,0x90,0x60,0x00, /* J */
265
+ 0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x00, /* K */
266
+ 0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00, /* L */
267
+ 0x88,0xD8,0xA8,0xA8,0x88,0x88,0x88,0x00, /* M */
268
+ 0x88,0xC8,0xA8,0x98,0x88,0x88,0x88,0x00, /* N */
269
+ 0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00, /* O */
270
+ 0xF0,0x88,0x88,0xF0,0x80,0x80,0x80,0x00, /* P */
271
+ 0x70,0x88,0x88,0x88,0xA8,0x90,0x68,0x00, /* Q */
272
+ 0xF0,0x88,0x88,0xF0,0xA0,0x90,0x88,0x00, /* R */
273
+ 0x78,0x80,0x80,0x70,0x08,0x08,0xF0,0x00, /* S */
274
+ 0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0x00, /* T */
275
+ 0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00, /* U */
276
+ 0x88,0x88,0x88,0x88,0x88,0x50,0x20,0x00, /* V */
277
+ 0x88,0x88,0x88,0xA8,0xA8,0xD8,0x88,0x00, /* W */
278
+ 0x88,0x88,0x50,0x20,0x50,0x88,0x88,0x00, /* X */
279
+ 0x88,0x88,0x50,0x20,0x20,0x20,0x20,0x00, /* Y */
280
+ 0xF8,0x08,0x10,0x20,0x40,0x80,0xF8,0x00, /* Z */
281
+ 0x00,0x00,0x00,0x78,0x00,0x00,0x00,0x00, /* - */
282
+ 0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* space */
283
+ };
284
+ /* nibble → 2bpp expansion: each 1 bit becomes pixel value 1 (palette c1) */
285
+ static const uint8_t NIB2[16] = {
286
+ 0x00,0x01,0x04,0x05,0x10,0x11,0x14,0x15,
287
+ 0x40,0x41,0x44,0x45,0x50,0x51,0x54,0x55,
288
+ };
289
+
290
+ /* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
291
+ * Solid band drawable for multi-line zones AND the one-way ledges. Inside a
292
+ * zone of height H, MARIA fetches scanline l's pixels from ADDR + (H-1-l)*256
293
+ * — the "offset addressing quirk". A multi-line drawable therefore needs
294
+ * valid data at the SAME low-byte offset across H consecutive 256-byte pages.
295
+ * For solid colour bands we sidestep alignment entirely: a 2KB ROM run of
296
+ * 0x55 means ANY address inside the first page works for zones up to 8 tall
297
+ * (8 pages x 256). Costs 2KB of a 32KB cart — ROM is the cheap resource here.
298
+ * The arena ledges reuse SOLID8 too: a ledge is a wide colour-1 object drawn
299
+ * into the one-line arena zones it spans (1-line zones ⇒ the quirk vanishes,
300
+ * any SOLID8 address works). */
301
+ #define S16 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
302
+ #define S256 S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16
303
+ static const uint8_t SOLID8[2048] = { S256,S256,S256,S256,S256,S256,S256,S256 };
304
+
305
+ /* Full-width band DL: a DL drawable is at most 32 bytes (128px), so a
306
+ * 160px line takes TWO 5-byte entries + terminator = 11 bytes. 5-byte
307
+ * form: lo, $40 (extended, write-mode 0 = 160A), hi, palette|width, X.
308
+ * Width 32 encodes as 0 in the low 5 bits — legal ONLY in 5-byte form. */
66
309
  #define MK_BAND(name, pal) static uint8_t name[11] = { \
67
- 0, 0x40, 0, ((pal) << 5) | 0, 0, /* 128 px @ x0 */ \
68
- 0, 0x40, 0, ((pal) << 5) | 24, 128, /* 32 px @ x128 */ \
310
+ 0, 0x40, 0, ((pal) << 5) | 0, 0, /* 128px @ x=0 */ \
311
+ 0, 0x40, 0, ((pal) << 5) | 24, 128, /* 32px @ x=128 */ \
69
312
  0 }
70
- MK_BAND(dl_field, 1);
71
- MK_BAND(dl_ground, 2);
72
- /* Ground strip starts just below where the player rests (GROUND_Y). */
73
- #define GROUND_ZONE 200
74
-
75
- static void set_band_addr(uint8_t* dl) {
76
- uint16_t a = (uint16_t)(uintptr_t)band_pix;
77
- dl[0] = dl[5] = (uint8_t)(a & 0xFF);
78
- dl[2] = dl[7] = (uint8_t)(a >> 8);
79
- }
80
-
81
- static uint16_t bg_zone_dl(int zone) {
82
- if (zone >= GROUND_ZONE) return (uint16_t)(uintptr_t)dl_ground;
83
- if (zone >= 28) return (uint16_t)(uintptr_t)dl_field;
84
- return (uint16_t)(uintptr_t)dl_empty;
85
- }
86
-
87
- #define DLL_ZONES 243
88
- static uint8_t dll[DLL_ZONES * 3];
89
-
90
- static void set_dl_addr(uint8_t* dl, const uint8_t* row) {
91
- uint16_t a = (uint16_t)(uintptr_t)row;
92
- dl[0] = (uint8_t)(a & 0xFF);
93
- dl[2] = (uint8_t)(a >> 8);
94
- }
95
-
96
- static void set_dll_entry(int idx, uint16_t dl_ptr) {
97
- dll[idx * 3 + 0] = 0;
98
- dll[idx * 3 + 1] = (uint8_t)(dl_ptr >> 8);
99
- dll[idx * 3 + 2] = (uint8_t)(dl_ptr & 0xFF);
100
- }
101
-
102
- static void build_dll(uint8_t y) {
103
- int i;
104
- for (i = 0; i < DLL_ZONES; i++) {
105
- uint16_t dl;
106
- int d = i - (int)y;
107
- switch (d) {
108
- case 0: dl = (uint16_t)(uintptr_t)dl_row0; break;
109
- case 1: dl = (uint16_t)(uintptr_t)dl_row1; break;
110
- case 2: dl = (uint16_t)(uintptr_t)dl_row2; break;
111
- case 3: dl = (uint16_t)(uintptr_t)dl_row3; break;
112
- case 4: dl = (uint16_t)(uintptr_t)dl_row4; break;
113
- case 5: dl = (uint16_t)(uintptr_t)dl_row5; break;
114
- case 6: dl = (uint16_t)(uintptr_t)dl_row6; break;
115
- case 7: dl = (uint16_t)(uintptr_t)dl_row7; break;
116
- default: dl = bg_zone_dl(i); break;
313
+ MK_BAND(dl_band_a, 6);
314
+ MK_BAND(dl_band_b, 7);
315
+ MK_BAND(dl_ground, 5); /* the floor surface band (HUD green) */
316
+ static uint8_t dl_empty[2] = { 0, 0 };
317
+
318
+ /* ════════════════════════════════════════════════════════════════════════
319
+ * ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
320
+ * THE DISPLAY-LIST POOL — how a populated arena gets drawn (the 7800's
321
+ * signature). Same machinery the dense 7800 shmup uses for its swarm; here
322
+ * it draws the hero, coins, spikes, AND the ledge bands.
323
+ *
324
+ * MARIA hierarchy refresher: DPP → DLL (one entry per ZONE: height + DL
325
+ * pointer) → DL (one 4/5-byte entry per OBJECT crossing that zone) pixel
326
+ * bytes. There is no sprite table; "an object" IS a DL entry.
327
+ *
328
+ * The arena is 120 one-scanline zones. Each has a fixed 14-byte DL slot:
329
+ * room for THREE 4-byte object entries + the terminator byte (MARIA reads
330
+ * the NEXT entry's mode byte after each entry; a 0 there ends the line —
331
+ * forget the terminator and MARIA walks into garbage and the screen dies).
332
+ *
333
+ * WHY 3 PER LINE — the MARIA DMA budget, the dial this whole game turns:
334
+ * MARIA steals the bus from the CPU to fetch each line's DL + pixels
335
+ * (~113 DMA cycles per scanline before the line visibly runs out). A
336
+ * 4-byte header costs ~8 cycles + 3/pixel-byte, so three 2-byte-wide
337
+ * objects ≈ 40 of 113 — comfortable. Eight would not be. When a 4th
338
+ * object-row lands on one line we DROP it for that frame — a one-line
339
+ * flicker on that object, exactly the artifact real dense 7800 games
340
+ * show. More objects per line ⇒ bigger slots ⇒ more RAM ⇒ fewer lines;
341
+ * quantity, width, and field height all trade against the same budget.
342
+ *
343
+ * The pool is SPLIT across two RAM regions because no single linker
344
+ * region fits 1680 bytes + the DLL + the canvases (see RAM MAP):
345
+ * lines 0-46 → pool_a[] (BSS, RAM1) 47 * 14 = 658 bytes
346
+ * lines 47-119 → POOLB ($2200, raw RAM3) 73 * 14 = 1022 bytes
347
+ * line_dl[] resolves an arena line to its slot; nothing else knows the split.
348
+ *
349
+ * Rebuild-vs-patch doctrine (MENTAL_MODEL.md): the DLL is built ONCE and
350
+ * only its 3-byte arena entries are repointed at state changes (with DMA
351
+ * off); per-frame work only rewrites bytes INSIDE existing 14-byte slots.
352
+ * Tearing down the DLL itself mid-game races MARIA's walker — the classic
353
+ * "works one frame then the screen falls apart" 7800 bug.
354
+ * ════════════════════════════════════════════════════════════════════════ */
355
+ #define LINE_BYTES 14
356
+ #define LINE_FULL 12 /* 3 entries * 4 bytes */
357
+ #define POOLA_LINES 47
358
+ static uint8_t pool_a[POOLA_LINES * LINE_BYTES];
359
+ static uint8_t* line_dl[FIELD_LINES];
360
+ static uint8_t line_used[FIELD_LINES];
361
+
362
+ static uint8_t dll[143 * 3];
363
+ static uint8_t hud_canvas[8 * 32]; /* 16-char text row, lives in BSS */
364
+ static uint8_t hud_dls[8 * 7]; /* one 5-byte DL + term per row */
365
+
366
+ /* Emit one object: a 4-byte direct DL entry into every arena line one of
367
+ * its rows crosses. gfx rows are consecutive (stride = width in bytes).
368
+ * Callers keep y in [0, FIELD_LINES - h] so no clipping is needed — keep
369
+ * that invariant if you change movement code, or add clipping here. */
370
+ static void emit_object(uint8_t y, uint8_t h, const uint8_t* gfx,
371
+ uint8_t stride, uint8_t mode, uint8_t x) {
372
+ uint8_t r, off;
373
+ uint8_t* dl;
374
+ for (r = 0; r < h; ++r) {
375
+ off = line_used[y];
376
+ if (off < LINE_FULL) { /* line full ⇒ drop row (flicker) */
377
+ dl = line_dl[y] + off;
378
+ dl[0] = (uint8_t)((uint16_t)(uintptr_t)gfx & 0xFF);
379
+ dl[1] = mode;
380
+ dl[2] = (uint8_t)((uint16_t)(uintptr_t)gfx >> 8);
381
+ dl[3] = x;
382
+ line_used[y] = off + 4;
117
383
  }
118
- set_dll_entry(i, dl);
384
+ ++y;
385
+ gfx += stride;
119
386
  }
120
387
  }
121
388
 
122
- static void set_x(uint8_t x) {
123
- dl_row0[4] = x; dl_row1[4] = x; dl_row2[4] = x; dl_row3[4] = x;
124
- dl_row4[4] = x; dl_row5[4] = x; dl_row6[4] = x; dl_row7[4] = x;
389
+ /* Emit a horizontal LEDGE: a 2-line-tall colour-1 band spanning [xl, xr].
390
+ * It's the same emit path as a sprite, just pointed at SOLID8 (any address
391
+ * in its first page works for a 1-line zone — the quirk note above). A single
392
+ * DL object draws at most 32 bytes (128px); wider ledges (our long floor) are
393
+ * tiled in ≤24-byte chunks so they never overrun a line's 3-object budget on
394
+ * their own 2-line zone (nothing else is emitted on a ledge's lines). */
395
+ static void emit_ledge(uint8_t y, uint8_t xl, uint8_t xr, uint8_t mode_pal) {
396
+ uint8_t x = xl;
397
+ while (x <= xr) {
398
+ uint8_t span = (uint8_t)(xr - x + 1); /* pixels remaining */
399
+ uint8_t w = (uint8_t)((span + 3) >> 2); /* round up to bytes */
400
+ uint8_t mode;
401
+ if (w > 24) w = 24; /* 96px chunk max */
402
+ mode = (uint8_t)((mode_pal << 5) | (32 - w));
403
+ emit_object(y, 2, SOLID8, 0, mode, x);
404
+ x = (uint8_t)(x + (w << 2));
405
+ if (w == 0) break;
406
+ }
125
407
  }
126
408
 
127
- static void vblank_wait(void) {
128
- while (MSTAT & 0x80) { }
129
- while (!(MSTAT & 0x80)) { }
409
+ static void field_open(void) { /* step 1: forget last frame */
410
+ memset(line_used, 0, FIELD_LINES);
411
+ }
412
+
413
+ static void field_close(void) { /* step 3: terminate every line */
414
+ uint8_t i;
415
+ for (i = 0; i < FIELD_LINES; ++i)
416
+ line_dl[i][line_used[i] + 1] = 0; /* next entry's MODE byte = 0 */
417
+ }
418
+
419
+ /* ── HARDWARE IDIOM (load-bearing) — DLL construction + zone repointing.
420
+ * Built once at boot; dll_zone appends one 3-byte entry (offset byte =
421
+ * height-1; DLI/holey bits stay 0 — no NMI handler, no holey DMA here). */
422
+ static uint8_t* dllp;
423
+ static void dll_zone(uint8_t height, uint16_t dl) {
424
+ dllp[0] = height - 1;
425
+ dllp[1] = (uint8_t)(dl >> 8);
426
+ dllp[2] = (uint8_t)(dl & 0xFF);
427
+ dllp += 3;
428
+ }
429
+
430
+ /* Repoint ONE arena line's DLL entry (title/menu/game-over text overlays
431
+ * borrow arena zones; play repoints them back at the pool slots). */
432
+ static void point_field_zone(uint8_t fline, uint16_t dl) {
433
+ uint8_t* e = dll + FIELD_DLL_OFF + (uint16_t)fline * 3;
434
+ e[0] = 0;
435
+ e[1] = (uint8_t)(dl >> 8);
436
+ e[2] = (uint8_t)(dl & 0xFF);
437
+ }
438
+
439
+ /* ── GAME LOGIC (clay) — text rendering into a 32-byte-wide RAM canvas ── */
440
+ static uint8_t glyph_index(char c) {
441
+ if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
442
+ if (c >= 'A' && c <= 'Z') return (uint8_t)(10 + c - 'A');
443
+ if (c == '-') return 36;
444
+ return 37; /* space */
445
+ }
446
+
447
+ static void draw_text(uint8_t* canvas, uint8_t col, const char* s) {
448
+ uint8_t r, b;
449
+ const uint8_t* g;
450
+ uint8_t* dst;
451
+ while (*s && col < 16) {
452
+ g = FONT + ((uint16_t)glyph_index(*s) << 3);
453
+ dst = canvas + ((uint16_t)col << 1);
454
+ for (r = 0; r < 8; ++r) {
455
+ b = g[r];
456
+ dst[0] = NIB2[b >> 4];
457
+ dst[1] = NIB2[b & 0x0F];
458
+ dst += 32;
459
+ }
460
+ ++s;
461
+ ++col;
462
+ }
463
+ }
464
+
465
+ static void digits5(char* d, uint16_t v) {
466
+ uint8_t i;
467
+ for (i = 0; i < 5; ++i) { d[4 - i] = (char)('0' + v % 10); v /= 10; }
468
+ }
469
+
470
+ /* Build the 8 one-line DLs that display an arbitrary RAM canvas at x=16
471
+ * (centered 128px). pal picks the text colour palette. dls = 8*7 bytes. */
472
+ static void canvas_dls(uint8_t* dls, const uint8_t* canvas, uint8_t pal) {
473
+ uint8_t r;
474
+ uint16_t a;
475
+ for (r = 0; r < 8; ++r) {
476
+ a = (uint16_t)(uintptr_t)canvas + ((uint16_t)r << 5);
477
+ dls[0] = (uint8_t)(a & 0xFF);
478
+ dls[1] = 0x40; /* 5-byte form, 160A write mode */
479
+ dls[2] = (uint8_t)(a >> 8);
480
+ dls[3] = (uint8_t)((pal << 5) | 0); /* width 32 bytes encodes as 0 */
481
+ dls[4] = 16;
482
+ dls[5] = 0;
483
+ dls[6] = 0; /* terminator for the next read */
484
+ dls += 7;
485
+ }
486
+ }
487
+
488
+ /* ── GAME LOGIC (clay) — the music. Two-voice TIA tune loop. ─────────────────
489
+ * The TIA's frequency divider is 5 bits — ~32 pitches TOTAL, none of them
490
+ * in tune with each other. Don't fight it: write the melody IN the TIA's
491
+ * crooked scale and it reads as "gritty 7800", fight it and it reads as
492
+ * "wrong". The note tables ARE the song — edit them to recompose.
493
+ * Voice 0 = melody (AUDC 4, square-ish). Voice 1 = bass (AUDC 6, deep
494
+ * buzz) — and voice 1 is SHARED with sound effects (TIA has only two
495
+ * voices): when the game fires an effect, sfx_hold mutes the bass for the
496
+ * effect's length, then the bass re-enters on its next note. That
497
+ * steal-and-return is the standard 2-voice arbitration trick. */
498
+ static const uint8_t MEL_F[16] = { 17,15,13,15, 17,17,20,255, 15,13,12,13, 15,17,15,255 };
499
+ static const uint8_t MEL_L[16] = { 8, 8, 8, 8, 8, 8,16, 8, 8, 8, 8, 8, 8, 8,16, 8 };
500
+ static const uint8_t BAS_F[8] = { 29,29,25,25, 27,27,23,25 };
501
+ static uint8_t mel_i, mel_t, bas_i, bas_t, sfx_hold;
502
+
503
+ static void music_tick(void) {
504
+ if (mel_t) --mel_t;
505
+ if (mel_t == 0) {
506
+ mel_i = (uint8_t)((mel_i + 1) & 15);
507
+ mel_t = MEL_L[mel_i];
508
+ if (MEL_F[mel_i] == 255) {
509
+ AUDV0 = 0; /* 255 = rest */
510
+ } else {
511
+ AUDC0 = 4; AUDF0 = MEL_F[mel_i]; AUDV0 = 6;
512
+ }
513
+ }
514
+ if (sfx_hold) { /* an effect owns voice 1 */
515
+ --sfx_hold;
516
+ if (sfx_hold == 0) bas_t = 1; /* bass re-enters next tick */
517
+ return;
518
+ }
519
+ if (bas_t) --bas_t;
520
+ if (bas_t == 0) {
521
+ bas_i = (uint8_t)((bas_i + 1) & 7);
522
+ bas_t = 16;
523
+ AUDC1 = 6; AUDF1 = BAS_F[bas_i]; AUDV1 = 5;
524
+ }
130
525
  }
131
526
 
132
- /* Physics in 4.4 fixed point 16 = 1 px, allows half-pixel velocity. */
133
- #define GRAVITY 6
134
- #define MOVE_PX 2 /* 1 px/frame read as 'doesn't move' — 2 is snappy */
135
- #define JUMP_VEL (-80) /* was -48: a 10px hop over ~12 frames read as 'jumps very slowly' — this is ~27px in the same time */
136
- #define MAXFALL 64
137
- #define GROUND_Y 200 /* DLL index of the ground line */
527
+ /* Effects (voice 1 via atari7800_sfx; sfx_hold keeps the bass out). */
528
+ static void fx_jump(void) { sfx_tone(1, 12, 5); sfx_hold = 6; }
529
+ static void fx_land(void) { sfx_tone(1, 22, 3); sfx_hold = 4; }
530
+ static void fx_coin(void) { sfx_tone(1, 6, 5); sfx_hold = 6; }
531
+ static void fx_die(void) { sfx_noise(22); sfx_hold = 23; }
532
+ static void fx_start(void) { sfx_tone(1, 8, 6); sfx_hold = 7; }
533
+
534
+ /* ── GAME LOGIC (clay — reshape freely) — THE LEVEL ──────────────────────────
535
+ * A fixed single-screen arena, expressed as one-way LEDGES (the floor plus
536
+ * floating slabs) in arena-line coordinates [0, FIELD_LINES). Each ledge:
537
+ * { y_top, x_left, x_right }. The hero stands on a ledge's TOP edge; a pit
538
+ * is simply the gap in the floor ledge (fall through it = death). Coins and
539
+ * spikes are placed relative to these ledges in begin_turn().
540
+ *
541
+ * Arena line 0 = zone line 26; the floor band sits just below arena line
542
+ * 119. Tweak this table freely — it's the whole level design surface. */
543
+ #define NUM_LEDGES 5
544
+ typedef struct { uint8_t yt, xl, xr; } Ledge;
545
+ static const Ledge LEDGES[NUM_LEDGES] = {
546
+ /* the FLOOR is a long run with a lethal pit cut into the right side, so
547
+ * the hero has plenty of room to walk before the gap. */
548
+ { 112, 2, 116 }, /* floor left (x 2..116) */
549
+ { 112, 140, 158 }, /* floor right (x 140..158) — pit is x 116..140 */
550
+ { 84, 24, 72 }, /* low slab */
551
+ { 56, 100, 148 }, /* mid slab */
552
+ { 32, 40, 96 }, /* high slab */
553
+ };
554
+ #define FLOOR_Y 112 /* the two floor ledges' top */
555
+ #define PIT_XL 116 /* lethal pit spans floor x [PIT_XL, PIT_XR] */
556
+ #define PIT_XR 140
557
+
558
+ /* ── GAME LOGIC (clay) — coins + spikes, placed on the ledges. ── */
559
+ #define NUM_COINS 6
560
+ #define NUM_SPIKES 5
561
+ static const uint8_t COIN_X[NUM_COINS] = { 44, 48, 16, 120, 124, 60 };
562
+ static const uint8_t COIN_Y[NUM_COINS] = { 104, 76, 66, 48, 76, 24 };
563
+ /* coin 2 (x16,y66) hangs just above the spawn — a single floor jump grabs
564
+ * it; coin 0 (x44,y104) is a short walk-right on the floor. The rest sit on
565
+ * the slabs (jump-and-platform to reach), and gathering ALL pays a bonus. */
566
+ static uint8_t coin_live[NUM_COINS];
567
+ /* spikes sit ON ledge tops (y = ledge_top - 5 so the 5px spike rests on it).
568
+ * Kept OFF the left spawn lane (hero starts at x≈12 on the left floor and can
569
+ * walk right to ~x100 before the first floor spike — room to traverse). */
570
+ static const uint8_t SPIKE_X[NUM_SPIKES] = { 150, 56, 108, 132, 72 };
571
+ static const uint8_t SPIKE_Y[NUM_SPIKES] = { 107, 79, 107, 51, 51 };
572
+
573
+ /* ── GAME LOGIC (clay — reshape freely) — game state ─────────────────────────
574
+ * Fixed object pools, no allocation (1.79MHz CPU, 4KB RAM — a heap is a
575
+ * cost with no payer). */
576
+ #define LIVES_START 3
577
+ #define HERO_W 8 /* hero pixel width */
578
+ #define HERO_H 12 /* hero pixel height */
579
+
580
+ /* Physics, all in quarter-pixels (Q.2 fixed point) for sub-pixel gravity:
581
+ * gravity adds <1px/tick near the apex, so integer Y alone would jerk. */
582
+ #define GRAVITY_Q2 2 /* +0.5 px/tick/tick */
583
+ #define JUMP_VEL_Q2 (-26) /* launch vy → ~6.5px first tick, ~40px apex */
584
+ #define MAX_VY_Q2 20 /* terminal fall 5px/tick (landing window 6) */
585
+ #define MOVE_SPEED 2 /* px/tick walk */
586
+
587
+ static uint8_t hx; /* hero pixel x (left edge) */
588
+ static uint16_t hy_q2; /* hero y, Q.2 (top edge) */
589
+ static int8_t vy_q2;
590
+ static uint8_t on_ground;
591
+ static uint8_t face_jump; /* pose select */
592
+
593
+ /* Players: 0 = P1 (port 0), 1 = P2 (port 1 — alternating turns). Each has
594
+ * own score + own lives; the HUD shows the CURRENT player's numbers. */
595
+ static uint8_t two_p;
596
+ static uint8_t cur_p;
597
+ static uint8_t p_lives[2];
598
+ static uint16_t p_score[2];
599
+ static uint16_t hiscore;
600
+ static uint8_t turn_pause; /* freeze frames after a turn swap */
601
+ static uint8_t dirty, prev_fire, over_lock;
602
+ static uint16_t rng = 0xACE1;
603
+
604
+ #define ST_TITLE 0
605
+ #define ST_PLAY 1
606
+ #define ST_OVER 2
607
+ static uint8_t state;
608
+
609
+ static uint8_t random8(void) { /* xorshift16 — cheap + fine */
610
+ uint16_t r = rng;
611
+ r ^= r << 7;
612
+ r ^= r >> 9;
613
+ r ^= r << 8;
614
+ rng = r;
615
+ return (uint8_t)r;
616
+ }
617
+
618
+ static uint8_t dist8(uint8_t a, uint8_t b) {
619
+ return (a > b) ? (a - b) : (b - a);
620
+ }
621
+
622
+ /* ── GAME LOGIC (clay) — HUD: "P1 S00000 H00000 L3" composed into canvas ── */
623
+ static void draw_hud(void) {
624
+ static char buf[17] = "P1 00000 00000 0";
625
+ buf[1] = (char)('1' + cur_p);
626
+ digits5(buf + 3, p_score[cur_p]);
627
+ digits5(buf + 9, hiscore);
628
+ buf[15] = (char)('0' + p_lives[cur_p]);
629
+ memset(hud_canvas, 0, sizeof(hud_canvas));
630
+ draw_text(hud_canvas, 0, buf);
631
+ dirty = 0;
632
+ }
633
+
634
+ static void draw_hud_title(void) {
635
+ static char buf[9] = "HI 00000";
636
+ digits5(buf + 3, hiscore);
637
+ memset(hud_canvas, 0, sizeof(hud_canvas));
638
+ draw_text(hud_canvas, 4, buf);
639
+ }
640
+
641
+ /* ── HARDWARE IDIOM (load-bearing) — paint functions bracket structural
642
+ * display-list changes with MARIA DMA OFF ($7F) / ON ($40), the 7800's
643
+ * version of the NES "rendering off before nametable writes" rule: MARIA
644
+ * may be mid-walk through the very lists being rewritten, and repointing
645
+ * dozens of zones under it glitches (or with bad luck hangs) the frame.
646
+ * CTRL $40 = DMA on, 160A read mode, colour burst on — forget to restore
647
+ * it and the screen stays the flat BACKGRND colour forever. ── */
648
+
649
+ /* Title screen: borrow arena zones for three text overlays composed in
650
+ * POOLB (the pool isn't drawing the arena on the title, so its RAM is free —
651
+ * 4KB machines make you reuse like this). Title is double-height by
652
+ * pointing TWO consecutive 1-line zones at each canvas row — zero extra
653
+ * RAM, pure DLL trickery. */
654
+ static void paint_title(void) {
655
+ uint8_t i;
656
+ uint8_t* c0 = POOLB; /* title canvas (256 bytes) */
657
+ uint8_t* c1 = POOLB + 256; /* menu line 1 (256 bytes) */
658
+ uint8_t* c2 = POOLB + 512; /* menu line 2 (256 bytes) */
659
+ uint8_t* td = POOLB + 768; /* 3 lines * 8 row-DLs * 7 */
660
+ CTRL = 0x7F; /* DMA off */
661
+ memset(POOLB, 0, 768);
662
+ draw_text(c0, (uint8_t)((16 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
663
+ draw_text(c1, 2, "1P - JUMP A");
664
+ draw_text(c2, 0, "2P - PAD 2 TURNS");
665
+ canvas_dls(td, c0, 0); /* white */
666
+ canvas_dls(td + 56, c1, 5); /* HUD green */
667
+ canvas_dls(td + 112, c2, 5);
668
+ for (i = 0; i < FIELD_LINES; ++i)
669
+ point_field_zone(i, (uint16_t)(uintptr_t)dl_empty);
670
+ for (i = 0; i < 16; ++i) /* double-height title rows */
671
+ point_field_zone((uint8_t)(8 + i),
672
+ (uint16_t)(uintptr_t)(td + ((i >> 1) * 7)));
673
+ for (i = 0; i < 8; ++i) {
674
+ point_field_zone((uint8_t)(56 + i), (uint16_t)(uintptr_t)(td + 56 + i * 7));
675
+ point_field_zone((uint8_t)(76 + i), (uint16_t)(uintptr_t)(td + 112 + i * 7));
676
+ }
677
+ draw_hud_title();
678
+ state = ST_TITLE;
679
+ CTRL = 0x40; /* DMA back on */
680
+ }
681
+
682
+ /* Game over: the pool RAM becomes the message overlay (same reuse trick as
683
+ * the title), the rest of the arena goes blank. */
684
+ static void paint_gameover(void) {
685
+ uint8_t i;
686
+ uint8_t* c0 = POOLB;
687
+ uint8_t* c1 = POOLB + 256;
688
+ uint8_t* c2 = POOLB + 512;
689
+ uint8_t* td = POOLB + 768;
690
+ static char buf[12] = "P1 00000";
691
+ CTRL = 0x7F;
692
+ memset(POOLB, 0, 768);
693
+ draw_text(c0, 3, "GAME OVER");
694
+ buf[1] = '1'; digits5(buf + 3, p_score[0]);
695
+ draw_text(c1, 4, buf);
696
+ if (two_p) {
697
+ static char buf2[12] = "P2 00000";
698
+ buf2[1] = '2'; digits5(buf2 + 3, p_score[1]);
699
+ draw_text(c2, 4, buf2);
700
+ }
701
+ canvas_dls(td, c0, 0);
702
+ canvas_dls(td + 56, c1, 5);
703
+ canvas_dls(td + 112, c2, 5);
704
+ for (i = 0; i < FIELD_LINES; ++i)
705
+ point_field_zone(i, (uint16_t)(uintptr_t)dl_empty);
706
+ for (i = 0; i < 8; ++i) {
707
+ point_field_zone((uint8_t)(40 + i), (uint16_t)(uintptr_t)(td + i * 7));
708
+ point_field_zone((uint8_t)(60 + i), (uint16_t)(uintptr_t)(td + 56 + i * 7));
709
+ if (two_p)
710
+ point_field_zone((uint8_t)(76 + i), (uint16_t)(uintptr_t)(td + 112 + i * 7));
711
+ }
712
+ over_lock = 30; /* swallow the held fire button */
713
+ state = ST_OVER;
714
+ CTRL = 0x40;
715
+ }
716
+
717
+ /* ── GAME LOGIC (clay) — landing probe: is feet on a ledge top? ──────────────
718
+ * One-way platforms, classic style: only catch while FALLING (vy>=0) through
719
+ * a 6px window at a ledge's top edge, and only if the hero's x overlaps the
720
+ * ledge span. Returns the ledge top (arena line) to snap to, or 0xFF = none.
721
+ * The window is feet-1 .. feet+4 so terminal velocity (5px) can't tunnel. */
722
+ static uint8_t land_top(uint8_t feet, uint8_t x) {
723
+ uint8_t i, top;
724
+ uint8_t xr = (uint8_t)(x + HERO_W - 1);
725
+ for (i = 0; i < NUM_LEDGES; ++i) {
726
+ top = LEDGES[i].yt;
727
+ if ((uint8_t)(feet + 1) >= top && feet <= (uint8_t)(top + 4) &&
728
+ xr >= LEDGES[i].xl && x <= LEDGES[i].xr) {
729
+ /* the pit: the floor ledges already exclude x [PIT_XL,PIT_XR], so a
730
+ * hero centered over the pit finds no ledge and keeps falling. */
731
+ return top;
732
+ }
733
+ }
734
+ return 0xFF;
735
+ }
736
+
737
+ /* ── GAME LOGIC (clay) — start one player's turn ── */
738
+ static void begin_turn(void) {
739
+ uint8_t i;
740
+ hx = 12;
741
+ hy_q2 = (uint16_t)(FLOOR_Y - HERO_H) << 2; /* stand on left floor */
742
+ vy_q2 = 0;
743
+ on_ground = 1;
744
+ face_jump = 0;
745
+ for (i = 0; i < NUM_COINS; ++i) coin_live[i] = 1;
746
+ turn_pause = 40; /* "ready" breather */
747
+ prev_fire = 0xFF; /* swallow held fire across *
748
+ * the turn change */
749
+ draw_hud();
750
+ }
751
+
752
+ static void start_game(uint8_t players) {
753
+ uint8_t i;
754
+ CTRL = 0x7F;
755
+ two_p = players;
756
+ cur_p = 0;
757
+ p_score[0] = p_score[1] = 0;
758
+ p_lives[0] = LIVES_START;
759
+ p_lives[1] = players ? LIVES_START : 0;
760
+ for (i = 0; i < FIELD_LINES; ++i) /* arena zones → pool slots */
761
+ point_field_zone(i, (uint16_t)(uintptr_t)line_dl[i]);
762
+ field_open();
763
+ field_close(); /* all lines empty + termed */
764
+ rng ^= (uint16_t)(hiscore * 251) ^ 0x1234;
765
+ begin_turn();
766
+ fx_start();
767
+ state = ST_PLAY;
768
+ CTRL = 0x40;
769
+ }
770
+
771
+ static void game_over(void) {
772
+ uint16_t best = p_score[0];
773
+ if (two_p && p_score[1] > best) best = p_score[1];
774
+ if (best > hiscore) {
775
+ hiscore = best;
776
+ /* HSC NOTE (see file header): on real hardware with a High Score Cart
777
+ * you would write the record into HSC RAM ($1000-$17FF) here. The
778
+ * bundled prosystem core has no HSC support and exposes no SAVE_RAM,
779
+ * so the record honestly lives only as long as the session. */
780
+ }
781
+ paint_gameover();
782
+ }
783
+
784
+ /* ── GAME LOGIC (clay) — death + alternating-turn handoff ── */
785
+ static void kill_player(void) {
786
+ uint8_t other;
787
+ fx_die();
788
+ if (p_lives[cur_p] > 0) --p_lives[cur_p];
789
+ if (two_p) {
790
+ other = (uint8_t)(cur_p ^ 1);
791
+ if (p_lives[other] > 0) cur_p = other; /* swap turns */
792
+ else if (p_lives[cur_p] == 0) { game_over(); return; }
793
+ } else if (p_lives[0] == 0) {
794
+ game_over();
795
+ return;
796
+ }
797
+ begin_turn();
798
+ }
799
+
800
+ /* ── GAME LOGIC (clay) — per-player movement + physics ── */
801
+ static void update_player(uint8_t pad, uint8_t fire) {
802
+ uint8_t lf, rt, feet, top;
803
+ if (cur_p == 0) { rt = pad & J1_RIGHT; lf = pad & J1_LEFT; }
804
+ else { rt = pad & J2_RIGHT; lf = pad & J2_LEFT; }
805
+
806
+ if (lf && hx > 2) hx -= MOVE_SPEED;
807
+ if (rt && hx < 150) hx += MOVE_SPEED;
808
+
809
+ /* jump on the fire EDGE while grounded (the SWCHA-port fire button) */
810
+ if (fire && !prev_fire && on_ground) {
811
+ vy_q2 = JUMP_VEL_Q2;
812
+ on_ground = 0;
813
+ face_jump = 1;
814
+ fx_jump();
815
+ }
816
+
817
+ /* gravity + sub-pixel Y */
818
+ if (vy_q2 < MAX_VY_Q2) vy_q2 += GRAVITY_Q2;
819
+ hy_q2 = (uint16_t)((int16_t)hy_q2 + vy_q2);
820
+ {
821
+ uint8_t y8 = (uint8_t)(hy_q2 >> 2);
822
+
823
+ /* fell past the floor (into the pit / off the bottom) → lose the turn */
824
+ if (y8 >= FIELD_LINES - HERO_H + 2) { kill_player(); return; }
825
+
826
+ /* landing probe while falling */
827
+ if (vy_q2 >= 0) {
828
+ feet = (uint8_t)(y8 + HERO_H);
829
+ top = land_top(feet, hx);
830
+ if (top != 0xFF) {
831
+ hy_q2 = (uint16_t)(top - HERO_H) << 2;
832
+ vy_q2 = 0;
833
+ if (!on_ground) fx_land();
834
+ on_ground = 1;
835
+ face_jump = 0;
836
+ } else {
837
+ on_ground = 0; /* walked off an edge */
838
+ }
839
+ }
840
+ }
841
+ }
842
+
843
+ static void vblank_wait(void) {
844
+ while (MSTAT & 0x80) { } /* leave the current vblank */
845
+ while (!(MSTAT & 0x80)) { } /* catch the next one starting */
846
+ }
138
847
 
139
848
  void main(void) {
140
- int16_t px = 80;
141
- int16_t py16 = (GROUND_Y - 8) << 4;
142
- int16_t vy = 0;
143
- uint8_t prev_btn = 0;
144
- uint16_t dll_addr;
145
-
146
- set_dl_addr(dl_row0, player_row0);
147
- set_dl_addr(dl_row1, player_row1);
148
- set_dl_addr(dl_row2, player_row2);
149
- set_dl_addr(dl_row3, player_row3);
150
- set_dl_addr(dl_row4, player_row4);
151
- set_dl_addr(dl_row5, player_row5);
152
- set_dl_addr(dl_row6, player_row6);
153
- set_dl_addr(dl_row7, player_row7);
154
- set_band_addr(dl_field);
155
- set_band_addr(dl_ground);
156
- set_x((uint8_t)px);
157
- build_dll((uint8_t)(py16 >> 4));
158
-
159
- BACKGRND = 0x84; /* sky */
160
- P0C1 = 0x46; /* player */
161
- P0C2 = 0x0F;
162
- P0C3 = 0x36;
163
- P1C1 = 0x96; /* distant field (teal) */
164
- P2C1 = 0x24; /* ground (brown) */
849
+ uint8_t i;
850
+ uint16_t a;
851
+
852
+ /* ── HARDWARE IDIOM (load-bearing) — boot order: build EVERYTHING the
853
+ * DLL will reference, then point DPP at it, THEN enable DMA. Enabling
854
+ * DMA over a half-built DLL is the 7800 black-screen classic. ── */
855
+
856
+ /* Resolve the pool split: arena line → 14-byte DL slot. */
857
+ for (i = 0; i < POOLA_LINES; ++i)
858
+ line_dl[i] = pool_a + (uint16_t)i * LINE_BYTES;
859
+ for (i = POOLA_LINES; i < FIELD_LINES; ++i)
860
+ line_dl[i] = POOLB + (uint16_t)(i - POOLA_LINES) * LINE_BYTES;
861
+
862
+ /* Patch the ROM band drawables' data pointers (SOLID8). */
863
+ a = (uint16_t)(uintptr_t)SOLID8;
864
+ dl_band_a[0] = dl_band_a[5] = (uint8_t)(a & 0xFF);
865
+ dl_band_a[2] = dl_band_a[7] = (uint8_t)(a >> 8);
866
+ dl_band_b[0] = dl_band_b[5] = (uint8_t)(a & 0xFF);
867
+ dl_band_b[2] = dl_band_b[7] = (uint8_t)(a >> 8);
868
+ dl_ground[0] = dl_ground[5] = (uint8_t)(a & 0xFF);
869
+ dl_ground[2] = dl_ground[7] = (uint8_t)(a >> 8);
870
+
871
+ canvas_dls(hud_dls, hud_canvas, 5);
872
+
873
+ /* The DLL — the screen layout, built once (see the layout table above).
874
+ * 143 entries, mixed zone heights; only the 120 arena entries are ever
875
+ * repointed after this. */
876
+ dllp = dll;
877
+ dll_zone(16, (uint16_t)(uintptr_t)dl_empty); /* lines 0-15 */
878
+ for (i = 0; i < 8; ++i) /* HUD 16-23 */
879
+ dll_zone(1, (uint16_t)(uintptr_t)(hud_dls + i * 7));
880
+ dll_zone(2, (uint16_t)(uintptr_t)dl_band_a); /* divider */
881
+ for (i = 0; i < FIELD_LINES; ++i) /* arena 26-145 */
882
+ dll_zone(1, (uint16_t)(uintptr_t)line_dl[i]);
883
+ dll_zone(2, (uint16_t)(uintptr_t)dl_ground); /* floor surface*/
884
+ /* Below-floor pit / decor stripes — also our anti-blank-screen ballast:
885
+ * with DMA fetching only objects, everything else is the single flat
886
+ * BACKGRND colour, and a mostly-one-colour frame reads as "dead". */
887
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
888
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
889
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_b);
890
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
891
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
892
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
893
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_b);
894
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
895
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
896
+ dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
897
+ dll_zone(8, (uint16_t)(uintptr_t)dl_band_b); /* …through 235 */
898
+ dll_zone(7, (uint16_t)(uintptr_t)dl_empty); /* 236-242 */
899
+
900
+ /* Palettes (Atari colour byte = hue<<4 | luminance). */
901
+ BACKGRND = 0x00; /* cave black */
902
+ P0C1 = 0x0F; /* title text white */
903
+ P1C1 = 0x96; P1C2 = 0x9C; P1C3 = 0x0F; /* P1 hero blues */
904
+ P2C1 = 0x26; P2C2 = 0x2C; P2C3 = 0x0F; /* P2 hero oranges */
905
+ P3C1 = 0x46; P3C2 = 0x0A; P3C3 = 0x0E; /* spike red + tips */
906
+ P4C1 = 0x1A; P4C2 = 0x18; P4C3 = 0x1E; /* coin gold */
907
+ P5C1 = 0xC9; /* HUD green / ledges / floor */
908
+ P6C1 = 0x24; /* decor band deep red (lava-ish)*/
909
+ P7C1 = 0x28; /* decor band brighter red */
165
910
  CHARBASE = 0;
166
- OFFSET = 0;
911
+ OFFSET = 0; /* must stay 0 (7800 standard) */
167
912
 
168
- dll_addr = (uint16_t)(uintptr_t)dll;
169
- DPPL = (uint8_t)(dll_addr & 0xFF);
170
- DPPH = (uint8_t)(dll_addr >> 8);
913
+ a = (uint16_t)(uintptr_t)dll;
914
+ DPPL = (uint8_t)(a & 0xFF);
915
+ DPPH = (uint8_t)(a >> 8);
171
916
 
172
- CTRL = 0x40;
173
917
  sfx_init();
918
+ hiscore = 0; /* in-session only — see header */
919
+ paint_title(); /* …turns DMA on */
174
920
 
175
921
  for (;;) {
176
- uint8_t pad, btn, grounded;
177
- int16_t py;
922
+ uint8_t pad, f1, f2, fire;
178
923
  vblank_wait();
179
924
  sfx_update();
925
+ music_tick();
926
+
927
+ pad = (uint8_t)~SWCHA;
928
+ f1 = (uint8_t)(!(INPT4 & 0x80));
929
+ f2 = (uint8_t)(!(INPT5 & 0x80));
930
+
931
+ if (state == ST_TITLE) {
932
+ /* ── GAME LOGIC (clay) — title: P1 fire = 1P, P2 fire = 2P turns ── */
933
+ if (f1 && !(prev_fire & 1)) start_game(0);
934
+ else if (f2 && !(prev_fire & 2)) start_game(1);
935
+ prev_fire = (uint8_t)(f1 | (f2 << 1));
936
+ continue;
937
+ }
180
938
 
181
- pad = ~SWCHA;
182
- if (pad & JOY_LEFT && px > 4) { px -= MOVE_PX; set_x((uint8_t)px); }
183
- if (pad & JOY_RIGHT && px < 152) { px += MOVE_PX; set_x((uint8_t)px); }
184
-
185
- py = py16 >> 4;
186
- grounded = (py >= GROUND_Y - 8);
187
- btn = (INPT4 & 0x80) ? 0 : 1;
188
- if (btn && !prev_btn && grounded) { vy = JUMP_VEL; sfx_tone(0, 6, 6); }
189
- prev_btn = btn;
190
-
191
- vy += GRAVITY;
192
- if (vy > MAXFALL) vy = MAXFALL;
193
- if (grounded && vy > 0) { vy = 0; py16 = (GROUND_Y - 8) << 4; }
194
- else {
195
- py16 += vy;
196
- if (py16 < 0) py16 = 0;
197
- if ((py16 >> 4) > GROUND_Y - 8) py16 = (GROUND_Y - 8) << 4;
939
+ if (state == ST_OVER) {
940
+ if (over_lock) { --over_lock; prev_fire = (uint8_t)(f1 | (f2 << 1)); continue; }
941
+ if ((f1 || f2) && !prev_fire) paint_title();
942
+ prev_fire = (uint8_t)(f1 | (f2 << 1));
943
+ continue;
198
944
  }
199
945
 
200
- build_dll((uint8_t)(py16 >> 4));
946
+ /* ── ST_PLAY ───────────────────────────────────────────────────── */
947
+ fire = cur_p ? f2 : f1;
948
+
949
+ if (turn_pause) { /* ready breather, frozen */
950
+ --turn_pause;
951
+ prev_fire = fire;
952
+ } else {
953
+ update_player(pad, fire);
954
+ prev_fire = fire;
955
+ if (state != ST_PLAY) continue; /* kill_player → game over */
956
+ }
957
+
958
+ /* ── GAME LOGIC (clay) — coins (collect) + spikes (death). The hero's
959
+ * AABB is (hx,y8)..(+8,+12); coins/spikes are 8px wide. ── */
960
+ {
961
+ uint8_t y8 = (uint8_t)(hy_q2 >> 2);
962
+ uint8_t hcx = (uint8_t)(hx + 4), hcy = (uint8_t)(y8 + 6);
963
+ for (i = 0; i < NUM_COINS; ++i) {
964
+ if (!coin_live[i]) continue;
965
+ if (dist8((uint8_t)(COIN_X[i] + 4), hcx) < 8 &&
966
+ dist8((uint8_t)(COIN_Y[i] + 3), hcy) < 9) {
967
+ coin_live[i] = 0;
968
+ p_score[cur_p] += 10;
969
+ if (p_score[cur_p] > 99999u) p_score[cur_p] = 99999u;
970
+ fx_coin();
971
+ dirty = 1;
972
+ }
973
+ }
974
+ /* all coins gathered → bonus + reset the board (endless score climb) */
975
+ {
976
+ uint8_t any = 0;
977
+ for (i = 0; i < NUM_COINS; ++i) any |= coin_live[i];
978
+ if (!any) {
979
+ p_score[cur_p] += 50;
980
+ if (p_score[cur_p] > 99999u) p_score[cur_p] = 99999u;
981
+ for (i = 0; i < NUM_COINS; ++i) coin_live[i] = 1;
982
+ dirty = 1;
983
+ }
984
+ }
985
+ if (turn_pause == 0) {
986
+ for (i = 0; i < NUM_SPIKES; ++i) {
987
+ if (dist8((uint8_t)(SPIKE_X[i] + 4), hcx) < 7 &&
988
+ dist8((uint8_t)(SPIKE_Y[i] + 2), hcy) < 7) {
989
+ kill_player();
990
+ break;
991
+ }
992
+ }
993
+ if (state != ST_PLAY) continue;
994
+ }
995
+ }
996
+
997
+ /* ── HARDWARE IDIOM (load-bearing) — the per-frame draw pass:
998
+ * open (clear counts) → emit the LEDGES + every object → close
999
+ * (terminators). Emission order = draw order on shared scanlines, and
1000
+ * when a line is full the LAST emitters get dropped — so the HERO goes
1001
+ * LAST among small objects but the ledges (structural) go first; the
1002
+ * player's own object should never be the one that flickers out, so we
1003
+ * keep ≤3 objects per line by design (the arena is sparse vertically). ── */
1004
+ field_open();
1005
+
1006
+ /* the ledges: structural geometry, emitted first */
1007
+ for (i = 0; i < NUM_LEDGES; ++i)
1008
+ emit_ledge(LEDGES[i].yt, LEDGES[i].xl, LEDGES[i].xr, 5);
1009
+
1010
+ /* coins */
1011
+ for (i = 0; i < NUM_COINS; ++i)
1012
+ if (coin_live[i]) emit_object(COIN_Y[i], 6, GFX_COIN, 2, MODE_COIN, COIN_X[i]);
1013
+
1014
+ /* spikes */
1015
+ for (i = 0; i < NUM_SPIKES; ++i)
1016
+ emit_object(SPIKE_Y[i], 5, GFX_SPIKE, 2, MODE_SPIKE, SPIKE_X[i]);
1017
+
1018
+ /* the hero — blink during the turn-change breather (SHIMMER, never
1019
+ * vanish: on blink ticks draw only the bottom half so the object stays
1020
+ * accounted-for on every frame — a fully-skipped sprite reads as "gone"
1021
+ * in any single sampled frame, the spawn-blink footgun from the gold
1022
+ * round). */
1023
+ {
1024
+ uint8_t y8 = (uint8_t)(hy_q2 >> 2);
1025
+ const uint8_t* g = face_jump ? GFX_HERO_JUMP : GFX_HERO_IDLE;
1026
+ uint8_t mode = cur_p ? MODE_HERO2 : MODE_HERO1;
1027
+ if (turn_pause && (turn_pause & 4))
1028
+ emit_object((uint8_t)(y8 + 6), 6, g + 12, 2, mode, hx);
1029
+ else
1030
+ emit_object(y8, HERO_H, g, 2, mode, hx);
1031
+ }
1032
+
1033
+ field_close();
1034
+
1035
+ if (dirty) draw_hud();
201
1036
  }
202
1037
  }