Skip to content
Game 1 Unit 11 of 20 1 hr learning time

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.

55% of Gloaming

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.

Step 1: a HUD strip and a pip that warms per lamp lit
+47-11
11 ; Gloaming — Unit 11: The Tally
22 ; 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.
44
55 org 32768
66
77 COBBLE equ %00000001 ; PAPER black, INK blue — floor
88 WALL equ %00001111 ; PAPER blue, INK white — solid
99 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
1111 LAMP_LIT equ %01000110 ; BRIGHT, PAPER black, INK yellow — a lit lamp
1212 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
1318
1419 START_COL equ 15
1520 START_ROW equ 11
...
1924 KEYS_A equ $FDFE
2025
2126 ; ============================================================================
22-; SETUP — runs once.
27+; SETUP — runs once. The frame now starts at row 1, leaving row 0 for the HUD.
2328 ; ============================================================================
2429 start:
2530 ld a, 0 ; border black
2631 out ($FE), a
2732
28- ld hl, $5800 ; wash the grid in cobbles
33+ ld hl, $5800 ; wash the whole grid in cobbles
2934 ld de, $5801
3035 ld (hl), COBBLE
3136 ld bc, 767
3237 ldir
3338
34- ld hl, $5800 ; wall frame — top row
39+ ld hl, $5820 ; top wall — ROW 1 now (row 0 is the HUD)
3540 ld b, 32
3641 .top:
3742 ld (hl), WALL
3843 inc hl
3944 djnz .top
4045
41- ld hl, $5AE0 ; bottom row
46+ ld hl, $5AE0 ; bottom wall — row 23
4247 ld b, 32
4348 .bottom:
4449 ld (hl), WALL
4550 inc hl
4651 djnz .bottom
4752
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
5055 .sides:
5156 ld (hl), WALL
5257 push hl
...
5863 add hl, de
5964 djnz .sides
6065
66+ call draw_pips ; the cold tally
6167 call draw_lamps
6268 call save_under
6369 call draw_lamp
6470
6571 ; ============================================================================
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.
6773 ; ============================================================================
6874 im 1
6975 ei
...
122128 ld (lamp_row), a
123129 call save_under
124130
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 ---
126132 ld a, (under_lamp + 8)
127133 cp LAMP_UNLIT
128134 jr nz, .not_lamp
129135 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
131138 .not_lamp:
132139 call draw_lamp
133140 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
134168
135169 ; ----------------------------------------------------------------------------
136170 ; draw_lamps / draw_lantern (Unit 9).
...
284318 tcol:
285319 defb 0
286320 trow:
321+ defb 0
322+lit_count:
287323 defb 0
288324
289325 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:

The lamplighter lights two lamps, and a pip at the top warms from cyan to yellow for each — filling left to right. Re-crossing a lit lamp leaves the bar untouched, because the lighting is idempotent.
The square with a HUD strip along the top; two pips are bright yellow and the rest cyan, matching two lit lamps below and the lamplighter beside them.
Two pips warm, two lamps lit — confirmed by reading lit_count straight out of memory (2). Glance at the top and you know how close you are without counting a thing.

When it's wrong, see why

The tally drifts when the count and the score disagree:

  • Pips never warm. light_pip is 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_BASE is off, or lit_count isn't being used as the index. The first pip is at PIP_BASE, the n-th at PIP_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_LAMPS doesn't match the number of lamps in lamp_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.