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

Nightheld

Win the game. When every lamp is lit, the lamplighter has held back the dark — and the game says so, printing a closing line with the Spectrum's own ROM font, then holding the end state.

60% of Gloaming

The tally can fill — so filling it should mean something. This unit gives Gloaming its ending: light every lamp and the lamplighter has held back the night. The game recognises the moment, prints a closing line, and stops. After this unit you can sit down and win.

It also teaches the last new drawing trick the game needs: printing text, using the Spectrum's own font straight out of ROM.

Where we start

Unit 11's scored game — the tally fills, but reaching the top means nothing yet. We make filling it the win, and give the game an ending to come to rest in.

The win is a single check

We already keep lit_count, and it only ever changes at one moment: when a cold lamp is lit. So that's the only place we need to look. Right after lighting, ask the question:

ld   a, (lit_count)
cp   NUM_LAMPS      ; all of them?
jp   z, win         ; then the game is won

That's the whole condition. A win isn't a special system — it's a state you're already tracking crossing a line you care about. Checking it the instant the count changes means the game ends the frame the last lamp warms.

Printing with the ROM font

The closing line is text, and text is more glyphs — except we don't have to draw the letters ourselves. The Spectrum keeps its entire character set in ROM, at $3C00, eight bytes per character, in code order. The shape for character code c sits at:

$3C00 + c * 8

So printing a letter is the glyph-draw from Unit 2 — eight bytes, INC H down the cell — with the bytes coming from ROM instead of our own data. print_char does one character; draw_message walks a string (ended with $FF), printing each and stepping one column along. The Spectrum draws its own font for us; we copy it where we want it.

Ending, and staying ended

