The Tally
Add a score you read at a glance. Dedicate row 0 as a HUD strip and lay a row of pips — one per lamp — that warm from cyan to yellow as each lights. Coloured cells, not drawn digits: the honest 'before' of a digit HUD.
Lamps light, but the game keeps no score and tells you nothing. A game should answer "how am I doing?" at a glance. So we add a tally: a row of pips along the top, one for each lamp, cold at the start and warming as you light them. Fill the row and you've filled the square.
We build it the plainest way that works — and that way turns out to be the first thing we ever drew, back in Unit 1: a coloured cell.
Where we start
Unit 10's game — lamps you light, with no record of it. We give it a memory and a face: a count, and a bar that shows it.
Pips are coloured cells
A pip doesn't need a picture. It's a single cell showing a colour — cyan for a lamp not yet lit, bright yellow for one that is. No glyph, no pixels: pure attribute, like the cobbles and walls in Unit 1. The whole tally is eight coloured blocks in a row.
PIP_UNLIT equ %00101000 ; PAPER cyan — cold
PIP_LIT equ %01110000 ; BRIGHT PAPER yellow — warm
Drawing actual numbers — a "3 / 8" readout — is a genuine technique: you need a font and a way to render digits, and a bigger game later builds exactly that. This coloured-pip bar is the honest, smaller version, and for a game with a handful of lamps it reads instantly, with no arithmetic.
A place to put it
The pips need to live somewhere the lamplighter can't trample. So we give the game a HUD strip: the top wall moves down from row 0 to row 1, and row 0 becomes a black band above the playfield where the pips sit. Because the wall is now at row 1, he's stopped at row 2 — he can never reach the HUD. The playfield loses one row; the score gains a home. (Every later game in the course carves out a HUD the same way.)
Counting, and warming the next pip
We keep one byte, lit_count. The lighting code from Unit 10 already knows the exact moment a cold lamp becomes lit — so that's where we bump the count and warm a pip. light_pip warms the pip at the current count, then increments it, so pips fill left to right in the order you light lamps. And because Unit 10's lighting is idempotent, re-crossing a lit lamp does nothing — the count can never run ahead of reality.
Milestone — the tally
We carve the HUD strip (top wall to row 1), lay eight cold pips along row 0, keep a lit_count, and call light_pip in the one place a cold lamp turns lit. Everything else — movement, collision, lighting — is unchanged.
| 1 | 1 | ; Gloaming — Unit 11: The Tally | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 is Unit 10's lit lamps — lighting works, but nothing keeps score. | |
| 3 | + | ; step-01 adds a HUD strip and a row of pips that warm as lamps light. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | COBBLE equ %00000001 ; PAPER black, INK blue — floor | |
| 8 | 8 | WALL equ %00001111 ; PAPER blue, INK white — solid | |
| 9 | 9 | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure | |
| 10 | - | LAMP_UNLIT equ %00000101 ; PAPER black, INK cyan — a cold, unlit lamp | |
| 10 | + | LAMP_UNLIT equ %00000101 ; PAPER black, INK cyan — a cold lamp | |
| 11 | 11 | LAMP_LIT equ %01000110 ; BRIGHT, PAPER black, INK yellow — a lit lamp | |
| 12 | 12 | WALL_BIT equ 3 | |
| 13 | + | | |
| 14 | + | PIP_UNLIT equ %00101000 ; PAPER cyan — a cold pip | |
| 15 | + | PIP_LIT equ %01110000 ; BRIGHT, PAPER yellow — a warm pip | |
| 16 | + | PIP_BASE equ $5800 + 12 ; row 0, column 12 — first pip | |
| 17 | + | NUM_LAMPS equ 8 | |
| 13 | 18 | | |
| 14 | 19 | START_COL equ 15 | |
| 15 | 20 | START_ROW equ 11 | |
| ... | |||
| 19 | 24 | KEYS_A equ $FDFE | |
| 20 | 25 | | |
| 21 | 26 | ; ============================================================================ | |
| 22 | - | ; SETUP — runs once. | |
| 27 | + | ; SETUP — runs once. The frame now starts at row 1, leaving row 0 for the HUD. | |
| 23 | 28 | ; ============================================================================ | |
| 24 | 29 | start: | |
| 25 | 30 | ld a, 0 ; border black | |
| 26 | 31 | out ($FE), a | |
| 27 | 32 | | |
| 28 | - | ld hl, $5800 ; wash the grid in cobbles | |
| 33 | + | ld hl, $5800 ; wash the whole grid in cobbles | |
| 29 | 34 | ld de, $5801 | |
| 30 | 35 | ld (hl), COBBLE | |
| 31 | 36 | ld bc, 767 | |
| 32 | 37 | ldir | |
| 33 | 38 | | |
| 34 | - | ld hl, $5800 ; wall frame — top row | |
| 39 | + | ld hl, $5820 ; top wall — ROW 1 now (row 0 is the HUD) | |
| 35 | 40 | ld b, 32 | |
| 36 | 41 | .top: | |
| 37 | 42 | ld (hl), WALL | |
| 38 | 43 | inc hl | |
| 39 | 44 | djnz .top | |
| 40 | 45 | | |
| 41 | - | ld hl, $5AE0 ; bottom row | |
| 46 | + | ld hl, $5AE0 ; bottom wall — row 23 | |
| 42 | 47 | ld b, 32 | |
| 43 | 48 | .bottom: | |
| 44 | 49 | ld (hl), WALL | |
| 45 | 50 | inc hl | |
| 46 | 51 | djnz .bottom | |
| 47 | 52 | | |
| 48 | - | ld hl, $5800 ; left and right columns | |
| 49 | - | ld b, 24 | |
| 53 | + | ld hl, $5820 ; left and right columns, rows 1..23 | |
| 54 | + | ld b, 23 | |
| 50 | 55 | .sides: | |
| 51 | 56 | ld (hl), WALL | |
| 52 | 57 | push hl | |
| ... | |||
| 58 | 63 | add hl, de | |
| 59 | 64 | djnz .sides | |
| 60 | 65 | | |
| 66 | + | call draw_pips ; the cold tally | |
| 61 | 67 | call draw_lamps | |
| 62 | 68 | call save_under | |
| 63 | 69 | call draw_lamp | |
| 64 | 70 | | |
| 65 | 71 | ; ============================================================================ | |
| 66 | - | ; THE HEARTBEAT — move, and light any unlit lamp stepped onto. | |
| 72 | + | ; THE HEARTBEAT — move, light lamps, and warm a pip for each new one. | |
| 67 | 73 | ; ============================================================================ | |
| 68 | 74 | im 1 | |
| 69 | 75 | ei | |
| ... | |||
| 122 | 128 | ld (lamp_row), a | |
| 123 | 129 | call save_under | |
| 124 | 130 | | |
| 125 | - | ; --- light it: if the saved cell is an unlit lamp, mark it lit --- | |
| 131 | + | ; --- light it, and warm a pip if this lamp was cold --- | |
| 126 | 132 | ld a, (under_lamp + 8) | |
| 127 | 133 | cp LAMP_UNLIT | |
| 128 | 134 | jr nz, .not_lamp | |
| 129 | 135 | ld a, LAMP_LIT | |
| 130 | - | ld (under_lamp + 8), a ; restore will paint it lit when he leaves | |
| 136 | + | ld (under_lamp + 8), a ; lamp will restore lit | |
| 137 | + | call light_pip ; warm the next pip on the tally | |
| 131 | 138 | .not_lamp: | |
| 132 | 139 | call draw_lamp | |
| 133 | 140 | jr game_loop | |
| 141 | + | | |
| 142 | + | ; ---------------------------------------------------------------------------- | |
| 143 | + | ; light_pip — warm the pip at index lit_count, then bump lit_count. | |
| 144 | + | ; ---------------------------------------------------------------------------- | |
| 145 | + | light_pip: | |
| 146 | + | ld a, (lit_count) | |
| 147 | + | ld e, a | |
| 148 | + | ld d, 0 | |
| 149 | + | inc a | |
| 150 | + | ld (lit_count), a | |
| 151 | + | ld hl, PIP_BASE | |
| 152 | + | add hl, de | |
| 153 | + | ld (hl), PIP_LIT | |
| 154 | + | ret | |
| 155 | + | | |
| 156 | + | ; ---------------------------------------------------------------------------- | |
| 157 | + | ; draw_pips — lay down NUM_LAMPS cold pips in a row at the start of row 0. | |
| 158 | + | ; ---------------------------------------------------------------------------- | |
| 159 | + | draw_pips: | |
| 160 | + | ld hl, PIP_BASE | |
| 161 | + | ld b, NUM_LAMPS | |
| 162 | + | ld a, PIP_UNLIT | |
| 163 | + | .dp: | |
| 164 | + | ld (hl), a | |
| 165 | + | inc hl | |
| 166 | + | djnz .dp | |
| 167 | + | ret | |
| 134 | 168 | | |
| 135 | 169 | ; ---------------------------------------------------------------------------- | |
| 136 | 170 | ; draw_lamps / draw_lantern (Unit 9). | |
| ... | |||
| 284 | 318 | tcol: | |
| 285 | 319 | defb 0 | |
| 286 | 320 | trow: | |
| 321 | + | defb 0 | |
| 322 | + | lit_count: | |
| 287 | 323 | defb 0 | |
| 288 | 324 | | |
| 289 | 325 | under_lamp: |
The complete program
; Gloaming — Unit 11: The Tally
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 adds a HUD strip and a row of pips that warm as lamps light.
org 32768
COBBLE equ %00000001 ; PAPER black, INK blue — floor
WALL equ %00001111 ; PAPER blue, INK white — solid
LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure
LAMP_UNLIT equ %00000101 ; PAPER black, INK cyan — a cold lamp
LAMP_LIT equ %01000110 ; BRIGHT, PAPER black, INK yellow — a lit lamp
WALL_BIT equ 3
PIP_UNLIT equ %00101000 ; PAPER cyan — a cold pip
PIP_LIT equ %01110000 ; BRIGHT, PAPER yellow — a warm pip
PIP_BASE equ $5800 + 12 ; row 0, column 12 — first pip
NUM_LAMPS equ 8
START_COL equ 15
START_ROW equ 11
KEYS_OP equ $DFFE
KEYS_Q equ $FBFE
KEYS_A equ $FDFE
; ============================================================================
; SETUP — runs once. The frame now starts at row 1, leaving row 0 for the HUD.
; ============================================================================
start:
ld a, 0 ; border black
out ($FE), a
ld hl, $5800 ; wash the whole grid in cobbles
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
ld hl, $5820 ; top wall — ROW 1 now (row 0 is the HUD)
ld b, 32
.top:
ld (hl), WALL
inc hl
djnz .top
ld hl, $5AE0 ; bottom wall — row 23
ld b, 32
.bottom:
ld (hl), WALL
inc hl
djnz .bottom
ld hl, $5820 ; left and right columns, rows 1..23
ld b, 23
.sides:
ld (hl), WALL
push hl
ld de, 31
add hl, de
ld (hl), WALL
pop hl
ld de, 32
add hl, de
djnz .sides
call draw_pips ; the cold tally
call draw_lamps
call save_under
call draw_lamp
; ============================================================================
; THE HEARTBEAT — move, light lamps, and warm a pip for each new one.
; ============================================================================
im 1
ei
game_loop:
halt
ld a, (lamp_col)
ld (tcol), a
ld a, (lamp_row)
ld (trow), a
ld bc, KEYS_OP
in a, (c)
bit 1, a
jr z, .left
bit 0, a
jr z, .right
ld bc, KEYS_Q
in a, (c)
bit 0, a
jr z, .up
ld bc, KEYS_A
in a, (c)
bit 0, a
jr z, .down
jr game_loop
.left:
ld hl, tcol
dec (hl)
jr .try
.right:
ld hl, tcol
inc (hl)
jr .try
.up:
ld hl, trow
dec (hl)
jr .try
.down:
ld hl, trow
inc (hl)
.try:
ld a, (trow)
ld b, a
ld a, (tcol)
ld c, a
call wall_at
jr nz, game_loop
call restore_under
ld a, (tcol)
ld (lamp_col), a
ld a, (trow)
ld (lamp_row), a
call save_under
; --- light it, and warm a pip if this lamp was cold ---
ld a, (under_lamp + 8)
cp LAMP_UNLIT
jr nz, .not_lamp
ld a, LAMP_LIT
ld (under_lamp + 8), a ; lamp will restore lit
call light_pip ; warm the next pip on the tally
.not_lamp:
call draw_lamp
jr game_loop
; ----------------------------------------------------------------------------
; light_pip — warm the pip at index lit_count, then bump lit_count.
; ----------------------------------------------------------------------------
light_pip:
ld a, (lit_count)
ld e, a
ld d, 0
inc a
ld (lit_count), a
ld hl, PIP_BASE
add hl, de
ld (hl), PIP_LIT
ret
; ----------------------------------------------------------------------------
; draw_pips — lay down NUM_LAMPS cold pips in a row at the start of row 0.
; ----------------------------------------------------------------------------
draw_pips:
ld hl, PIP_BASE
ld b, NUM_LAMPS
ld a, PIP_UNLIT
.dp:
ld (hl), a
inc hl
djnz .dp
ret
; ----------------------------------------------------------------------------
; draw_lamps / draw_lantern (Unit 9).
; ----------------------------------------------------------------------------
draw_lamps:
ld hl, lamp_data
.next:
ld a, (hl)
cp $FF
ret z
ld c, a
inc hl
ld b, (hl)
inc hl
push hl
call draw_lantern
pop hl
jr .next
draw_lantern:
call attr_addr_cr
ld (hl), LAMP_UNLIT
call scr_addr_cr
ld de, lantern
ld b, 8
.dlt:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dlt
ret
; ----------------------------------------------------------------------------
; scr_addr_cr / attr_addr_cr / wall_at / pos_bc (Unit 8).
; ----------------------------------------------------------------------------
scr_addr_cr:
ld a, b
and %00011000
or %01000000
ld h, a
ld a, b
and %00000111
rrca
rrca
rrca
or c
ld l, a
ret
attr_addr_cr:
ld a, b
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld de, $5800
add hl, de
ld a, c
ld e, a
ld d, 0
add hl, de
ret
wall_at:
call attr_addr_cr
bit WALL_BIT, (hl)
ret
pos_bc:
ld a, (lamp_row)
ld b, a
ld a, (lamp_col)
ld c, a
ret
; ----------------------------------------------------------------------------
; save_under / restore_under / draw_lamp (Unit 8).
; ----------------------------------------------------------------------------
save_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.su:
ld a, (hl)
ld (de), a
inc de
inc h
djnz .su
call pos_bc
call attr_addr_cr
ld a, (hl)
ld (under_lamp + 8), a
ret
restore_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.ru:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .ru
call pos_bc
call attr_addr_cr
ld a, (under_lamp + 8)
ld (hl), a
ret
draw_lamp:
call pos_bc
call attr_addr_cr
ld (hl), LAMP_ATTR
call pos_bc
call scr_addr_cr
ld de, lamplighter
ld b, 8
.dl:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dl
ret
; ----------------------------------------------------------------------------
; Level data, state, buffer, and shapes.
; ----------------------------------------------------------------------------
lamp_data:
defb 4, 3
defb 27, 3
defb 9, 7
defb 22, 7
defb 6, 15
defb 25, 15
defb 13, 20
defb 18, 20
defb $FF
lamp_col:
defb START_COL
lamp_row:
defb START_ROW
tcol:
defb 0
trow:
defb 0
lit_count:
defb 0
under_lamp:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
lantern:
defb %00011000
defb %00100100
defb %01111110
defb %01111110
defb %01011010
defb %01111110
defb %01111110
defb %00111100
end start
Each cold lamp lit warms one more pip — light two lamps and two pips glow, the tally and the lamps telling the same story two ways:
When it's wrong, see why
The tally drifts when the count and the score disagree:
- Pips never warm.
light_pipis in the wrong place — it must be called only inside the "this lamp was cold" branch, right where you set the lit attribute. - The wrong pip warms, or it spills past the bar.
PIP_BASEis off, orlit_countisn't being used as the index. The first pip is atPIP_BASE, the n-th atPIP_BASE + n. - The lamplighter walks into the HUD. The top wall isn't at row 1, so row 0 is reachable. Draw the top wall at
$5820, not$5800. - The bar can't fill, or fills too early.
NUM_LAMPSdoesn't match the number of lamps inlamp_data. Keep them in step.
Before and after
You started with a game that scored nothing and finished with one you read at a glance: a strip of cells outside the playfield, one warming per lamp lit. The HUD is more cells; the score is a byte; the readout is a colour. And because the count rides on the same idempotent lighting from Unit 10, it can't lie — cross a lit lamp a hundred times and the tally holds. Fill the bar and you've filled the square, which is exactly the win we build next.
Try this: change the lamp count
Add or remove lamps in lamp_data, and set NUM_LAMPS to match. The pip bar grows or shrinks with it — the tally is sized by one constant. (Try it with the count wrong on purpose: too few pips and the ninth lamp warms nothing; too many and the bar can never fill. A score and the thing it scores must agree.)
Try this: move the HUD
Put the pips along the bottom instead. They're cells: change PIP_BASE to a row-23 address ($5800 + 23*32 + 12), and they'll sit in the bottom band. Or recolour them — make unlit pips dim blue and lit ones flashing — to taste. The readout's look is yours.
Try this: count down instead
Flip the meaning: start all eight pips lit and turn one cold for each lamp you light — "lamps remaining". Same mechanism, opposite reading. Which feels better — filling up, or running down? That's a real design choice, and you can try both in a minute.
What you've learnt
- A HUD is just more cells — dedicate a strip outside the playfield and the sprite can't disturb it.
- A coloured-cell readout (pure attribute) is an instantly-readable score — no digits required.
- Track game state in a byte (
lit_count) and reflect it in the HUD as it changes. - Idempotent lighting keeps the count honest — the score can't drift ahead of the game.
What's next
The tally can fill — so now filling it should mean something. In Unit 12 we add the win: when every pip is warm, the lamplighter has held back the dark, and the game says so with a closing line. A goal, reached, and acknowledged — the moment a loop of mechanics becomes a game you can finish.