Phase 10: Fix all 28 code review bugs

Critical fixes:
- Screen RAM relocated from  to  (VIC register collision)
- RST8 preserved in vic_setup_mcm() (raster IRQ line 311 stability)
-  random source armed with NOISE waveform (WAIT duration variety)
- Makefile  now has real file rule (run-vice works from clean)
- .PHONY lists corrected

Medium fixes:
- DRAW fault timeout >= 500 (was > 500)
- Counter minimum now 001 (init to 0, increment before render)
- WAIT duration upper bound 250 (was 249)
- Border strobe 4 frames (was 5)
- GAMEOVER shows final score, reset on TITLE entry
- ADSR decay comments corrected
- clear_color_ram() redundant loop removed
- memmap_setup() redundant MMAP_RAM removed
- Makefile: VICE/:0/c1541 checks, setsid pgrep, ensure-oscar64
- WAIT stinger uses voice 0 (protects voice 2 for RNG)

Low fixes:
- WHACKED references updated to Nyuller
- font arrays use unsigned char
- memmap_restore() documented as unused
- PROG_C64.md banking table corrected
- Makefile: clean @ prefix, help docs, OPT guard
- Audio schedule off-by-one corrected (+1 frame/note)
This commit is contained in:
ballz
2026-07-18 20:04:15 +02:00
parent 97d4f4216b
commit 64b0790519
13 changed files with 183 additions and 158 deletions
+1 -1
View File
@@ -30,7 +30,7 @@ colors in the files are the colors the C64 will display):
| File | Resolution | Used for | | File | Resolution | Used for |
|------|------------|----------| |------|------------|----------|
| `source_images/screen_title.png` | 1390×1130 | Title screen — "WHACKED" logo, both characters facing off, sunset farm background. | | `source_images/screen_title.png` | 1390×1130 | Title screen — "Nyuller" logo, both characters facing off, sunset farm background. |
| `source_images/screen_waiting1.png` | 1401×1123 | "Get ready" screen — the two characters glaring at each other in profile. | | `source_images/screen_waiting1.png` | 1401×1123 | "Get ready" screen — the two characters glaring at each other in profile. |
| `source_images/screen_waiting_2.png` | 1390×1130 | "Wait for it" screen — full-body shot of the two characters in their stances, same scene as the title but cropped. | | `source_images/screen_waiting_2.png` | 1390×1130 | "Wait for it" screen — full-body shot of the two characters in their stances, same scene as the title but cropped. |
| `source_images/screen_win_hare.png` | 1391×1131 | Hare won — Hare punching Scoot in the face mid-scooter. | | `source_images/screen_win_hare.png` | 1391×1131 | Hare won — Hare punching Scoot in the face mid-scooter. |
+45 -25
View File
@@ -20,14 +20,18 @@ SRC_DIR := $(ROOT)/src
# --- oscar64 flags ---------------------------------------- # --- oscar64 flags ----------------------------------------
# Override with: make OPT=O3 (or O0/O1/O2/Os/g) # Override with: make OPT=O3 (or O0/O1/O2/Os/g)
OPT ?= O1 OPT ?= O1
OPT_FLAGS := -$(OPT) OPT_FLAGS := $(if $(OPT),-$(OPT))
# --- VICE flags ------------------------------------------- # --- VICE flags -------------------------------------------
VICE_FLAGS := +confirmonexit -drive8type 1541 VICE_FLAGS := +confirmonexit -drive8type 1541
# --- source files (for dependency tracking) ----------------
SRC_FILES := $(wildcard $(SRC_DIR)/*.c)
HDR_FILES := $(wildcard $(SRC_DIR)/*.h)
# --- phony targets ---------------------------------------- # --- phony targets ----------------------------------------
.PHONY: help compile run run-vice run-vice-cycle-exact \ .PHONY: help compile run run-vice run-vice-cycle \
play play-cycle-exact kill clean ensure-build-dir play play-cycle kill clean ensure-build-dir ensure-oscar64
# ============================================================ # ============================================================
# help (default) # help (default)
@@ -63,6 +67,9 @@ help:
@echo " nyuller.asm full 6502 listing" @echo " nyuller.asm full 6502 listing"
@echo " nyuller.map region/section/object placement" @echo " nyuller.map region/section/object placement"
@echo " nyuller.lbl VICE monitor label commands" @echo " nyuller.lbl VICE monitor label commands"
@echo " nyuller.int intermediate compiler output"
@echo " nyuller.dbj intermediate compiler output"
@echo " nyuller.csz intermediate compiler output"
@echo "" @echo ""
@echo " VICE note: -drive8type 1541 is required for autostart to work." @echo " VICE note: -drive8type 1541 is required for autostart to work."
@echo " Without this flag the autostart LOAD\"*\",8,1 fails with ?DEVICE" @echo " Without this flag the autostart LOAD\"*\",8,1 fails with ?DEVICE"
@@ -81,10 +88,9 @@ ensure-build-dir:
@mkdir -p "$(BUILD_DIR)" @mkdir -p "$(BUILD_DIR)"
# ============================================================ # ============================================================
# compile # ensure-oscar64 — build the compiler if missing
# ============================================================ # ============================================================
compile: ensure-build-dir ensure-oscar64:
@# Build the oscar64 compiler first if it doesn't exist.
@if [ ! -x "$(OSCAR64_BIN)" ]; then \ @if [ ! -x "$(OSCAR64_BIN)" ]; then \
echo "oscar64 compiler not found at $(OSCAR64_BIN); building it..."; \ echo "oscar64 compiler not found at $(OSCAR64_BIN); building it..."; \
cd "$(OSCAR64_DIR)" && make -C make compiler; \ cd "$(OSCAR64_DIR)" && make -C make compiler; \
@@ -94,22 +100,23 @@ compile: ensure-build-dir
echo " try: cd $(OSCAR64_DIR) && make -C make compiler" >&2; \ echo " try: cd $(OSCAR64_DIR) && make -C make compiler" >&2; \
exit 1; \ exit 1; \
fi fi
# ============================================================
# $(PRG) — compile main.c → nyuller.prg
# ============================================================
$(PRG): $(SRC_FILES) $(HDR_FILES) | ensure-build-dir ensure-oscar64
@echo "compiling $(SRC) with $(OSCAR64_BIN) -> $(BUILD_DIR)/" @echo "compiling $(SRC) with $(OSCAR64_BIN) -> $(BUILD_DIR)/"
cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) "$(SRC)" cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) "$(SRC)"
# ============================================================
# compile — convenience alias for $(PRG)
# ============================================================
compile: $(PRG)
# ============================================================ # ============================================================
# run — oscar64 built-in emulator (headless, fast) # run — oscar64 built-in emulator (headless, fast)
# ============================================================ # ============================================================
run: ensure-build-dir run: ensure-build-dir ensure-oscar64
@if [ ! -x "$(OSCAR64_BIN)" ]; then \
echo "oscar64 compiler not found at $(OSCAR64_BIN); building it..."; \
cd "$(OSCAR64_DIR)" && make -C make compiler; \
fi
@if [ ! -x "$(OSCAR64_BIN)" ]; then \
echo "error: $(OSCAR64_BIN) is still missing after build" >&2; \
echo " try: cd $(OSCAR64_DIR) && make -C make compiler" >&2; \
exit 1; \
fi
@echo "running $(SRC) in oscar64's built-in emulator" @echo "running $(SRC) in oscar64's built-in emulator"
cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) -e "$(SRC)" cd "$(SRC_DIR)" && "$(OSCAR64_BIN)" -i="$(OSCAR64_DIR)/include" -o="$(PRG)" $(OPT_FLAGS) -e "$(SRC)"
@@ -117,6 +124,8 @@ run: ensure-build-dir
# run-vice — VICE x64 (foreground, blocks terminal) # run-vice — VICE x64 (foreground, blocks terminal)
# ============================================================ # ============================================================
run-vice: $(D64) run-vice: $(D64)
@command -v x64 >/dev/null 2>&1 || { echo "error: x64 not found in PATH" >&2; exit 1; }
@[ -n "$$DISPLAY" ] || { echo "error: no \$DISPLAY" >&2; exit 1; }
@echo "running nyuller in VICE x64 (blocking)" @echo "running nyuller in VICE x64 (blocking)"
x64 $(VICE_FLAGS) -autostart "$(D64)" x64 $(VICE_FLAGS) -autostart "$(D64)"
@@ -124,13 +133,17 @@ run-vice: $(D64)
# run-vice-cycle — VICE x64sc (foreground, cycle-exact, slow) # run-vice-cycle — VICE x64sc (foreground, cycle-exact, slow)
# ============================================================ # ============================================================
run-vice-cycle: $(D64) run-vice-cycle: $(D64)
@command -v x64sc >/dev/null 2>&1 || { echo "error: x64sc not found in PATH" >&2; exit 1; }
@[ -n "$$DISPLAY" ] || { echo "error: no \$DISPLAY" >&2; exit 1; }
@echo "running nyuller in VICE x64sc (cycle-exact, blocking)" @echo "running nyuller in VICE x64sc (cycle-exact, blocking)"
x64sc $(VICE_FLAGS) -autostart "$(D64)" x64sc $(VICE_FLAGS) -autostart "$(D64)"
# ============================================================ # ============================================================
# play — VICE x64 detached (returns to shell) # play — VICE x64 detached (returns to shell)
# ============================================================ # ============================================================
play: compile $(D64) play: $(D64)
@command -v x64 >/dev/null 2>&1 || { echo "error: x64 not found in PATH" >&2; exit 1; }
@[ -n "$$DISPLAY" ] || { echo "error: no \$DISPLAY" >&2; exit 1; }
@# Kill any previous VICE first. @# Kill any previous VICE first.
@existing=$$(pgrep -f 'x64(sc)? \+confirmonexit.*nyuller\.d64' 2>/dev/null || true); \ @existing=$$(pgrep -f 'x64(sc)? \+confirmonexit.*nyuller\.d64' 2>/dev/null || true); \
if [ -n "$$existing" ]; then \ if [ -n "$$existing" ]; then \
@@ -141,11 +154,13 @@ play: compile $(D64)
fi fi
@echo "launching VICE x64 in the background with $(D64)..." @echo "launching VICE x64 in the background with $(D64)..."
@setsid nohup x64 $(VICE_FLAGS) -autostart "$(D64)" \ @setsid nohup x64 $(VICE_FLAGS) -autostart "$(D64)" \
> "$(LOG)" 2>&1 < /dev/null & echo $$! > "$(PID_FILE)" > "$(LOG)" 2>&1 < /dev/null &
@sleep 1 @sleep 1
@if kill -0 "$$(cat "$(PID_FILE)")" 2>/dev/null; then \ @pid=$$(pgrep -f 'x64 \+confirmonexit.*nyuller\.d64' 2>/dev/null | head -1); \
if [ -n "$$pid" ]; then \
echo "$$pid" > "$(PID_FILE)"; \
echo ""; \ echo ""; \
echo "VICE launched (PID $$(cat "$(PID_FILE)")). Look for the x64 window on your desktop."; \ echo "VICE launched (PID $$pid). Look for the x64 window on your desktop."; \
echo "Default keys: Port 1 (Hare) = Arrows + Right Shift (fire)"; \ echo "Default keys: Port 1 (Hare) = Arrows + Right Shift (fire)"; \
echo " Port 2 (Scoot) = W A S D + Left Ctrl (fire)"; \ echo " Port 2 (Scoot) = W A S D + Left Ctrl (fire)"; \
echo "Rebind under Settings → Input devices → Joystick settings."; \ echo "Rebind under Settings → Input devices → Joystick settings."; \
@@ -159,7 +174,9 @@ play: compile $(D64)
# ============================================================ # ============================================================
# play-cycle — VICE x64sc detached (cycle-exact, returns to shell) # play-cycle — VICE x64sc detached (cycle-exact, returns to shell)
# ============================================================ # ============================================================
play-cycle: compile $(D64) play-cycle: $(D64)
@command -v x64sc >/dev/null 2>&1 || { echo "error: x64sc not found in PATH" >&2; exit 1; }
@[ -n "$$DISPLAY" ] || { echo "error: no \$DISPLAY" >&2; exit 1; }
@existing=$$(pgrep -f 'x64(sc)? \+confirmonexit.*nyuller\.d64' 2>/dev/null || true); \ @existing=$$(pgrep -f 'x64(sc)? \+confirmonexit.*nyuller\.d64' 2>/dev/null || true); \
if [ -n "$$existing" ]; then \ if [ -n "$$existing" ]; then \
echo "killing previous VICE process(es): $$existing"; \ echo "killing previous VICE process(es): $$existing"; \
@@ -169,11 +186,13 @@ play-cycle: compile $(D64)
fi fi
@echo "launching VICE x64sc in the background with $(D64)..." @echo "launching VICE x64sc in the background with $(D64)..."
@setsid nohup x64sc $(VICE_FLAGS) -autostart "$(D64)" \ @setsid nohup x64sc $(VICE_FLAGS) -autostart "$(D64)" \
> "$(LOG)" 2>&1 < /dev/null & echo $$! > "$(PID_FILE)" > "$(LOG)" 2>&1 < /dev/null &
@sleep 1 @sleep 1
@if kill -0 "$$(cat "$(PID_FILE)")" 2>/dev/null; then \ @pid=$$(pgrep -f 'x64sc \+confirmonexit.*nyuller\.d64' 2>/dev/null | head -1); \
if [ -n "$$pid" ]; then \
echo "$$pid" > "$(PID_FILE)"; \
echo ""; \ echo ""; \
echo "VICE launched (PID $$(cat "$(PID_FILE)")). Look for the x64sc window on your desktop."; \ echo "VICE launched (PID $$pid). Look for the x64sc window on your desktop."; \
echo "Default keys: Port 1 (Hare) = Arrows + Right Shift (fire)"; \ echo "Default keys: Port 1 (Hare) = Arrows + Right Shift (fire)"; \
echo " Port 2 (Scoot) = W A S D + Left Ctrl (fire)"; \ echo " Port 2 (Scoot) = W A S D + Left Ctrl (fire)"; \
echo "Rebind under Settings → Input devices → Joystick settings."; \ echo "Rebind under Settings → Input devices → Joystick settings."; \
@@ -213,6 +232,7 @@ kill:
# All c1541 calls are wrapped in `|| true` so the final # All c1541 calls are wrapped in `|| true` so the final
# list-and-grep is the authoritative check. # list-and-grep is the authoritative check.
$(D64): $(PRG) | ensure-build-dir $(D64): $(PRG) | ensure-build-dir
@command -v c1541 >/dev/null 2>&1 || { echo "error: c1541 not found" >&2; exit 1; }
@echo "wrapping $(PRG) in $(D64) (VICE autostart needs a disk image)..." @echo "wrapping $(PRG) in $(D64) (VICE autostart needs a disk image)..."
@c1541 -format ny,of d64 "$(D64)" >/dev/null 2>&1 || true @c1541 -format ny,of d64 "$(D64)" >/dev/null 2>&1 || true
@echo "write $(PRG)" | c1541 "$(D64)" >/dev/null 2>&1 || true @echo "write $(PRG)" | c1541 "$(D64)" >/dev/null 2>&1 || true
@@ -228,7 +248,7 @@ $(D64): $(PRG) | ensure-build-dir
# clean # clean
# ============================================================ # ============================================================
clean: clean:
rm -f "$(PRG)" "$(D64)" "$(BUILD_DIR)/nyuller.asm" "$(BUILD_DIR)/nyuller.map" \ @rm -f "$(PRG)" "$(D64)" "$(BUILD_DIR)/nyuller.asm" "$(BUILD_DIR)/nyuller.map" \
"$(BUILD_DIR)/nyuller.lbl" "$(BUILD_DIR)/nyuller.int" "$(BUILD_DIR)/nyuller.dbj" \ "$(BUILD_DIR)/nyuller.lbl" "$(BUILD_DIR)/nyuller.int" "$(BUILD_DIR)/nyuller.dbj" \
"$(BUILD_DIR)/nyuller.csz" "$(PID_FILE)" "$(LOG)" "$(BUILD_DIR)/nyuller.csz" "$(PID_FILE)" "$(LOG)"
@echo "cleaned build/" @echo "cleaned build/"
+3 -3
View File
@@ -125,8 +125,8 @@ is also the CPU's bank-switch latch. The bit meanings:
| Bit | Name | Effect | | Bit | Name | Effect |
|-----|------|--------| |-----|------|--------|
| 0 | LORAM | 0=RAM at $A000-$BFFF, 1=BASIC ROM | | 0 | LORAM | Controls $A000-$BFFF (with HIRAM): 1+BASIC ROM visible |
| 1 | HIRAM | 0=RAM at $E000-$FFFF, 1=KERNAL ROM | | 1 | HIRAM | 0=RAM at $E000-$FFFF, 1=KERNAL ROM. Also gates BASIC ROM. |
| 2 | CHAREN | 0=CHAR ROM at $D000-$DFFF, 1=I/O (default) | | 2 | CHAREN | 0=CHAR ROM at $D000-$DFFF, 1=I/O (default) |
| 3 | Cassette Data Out | | 3 | Cassette Data Out |
| 4 | Cassette Switch Sense | | 4 | Cassette Switch Sense |
@@ -156,7 +156,7 @@ prefetch), but you almost never need to.
| $0300-$03FF | More KERNAL/BASIC pointers; **$0314-$0315 = IRQ vector, $0316-$0317 = BRK vector, $0318-$0319 = NMI vector** | | $0300-$03FF | More KERNAL/BASIC pointers; **$0314-$0315 = IRQ vector, $0316-$0317 = BRK vector, $0318-$0319 = NMI vector** |
| $0400-$07FF | Screen RAM (1000 bytes for the 40×25 text screen) | | $0400-$07FF | Screen RAM (1000 bytes for the 40×25 text screen) |
| $0800-$9FFF | Free BASIC program storage (38911 bytes) | | $0800-$9FFF | Free BASIC program storage (38911 bytes) |
| $A000-$BFFF | BASIC ROM (8 KB, visible iff LORAM=1) | | $A000-$BFFF | BASIC ROM (8 KB, visible iff LORAM=1 AND HIRAM=1) |
| $C000-$CFFF | Free for ML programs | | $C000-$CFFF | Free for ML programs |
| $D000-$D3FF | VIC-II registers (47 of them, mirrored every 64 bytes) | | $D000-$D3FF | VIC-II registers (47 of them, mirrored every 64 bytes) |
| $D400-$D7FF | SID registers (mirrored every 32 bytes; not on C128) | | $D400-$D7FF | SID registers (mirrored every 32 bytes; not on C128) |
+1 -1
View File
@@ -139,7 +139,7 @@ $21-$24) goes to $D800-$DBE7.
| Element | Value | | Element | Value |
|---------|-------| |---------|-------|
| Title screen | `source_images/screen_title.png` — full-screen image with "WHACKED" logo | | Title screen | `source_images/screen_title.png` — full-screen image with "Nyuller" logo |
| Waiting 1 | `source_images/screen_waiting1.png` — 1.2 sec, "ping" jingle | | Waiting 1 | `source_images/screen_waiting1.png` — 1.2 sec, "ping" jingle |
| Waiting 2 | `source_images/screen_waiting_2.png` — random 2-5 sec, suspense music | | Waiting 2 | `source_images/screen_waiting_2.png` — random 2-5 sec, suspense music |
| DRAW | white screen, big counter incrementing each frame, sharp stab | | DRAW | white screen, big counter incrementing each frame, sharp stab |
+3 -3
View File
@@ -72,9 +72,9 @@ total), DRAW noise (4 frames — checks `>= 4` which also lines up
correctly), WAIT retrigger cadence (the `mod 16` checks produce the correctly), WAIT retrigger cadence (the `mod 16` checks produce the
right 16-frame cycles). right 16-frame cycles).
**Decision:** Cosmetic. Not worth the schedule risk of changing **Decision:** Fixed. Adjusted schedule frame numbers: WIN now uses
`audio_step++` to the end of the function (would need to verify 11-frame notes (total 44 frames = 0.88s), GAMEOVER uses 31-frame
all 5 state schedules still work). notes (total 155 frames = 3.1s).
## 4. `docs/c64/vic/graphics_modes.md` has wrong color mapping (Review 7) ## 4. `docs/c64/vic/graphics_modes.md` has wrong color mapping (Review 7)
+36 -31
View File
@@ -20,12 +20,12 @@
// sounds on frames 8-15, 24-31, ...). // sounds on frames 8-15, 24-31, ...).
// STATE_DRAW voice 2 noise, attack=0, decay=1, sustain=0, // STATE_DRAW voice 2 noise, attack=0, decay=1, sustain=0,
// release=2; gated on for 4 frames. // release=2; gated on for 4 frames.
// STATE_WIN_* voice 0 triangle C5-E5-G5-C6 arpeggio, 10 frames // STATE_WIN_* voice 0 triangle C5-E5-G5-C6 arpeggio, 11 frames
// per note (~0.8s total); voice 1 triangle a major // per note (~0.88s total); voice 1 triangle a major
// third below (A4, C5, E5, A5) with sustain=4 to // third below (A4, C5, E5, A5) with sustain=4 to
// keep it "very quiet". // keep it "very quiet".
// STATE_GAMEOVER voice 0 triangle G4-C5-E5-G5-C6 fanfare, 30 // STATE_GAMEOVER voice 0 triangle G4-C5-E5-G5-C6 fanfare, 31
// frames per note (~3s total); voice 1 sustained C4 // frames per note (~3.1s total); voice 1 sustained C4
// (a perfect fifth below G4) throughout. // (a perfect fifth below G4) throughout.
// //
// **No leakage between states.** audio_state_enter() calls // **No leakage between states.** audio_state_enter() calls
@@ -58,7 +58,7 @@ static byte audio_state;
// Per-state frame counter. Reset to 0 on each audio_state_enter() // Per-state frame counter. Reset to 0 on each audio_state_enter()
// call and incremented at the start of each audio_state_step() call. // call and incremented at the start of each audio_state_step() call.
// 16 bits is enough for the longest cue (GAMEOVER is 150 frames). // 16 bits is enough for the longest cue (GAMEOVER is 155 frames).
static unsigned short audio_step; static unsigned short audio_step;
// Fault stinger counter. > 0 means the stinger is currently playing // Fault stinger counter. > 0 means the stinger is currently playing
@@ -100,19 +100,24 @@ void audio_init(void)
// wrote freq=0 to all 3 voices, which leaves the oscillator // wrote freq=0 to all 3 voices, which leaves the oscillator
// stopped and $D41B stuck at 0 — so the first round of every // stopped and $D41B stuck at 0 — so the first round of every
// session would always get the same (deterministic) WAIT // session would always get the same (deterministic) WAIT
// duration. Set voice 3's freq to max so the oscillator runs // duration. Set voice 3's freq to max and select the NOISE
// and $D41B is properly random from the very first sample. // waveform (required for the LFSR to run) so $D41B is properly
// random from the very first sample. No GATE bit → silent.
sid.voices[2].freq = 0xffff; sid.voices[2].freq = 0xffff;
sid.voices[2].ctrl = SID_CTRL_NOISE;
} }
void audio_stop(void) void audio_stop(void)
{ {
// Gate off all 3 voices and clear their registers. Cheap to do // Gate off voices 0-1 and clear their registers. Voice 3
// (24 bytes of writes) and guarantees no audio leakage when a // (index 2) is exempt from freq/ctrl zeroing because the game
// state transitions out. // uses its LFSR as a random source ($D41B). Setting ctrl to
// SID_CTRL_NOISE without GATE keeps it silent while the
// oscillator keeps running — zeroing freq/ctrl would freeze
// the LFSR and make $D41B return 0 every time.
sid.voices[0].ctrl = 0; sid.voices[0].ctrl = 0;
sid.voices[1].ctrl = 0; sid.voices[1].ctrl = 0;
sid.voices[2].ctrl = 0; sid.voices[2].ctrl = SID_CTRL_NOISE;
sid.voices[0].attdec = 0; sid.voices[0].attdec = 0;
sid.voices[1].attdec = 0; sid.voices[1].attdec = 0;
sid.voices[2].attdec = 0; sid.voices[2].attdec = 0;
@@ -121,7 +126,7 @@ void audio_stop(void)
sid.voices[2].susrel = 0; sid.voices[2].susrel = 0;
sid.voices[0].freq = 0; sid.voices[0].freq = 0;
sid.voices[1].freq = 0; sid.voices[1].freq = 0;
sid.voices[2].freq = 0; sid.voices[2].freq = 0xffff;
sid.voices[0].pwm = 0; sid.voices[0].pwm = 0;
sid.voices[1].pwm = 0; sid.voices[1].pwm = 0;
sid.voices[2].pwm = 0; sid.voices[2].pwm = 0;
@@ -241,7 +246,7 @@ void audio_state_enter(int state)
case STATE_READY: case STATE_READY:
// Ping-ping: triangle A4 then A5, 8 frames each. // Ping-ping: triangle A4 then A5, 8 frames each.
// ADSR: attack=0 (2ms), decay=9 (~114ms), no // ADSR: attack=0 (2ms), decay=4 (~114ms), no
// sustain, release=0 (6ms). With sustain=0 the // sustain, release=0 (6ms). With sustain=0 the
// envelope drops to 0 once decay finishes, but // envelope drops to 0 once decay finishes, but
// audio_state_step() retriggers the gate every 8 // audio_state_step() retriggers the gate every 8
@@ -283,7 +288,7 @@ void audio_state_enter(int state)
// pitch is taken from voice 3's oscillator // pitch is taken from voice 3's oscillator
// frequency (a SID hardware quirk); we set the // frequency (a SID hardware quirk); we set the
// freq to max for a "hissy" stab. ADSR: // freq to max for a "hissy" stab. ADSR:
// attack=0, decay=1 (6ms), sustain=0, release=2. // attack=0, decay=0 (6ms), sustain=0, release=2.
// Sustain=0 means the envelope drops to 0 almost // Sustain=0 means the envelope drops to 0 almost
// immediately, but the gate is held on for 4 // immediately, but the gate is held on for 4
// frames so the noise is at full volume the whole // frames so the noise is at full volume the whole
@@ -304,8 +309,8 @@ void audio_state_enter(int state)
// it "very quiet" per the spec. // it "very quiet" per the spec.
// ADSR: attack=0, decay=9 (~114ms), sustain=15 // ADSR: attack=0, decay=9 (~114ms), sustain=15
// (voice 0) or 4 (voice 1), release=15. Note // (voice 0) or 4 (voice 1), release=15. Note
// durations are 10 frames per note — total 40 // durations are 11 frames per note — total 44
// frames = 0.8s at 50 Hz. // frames = 0.88s at 50 Hz.
audio_setup_voice(0, NOTE_C5, SID_CTRL_TRI, audio_setup_voice(0, NOTE_C5, SID_CTRL_TRI,
SID_ATK_2 | SID_DKY_114, SID_ATK_2 | SID_DKY_114,
(15 << 4) | 0x0f, (15 << 4) | 0x0f,
@@ -322,8 +327,8 @@ void audio_state_enter(int state)
case STATE_GAMEOVER: case STATE_GAMEOVER:
// Voice 0: triangle G4-C5-E5-G5-C6 fanfare. // Voice 0: triangle G4-C5-E5-G5-C6 fanfare.
// Voice 1: sustained C4 (a perfect fifth below // Voice 1: sustained C4 (a perfect fifth below
// G4) throughout. Note durations are 30 frames // G4) throughout. Note durations are 31 frames
// — total 150 frames = 3.0s at 50 Hz. // — total 155 frames = 3.1s at 50 Hz.
// ADSR: attack=0, decay=9, sustain=15, release=15 // ADSR: attack=0, decay=9, sustain=15, release=15
// (a long held chord that rings out). // (a long held chord that rings out).
audio_setup_voice(0, NOTE_G4, SID_CTRL_TRI, audio_setup_voice(0, NOTE_G4, SID_CTRL_TRI,
@@ -413,60 +418,60 @@ void audio_state_step(int state)
case STATE_WIN_P1: case STATE_WIN_P1:
case STATE_WIN_P2: case STATE_WIN_P2:
// Arpeggio every 10 frames. Both voices // Arpeggio every 11 frames. Both voices
// advance in lockstep. Voice 0 plays the // advance in lockstep. Voice 0 plays the
// melody (C5, E5, G5, C6); voice 1 plays // melody (C5, E5, G5, C6); voice 1 plays
// the same notes a major third below // the same notes a major third below
// (A4, C5, E5, A5). At frame 40 we gate // (A4, C5, E5, A5). At frame 44 we gate
// both off (end of cue). // both off (end of cue).
if (audio_step == 10) { if (audio_step == 11) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
sid.voices[0].freq = NOTE_E5; sid.voices[0].freq = NOTE_E5;
audio_gate_on(0, SID_CTRL_TRI); audio_gate_on(0, SID_CTRL_TRI);
audio_gate_off(1, SID_CTRL_TRI); audio_gate_off(1, SID_CTRL_TRI);
sid.voices[1].freq = NOTE_C5; sid.voices[1].freq = NOTE_C5;
audio_gate_on(1, SID_CTRL_TRI); audio_gate_on(1, SID_CTRL_TRI);
} else if (audio_step == 20) { } else if (audio_step == 22) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
sid.voices[0].freq = NOTE_G5; sid.voices[0].freq = NOTE_G5;
audio_gate_on(0, SID_CTRL_TRI); audio_gate_on(0, SID_CTRL_TRI);
audio_gate_off(1, SID_CTRL_TRI); audio_gate_off(1, SID_CTRL_TRI);
sid.voices[1].freq = NOTE_E5; sid.voices[1].freq = NOTE_E5;
audio_gate_on(1, SID_CTRL_TRI); audio_gate_on(1, SID_CTRL_TRI);
} else if (audio_step == 30) { } else if (audio_step == 33) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
sid.voices[0].freq = NOTE_C6; sid.voices[0].freq = NOTE_C6;
audio_gate_on(0, SID_CTRL_TRI); audio_gate_on(0, SID_CTRL_TRI);
audio_gate_off(1, SID_CTRL_TRI); audio_gate_off(1, SID_CTRL_TRI);
sid.voices[1].freq = NOTE_A5; sid.voices[1].freq = NOTE_A5;
audio_gate_on(1, SID_CTRL_TRI); audio_gate_on(1, SID_CTRL_TRI);
} else if (audio_step >= 40) { } else if (audio_step >= 44) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
audio_gate_off(1, SID_CTRL_TRI); audio_gate_off(1, SID_CTRL_TRI);
} }
break; break;
case STATE_GAMEOVER: case STATE_GAMEOVER:
// Fanfare every 30 frames on voice 0; voice 1 // Fanfare every 31 frames on voice 0; voice 1
// sustains C4 throughout the entire cue and is // sustains C4 throughout the entire cue and is
// only gated off at the end (frame 150+). // only gated off at the end (frame 155+).
if (audio_step == 30) { if (audio_step == 31) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
sid.voices[0].freq = NOTE_C5; sid.voices[0].freq = NOTE_C5;
audio_gate_on(0, SID_CTRL_TRI); audio_gate_on(0, SID_CTRL_TRI);
} else if (audio_step == 60) { } else if (audio_step == 62) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
sid.voices[0].freq = NOTE_E5; sid.voices[0].freq = NOTE_E5;
audio_gate_on(0, SID_CTRL_TRI); audio_gate_on(0, SID_CTRL_TRI);
} else if (audio_step == 90) { } else if (audio_step == 93) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
sid.voices[0].freq = NOTE_G5; sid.voices[0].freq = NOTE_G5;
audio_gate_on(0, SID_CTRL_TRI); audio_gate_on(0, SID_CTRL_TRI);
} else if (audio_step == 120) { } else if (audio_step == 124) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
sid.voices[0].freq = NOTE_C6; sid.voices[0].freq = NOTE_C6;
audio_gate_on(0, SID_CTRL_TRI); audio_gate_on(0, SID_CTRL_TRI);
} else if (audio_step >= 150) { } else if (audio_step >= 155) {
audio_gate_off(0, SID_CTRL_TRI); audio_gate_off(0, SID_CTRL_TRI);
audio_gate_off(1, SID_CTRL_TRI); audio_gate_off(1, SID_CTRL_TRI);
} }
+2 -2
View File
@@ -26,7 +26,7 @@
// to use the font, but the banner is a separate concern: it doesn't // to use the font, but the banner is a separate concern: it doesn't
// touch the score bar's labels or pips, and it renders below the // touch the score bar's labels or pips, and it renders below the
// score bar (row 1) or in the body of the screen (rows 2+). // score bar (row 1) or in the body of the screen (rows 2+).
extern const char font[FONT_COUNT][8]; extern const unsigned char font[FONT_COUNT][8];
// Pixel-pair expansion table (same as score.c). Each 4-bit font // Pixel-pair expansion table (same as score.c). Each 4-bit font
// nibble is expanded to a byte where each "1" becomes "11" and each // nibble is expanded to a byte where each "1" becomes "11" and each
@@ -41,7 +41,7 @@ static const char expand4[16] = {
// — duplicated here so banner.c is self-contained. // — duplicated here so banner.c is self-contained.
#define BM_BASE ((char *)0xE000) #define BM_BASE ((char *)0xE000)
#define CRAM_BASE ((char *)0xD800) #define CRAM_BASE ((char *)0xD800)
#define SMEM_BASE ((char *)0xD000) #define SMEM_BASE ((char *)0xC400)
#define CELLS_PER_ROW 40 #define CELLS_PER_ROW 40
#define BYTES_PER_CELL 8 #define BYTES_PER_CELL 8
#define BYTES_PER_ROW 320 #define BYTES_PER_ROW 320
+20 -19
View File
@@ -106,7 +106,7 @@ static unsigned short draw_counter;
// //
// STATE_TITLE -> voice 0 (TITLE is silent; voice 1+2 free) // STATE_TITLE -> voice 0 (TITLE is silent; voice 1+2 free)
// STATE_READY -> voice 1 (READY uses voice 0 only; voice 1+2 free) // STATE_READY -> voice 1 (READY uses voice 0 only; voice 1+2 free)
// STATE_WAIT -> voice 2 (WAIT uses voice 0+1; voice 2 free) // STATE_WAIT -> voice 0 (WAIT uses voice 0+1; voice 2 is the RNG source)
// STATE_DRAW -> voice 0 (DRAW uses voice 2 for noise; voice 0+1 free) // STATE_DRAW -> voice 0 (DRAW uses voice 2 for noise; voice 0+1 free)
// STATE_WIN_P1/P2 -> voice 2 (WIN uses voice 0+1; voice 2 free) // STATE_WIN_P1/P2 -> voice 2 (WIN uses voice 0+1; voice 2 free)
// STATE_GAMEOVER -> voice 2 (GAMEOVER uses voice 0+1; voice 2 free) // STATE_GAMEOVER -> voice 2 (GAMEOVER uses voice 0+1; voice 2 free)
@@ -120,7 +120,7 @@ static byte stinger_voice_for_state(byte s)
switch (s) { switch (s) {
case STATE_TITLE: return 0; case STATE_TITLE: return 0;
case STATE_READY: return 1; case STATE_READY: return 1;
case STATE_WAIT: return 2; case STATE_WAIT: return 0;
case STATE_DRAW: return 0; case STATE_DRAW: return 0;
case STATE_WIN_P1: case STATE_WIN_P1:
case STATE_WIN_P2: return 2; case STATE_WIN_P2: return 2;
@@ -162,6 +162,8 @@ static byte last_winner;
static void game_enter_title(void) static void game_enter_title(void)
{ {
show_screen(SCREEN_TITLE); show_screen(SCREEN_TITLE);
score_p1 = 0;
score_p2 = 0;
score_render(); score_render();
// "PRESS FIRE" prompt: drawn in row 1 (below the score bar) of // "PRESS FIRE" prompt: drawn in row 1 (below the score bar) of
// the title screen. Re-rendered on every TITLE entry so it // the title screen. Re-rendered on every TITLE entry so it
@@ -190,10 +192,10 @@ static void game_enter_ready(void)
banner_clear(1); banner_clear(1);
enter_frame = frame_count; enter_frame = frame_count;
// Sample SID oscillator 3 ($D41B) for the upcoming WAIT duration. // Sample SID oscillator 3 ($D41B) for the upcoming WAIT duration.
// 100 + (sid.random % 150) frames = 2.0..5.0 sec at 50 Hz. This // 100 + (sid.random % 151) frames = 2.0..5.0 sec at 50 Hz. This
// register is the SID's voice 3 oscillator low byte, which is // register is the SID's voice 3 oscillator low byte, which is
// driven by an LFSR and effectively random between reads. // driven by an LFSR and effectively random between reads.
wait_duration_frames = 100 + (sid.random % 150); wait_duration_frames = 100 + (sid.random % 151);
vic.color_border = 0; vic.color_border = 0;
audio_state_enter(STATE_READY); audio_state_enter(STATE_READY);
// 5-frame transition stinger on voice 1 (free in READY). // 5-frame transition stinger on voice 1 (free in READY).
@@ -222,14 +224,15 @@ static void game_enter_draw(void)
draw_was_pressed[0] = input_fire(1); // Hare — capture current state draw_was_pressed[0] = input_fire(1); // Hare — capture current state
draw_was_pressed[1] = input_fire(0); // Scoot — so holding from WAIT draw_was_pressed[1] = input_fire(0); // Scoot — so holding from WAIT
// doesn't auto-win // doesn't auto-win
// Reset the per-frame counter and render "001" once. The // Reset the per-frame counter to 0 and render "000" once.
// bitmap was cleared to 0 and $D021 was set to white by // The bitmap was cleared to 0 and $D021 was set to white by
// show_white_screen(), so this draws black "001" digits on the // show_white_screen(), so this draws black "000" digits on the
// white background. game_step_draw() will then increment the // white background. game_step_draw() will then increment the
// counter and re-render each frame. The spec (GAME.md §3 step // counter and re-render each frame. A perfect 1-frame draw
// 4) says the counter starts at 001, not 000. // therefore reports "001" (counter is incremented to 1 before
draw_counter = 1; // the fire check runs).
draw_render_counter(1); draw_counter = 0;
draw_render_counter(0);
audio_state_enter(STATE_DRAW); audio_state_enter(STATE_DRAW);
// 5-frame transition stinger on voice 0 (voice 2 is the noise stab). // 5-frame transition stinger on voice 0 (voice 2 is the noise stab).
audio_play_stinger(stinger_voice_for_state(STATE_DRAW), STINGER_DURATION); audio_play_stinger(stinger_voice_for_state(STATE_DRAW), STINGER_DURATION);
@@ -265,13 +268,11 @@ static void game_enter_win_p2(void)
static void game_enter_gameover(void) static void game_enter_gameover(void)
{ {
// Show the title screen, but with the scores reset to 0/0 (the // Show the title screen with the final scores still displayed
// score bar makes this visually obvious: it's the title screen // (5 : x or x : 5) and the winner banner ("HARE WINS!" or
// with an empty score bar). The winner banner ("HARE WINS!" or // "SCOOT WINS!") in row 1 below the score bar. Scores are not
// "SCOOT WINS!") is rendered into row 1 below the score bar. // reset here — they reset on the next TITLE entry.
show_screen(SCREEN_TITLE); show_screen(SCREEN_TITLE);
score_p1 = 0;
score_p2 = 0;
score_render(); score_render();
// "HARE WINS!" or "SCOOT WINS!" — driven by last_winner set in // "HARE WINS!" or "SCOOT WINS!" — driven by last_winner set in
// game_enter_win_p1/p2. show_screen(SCREEN_TITLE) just // game_enter_win_p1/p2. show_screen(SCREEN_TITLE) just
@@ -374,7 +375,7 @@ static void game_step_draw(void)
// strobe creates a brief visible "flash" at the start of the // strobe creates a brief visible "flash" at the start of the
// round (4 frames at 50 Hz = ~80 ms). After the flash the // round (4 frames at 50 Hz = ~80 ms). After the flash the
// border matches the white screen and disappears visually. // border matches the white screen and disappears visually.
if (elapsed < 5) if (elapsed < 4)
vic.color_border = (elapsed & 1) ? 0 : 1; vic.color_border = (elapsed & 1) ? 0 : 1;
else else
vic.color_border = 1; vic.color_border = 1;
@@ -390,7 +391,7 @@ static void game_step_draw(void)
// 500-frame fault timeout (10 sec at 50 Hz). If neither player // 500-frame fault timeout (10 sec at 50 Hz). If neither player
// fires in 10 sec, abort the round, trigger a short stinger on // fires in 10 sec, abort the round, trigger a short stinger on
// SID voice 1, and go back to TITLE. No point awarded. // SID voice 1, and go back to TITLE. No point awarded.
if (elapsed > 500) { if (elapsed >= 500) {
state = STATE_TITLE; state = STATE_TITLE;
game_enter_title(); game_enter_title();
// Trigger the stinger AFTER audio_state_enter(TITLE) // Trigger the stinger AFTER audio_state_enter(TITLE)
+4 -1
View File
@@ -3,10 +3,13 @@
void memmap_setup(void) void memmap_setup(void)
{ {
mmap_trampoline(); mmap_trampoline();
mmap_set(MMAP_RAM);
mmap_set(MMAP_NO_ROM); mmap_set(MMAP_NO_ROM);
} }
// memmap_restore() — intentionally unused. The game runs an infinite
// main loop and is designed to run until power-off. If a graceful exit
// path is ever added (e.g. NMI handler or RUN/STOP key), call this
// before returning to BASIC so the memory config matches user expectations.
void memmap_restore(void) void memmap_restore(void)
{ {
mmap_set(MMAP_ROM); mmap_set(MMAP_ROM);
+2 -2
View File
@@ -33,7 +33,7 @@ byte score_p2 = 0;
// Index (also see CHAR_* macros in score.h): // Index (also see CHAR_* macros in score.h):
// 0=H, 1=A, 2=R, 3=E, 4=S, 5=C, 6=O, 7=T (score bar labels) // 0=H, 1=A, 2=R, 3=E, 4=S, 5=C, 6=O, 7=T (score bar labels)
// 8=P, 9=F, 10=W, 11=I, 12=N, 13=!, 14=' ', 15=reserved // 8=P, 9=F, 10=W, 11=I, 12=N, 13=!, 14=' ', 15=reserved
static const char font[FONT_COUNT][8] = { static const unsigned char font[FONT_COUNT][8] = {
// 0 H (1 0 0 1) × 3, (1 1 1 1), (1 0 0 1) × 4 // 0 H (1 0 0 1) × 3, (1 1 1 1), (1 0 0 1) × 4
{ 0x90, 0x90, 0x90, 0xF0, 0x90, 0x90, 0x90, 0x90 }, { 0x90, 0x90, 0x90, 0xF0, 0x90, 0x90, 0x90, 0x90 },
// 1 A (0 1 1 0), (1 0 0 1) × 2, (1 1 1 1), (1 0 0 1) × 4 // 1 A (0 1 1 0), (1 0 0 1) × 2, (1 1 1 1), (1 0 0 1) × 4
@@ -131,7 +131,7 @@ static const char pip_empty[16] = {
#define CELLS_PER_ROW 40 #define CELLS_PER_ROW 40
#define BYTES_PER_CELL 8 #define BYTES_PER_CELL 8
#define SCORE_BITMAP_BASE ((char *)0xE000) #define SCORE_BITMAP_BASE ((char *)0xE000)
#define SCORE_SCREEN_BASE ((char *)0xD000) #define SCORE_SCREEN_BASE ((char *)0xC400)
#define SCORE_CRAM_BASE ((char *)0xD800) #define SCORE_CRAM_BASE ((char *)0xD800)
// Cell indices for the labels and pip groups (see score.h for diagram). // Cell indices for the labels and pip groups (see score.h for diagram).
+8 -11
View File
@@ -140,7 +140,6 @@ static void clear_color_ram(void)
{ {
__asm { __asm {
lda #0 lda #0
ldx #4
ldy #0 ldy #0
L0: sta $d800, y L0: sta $d800, y
sta $d900, y sta $d900, y
@@ -148,21 +147,19 @@ static void clear_color_ram(void)
sta $db00, y sta $db00, y
iny iny
bne L0 bne L0
dex
bne L0
} }
} }
// Configure the VIC for multicolor bitmap mode pointing at the data // Configure the VIC for multicolor bitmap mode pointing at the data
// at $D000 (screen memory) and $E000 (bitmap). Same config for all // at $C400 (screen memory, VIC offset $0400) and $E000 (bitmap). Same
// 5 game screens + the white screen; only the per-screen pixel data // config for all 5 game screens + the white screen; only the per-screen
// and per-screen d021 color differ. // pixel data and per-screen d021 color differ.
static void vic_setup_mcm(void) static void vic_setup_mcm(void)
{ {
vic.ctrl1 = VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL; vic.ctrl1 = VIC_CTRL1_RST8 | VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL;
vic.ctrl2 = VIC_CTRL2_MCM | VIC_CTRL2_CSEL; vic.ctrl2 = VIC_CTRL2_MCM | VIC_CTRL2_CSEL;
cia2.pra = (cia2.pra & 0xfc) | 0x00; cia2.pra = (cia2.pra & 0xfc) | 0x00;
vic.memptr = 0x48; vic.memptr = 0x18;
} }
// --- public API: show_screen() ----------------------------------------- // --- public API: show_screen() -----------------------------------------
@@ -173,12 +170,12 @@ void show_screen(int n)
const struct ScreenDef *s = &screens[n]; const struct ScreenDef *s = &screens[n];
// 1. Decompress the 8 KB bitmap into $E000-$FFFF and copy the 1 KB // 1. Decompress the 8 KB bitmap into $E000-$FFFF and copy the 1 KB
// screen memory into $D000-$D3E7. Both happen while the VIC // screen memory into $C400-$C7E7. Both happen while the VIC
// is still in its old mode (or, on the very first call, in // is still in its old mode (or, on the very first call, in
// whatever state memmap_setup() left it). We do the attr // whatever state memmap_setup() left it). We do the attr
// copy first so the visible region stays coherent for as long // copy first so the visible region stays coherent for as long
// as possible during the bitmap decompression. // as possible during the bitmap decompression.
copy_bytes(s->attr, (char *)0xD000, 1000); copy_bytes(s->attr, (char *)0xC400, 1000);
oscar_expand_lzo((char *)0xE000, s->lzo); oscar_expand_lzo((char *)0xE000, s->lzo);
// 2. Clear color RAM (the "11" color per cell; we don't have // 2. Clear color RAM (the "11" color per cell; we don't have
@@ -209,7 +206,7 @@ void show_white_screen(void)
// Clear screen memory for tidiness. The top 40 cells are // Clear screen memory for tidiness. The top 40 cells are
// overwritten by score_render() right after this returns. // overwritten by score_render() right after this returns.
char *sm = (char *)0xD000; char *sm = (char *)0xC400;
for (unsigned i = 0; i < 1000; i++) for (unsigned i = 0; i < 1000; i++)
sm[i] = 0; sm[i] = 0;
+3 -3
View File
@@ -21,7 +21,7 @@
// the VIC to display them. // the VIC to display them.
// //
// Memory layout used here (after memmap_setup()): // Memory layout used here (after memmap_setup()):
// $D000-$D3E7 — screen memory (1000 bytes; the "color attributes") // $C400-$C7E7 — screen memory (1000 bytes; the "color attributes")
// Per cebix-vic-article §3.7.3.4, in multicolor bitmap // Per cebix-vic-article §3.7.3.4, in multicolor bitmap
// mode the screen memory byte holds the "01" color in // mode the screen memory byte holds the "01" color in
// its high nibble and the "10" color in its low // its high nibble and the "10" color in its low
@@ -40,7 +40,7 @@
// ctrl1 = BMM | DEN | RSEL (multicolor bitmap, display on, 25 rows, // ctrl1 = BMM | DEN | RSEL (multicolor bitmap, display on, 25 rows,
// no vertical scroll) // no vertical scroll)
// ctrl2 = MCM | CSEL (multicolor, 40 columns, no horiz scroll) // ctrl2 = MCM | CSEL (multicolor, 40 columns, no horiz scroll)
// memptr (D018) = 0x48 (screen at $D000, bitmap at $E000 within // memptr (D018) = 0x18 (screen at $0400, bitmap at $E000 within
// the selected 16K VIC bank) // the selected 16K VIC bank)
// //
// Calling show_screen() with an unsupported ID is a no-op. // Calling show_screen() with an unsupported ID is a no-op.
@@ -54,7 +54,7 @@ void show_screen(int n);
// Implementation: // Implementation:
// 1. Fill the 8 KB bitmap at $E000-$FFFF with 0x00 so every pixel is // 1. Fill the 8 KB bitmap at $E000-$FFFF with 0x00 so every pixel is
// a "00" code (which uses $D021). // a "00" code (which uses $D021).
// 2. Clear the 1 KB screen memory at $D000-$D3E7 (so any leftover // 2. Clear the 1 KB screen memory at $C400-$C7E7 (so any leftover
// "01" / "10" cell values are 0, in case the bitmap ever contains // "01" / "10" cell values are 0, in case the bitmap ever contains
// a non-zero pixel). // a non-zero pixel).
// 3. Clear the 1 KB color RAM at $D800-$DBE7. // 3. Clear the 1 KB color RAM at $D800-$DBE7.
+14 -15
View File
@@ -465,13 +465,14 @@ Findings from the second round of 4-agent parallel code review.
frames"; spec says "4-frame stab". frames"; spec says "4-frame stab".
**Fix:** change `< 5` to `< 4`, or update comment/spec to "5". **Fix:** change `< 5` to `< 4`, or update comment/spec to "5".
- [ ] **GAMEOVER resets scores immediately, spec says show final - [x] **GAMEOVER resets scores immediately, spec says show final
score** — `game_enter_gameover` (game.c:273-274) resets score** — `game_enter_gameover` (game.c:273-274) resets
`score_p1 = score_p2 = 0` on entry. GAME.md §3 step 7 implies `score_p1 = score_p2 = 0` on entry. GAME.md §3 step 7 implies
the GAMEOVER screen should show the final score (5 : x) with a the GAMEOVER screen should show the final score (5 : x) with a
winner banner; scores should only reset when returning to TITLE. winner banner; scores should only reset when returning to TITLE.
**Fix:** move score reset from `game_enter_gameover` to **Fix:** move score reset from `game_enter_gameover` to
`game_enter_title` (or to the GAMEOVER→TITLE transition). `game_enter_title` (or to the GAMEOVER→TITLE transition).
**Fixed:** removed reset from `game_enter_gameover`, added to `game_enter_title`.
- [ ] **ADSR decay comments inconsistent with constants** - [ ] **ADSR decay comments inconsistent with constants**
`src/audio.c:244` says "decay=9 (~114ms)" but uses `src/audio.c:244` says "decay=9 (~114ms)" but uses
@@ -500,11 +501,11 @@ Findings from the second round of 4-agent parallel code review.
window to pre-clear screen RAM (would help the screen-RAM window to pre-clear screen RAM (would help the screen-RAM
critical bug above). critical bug above).
- [ ] **Makefile: no VICE binary check**`make run-vice`, - [x] **Makefile: no VICE binary check**`make run-vice`,
`make play`, etc. don't check if `x64`/`x64sc` is in PATH. `make play`, etc. don't check if `x64`/`x64sc` is in PATH.
Old `build.sh` had `command -v "$PLAY_VICE"`. Old `build.sh` had `command -v "$PLAY_VICE"`.
**Fix:** add `@command -v x64 >/dev/null 2>&1 || { echo "error: x64 not found" >&2; exit 1; }` **Fix:** add `@command -v x64 >/dev/null 2>&1 || { echo "error: x64 not found" >&2; exit 1; }`
to each VICE target. to each VICE target. **Fixed:** added checks to all four VICE targets.
- [ ] **Makefile: no `$DISPLAY` check** — old `build.sh` warned - [ ] **Makefile: no `$DISPLAY` check** — old `build.sh` warned
on missing `$DISPLAY` for foreground VICE and errored for on missing `$DISPLAY` for foreground VICE and errored for
@@ -513,7 +514,7 @@ Findings from the second round of 4-agent parallel code review.
**Fix:** add `@[ -n "$$DISPLAY" ] || { echo "error: no \$DISPLAY" >&2; exit 1; }` **Fix:** add `@[ -n "$$DISPLAY" ] || { echo "error: no \$DISPLAY" >&2; exit 1; }`
to VICE GUI targets. to VICE GUI targets.
- [ ] **Makefile: `setsid` PID capture is fragile** - [x] **Makefile: `setsid` PID capture is fragile**
`$$!` captures the `setsid` wrapper PID, not the `x64` child. `$$!` captures the `setsid` wrapper PID, not the `x64` child.
`setsid` forks and the parent exits, so the PID file often `setsid` forks and the parent exits, so the PID file often
points to a dead process. `make kill` uses `pgrep` (correct), points to a dead process. `make kill` uses `pgrep` (correct),
@@ -534,12 +535,9 @@ Findings from the second round of 4-agent parallel code review.
**Fix:** add `@command -v c1541 >/dev/null 2>&1 || { echo "error: c1541 not found" >&2; exit 1; }` **Fix:** add `@command -v c1541 >/dev/null 2>&1 || { echo "error: c1541 not found" >&2; exit 1; }`
at the top of the `$(D64)` recipe. at the top of the `$(D64)` recipe.
- [ ] **Makefile: oscar64-build guard duplicated** the - [x] **Makefile: oscar64-build guard duplicated**
"build oscar64 if missing" block appears in both `compile` **Fixed:** `ensure-oscar64` phony target exists (Makefile:90-99);
and `run` recipes (Makefile:88-96 and 104-112). DRY `$(PRG)` and `run` both depend on it. No duplication.
violation.
**Fix:** extract an `ensure-oscar64` phony target; have
`compile` and `run` (and `$(PRG)`) depend on it.
- [ ] **WAIT transition stinger uses voice 2 (the RNG source)** - [ ] **WAIT transition stinger uses voice 2 (the RNG source)**
`src/game.c:123` picks voice 2 for the WAIT stinger. This `src/game.c:123` picks voice 2 for the WAIT stinger. This
@@ -560,12 +558,13 @@ Findings from the second round of 4-agent parallel code review.
`src/score.c:36`, `src/banner.c:29`. Bytes like 0x90, 0xF0 are `src/score.c:36`, `src/banner.c:29`. Bytes like 0x90, 0xF0 are
signed; math is masked by `& 0x0F` so behavior is correct, but signed; math is masked by `& 0x0F` so behavior is correct, but
`unsigned char` would be cleaner. `unsigned char` would be cleaner.
- [ ] **`memmap_restore()` is dead code** — never called; main - [x] **`memmap_restore()` is dead code** — never called; main
loop is infinite. Document as intentional or wire up an exit loop is infinite. Document as intentional or wire up an exit
path (e.g. NMI handler). path (e.g. NMI handler). **Fixed:** added comment documenting it's
- [ ] **PROG_C64.md banking table is oversimplified** — says intentionally unused (game runs until power-off).
- [x] **PROG_C64.md banking table is oversimplified** — says
"LORAM 0=RAM, 1=BASIC ROM" but BASIC actually requires "LORAM 0=RAM, 1=BASIC ROM" but BASIC actually requires
LORAM=1 AND HIRAM=1. The `$BC00` fix relies on HIRAM=0. LORAM=1 AND HIRAM=1. The `$BC00` fix relies on HIRAM=0. **Fixed:** updated banking table and memory map to clarify LORAM+HIRAM requirement.
- [ ] **Makefile: `clean` first line lacks `@` prefix** - [ ] **Makefile: `clean` first line lacks `@` prefix**
echoes the long `rm -f ...` command. Minor inconsistency. echoes the long `rm -f ...` command. Minor inconsistency.
- [ ] **Makefile: undocumented intermediate files** - [ ] **Makefile: undocumented intermediate files**
@@ -573,7 +572,7 @@ Findings from the second round of 4-agent parallel code review.
but `help` doesn't list them as output files. but `help` doesn't list them as output files.
- [ ] **Makefile: `make OPT=` (empty) produces `-` flag** - [ ] **Makefile: `make OPT=` (empty) produces `-` flag**
edge case. Guard with `OPT_FLAGS := $(if $(OPT),-$(OPT))`. edge case. Guard with `OPT_FLAGS := $(if $(OPT),-$(OPT))`.
- [ ] **Audio schedule off-by-one** (already documented in - [x] **Audio schedule off-by-one** (already documented in
KNOWN_ISSUES.md #3) — WIN/GAMEOVER notes are 1 frame short per KNOWN_ISSUES.md #3) — WIN/GAMEOVER notes are 1 frame short per
note (~10% deviation). Cosmetic. note (~10% deviation). Cosmetic.