On a win we do three small things: restore_under so the last lamp shows (the lamplighter steps out of the cell he's standing on), draw_message to print the line, and then a loop that just halts forever. The game is over; there's nothing more to do but hold the picture. That final loop is the end state — every game needs somewhere to come to rest.

Milestone — the win

We add the cp NUM_LAMPS / jp z, win check right where a lamp lights, a win routine that reveals the last lamp and prints the line, and the print_char / draw_message pair that blits glyphs from the ROM font. Everything else is the game you already had.

Step 1: a win check, and a closing line from the ROM font
+87-24
11 ; Gloaming — Unit 12: Nightheld
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 11's scored game — winnable in spirit, but with no ending.
3+; step-01 adds the win: all lamps lit prints a closing line from the ROM font.
44
55 org 32768
66
7-COBBLE equ %00000001 ; PAPER black, INK blue — floor
8-WALL equ %00001111 ; PAPER blue, INK white — solid
9-LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure
10-LAMP_UNLIT equ %00000101 ; PAPER black, INK cyan — a cold lamp
11-LAMP_LIT equ %01000110 ; BRIGHT, PAPER black, INK yellow — a lit lamp
7+COBBLE equ %00000001
8+WALL equ %00001111
9+LAMP_ATTR equ %01000111
10+LAMP_UNLIT equ %00000101
11+LAMP_LIT equ %01000110
1212 WALL_BIT equ 3
1313
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
14+PIP_UNLIT equ %00101000
15+PIP_LIT equ %01110000
16+PIP_BASE equ $5800 + 12
1717 NUM_LAMPS equ 8
18+
19+MSG_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the closing line
20+MSG_ROW equ 11
21+MSG_COL equ 7 ; centred for a 17-character line
22+FONT equ $3C00 ; ROM font base; glyph for code c is FONT + c*8
1823
1924 START_COL equ 15
2025 START_ROW equ 11
...
2429 KEYS_A equ $FDFE
2530
2631 ; ============================================================================
27-; SETUP — runs once. The frame now starts at row 1, leaving row 0 for the HUD.
32+; SETUP — runs once.
2833 ; ============================================================================
2934 start:
30- ld a, 0 ; border black
35+ ld a, 0
3136 out ($FE), a
3237
33- ld hl, $5800 ; wash the whole grid in cobbles
38+ ld hl, $5800
3439 ld de, $5801
3540 ld (hl), COBBLE
3641 ld bc, 767
3742 ldir
3843
39- ld hl, $5820 ; top wall — ROW 1 now (row 0 is the HUD)
44+ ld hl, $5820 ; top wall — row 1
4045 ld b, 32
4146 .top:
4247 ld (hl), WALL
...
5055 inc hl
5156 djnz .bottom
5257
53- ld hl, $5820 ; left and right columns, rows 1..23
58+ ld hl, $5820 ; sides, rows 1..23
5459 ld b, 23
5560 .sides:
5661 ld (hl), WALL
...
6368 add hl, de
6469 djnz .sides
6570
66- call draw_pips ; the cold tally
71+ call draw_pips
6772 call draw_lamps
6873 call save_under
6974 call draw_lamp
7075
7176 ; ============================================================================
72-; THE HEARTBEAT — move, light lamps, and warm a pip for each new one.
77+; THE HEARTBEAT — move, light, tally, and check for the win.
7378 ; ============================================================================
7479 im 1
7580 ei
...
128133 ld (lamp_row), a
129134 call save_under
130135
131- ; --- light it, and warm a pip if this lamp was cold ---
132136 ld a, (under_lamp + 8)
133137 cp LAMP_UNLIT
134138 jr nz, .not_lamp
135139 ld a, LAMP_LIT
136- ld (under_lamp + 8), a ; lamp will restore lit
137- call light_pip ; warm the next pip on the tally
140+ ld (under_lamp + 8), a
141+ call light_pip
138142 .not_lamp:
139143 call draw_lamp
144+
145+ ld a, (lit_count) ; all lamps lit?
146+ cp NUM_LAMPS
147+ jp z, win
140148 jr game_loop
141149
142150 ; ----------------------------------------------------------------------------
143-; light_pip — warm the pip at index lit_count, then bump lit_count.
151+; win — reveal the last lamp, print the closing line, hold the end state.
152+; ----------------------------------------------------------------------------
153+win:
154+ call restore_under ; the lamplighter steps aside; last lamp shows
155+ call draw_message
156+.hold:
157+ halt
158+ jr .hold
159+
160+; ----------------------------------------------------------------------------
161+; draw_message — print msg_text from (MSG_ROW, MSG_COL), ended by $FF.
162+; ----------------------------------------------------------------------------
163+draw_message:
164+ ld hl, msg_text
165+ ld c, MSG_COL
166+.dm:
167+ ld a, (hl)
168+ cp $FF
169+ ret z
170+ push hl
171+ ld b, MSG_ROW
172+ call print_char ; A=char, B=row, C=col; preserves C
173+ pop hl
174+ inc hl
175+ inc c ; next column
176+ jr .dm
177+
178+; ----------------------------------------------------------------------------
179+; print_char — A=char code, B=row, C=col. Copy the ROM-font glyph into the cell.
180+; ----------------------------------------------------------------------------
181+print_char:
182+ ld l, a ; HL = code * 8
183+ ld h, 0
184+ add hl, hl
185+ add hl, hl
186+ add hl, hl
187+ ld de, FONT
188+ add hl, de
189+ ex de, hl ; DE = glyph address in ROM
190+ push de
191+ call attr_addr_cr ; HL = attribute of (B,C); BC preserved
192+ ld (hl), MSG_ATTR
193+ call scr_addr_cr ; HL = screen of (B,C)
194+ pop de ; DE = glyph address
195+ ld b, 8
196+.pc:
197+ ld a, (de)
198+ ld (hl), a
199+ inc de
200+ inc h
201+ djnz .pc
202+ ret
203+
204+; ----------------------------------------------------------------------------
205+; light_pip / draw_pips (Unit 11).
144206 ; ----------------------------------------------------------------------------
145207 light_pip:
146208 ld a, (lit_count)
...
153215 ld (hl), PIP_LIT
154216 ret
155217
156-; ----------------------------------------------------------------------------
157-; draw_pips — lay down NUM_LAMPS cold pips in a row at the start of row 0.
158-; ----------------------------------------------------------------------------
159218 draw_pips:
160219 ld hl, PIP_BASE
161220 ld b, NUM_LAMPS
...
298357 ret
299358
300359 ; ----------------------------------------------------------------------------
301-; Level data, state, buffer, and shapes.
360+; Level data, state, buffer, shapes, and the closing line.
302361 ; ----------------------------------------------------------------------------
303362 lamp_data:
304363 defb 4, 3
...
344403 defb %01111110
345404 defb %01111110
346405 defb %00111100
406+
407+msg_text:
408+ defb "THE NIGHT IS HELD"
409+ defb $FF
347410
348411 end start
349412
The complete program
; Gloaming — Unit 12: Nightheld
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 adds the win: all lamps lit prints a closing line from the ROM font.

            org     32768

COBBLE      equ     %00000001
WALL        equ     %00001111
LAMP_ATTR   equ     %01000111
LAMP_UNLIT  equ     %00000101
LAMP_LIT    equ     %01000110
WALL_BIT    equ     3

PIP_UNLIT   equ     %00101000
PIP_LIT     equ     %01110000
PIP_BASE    equ     $5800 + 12
NUM_LAMPS   equ     8

MSG_ATTR    equ     %01000111       ; BRIGHT, PAPER black, INK white — the closing line
MSG_ROW     equ     11
MSG_COL     equ     7               ; centred for a 17-character line
FONT        equ     $3C00           ; ROM font base; glyph for code c is FONT + c*8

START_COL   equ     15
START_ROW   equ     11

KEYS_OP     equ     $DFFE
KEYS_Q      equ     $FBFE
KEYS_A      equ     $FDFE

; ============================================================================
; SETUP — runs once.
; ============================================================================
start:
            ld      a, 0
            out     ($FE), a

            ld      hl, $5800
            ld      de, $5801
            ld      (hl), COBBLE
            ld      bc, 767
            ldir

            ld      hl, $5820       ; top wall — row 1
            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       ; sides, 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
            call    draw_lamps
            call    save_under
            call    draw_lamp

; ============================================================================
; THE HEARTBEAT — move, light, tally, and check for the win.
; ============================================================================
            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

            ld      a, (under_lamp + 8)
            cp      LAMP_UNLIT
            jr      nz, .not_lamp
            ld      a, LAMP_LIT
            ld      (under_lamp + 8), a
            call    light_pip
.not_lamp:
            call    draw_lamp

            ld      a, (lit_count)  ; all lamps lit?
            cp      NUM_LAMPS
            jp      z, win
            jr      game_loop

; ----------------------------------------------------------------------------
; win — reveal the last lamp, print the closing line, hold the end state.
; ----------------------------------------------------------------------------
win:
            call    restore_under   ; the lamplighter steps aside; last lamp shows
            call    draw_message
.hold:
            halt
            jr      .hold

; ----------------------------------------------------------------------------
; draw_message — print msg_text from (MSG_ROW, MSG_COL), ended by $FF.
; ----------------------------------------------------------------------------
draw_message:
            ld      hl, msg_text
            ld      c, MSG_COL
.dm:
            ld      a, (hl)
            cp      $FF
            ret     z
            push    hl
            ld      b, MSG_ROW
            call    print_char      ; A=char, B=row, C=col; preserves C
            pop     hl
            inc     hl
            inc     c               ; next column
            jr      .dm

; ----------------------------------------------------------------------------
; print_char — A=char code, B=row, C=col. Copy the ROM-font glyph into the cell.
; ----------------------------------------------------------------------------
print_char:
            ld      l, a            ; HL = code * 8
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, FONT
            add     hl, de
            ex      de, hl          ; DE = glyph address in ROM
            push    de
            call    attr_addr_cr    ; HL = attribute of (B,C); BC preserved
            ld      (hl), MSG_ATTR
            call    scr_addr_cr     ; HL = screen of (B,C)
            pop     de              ; DE = glyph address
            ld      b, 8
.pc:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .pc
            ret

; ----------------------------------------------------------------------------
; light_pip / draw_pips  (Unit 11).
; ----------------------------------------------------------------------------
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:
            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, shapes, and the closing line.
; ----------------------------------------------------------------------------
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

msg_text:
            defb    "THE NIGHT IS HELD"
            defb    $FF

            end     start

Light the eighth lamp and the square freezes into its ending — every lamp gold, the tally full, the line written across the middle:

All eight lamps lit gold around the square, the tally fully warm at the top, and the words THE NIGHT IS HELD printed across the middle.
The win, reached by playing — all eight lamps lit (lit_count = 8, read from memory), the tally full, and THE NIGHT IS HELD printed in the Spectrum's own ROM font. The screen the whole game was building toward.

When it's wrong, see why

The win and the text fail in their own ways:

  • The line never appears. The win check isn't reached, or NUM_LAMPS doesn't match the lamps placed, so lit_count never gets there. Confirm the cp NUM_LAMPS / jp z, win sits right after a lamp is lit.
  • The text is garbled. The font address is wrong. Each glyph is at $3C00 + code * 8 — check the code * 8 (three ADD HL,HL) and the $3C00 base.
  • The text prints in the wrong place or off the edge. MSG_ROW / MSG_COL are off, or the line is too long to fit from that column. Re-centre it.
  • He keeps moving after the win. The win routine returned to the game loop. It must end in its own halt loop and never fall back through.

Before and after

You started with a game that couldn't end and finished with one you can win — and the win is a cp and a jp, slipped in where the only meaningful number changes. The ending is the glyph-draw you've had since Unit 2, fed from the ROM font; the rest-state is one halt loop. A loop of mechanics has become a game with a beginning you drop into, a middle you play, and an end you earn.

Try this: your own closing line

Change msg_text to whatever you like — "WELL DONE", "DAWN COMES", your name. Keep the $FF on the end, and adjust MSG_COL to re-centre it: a line of N characters starts at column (32 - N) / 2. Different words, same machinery.

Try this: a flourish on the win

A bare freeze is a little flat. Before printing the line, add a beat of celebration: flash the border a few times (out ($FE), a with changing colours in a short loop), or sweep all the lamps to bright white. A win deserves a moment of spectacle — and it's a few instructions.

Try this: lose your way back

Notice you can't restart without reloading. That's deliberate — restarting is its own unit (Unit 19, "Again"). For now, try adding a single key check in the end loop that jumps back to start: the crudest possible "play again". It'll work, and seeing why it's crude — the state isn't reset — is exactly the problem that later unit solves properly.

What you've learnt

  • A win condition is a state check at the moment the state changes — not a separate system.
  • The Spectrum's font lives in ROM at $3C00, eight bytes per character; print text by blitting glyphs exactly like sprites.
  • A game needs an end state — a small loop it rests in once it's over.
  • This is the line between a set of mechanics and a game you can finish.

What's next

Gloaming is winnable — but it can't yet be lost, and a game with no jeopardy has no tension. Phase D brings the threat. In Unit 13, "The Draught", a cold wisp drifts through the square: a second moving sprite, driven by the same cell-sprite engine the lamplighter uses — proof that the technique you built pays for more than one character. The dark starts to push back.