From a8ce56464da2f51a355f935309960092e527726d Mon Sep 17 00:00:00 2001 From: ballz Date: Fri, 17 Jul 2026 02:30:43 +0200 Subject: [PATCH] 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. --- src/draw.c | 104 +++++++++++++++++++++++++++++++++++++++++++++++++++++ src/draw.h | 20 +++++++++++ src/game.c | 35 ++++++++++++++++++ src/game.h | 5 +-- 4 files changed, 162 insertions(+), 2 deletions(-) create mode 100644 src/draw.c create mode 100644 src/draw.h diff --git a/src/draw.c b/src/draw.c new file mode 100644 index 0000000..479f6a4 --- /dev/null +++ b/src/draw.c @@ -0,0 +1,104 @@ +// draw.c — DRAW screen counter rendering (Phase 7). +// +// 3-digit 7-segment counter, each digit 3 cells wide × 8 cells tall. +// Layout of segments within a digit (col 0..2 left-to-right, row +// 0..7 top-to-bottom): +// +// row 0: [ ] [a] [ ] +// row 1: [f] [ ] [b] +// row 2: [f] [ ] [b] +// row 3: [f] [g] [b] +// row 4: [e] [g] [c] +// row 5: [e] [ ] [c] +// row 6: [e] [ ] [c] +// row 7: [ ] [d] [ ] +// +// "On" segments: bitmap = 0xFF (all "11" pixels) + color RAM = 0 +// (black). "Off" segments: bitmap = 0x00 (all "00" pixels) + +// color RAM = 0 (no-op for "00" pixels, which use $D021 = white). +// On the white background, the result is: black digits on white. +// +// The middle segment (g) is 2 cells tall to match the 3-cell-tall +// vertical segments visually. 16 of 24 cells are filled when all +// 7 segments are on (the "8"); only 6 of 24 are filled for the "1". + +#include "draw.h" +#include + +// 7-segment pattern table. Bit 0 = a, bit 1 = b, bit 2 = c, +// bit 3 = d, bit 4 = e, bit 5 = f, bit 6 = g. +static const char seg7[10] = { + 0x3F, // 0: a b c d e f + 0x06, // 1: b c + 0x5B, // 2: a b d e g + 0x4F, // 3: a b c d g + 0x66, // 4: b c f g + 0x6D, // 5: a c d f g + 0x7D, // 6: a c d e f g + 0x07, // 7: a b c + 0x7F, // 8: a b c d e f g + 0x6F, // 9: a b c d f g +}; + +// Bitmap at $E000, color RAM at $D800. Each cell = 8 bitmap bytes. +#define BM_BASE ((char *)0xE000) +#define CRAM_BASE ((char *)0xD800) + +#define DIGIT_START_ROW 8 +#define DIGIT_START_COL 15 +#define DIGIT_CELLS_WIDE 3 +#define DIGIT_CELLS_TALL 8 +#define BYTES_PER_CELL 8 +#define CELLS_PER_ROW 40 + +// Returns 1 if the cell at (row, col) within a 3x8 digit is "on" +// for the given 7-segment bit pattern. +static char cell_is_on(byte pattern, byte row, byte col) +{ + if (col == 0) { + if (row >= 1 && row <= 3) return (pattern & 0x20) != 0; // f + if (row >= 4 && row <= 6) return (pattern & 0x10) != 0; // e + } else if (col == 2) { + if (row >= 1 && row <= 3) return (pattern & 0x02) != 0; // b + if (row >= 4 && row <= 6) return (pattern & 0x04) != 0; // c + } else { + if (row == 0) return (pattern & 0x01) != 0; // a + if (row == 3 || row == 4) return (pattern & 0x40) != 0; // g + if (row == 7) return (pattern & 0x08) != 0; // d + } + return 0; +} + +// Render a single digit at the given top-left cell. Writes to the +// bitmap and color RAM for the 24 cells of the digit. Color RAM is +// always zeroed (black) so any "11" pixel reads as black; "00" +// pixels (off segments) read $D021 = white regardless. +static void render_digit(unsigned start_cell, byte digit) +{ + byte pattern = seg7[digit]; + for (byte row = 0; row < DIGIT_CELLS_TALL; row++) { + for (byte col = 0; col < DIGIT_CELLS_WIDE; col++) { + unsigned cell = start_cell + (unsigned)row * CELLS_PER_ROW + col; + char *bm = BM_BASE + cell * BYTES_PER_CELL; + char fill = cell_is_on(pattern, row, col) ? (char)0xFF : (char)0x00; + for (byte r = 0; r < BYTES_PER_CELL; r++) + bm[r] = fill; + CRAM_BASE[cell] = 0; + } + } +} + +void draw_render_counter(int value) +{ + if (value < 0) value = 0; + if (value > 999) value = 999; + + byte d0 = (byte)(value / 100); + byte d1 = (byte)((value / 10) % 10); + byte d2 = (byte)(value % 10); + + unsigned base = (unsigned)DIGIT_START_ROW * CELLS_PER_ROW + DIGIT_START_COL; + render_digit(base + 0, d0); + render_digit(base + 3, d1); + render_digit(base + 6, d2); +} diff --git a/src/draw.h b/src/draw.h new file mode 100644 index 0000000..59229a6 --- /dev/null +++ b/src/draw.h @@ -0,0 +1,20 @@ +#ifndef WHACK_HARE_DRAW_H +#define WHACK_HARE_DRAW_H + +// draw.h — DRAW screen counter rendering. +// +// The DRAW screen is solid white (set up by show_white_screen). +// This header exposes the per-frame counter renderer, which writes +// 3 seven-segment-style digits (each 3 cells wide × 8 cells tall, +// 1 cell per segment) into the bitmap at row 8, columns 15-23. +// +// Public API: +// draw_render_counter(value) — render the 0..999 value as a +// 3-digit counter on the white screen. Values outside 0..999 +// are clamped. + +void draw_render_counter(int value); + +#pragma compile("draw.c") + +#endif diff --git a/src/game.c b/src/game.c index efdcae1..d2737b8 100644 --- a/src/game.c +++ b/src/game.c @@ -26,6 +26,7 @@ #include "screens.h" #include "input.h" #include "score.h" +#include "draw.h" #include #include @@ -79,6 +80,13 @@ static unsigned short title_first_frame; // entry to DRAW (in game_enter_draw()). static char draw_was_pressed[2]; +// The DRAW screen's per-frame counter. Reset to 0 in game_enter_draw +// (with an initial draw_render_counter(0) call to draw "000" on the +// freshly-cleared white screen), then incremented each frame in +// game_step_draw up to a cap of 999. The 16-bit type is more than +// enough: we never store more than 999, and 999 fits in 10 bits. +static unsigned short draw_counter; + // --- fault stinger ----------------------------------------------------- // // When the DRAW state's 500-frame fault timeout fires, game_step_draw @@ -98,6 +106,8 @@ static char draw_was_pressed[2]; // other states are black) // - samples the SID random for WAIT duration (in READY enter, not // WAIT enter, per the Phase 5 spec) +// - resets and renders the DRAW counter (DRAW only: draw_counter +// = 0, draw_render_counter(0)) // - calls audio_state_enter(state) to set up the SID for this // state's cue. This is the last step so the previous state's // audio (if any) is still playing while we set up the screen, @@ -151,6 +161,13 @@ static void game_enter_draw(void) vic.color_border = 1; // white border matches the white screen draw_was_pressed[0] = 0; draw_was_pressed[1] = 0; + // Reset the per-frame counter and render "000" once. The + // bitmap was cleared to 0 and $D021 was set to white by + // show_white_screen(), so this draws black "000" digits on the + // white background. game_step_draw() will then increment the + // counter and re-render each frame. + draw_counter = 0; + draw_render_counter(0); audio_state_enter(STATE_DRAW); } @@ -266,6 +283,24 @@ static void game_step_draw(void) { unsigned short elapsed = frame_count - enter_frame; + // Flash effect: the border $D020 strobes between black and white + // for the first 4 frames of DRAW, then settles to white. The + // strobe creates a brief visible "flash" at the start of the + // round (4 frames at 50 Hz = ~80 ms). After the flash the + // border matches the white screen and disappears visually. + if (elapsed < 5) + vic.color_border = (elapsed & 1) ? 0 : 1; + else + vic.color_border = 1; + + // Per-frame counter: increment (capped at 999) and re-render. + // Drawn after the flash so the border strobe and the digit + // update happen in the same IRQ; the next visible frame shows + // both updates together. + if (draw_counter < 999) + draw_counter++; + draw_render_counter(draw_counter); + // 500-frame fault timeout (10 sec at 50 Hz). If neither player // fires in 10 sec, abort the round, trigger a short stinger on // SID voice 1, and go back to TITLE. No point awarded. diff --git a/src/game.h b/src/game.h index 2df495e..0dad86d 100644 --- a/src/game.h +++ b/src/game.h @@ -73,8 +73,9 @@ void game_init(void); // 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 and -// the fault-stinger audio gate-off counter). +// every frame (the TITLE border flash at 25 Hz, the DRAW border +// flash + per-frame counter, and the fault-stinger audio gate-off +// counter). void game_step(void); #pragma compile("game.c")