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:
@@ -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. |
|
||||||
|
|||||||
@@ -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
@@ -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) |
|
||||||
|
|||||||
@@ -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
@@ -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)
|
||||||
|
|
||||||
|
|||||||
+77
-72
@@ -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,63 +418,63 @@ 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);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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.
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user