Commit Graph
9 Commits
Author SHA1 Message Date
mr.one 7d69e6ec25 WIP: integrate NUFLI displayer for static screens 2026-07-20 16:37:46 +02:00
mr.one f4b344cccd Implement NUFLI delta encoding with scanlines
- Add tools/apply_scanlines.py for CRT scanline effect
- Add tools/nufli_delta.py for delta encoding (base + bitmask deltas)
- Delta format: 2880 byte bitmask + N byte values per screen
- 40.1% size reduction vs raw NUFLI (69KB vs 115KB for 5 screens)
- Base (23KB) at 000-FFF, title delta (7KB) at -
- Other deltas use title as fallback (TODO: disk loading)
- Update tasks.md with scanline and delta encoding documentation
2026-07-19 00:12:37 +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
Whack Hare Agent b7bd76dee3 Phase 8: polish and end-to-end test
Banner text rendering (PRESS FIRE / HARE WINS! / SCOOT WINS!):
- src/banner.c, src/banner.h: new module; renders NUL-terminated
  ASCII text in any character row of the multicolor bitmap, using
  the same 4x8 custom font as the score bar.  Truncates to 40 chars,
  centers in the row, clears the row first.
- src/score.c, src/score.h: font extended from 8 to 16 entries —
  added P, F, W, I, N, !, space (and one reserved).  font_lookup()
  maps ASCII to font index.
- src/game.c: TITLE renders 'PRESS FIRE' in row 1 below the score
  bar; GAMEOVER renders 'HARE WINS!' or 'SCOOT WINS!' based on
  last_winner (set by game_enter_win_p1/p2).  Other states clear
  row 1 on entry.

State transition stinger:
- src/audio.c, src/audio.h: audio_play_stinger(voice, duration)
  starts a low square-wave burst that auto-cleans up via
  audio_advance_stinger (called from audio_state_step).  A 5-frame
  stinger is fired on every state transition; the per-state voice
  is chosen to avoid colliding with the new state's audio
  (TITLE/READY: voice 0/1 free, WAIT: voice 2 free, DRAW: voice 0
  free, WIN/GAMEOVER: voice 2 free).  Stinger is silenced by the
  next audio_state_enter() via the existing audio_stop() call.

TITLE border flash changed from 12.5 Hz to 1 Hz (50 on, 50 off).

Build system:
- src/build.sh: added -O0/-O1/-O2/-O3/-Os/-g flag handling.  Both
  default (-O1) and -O3 builds produce a 43913-byte .prg
  (well under the 51308-byte LOAD"*",8,1 limit).
- src/main.c: pinned #pragma stacksize(0x400) — the oscar64 default
  is the same, but pinning makes the layout predictable across
  optimization levels.  -O3 needs this exact size; larger values
  cause 'Cannot place stack section' link errors because the
  optimizer's larger code section leaves less room in the
  stack/heap gap.
- src/tick.c: marked frame_tick_handler __noinline so -O3 doesn't
  inline the entire state machine (6000+ bytes) into the IRQ
  handler.  With __noinline, the handler is 136 bytes — small
  enough for the raster line budget.
- src/game.h: marked game_step __noinline for the same reason.

The .prg is 173 blocks (out of 202 max), well within the BASIC
load area.  Both default and -O3 builds run cleanly in the oscar64
built-in emulator.
2026-07-17 02:55:16 +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