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.
- package/AGENTS.md +51 -41
- package/CHANGELOG.md +46 -0
- package/README.md +3 -3
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1225 -332
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +906 -275
- package/examples/atari2600/templates/shmup.asm +1031 -239
- package/examples/atari2600/templates/sports.asm +1135 -253
- package/examples/atari7800/templates/platformer.c +991 -156
- package/examples/atari7800/templates/puzzle.c +1091 -148
- package/examples/atari7800/templates/racing.c +952 -124
- package/examples/atari7800/templates/shmup.c +812 -134
- package/examples/atari7800/templates/sports.c +820 -184
- package/examples/c64/templates/platformer.c +879 -164
- package/examples/c64/templates/puzzle.c +855 -178
- package/examples/c64/templates/racing.c +873 -97
- package/examples/c64/templates/shmup.c +757 -161
- package/examples/c64/templates/sports.c +755 -100
- package/examples/gb/templates/platformer.c +841 -179
- package/examples/gb/templates/puzzle.c +986 -246
- package/examples/gb/templates/racing.c +754 -174
- package/examples/gb/templates/shmup.c +673 -175
- package/examples/gb/templates/sports.c +790 -159
- package/examples/gba/templates/platformer.c +626 -165
- package/examples/gba/templates/puzzle.c +519 -269
- package/examples/gba/templates/racing.c +511 -206
- package/examples/gba/templates/shmup.c +564 -179
- package/examples/gba/templates/sports.c +454 -174
- package/examples/gbc/templates/platformer.c +944 -180
- package/examples/gbc/templates/puzzle.c +363 -109
- package/examples/gbc/templates/racing.c +884 -180
- package/examples/gbc/templates/shmup.c +821 -185
- package/examples/gbc/templates/sports.c +870 -162
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +694 -261
- package/examples/genesis/templates/racing.c +726 -203
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +880 -215
- package/examples/gg/templates/puzzle.c +875 -216
- package/examples/gg/templates/racing.c +915 -172
- package/examples/gg/templates/shmup.c +714 -191
- package/examples/gg/templates/sports.c +732 -129
- package/examples/lynx/templates/platformer.c +604 -69
- package/examples/lynx/templates/puzzle.c +498 -158
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +458 -131
- package/examples/lynx/templates/sports.c +496 -72
- package/examples/msx/platformer/main.c +649 -162
- package/examples/msx/puzzle/main.c +742 -240
- package/examples/msx/racing/main.c +669 -178
- package/examples/msx/shmup/main.c +460 -178
- package/examples/msx/sports/main.c +592 -126
- package/examples/nes/templates/platformer.c +589 -171
- package/examples/nes/templates/puzzle.c +563 -242
- package/examples/nes/templates/racing.c +502 -208
- package/examples/nes/templates/shmup.c +339 -145
- package/examples/nes/templates/sports.c +341 -183
- package/examples/pce/platformer/main.c +874 -205
- package/examples/pce/puzzle/main.c +802 -287
- package/examples/pce/racing/main.c +783 -208
- package/examples/pce/shmup/main.c +638 -212
- package/examples/pce/sports/main.c +586 -169
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +762 -177
- package/examples/sms/templates/puzzle.c +752 -212
- package/examples/sms/templates/racing.c +808 -145
- package/examples/sms/templates/shmup.c +599 -162
- package/examples/sms/templates/sports.c +630 -122
- package/examples/snes/templates/music_demo.c +7 -0
- package/examples/snes/templates/platformer-data.asm +123 -24
- package/examples/snes/templates/platformer-hdr.asm +57 -0
- package/examples/snes/templates/platformer.c +586 -165
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +614 -235
- package/examples/snes/templates/racing-data.asm +390 -32
- package/examples/snes/templates/racing-hdr.asm +57 -0
- package/examples/snes/templates/racing.c +807 -196
- package/examples/snes/templates/shmup-data.asm +87 -29
- package/examples/snes/templates/shmup-hdr.asm +57 -0
- package/examples/snes/templates/shmup.c +459 -198
- package/examples/snes/templates/sports-data.asm +48 -2
- package/examples/snes/templates/sports-hdr.asm +57 -0
- package/examples/snes/templates/sports.c +414 -163
- package/package.json +1 -1
- package/src/host/LibretroHost.js +59 -1
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/frame.js +3 -2
- package/src/mcp/tools/index.js +3 -3
- package/src/mcp/tools/input.js +5 -4
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1098 -130
- package/src/mcp/tools/rom-id.js +5 -1
- package/src/mcp/tools/run-until.js +4 -2
- package/src/mcp/tools/snippets.js +6 -6
- package/src/mcp/tools/sprite-pipeline.js +14 -2
- package/src/mcp/tools/state.js +2 -1
- package/src/mcp/tools/tile-inspect.js +8 -1
- package/src/mcp/tools/toolchain.js +12 -1
- package/src/mcp/tools/watch-memory.js +4 -3
- package/src/observer/bus.js +73 -0
- package/src/observer/livestream.html +4 -2
- package/src/observer/tool-wrap.js +17 -14
- package/src/platforms/atari7800/MENTAL_MODEL.md +5 -5
- package/src/platforms/atari7800/TROUBLESHOOTING.md +5 -5
- package/src/platforms/c64/MENTAL_MODEL.md +11 -4
- package/src/platforms/c64/TROUBLESHOOTING.md +13 -0
- package/src/platforms/gb/MENTAL_MODEL.md +3 -3
- package/src/platforms/gb/TROUBLESHOOTING.md +61 -8
- package/src/platforms/gb/lib/c/README.md +10 -11
- package/src/platforms/gb/lib/c/gb_crt0.s +27 -3
- package/src/platforms/gb/lib/c/patch-header.js +13 -3
- package/src/platforms/gba/MENTAL_MODEL.md +4 -4
- package/src/platforms/gba/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gba/lib/c/gba_sfx.c +40 -0
- package/src/platforms/gba/lib/c/gba_sfx.h +10 -0
- package/src/platforms/gbc/MENTAL_MODEL.md +4 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +4 -4
- package/src/platforms/gbc/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gbc/lib/c/README.md +10 -11
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +13 -3
- package/src/platforms/genesis/MENTAL_MODEL.md +3 -3
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +3 -3
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/joypad_read.c +29 -0
- package/src/platforms/lynx/MENTAL_MODEL.md +1 -1
- package/src/platforms/lynx/TROUBLESHOOTING.md +3 -3
- package/src/platforms/msx/MENTAL_MODEL.md +5 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +2 -2
- package/src/platforms/msx/lib/c/msx_hw.h +1 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +25 -0
- package/src/platforms/nes/MENTAL_MODEL.md +2 -2
- package/src/platforms/nes/lib/c/nes_runtime.c +149 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +34 -1
- package/src/platforms/pce/MENTAL_MODEL.md +5 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +1 -1
- package/src/platforms/pce/lib/c/pce_hw.h +11 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +6 -6
- package/src/platforms/snes/MENTAL_MODEL.md +2 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.cfg +13 -8
- package/src/toolchains/cc65/presets/nes/chr-ram-runtime.crt0.s +58 -5
- package/src/toolchains/cc65/presets/nes/chr-rom.crt0.s +52 -3
- package/src/toolchains/cc65/presets/pce/rom32k.cfg +52 -0
- package/src/toolchains/index.js +27 -11
|
@@ -1,177 +1,1005 @@
|
|
|
1
|
-
/* racing.c — Atari 7800 top-down
|
|
1
|
+
/* ── racing.c — Atari 7800 top-down road racer (complete example game) ────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
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
|
|
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
|
|
29
|
-
|
|
30
|
-
/* SWCHA bit order is Right(0x80)/Left(0x40)/Down(0x20)/Up(0x10) — the
|
|
31
|
-
* old 0x20/0x10 masks here were the DOWN/UP bits, so the stick's
|
|
32
|
-
* vertical axis steered horizontally. */
|
|
33
|
-
#define JOY_LEFT 0x40
|
|
34
|
-
#define JOY_RIGHT 0x80
|
|
35
|
-
|
|
36
|
-
/* 16-pixel-wide (= 4 bytes in 160A) × 8 row car sprite. */
|
|
37
|
-
static const uint8_t car_row0[4] = { 0x05, 0x55, 0x55, 0x50 };
|
|
38
|
-
static const uint8_t car_row1[4] = { 0x5A, 0x5A, 0xA5, 0xA5 };
|
|
39
|
-
static const uint8_t car_row2[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
|
|
40
|
-
static const uint8_t car_row3[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
|
|
41
|
-
static const uint8_t car_row4[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
|
|
42
|
-
static const uint8_t car_row5[4] = { 0xAA, 0xFF, 0xFF, 0xAA };
|
|
43
|
-
static const uint8_t car_row6[4] = { 0x5A, 0x5A, 0xA5, 0xA5 };
|
|
44
|
-
static const uint8_t car_row7[4] = { 0x05, 0x55, 0x55, 0x50 };
|
|
45
|
-
|
|
46
|
-
#define MK_DL(name) static uint8_t name[7] = { 0, 0x40, 0, 0x1C, 80, 0, 0 }
|
|
47
|
-
MK_DL(dl_row0); MK_DL(dl_row1); MK_DL(dl_row2); MK_DL(dl_row3);
|
|
48
|
-
MK_DL(dl_row4); MK_DL(dl_row5); MK_DL(dl_row6); MK_DL(dl_row7);
|
|
118
|
+
#define P7C1 (*(volatile uint8_t*)0x3D)
|
|
49
119
|
|
|
50
|
-
|
|
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)
|
|
129
|
+
|
|
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) */
|
|
133
|
+
|
|
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
|
|
152
|
+
|
|
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[] */
|
|
51
182
|
|
|
52
|
-
/* ──
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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)*/
|
|
62
203
|
};
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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) */
|
|
70
216
|
};
|
|
71
217
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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 */
|
|
224
|
+
|
|
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 */
|
|
270
|
+
};
|
|
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,
|
|
275
|
+
};
|
|
276
|
+
|
|
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
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
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
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
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];
|
|
543
|
+
|
|
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;
|
|
560
|
+
|
|
561
|
+
#define ST_TITLE 0
|
|
562
|
+
#define ST_PLAY 1
|
|
563
|
+
#define ST_OVER 2
|
|
564
|
+
static uint8_t state;
|
|
565
|
+
|
|
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;
|
|
76
573
|
}
|
|
77
574
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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);
|
|
81
580
|
}
|
|
82
581
|
|
|
83
|
-
|
|
84
|
-
|
|
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;
|
|
602
|
+
}
|
|
85
603
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
static
|
|
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;
|
|
626
|
+
}
|
|
627
|
+
line_used[i] = off; /* cars emit AFTER the road bytes */
|
|
628
|
+
}
|
|
629
|
+
}
|
|
89
630
|
|
|
90
|
-
|
|
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
|
+
}
|
|
91
649
|
|
|
92
|
-
static void
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
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);
|
|
96
655
|
}
|
|
97
656
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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 */
|
|
102
696
|
}
|
|
103
697
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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;
|
|
107
727
|
}
|
|
108
728
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
case 3: dl = (uint16_t)(uintptr_t)dl_row3; break;
|
|
119
|
-
case 4: dl = (uint16_t)(uintptr_t)dl_row4; break;
|
|
120
|
-
case 5: dl = (uint16_t)(uintptr_t)dl_row5; break;
|
|
121
|
-
case 6: dl = (uint16_t)(uintptr_t)dl_row6; break;
|
|
122
|
-
case 7: dl = (uint16_t)(uintptr_t)dl_row7; break;
|
|
123
|
-
default: dl = bg_zone_dl(i); break;
|
|
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;
|
|
124
738
|
}
|
|
125
|
-
set_dll_entry(i, dl);
|
|
126
739
|
}
|
|
127
740
|
}
|
|
128
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
|
+
|
|
129
811
|
static void vblank_wait(void) {
|
|
130
|
-
while (MSTAT & 0x80) { }
|
|
131
|
-
while (!(MSTAT & 0x80)) { }
|
|
812
|
+
while (MSTAT & 0x80) { } /* leave the current vblank */
|
|
813
|
+
while (!(MSTAT & 0x80)) { } /* catch the next one starting */
|
|
132
814
|
}
|
|
133
815
|
|
|
134
816
|
void main(void) {
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
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 */
|
|
158
877
|
CHARBASE = 0;
|
|
159
|
-
OFFSET
|
|
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);
|
|
160
883
|
|
|
161
|
-
dll_addr = (uint16_t)(uintptr_t)dll;
|
|
162
|
-
DPPL = (uint8_t)(dll_addr & 0xFF);
|
|
163
|
-
DPPH = (uint8_t)(dll_addr >> 8);
|
|
164
|
-
CTRL = 0x40;
|
|
165
884
|
sfx_init();
|
|
885
|
+
best = 0; /* in-session only — see header */
|
|
886
|
+
paint_title(); /* …turns DMA on */
|
|
166
887
|
|
|
167
888
|
for (;;) {
|
|
168
|
-
uint8_t pad;
|
|
889
|
+
uint8_t pad, f1, f2, pr0, pr1;
|
|
169
890
|
vblank_wait();
|
|
170
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();
|
|
171
1001
|
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
if ((pad & JOY_RIGHT) && !(prev_pad & JOY_RIGHT) && lane < LANES - 1) { lane++; set_x(lane_xs[lane]); sfx_tone(0, 6, 4); }
|
|
175
|
-
prev_pad = pad;
|
|
1002
|
+
if (dirty) draw_hud();
|
|
1003
|
+
pf0 = f1; pf1 = f2;
|
|
176
1004
|
}
|
|
177
1005
|
}
|