romdevtools 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +56 -44
- package/CHANGELOG.md +355 -0
- package/README.md +4 -4
- package/examples/README.md +7 -7
- package/examples/atari2600/templates/platformer.asm +1227 -325
- package/examples/atari2600/templates/puzzle.asm +1056 -0
- package/examples/atari2600/templates/racing.asm +909 -257
- package/examples/atari2600/templates/shmup.asm +1035 -218
- package/examples/atari2600/templates/sports.asm +1143 -229
- package/examples/atari7800/templates/hello_sprite.c +8 -4
- package/examples/atari7800/templates/platformer.c +991 -152
- package/examples/atari7800/templates/puzzle.c +1091 -145
- package/examples/atari7800/templates/racing.c +949 -118
- package/examples/atari7800/templates/shmup.c +812 -130
- package/examples/atari7800/templates/sports.c +820 -181
- package/examples/c64/templates/platformer.c +876 -157
- package/examples/c64/templates/puzzle.c +881 -143
- package/examples/c64/templates/racing.c +873 -88
- package/examples/c64/templates/shmup.c +762 -154
- package/examples/c64/templates/sports.c +755 -95
- package/examples/gb/templates/platformer.c +841 -175
- package/examples/gb/templates/puzzle.c +1094 -176
- package/examples/gb/templates/racing.c +761 -169
- package/examples/gb/templates/shmup.c +679 -169
- package/examples/gb/templates/sports.c +790 -153
- package/examples/gba/templates/platformer.c +624 -169
- package/examples/gba/templates/puzzle.c +535 -207
- package/examples/gba/templates/racing.c +513 -196
- package/examples/gba/templates/shmup.c +565 -168
- package/examples/gba/templates/sports.c +454 -162
- package/examples/gbc/templates/platformer.c +944 -176
- package/examples/gbc/templates/puzzle.c +1131 -177
- package/examples/gbc/templates/racing.c +891 -175
- package/examples/gbc/templates/shmup.c +827 -179
- package/examples/gbc/templates/sports.c +870 -156
- package/examples/genesis/templates/platformer.c +747 -129
- package/examples/genesis/templates/puzzle.c +702 -208
- package/examples/genesis/templates/racing.c +728 -193
- package/examples/genesis/templates/shmup.c +535 -142
- package/examples/genesis/templates/shmup_2p.c +13 -1
- package/examples/genesis/templates/sports.c +495 -158
- package/examples/gg/templates/platformer.c +883 -214
- package/examples/gg/templates/puzzle.c +906 -181
- package/examples/gg/templates/racing.c +919 -160
- package/examples/gg/templates/shmup.c +716 -177
- package/examples/gg/templates/sports.c +735 -128
- package/examples/lynx/templates/platformer.c +604 -50
- package/examples/lynx/templates/puzzle.c +533 -130
- package/examples/lynx/templates/racing.c +538 -102
- package/examples/lynx/templates/shmup.c +461 -122
- package/examples/lynx/templates/sports.c +496 -69
- package/examples/msx/platformer/main.c +648 -159
- package/examples/msx/puzzle/main.c +750 -185
- package/examples/msx/racing/main.c +669 -177
- package/examples/msx/shmup/main.c +460 -177
- package/examples/msx/sports/main.c +591 -124
- package/examples/nes/templates/platformer.c +586 -160
- package/examples/nes/templates/puzzle.c +603 -222
- package/examples/nes/templates/racing.c +505 -197
- package/examples/nes/templates/shmup.c +339 -144
- package/examples/nes/templates/sports.c +341 -182
- package/examples/pce/platformer/main.c +875 -204
- package/examples/pce/puzzle/main.c +797 -216
- package/examples/pce/racing/main.c +782 -206
- package/examples/pce/shmup/main.c +638 -211
- package/examples/pce/sports/main.c +585 -167
- package/examples/porting-across-platforms/README.md +1 -1
- package/examples/sms/templates/platformer.c +765 -176
- package/examples/sms/templates/puzzle.c +783 -177
- package/examples/sms/templates/racing.c +812 -133
- package/examples/sms/templates/shmup.c +601 -148
- package/examples/sms/templates/shmup_2p.c +17 -1
- package/examples/sms/templates/sports.c +633 -121
- 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 +587 -149
- package/examples/snes/templates/puzzle-data.asm +116 -21
- package/examples/snes/templates/puzzle-hdr.asm +57 -0
- package/examples/snes/templates/puzzle.c +632 -185
- 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 -177
- 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 -180
- 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 -156
- package/package.json +12 -12
- package/src/cores/wasm/bluemsx_libretro.js +1 -1
- package/src/cores/wasm/bluemsx_libretro.wasm +0 -0
- package/src/cores/wasm/fceumm_libretro.js +1 -1
- package/src/cores/wasm/fceumm_libretro.wasm +0 -0
- package/src/cores/wasm/gambatte_libretro.js +1 -1
- package/src/cores/wasm/gambatte_libretro.wasm +0 -0
- package/src/cores/wasm/geargrafx_libretro.js +1 -1
- package/src/cores/wasm/geargrafx_libretro.wasm +0 -0
- package/src/cores/wasm/genesis_plus_gx_libretro.js +1 -1
- package/src/cores/wasm/genesis_plus_gx_libretro.wasm +0 -0
- package/src/cores/wasm/handy_libretro.js +1 -1
- package/src/cores/wasm/handy_libretro.wasm +0 -0
- package/src/cores/wasm/mgba_libretro.js +1 -1
- package/src/cores/wasm/mgba_libretro.wasm +0 -0
- package/src/cores/wasm/prosystem_libretro.js +1 -1
- package/src/cores/wasm/prosystem_libretro.wasm +0 -0
- package/src/cores/wasm/snes9x_libretro.js +1 -1
- package/src/cores/wasm/snes9x_libretro.wasm +0 -0
- package/src/cores/wasm/stella2014_libretro.js +1 -1
- package/src/cores/wasm/stella2014_libretro.wasm +0 -0
- package/src/cores/wasm/vice_x64_libretro.js +1 -1
- package/src/cores/wasm/vice_x64_libretro.wasm +0 -0
- package/src/host/LibretroHost.js +304 -11
- package/src/http/tool-registry.js +11 -11
- package/src/mcp/server.js +6 -0
- package/src/mcp/tools/cheats.js +2 -1
- package/src/mcp/tools/disasm-rebuild.js +315 -65
- package/src/mcp/tools/disasm.js +149 -28
- package/src/mcp/tools/find-references.js +216 -51
- package/src/mcp/tools/frame.js +14 -6
- package/src/mcp/tools/index.js +18 -4
- package/src/mcp/tools/input.js +31 -7
- package/src/mcp/tools/lifecycle.js +6 -4
- package/src/mcp/tools/memory.js +208 -39
- package/src/mcp/tools/platform-docs.js +1 -1
- package/src/mcp/tools/playtest.js +56 -4
- package/src/mcp/tools/preview-tile.js +6 -2
- package/src/mcp/tools/project.js +1114 -120
- 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 +55 -11
- package/src/mcp/tools/watch-memory.js +145 -27
- 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/_guides/ROMHACKING_PLAYBOOK.md +64 -17
- package/src/platforms/atari2600/MENTAL_MODEL.md +5 -1
- package/src/platforms/atari2600/TROUBLESHOOTING.md +40 -0
- package/src/platforms/atari7800/MENTAL_MODEL.md +32 -11
- 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 +19 -4
- package/src/platforms/gb/TROUBLESHOOTING.md +101 -6
- 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 +19 -6
- 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 +16 -4
- package/src/platforms/gbc/TROUBLESHOOTING.md +24 -3
- 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/font.h +43 -0
- package/src/platforms/gbc/lib/c/gb_crt0.s +26 -3
- package/src/platforms/gbc/lib/c/patch-header.js +19 -6
- package/src/platforms/genesis/MENTAL_MODEL.md +43 -9
- package/src/platforms/genesis/TROUBLESHOOTING.md +2 -2
- package/src/platforms/genesis/lib/c/genesis_sfx.c +37 -0
- package/src/platforms/genesis/lib/c/genesis_sfx.h +1 -0
- package/src/platforms/gg/MENTAL_MODEL.md +4 -4
- package/src/platforms/gg/TROUBLESHOOTING.md +14 -18
- package/src/platforms/gg/UPSTREAM_SOURCES.md +1 -1
- package/src/platforms/gg/lib/c/gg_crt0.s +14 -2
- 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/lynx/lib/c/lynx_sfx.c +38 -2
- package/src/platforms/lynx/lib/c/lynx_sfx.h +1 -0
- package/src/platforms/msx/MENTAL_MODEL.md +11 -5
- package/src/platforms/msx/TROUBLESHOOTING.md +21 -0
- package/src/platforms/msx/lib/c/msx_crt0.s +27 -0
- package/src/platforms/msx/lib/c/msx_hw.h +3 -0
- package/src/platforms/msx/lib/c/msx_vdp.c +70 -0
- package/src/platforms/nes/MENTAL_MODEL.md +12 -5
- package/src/platforms/nes/lib/c/nes_runtime.c +190 -34
- package/src/platforms/nes/lib/c/nes_runtime.h +35 -0
- package/src/platforms/pce/MENTAL_MODEL.md +14 -5
- package/src/platforms/pce/TROUBLESHOOTING.md +9 -0
- package/src/platforms/pce/lib/c/pce_hw.h +13 -1
- package/src/platforms/pce/lib/c/pce_sound.c +22 -0
- package/src/platforms/pce/lib/c/pce_video.c +32 -0
- package/src/platforms/sms/MENTAL_MODEL.md +11 -6
- package/src/platforms/sms/TROUBLESHOOTING.md +6 -0
- package/src/platforms/sms/lib/c/sms_crt0.s +14 -2
- package/src/platforms/snes/MENTAL_MODEL.md +7 -2
- package/src/platforms/snes/TROUBLESHOOTING.md +40 -1
- package/src/playtest/playtest.js +73 -3
- 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 +64 -19
|
@@ -1,242 +1,881 @@
|
|
|
1
|
-
/* sports.c — Atari 7800
|
|
1
|
+
/* ── sports.c — Atari 7800 versus court game (complete example) ──────────────
|
|
2
2
|
*
|
|
3
|
-
*
|
|
4
|
-
*
|
|
3
|
+
* FLUX FENCE — a COMPLETE, working game: title screen, 1P vs a beatable CPU
|
|
4
|
+
* and 2P SIMULTANEOUS VERSUS (P2 on JOYSTICK PORT 1), first-to-5 match flow
|
|
5
|
+
* with a result screen, two-voice TIA music + SFX, and an in-session record
|
|
6
|
+
* (longest win streak vs the CPU). It's the Pong lineage rebuilt on MARIA: the
|
|
7
|
+
* two paddles, the ball, and the centre net are all just display-list OBJECTS
|
|
8
|
+
* MARIA DMAs per scanline — the same per-line object pool the 7800 shmup uses
|
|
9
|
+
* for a swarm, here spent on a sparse court (≤2 objects ever share a line, so
|
|
10
|
+
* the whole frame sits comfortably inside the MARIA DMA budget).
|
|
5
11
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
13
|
+
* very different one. The markers tell you what's what:
|
|
14
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented 7800/MARIA footgun;
|
|
15
|
+
* reshape your gameplay around it (see TROUBLESHOOTING before changing).
|
|
16
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
17
|
+
* reshape freely.
|
|
12
18
|
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
+
* PERSISTENCE — honest note: the canonical 7800 save path is the High Score
|
|
28
|
+
* Cart (HSC): a pass-through cartridge with 2KB battery RAM at $1000-$17FF
|
|
29
|
+
* plus a directory ROM. The bundled prosystem core does NOT implement HSC
|
|
30
|
+
* (probed 2026-06: retro_get_memory(SAVE_RAM) size = 0, and the core binary
|
|
31
|
+
* has no HSC code at all), so this game keeps its RECORD IN-SESSION ONLY (it
|
|
32
|
+
* survives play → title → play, dies on power-off). For a VERSUS game a raw
|
|
33
|
+
* hi-score is meaningless (every match ends 5-x), so the record we keep is the
|
|
34
|
+
* longest 1P win streak vs the CPU — the stat a returning player chases. 2P
|
|
35
|
+
* matches never touch it (humans beating each other isn't a record). Do not
|
|
36
|
+
* fake persistence the hardware path can't back — if a future core round adds
|
|
37
|
+
* HSC, wire best_streak into $1000-$17FF and it becomes real.
|
|
38
|
+
*
|
|
39
|
+
* Frame budget (NTSC): the per-tick update is tiny — two paddle moves + one
|
|
40
|
+
* ball step + two AABB paddle tests + a couple of HUD digits. The per-frame
|
|
41
|
+
* draw pass re-emits only the net, two paddles, and the ball (≤2 objects on
|
|
42
|
+
* any one scanline), well inside one 60Hz frame. MARIA does not care how far
|
|
43
|
+
* the CPU falls behind — it re-walks the same display lists at 60Hz — but that
|
|
44
|
+
* budget only holds because of the #pragma optimize(on) right below: read its
|
|
45
|
+
* comment before deleting it.
|
|
16
46
|
*/
|
|
47
|
+
|
|
17
48
|
#include <stdint.h>
|
|
49
|
+
#include <string.h>
|
|
18
50
|
#include "atari7800_sfx.h"
|
|
19
51
|
|
|
52
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
53
|
+
* cc65 SHIPS WITH ITS OPTIMIZER OFF, and this toolchain does not pass -O —
|
|
54
|
+
* each translation unit must opt in. Without this pragma the unoptimized
|
|
55
|
+
* emit pass made the main loop take ~9 frames per sim tick instead of 1-2
|
|
56
|
+
* (measured on the 7800 shmup: 8.8 → 1.7 frames/tick on prosystem), and
|
|
57
|
+
* every TICK-DENOMINATED timer silently stretched 4-5x in wall-clock terms:
|
|
58
|
+
* the serve pause, the result-screen lock, the ball speed — all ~4.5x too
|
|
59
|
+
* slow, so the ball crawled and the game "looked broken". But the DLL, the
|
|
60
|
+
* zone pointers, and every pool slot were byte-perfect when read back from
|
|
61
|
+
* RAM. The footgun generalizes: on a 1.79MHz 6502 the C optimizer is not a
|
|
62
|
+
* nicety, it IS the frame budget, and a too-slow loop shows up as broken GAME
|
|
63
|
+
* RULES (a sluggish ball, missed 1-frame input edges), not as a slow-looking
|
|
64
|
+
* screen — MARIA keeps repainting the same display lists at a rock-steady
|
|
65
|
+
* 60Hz no matter how far behind the CPU falls. If your fork feels like
|
|
66
|
+
* molasses or "ignores" short button taps, check this pragma is still here
|
|
67
|
+
* before debugging the display lists. */
|
|
68
|
+
#pragma optimize(on)
|
|
69
|
+
|
|
70
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
71
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
72
|
+
#define GAME_TITLE "FLUX FENCE"
|
|
73
|
+
|
|
74
|
+
/* ── MARIA + TIA + RIOT registers (full list in MENTAL_MODEL.md) ── */
|
|
20
75
|
#define BACKGRND (*(volatile uint8_t*)0x20)
|
|
21
76
|
#define P0C1 (*(volatile uint8_t*)0x21)
|
|
22
77
|
#define P0C2 (*(volatile uint8_t*)0x22)
|
|
23
78
|
#define P0C3 (*(volatile uint8_t*)0x23)
|
|
24
79
|
#define P1C1 (*(volatile uint8_t*)0x25)
|
|
25
|
-
#define
|
|
80
|
+
#define P1C2 (*(volatile uint8_t*)0x26)
|
|
81
|
+
#define P1C3 (*(volatile uint8_t*)0x27)
|
|
26
82
|
#define MSTAT (*(volatile uint8_t*)0x28)
|
|
83
|
+
#define P2C1 (*(volatile uint8_t*)0x29)
|
|
84
|
+
#define P2C2 (*(volatile uint8_t*)0x2A)
|
|
85
|
+
#define P2C3 (*(volatile uint8_t*)0x2B)
|
|
27
86
|
#define DPPH (*(volatile uint8_t*)0x2C)
|
|
87
|
+
#define P3C1 (*(volatile uint8_t*)0x2D)
|
|
88
|
+
#define P3C2 (*(volatile uint8_t*)0x2E)
|
|
89
|
+
#define P3C3 (*(volatile uint8_t*)0x2F)
|
|
28
90
|
#define DPPL (*(volatile uint8_t*)0x30)
|
|
91
|
+
#define P4C1 (*(volatile uint8_t*)0x31)
|
|
92
|
+
#define P4C2 (*(volatile uint8_t*)0x32)
|
|
93
|
+
#define P4C3 (*(volatile uint8_t*)0x33)
|
|
29
94
|
#define CHARBASE (*(volatile uint8_t*)0x34)
|
|
95
|
+
#define P5C1 (*(volatile uint8_t*)0x35)
|
|
30
96
|
#define OFFSET (*(volatile uint8_t*)0x38)
|
|
97
|
+
#define P6C1 (*(volatile uint8_t*)0x39)
|
|
31
98
|
#define CTRL (*(volatile uint8_t*)0x3C)
|
|
32
|
-
#define
|
|
33
|
-
|
|
34
|
-
#define P1_UP 0x80
|
|
35
|
-
#define P1_DOWN 0x40
|
|
36
|
-
#define P2_UP 0x08
|
|
37
|
-
#define P2_DOWN 0x04
|
|
38
|
-
|
|
39
|
-
#define PADDLE_W_BYTES 1 /* 1 byte = 4 px wide in 160A */
|
|
40
|
-
#define PADDLE_H 16
|
|
41
|
-
#define BALL_H 4
|
|
42
|
-
#define PADDLE_X1 16
|
|
43
|
-
#define PADDLE_X2 140
|
|
44
|
-
#define PLAY_LINES 100
|
|
45
|
-
#define COURT_TOP 60 /* DLL index */
|
|
46
|
-
#define COURT_BOT (COURT_TOP + PLAY_LINES)
|
|
47
|
-
|
|
48
|
-
/* Sprite rows. 0x55 = all 4 pixels = palette index 1 (lit). */
|
|
49
|
-
static const uint8_t solid_row[1] = { 0x55 };
|
|
50
|
-
|
|
51
|
-
/* Scanline DL pool — one DL per scanline in the play area. Each DL
|
|
52
|
-
* has slots for up to 3 objects (5 bytes each) + 2 terminator bytes
|
|
53
|
-
* (1 for the "next entry's mode = 0", 1 for safety). */
|
|
54
|
-
/* Per-line DL capacity: paddles+ball never share a scanline since
|
|
55
|
-
* they're horizontally separated, so MAX_OBJS=3 fits with margin.
|
|
56
|
-
* 17 bytes/line × 100 court lines = 1700 B + 729 B DLL = 2429 B —
|
|
57
|
-
* out of the 7800's 2 KB RAM1, doesn't fit. Use 100 lines × 12B
|
|
58
|
-
* (MAX=2) = 1200 B + 729 = 1929 B. Fits within 2112 B RAM1. */
|
|
59
|
-
#define MAX_OBJS_PER_LINE 2
|
|
60
|
-
#define DL_BYTES_PER_LINE (5 * MAX_OBJS_PER_LINE + 2)
|
|
61
|
-
static uint8_t scanline_dls[PLAY_LINES * DL_BYTES_PER_LINE];
|
|
99
|
+
#define P7C1 (*(volatile uint8_t*)0x3D)
|
|
62
100
|
|
|
63
|
-
|
|
101
|
+
/* TIA audio (shared with the music player below; atari7800_sfx.c has the
|
|
102
|
+
* same defines — the chip is tiny enough that duplicating 6 lines beats a
|
|
103
|
+
* header dependency the fork machinery would have to carry). */
|
|
104
|
+
#define AUDC0 (*(volatile uint8_t*)0x15)
|
|
105
|
+
#define AUDC1 (*(volatile uint8_t*)0x16)
|
|
106
|
+
#define AUDF0 (*(volatile uint8_t*)0x17)
|
|
107
|
+
#define AUDF1 (*(volatile uint8_t*)0x18)
|
|
108
|
+
#define AUDV0 (*(volatile uint8_t*)0x19)
|
|
109
|
+
#define AUDV1 (*(volatile uint8_t*)0x1A)
|
|
110
|
+
|
|
111
|
+
#define SWCHA (*(volatile uint8_t*)0x280)
|
|
112
|
+
#define INPT4 (*(volatile uint8_t*)0x0C) /* P1 fire, active low (bit 7) */
|
|
113
|
+
#define INPT5 (*(volatile uint8_t*)0x0D) /* P2 fire, active low (bit 7) */
|
|
114
|
+
|
|
115
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
116
|
+
* SWCHA joystick bit order — the #1 7800 input footgun. After the ~SWCHA
|
|
117
|
+
* invert, port 0 (left jack) lives in the HIGH nibble as
|
|
118
|
+
* Right($80) Left($40) Down($20) Up($10), and port 1 (right jack) in the
|
|
119
|
+
* LOW nibble as Right($08) Left($04) Down($02) Up($01). Writing the masks
|
|
120
|
+
* in "natural reading order" (UP=0x80…) is exactly REVERSED and makes the
|
|
121
|
+
* stick's vertical axis steer horizontally — a bug weird enough to
|
|
122
|
+
* misdiagnose as a core problem. Verified bit-by-bit against prosystem.
|
|
123
|
+
* 2P versus uses BOTH ports: player 0 (left paddle) reads the high nibble +
|
|
124
|
+
* INPT4 fire, player 1 (right paddle) the low nibble + INPT5 fire. */
|
|
125
|
+
#define J1_RIGHT 0x80
|
|
126
|
+
#define J1_LEFT 0x40
|
|
127
|
+
#define J1_DOWN 0x20
|
|
128
|
+
#define J1_UP 0x10
|
|
129
|
+
#define J2_RIGHT 0x08
|
|
130
|
+
#define J2_LEFT 0x04
|
|
131
|
+
#define J2_DOWN 0x02
|
|
132
|
+
#define J2_UP 0x01
|
|
133
|
+
|
|
134
|
+
/* ════════════════════════════════════════════════════════════════════════
|
|
135
|
+
* RAM MAP — the 7800 gives you 4KB ($1800-$27FF) and the stock cc65 config
|
|
136
|
+
* only hands the linker the first 2112 bytes of it:
|
|
137
|
+
*
|
|
138
|
+
* $1800-$203F RAM1 — cc65 DATA + BSS (everything `static` below)
|
|
139
|
+
* $2040-$20FF (gap the cc65 cfg skips — unused here)
|
|
140
|
+
* $2100-$213F RAM2 — unused here
|
|
141
|
+
* $2200-$25FD RAM3 bottom — OUR display-list pool/canvas arena (POOLB):
|
|
142
|
+
* raw pointer, invisible to the linker, 1022 bytes
|
|
143
|
+
* $25FE-$27FF RAM3 top — cc65 C parameter stack (crt0 starts it at $2800
|
|
144
|
+
* growing DOWN; ~510 bytes is plenty for these call depths,
|
|
145
|
+
* but if you add deep recursion, shrink POOLB_LINES first)
|
|
146
|
+
* ════════════════════════════════════════════════════════════════════════ */
|
|
147
|
+
#define POOLB ((uint8_t*)0x2200)
|
|
148
|
+
|
|
149
|
+
/* ── Screen layout (243 NTSC zone-lines; the visible frame is ~lines 9-232) ──
|
|
150
|
+
* lines 0- 15 blank (top overscan) 1 DLL entry, 16 tall
|
|
151
|
+
* lines 16- 23 HUD text row (RAM canvas) 8 entries, 1 tall each
|
|
152
|
+
* lines 24- 25 TOP RAIL band (court boundary) 1 entry, 2 tall
|
|
153
|
+
* lines 26-145 THE COURT — 120 one-line zones 120 entries (the pool)
|
|
154
|
+
* lines 146-147 BOTTOM RAIL band (court bound.) 1 entry, 2 tall
|
|
155
|
+
* lines 148-242 decor stripes (cabinet glow) 12 entries, 8/7 tall
|
|
156
|
+
* Total: 143 DLL entries = 429 bytes (vs 729 for the naive all-1-line DLL —
|
|
157
|
+
* mixed zone heights are how real 7800 games keep the DLL small).
|
|
158
|
+
* The COURT pool holds the moving objects: the centre net, the two paddles,
|
|
159
|
+
* and the ball — every one of them a display-list object (no tilemap). The
|
|
160
|
+
* top/bottom rails are fixed band zones the ball bounces between. */
|
|
161
|
+
#define FIELD_LINES 120
|
|
162
|
+
#define FIELD_DLL_OFF 30 /* byte offset of court entry 0 in dll[] */
|
|
64
163
|
|
|
65
|
-
/* ──
|
|
66
|
-
*
|
|
67
|
-
*
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
164
|
+
/* ── GAME LOGIC (clay — reshape freely) ──────────────────────────────────────
|
|
165
|
+
* Object art. 160A mode: 1 byte = 4 pixels of 2 bits each; pixel value
|
|
166
|
+
* 1/2/3 = colour 1/2/3 of the palette the DL entry names, 0 = transparent.
|
|
167
|
+
* Rows are stored top-down, consecutive (the 1-scanline-zone pattern below
|
|
168
|
+
* means NO page-alignment dance — see "offset addressing quirk" in
|
|
169
|
+
* MENTAL_MODEL.md for what multi-line zones would demand instead). */
|
|
170
|
+
|
|
171
|
+
/* Paddle — a solid 8px-wide (2 bytes) colour-1 bar, PADDLE_H rows tall. Each
|
|
172
|
+
* row is two value-1 nibble bytes (0x55 = four colour-1 pixels); drawn with
|
|
173
|
+
* palette 1 (P1 blue) or 2 (P2 red). */
|
|
174
|
+
#define PADDLE_H 16
|
|
175
|
+
static const uint8_t GFX_PADDLE[PADDLE_H * 2] = {
|
|
176
|
+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
177
|
+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
178
|
+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
179
|
+
0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55, 0x55,
|
|
75
180
|
};
|
|
181
|
+
|
|
182
|
+
/* Ball — a 4px-wide (1 byte) colour-3 pip, BALL_H rows tall, palette 3. */
|
|
183
|
+
#define BALL_H 4
|
|
184
|
+
static const uint8_t GFX_BALL[BALL_H] = { 0x55, 0x55, 0x55, 0x55 };
|
|
185
|
+
|
|
186
|
+
/* DL mode bytes for the 4-byte (direct) entry form: palette in bits 5-7,
|
|
187
|
+
* width as (32 - width_bytes) in bits 0-4 (must be non-zero — a zero low
|
|
188
|
+
* 5 bits would make MARIA parse a 5-byte entry instead). */
|
|
189
|
+
#define MODE_PADDLE1 ((1u << 5) | (32 - 2)) /* palette 1, 2 bytes wide */
|
|
190
|
+
#define MODE_PADDLE2 ((2u << 5) | (32 - 2)) /* palette 2 */
|
|
191
|
+
#define MODE_BALL ((3u << 5) | (32 - 1)) /* palette 3, 1 byte wide */
|
|
192
|
+
#define MODE_NET ((5u << 5) | (32 - 1)) /* HUD-green, 1 byte wide */
|
|
193
|
+
|
|
194
|
+
/* ── GAME LOGIC (clay) — 8x8 text font, 1 bit per pixel, 7px glyphs.
|
|
195
|
+
* The 7800 has NO text mode and no tilemap; text is just more objects.
|
|
196
|
+
* The text path here: expand glyphs into a 32-byte-wide RAM canvas
|
|
197
|
+
* (= 128px, 16 characters), then show the canvas with ONE wide DL entry
|
|
198
|
+
* per scanline. One drawable per line beats one-DL-entry-per-character
|
|
199
|
+
* by 16x in MARIA DMA time. Index order: 0-9 A-Z dash space. */
|
|
200
|
+
static const uint8_t FONT[38 * 8] = {
|
|
201
|
+
0x70,0x88,0x98,0xA8,0xC8,0x88,0x70,0x00, /* 0 */
|
|
202
|
+
0x20,0x60,0x20,0x20,0x20,0x20,0x70,0x00, /* 1 */
|
|
203
|
+
0x70,0x88,0x08,0x30,0x40,0x80,0xF8,0x00, /* 2 */
|
|
204
|
+
0x70,0x88,0x08,0x30,0x08,0x88,0x70,0x00, /* 3 */
|
|
205
|
+
0x10,0x30,0x50,0x90,0xF8,0x10,0x10,0x00, /* 4 */
|
|
206
|
+
0xF8,0x80,0xF0,0x08,0x08,0x88,0x70,0x00, /* 5 */
|
|
207
|
+
0x30,0x40,0x80,0xF0,0x88,0x88,0x70,0x00, /* 6 */
|
|
208
|
+
0xF8,0x08,0x10,0x20,0x40,0x40,0x40,0x00, /* 7 */
|
|
209
|
+
0x70,0x88,0x88,0x70,0x88,0x88,0x70,0x00, /* 8 */
|
|
210
|
+
0x70,0x88,0x88,0x78,0x08,0x10,0x60,0x00, /* 9 */
|
|
211
|
+
0x20,0x50,0x88,0x88,0xF8,0x88,0x88,0x00, /* A */
|
|
212
|
+
0xF0,0x88,0x88,0xF0,0x88,0x88,0xF0,0x00, /* B */
|
|
213
|
+
0x70,0x88,0x80,0x80,0x80,0x88,0x70,0x00, /* C */
|
|
214
|
+
0xF0,0x88,0x88,0x88,0x88,0x88,0xF0,0x00, /* D */
|
|
215
|
+
0xF8,0x80,0x80,0xF0,0x80,0x80,0xF8,0x00, /* E */
|
|
216
|
+
0xF8,0x80,0x80,0xF0,0x80,0x80,0x80,0x00, /* F */
|
|
217
|
+
0x70,0x88,0x80,0xB8,0x88,0x88,0x70,0x00, /* G */
|
|
218
|
+
0x88,0x88,0x88,0xF8,0x88,0x88,0x88,0x00, /* H */
|
|
219
|
+
0x70,0x20,0x20,0x20,0x20,0x20,0x70,0x00, /* I */
|
|
220
|
+
0x38,0x10,0x10,0x10,0x10,0x90,0x60,0x00, /* J */
|
|
221
|
+
0x88,0x90,0xA0,0xC0,0xA0,0x90,0x88,0x00, /* K */
|
|
222
|
+
0x80,0x80,0x80,0x80,0x80,0x80,0xF8,0x00, /* L */
|
|
223
|
+
0x88,0xD8,0xA8,0xA8,0x88,0x88,0x88,0x00, /* M */
|
|
224
|
+
0x88,0xC8,0xA8,0x98,0x88,0x88,0x88,0x00, /* N */
|
|
225
|
+
0x70,0x88,0x88,0x88,0x88,0x88,0x70,0x00, /* O */
|
|
226
|
+
0xF0,0x88,0x88,0xF0,0x80,0x80,0x80,0x00, /* P */
|
|
227
|
+
0x70,0x88,0x88,0x88,0xA8,0x90,0x68,0x00, /* Q */
|
|
228
|
+
0xF0,0x88,0x88,0xF0,0xA0,0x90,0x88,0x00, /* R */
|
|
229
|
+
0x78,0x80,0x80,0x70,0x08,0x08,0xF0,0x00, /* S */
|
|
230
|
+
0xF8,0x20,0x20,0x20,0x20,0x20,0x20,0x00, /* T */
|
|
231
|
+
0x88,0x88,0x88,0x88,0x88,0x88,0x70,0x00, /* U */
|
|
232
|
+
0x88,0x88,0x88,0x88,0x88,0x50,0x20,0x00, /* V */
|
|
233
|
+
0x88,0x88,0x88,0xA8,0xA8,0xD8,0x88,0x00, /* W */
|
|
234
|
+
0x88,0x88,0x50,0x20,0x50,0x88,0x88,0x00, /* X */
|
|
235
|
+
0x88,0x88,0x50,0x20,0x20,0x20,0x20,0x00, /* Y */
|
|
236
|
+
0xF8,0x08,0x10,0x20,0x40,0x80,0xF8,0x00, /* Z */
|
|
237
|
+
0x00,0x00,0x00,0x78,0x00,0x00,0x00,0x00, /* - */
|
|
238
|
+
0x00,0x00,0x00,0x00,0x00,0x00,0x00,0x00, /* space */
|
|
239
|
+
};
|
|
240
|
+
/* nibble → 2bpp expansion: each 1 bit becomes pixel value 1 (palette c1) */
|
|
241
|
+
static const uint8_t NIB2[16] = {
|
|
242
|
+
0x00,0x01,0x04,0x05,0x10,0x11,0x14,0x15,
|
|
243
|
+
0x40,0x41,0x44,0x45,0x50,0x51,0x54,0x55,
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
247
|
+
* Solid band drawable for multi-line zones (the rails + decor stripes) AND
|
|
248
|
+
* the net. Inside a zone of height H, MARIA fetches scanline l's pixels from
|
|
249
|
+
* ADDR + (H-1-l)*256 — the "offset addressing quirk". A multi-line drawable
|
|
250
|
+
* therefore needs valid data at the SAME low-byte offset across H consecutive
|
|
251
|
+
* 256-byte pages. For solid colour bands we sidestep alignment entirely: a
|
|
252
|
+
* 2KB ROM run of 0x55 means ANY address inside the first page works for zones
|
|
253
|
+
* up to 8 tall (8 pages x 256). Costs 2KB of a 32KB cart — ROM is the cheap
|
|
254
|
+
* resource here. The net reuses SOLID8 too: it's a thin colour object drawn
|
|
255
|
+
* into the one-line court zones it spans (1-line zones ⇒ the quirk vanishes,
|
|
256
|
+
* any SOLID8 address works). */
|
|
257
|
+
#define S16 0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55,0x55
|
|
258
|
+
#define S256 S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16,S16
|
|
259
|
+
static const uint8_t SOLID8[2048] = { S256,S256,S256,S256,S256,S256,S256,S256 };
|
|
260
|
+
|
|
261
|
+
/* Full-width band DL: a DL drawable is at most 32 bytes (128px), so a
|
|
262
|
+
* 160px line takes TWO 5-byte entries + terminator = 11 bytes. 5-byte
|
|
263
|
+
* form: lo, $40 (extended, write-mode 0 = 160A), hi, palette|width, X.
|
|
264
|
+
* Width 32 encodes as 0 in the low 5 bits — legal ONLY in 5-byte form. */
|
|
76
265
|
#define MK_BAND(name, pal) static uint8_t name[11] = { \
|
|
77
|
-
0, 0x40, 0, ((pal) << 5) | 0, 0, /*
|
|
78
|
-
0, 0x40, 0, ((pal) << 5) | 24, 128, /*
|
|
266
|
+
0, 0x40, 0, ((pal) << 5) | 0, 0, /* 128px @ x=0 */ \
|
|
267
|
+
0, 0x40, 0, ((pal) << 5) | 24, 128, /* 32px @ x=128 */ \
|
|
79
268
|
0 }
|
|
80
|
-
MK_BAND(
|
|
269
|
+
MK_BAND(dl_band_a, 6);
|
|
270
|
+
MK_BAND(dl_band_b, 7);
|
|
271
|
+
MK_BAND(dl_rail, 5); /* the top/bottom court rails (green) */
|
|
272
|
+
static uint8_t dl_empty[2] = { 0, 0 };
|
|
273
|
+
|
|
274
|
+
/* ════════════════════════════════════════════════════════════════════════
|
|
275
|
+
* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
276
|
+
* THE DISPLAY-LIST POOL — how the court's moving objects get drawn (the
|
|
277
|
+
* 7800's signature). Same machinery the dense 7800 shmup uses for its swarm;
|
|
278
|
+
* here it draws the net, the two paddles, and the ball.
|
|
279
|
+
*
|
|
280
|
+
* MARIA hierarchy refresher: DPP → DLL (one entry per ZONE: height + DL
|
|
281
|
+
* pointer) → DL (one 4/5-byte entry per OBJECT crossing that zone) → pixel
|
|
282
|
+
* bytes. There is no sprite table; "an object" IS a DL entry.
|
|
283
|
+
*
|
|
284
|
+
* The court is 120 one-scanline zones. Each has a fixed 14-byte DL slot:
|
|
285
|
+
* room for THREE 4-byte object entries + the terminator byte (MARIA reads
|
|
286
|
+
* the NEXT entry's mode byte after each entry; a 0 there ends the line —
|
|
287
|
+
* forget the terminator and MARIA walks into garbage and the screen dies).
|
|
288
|
+
* A court game is the EASY case for this budget — the net + at most one
|
|
289
|
+
* paddle + the ball ever share a scanline (2-3 objects), miles under the
|
|
290
|
+
* ~3-per-line DMA ceiling. We keep the same 3-slot machinery the shmup uses
|
|
291
|
+
* so a fork that adds more objects (a second ball, power-ups) inherits the
|
|
292
|
+
* flicker-drop safety valve for free.
|
|
293
|
+
*
|
|
294
|
+
* The pool is SPLIT across two RAM regions because no single linker region
|
|
295
|
+
* fits 1680 bytes + the DLL + the canvases (see RAM MAP):
|
|
296
|
+
* lines 0-46 → pool_a[] (BSS, RAM1) 47 * 14 = 658 bytes
|
|
297
|
+
* lines 47-119 → POOLB ($2200, raw RAM3) 73 * 14 = 1022 bytes
|
|
298
|
+
* line_dl[] resolves a court line to its slot; nothing else knows the split.
|
|
299
|
+
*
|
|
300
|
+
* Rebuild-vs-patch doctrine (MENTAL_MODEL.md): the DLL is built ONCE and
|
|
301
|
+
* only its 3-byte court entries are repointed at state changes (with DMA
|
|
302
|
+
* off); per-frame work only rewrites bytes INSIDE existing 14-byte slots.
|
|
303
|
+
* Tearing down the DLL itself mid-game races MARIA's walker — the classic
|
|
304
|
+
* "works one frame then the screen falls apart" 7800 bug.
|
|
305
|
+
* ════════════════════════════════════════════════════════════════════════ */
|
|
306
|
+
#define LINE_BYTES 14
|
|
307
|
+
#define LINE_FULL 12 /* 3 entries * 4 bytes */
|
|
308
|
+
#define POOLA_LINES 47
|
|
309
|
+
static uint8_t pool_a[POOLA_LINES * LINE_BYTES];
|
|
310
|
+
static uint8_t* line_dl[FIELD_LINES];
|
|
311
|
+
static uint8_t line_used[FIELD_LINES];
|
|
312
|
+
|
|
313
|
+
static uint8_t dll[143 * 3];
|
|
314
|
+
static uint8_t hud_canvas[8 * 32]; /* 16-char text row, lives in BSS */
|
|
315
|
+
static uint8_t hud_dls[8 * 7]; /* one 5-byte DL + term per row */
|
|
81
316
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
317
|
+
/* Emit one object: a 4-byte direct DL entry into every court line one of
|
|
318
|
+
* its rows crosses. gfx rows are consecutive (stride = width in bytes).
|
|
319
|
+
* Callers keep y in [0, FIELD_LINES - h] so no clipping is needed — keep
|
|
320
|
+
* that invariant if you change movement code, or add clipping here. */
|
|
321
|
+
static void emit_object(uint8_t y, uint8_t h, const uint8_t* gfx,
|
|
322
|
+
uint8_t stride, uint8_t mode, uint8_t x) {
|
|
323
|
+
uint8_t r, off;
|
|
324
|
+
uint8_t* dl;
|
|
325
|
+
for (r = 0; r < h; ++r) {
|
|
326
|
+
off = line_used[y];
|
|
327
|
+
if (off < LINE_FULL) { /* line full ⇒ drop row (flicker) */
|
|
328
|
+
dl = line_dl[y] + off;
|
|
329
|
+
dl[0] = (uint8_t)((uint16_t)(uintptr_t)gfx & 0xFF);
|
|
330
|
+
dl[1] = mode;
|
|
331
|
+
dl[2] = (uint8_t)((uint16_t)(uintptr_t)gfx >> 8);
|
|
332
|
+
dl[3] = x;
|
|
333
|
+
line_used[y] = off + 4;
|
|
334
|
+
}
|
|
335
|
+
++y;
|
|
336
|
+
gfx += stride;
|
|
337
|
+
}
|
|
86
338
|
}
|
|
87
339
|
|
|
88
|
-
|
|
89
|
-
|
|
340
|
+
static void field_open(void) { /* step 1: forget last frame */
|
|
341
|
+
memset(line_used, 0, FIELD_LINES);
|
|
342
|
+
}
|
|
90
343
|
|
|
91
|
-
static
|
|
92
|
-
|
|
93
|
-
|
|
344
|
+
static void field_close(void) { /* step 3: terminate every line */
|
|
345
|
+
uint8_t i;
|
|
346
|
+
for (i = 0; i < FIELD_LINES; ++i)
|
|
347
|
+
line_dl[i][line_used[i] + 1] = 0; /* next entry's MODE byte = 0 */
|
|
348
|
+
}
|
|
94
349
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/*
|
|
107
|
-
*
|
|
108
|
-
static
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
} else {
|
|
136
|
-
set_dll_entry(i, wall);
|
|
350
|
+
/* ── HARDWARE IDIOM (load-bearing) — DLL construction + zone repointing.
|
|
351
|
+
* Built once at boot; dll_zone appends one 3-byte entry (offset byte =
|
|
352
|
+
* height-1; DLI/holey bits stay 0 — no NMI handler, no holey DMA here). */
|
|
353
|
+
static uint8_t* dllp;
|
|
354
|
+
static void dll_zone(uint8_t height, uint16_t dl) {
|
|
355
|
+
dllp[0] = height - 1;
|
|
356
|
+
dllp[1] = (uint8_t)(dl >> 8);
|
|
357
|
+
dllp[2] = (uint8_t)(dl & 0xFF);
|
|
358
|
+
dllp += 3;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/* Repoint ONE court line's DLL entry (title/result text overlays borrow
|
|
362
|
+
* court zones; play repoints them back at the pool slots). */
|
|
363
|
+
static void point_field_zone(uint8_t fline, uint16_t dl) {
|
|
364
|
+
uint8_t* e = dll + FIELD_DLL_OFF + (uint16_t)fline * 3;
|
|
365
|
+
e[0] = 0;
|
|
366
|
+
e[1] = (uint8_t)(dl >> 8);
|
|
367
|
+
e[2] = (uint8_t)(dl & 0xFF);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/* ── GAME LOGIC (clay) — text rendering into a 32-byte-wide RAM canvas ── */
|
|
371
|
+
static uint8_t glyph_index(char c) {
|
|
372
|
+
if (c >= '0' && c <= '9') return (uint8_t)(c - '0');
|
|
373
|
+
if (c >= 'A' && c <= 'Z') return (uint8_t)(10 + c - 'A');
|
|
374
|
+
if (c == '-') return 36;
|
|
375
|
+
return 37; /* space */
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
static void draw_text(uint8_t* canvas, uint8_t col, const char* s) {
|
|
379
|
+
uint8_t r, b;
|
|
380
|
+
const uint8_t* g;
|
|
381
|
+
uint8_t* dst;
|
|
382
|
+
while (*s && col < 16) {
|
|
383
|
+
g = FONT + ((uint16_t)glyph_index(*s) << 3);
|
|
384
|
+
dst = canvas + ((uint16_t)col << 1);
|
|
385
|
+
for (r = 0; r < 8; ++r) {
|
|
386
|
+
b = g[r];
|
|
387
|
+
dst[0] = NIB2[b >> 4];
|
|
388
|
+
dst[1] = NIB2[b & 0x0F];
|
|
389
|
+
dst += 32;
|
|
137
390
|
}
|
|
391
|
+
++s;
|
|
392
|
+
++col;
|
|
138
393
|
}
|
|
394
|
+
}
|
|
139
395
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
for (
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
/*
|
|
159
|
-
|
|
160
|
-
|
|
396
|
+
static void digits5(char* d, uint16_t v) {
|
|
397
|
+
uint8_t i;
|
|
398
|
+
for (i = 0; i < 5; ++i) { d[4 - i] = (char)('0' + v % 10); v /= 10; }
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* Build the 8 one-line DLs that display an arbitrary RAM canvas at x=16
|
|
402
|
+
* (centered 128px). pal picks the text colour palette. dls = 8*7 bytes. */
|
|
403
|
+
static void canvas_dls(uint8_t* dls, const uint8_t* canvas, uint8_t pal) {
|
|
404
|
+
uint8_t r;
|
|
405
|
+
uint16_t a;
|
|
406
|
+
for (r = 0; r < 8; ++r) {
|
|
407
|
+
a = (uint16_t)(uintptr_t)canvas + ((uint16_t)r << 5);
|
|
408
|
+
dls[0] = (uint8_t)(a & 0xFF);
|
|
409
|
+
dls[1] = 0x40; /* 5-byte form, 160A write mode */
|
|
410
|
+
dls[2] = (uint8_t)(a >> 8);
|
|
411
|
+
dls[3] = (uint8_t)((pal << 5) | 0); /* width 32 bytes encodes as 0 */
|
|
412
|
+
dls[4] = 16;
|
|
413
|
+
dls[5] = 0;
|
|
414
|
+
dls[6] = 0; /* terminator for the next read */
|
|
415
|
+
dls += 7;
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/* ── GAME LOGIC (clay) — the music. Two-voice TIA tune loop. ─────────────────
|
|
420
|
+
* The TIA's frequency divider is 5 bits — ~32 pitches TOTAL, none of them
|
|
421
|
+
* in tune with each other. Don't fight it: write the melody IN the TIA's
|
|
422
|
+
* crooked scale and it reads as "gritty 7800", fight it and it reads as
|
|
423
|
+
* "wrong". The note tables ARE the song — edit them to recompose.
|
|
424
|
+
* Voice 0 = melody (AUDC 4, square-ish). Voice 1 = bass (AUDC 6, deep
|
|
425
|
+
* buzz) — and voice 1 is SHARED with sound effects (TIA has only two
|
|
426
|
+
* voices): when the game fires an effect, sfx_hold mutes the bass for the
|
|
427
|
+
* effect's length, then the bass re-enters on its next note. That
|
|
428
|
+
* steal-and-return is the standard 2-voice arbitration trick. */
|
|
429
|
+
static const uint8_t MEL_F[16] = { 15,17,19,17, 15,19,22,255, 17,19,21,19, 17,15,17,255 };
|
|
430
|
+
static const uint8_t MEL_L[16] = { 8, 8, 8, 8, 8, 8,16, 8, 8, 8, 8, 8, 8, 8,16, 8 };
|
|
431
|
+
static const uint8_t BAS_F[8] = { 25,25,29,29, 27,27,23,27 };
|
|
432
|
+
static uint8_t mel_i, mel_t, bas_i, bas_t, sfx_hold;
|
|
433
|
+
|
|
434
|
+
static void music_tick(void) {
|
|
435
|
+
if (mel_t) --mel_t;
|
|
436
|
+
if (mel_t == 0) {
|
|
437
|
+
mel_i = (uint8_t)((mel_i + 1) & 15);
|
|
438
|
+
mel_t = MEL_L[mel_i];
|
|
439
|
+
if (MEL_F[mel_i] == 255) {
|
|
440
|
+
AUDV0 = 0; /* 255 = rest */
|
|
441
|
+
} else {
|
|
442
|
+
AUDC0 = 4; AUDF0 = MEL_F[mel_i]; AUDV0 = 6;
|
|
161
443
|
}
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
444
|
+
}
|
|
445
|
+
if (sfx_hold) { /* an effect owns voice 1 */
|
|
446
|
+
--sfx_hold;
|
|
447
|
+
if (sfx_hold == 0) bas_t = 1; /* bass re-enters next tick */
|
|
448
|
+
return;
|
|
449
|
+
}
|
|
450
|
+
if (bas_t) --bas_t;
|
|
451
|
+
if (bas_t == 0) {
|
|
452
|
+
bas_i = (uint8_t)((bas_i + 1) & 7);
|
|
453
|
+
bas_t = 16;
|
|
454
|
+
AUDC1 = 6; AUDF1 = BAS_F[bas_i]; AUDV1 = 5;
|
|
165
455
|
}
|
|
166
456
|
}
|
|
167
457
|
|
|
458
|
+
/* Effects (voice 1 via atari7800_sfx; sfx_hold keeps the bass out). */
|
|
459
|
+
static void fx_hit(void) { sfx_tone(1, 14, 4); sfx_hold = 5; }
|
|
460
|
+
static void fx_wall(void) { sfx_tone(1, 20, 3); sfx_hold = 4; }
|
|
461
|
+
static void fx_score(void) { sfx_noise(8); sfx_hold = 9; }
|
|
462
|
+
static void fx_win(void) { sfx_tone(1, 8, 8); sfx_hold = 9; }
|
|
463
|
+
static void fx_start(void) { sfx_tone(1, 10, 6); sfx_hold = 7; }
|
|
464
|
+
|
|
465
|
+
/* ── GAME LOGIC (clay — reshape freely) — court geometry + match rules ────────
|
|
466
|
+
* The court is the 120-line field between the two rails. Y is in COURT LINES
|
|
467
|
+
* [0, FIELD_LINES); X is in 7800 pixels [0, 160). Paddles ride the left/right
|
|
468
|
+
* edges and slide vertically; the ball bounces between the rails and is scored
|
|
469
|
+
* when it passes a paddle. */
|
|
470
|
+
#define PADDLE_X1 8 /* left paddle (P1) */
|
|
471
|
+
#define PADDLE_X2 148 /* right paddle (P2 / CPU) */
|
|
472
|
+
#define NET_X 79 /* centre net column */
|
|
473
|
+
#define BALL_W 4
|
|
474
|
+
#define COURT_T 2 /* first court line the ball may occupy */
|
|
475
|
+
#define COURT_B (FIELD_LINES - BALL_H - 2)
|
|
476
|
+
#define PADDLE_TOP_MAX (FIELD_LINES - PADDLE_H)
|
|
477
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
478
|
+
#define PADDLE_SPEED 3
|
|
479
|
+
#define CPU_SPEED 2 /* < player speed → beatable by steep angles */
|
|
480
|
+
|
|
481
|
+
/* Ball position/velocity in COURT-LINE / PIXEL units (signed for the math). */
|
|
482
|
+
static int16_t bx, by;
|
|
483
|
+
static int8_t bdx, bdy;
|
|
484
|
+
static uint8_t pad_y[2]; /* paddle TOP, court lines (0 = P1, 1 = P2) */
|
|
485
|
+
static uint8_t score[2];
|
|
486
|
+
static uint8_t serve_timer; /* freeze frames between points */
|
|
487
|
+
static uint8_t two_p; /* 0 = 1P vs CPU, 1 = 2P versus */
|
|
488
|
+
static uint8_t streak; /* current 1P-vs-CPU win streak (RAM) */
|
|
489
|
+
static uint16_t best_streak; /* in-session record — see header */
|
|
490
|
+
static uint8_t new_record; /* result screen flags a NEW RECORD */
|
|
491
|
+
static uint8_t winner; /* result: 0 = left/P1, 1 = right/P2/CPU */
|
|
492
|
+
static uint8_t over_lock; /* swallow the held fire on the result */
|
|
493
|
+
static uint8_t dirty;
|
|
494
|
+
static uint16_t rng = 0xACE1;
|
|
495
|
+
|
|
496
|
+
#define ST_TITLE 0
|
|
497
|
+
#define ST_PLAY 1
|
|
498
|
+
#define ST_OVER 2
|
|
499
|
+
static uint8_t state;
|
|
500
|
+
|
|
501
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG. A versus game NEEDS this: the 7800
|
|
502
|
+
* is fully deterministic, so without a noise source two fixed strategies lock
|
|
503
|
+
* into an infinite rally loop (the exact same cycle, forever). random8() is
|
|
504
|
+
* ticked once per play frame AND spins the serve/return angle so an idle
|
|
505
|
+
* match — CPU vs a still paddle — still drifts off true and ENDS rather than
|
|
506
|
+
* rallying without limit. */
|
|
507
|
+
static uint8_t random8(void) {
|
|
508
|
+
uint16_t r = rng;
|
|
509
|
+
r ^= r << 7;
|
|
510
|
+
r ^= r >> 9;
|
|
511
|
+
r ^= r << 8;
|
|
512
|
+
rng = r;
|
|
513
|
+
return (uint8_t)r;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side, with
|
|
517
|
+
* a PRNG-spun vertical angle so no two serves trace the same path (the
|
|
518
|
+
* idle-match-must-end guarantee). ── */
|
|
168
519
|
static void serve_ball(uint8_t to_left) {
|
|
169
|
-
bx =
|
|
170
|
-
by =
|
|
520
|
+
bx = NET_X - 1;
|
|
521
|
+
by = FIELD_LINES / 2;
|
|
171
522
|
bdx = to_left ? -2 : 2;
|
|
172
|
-
bdy = 1;
|
|
523
|
+
bdy = (int8_t)((random8() & 2) - 1); /* -1 or +1 */
|
|
524
|
+
if (bdy == 0) bdy = 1;
|
|
525
|
+
serve_timer = 30; /* half-second breather */
|
|
173
526
|
}
|
|
174
527
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
528
|
+
/* ── GAME LOGIC (clay) — HUD: "P1 s s CP" score line (s = digit). ── */
|
|
529
|
+
static void draw_hud(void) {
|
|
530
|
+
static char buf[17] = "P1 0 0 CP";
|
|
531
|
+
buf[3] = (char)('0' + (score[0] > 9 ? 9 : score[0]));
|
|
532
|
+
buf[12] = (char)('0' + (score[1] > 9 ? 9 : score[1]));
|
|
533
|
+
buf[14] = two_p ? 'P' : 'C';
|
|
534
|
+
buf[15] = two_p ? '2' : 'P';
|
|
535
|
+
memset(hud_canvas, 0, sizeof(hud_canvas));
|
|
536
|
+
draw_text(hud_canvas, 0, buf);
|
|
537
|
+
dirty = 0;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
static void draw_hud_title(void) {
|
|
541
|
+
static char buf[12] = "BEST 00000";
|
|
542
|
+
digits5(buf + 5, best_streak);
|
|
543
|
+
memset(hud_canvas, 0, sizeof(hud_canvas));
|
|
544
|
+
draw_text(hud_canvas, 3, buf);
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
/* ── HARDWARE IDIOM (load-bearing) — paint functions bracket structural
|
|
548
|
+
* display-list changes with MARIA DMA OFF ($7F) / ON ($40), the 7800's
|
|
549
|
+
* version of the NES "rendering off before nametable writes" rule: MARIA
|
|
550
|
+
* may be mid-walk through the very lists being rewritten, and repointing
|
|
551
|
+
* dozens of zones under it glitches (or with bad luck hangs) the frame.
|
|
552
|
+
* CTRL $40 = DMA on, 160A read mode, colour burst on — forget to restore
|
|
553
|
+
* it and the screen stays the flat BACKGRND colour forever. ── */
|
|
554
|
+
|
|
555
|
+
/* Title screen: borrow court zones for three text overlays composed in
|
|
556
|
+
* POOLB (the pool isn't drawing the court on the title, so its RAM is free —
|
|
557
|
+
* 4KB machines make you reuse like this). Title is double-height by pointing
|
|
558
|
+
* TWO consecutive 1-line zones at each canvas row — zero extra RAM, pure DLL
|
|
559
|
+
* trickery. */
|
|
560
|
+
static void paint_title(void) {
|
|
561
|
+
uint8_t i;
|
|
562
|
+
uint8_t* c0 = POOLB; /* title canvas (256 bytes) */
|
|
563
|
+
uint8_t* c1 = POOLB + 256; /* menu line 1 (256 bytes) */
|
|
564
|
+
uint8_t* c2 = POOLB + 512; /* menu line 2 (256 bytes) */
|
|
565
|
+
uint8_t* td = POOLB + 768; /* 3 lines * 8 row-DLs * 7 */
|
|
566
|
+
CTRL = 0x7F; /* DMA off */
|
|
567
|
+
memset(POOLB, 0, 768);
|
|
568
|
+
draw_text(c0, (uint8_t)((16 - (sizeof(GAME_TITLE) - 1)) / 2), GAME_TITLE);
|
|
569
|
+
draw_text(c1, 1, "1P FIRE VS CPU");
|
|
570
|
+
draw_text(c2, 1, "2P PAD2 RIVAL");
|
|
571
|
+
canvas_dls(td, c0, 0); /* white */
|
|
572
|
+
canvas_dls(td + 56, c1, 5); /* HUD green */
|
|
573
|
+
canvas_dls(td + 112, c2, 5);
|
|
574
|
+
for (i = 0; i < FIELD_LINES; ++i)
|
|
575
|
+
point_field_zone(i, (uint16_t)(uintptr_t)dl_empty);
|
|
576
|
+
for (i = 0; i < 16; ++i) /* double-height title rows */
|
|
577
|
+
point_field_zone((uint8_t)(8 + i),
|
|
578
|
+
(uint16_t)(uintptr_t)(td + ((i >> 1) * 7)));
|
|
579
|
+
for (i = 0; i < 8; ++i) {
|
|
580
|
+
point_field_zone((uint8_t)(56 + i), (uint16_t)(uintptr_t)(td + 56 + i * 7));
|
|
581
|
+
point_field_zone((uint8_t)(76 + i), (uint16_t)(uintptr_t)(td + 112 + i * 7));
|
|
582
|
+
}
|
|
583
|
+
draw_hud_title();
|
|
584
|
+
state = ST_TITLE;
|
|
585
|
+
CTRL = 0x40; /* DMA back on */
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
/* Result screen: the pool RAM becomes the message overlay (same reuse trick
|
|
589
|
+
* as the title), the rest of the court goes blank. */
|
|
590
|
+
static void paint_result(void) {
|
|
591
|
+
uint8_t i;
|
|
592
|
+
uint8_t* c0 = POOLB;
|
|
593
|
+
uint8_t* c1 = POOLB + 256;
|
|
594
|
+
uint8_t* c2 = POOLB + 512;
|
|
595
|
+
uint8_t* td = POOLB + 768;
|
|
596
|
+
static char buf[8] = "0 - 0";
|
|
597
|
+
CTRL = 0x7F;
|
|
598
|
+
memset(POOLB, 0, 768);
|
|
599
|
+
if (two_p) draw_text(c0, 4, winner ? "P2 WINS" : "P1 WINS");
|
|
600
|
+
else draw_text(c0, 3, winner ? "CPU WINS" : "P1 WINS");
|
|
601
|
+
buf[0] = (char)('0' + score[0]);
|
|
602
|
+
buf[4] = (char)('0' + score[1]);
|
|
603
|
+
draw_text(c1, 6, buf);
|
|
604
|
+
if (new_record) draw_text(c2, 3, "NEW RECORD");
|
|
605
|
+
else draw_text(c2, 2, "FIRE - TITLE");
|
|
606
|
+
canvas_dls(td, c0, 0);
|
|
607
|
+
canvas_dls(td + 56, c1, 5);
|
|
608
|
+
canvas_dls(td + 112, c2, 5);
|
|
609
|
+
for (i = 0; i < FIELD_LINES; ++i)
|
|
610
|
+
point_field_zone(i, (uint16_t)(uintptr_t)dl_empty);
|
|
611
|
+
for (i = 0; i < 8; ++i) {
|
|
612
|
+
point_field_zone((uint8_t)(36 + i), (uint16_t)(uintptr_t)(td + i * 7));
|
|
613
|
+
point_field_zone((uint8_t)(56 + i), (uint16_t)(uintptr_t)(td + 56 + i * 7));
|
|
614
|
+
point_field_zone((uint8_t)(76 + i), (uint16_t)(uintptr_t)(td + 112 + i * 7));
|
|
615
|
+
}
|
|
616
|
+
over_lock = 30; /* swallow the held fire button */
|
|
617
|
+
state = ST_OVER;
|
|
618
|
+
CTRL = 0x40;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* ── GAME LOGIC (clay) — match over: result + record bookkeeping (see header
|
|
622
|
+
* for why the record is the longest 1P-vs-CPU win streak, in-session only). ── */
|
|
623
|
+
static void end_match(void) {
|
|
624
|
+
new_record = 0;
|
|
625
|
+
if (score[0] >= WIN_SCORE) { /* P1 won */
|
|
626
|
+
winner = 0;
|
|
627
|
+
if (!two_p) { /* vs CPU: extend + record streak */
|
|
628
|
+
++streak;
|
|
629
|
+
if (streak > best_streak) {
|
|
630
|
+
best_streak = streak;
|
|
631
|
+
new_record = 1;
|
|
632
|
+
/* HSC NOTE (see file header): on real hardware with a High Score
|
|
633
|
+
* Cart you would write the record into HSC RAM ($1000-$17FF) here.
|
|
634
|
+
* The bundled prosystem core has no HSC support and exposes no
|
|
635
|
+
* SAVE_RAM, so the record honestly lives only as long as the session. */
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
} else { /* P2 / CPU won */
|
|
639
|
+
winner = 1;
|
|
640
|
+
if (!two_p) streak = 0; /* the streak dies with the loss */
|
|
641
|
+
}
|
|
642
|
+
fx_win();
|
|
643
|
+
paint_result();
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
647
|
+
static void score_point(uint8_t for_left) {
|
|
648
|
+
if (for_left) ++score[0]; else ++score[1];
|
|
649
|
+
fx_score();
|
|
650
|
+
dirty = 1;
|
|
651
|
+
if (score[0] >= WIN_SCORE || score[1] >= WIN_SCORE) end_match();
|
|
652
|
+
else serve_ball((uint8_t)(for_left ? 0 : 1)); /* loser of the point is served at */
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
|
|
656
|
+
* Centre = flat-ish, edges = steep. Max |bdy| is 2; the CPU moves at
|
|
657
|
+
* CPU_SPEED (< player) so an edge hit is exactly how a human beats it. A ±1
|
|
658
|
+
* random spin on every return keeps rallies from repeating (PRNG note). ── */
|
|
659
|
+
static void deflect(uint8_t paddle_top) {
|
|
660
|
+
int16_t rel = (by + BALL_H / 2) - (int16_t)(paddle_top + PADDLE_H / 2);
|
|
661
|
+
bdy = (int8_t)(rel >> 3);
|
|
662
|
+
bdy += (int8_t)((random8() & 2) - 1); /* spin: -1 or +1 */
|
|
663
|
+
if (bdy > 2) bdy = 2;
|
|
664
|
+
if (bdy < -2) bdy = -2;
|
|
665
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
666
|
+
fx_hit();
|
|
667
|
+
}
|
|
179
668
|
|
|
180
|
-
|
|
669
|
+
/* ── GAME LOGIC (clay) — start a match ── */
|
|
670
|
+
static void start_match(uint8_t players) {
|
|
671
|
+
uint8_t i;
|
|
672
|
+
CTRL = 0x7F;
|
|
673
|
+
two_p = players;
|
|
674
|
+
pad_y[0] = (FIELD_LINES - PADDLE_H) / 2;
|
|
675
|
+
pad_y[1] = (FIELD_LINES - PADDLE_H) / 2;
|
|
676
|
+
score[0] = 0; score[1] = 0;
|
|
677
|
+
new_record = 0;
|
|
678
|
+
winner = 0;
|
|
679
|
+
for (i = 0; i < FIELD_LINES; ++i) /* court zones → pool slots */
|
|
680
|
+
point_field_zone(i, (uint16_t)(uintptr_t)line_dl[i]);
|
|
681
|
+
field_open();
|
|
682
|
+
field_close(); /* all lines empty + termed */
|
|
683
|
+
rng ^= (uint16_t)(best_streak * 251) ^ 0x1234;
|
|
181
684
|
serve_ball(0);
|
|
685
|
+
draw_hud();
|
|
686
|
+
fx_start();
|
|
687
|
+
state = ST_PLAY;
|
|
688
|
+
CTRL = 0x40;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
static void vblank_wait(void) {
|
|
692
|
+
while (MSTAT & 0x80) { } /* leave the current vblank */
|
|
693
|
+
while (!(MSTAT & 0x80)) { } /* catch the next one starting */
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
/* ── GAME LOGIC (clay) — per-player paddle move from a joystick port. ── */
|
|
697
|
+
static void move_paddle(uint8_t p, uint8_t pad) {
|
|
698
|
+
uint8_t up, dn;
|
|
699
|
+
if (p == 0) { up = (uint8_t)(pad & J1_UP); dn = (uint8_t)(pad & J1_DOWN); }
|
|
700
|
+
else { up = (uint8_t)(pad & J2_UP); dn = (uint8_t)(pad & J2_DOWN); }
|
|
701
|
+
if (up && pad_y[p] >= PADDLE_SPEED) pad_y[p] -= PADDLE_SPEED;
|
|
702
|
+
if (dn && pad_y[p] <= PADDLE_TOP_MAX - PADDLE_SPEED) pad_y[p] += PADDLE_SPEED;
|
|
703
|
+
}
|
|
704
|
+
|
|
705
|
+
/* ── GAME LOGIC (clay) — CPU paddle: chase the ball's centre at CPU_SPEED
|
|
706
|
+
* (< player) with a small dead zone. Beatable by design: steep deflections
|
|
707
|
+
* outrun it, and the PRNG spin keeps it from ever locking into a perfect
|
|
708
|
+
* rally — an unattended match therefore always ENDS. ── */
|
|
709
|
+
static void move_cpu(void) {
|
|
710
|
+
int16_t target = by + BALL_H / 2 - PADDLE_H / 2;
|
|
711
|
+
if ((int16_t)pad_y[1] + 2 < target && pad_y[1] <= PADDLE_TOP_MAX - CPU_SPEED)
|
|
712
|
+
pad_y[1] += CPU_SPEED;
|
|
713
|
+
else if ((int16_t)pad_y[1] > target + 2 && pad_y[1] >= CPU_SPEED)
|
|
714
|
+
pad_y[1] -= CPU_SPEED;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
void main(void) {
|
|
718
|
+
uint8_t i;
|
|
719
|
+
uint16_t a;
|
|
720
|
+
|
|
721
|
+
/* ── HARDWARE IDIOM (load-bearing) — boot order: build EVERYTHING the
|
|
722
|
+
* DLL will reference, then point DPP at it, THEN enable DMA. Enabling
|
|
723
|
+
* DMA over a half-built DLL is the 7800 black-screen classic. ── */
|
|
724
|
+
|
|
725
|
+
/* Resolve the pool split: court line → 14-byte DL slot. */
|
|
726
|
+
for (i = 0; i < POOLA_LINES; ++i)
|
|
727
|
+
line_dl[i] = pool_a + (uint16_t)i * LINE_BYTES;
|
|
728
|
+
for (i = POOLA_LINES; i < FIELD_LINES; ++i)
|
|
729
|
+
line_dl[i] = POOLB + (uint16_t)(i - POOLA_LINES) * LINE_BYTES;
|
|
182
730
|
|
|
183
|
-
/*
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
731
|
+
/* Patch the ROM band drawables' data pointers (SOLID8). */
|
|
732
|
+
a = (uint16_t)(uintptr_t)SOLID8;
|
|
733
|
+
dl_band_a[0] = dl_band_a[5] = (uint8_t)(a & 0xFF);
|
|
734
|
+
dl_band_a[2] = dl_band_a[7] = (uint8_t)(a >> 8);
|
|
735
|
+
dl_band_b[0] = dl_band_b[5] = (uint8_t)(a & 0xFF);
|
|
736
|
+
dl_band_b[2] = dl_band_b[7] = (uint8_t)(a >> 8);
|
|
737
|
+
dl_rail[0] = dl_rail[5] = (uint8_t)(a & 0xFF);
|
|
738
|
+
dl_rail[2] = dl_rail[7] = (uint8_t)(a >> 8);
|
|
739
|
+
|
|
740
|
+
canvas_dls(hud_dls, hud_canvas, 5);
|
|
741
|
+
|
|
742
|
+
/* The DLL — the screen layout, built once (see the layout table above).
|
|
743
|
+
* 143 entries, mixed zone heights; only the 120 court entries are ever
|
|
744
|
+
* repointed after this. */
|
|
745
|
+
dllp = dll;
|
|
746
|
+
dll_zone(16, (uint16_t)(uintptr_t)dl_empty); /* lines 0-15 */
|
|
747
|
+
for (i = 0; i < 8; ++i) /* HUD 16-23 */
|
|
748
|
+
dll_zone(1, (uint16_t)(uintptr_t)(hud_dls + i * 7));
|
|
749
|
+
dll_zone(2, (uint16_t)(uintptr_t)dl_rail); /* top rail */
|
|
750
|
+
for (i = 0; i < FIELD_LINES; ++i) /* court 26-145 */
|
|
751
|
+
dll_zone(1, (uint16_t)(uintptr_t)line_dl[i]);
|
|
752
|
+
dll_zone(2, (uint16_t)(uintptr_t)dl_rail); /* bottom rail */
|
|
753
|
+
/* Below-court decor stripes — also our anti-blank-screen ballast: with DMA
|
|
754
|
+
* fetching only objects, everything else is the single flat BACKGRND
|
|
755
|
+
* colour, and a mostly-one-colour frame reads as "dead". */
|
|
756
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
|
|
757
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
|
|
758
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_band_b);
|
|
759
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
|
|
760
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
|
|
761
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
|
|
762
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_band_b);
|
|
763
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
|
|
764
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_band_a);
|
|
765
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_empty);
|
|
766
|
+
dll_zone(8, (uint16_t)(uintptr_t)dl_band_b); /* …through 235 */
|
|
767
|
+
dll_zone(7, (uint16_t)(uintptr_t)dl_empty); /* 236-242 */
|
|
768
|
+
|
|
769
|
+
/* Palettes (Atari colour byte = hue<<4 | luminance). */
|
|
770
|
+
BACKGRND = 0x00; /* court black */
|
|
771
|
+
P0C1 = 0x0F; /* title text white */
|
|
772
|
+
P1C1 = 0x96; /* P1 paddle blue */
|
|
773
|
+
P2C1 = 0x46; /* P2 / CPU paddle red */
|
|
774
|
+
P3C1 = 0x0E; /* ball white */
|
|
775
|
+
P4C1 = 0x1A; /* (spare) */
|
|
776
|
+
P5C1 = 0xC9; /* HUD green / rails / net */
|
|
777
|
+
P6C1 = 0x84; /* decor band deep teal */
|
|
778
|
+
P7C1 = 0x88; /* decor band brighter teal */
|
|
192
779
|
CHARBASE = 0;
|
|
193
|
-
OFFSET
|
|
780
|
+
OFFSET = 0; /* must stay 0 (7800 standard) */
|
|
194
781
|
|
|
195
|
-
|
|
196
|
-
|
|
782
|
+
a = (uint16_t)(uintptr_t)dll;
|
|
783
|
+
DPPL = (uint8_t)(a & 0xFF);
|
|
784
|
+
DPPH = (uint8_t)(a >> 8);
|
|
197
785
|
|
|
198
|
-
dll_addr = (uint16_t)(uintptr_t)dll;
|
|
199
|
-
DPPL = (uint8_t)(dll_addr & 0xFF);
|
|
200
|
-
DPPH = (uint8_t)(dll_addr >> 8);
|
|
201
|
-
CTRL = 0x40;
|
|
202
786
|
sfx_init();
|
|
787
|
+
best_streak = 0; /* in-session only — see header */
|
|
788
|
+
streak = 0;
|
|
789
|
+
paint_title(); /* …turns DMA on */
|
|
203
790
|
|
|
204
791
|
for (;;) {
|
|
792
|
+
uint8_t pad, f1, f2;
|
|
793
|
+
static uint8_t pf; /* fire edge across title/result */
|
|
205
794
|
vblank_wait();
|
|
206
795
|
sfx_update();
|
|
796
|
+
music_tick();
|
|
207
797
|
|
|
208
|
-
pad = (uint8_t)
|
|
209
|
-
|
|
210
|
-
|
|
798
|
+
pad = (uint8_t)~SWCHA;
|
|
799
|
+
f1 = (uint8_t)(!(INPT4 & 0x80));
|
|
800
|
+
f2 = (uint8_t)(!(INPT5 & 0x80));
|
|
211
801
|
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
if ((
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
else if (p2y > target && p2y > COURT_TOP) p2y -= 1;
|
|
802
|
+
if (state == ST_TITLE) {
|
|
803
|
+
/* ── GAME LOGIC (clay) — title: P1 fire = 1P vs CPU, P2 fire = 2P ── */
|
|
804
|
+
if (f1 && !(pf & 1)) start_match(0);
|
|
805
|
+
else if (f2 && !(pf & 2)) start_match(1);
|
|
806
|
+
pf = (uint8_t)(f1 | (f2 << 1));
|
|
807
|
+
continue;
|
|
219
808
|
}
|
|
220
809
|
|
|
221
|
-
|
|
222
|
-
|
|
810
|
+
if (state == ST_OVER) {
|
|
811
|
+
if (over_lock) { --over_lock; pf = (uint8_t)(f1 | (f2 << 1)); continue; }
|
|
812
|
+
if ((f1 || f2) && !pf) paint_title();
|
|
813
|
+
pf = (uint8_t)(f1 | (f2 << 1));
|
|
814
|
+
continue;
|
|
815
|
+
}
|
|
223
816
|
|
|
224
|
-
|
|
225
|
-
|
|
817
|
+
/* ── ST_PLAY ───────────────────────────────────────────────────── */
|
|
818
|
+
random8(); /* tick the noise every frame */
|
|
226
819
|
|
|
227
|
-
/*
|
|
228
|
-
if (
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (
|
|
233
|
-
|
|
234
|
-
|
|
820
|
+
move_paddle(0, pad); /* P1 — port 0 */
|
|
821
|
+
if (two_p) move_paddle(1, pad); /* P2 — port 1 */
|
|
822
|
+
else move_cpu(); /* CPU drives the right paddle */
|
|
823
|
+
|
|
824
|
+
/* Ball update (frozen during the post-point serve pause). */
|
|
825
|
+
if (serve_timer > 0) {
|
|
826
|
+
--serve_timer;
|
|
827
|
+
} else {
|
|
828
|
+
bx += bdx;
|
|
829
|
+
by += bdy;
|
|
830
|
+
|
|
831
|
+
/* Rail bounce (top/bottom court boundaries). */
|
|
832
|
+
if (by < COURT_T) { by = COURT_T; bdy = (int8_t)(-bdy); fx_wall(); }
|
|
833
|
+
if (by > COURT_B) { by = COURT_B; bdy = (int8_t)(-bdy); fx_wall(); }
|
|
834
|
+
|
|
835
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
836
|
+
if (bdx < 0
|
|
837
|
+
&& bx <= PADDLE_X1 + 8 && bx + BALL_W >= PADDLE_X1
|
|
838
|
+
&& by + BALL_H > pad_y[0] && by < pad_y[0] + PADDLE_H) {
|
|
839
|
+
bdx = (int8_t)(-bdx);
|
|
840
|
+
bx = PADDLE_X1 + 8;
|
|
841
|
+
deflect(pad_y[0]);
|
|
842
|
+
}
|
|
843
|
+
if (bdx > 0
|
|
844
|
+
&& bx + BALL_W >= PADDLE_X2 && bx <= PADDLE_X2 + 8
|
|
845
|
+
&& by + BALL_H > pad_y[1] && by < pad_y[1] + PADDLE_H) {
|
|
846
|
+
bdx = (int8_t)(-bdx);
|
|
847
|
+
bx = PADDLE_X2 - BALL_W;
|
|
848
|
+
deflect(pad_y[1]);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/* Off either side → a point for the opposite player. */
|
|
852
|
+
if (bx < 2) score_point(0); /* past P1 → right side scores */
|
|
853
|
+
else if (bx > 160 - 2) score_point(1); /* past P2 → left side scores */
|
|
854
|
+
if (state != ST_PLAY) continue; /* end_match → result */
|
|
235
855
|
}
|
|
236
|
-
|
|
237
|
-
|
|
856
|
+
|
|
857
|
+
/* ── HARDWARE IDIOM (load-bearing) — the per-frame draw pass:
|
|
858
|
+
* open (clear counts) → emit the net + both paddles + the ball → close
|
|
859
|
+
* (terminators). Emission order = draw order on shared scanlines; a court
|
|
860
|
+
* game never fills a line (≤2-3 objects), so nothing flickers — but the
|
|
861
|
+
* BALL goes LAST so that if a future fork DOES crowd a line, the player's
|
|
862
|
+
* paddles win the slot and only the ball blinks. ── */
|
|
863
|
+
field_open();
|
|
864
|
+
/* the centre net: a dashed thin colour-5 column, 4-on/4-off down the
|
|
865
|
+
* court (the structural court landmark, emitted first). */
|
|
866
|
+
for (i = 0; i + 4 <= FIELD_LINES; i += 8)
|
|
867
|
+
emit_object(i, 4, SOLID8, 0, MODE_NET, NET_X);
|
|
868
|
+
/* paddles */
|
|
869
|
+
emit_object(pad_y[0], PADDLE_H, GFX_PADDLE, 2, MODE_PADDLE1, PADDLE_X1);
|
|
870
|
+
emit_object(pad_y[1], PADDLE_H, GFX_PADDLE, 2, MODE_PADDLE2, PADDLE_X2);
|
|
871
|
+
/* the ball — clamped into the court so emit never runs past the pool */
|
|
872
|
+
{
|
|
873
|
+
uint8_t byl = (uint8_t)(by < 0 ? 0 : (by > FIELD_LINES - BALL_H ? FIELD_LINES - BALL_H : by));
|
|
874
|
+
uint8_t bxl = (uint8_t)(bx < 0 ? 0 : (bx > 159 ? 159 : bx));
|
|
875
|
+
emit_object(byl, BALL_H, GFX_BALL, 1, MODE_BALL, bxl);
|
|
238
876
|
}
|
|
877
|
+
field_close();
|
|
239
878
|
|
|
240
|
-
|
|
879
|
+
if (dirty) draw_hud();
|
|
241
880
|
}
|
|
242
881
|
}
|