diff --git a/src/game.c b/src/game.c new file mode 100644 index 0000000..b0edc94 --- /dev/null +++ b/src/game.c @@ -0,0 +1,292 @@ +// game.c — Whack Hare! state machine. +// +// See game.h for the transition diagram and game_init() / game_step() +// documentation. Each state has an "enter" action (set the screen, +// reset the frame counter, reset per-state state) and a "step" action +// (check inputs, advance the frame counter, transition). +// +// In Phase 4 the main loop is `while (1) game_step();`. In Phase 5 +// the body of the main loop will be replaced by a raster IRQ handler +// that calls game_step() on line 311. Either way, game_step() is the +// only thing that needs to run "once per frame". + +#include "game.h" +#include "screens.h" +#include "input.h" +#include "score.h" +#include + +// Current state. Set by game_init() and by the per-state step +// functions when a transition is triggered. Read by the switch in +// game_step(). +static byte state; + +// Frame counter for the current state. Reset to 0 by each per-state +// "enter" function. Compared against per-state constants in the step +// functions (60 for READY, 100 for WAIT and WIN, 300 for GAMEOVER, +// 500 for the DRAW fault timeout). unsigned short is enough for +// 65535 frames = ~22 minutes at 50 Hz; the longest single state is +// 500 frames (DRAW fault), so 16 bits is more than enough. +static unsigned short frame; + +// --- TITLE-specific input state ---------------------------------------- +// +// The TITLE state waits for "both fire buttons pressed simultaneously". +// The de-bounce rule (GAME.md §9): the two buttons must be pressed +// within 8 frames of each other; if not, the game requires both +// buttons to be released before re-trying. The protocol: +// +// IDLE -> on (both pressed at once) -> READY +// -> on (one pressed) -> FIRST_HELD +// FIRST_HELD -> on (other pressed, within 8 fr) -> READY +// -> on (timeout reached) -> WAIT_RELEASE +// -> on (both released) -> IDLE +// WAIT_RELEASE -> on (both released) -> IDLE +#define TITLE_IDLE 0 +#define TITLE_FIRST_HELD 1 +#define TITLE_WAIT_RELEASE 2 +static byte title_input; +static unsigned short title_first_frame; + +// --- DRAW-specific input state ----------------------------------------- +// +// The DRAW state needs rising-edge detection: holding fire from before +// DRAW starts (e.g. during WAIT) must not auto-trigger a win. We +// track the previous frame's state for each port. Initialized on +// entry to DRAW (in game_enter_draw()). +static char draw_was_pressed[2]; + +// --- per-state enter functions ----------------------------------------- +// +// Each "enter" function: +// - calls show_screen() (or show_white_screen() for DRAW) +// - calls score_render() +// - resets `frame` to 0 +// - resets any per-state state (e.g. title_input, draw_was_pressed) +// - sets the border color (TITLE flashes it; DRAW is all-white; +// other states are black) + +static void game_enter_title(void) +{ + show_screen(SCREEN_TITLE); + score_render(); + frame = 0; + title_input = TITLE_IDLE; + title_first_frame = 0; + // Border starts white (the "PRESS FIRE" prompt is visible). + vic.color_border = 1; +} + +static void game_enter_ready(void) +{ + show_screen(SCREEN_WAITING1); + score_render(); + frame = 0; + vic.color_border = 0; +} + +static void game_enter_wait(void) +{ + show_screen(SCREEN_WAITING2); + score_render(); + frame = 0; + vic.color_border = 0; +} + +static void game_enter_draw(void) +{ + show_white_screen(); + score_render(); + frame = 0; + vic.color_border = 1; // white border matches the white screen + draw_was_pressed[0] = 0; + draw_was_pressed[1] = 0; +} + +static void game_enter_win_p1(void) +{ + show_screen(SCREEN_WIN_HARE); + score_p1++; + score_render(); + frame = 0; + vic.color_border = 0; +} + +static void game_enter_win_p2(void) +{ + show_screen(SCREEN_WIN_SCOOT); + score_p2++; + score_render(); + frame = 0; + vic.color_border = 0; +} + +static void game_enter_gameover(void) +{ + // Show the title screen, but with the scores reset to 0/0 (the + // score bar makes this visually obvious: it's the title screen + // with an empty score bar). + show_screen(SCREEN_TITLE); + score_p1 = 0; + score_p2 = 0; + score_render(); + frame = 0; + vic.color_border = 0; +} + +// --- per-state step functions ------------------------------------------ + +static void game_step_title(void) +{ + // Flash the border at 25 Hz (toggle every 2 frames at 50 Hz). + // The visible effect is a 12.5 Hz blink on the border around the + // title screen image. Phase 8 will replace this with actual + // "PRESS FIRE" text rendered into the bitmap. + if ((frame & 1) == 0) + vic.color_border ^= 1; + + char p1 = input_fire(1); // Hare (port 1) + char p2 = input_fire(0); // Scoot (port 0) + char both = p1 && p2; + char none = !p1 && !p2; + + switch (title_input) { + case TITLE_IDLE: + if (both) { + state = STATE_READY; + game_enter_ready(); + } else if (p1 || p2) { + title_input = TITLE_FIRST_HELD; + title_first_frame = frame; + } + break; + + case TITLE_FIRST_HELD: + if (both) { + if (frame - title_first_frame <= 8) { + state = STATE_READY; + game_enter_ready(); + } else { + title_input = TITLE_WAIT_RELEASE; + } + } else if (none) { + title_input = TITLE_IDLE; + } else if (frame - title_first_frame > 8) { + // Held too long without the other button following. + title_input = TITLE_WAIT_RELEASE; + } + // else: one still held, other not yet, within 8 frames. + break; + + case TITLE_WAIT_RELEASE: + if (none) { + title_input = TITLE_IDLE; + } + break; + } +} + +static void game_step_ready(void) +{ + if (frame >= 60) { + state = STATE_WAIT; + game_enter_wait(); + } +} + +static void game_step_wait(void) +{ + // Phase 4: fixed 100 frames. Phase 5: random 100-250 from + // PEEK(0xD41B) sampled on READY enter. + if (frame >= 100) { + state = STATE_DRAW; + game_enter_draw(); + } +} + +static void game_step_draw(void) +{ + // 500-frame fault timeout (10 sec at 50 Hz). If neither player + // fires in 10 sec, abort the round and go back to TITLE. + if (frame > 500) { + state = STATE_TITLE; + game_enter_title(); + return; + } + + // First to fire wins. Rising-edge detection so holding fire from + // before DRAW doesn't auto-trigger a win (e.g. if the player + // presses during WAIT and keeps it held). P1 wins ties (matches + // the "fire1 first" branch in GAME.md §9). + char now_p1 = input_fire(1); + char now_p2 = input_fire(0); + char edge_p1 = now_p1 && !draw_was_pressed[0]; + char edge_p2 = now_p2 && !draw_was_pressed[1]; + draw_was_pressed[0] = now_p1; + draw_was_pressed[1] = now_p2; + + if (edge_p1) { + state = STATE_WIN_P1; + game_enter_win_p1(); + } else if (edge_p2) { + state = STATE_WIN_P2; + game_enter_win_p2(); + } +} + +static void game_step_win_p1(void) +{ + if (frame >= 100) { + if (score_p1 >= 5) { + state = STATE_GAMEOVER; + game_enter_gameover(); + } else { + state = STATE_READY; + game_enter_ready(); + } + } +} + +static void game_step_win_p2(void) +{ + if (frame >= 100) { + if (score_p2 >= 5) { + state = STATE_GAMEOVER; + game_enter_gameover(); + } else { + state = STATE_READY; + game_enter_ready(); + } + } +} + +static void game_step_gameover(void) +{ + if (frame >= 300) { + state = STATE_TITLE; + game_enter_title(); + } +} + +// --- public API -------------------------------------------------------- + +void game_init(void) +{ + state = STATE_TITLE; + game_enter_title(); +} + +void game_step(void) +{ + frame++; + + switch (state) { + case STATE_TITLE: game_step_title(); break; + case STATE_READY: game_step_ready(); break; + case STATE_WAIT: game_step_wait(); break; + case STATE_DRAW: game_step_draw(); break; + case STATE_WIN_P1: game_step_win_p1(); break; + case STATE_WIN_P2: game_step_win_p2(); break; + case STATE_GAMEOVER: game_step_gameover(); break; + } +} diff --git a/src/game.h b/src/game.h new file mode 100644 index 0000000..720c647 --- /dev/null +++ b/src/game.h @@ -0,0 +1,61 @@ +#ifndef WHACK_HARE_GAME_H +#define WHACK_HARE_GAME_H + +// game.h — Whack Hare! state machine. +// +// States (see game.c for the per-state behavior and the transition +// diagram from GAME.md §9): +// +// STATE_TITLE — title screen, flashing border, wait for both fire +// STATE_READY — "ready" screen, 60 frames (1.2 s at 50 Hz) +// STATE_WAIT — "wait" screen, 100 frames (2 s; Phase 5: random) +// STATE_DRAW — white screen, wait for first fire (or 500-frame +// fault timeout) +// STATE_WIN_P1 — "Hare won" screen, 100 frames, +1 to player 1 +// STATE_WIN_P2 — "Scoot won" screen, 100 frames, +1 to player 2 +// STATE_GAMEOVER — title screen with scores reset, 300 frames +// +// Transitions: +// +// TITLE --both fire (within 8 frames)--> READY +// READY --60 frames--------------------> WAIT +// WAIT --100 frames-------------------> DRAW +// DRAW --port 1 fire (rising edge)---> WIN_P1 +// DRAW --port 0 fire (rising edge)---> WIN_P2 +// DRAW --500 frames (fault)----------> TITLE +// WIN_P1 --100 frames + score < 5-----> READY +// WIN_P1 --100 frames + score == 5----> GAMEOVER +// WIN_P2 --100 frames + score < 5-----> READY +// WIN_P2 --100 frames + score == 5----> GAMEOVER +// GAMEOVER --300 frames (scores reset)--> TITLE + +#define STATE_TITLE 0 +#define STATE_READY 1 +#define STATE_WAIT 2 +#define STATE_DRAW 3 +#define STATE_WIN_P1 4 +#define STATE_WIN_P2 5 +#define STATE_GAMEOVER 6 + +// game_init() — set up the state machine and enter the TITLE state. +// Call once at startup, after memmap_setup() and score_init(). +// Calls show_screen(SCREEN_TITLE) and score_render() as part of the +// TITLE entry action. +void game_init(void); + +// game_step() — advance the state machine by one frame. +// +// In Phase 4 the main loop calls this in a tight busy-wait. In +// Phase 5 a raster IRQ handler at line 311 will call it at 50 Hz. +// Per-state step actions read both fire buttons, advance the per-state +// frame counter, and trigger state transitions (each transition calls +// the new state's "enter" action immediately, so the next step +// operates on the new state). +// +// game_step() also drives per-state visual updates that need to run +// every frame (currently just the TITLE border flash at 25 Hz). +void game_step(void); + +#pragma compile("game.c") + +#endif diff --git a/src/main.c b/src/main.c index fa5b759..efc2596 100644 --- a/src/main.c +++ b/src/main.c @@ -1,39 +1,39 @@ -// main.c — Whack Hare! entry point (Phase 3: score bar). +// main.c — Whack Hare! entry point (Phase 4: state machine skeleton). // // Flow: // 1. memmap_setup() — bank out KERNAL/BASIC/CHAR ROM. -// 2. show_screen(TITLE) — copy the title bitmap into $E000 and -// configure the VIC for multicolor bitmap -// mode. -// 3. score_init() — zero both player scores. -// 4. score_render() — overlay the score bar on the top 8 rows. -// 5. Loop: poll joystick port 1 for fire. When pressed, restore -// the C64's default memory config and return from main() (which -// ends the program). +// 2. score_init() — zero both player scores. +// 3. game_init() — enter the TITLE state (which also loads +// the title screen and renders the score +// bar). +// 4. while (1) game_step() — the state machine runs forever. // -// Build: ./build.sh (compile to src/build/whack_hare.prg) -// Run: ./build.sh -e (headless, in oscar64's built-in emulator) -// ./build.sh -v (VICE x64, interactive, needs a display) +// In Phase 4 this is a busy-wait loop: game_step() is called in a +// tight loop with no real timing. The "frame counter" inside game.c +// is just a count of how many times game_step() has been called, so +// the state durations are CPU-bound, not wall-clock-bound. +// +// In Phase 5 the busy-wait body of the main loop becomes a raster +// IRQ handler that runs at 50 Hz, and game_step() is called from +// the IRQ. The function signature doesn't change — only the call +// site does — so the game logic is the same in both phases. #include "memmap.h" -#include "screens.h" -#include "input.h" +#include "game.h" #include "score.h" +// We don't malloc, so the heap is unused. Setting it to 0 frees the +// space for the screen data in the main region. +#pragma heapsize(0) + int main(void) { memmap_setup(); - show_screen(SCREEN_TITLE); - score_init(); - score_render(); + game_init(); - // Phase 3: just poll for fire on port 1 (the Hare / right joystick). - // In Phase 4 this becomes a 50 Hz raster IRQ handler that drives the - // full state machine. - while (!input_fire(1)) - ; + while (1) + game_step(); - memmap_restore(); return 0; } diff --git a/src/screens.c b/src/screens.c index 7842c39..5d4d414 100644 --- a/src/screens.c +++ b/src/screens.c @@ -1,114 +1,225 @@ #include "screens.h" #include #include +#include #include -// Embedded screen data. In Phase 2 only the title screen is wired in; -// the other 4 will be added in Phase 4 (state machine) when we need -// them. The arrays are const so they live in the ROM/load-image part -// of the .prg, not in BSS. +// --- memory layout for the embedded screen data ------------------------ // -// Screen memory layout (in multicolor bitmap mode) per -// cebix-vic-article §3.7.3.4: -// "00" pixel -> $D021 (background, per-screen) -// "01" pixel -> high nibble of screen memory byte -// "10" pixel -> low nibble of screen memory byte -// "11" pixel -> color RAM nibble (we set to 0 = black) +// The 5 LZO-compressed bitmaps are ~28 KB total (5500-6100 bytes per +// screen). The 5 .attr tables are 5 KB total (1000 bytes each, kept +// uncompressed because they don't compress well). Together that's +// ~33 KB, more than the default main region (~34 KB) can comfortably +// hold alongside code (~700 B), BSS (~50 B), stack, and heap. // -// convert_screens.py's .attr file stores the screen memory byte: -// bits 0-3 = "10" color -// bits 4-7 = "01" color -// (the "11" color is not stored; color RAM defaults to black at boot). +// So we split the data across two regions: +// +// 1. LZO bitmaps in the main region (default data section, $0880- +// $9000). They're read-only, used once per state transition, and +// they go right after the code so the linker can pack them +// tightly. +// +// 2. .attr tables in a custom "screens" region at $A000-$C000 (the +// BASIC ROM area, which is banked out as RAM after +// memmap_setup() and is not used by the trampoline). This is +// 8 KB, more than enough for 5 × 1000 = 5 KB of attr data. +// +// The trampoline is in the code section (around $08B3-$08FD), not at +// $A000+ — the $A000+ entries in the .map are sstack (subroutine +// stack) declarations of size 0, not actual data. So the entire +// $A000-$C000 range is free for our use. +// +// We don't need a heap for this program (no malloc). We set +// heapsize(0) below in main.c to maximize the room available for data. + +// --- embedded bitmap data (LZO-compressed, in main region) ------------ const char ScreenTitleBin[] = { - #embed "data/processed/title.bin" + #embed 8000 0 lzo "data/processed/title.bin" }; +const char ScreenWaiting1Bin[] = { + #embed 8000 0 lzo "data/processed/waiting1.bin" +}; + +const char ScreenWaiting2Bin[] = { + #embed 8000 0 lzo "data/processed/waiting2.bin" +}; + +const char ScreenWinHareBin[] = { + #embed 8000 0 lzo "data/processed/win_hare.bin" +}; + +const char ScreenWinScootBin[] = { + #embed 8000 0 lzo "data/processed/win_scoot.bin" +}; + +// --- embedded .attr data (uncompressed, in custom "screens" region) --- + +#pragma section( screens, 0) + +#pragma region( screens, 0xA000, 0xC000, , , {screens} ) + +#pragma data(screens) + const char ScreenTitleAttr[] = { #embed "data/processed/title.attr" }; -// The $D021 background color for the title screen. Generated by -// tools/convert_screens.py as a separate .d021 sidecar, but the value -// is a single small integer (0..15) and we hardcode it here rather -// than read the file at runtime. When we add the other 4 screens in -// Phase 4 we'll fold the d021 value into a per-screen descriptor -// struct so the compiler can't constant-fold it. -#define SCREEN_TITLE_D021 0 +const char ScreenWaiting1Attr[] = { + #embed "data/processed/waiting1.attr" +}; -// Copy `len` bytes from `src` to `dst`. +const char ScreenWaiting2Attr[] = { + #embed "data/processed/waiting2.attr" +}; + +const char ScreenWinHareAttr[] = { + #embed "data/processed/win_hare.attr" +}; + +const char ScreenWinScootAttr[] = { + #embed "data/processed/win_scoot.attr" +}; + +#pragma data(data) + +// --- per-screen descriptor --------------------------------------------- // -// Both source and destination are 16-bit-addressed memory regions in -// the C64. Using a simple byte-by-byte copy (rather than the c64 -// memcpy helper, which is in the runtime and pulls in more code) keeps -// the .prg small. The compiler turns this into a tight loop; for -// 8000 bytes that's well under a frame at 1 MHz. +// d021 is the $D021 background color value generated by +// tools/convert_screens.py as a .d021 sidecar file. We inline the +// value here rather than reading the file at runtime: +// title=0 (black), waiting1=0 (black), waiting2=11 (dark grey), +// win_hare=0 (black), win_scoot=0 (black). + +struct ScreenDef { + const char *lzo; // LZO-compressed bitmap + const char *attr; // raw screen memory (1000 bytes) + byte d021; // background color +}; + +static const struct ScreenDef screens[5] = { + { ScreenTitleBin, ScreenTitleAttr, 0 }, // SCREEN_TITLE + { ScreenWaiting1Bin, ScreenWaiting1Attr, 0 }, // SCREEN_WAITING1 + { ScreenWaiting2Bin, ScreenWaiting2Attr, 11 }, // SCREEN_WAITING2 + { ScreenWinHareBin, ScreenWinHareAttr, 0 }, // SCREEN_WIN_HARE + { ScreenWinScootBin, ScreenWinScootAttr, 0 }, // SCREEN_WIN_SCOOT +}; + +// --- helpers ----------------------------------------------------------- + +// Copy `len` bytes from `src` to `dst`. Used for the .attr data +// (1000 bytes) and for the show_white_screen() bitmap clear. The +// compiler turns this into a tight loop; for 1000 bytes that's well +// under a frame at 1 MHz. static void copy_bytes(const char *src, char *dst, unsigned len) { - for (unsigned i = 0; i < len; i++) - dst[i] = src[i]; + for (unsigned i = 0; i < len; i++) + dst[i] = src[i]; } +// Clear the color RAM at $D800-$DBFF (4 pages × 256 bytes, slightly +// more than the 1000-byte logical range $D800-$DBE7; the extra 24 +// bytes are harmless mirrors). Each nibble = 0 = black, which is the +// default "11" pixel value for cells that don't explicitly set color +// RAM. +static void clear_color_ram(void) +{ + __asm { + lda #0 + ldx #4 + ldy #0 + L0: sta $d800, y + sta $d900, y + sta $da00, y + sta $db00, y + iny + bne L0 + dex + bne L0 + } +} + +// Configure the VIC for multicolor bitmap mode pointing at the data +// at $D000 (screen memory) and $E000 (bitmap). Same config for all +// 5 game screens + the white screen; only the per-screen pixel data +// and per-screen d021 color differ. +static void vic_setup_mcm(void) +{ + vic.ctrl1 = VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL; + vic.ctrl2 = VIC_CTRL2_MCM | VIC_CTRL2_CSEL; + cia2.pra = (cia2.pra & 0xfc) | 0x00; + vic.memptr = 0x48; +} + +// --- public API: show_screen() ----------------------------------------- + void show_screen(int n) { - // 1. Write the new pixel/attribute data into the destination - // while the VIC is still in its old mode. This avoids a - // visible garbage frame during the mode switch. - // - // 2. Then flip the VIC into multicolor bitmap mode pointing at - // the new data. The VIC's line buffer absorbs the transition - // cleanly enough for our purposes. - if (n == SCREEN_TITLE) - { - copy_bytes(ScreenTitleBin, (char *)0xE000, sizeof(ScreenTitleBin)); - copy_bytes(ScreenTitleAttr, (char *)0xD000, sizeof(ScreenTitleAttr)); + if (n < 0 || n >= 5) return; + const struct ScreenDef *s = &screens[n]; - // Color RAM (the "11" color per cell). We don't have per-cell - // "11" data in the .attr file, so zero it (= black). This - // reduces each cell from 4 to 3 distinct colors but matches - // what convert_screens.py produces. - __asm - { - lda #0 - ldx #4 - ldy #0 - L0: sta $d800, y - sta $d900, y - sta $da00, y - sta $db00, y - iny - bne L0 - dex - bne L0 - } + // 1. Decompress the 8 KB bitmap into $E000-$FFFF and copy the 1 KB + // screen memory into $D000-$D3E7. Both happen while the VIC + // is still in its old mode (or, on the very first call, in + // whatever state memmap_setup() left it). We do the attr + // copy first so the visible region stays coherent for as long + // as possible during the bitmap decompression. + copy_bytes(s->attr, (char *)0xD000, 1000); + oscar_expand_lzo((char *)0xE000, s->lzo); - // Background color ($D021, the "00" color). The title screen - // uses color 0 (black); per-screen .d021 files are - // documentation, the value is inlined here. - vic.color_back = SCREEN_TITLE_D021; - vic.color_border = 0; + // 2. Clear color RAM (the "11" color per cell; we don't have + // per-cell "11" data in the .attr files, so it stays 0). + clear_color_ram(); - // 3. Now flip the VIC into multicolor bitmap mode. - // - // We set the registers explicitly rather than calling - // vic_setmode() so the yscroll/xscroll bits stay 0 - // (vic_setmode() hardcodes yscroll=3, which we don't want - // for a non-scrolling display). - // - // ctrl1 = BMM(1) | DEN(1) | RSEL(1) | yscroll(0) = 0x38 - // ctrl2 = MCM(1) | CSEL(1) | xscroll(0) = 0x18 - // - // CIA2 PRA low 2 bits = 0 -> VIC bank 0 = CPU $C000-$FFFF - // (both our $D000 screen and $E000 bitmap live there). - // - // memptr (D018) = 0x48 - // bits 7-4 = (0xD000 >> 6) & 0xF0 = 0x40 -> screen at $D000 - // bits 3-1 = (0xE000 >> 10) & 0x0E = 0x08 -> bitmap upper 8K - // bit 0 = 0 -> unused in BMM - vic.ctrl1 = VIC_CTRL1_BMM | VIC_CTRL1_DEN | VIC_CTRL1_RSEL; - vic.ctrl2 = VIC_CTRL2_MCM | VIC_CTRL2_CSEL; - cia2.pra = (cia2.pra & 0xfc) | 0x00; - vic.memptr = 0x48; - } - // Other SCREEN_* IDs: reserved for Phase 4. + // 3. Set the per-screen colors. + vic.color_back = s->d021; + vic.color_back1 = 0; // unused in the 5 menu screens (no "01" pixels) + vic.color_back2 = 0; // unused in the 5 menu screens (no "10" pixels) + vic.color_back3 = 0; + vic.color_border = 0; + + // 4. Flip the VIC into multicolor bitmap mode. + vic_setup_mcm(); +} + +// --- public API: show_white_screen() ----------------------------------- + +void show_white_screen(void) +{ + // Fill the 8 KB bitmap at $E000-$FFFF with 0x00 so every pixel is + // a "00" code (which uses $D021). At ~1 byte per 2-3 cycles via + // a simple loop, this is ~5-8 ms, well under a 20 ms frame. + char *p = (char *)0xE000; + for (unsigned i = 0; i < 8000; i++) + p[i] = 0; + + // Clear screen memory for tidiness. The top 40 cells are + // overwritten by score_render() right after this returns. + char *sm = (char *)0xD000; + for (unsigned i = 0; i < 1000; i++) + sm[i] = 0; + + // Clear color RAM. With the bitmap = 0 there are no "11" pixels, + // so this is technically unnecessary, but doing it keeps the + // score bar cells predictable (score_render() will set the top 40 + // cells' color RAM to 1 = white). + clear_color_ram(); + + // Whole screen white, including the border. $D021 is the "00" + // color and is the only one that matters for the body of the + // screen (bitmap is 0); $D022 and $D023 are set to white too in + // case any stray "01" / "10" pixel ever appears. $D020 is the + // visible border around the bitmap. + vic.color_back = 1; // $D021 = white + vic.color_back1 = 1; // $D022 = white (for any "01" pixel) + vic.color_back2 = 1; // $D023 = white (for any "10" pixel) + vic.color_back3 = 0; + vic.color_border = 1; // $D020 = white border + + // Same VIC mode config as show_screen() — the body of the + // bitmap happens to be 0, but the score bar overlay (drawn by + // score_render()) writes to the top 8 rows and expects the VIC + // to be in multicolor bitmap mode. + vic_setup_mcm(); } diff --git a/src/screens.h b/src/screens.h index 7a5fa3e..13a79d6 100644 --- a/src/screens.h +++ b/src/screens.h @@ -43,12 +43,37 @@ // memptr (D018) = 0x48 (screen at $D000, bitmap at $E000 within // the selected 16K VIC bank) // -// For Phase 2 only SCREEN_TITLE has embedded data; the other IDs are -// reserved for Phase 4 (state machine). Calling show_screen() with an -// unsupported ID is a no-op. +// Calling show_screen() with an unsupported ID is a no-op. void show_screen(int n); +// show_white_screen() — switch to the DRAW screen: solid white fill +// over the entire visible area (including the score bar, which becomes +// invisible). +// +// Implementation: +// 1. Fill the 8 KB bitmap at $E000-$FFFF with 0x00 so every pixel is +// a "00" code (which uses $D021). +// 2. Clear the 1 KB screen memory at $D000-$D3E7 (so any leftover +// "01" / "10" cell values are 0, in case the bitmap ever contains +// a non-zero pixel). +// 3. Clear the 1 KB color RAM at $D800-$DBE7. +// 4. Set $D021 (background) = white, $D020 (border) = white. Also +// set $D022 / $D023 to white for safety (they're not used for +// "00" pixels, but any stray "01" / "10" pixel would be black +// otherwise). +// 5. Same VIC mode config as show_screen(). +// +// The score bar overlay (drawn by score_render() in game.c) writes +// "00" (black, from $D021) and "11" (white, from color RAM) pixels in +// the top 8 rows. With $D021 and color RAM both white, the entire +// score bar is white and invisible against the white background. +// The bitmap rewrite on the score bar cells is harmless; it just +// produces more white. +// +// Called by game.c on entry to STATE_DRAW. +void show_white_screen(void); + #pragma compile("screens.c") #endif