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,142 +1,797 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
1
|
+
/* ── sports.c — C64 head-to-head court sports (complete example game) ─────────
|
|
2
|
+
*
|
|
3
|
+
* DELTA DUEL — a COMPLETE, working game (Pong lineage): a title screen with
|
|
4
|
+
* 1P vs a BEATABLE CPU and 2P SIMULTANEOUS VERSUS (both paddles live at once,
|
|
5
|
+
* P1 on CONTROL PORT 2, P2 on CONTROL PORT 1), a first-to-5 match into a
|
|
6
|
+
* result screen, in-session best 1P-vs-CPU win streak behind the gated
|
|
7
|
+
* persistence seam, 2-voice SID music with the C64's signature filter sweep +
|
|
8
|
+
* SFX, and the C64's signature raster-IRQ split: a fixed HUD bar over the
|
|
9
|
+
* court. The two paddles and the ball are VIC-II HARDWARE SPRITES.
|
|
10
|
+
*
|
|
11
|
+
* The game: two paddles guard the left and right edges of a court; a ball
|
|
12
|
+
* rallies between them. UP/DOWN slide your paddle; the ball deflects by where
|
|
13
|
+
* it strikes (centre = flat, edges = steep) plus a ±1 PRNG "spin" so no two
|
|
14
|
+
* rallies repeat — an idle match provably ENDS instead of looping forever.
|
|
15
|
+
* Miss the ball past your edge and your rival scores. First to 5 wins.
|
|
16
|
+
*
|
|
17
|
+
* THIS FILE IS MEANT TO BE FORKED AND MODIFIED into your own game — even a
|
|
18
|
+
* very different one. The markers tell you what's what:
|
|
19
|
+
* HARDWARE IDIOM (load-bearing) — dodges a documented C64 footgun; reshape
|
|
20
|
+
* your gameplay around it (see TROUBLESHOOTING before changing).
|
|
21
|
+
* GAME LOGIC (clay) — court art, ball physics, CPU skill, scoring rules:
|
|
22
|
+
* reshape freely.
|
|
23
|
+
*
|
|
24
|
+
* What depends on what:
|
|
25
|
+
* c64_registers.h — VIC-II / SID / CIA symbolic addresses (header only).
|
|
26
|
+
* c64_sfx.{h,c} — one-shot SID sound effects on voice 2.
|
|
27
|
+
* The BASIC stub + crt0 come from cc65's c64 target: the .prg loads at
|
|
28
|
+
* $0801, a tiny BASIC line does SYS into the C runtime, and the KERNAL
|
|
29
|
+
* stays banked in (we lean on that for the IRQ vector — see below).
|
|
30
|
+
*
|
|
31
|
+
* Memory map this file assumes (VIC bank 0 = $0000-$3FFF):
|
|
32
|
+
* $0400 screen RAM (40×25 chars) $D800 color RAM (per-cell color)
|
|
33
|
+
* $0801 this program (code+data grow up from here)
|
|
34
|
+
* $3F00 sprite images (2 × 64 bytes) — NOT $0800, which collides with
|
|
35
|
+
* the .prg load address, and NOT $1000-$1FFF, where the VIC sees
|
|
36
|
+
* the character ROM instead of RAM (a classic invisible-sprite trap).
|
|
37
|
+
* Keep the program under ~14 KB so it stays below $3F00.
|
|
38
|
+
*
|
|
39
|
+
* Frame budget (PAL, 50fps): 3 sprites + 2 paddle AABB tests + a couple of
|
|
40
|
+
* HUD digits — a sliver of one frame even on the 1MHz 6510. The court is a
|
|
41
|
+
* STATIC field of chars painted once at match start and never touched during
|
|
42
|
+
* play (only the HUD digits change, and they live in the fixed bar), so the
|
|
43
|
+
* C64's full-repaint famine (a whole-screen 880-cell repaint freezes ~50
|
|
44
|
+
* frames; see the puzzle template's cell-diff note) never comes up here.
|
|
45
|
+
*/
|
|
6
46
|
|
|
7
47
|
#include "c64_registers.h"
|
|
8
48
|
#include "c64_sfx.h"
|
|
9
49
|
#include <stdint.h>
|
|
10
50
|
|
|
51
|
+
/* cc65 KERNAL disk-I/O prototypes. We DON'T #include <cbm.h> — it drags in
|
|
52
|
+
* <c64.h>, whose VIC/SID/JOY macros collide with this project's
|
|
53
|
+
* c64_registers.h (cc65 errors "macro redefinition is not identical"). These
|
|
54
|
+
* four are the stable cc65 ABI; declaring them directly avoids the clash. */
|
|
55
|
+
unsigned char __fastcall__ cbm_open(unsigned char lfn, unsigned char device,
|
|
56
|
+
unsigned char sec_addr, const char *name);
|
|
57
|
+
void __fastcall__ cbm_close(unsigned char lfn);
|
|
58
|
+
int __fastcall__ cbm_read(unsigned char lfn, void *buffer, unsigned int size);
|
|
59
|
+
int __fastcall__ cbm_write(unsigned char lfn, const void *buffer, unsigned int size);
|
|
60
|
+
|
|
61
|
+
/* The title screen renders this — examples({op:'fork'}) stamps your game's
|
|
62
|
+
* name here automatically. Keep it ≤16 chars of A-Z 0-9 space dash. */
|
|
63
|
+
#define GAME_TITLE "DELTA DUEL"
|
|
64
|
+
|
|
11
65
|
#define POKE(addr, val) (*(volatile uint8_t*)(addr) = (val))
|
|
12
66
|
#define PEEK(addr) (*(volatile uint8_t*)(addr))
|
|
13
67
|
|
|
14
68
|
#define SCREEN ((volatile uint8_t*)0x0400)
|
|
15
69
|
#define COLORS ((volatile uint8_t*)0xD800)
|
|
70
|
+
#define SPRITE_POINTERS ((volatile uint8_t*)0x07F8) /* last 8 bytes of screen RAM */
|
|
71
|
+
|
|
72
|
+
/* ── GAME LOGIC (clay — reshape freely) — sprite art (24×21, 3 bytes/row) ──
|
|
73
|
+
* Three VIC-II hardware sprites: P1 paddle, P2/CPU paddle, the ball. The
|
|
74
|
+
* court (rails + net + floor) is CHARACTERS in screen RAM, so it costs no
|
|
75
|
+
* sprite slots — leaving the other 5 VIC sprites free for your fork. */
|
|
76
|
+
#define SLOT_P1 0
|
|
77
|
+
#define SLOT_P2 1
|
|
78
|
+
#define SLOT_BALL 2
|
|
79
|
+
#define SPR_DATA(img) (0x3F00 + (img) * 64)
|
|
80
|
+
#define SPR_PTR(img) (uint8_t)(SPR_DATA(img) / 64) /* $3F00/64 = $FC */
|
|
81
|
+
#define IMG_PADDLE 0
|
|
82
|
+
#define IMG_BALL 1
|
|
83
|
+
|
|
84
|
+
/* A vertical bar ~6 px wide, 21 px tall — a paddle. (24×21 sprite; we light
|
|
85
|
+
* the middle columns so a thin paddle reads cleanly at any Y.) */
|
|
86
|
+
static const uint8_t paddle_sprite[64] = {
|
|
87
|
+
0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00,
|
|
88
|
+
0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00,
|
|
89
|
+
0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00,
|
|
90
|
+
0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00,
|
|
91
|
+
0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00, 0x07,0xE0,0x00, 0,
|
|
92
|
+
};
|
|
93
|
+
static const uint8_t ball_sprite[64] = { /* a small round-ish blob */
|
|
94
|
+
0,0,0, 0,0,0, 0,0,0, 0x03,0xC0,0x00, 0x0F,0xF0,0x00,
|
|
95
|
+
0x1F,0xF8,0x00, 0x1F,0xF8,0x00, 0x1F,0xF8,0x00, 0x1F,0xF8,0x00,
|
|
96
|
+
0x0F,0xF0,0x00, 0x03,0xC0,0x00, 0,0,0, 0,0,0,
|
|
97
|
+
0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,0,0, 0,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
/* ── HARDWARE IDIOM (load-bearing — reshape gameplay around this; see TROUBLESHOOTING) ──
|
|
101
|
+
* THE RASTER-IRQ SPLIT — the C64's classic "fixed status bar over a moving
|
|
102
|
+
* world" trick (and the gateway drug to all raster effects). Here it pins a
|
|
103
|
+
* HUD bar at the top while the court lives below it. The VIC-II has ONE
|
|
104
|
+
* $D016 fine-scroll for the whole frame; we don't scroll the court (a Pong
|
|
105
|
+
* arena holds still), but the split is STILL the idiomatic way to guarantee
|
|
106
|
+
* the HUD's first rows render in a known, fixed scroll state regardless of
|
|
107
|
+
* what the rest of the frame does — and it gives you the per-frame heartbeat
|
|
108
|
+
* the main loop paces on. Two IRQs ping-pong per frame:
|
|
109
|
+
*
|
|
110
|
+
* line 68 (inside the blank spacer row 2): assert the court's $D016
|
|
111
|
+
* → everything below the split renders in the court's scroll state
|
|
112
|
+
* line 251 (just past the text window): assert the bar's $D016
|
|
113
|
+
* → next frame's HUD rows render fixed; this IRQ is also the
|
|
114
|
+
* game's frame heartbeat (increments frame_count)
|
|
115
|
+
*
|
|
116
|
+
* The handshake, register by register:
|
|
117
|
+
* $D012 raster compare line (low 8 bits)
|
|
118
|
+
* $D011 b7 raster compare bit 8 — MUST be 0 here (both lines < 256).
|
|
119
|
+
* Forgetting this bit is the classic "my IRQ fires on the
|
|
120
|
+
* wrong line / twice" bug when lines ≥ 256 get involved.
|
|
121
|
+
* $D01A b0 raster IRQ enable
|
|
122
|
+
* $D019 IRQ latch. ACK by WRITING THE BITS BACK (write-1-to-clear).
|
|
123
|
+
* THE LOAD-BEARING LINE: skip the ack and the IRQ re-fires the
|
|
124
|
+
* instant it returns, forever — the main loop starves and the
|
|
125
|
+
* machine looks hung.
|
|
126
|
+
* $0314/15 the KERNAL's IRQ indirection. The hardware vector ($FFFE)
|
|
127
|
+
* points into KERNAL ROM, which saves A/X/Y and jumps through
|
|
128
|
+
* $0314 — so with the KERNAL banked in (cc65 default) we just
|
|
129
|
+
* repoint $0314. Exit via jmp $EA81 (KERNAL: restore regs +
|
|
130
|
+
* rti), SKIPPING $EA31's jiffy-clock/keyboard scan.
|
|
131
|
+
* $DC0D CIA1 interrupt control. The KERNAL leaves a 60Hz CIA timer
|
|
132
|
+
* IRQ running (the jiffy clock); disable it ($7F = clear all
|
|
133
|
+
* sources) and ack it (read $DC0D) or it shares the IRQ line
|
|
134
|
+
* with the raster and fires our handler at random lines.
|
|
135
|
+
*
|
|
136
|
+
* JITTER: an IRQ only starts after the current instruction finishes, so the
|
|
137
|
+
* handler begins 0-7 cycles late, plus the KERNAL thunk (~35 cycles) — the
|
|
138
|
+
* $D016 write lands one-to-two raster lines after SPLIT_LINE. We hide that
|
|
139
|
+
* by splitting inside a UNIFORM blank row, where shifting the (invisible)
|
|
140
|
+
* pixels mid-line changes nothing. Splits next to visible detail need
|
|
141
|
+
* cycle-exact stabilization (double-IRQ trick) — don't go there until you do.
|
|
142
|
+
*
|
|
143
|
+
* The handler is ASSEMBLY-IN-C on purpose: cc65's generated C uses shared
|
|
144
|
+
* zero-page scratch registers, so a C-level IRQ body would corrupt whatever
|
|
145
|
+
* the main loop was computing. These asm lines touch only A + the flags
|
|
146
|
+
* (which the KERNAL thunk already saved). requires: KERNAL banked in,
|
|
147
|
+
* frame_count file-scope NON-static (asm %v needs the symbol). */
|
|
148
|
+
#define SPLIT_LINE 68 /* inside spacer row 2 (67-74): jitter-proof */
|
|
149
|
+
#define BOTTOM_LINE 251 /* first line below the 25-row text window */
|
|
150
|
+
#define D016_BAR 0xC0 /* fine X = 0, 38-col mode for both halves */
|
|
151
|
+
|
|
152
|
+
volatile uint8_t frame_count; /* bumped by the bottom IRQ — frame heartbeat */
|
|
16
153
|
|
|
154
|
+
void raster_irq(void) {
|
|
155
|
+
asm("lda $d019"); /* read VIC IRQ latch... */
|
|
156
|
+
asm("sta $d019"); /* ...write it back = ACK (write-1-to-clear).
|
|
157
|
+
* THE line you must not lose (see above). */
|
|
158
|
+
asm("lda $d012"); /* which raster line woke us? (self-correcting
|
|
159
|
+
* dispatch — no phase variable to desync) */
|
|
160
|
+
asm("cmp #150");
|
|
161
|
+
asm("bcs %g", at_bottom); /* ≥150 → we're at BOTTOM_LINE */
|
|
162
|
+
/* — split point (line ~68, inside the blank spacer row) — */
|
|
163
|
+
asm("lda #$C0"); /* = D016_BAR — court holds still, same scroll */
|
|
164
|
+
asm("sta $d016");
|
|
165
|
+
asm("lda #251"); /* = BOTTOM_LINE (cc65's asm %b only takes */
|
|
166
|
+
asm("sta $d012"); /* signed bytes, so these are literals — the */
|
|
167
|
+
asm("jmp $ea81"); /* #if below keeps them honest) */
|
|
168
|
+
at_bottom:
|
|
169
|
+
asm("lda #$C0"); /* = D016_BAR */
|
|
170
|
+
asm("sta $d016"); /* bar scroll for the top of the NEXT frame */
|
|
171
|
+
asm("inc %v", frame_count);/* frame heartbeat for the main loop */
|
|
172
|
+
asm("lda #%b", SPLIT_LINE);
|
|
173
|
+
asm("sta $d012"); /* next stop: the split line */
|
|
174
|
+
asm("jmp $ea81"); /* KERNAL: pla/tay/pla/tax/pla/rti */
|
|
175
|
+
}
|
|
176
|
+
#if BOTTOM_LINE != 251 || D016_BAR != 0xC0
|
|
177
|
+
#error raster_irq's asm immediates are out of sync with BOTTOM_LINE / D016_BAR
|
|
178
|
+
#endif
|
|
179
|
+
|
|
180
|
+
static void install_raster_irq(void) {
|
|
181
|
+
asm("sei"); /* no IRQs while we rewire them */
|
|
182
|
+
POKE(CIA1_PRA + 0x0D, 0x7F); /* $DC0D: disable ALL CIA1 IRQ sources
|
|
183
|
+
* (kills the KERNAL jiffy/keyboard IRQ
|
|
184
|
+
* — we read the sticks ourselves) */
|
|
185
|
+
(void)PEEK(CIA1_PRA + 0x0D); /* reading $DC0D acks anything pending */
|
|
186
|
+
POKE(0x0314, (uint8_t)((unsigned)raster_irq & 0xFF));
|
|
187
|
+
POKE(0x0315, (uint8_t)((unsigned)raster_irq >> 8));
|
|
188
|
+
POKE(VIC_CTRL1, 0x1B); /* $D011 = power-on default: screen on,
|
|
189
|
+
* 25 rows, YSCROLL=3, and bit 7 (raster
|
|
190
|
+
* compare bit 8) = 0 — both lines < 256 */
|
|
191
|
+
POKE(VIC_RASTER, SPLIT_LINE); /* first stop */
|
|
192
|
+
POKE(VIC_IRQ_ENA, 0x01); /* raster IRQ on */
|
|
193
|
+
POKE(VIC_IRQ, 0xFF); /* clear any stale latch bits */
|
|
194
|
+
asm("cli");
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* Wait for the bottom IRQ's heartbeat. Replaces the usual poll-$D012 loop —
|
|
198
|
+
* the IRQ owns the raster now, the main loop just paces itself on it. */
|
|
199
|
+
static void wait_frame(void) {
|
|
200
|
+
uint8_t f = frame_count;
|
|
201
|
+
while (frame_count == f) { }
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/* ── HARDWARE IDIOM (load-bearing — see TROUBLESHOOTING) ── reading BOTH
|
|
205
|
+
* joystick ports. CIA1 port A ($DC00) = control port 2, port B ($DC01) =
|
|
206
|
+
* control port 1. Active-low: a pressed switch reads 0, so invert and mask
|
|
207
|
+
* to bits 0-4 (up/down/left/right/fire).
|
|
208
|
+
*
|
|
209
|
+
* THE PORT-1 GOTCHA: $DC01 is ALSO the keyboard row register — the matrix
|
|
210
|
+
* hangs off the same CIA lines. Writing $FF to $DC00 first deselects every
|
|
211
|
+
* keyboard column, so held keys can't pull $DC01 rows low and ghost into
|
|
212
|
+
* the port-1 stick. That's also why "port 2 is the C64 game port": P1 lives
|
|
213
|
+
* there by convention, and this game puts the SECOND player on port 1.
|
|
214
|
+
* requires: install_raster_irq already disabled the KERNAL's keyboard scan,
|
|
215
|
+
* so nothing else rewrites $DC00. */
|
|
216
|
+
static uint8_t read_stick_port2(void) { /* player 1 */
|
|
217
|
+
POKE(CIA1_PRA, 0xFF);
|
|
218
|
+
return (uint8_t)(~PEEK(CIA1_PRA) & 0x1F);
|
|
219
|
+
}
|
|
220
|
+
static uint8_t read_stick_port1(void) { /* player 2 */
|
|
221
|
+
POKE(CIA1_PRA, 0xFF);
|
|
222
|
+
return (uint8_t)(~PEEK(CIA1_PRB) & 0x1F);
|
|
223
|
+
}
|
|
17
224
|
#define JOY_UP 0x01
|
|
18
225
|
#define JOY_DOWN 0x02
|
|
226
|
+
#define JOY_LEFT 0x04
|
|
227
|
+
#define JOY_RIGHT 0x08
|
|
228
|
+
#define JOY_FIRE 0x10
|
|
19
229
|
|
|
20
|
-
|
|
230
|
+
/* ── HARDWARE IDIOM (load-bearing) — best-streak persistence: DISK SAVE ──────
|
|
231
|
+
* The C64 has no battery SRAM — the honest save medium is the FLOPPY. The game
|
|
232
|
+
* persists by writing a file to drive 8; VICE commits it into the live 1541
|
|
233
|
+
* disk image (true-drive GCR write-back), so a save survives a power cycle
|
|
234
|
+
* exactly as on real hardware. REQUIRES THE GAME RUN FROM A DISK (build/load a
|
|
235
|
+
* .d64); a bare .prg has no mounted disk, so the save is a silent no-op (still
|
|
236
|
+
* honest — the record just stays in-session). Implemented in the load/save
|
|
237
|
+
* functions below; these two are the STABLE SEAM (load at boot, save on a new
|
|
238
|
+
* record) — reshape the record format freely, keep the signatures.
|
|
239
|
+
*
|
|
240
|
+
* Persistence choice (same as every platform's sports template): for a VERSUS
|
|
241
|
+
* game a raw hi-score is meaningless (every match ends 5-x), so we persist the
|
|
242
|
+
* LONGEST 1P-vs-CPU WIN STREAK — the stat a returning player actually chases.
|
|
243
|
+
* 2P matches never touch it (humans beating each other isn't a record). */
|
|
244
|
+
/* ── HARDWARE IDIOM (load-bearing) — record persistence: DISK SAVE ─────────
|
|
245
|
+
* The C64 has no battery SRAM — the honest save medium is the FLOPPY. A game
|
|
246
|
+
* persists by writing a file to drive 8; VICE commits it into the live 1541
|
|
247
|
+
* disk image (true-drive GCR write-back), so a save survives a power cycle
|
|
248
|
+
* exactly as it did on real hardware. (To capture it headlessly the host does
|
|
249
|
+
* state({op:'exportDisk', path}); re-loading that .d64 restores the save.)
|
|
250
|
+
*
|
|
251
|
+
* REQUIRES THE GAME RUN FROM A DISK: build/package it as a .d64 and load THAT
|
|
252
|
+
* (loadMedia autostarts it). A bare .prg injected straight into RAM has no
|
|
253
|
+
* mounted disk to save to, so the save is a silent no-op — still honest (the
|
|
254
|
+
* value just stays in-session), it simply has nowhere to persist.
|
|
255
|
+
*
|
|
256
|
+
* We keep a 2-byte record in a SEQ file "HI" on drive 8. These are the STABLE
|
|
257
|
+
* SEAM: the game calls hiscore_load at boot and hiscore_save on a new record;
|
|
258
|
+
* reshape the record format freely, just keep the two function signatures. */
|
|
259
|
+
#define SAVE_NAME "@0:HI,S,W" /* @ = replace-if-exists; S=SEQ, W=write */
|
|
260
|
+
#define LOAD_NAME "0:HI,S,R"
|
|
21
261
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
262
|
+
static uint16_t hiscore_load(void) {
|
|
263
|
+
uint16_t v = 0;
|
|
264
|
+
uint8_t buf[2];
|
|
265
|
+
if (cbm_open(2, 8, 2, LOAD_NAME) == 0) {
|
|
266
|
+
if (cbm_read(2, buf, 2) == 2) v = (uint16_t)buf[0] | ((uint16_t)buf[1] << 8);
|
|
267
|
+
cbm_close(2);
|
|
268
|
+
}
|
|
269
|
+
return v; /* 0 if the file isn't there yet (first ever boot) */
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
static void hiscore_save(uint16_t v) {
|
|
273
|
+
uint8_t buf[2];
|
|
274
|
+
buf[0] = (uint8_t)(v & 0xFF);
|
|
275
|
+
buf[1] = (uint8_t)(v >> 8);
|
|
276
|
+
if (cbm_open(2, 8, 2, SAVE_NAME) == 0) {
|
|
277
|
+
cbm_write(2, buf, 2);
|
|
278
|
+
cbm_close(2);
|
|
279
|
+
}
|
|
280
|
+
/* No disk mounted (ran as a bare .prg) -> cbm_open fails -> silent no-op. */
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
/* ── GAME LOGIC (clay) — SID music: 2 voices + THE filter sweep ─────────────
|
|
284
|
+
* Voice 0 = melody (pulse), voice 1 = bass (sawtooth THROUGH THE FILTER),
|
|
285
|
+
* voice 2 is reserved for sound effects (c64_sfx). Each voice walks a
|
|
286
|
+
* (freq, frames) note table once per frame; end wraps → continuous loop.
|
|
287
|
+
*
|
|
288
|
+
* THE SID FILTER — the C64's sonic signature, and the part most "music
|
|
289
|
+
* drivers ported from other chips" miss. One analog-modeled filter, shared
|
|
290
|
+
* by all voices, four registers:
|
|
291
|
+
* $D415 cutoff low 3 bits $D416 cutoff high 8 bits (11-bit total)
|
|
292
|
+
* $D417 high nibble = resonance 0-15; low 3 bits ROUTE voices into the
|
|
293
|
+
* filter (bit0=voice0, bit1=voice1, bit2=voice2)
|
|
294
|
+
* $D418 bit4=lowpass bit5=bandpass bit6=highpass — AND master volume in
|
|
295
|
+
* bits 0-3. Volume and filter mode share a register: any "set
|
|
296
|
+
* volume" helper that writes plain $0F silently turns the filter
|
|
297
|
+
* OFF (c64_sfx's sfx_init does exactly that, so music_init runs
|
|
298
|
+
* AFTER it and re-asserts the mode bits).
|
|
299
|
+
* FOOTGUN: bit 7 of $D418 is "3OFF" — it MUTES voice 3 entirely.
|
|
300
|
+
* Set it by accident and all your sound effects vanish.
|
|
301
|
+
* The sweep: a triangle LFO walks the cutoff up and down each frame over
|
|
302
|
+
* the resonant lowpass — the bass goes from muffled to snarling and back,
|
|
303
|
+
* the "wah" that screams Commodore. Hear it change: that IS the chip. */
|
|
304
|
+
#define N_C3 0x1199u
|
|
305
|
+
#define N_D3 0x13EEu
|
|
306
|
+
#define N_E3 0x1666u
|
|
307
|
+
#define N_F3 0x1798u
|
|
308
|
+
#define N_G3 0x1AE6u
|
|
309
|
+
#define N_A3 0x1E78u
|
|
310
|
+
#define N_B3 0x2253u
|
|
311
|
+
#define N_C4 0x2333u
|
|
312
|
+
#define N_D4 0x27DDu
|
|
313
|
+
#define N_E4 0x2CCCu
|
|
314
|
+
#define N_F4 0x2F30u
|
|
315
|
+
#define N_G4 0x35CCu
|
|
316
|
+
#define N_A4 0x3CF1u
|
|
317
|
+
#define N_B4 0x44A7u
|
|
318
|
+
#define N_C5 0x4666u
|
|
319
|
+
#define N_D5 0x4FBAu
|
|
320
|
+
#define N_E5 0x5998u
|
|
321
|
+
#define N_REST 0u
|
|
322
|
+
#define STEP 9 /* frames per melodic eighth-note (~140 BPM PAL) */
|
|
323
|
+
|
|
324
|
+
typedef struct { uint16_t freq; uint8_t len; } Note;
|
|
325
|
+
|
|
326
|
+
/* The table IS the song — edit these to rescore your fork. A driving, bright
|
|
327
|
+
* sporting march to keep a rally tense. */
|
|
328
|
+
static const Note melody[] = {
|
|
329
|
+
{ N_E4, STEP }, { N_G4, STEP }, { N_C5, STEP*2 }, { N_G4, STEP }, { N_E4, STEP*2 }, { N_G4, STEP },
|
|
330
|
+
{ N_A4, STEP }, { N_C5, STEP }, { N_E5, STEP*2 }, { N_C5, STEP }, { N_A4, STEP*2 }, { N_REST, STEP },
|
|
331
|
+
{ N_D4, STEP }, { N_F4, STEP }, { N_A4, STEP*2 }, { N_F4, STEP }, { N_D4, STEP*2 }, { N_A4, STEP },
|
|
332
|
+
{ N_G4, STEP }, { N_B4, STEP }, { N_D5, STEP*2 }, { N_B4, STEP }, { N_G4, STEP*2 }, { N_REST, STEP },
|
|
333
|
+
{ N_C5, STEP }, { N_B4, STEP }, { N_A4, STEP }, { N_G4, STEP }, { N_A4, STEP }, { N_B4, STEP }, { N_C5, STEP*2 },
|
|
334
|
+
{ N_E4, STEP }, { N_G4, STEP }, { N_C5, STEP }, { N_E5, STEP }, { N_C5, STEP*2 }, { N_G4, STEP*2 },
|
|
30
335
|
};
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
0,0,0, 0,
|
|
336
|
+
static const Note bassline[] = {
|
|
337
|
+
/* Octave-pumping bass — the filter sweep chews on this. */
|
|
338
|
+
{ N_C3, STEP*3 }, { N_C4, STEP }, { N_C3, STEP*2 }, { N_G3, STEP*2 },
|
|
339
|
+
{ N_A3, STEP*3 }, { N_E3, STEP }, { N_A3, STEP*2 }, { N_C4, STEP*2 },
|
|
340
|
+
{ N_F3, STEP*3 }, { N_C3, STEP }, { N_F3, STEP*2 }, { N_A3, STEP*2 },
|
|
341
|
+
{ N_G3, STEP*3 }, { N_D3, STEP }, { N_G3, STEP*2 }, { N_B3, STEP*2 },
|
|
38
342
|
};
|
|
343
|
+
#define MELODY_LEN (sizeof(melody) / sizeof(melody[0]))
|
|
344
|
+
#define BASS_LEN (sizeof(bassline) / sizeof(bassline[0]))
|
|
39
345
|
|
|
40
|
-
static
|
|
41
|
-
|
|
42
|
-
|
|
346
|
+
static uint8_t m_pos[2], m_left[2];
|
|
347
|
+
static uint16_t filter_cut; /* 11-bit cutoff, 0-2047 */
|
|
348
|
+
static uint8_t filter_up;
|
|
349
|
+
|
|
350
|
+
static void music_trigger(uint8_t v, uint16_t freq, uint8_t wave) {
|
|
351
|
+
if (freq == N_REST) {
|
|
352
|
+
POKE(SID_CTRL(v), wave); /* gate off: release tail plays */
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
POKE(SID_FREQ_LO(v), (uint8_t)(freq & 0xFF));
|
|
356
|
+
POKE(SID_FREQ_HI(v), (uint8_t)(freq >> 8));
|
|
357
|
+
POKE(SID_CTRL(v), wave); /* gate OFF then ON — the 6581/8580 */
|
|
358
|
+
POKE(SID_CTRL(v), wave | SID_GATE); /* envelope only retriggers on the
|
|
359
|
+
* 0→1 gate edge */
|
|
43
360
|
}
|
|
44
361
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
/*
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
362
|
+
static void music_init(void) {
|
|
363
|
+
/* Melody: pulse at 50% duty, snappy envelope. */
|
|
364
|
+
POKE(SID_PW_LO(0), 0x00); POKE(SID_PW_HI(0), 0x08);
|
|
365
|
+
POKE(SID_AD(0), 0x07); /* attack 0, decay 7 */
|
|
366
|
+
POKE(SID_SR(0), 0x84); /* sustain 8, release 4 */
|
|
367
|
+
/* Bass: sawtooth (harmonically rich — gives the filter teeth to chew). */
|
|
368
|
+
POKE(SID_AD(1), 0x06);
|
|
369
|
+
POKE(SID_SR(1), 0xA5);
|
|
370
|
+
/* Filter: route VOICE 1 ONLY into it (bit 1 of $D417), resonance 13/15. */
|
|
371
|
+
POKE(SID_RES_FILT, 0xD2);
|
|
372
|
+
/* Lowpass mode + master volume 15. NOTE bits shared with volume, and bit
|
|
373
|
+
* 7 (3OFF) stays 0 or voice-2 sound effects go silent — see block doc. */
|
|
374
|
+
POKE(SID_VOL_MODE, 0x1F);
|
|
375
|
+
filter_cut = 0x180; filter_up = 1;
|
|
376
|
+
m_pos[0] = m_pos[1] = 0;
|
|
377
|
+
m_left[0] = m_left[1] = 1; /* triggers both voices on the next update */
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
static void music_update(void) {
|
|
381
|
+
/* Note sequencing, one table per voice. */
|
|
382
|
+
if (--m_left[0] == 0) {
|
|
383
|
+
music_trigger(0, melody[m_pos[0]].freq, SID_PULSE);
|
|
384
|
+
m_left[0] = melody[m_pos[0]].len;
|
|
385
|
+
if (++m_pos[0] >= MELODY_LEN) m_pos[0] = 0;
|
|
57
386
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
387
|
+
if (--m_left[1] == 0) {
|
|
388
|
+
music_trigger(1, bassline[m_pos[1]].freq, SID_SAWTOOTH);
|
|
389
|
+
m_left[1] = bassline[m_pos[1]].len;
|
|
390
|
+
if (++m_pos[1] >= BASS_LEN) m_pos[1] = 0;
|
|
62
391
|
}
|
|
63
|
-
/*
|
|
64
|
-
|
|
65
|
-
|
|
392
|
+
/* THE FILTER SWEEP — triangle LFO on the cutoff, ~10s round trip.
|
|
393
|
+
* 11-bit value split across two registers: low 3 bits in $D415,
|
|
394
|
+
* high 8 in $D416. */
|
|
395
|
+
if (filter_up) { filter_cut += 6; if (filter_cut >= 0x700) filter_up = 0; }
|
|
396
|
+
else { filter_cut -= 6; if (filter_cut <= 0x180) filter_up = 1; }
|
|
397
|
+
POKE(SID_FILTER_LO, (uint8_t)(filter_cut & 0x07));
|
|
398
|
+
POKE(SID_FILTER_HI, (uint8_t)(filter_cut >> 3));
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/* ── GAME LOGIC (clay) — screen text. The C64 has NO VRAM port: screen RAM
|
|
402
|
+
* is plain memory, writable any time, mid-frame, no vblank dance. The only
|
|
403
|
+
* translation is ASCII → SCREEN CODES (not PETSCII!): A-Z land at 1-26;
|
|
404
|
+
* space through '?' (incl. digits) keep their ASCII values. ── */
|
|
405
|
+
static void draw_text(uint8_t row, uint8_t col, const char *s) {
|
|
406
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
407
|
+
uint8_t ch;
|
|
408
|
+
while ((ch = (uint8_t)*s++) != 0) {
|
|
409
|
+
if (ch >= 'A' && ch <= 'Z') ch -= 64; /* A-Z → screen codes 1-26 */
|
|
410
|
+
SCREEN[off] = ch; /* 32-63 map straight through */
|
|
411
|
+
COLORS[off] = COLOR_WHITE;
|
|
412
|
+
++off;
|
|
66
413
|
}
|
|
67
414
|
}
|
|
415
|
+
/* Blank the whole 40-col row, then draw `s` on it — a clean text BAND, so
|
|
416
|
+
* message text reads cleanly over whatever the court left behind. */
|
|
417
|
+
static void draw_text_band(uint8_t row, uint8_t col, const char *s) {
|
|
418
|
+
uint8_t c;
|
|
419
|
+
volatile uint8_t *p = SCREEN + (uint16_t)row * 40;
|
|
420
|
+
for (c = 0; c < 40; c++) p[c] = 0x20;
|
|
421
|
+
draw_text(row, col, s);
|
|
422
|
+
}
|
|
68
423
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
424
|
+
/* One digit, used for the score readouts (a single 0-9). */
|
|
425
|
+
static void draw_digit(uint8_t row, uint8_t col, uint8_t d) {
|
|
426
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
427
|
+
SCREEN[off] = (uint8_t)('0' + (d % 10)); /* digit screen code = ASCII */
|
|
428
|
+
COLORS[off] = COLOR_WHITE;
|
|
429
|
+
}
|
|
430
|
+
static void draw_u16(uint8_t row, uint8_t col, uint16_t v) {
|
|
431
|
+
uint8_t i, dgt[5];
|
|
432
|
+
uint16_t off = (uint16_t)row * 40 + col;
|
|
433
|
+
for (i = 0; i < 5; i++) { dgt[i] = v % 10; v /= 10; }
|
|
434
|
+
for (i = 0; i < 5; i++) {
|
|
435
|
+
SCREEN[off + i] = (uint8_t)('0' + dgt[4 - i]);
|
|
436
|
+
COLORS[off + i] = COLOR_WHITE;
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/* ── GAME LOGIC (clay) — xorshift16 PRNG (a few instructions) ────────────────
|
|
441
|
+
* A versus game NEEDS this: the C64 is fully deterministic, so without a
|
|
442
|
+
* noise source two fixed strategies lock into an infinite rally loop (the
|
|
443
|
+
* exact same cycle, forever — an idle match would never end). random8() is
|
|
444
|
+
* ticked once per play frame, and a ±1 "spin" rides every deflection, so
|
|
445
|
+
* identical game states a few seconds apart diverge and the rally resolves. */
|
|
446
|
+
static uint16_t rng = 0xACE1;
|
|
447
|
+
static uint8_t random8(void) {
|
|
448
|
+
uint16_t r = rng;
|
|
449
|
+
r ^= r << 7;
|
|
450
|
+
r ^= r >> 9;
|
|
451
|
+
r ^= r << 8;
|
|
452
|
+
rng = r;
|
|
453
|
+
return (uint8_t)r;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/* ── GAME LOGIC (clay — reshape freely) — court geometry + match rules ───────
|
|
457
|
+
* The court window is char rows 3..24 (the raster split fixes rows 0-2 as the
|
|
458
|
+
* HUD bar). Paddles + ball are HARDWARE SPRITES positioned in VIC sprite-pixel
|
|
459
|
+
* coordinates, NOT char cells. VIC visible area starts at sprite X≈24, Y≈50;
|
|
460
|
+
* the 320×200 display spans X 24..343, Y 50..249. We keep the playfield inside
|
|
461
|
+
* a top/bottom margin so the ball stays under the HUD bar. */
|
|
462
|
+
#define COURT_TOP 84 /* sprite-Y of the top rail (under the bar) */
|
|
463
|
+
#define COURT_BOT 240 /* sprite-Y just below the court floor */
|
|
464
|
+
#define PADDLE_H 21 /* sprite is 21 px tall */
|
|
465
|
+
#define PADDLE_X1 40 /* P1 paddle X (left) */
|
|
466
|
+
#define PADDLE_X2 300 /* P2/CPU paddle X (right) */
|
|
467
|
+
#define BALL_LEFT 48 /* ball past here → P2 scores */
|
|
468
|
+
#define BALL_RIGHT 296 /* ball past here → P1 scores */
|
|
469
|
+
#define BALL_SZ 8 /* ball collision box */
|
|
470
|
+
#define WIN_SCORE 5 /* first to 5 takes the match */
|
|
471
|
+
#define P_SPEED 2 /* human paddle px/frame */
|
|
472
|
+
#define CPU_SPEED 1 /* CPU px/frame — half speed: BEATABLE */
|
|
473
|
+
|
|
474
|
+
static int16_t p1y, p2y; /* paddle top Y (sprite px) */
|
|
475
|
+
static int16_t bx, by; /* ball position (sprite px) */
|
|
476
|
+
static int8_t bdx, bdy; /* ball velocity (px/frame) */
|
|
477
|
+
static uint8_t score1, score2;
|
|
478
|
+
static uint8_t serve_timer; /* freeze frames between points */
|
|
479
|
+
static uint8_t two_player; /* title pick: 0 = vs CPU, 1 = 2P versus */
|
|
480
|
+
static uint8_t streak; /* current 1P-vs-CPU win streak (RAM) */
|
|
481
|
+
static uint16_t best_streak; /* record — see end_match / the seam */
|
|
482
|
+
static uint8_t new_record; /* result screen shows NEW RECORD */
|
|
483
|
+
|
|
484
|
+
/* Game states — the shell every example shares: title → play → result. */
|
|
485
|
+
#define ST_TITLE 0
|
|
486
|
+
#define ST_PLAY 1
|
|
487
|
+
#define ST_OVER 2
|
|
488
|
+
static uint8_t state;
|
|
489
|
+
static uint8_t prev0, prev1; /* edge-triggered FIRE per port */
|
|
490
|
+
|
|
491
|
+
/* ── HARDWARE IDIOM (load-bearing) — staging a sprite with the 9th X bit.
|
|
492
|
+
* VIC sprite X is 9 bits: low 8 in $D000+2n, bit 8 for ALL sprites packed
|
|
493
|
+
* into $D010. Forget $D010 and anything past X=255 wraps back to the left
|
|
494
|
+
* edge — the classic "my sprite teleports at two-thirds screen" bug. The
|
|
495
|
+
* right paddle at X=300 lives ENTIRELY past 255, so this is load-bearing
|
|
496
|
+
* here, not optional. We accumulate the MSB bits while staging and commit
|
|
497
|
+
* the byte once. ── */
|
|
498
|
+
static uint8_t spr_msb, spr_ena;
|
|
499
|
+
static void stage_begin(void) { spr_msb = 0; spr_ena = 0; }
|
|
500
|
+
static void stage_sprite(uint8_t slot, int16_t x, uint8_t y) {
|
|
501
|
+
POKE(VIC_SPRITE_X(slot), (uint8_t)(x & 0xFF));
|
|
502
|
+
POKE(VIC_SPRITE_Y(slot), y);
|
|
503
|
+
if (x > 255) spr_msb |= (uint8_t)(1 << slot);
|
|
504
|
+
spr_ena |= (uint8_t)(1 << slot);
|
|
505
|
+
}
|
|
506
|
+
static void stage_commit(void) {
|
|
507
|
+
POKE(VIC_SPRITES_X8, spr_msb);
|
|
508
|
+
POKE(VIC_SPR_ENA, spr_ena); /* unstaged slots vanish — no stale sprites */
|
|
509
|
+
}
|
|
510
|
+
static void stage_actors(void) {
|
|
511
|
+
stage_begin();
|
|
512
|
+
stage_sprite(SLOT_P1, PADDLE_X1, (uint8_t)p1y);
|
|
513
|
+
stage_sprite(SLOT_P2, PADDLE_X2, (uint8_t)p2y);
|
|
514
|
+
stage_sprite(SLOT_BALL, bx, (uint8_t)by);
|
|
515
|
+
stage_commit();
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
/* ── GAME LOGIC (clay) — the HUD bar (rows 0-2, the fixed split) ────────────
|
|
519
|
+
* Scores live on the bar — the ONLY thing that changes during play, and it's
|
|
520
|
+
* one digit each. The court chars below never change mid-match, so play frames
|
|
521
|
+
* touch only 3 sprite positions + (on a point) 1 digit. ── */
|
|
522
|
+
static void draw_bar_labels(void) {
|
|
523
|
+
uint8_t c;
|
|
524
|
+
for (c = 0; c < 40; c++) { /* row 1: solid divider line */
|
|
525
|
+
SCREEN[40 + c] = 0xA0; /* reverse-space block */
|
|
526
|
+
COLORS[40 + c] = COLOR_DARK_GRAY;
|
|
527
|
+
SCREEN[80 + c] = 0x20; /* row 2: the blank spacer the
|
|
528
|
+
* raster split hides in */
|
|
529
|
+
SCREEN[c] = 0x20;
|
|
530
|
+
}
|
|
531
|
+
draw_text(0, 1, "P1");
|
|
532
|
+
draw_text(0, 31, two_player ? "P2" : "CPU");
|
|
533
|
+
}
|
|
534
|
+
static void draw_bar_stats(void) {
|
|
535
|
+
draw_digit(0, 4, score1);
|
|
536
|
+
draw_digit(0, 35, score2);
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/* ── GAME LOGIC (clay) — court field chars (rows 3..24). Painted ONCE per
|
|
540
|
+
* match start (a static screen — free to write directly), never during play.
|
|
541
|
+
* Top + bottom rails frame the court; a dashed net runs down the centre; a
|
|
542
|
+
* faint floor speckle so the arena reads as a court instead of a black void.
|
|
543
|
+
* (Compare the puzzle template's cell-diff: a Pong court doesn't change during
|
|
544
|
+
* play, so it needs NO per-frame repaint machinery at all.) ── */
|
|
545
|
+
#define CH_RAIL 0xA0 /* reverse-space solid block = rail */
|
|
546
|
+
#define CH_NET 0x5D /* vertical-bar glyph = centre net */
|
|
547
|
+
#define CH_DOT 0x2E /* '.' faint floor speckle */
|
|
548
|
+
#define CH_BLANK 0x20
|
|
549
|
+
#define FIELD_TOP 3
|
|
550
|
+
#define NET_COL 19 /* centre column of the 40-col field */
|
|
551
|
+
|
|
552
|
+
static void paint_court(void) {
|
|
553
|
+
uint8_t r, c;
|
|
554
|
+
for (r = FIELD_TOP; r < 25; r++) {
|
|
555
|
+
volatile uint8_t *srow = SCREEN + (uint16_t)r * 40;
|
|
556
|
+
volatile uint8_t *crow = COLORS + (uint16_t)r * 40;
|
|
557
|
+
for (c = 0; c < 40; c++) {
|
|
558
|
+
uint8_t ch = CH_BLANK, col = COLOR_BLACK;
|
|
559
|
+
if (r == FIELD_TOP || r == 24) { ch = CH_RAIL; col = COLOR_LIGHT_GRAY; }
|
|
560
|
+
else if (c == NET_COL) { ch = CH_NET; col = COLOR_DARK_GRAY; }
|
|
561
|
+
else if (((uint8_t)(c + (r << 2)) & 7) == 0) { ch = CH_DOT; col = COLOR_GREEN; }
|
|
562
|
+
srow[c] = ch;
|
|
563
|
+
crow[c] = col;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
/* Clear the whole 25-row screen to blanks. Static-screen op — cheap once. */
|
|
569
|
+
static void clear_screen(void) {
|
|
570
|
+
uint16_t i;
|
|
571
|
+
for (i = 0; i < 1000; i++) { SCREEN[i] = CH_BLANK; COLORS[i] = COLOR_BLACK; }
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/* ── GAME LOGIC (clay) — the title screen (static, free to repaint) ── */
|
|
575
|
+
static void paint_title(void) {
|
|
576
|
+
clear_screen();
|
|
577
|
+
two_player = 0;
|
|
578
|
+
draw_bar_labels();
|
|
579
|
+
draw_bar_stats();
|
|
580
|
+
draw_text_band(8, (40 - (sizeof(GAME_TITLE) - 1)) / 2, GAME_TITLE);
|
|
581
|
+
draw_text_band(12, 11, "PORT 2 FIRE - 1P VS CPU");
|
|
582
|
+
draw_text_band(14, 11, "PORT 1 FIRE - 2P VERSUS");
|
|
583
|
+
draw_text_band(16, 12, "UP DOWN - MOVE PADDLE");
|
|
584
|
+
draw_text_band(18, 13, "FIRST TO 5 WINS");
|
|
585
|
+
draw_text_band(21, 11, "BEST STREAK");
|
|
586
|
+
draw_u16(21, 23, best_streak);
|
|
587
|
+
POKE(VIC_SPR_ENA, 0); /* no sprites on the title */
|
|
588
|
+
state = ST_TITLE;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
/* ── GAME LOGIC (clay) — serve: ball to centre, toward the chosen side ──
|
|
592
|
+
* bx centre = 172 (midway between BALL_LEFT 48 and BALL_RIGHT 296). The serve
|
|
593
|
+
* angle alternates so successive serves don't trace the same path. */
|
|
594
|
+
static void serve_ball(uint8_t to_left) {
|
|
595
|
+
bx = 172;
|
|
596
|
+
by = 160;
|
|
597
|
+
bdx = to_left ? -2 : 2;
|
|
598
|
+
bdy = ((score1 + score2) & 1) ? -1 : 1;
|
|
599
|
+
serve_timer = 30; /* ~half-second breather */
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/* ── GAME LOGIC (clay) — start a match ── */
|
|
603
|
+
static void start_match(uint8_t players) {
|
|
604
|
+
two_player = players;
|
|
605
|
+
p1y = 150; p2y = 150;
|
|
606
|
+
score1 = 0; score2 = 0;
|
|
607
|
+
new_record = 0;
|
|
608
|
+
/* Stir the PRNG with time-spent-on-title so runs differ. */
|
|
609
|
+
rng ^= (uint16_t)frame_count ^ ((uint16_t)frame_count << 7);
|
|
610
|
+
if (rng == 0) rng = 0xACE1;
|
|
611
|
+
clear_screen();
|
|
612
|
+
draw_bar_labels();
|
|
613
|
+
draw_bar_stats();
|
|
614
|
+
paint_court();
|
|
615
|
+
serve_ball(0);
|
|
616
|
+
state = ST_PLAY;
|
|
617
|
+
prev0 = prev1 = 0x1F; /* swallow the FIRE that started the match */
|
|
618
|
+
sfx_tone(2, 0x00, 0x20, 10); /* start jingle */
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/* ── GAME LOGIC (clay) — match over: result + record bookkeeping.
|
|
622
|
+
* For a VERSUS sports game a raw hi-score is meaningless (every match ends
|
|
623
|
+
* 5-x), so we persist the longest 1P-vs-CPU win streak — the stat a returning
|
|
624
|
+
* player actually chases. 2P matches never touch it. ── */
|
|
625
|
+
static void end_match(void) {
|
|
626
|
+
uint8_t p1_won = (score1 >= WIN_SCORE);
|
|
627
|
+
clear_screen();
|
|
628
|
+
draw_bar_labels();
|
|
629
|
+
draw_bar_stats();
|
|
630
|
+
if (two_player) {
|
|
631
|
+
draw_text_band(8, 16, p1_won ? "P1 WINS" : "P2 WINS");
|
|
632
|
+
} else if (p1_won) {
|
|
633
|
+
draw_text_band(8, 16, "YOU WIN");
|
|
634
|
+
++streak;
|
|
635
|
+
if (streak > best_streak) {
|
|
636
|
+
best_streak = streak;
|
|
637
|
+
new_record = 1;
|
|
638
|
+
hiscore_save(best_streak); /* the persistence seam — see its block doc */
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
draw_text_band(8, 16, "CPU WINS");
|
|
642
|
+
streak = 0; /* the streak dies with the loss */
|
|
643
|
+
}
|
|
644
|
+
draw_text_band(11, 13, "P1");
|
|
645
|
+
draw_digit(11, 17, score1);
|
|
646
|
+
draw_text_band(13, 13, two_player ? "P2" : "CPU");
|
|
647
|
+
draw_digit(13, 18, score2);
|
|
648
|
+
draw_text_band(16, 11, "BEST STREAK");
|
|
649
|
+
draw_u16(16, 23, best_streak);
|
|
650
|
+
if (new_record) draw_text_band(18, 15, "NEW RECORD");
|
|
651
|
+
draw_text_band(21, 13, "FIRE - TITLE");
|
|
652
|
+
POKE(VIC_SPR_ENA, 0); /* sprites off on the result screen */
|
|
653
|
+
sfx_noise(24); /* end-of-match whistle */
|
|
654
|
+
state = ST_OVER;
|
|
655
|
+
prev0 = prev1 = 0x1F; /* swallow the held FIRE */
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
/* ── GAME LOGIC (clay) — one point scored ── */
|
|
659
|
+
static void score_point(uint8_t for_p1) {
|
|
660
|
+
if (for_p1) ++score1; else ++score2;
|
|
661
|
+
sfx_noise(6);
|
|
662
|
+
draw_bar_stats();
|
|
663
|
+
if (score1 >= WIN_SCORE || score2 >= WIN_SCORE) end_match();
|
|
664
|
+
else serve_ball(for_p1); /* loser of the point serves toward winner */
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/* ── GAME LOGIC (clay) — paddle hit: deflect by where the ball struck.
|
|
668
|
+
* Centre = flat-ish, edges = steep. Max |bdy| is 2 — the CPU moves at 1,
|
|
669
|
+
* so an edge hit is exactly how a human beats it. A ±1 random "spin" on
|
|
670
|
+
* every return keeps rallies from repeating (see the PRNG note above). ── */
|
|
671
|
+
static void deflect(int16_t paddle_y) {
|
|
672
|
+
int16_t rel = (by + BALL_SZ / 2) - (paddle_y + PADDLE_H / 2);
|
|
673
|
+
bdy = (int8_t)(rel >> 4);
|
|
674
|
+
bdy += (int8_t)((random8() & 2) - 1); /* spin: -1 or +1 */
|
|
675
|
+
if (bdy > 2) bdy = 2;
|
|
676
|
+
if (bdy < -2) bdy = -2;
|
|
677
|
+
if (bdy == 0) bdy = (rel < 0) ? -1 : 1; /* never return a flat ball */
|
|
678
|
+
sfx_tone(2, 0x00, 0x30, 4); /* paddle ping */
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/* ── GAME LOGIC (clay) — one player's paddle from a stick read ── */
|
|
682
|
+
static void move_paddle(int16_t *py, uint8_t pad) {
|
|
683
|
+
if ((pad & JOY_UP) && *py > COURT_TOP) *py -= P_SPEED;
|
|
684
|
+
if ((pad & JOY_DOWN) && *py < COURT_BOT - PADDLE_H) *py += P_SPEED;
|
|
73
685
|
}
|
|
74
686
|
|
|
75
687
|
void main(void) {
|
|
76
|
-
uint8_t
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
POKE(VIC_SPR_ENA, 0);
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
POKE(
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
POKE(VIC_SPRITE_X(1), 310 - 256);
|
|
688
|
+
uint8_t pad0, pad1;
|
|
689
|
+
uint8_t i;
|
|
690
|
+
|
|
691
|
+
/* ── HARDWARE IDIOM (load-bearing) — boot order. VIC + SID config before
|
|
692
|
+
* the IRQ goes live; sfx_init BEFORE music_init (sfx_init writes a plain
|
|
693
|
+
* volume to $D418, music_init re-asserts the filter-mode bits on top). ── */
|
|
694
|
+
POKE(VIC_SPR_ENA, 0); /* sprites off until staged */
|
|
695
|
+
POKE(VIC_BORDER, COLOR_BLUE); /* a coloured border keeps the
|
|
696
|
+
* screen visibly alive (no single
|
|
697
|
+
* colour dominates the pixel scan) */
|
|
698
|
+
POKE(VIC_BG0, COLOR_BLACK);
|
|
699
|
+
POKE(VIC_CTRL2, D016_BAR); /* 38-col mode from the start */
|
|
700
|
+
POKE(CIA1_DDRA, 0xFF); /* port A drives keyboard columns */
|
|
701
|
+
POKE(CIA1_DDRB, 0x00); /* port B reads rows / stick 1 */
|
|
702
|
+
|
|
703
|
+
/* Upload the two sprite images and point all three slots at them. */
|
|
704
|
+
{
|
|
705
|
+
volatile uint8_t *pd = (volatile uint8_t*)SPR_DATA(IMG_PADDLE);
|
|
706
|
+
volatile uint8_t *bd = (volatile uint8_t*)SPR_DATA(IMG_BALL);
|
|
707
|
+
for (i = 0; i < 64; i++) { pd[i] = paddle_sprite[i]; bd[i] = ball_sprite[i]; }
|
|
708
|
+
}
|
|
709
|
+
SPRITE_POINTERS[SLOT_P1] = SPR_PTR(IMG_PADDLE);
|
|
710
|
+
SPRITE_POINTERS[SLOT_P2] = SPR_PTR(IMG_PADDLE);
|
|
711
|
+
SPRITE_POINTERS[SLOT_BALL] = SPR_PTR(IMG_BALL);
|
|
712
|
+
POKE(VIC_SPR_COL(SLOT_P1), COLOR_CYAN);
|
|
713
|
+
POKE(VIC_SPR_COL(SLOT_P2), COLOR_LIGHT_RED);
|
|
714
|
+
POKE(VIC_SPR_COL(SLOT_BALL), COLOR_YELLOW);
|
|
104
715
|
|
|
105
716
|
sfx_init();
|
|
106
|
-
|
|
717
|
+
music_init();
|
|
718
|
+
best_streak = hiscore_load(); /* 0 until the core save round lands */
|
|
719
|
+
streak = 0;
|
|
720
|
+
|
|
721
|
+
clear_screen();
|
|
722
|
+
install_raster_irq(); /* the split + heartbeat go live */
|
|
723
|
+
paint_title();
|
|
107
724
|
|
|
108
725
|
for (;;) {
|
|
109
|
-
|
|
110
|
-
|
|
726
|
+
wait_frame(); /* the line-251 IRQ paces everything */
|
|
727
|
+
|
|
728
|
+
music_update();
|
|
111
729
|
sfx_update();
|
|
730
|
+
pad0 = read_stick_port2(); /* P1 — control port 2 (convention) */
|
|
731
|
+
pad1 = read_stick_port1(); /* P2 — control port 1 */
|
|
732
|
+
|
|
733
|
+
if (state == ST_TITLE) {
|
|
734
|
+
/* Mode select doubles as a controls demo: the stick that presses FIRE
|
|
735
|
+
* picks the mode — port 2 starts 1P vs CPU, port 1 starts 2P versus. */
|
|
736
|
+
if ((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) start_match(0);
|
|
737
|
+
else if ((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE)) start_match(1);
|
|
738
|
+
prev0 = pad0; prev1 = pad1;
|
|
739
|
+
continue;
|
|
740
|
+
}
|
|
112
741
|
|
|
113
|
-
if (
|
|
114
|
-
|
|
742
|
+
if (state == ST_OVER) {
|
|
743
|
+
if (((pad0 & JOY_FIRE) && !(prev0 & JOY_FIRE)) ||
|
|
744
|
+
((pad1 & JOY_FIRE) && !(prev1 & JOY_FIRE))) paint_title();
|
|
745
|
+
prev0 = pad0; prev1 = pad1;
|
|
746
|
+
continue;
|
|
747
|
+
}
|
|
115
748
|
|
|
116
|
-
/*
|
|
117
|
-
|
|
118
|
-
|
|
749
|
+
/* ── ST_PLAY ─────────────────────────────────────────────────────────
|
|
750
|
+
* Both paddles update EVERY frame: P1 from port 2, and either P2 from
|
|
751
|
+
* port 1 (2P) or the CPU (1P). Simultaneous versus, never alternating. */
|
|
752
|
+
random8(); /* tick the noise source per frame */
|
|
753
|
+
move_paddle(&p1y, pad0);
|
|
119
754
|
|
|
755
|
+
if (two_player) {
|
|
756
|
+
move_paddle(&p2y, pad1); /* P2 — control port 1 */
|
|
757
|
+
} else {
|
|
758
|
+
/* CPU — chases the ball centre at half player speed with a dead zone.
|
|
759
|
+
* Beatable by design: a steep edge-deflection outruns it. */
|
|
760
|
+
int16_t target = by + BALL_SZ / 2 - PADDLE_H / 2;
|
|
761
|
+
if (p2y + 2 < target && p2y < COURT_BOT - PADDLE_H) p2y += CPU_SPEED;
|
|
762
|
+
else if (p2y > target + 2 && p2y > COURT_TOP) p2y -= CPU_SPEED;
|
|
763
|
+
}
|
|
764
|
+
prev0 = pad0; prev1 = pad1;
|
|
765
|
+
|
|
766
|
+
/* Ball update (frozen during the post-point serve pause). */
|
|
767
|
+
if (serve_timer > 0) { --serve_timer; stage_actors(); continue; }
|
|
120
768
|
bx += bdx;
|
|
121
769
|
by += bdy;
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
if (
|
|
126
|
-
|
|
770
|
+
|
|
771
|
+
/* Rail bounce (top/bottom of the court). */
|
|
772
|
+
if (by < COURT_TOP) { by = COURT_TOP; bdy = -bdy; sfx_tone(2, 0x00, 0x20, 3); }
|
|
773
|
+
if (by + BALL_SZ > COURT_BOT) { by = COURT_BOT - BALL_SZ; bdy = -bdy; sfx_tone(2, 0x00, 0x20, 3); }
|
|
774
|
+
|
|
775
|
+
/* Paddle collisions (direction-gated so the ball can't double-hit). */
|
|
776
|
+
if (bdx < 0
|
|
777
|
+
&& bx <= PADDLE_X1 + 8 && bx + BALL_SZ >= PADDLE_X1
|
|
778
|
+
&& by + BALL_SZ > p1y && by < p1y + PADDLE_H) {
|
|
779
|
+
bdx = -bdx;
|
|
780
|
+
bx = PADDLE_X1 + 8;
|
|
781
|
+
deflect(p1y);
|
|
127
782
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
783
|
+
if (bdx > 0
|
|
784
|
+
&& bx + BALL_SZ >= PADDLE_X2 && bx <= PADDLE_X2 + 8
|
|
785
|
+
&& by + BALL_SZ > p2y && by < p2y + PADDLE_H) {
|
|
786
|
+
bdx = -bdx;
|
|
787
|
+
bx = PADDLE_X2 - BALL_SZ;
|
|
788
|
+
deflect(p2y);
|
|
131
789
|
}
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (bx
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
POKE(VIC_SPRITE_X(2), (uint8_t)bx);
|
|
139
|
-
POKE(VIC_SPRITES_X8, (uint8_t)(0x02 | ((bx > 255) ? 0x04 : 0x00)));
|
|
140
|
-
POKE(VIC_SPRITE_Y(2), (uint8_t)by);
|
|
790
|
+
|
|
791
|
+
/* Off either side → point. (score_point may end the match.) */
|
|
792
|
+
if (bx < BALL_LEFT) { score_point(0); if (state != ST_PLAY) continue; }
|
|
793
|
+
if (bx > BALL_RIGHT) { score_point(1); if (state != ST_PLAY) continue; }
|
|
794
|
+
|
|
795
|
+
stage_actors(); /* commit the 3 sprite positions */
|
|
141
796
|
}
|
|
142
797
|
}
|