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.
This commit is contained in:
ballz
2026-07-17 02:30:43 +02:00
parent a621494a21
commit a8ce56464d
4 changed files with 162 additions and 2 deletions
+104
View File
@@ -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 <c64/types.h>
// 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);
}
+20
View File
@@ -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
+35
View File
@@ -26,6 +26,7 @@
#include "screens.h"
#include "input.h"
#include "score.h"
#include "draw.h"
#include <c64/vic.h>
#include <c64/sid.h>
@@ -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.
+3 -2
View File
@@ -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")