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.
This commit is contained in:
ballz
2026-07-17 01:35:40 +02:00
parent 4d697ad8f1
commit 9f086c3a4f
3 changed files with 278 additions and 3 deletions
+9 -3
View File
@@ -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))
+207
View File
@@ -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
}
+62
View File
@@ -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 <c64/types.h>
// 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