From 9f086c3a4f1a2fd012ba6ac7328eb74d46a87fa4 Mon Sep 17 00:00:00 2001 From: ballz Date: Fri, 17 Jul 2026 01:35:40 +0200 Subject: [PATCH] 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. --- src/main.c | 12 ++- src/score.c | 207 ++++++++++++++++++++++++++++++++++++++++++++++++++++ src/score.h | 62 ++++++++++++++++ 3 files changed, 278 insertions(+), 3 deletions(-) create mode 100644 src/score.c create mode 100644 src/score.h diff --git a/src/main.c b/src/main.c index 9fe96eb..fa5b759 100644 --- a/src/main.c +++ b/src/main.c @@ -1,11 +1,13 @@ -// main.c — Whack Hare! entry point (Phase 2: display title screen). +// main.c — Whack Hare! entry point (Phase 3: score bar). // // 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. Loop: poll joystick port 1 for fire. When pressed, restore +// 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). // @@ -16,13 +18,17 @@ #include "memmap.h" #include "screens.h" #include "input.h" +#include "score.h" int main(void) { memmap_setup(); show_screen(SCREEN_TITLE); - // Phase 2: just poll for fire on port 1 (the Hare / right joystick). + score_init(); + score_render(); + + // 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)) diff --git a/src/score.c b/src/score.c new file mode 100644 index 0000000..d3c177a --- /dev/null +++ b/src/score.c @@ -0,0 +1,207 @@ +// score.c — score bar rendering for the top 8 rows of the multicolor +// bitmap. See score.h for the layout. +// +// In multicolor bitmap mode the bitmap is 160 px wide × 200 px tall +// (2 bpp) = 8000 bytes. One row of pixels = 40 bytes (160 px × 2 bpp +// / 8 bits). The top 8 rows = 320 bytes at $E000..$E13F. +// +// We draw three kinds of things into those 320 bytes: +// 1. 4×8 label characters (one cell = 8 bytes of bitmap). +// 2. 8×8 pips (two cells = 16 bytes of bitmap). +// 3. Background fill (00 pixel = $D021 black) for the cells outside +// the labels and pips (the margins). +// +// All visible pixels are either "00" (black, from $D021) or "11" (white, +// from color RAM). We rewrite the top 40 cells' attributes +// (screen memory → 0, color RAM → 1) so the cell palette is exactly +// 2 colors and we don't have to think about 01/10 mismatches across +// the 4 cells of the bar. + +#include "score.h" + +// Player scores. 0..5 (clamped at the render side). Stored in zero-page- +// adjacent BSS, default 0. Phase 4 will mutate these on WIN_P1/WIN_P2. +byte score_p1 = 0; +byte score_p2 = 0; + +// --- custom 4×8 font --------------------------------------------------- +// +// 8 characters, each 4 px wide × 8 px tall = 8 bytes (one byte per row, +// top 4 bits are the 4 pixels of that row, MSB = leftmost pixel). +// 64 bytes total. Bottom 4 bits of every byte are 0. +// +// Index: +// 0 = H, 1 = A, 2 = R, 3 = E, 4 = S, 5 = C, 6 = O, 7 = T +static const char font[8][8] = { + // H (1 0 0 1) × 3, (1 1 1 1), (1 0 0 1) × 4 + { 0x90, 0x90, 0x90, 0xF0, 0x90, 0x90, 0x90, 0x90 }, + // A (0 1 1 0), (1 0 0 1) × 2, (1 1 1 1), (1 0 0 1) × 4 + { 0x60, 0x90, 0x90, 0xF0, 0x90, 0x90, 0x90, 0x90 }, + // R (1 1 1 0), (1 0 0 1) × 2, (1 1 1 0), (1 0 1 0), + // (1 0 0 1) × 3 + { 0xE0, 0x90, 0x90, 0xE0, 0xA0, 0x90, 0x90, 0x90 }, + // E (1 1 1 1), (1 0 0 0) × 2, (1 1 1 0), (1 0 0 0) × 3, (1 1 1 1) + { 0xF0, 0x80, 0x80, 0xE0, 0x80, 0x80, 0x80, 0xF0 }, + // S (0 1 1 1), (1 0 0 0) × 2, (0 1 1 0), (0 0 0 1) × 3, (1 1 1 0) + { 0x70, 0x80, 0x80, 0x60, 0x10, 0x10, 0x10, 0xE0 }, + // C (0 1 1 0), (1 0 0 1), (1 0 0 0) × 4, (1 0 0 1), (0 1 1 0) + { 0x60, 0x90, 0x80, 0x80, 0x80, 0x80, 0x90, 0x60 }, + // O (0 1 1 0), (1 0 0 1) × 5, (0 1 1 0) + { 0x60, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x60 }, + // T (1 1 1 1), (0 0 1 0) × 7 + { 0xF0, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20 }, +}; + +// --- pixel-pair expansion table ---------------------------------------- +// +// For each 4-bit pixel pattern (1 = on, 0 = off), the corresponding +// multicolor bitmap byte. Each "1" becomes "11" (white, from color RAM); +// each "0" becomes "00" (background, from $D021 = black). +// +// bit pattern | multicolor byte +// 0000 | 00 00 00 00 = 0x00 +// 0001 | 00 00 00 11 = 0x03 +// 0010 | 00 00 11 00 = 0x0C +// 0011 | 00 00 11 11 = 0x0F +// 0100 | 00 11 00 00 = 0x30 +// 0101 | 00 11 00 11 = 0x33 +// 0110 | 00 11 11 00 = 0x3C +// 0111 | 00 11 11 11 = 0x3F +// 1000 | 11 00 00 00 = 0xC0 +// 1001 | 11 00 00 11 = 0xC3 +// 1010 | 11 00 11 00 = 0xCC +// 1011 | 11 00 11 11 = 0xCF +// 1100 | 11 11 00 00 = 0xF0 +// 1101 | 11 11 00 11 = 0xF3 +// 1110 | 11 11 11 00 = 0xFC +// 1111 | 11 11 11 11 = 0xFF +static const char expand4[16] = { + 0x00, 0x03, 0x0C, 0x0F, 0x30, 0x33, 0x3C, 0x3F, + 0xC0, 0xC3, 0xCC, 0xCF, 0xF0, 0xF3, 0xFC, 0xFF +}; + +// --- pip bitmaps ------------------------------------------------------- +// +// 8×8 pixels = 2 cells wide × 1 cell tall = 16 bytes of bitmap +// (8 rows × 2 bytes/row, 4 pixels per byte in 2 bpp). +// +// Filled pip: 1-pixel white border around a 6×6 black block. +// Empty pip: 8×8 white square (the "border only" with no fill). +// +// Row 0 = top border: 8 white pixels = $FF $FF +// Rows 1..6: 1 white + 6 black + 1 white: +// left byte = 11 00 00 00 = 0xC0 +// right byte = 00 00 00 11 = 0x03 +// Row 7 = bottom border: $FF $FF +static const char pip_filled[16] = { + 0xFF, 0xFF, 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, + 0xC0, 0x03, 0xC0, 0x03, 0xC0, 0x03, 0xFF, 0xFF +}; + +static const char pip_empty[16] = { + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, + 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF +}; + +// --- cell layout constants -------------------------------------------- +// +// The score bar fits in the top 40 cells (one character row). Each +// cell is 4 px wide × 8 px tall = 8 bytes of bitmap. The whole top +// row = 40 × 8 = 320 bytes at $E000..$E13F. + +#define SCORE_BAR_BYTES 320 +#define CELLS_PER_ROW 40 +#define BYTES_PER_CELL 8 +#define SCORE_BITMAP_BASE ((char *)0xE000) +#define SCORE_SCREEN_BASE ((char *)0xD000) +#define SCORE_CRAM_BASE ((char *)0xD800) + +// Cell indices for the labels and pip groups (see score.h for diagram). +// Pips are 2 cells wide; the left pips start at cell 7, the right pips +// at cell 22. Stride between adjacent pips is 2 cells (no gap; the +// 1-px borders of adjacent pips merge into a 2-px white vertical line). +#define HARE_FIRST_CELL 2 +#define LEFT_PIPS_FIRST_CELL 7 +#define RIGHT_PIPS_FIRST_CELL 22 +#define SCOOT_FIRST_CELL 33 + +// --- helpers ----------------------------------------------------------- + +static void clear_score_bar(void) +{ + for (unsigned i = 0; i < SCORE_BAR_BYTES; i++) + SCORE_BITMAP_BASE[i] = 0; +} + +static void setup_score_bar_cells(void) +{ + // Top 40 cells: screen memory = 0, color RAM = 1. + // Screen memory 0 means both "01" and "10" pixel codes map to black + // (we don't use either code in the score bar). Color RAM 1 means + // the "11" pixel code maps to white — that's the color of the + // pip borders and the label characters. + for (unsigned i = 0; i < CELLS_PER_ROW; i++) { + SCORE_SCREEN_BASE[i] = 0; + SCORE_CRAM_BASE[i] = 1; + } +} + +static void render_char(unsigned cell_idx, unsigned char_idx) +{ + char *dst = SCORE_BITMAP_BASE + cell_idx * BYTES_PER_CELL; + const char *src = font[char_idx]; + for (unsigned row = 0; row < BYTES_PER_CELL; row++) { + unsigned nibble = ((unsigned)src[row] >> 4) & 0x0F; + dst[row] = expand4[nibble]; + } +} + +static void render_pip(unsigned cell_idx, char filled) +{ + char *dst = SCORE_BITMAP_BASE + cell_idx * BYTES_PER_CELL; + const char *src = filled ? pip_filled : pip_empty; + for (unsigned i = 0; i < 16; i++) + dst[i] = src[i]; +} + +// --- public API -------------------------------------------------------- + +void score_init(void) +{ + score_p1 = 0; + score_p2 = 0; +} + +void score_render(void) +{ + // Clamp to 0..5 so a future bug in Phase 4 (e.g. score == 6 on a + // double-tap) doesn't write past the pip array. + byte p1 = score_p1; + if (p1 > 5) p1 = 5; + byte p2 = score_p2; + if (p2 > 5) p2 = 5; + + setup_score_bar_cells(); + clear_score_bar(); + + // HARE label: 4 cells, 4 unique chars. + render_char(HARE_FIRST_CELL + 0, 0); // H + render_char(HARE_FIRST_CELL + 1, 1); // A + render_char(HARE_FIRST_CELL + 2, 2); // R + render_char(HARE_FIRST_CELL + 3, 3); // E + + // 5 pips for player 1 (left side). 2 cells per pip, no gap. + for (unsigned p = 0; p < 5; p++) + render_pip(LEFT_PIPS_FIRST_CELL + p * 2, (byte)p < p1); + + // 5 pips for player 2 (right side). + for (unsigned p = 0; p < 5; p++) + render_pip(RIGHT_PIPS_FIRST_CELL + p * 2, (byte)p < p2); + + // SCOOT label: 5 cells, 4 unique chars (O repeats). + render_char(SCOOT_FIRST_CELL + 0, 4); // S + render_char(SCOOT_FIRST_CELL + 1, 5); // C + render_char(SCOOT_FIRST_CELL + 2, 6); // O + render_char(SCOOT_FIRST_CELL + 3, 6); // O + render_char(SCOOT_FIRST_CELL + 4, 7); // T +} diff --git a/src/score.h b/src/score.h new file mode 100644 index 0000000..7411401 --- /dev/null +++ b/src/score.h @@ -0,0 +1,62 @@ +#ifndef WHACK_HARE_SCORE_H +#define WHACK_HARE_SCORE_H + +// score.h — score state and rendering for the top-of-screen score bar. +// +// The score bar lives in the top 8 rows of the multicolor bitmap at +// $E000-$E13F. Those rows are one full character row of the screen +// (40 cells × 8 px tall × 2 bpp = 320 bytes). show_screen() loads the +// title artwork into the bitmap; score_render() overwrites the first +// 320 bytes with the score bar. +// +// Layout (40 cells = 160 px, since multicolor is half-width hires): +// +// cell 0 1 2 3 4 5 6 7 ... 16 17 18 19 20 21 22 ... 31 32 33 34 35 36 37 38 39 +// |marg| H A R E |gap| 5 pips |marg|marg| 5 pips |gap| S C O O T |marg| +// +// HARE label: cells 2..5 (4 chars) +// 5 pips P1: cells 7..16 (10 cells, 2 cells per pip, no gap) +// 5 pips P2: cells 22..31 (10 cells) +// SCOOT label: cells 33..37 (5 chars) +// +// Each pip is 8×8 pixels (2 cells wide × 1 cell tall) = 16 bytes of bitmap: +// - filled: 1-pixel white border around a 6×6 black block +// - empty: just the white border (an 8×8 white square with no inside) +// +// Each label char is 4×8 pixels (1 cell) = 8 bytes of bitmap, drawn as +// "11" (white) on "00" (background black). Cell attributes for the top +// row are rewritten: screen memory = 0 (both halves black), color RAM = +// 1 (white, so the "11" pixel value maps to white). +// +// All 4 colors used by the bitmap (00/01/10/11) per c64-wiki: +// 00 -> $D021 (background; set to 0 = black by show_screen) +// 01 -> high nibble of screen mem (we set 0 = black; unused here) +// 10 -> low nibble of screen mem (we set 0 = black; unused here) +// 11 -> color RAM nibble (we set 1 = white for the bar's cells) +// The score bar only uses 00 and 11, so the cell palette is effectively +// 2 colors: black and white. + +#include + +// Player 1 (Hare) score, 0..5. +extern byte score_p1; + +// Player 2 (Scoot) score, 0..5. +extern byte score_p2; + +// score_init() — zero both scores. Called once at startup. +// (The variables already default to 0 in BSS; this is for symmetry with +// the future per-state initialization in Phase 4.) +void score_init(void); + +// score_render() — draw the score bar into the top 8 rows of the active +// multicolor bitmap at $E000. Overwrites the top row of the current +// screen's artwork; the rest of the screen is untouched. +// +// Call this after show_screen() to overlay the score bar on top of the +// screen art. In Phase 4 it will be called on entry to each state. +void score_render(void); + +#pragma compile("score.c") + +#endif