Commit Graph
21 Commits
Author SHA1 Message Date
ballz 97d4f4216b Add Phase 10: code review findings as pending tasks
Second round of 4-agent parallel code review found several
critical bugs that were not caught in the first review:

Critical (game-breaking):
1. Screen RAM writes go to VIC registers, not DRAM — with
   $01=$35, writes to $D000 hit VIC-II registers, not screen
   RAM. Also disables raster IRQ ($D01A overwritten). Works in
   oscar64 emulator (doesn't simulate banking).
2. vic_setup_mcm() clears RST8 on every screen change — moves
   raster IRQ from line 311 to line 55 (visible display).
3. $D41B random still frozen — audio_stop() re-zeroes voice 3
   freq; NOISE waveform bit never set. WAIT always 100 frames.
4. Makefile: $(PRG) has no rule — make run-vice fails on clean
   checkout.
5. Makefile: .PHONY lists non-existent targets.

Medium (spec deviations, edge cases):
- DRAW fault >500 vs >=500 (1 frame off)
- Counter minimum value 2 not 1
- WAIT upper bound 249 not 250
- DRAW strobe 5 frames not 4
- GAMEOVER resets scores immediately (spec: show final score)
- ADSR decay comments inconsistent with constants
- clear_color_ram() 4x redundant
- memmap_setup() redundant MMAP_RAM call
- Makefile: no VICE/c1541/$DISPLAY checks, setsid PID fragile,
  parallel race, oscar64 guard duplicated

Low (cosmetic, docs):
- Doc 'WHACKED' references, font array signedness,
  memmap_restore dead code, PROG_C64 banking oversimplification,
  Makefile clean @ prefix, undocumented intermediates

No fixes applied — these are tasks for a future Phase 10 work
session.
2026-07-18 18:36:22 +02:00
ballz f0c2ca07f2 Update tasks.md: mark Phases 0-8 complete, add post-Phase 8 section
All 8 implementation phases are done. Changes:
- Marked every phase as  done with all tasks checked
- Fixed verify steps to use 'make run' / 'make play' (was broken
  shell commands like 'cd src && ./make run')
- Added 'Post-Phase 8 work' section documenting the Makefile
  migration, VICE autostart fix, rename to Nyuller, build dir
  move, and code review fixes
- Updated VICE section: now works with -drive8type 1541, make
  play/kill manage the lifecycle
- Added LZO screen swap refactor and trampoline re-enable to
  optional Phase 9 features
2026-07-18 18:01:05 +02:00
ballz b7219dccfc Replace src/build.sh with Makefile (GNU make)
New targets:
  help (default)     — show usage (was the -help/--help equivalent)
  compile            — compile main.c → build/nyuller.prg
  run                — compile + run in oscar64 built-in emulator
  run-vice           — compile + run in VICE x64 (foreground)
  run-vice-cycle     — compile + run in VICE x64sc (cycle-exact)
  play               — compile + launch VICE x64 detached
  play-cycle         — compile + launch VICE x64sc detached
  kill               — kill any detached VICE
  clean              — remove build/ artifacts
  build/nyuller.d64  — build the .d64 disk image from the .prg

Optimization: override with 'make OPT=O3' (or O0/O1/O2/Os/g).
Default is -O1 (oscar64 default).

The Makefile builds oscar64 automatically if missing, uses
c1541 to create the .d64 with verification, and passes
-drive8type 1541 to VICE (required for autostart).

Removed src/build.sh and updated all docs (README, tasks.md,
AGENT_CONTEXT.md, GAME.md, helloworld.c) to reference make.
2026-07-18 16:22:38 +02:00
ballz 3e9683a2fc Pass -drive8type 1541 to VICE so autostart works
VICE defaults to the 1541-II (drive type 1542), whose ROM we don't
have installed.  Without this flag the autostart LOAD"*",8,1 fails
with ?DEVICE NOT PRESENT.  We have the original 1541 ROM at
~/.local/share/vice/DRIVES/dos1541-325302-01+901229-05.bin, so
forcing drive type 1541 makes autostart succeed.

Added to both the foreground (-v/-V) and background (-p/-P) VICE
launches.  The --kill pgrep regex still matches (the .* after
+confirmonexit covers the new -drive8type flag).
2026-07-18 16:05:22 +02:00
ballz d78eb976cf Rename game from Whack Hare! to Nyuller
All references updated:
- src/build.sh: PRG=nyuller.prg, D64=nyuller.d64, disk name 'ny',
  all grep/pgrep patterns updated, all comments updated
- All 10 header files: include guards WHACK_HARE_* → NYULLER_*
- C source comments: 'Whack Hare!' → 'Nyuller'
- README.md: updated build command output path
- GAME.md: title updated
- tasks.md: all whack_hare.prg → nyuller.prg
- src/AGENT_CONTEXT.md: project name updated
- tools/convert_screens.py: project name updated

Build artifacts renamed: whack_hare.* → nyuller.*, whack.d64 → nyuller.d64

Note: title screen image (source_images/screen_title.png) still shows
'WHACKED' logo — that's a visual asset, not a code reference.
2026-07-18 15:48:50 +02:00
ballz 0bf6818a3c Move build output from src/build/ to ./build (repo root)
The build directory was under src/, which is unusual for a project
with a separate source tree. Moving it to the repo root's build/
directory follows the conventional project layout:

  ./build/           # build output (gitignored)
  ./src/             # source code only
  ./oscar64/         # compiler (submodule)

Changes:
- src/build.sh: BUILD_DIR changed from $SCRIPT_DIR/build to
  $ROOT/build
- .gitignore: replaced src/build/ with build/
- README.md: updated layout tree, build commands, and output path
- src/AGENT_CONTEXT.md: updated gitignore reference
- tasks.md: replaced all src/build/ with build/
2026-07-18 15:42:20 +02:00
ballz 31cbe00486 Apply fixes from 7-agent code review
Critical bugs found by the parallel code review:

1. .attr tables were at $A000-$B388 — inside the BASIC ROM.  The
   KERNAL LOAD routine doesn't toggle $01, so writes were silently
   dropped on real C64 hardware.  Moved the screens region to
   $BC00-$D000 (5 KB always-RAM under the banked-out KERNAL and
   below the I/O area).

2. game_enter_draw() initialized draw_was_pressed[] to 0, so a
   player holding fire from WAIT into DRAW got an instant win
   with 0 reaction time.  Now initialized from input_fire() so
   the current state is the baseline, not zero.

3. First-round WAIT was always 100 frames because audio_stop()
   wrote freq=0 to all 3 voices — $D41B stuck at 0 so the
   random was deterministic.  audio_init() now sets voice 3's
   freq to 0xffff after audio_stop() so the oscillator is
   running from the first sample.

4. build.sh --kill pgrep regex was broken: 'x64sc?' matches
   'x64s' or 'x64sc' but never 'x64', and the '+' is a regex
   quantifier not a literal.  Fixed: x64(sc)? \\+confirmonexit.

5. -v and -p (or -V and -P) would both fire and launch two
   VICE processes.  Added mutual-exclusion check.

6. c1541 exits 0 even on failure, so set -e couldn't catch a
   broken d64 build.  Extracted build_d64() helper that
   verifies the PRG landed in the d64 via c1541 -list (in the
   interactive heredoc form, since the flag form doesn't work
   on this c1541 version).

7. Counter showed '000' for one frame before '001'.  Init to 1
   not 0 (GAME.md spec).

Build script also now:
- --kill removes stale vice.pid / vice.log
- -p / -P kill any prior VICE before launching a new one
- Has a clear error for -v + -p combination

Also: KNOWN_ISSUES.md documents 3 deferred items (LZO-in-IRQ
screen tearing, IRQ half of mmap_trampoline silently disabled,
audio schedule 1 frame short per note in WIN/GAMEOVER) that
require larger refactors to fix.
2026-07-17 09:56:49 +02:00
ballz aa9499766f Add -p (play) and -P (play cycle-exact) flags to build.sh
Previously the only VICE mode was the foreground -v / -V which
exited immediately because the autostart mechanism doesn't
recognize raw .prg files. This change:

- Adds -p / -P: compile, wrap the .prg in a .d64 (using c1541),
  then launch x64 / x64sc detached with setsid+nohup so the
  emulator survives the build.sh exit. Prints the VICE PID and
  the default joystick key bindings (WASD+LCtrl for port 2
  Scoot, Arrows+RShift for port 1 Hare).

- Adds --kill to terminate the detached VICE process.

- Documents the default joystick bindings and a 'press fire on
  both ports' hint to start the game.

Tested: the VICE window appears in the X client list (xlsclients)
on the GNOME session, the .d64 autostarts whack_hare.prg, and
--kill cleanly stops the process.
2026-07-17 08:30:56 +02:00
ballz a8ce56464d Phase 7: DRAW screen + 3-digit counter
Add draw.c/draw.h with draw_render_counter(value): renders a 0..999
counter as 3 seven-segment-style digits, each 3 cells wide x 8 cells
tall (1 cell per segment), centered on the white DRAW screen at row 8,
columns 15-23. On segments write 0xFF bitmap + color RAM 0 (black);
off segments write 0x00 bitmap (white from $D021).

Wire into game.c:
- game_enter_draw resets draw_counter = 0 and calls draw_render_counter(0).
- game_step_draw increments draw_counter (cap 999) and re-renders each
  frame, before the existing fire-detection and fault logic.
- Flash effect: for the first 4 frames of DRAW, $D020 strobes between
  black and white, then settles to white (matches the white screen).

No audio or other behavior changes.
2026-07-17 02:30:43 +02:00
ballz a621494a21 Phase 6: SID audio (5 cues + fault stinger) 2026-07-17 02:15:54 +02:00
ballz d538cf46e2 Phase 5: raster IRQ + frame timing
Replace busy-wait frame counter with a 50 Hz raster IRQ at line 311
(PAL stable line).  The IRQ handler increments a 16-bit frame_count
and calls game_step() once per frame.  Per-state timing now uses
enter_frame timestamps + frame_count comparisons.

- New tick.h/tick.c: install one RIRQ via Oscar64's rirq library,
  call a __interrupt handler that bumps frame_count and runs the
  state machine.  Mask CIA 1 + CIA 2 IRQs and set RST8 (the high
  bit of the 9-bit raster register) so the IRQ fires at line 311
  not line 55.
- game.h: expose volatile frame_count, replace per-state 'frame'
  counter with enter_frame timestamps.
- game.c: use frame_count - enter_frame everywhere; sample SID
  $D41B at READY enter for a random 100..250 frame WAIT duration;
  trigger a low-square-wave stinger on SID voice 1 when DRAW
  faults out (no fire for 500 frames) and gate it off ~0.2 sec
  later via a counter decremented every frame.
- main.c: replace the busy-wait loop with rasterirq_setup() and
  an empty for(;;); idle.

(Filename is tick.c/.h not rasterirq.c/.h because the oscar64
library's own rasterirq.c does '#include "rasterirq.h"' to pull
in its own header, and that include would otherwise pick up
ours and lose NUM_IRQS.)
2026-07-17 02:02:51 +02:00
ballz 65a2e5d2a1 Phase 4: state machine skeleton (TITLE→READY→WAIT→DRAW→WIN)
Implement the full state machine from GAME.md §9 with no audio yet,
fixed (not random) durations, and the basic score-update + reset logic
needed to drive the GAMEOVER transition.

src/game.h / src/game.c:
  - enum: STATE_TITLE, STATE_READY, STATE_WAIT, STATE_DRAW,
    STATE_WIN_P1, STATE_WIN_P2, STATE_GAMEOVER
  - byte state global + game_init() / game_step() pair
  - per-state enter (show_screen + score_render + reset frame counter)
    and step (check inputs, advance counter, transition) functions
  - TITLE: flash border at 25 Hz, debounce both-fire-within-8-frames
  - READY: 60 frames, WAIT: 100, DRAW: 500-frame fault timeout,
    WIN: 100 frames, GAMEOVER: 300 frames
  - DRAW uses rising-edge detection so holding fire from before
    DRAW doesn't auto-trigger a win
  - WIN_P1/WIN_P2 increment the score and re-render the score bar
  - GAMEOVER shows the title screen with scores reset to 0/0

src/screens.c / src/screens.h:
  - show_screen(n) now handles all 5 SCREEN_* IDs via a per-screen
    descriptor table (5 × 8000-byte bitmaps + 5 × 1000-byte attrs)
  - LZO-compress the 8000-byte bitmaps at build time so the 5 screens
    fit in the main region; the 5 .attr tables go in a custom 'screens'
    region at $A000-$C000 (BASIC ROM area, banked out as RAM)
  - new show_white_screen() fills the bitmap with 0s, sets $D021/
    $D022/$D023/$D020 to white, for the all-white DRAW screen

src/main.c:
  - simplified to: memmap_setup → score_init → game_init → while (1)
    game_step()
  - sets #pragma heapsize(0) since the program doesn't use malloc

Build: ./build.sh -e — .prg is 43.9 KB, well under the 51 KB BASIC
load limit. The oscar64 emulator runs the state machine indefinitely
(both fire buttons read as pressed in the emulator, so the game
cycles through the states); no crash.

Memory layout (from .map):
  $0801-$0853  startup
  $0880-$0E51  code (1489 bytes)
  $0E51-$7FE4  data: 5 LZO bitmaps + small tables (28723 bytes)
  $7FE4-$7FEC  BSS: state, frame, title_input, etc.
  $7FF0-$9000  heap (16 bytes)
  $9000-$A000  stack
  $A000-$B388  custom 'screens' region: 5 .attr tables (5000 bytes)
2026-07-17 01:50:03 +02:00
ballz 9f086c3a4f Phase 3: score bar (HARE/SCOOT labels + 5 pips per side)
Draws a score bar across the top 8 rows of the multicolor bitmap at
$E000-$E13F (320 bytes), with HARE on the far left, SCOOT on the far
right, and 5 pips for each player between them.

A pip is 8x8 pixels (2 cells wide x 1 cell tall = 16 bitmap bytes):
filled = 1-pixel white border around a 6x6 black block; empty = an 8x8
white square (the border alone).  Pips are packed 2 cells each with
no gap, so adjacent pip borders merge into a 2-px white vertical
divider.

Labels use a hand-built 4x8 font (1 byte per row, top 4 bits = 4
pixels) for the 8 chars H/A/R/E/S/C/O/T (64 bytes total).  Each font
row is expanded 1bpp -> 2bpp via a 16-byte lookup table (expand4):
each '1' becomes '11' (white, from color RAM), each '0' becomes '00'
(background black, from $D021).

score_render() also rewrites the top 40 cells' attributes
(screen memory = 0, color RAM = 1) so the score bar's palette is
exactly 2 colors.  Scores are clamped to 0..5 on render to defend
against double-tap bugs in Phase 4.
2026-07-17 01:35:40 +02:00
ballz 4d697ad8f1 Phase 2: display the title screen (mmap, vic, screen swap)
Replace helloworld.c with main.c. Add memmap_setup/restore (banks out
KERNAL+BASIC+CHAR ROMs so $E000-$FFFF is free for the 8 KB bitmap and
$D000-$DFFF is I/O), show_screen(n) which copies the title .bin to
$E000 and the .attr to $D000 (screen memory in multicolor bitmap mode)
then flips the VIC into BMM=1 MCM=1 CSEL=1 RSEL=1 DEN=1 with bitmap
base CB13=1 and screen base VM13-VM10=4 ($D018 = 0x48), and
input_fire(port) which reads bit 4 of CIA1 $DC00/$DC01.

build.sh default target is now main.c -> whack_hare.prg (still
builds helloworld explicitly via the existing -c flag if needed).

Verified: ./build.sh -e runs the title screen in oscar64's built-in
emulator and exits cleanly. .map shows code at $0880-$09A6 and
embedded title data at $09A7-$2CCF, well within the 38 KB main
region. .prg is 9428 bytes, well under the 202-block LOAD"*",8,1
limit.

Concerns for Phase 3 (score bar): the score bar will overlay the top
8 pixel rows of the bitmap ($E000-$E13F, 320 bytes). show_screen()
will continue to copy the full .bin to $E000; score_render() will be
called immediately after and overwrite the top 320 bytes. This keeps
show_screen() dumb and lets the score bar be re-rendered on state
change without re-copying the whole 8 KB bitmap.
2026-07-17 01:27:28 +02:00
ballz 33d628aa65 Phase 1: asset pipeline (Python script + 5 processed .bin/.attr files)
Adds tools/convert_screens.py which converts each source PNG to a
160x200 multicolor bitmap (8000 B .bin) plus a 1000 B .attr screen-
memory table, following c64-wiki Multicolor_Bitmap_Mode:

  - 2-bit pixel: 00=$D021, 01=attr high nibble, 10=attr low nibble,
    11=color RAM nibble.
  - Per 4x8 cell: $D021 is forced into the 4-color set (it's global);
    the other 3 are the 3 most common non-d021 colors in the cell.
  - Pixels snap to the nearest of those 4, then packed 4-per-byte
    MSB-first into the bitmap; attr byte = (cell_color_2 << 4) |
    cell_color_1 (low nibble = '10' color, high = '01' color).

Tried two downscale strategies on all 5 source images; both produce
visually equivalent output at the ~9x source-to-target scale. Picked
Strategy A (direct 160x200 LANCZOS) as default because it's simpler
and slightly faster; Strategy B (LANCZOS 320x200 then 2x BOX 160x200)
remains available via --strategy. See the script docstring for the
detailed rationale.

$D021 is auto-picked as the most common C64 palette color in the
downscaled image (turns out to be black for 4 of 5 screens, dark grey
for waiting2). The .d021 sidecar files document this value so Phase 2
can program the VIC without re-deriving it.

The '11' color (color RAM nibble per cell) is not stored in .attr (no
room in 1000 B). Default is to leave color RAM at the C64 boot value
(black), which gives effectively 3 unique colors per cell + 1 global.
Phase 2 can either accept this or extend the format with a per-cell
color RAM table.

Generated files are checked in so the build doesn't depend on Python.

Output sizes verified: each .bin is exactly 8000 B, each .attr is
exactly 1000 B, total 9000 B per screen. The .bin files are not all
zero (sanity check passed).
2026-07-17 01:16:51 +02:00
ballz 558ab27668 Make VICE non-reliance explicit: no phase uses it for verification 2026-07-17 01:03:26 +02:00
ballz 1cd3a962e6 Document VICE headless limitations; rely on oscar64 emulator for default test path
VICE x64 is a GUI emulator that requires a real X11/Wayland display.
In this headless environment the KERNAL/BASIC/CHAR/1541 ROMs had to be
fetched manually, autostart produced blank screenshots (no display
to render to), and no other C64 emulator is installed. The oscar64
built-in emulator (-e) is the practical headless test tool. VICE is
left in build.sh as -v / -V for interactive use in a real terminal
session, but tasks.md and README.md are updated to reflect that the
development loop and all Verify steps use the oscar64 emulator.
Removed the failed test artifacts from src/build/.
2026-07-17 01:03:01 +02:00
ballz 0f8503f9f7 Note VICE availability: add x64/x64sc test paths to plan and build script 2026-07-17 00:48:06 +02:00
ballz cb3a8c4a2e Add tasks.md: phased implementation plan 2026-07-17 00:45:46 +02:00
ballz 9242dca9e7 Add GAME.md: design spec for the quick-draw game 2026-07-17 00:42:20 +02:00
ballz cbe5cf6a47 Initial commit: C64 project skeleton with oscar64 submodule 2026-07-17 00:36:14 +02:00