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

Lives, and the Fall of Night

Phase D's finale. The draught and the lamplighter can now collide — each hit costs a life from a row of pips — and when the lives run out, night falls: the screen goes dark and the game ends. Gloaming can be lost as well as won.

75% of Gloaming

The draught can undo your work, but it still can't touch you. This unit lets it — and gives the game its stakes. The two can collide, each collision costs a life, and when the lives run out, night falls: the square goes dark and the game ends. With this, Gloaming becomes a real game, one you can lose as well as win — and that closes Phase D.

Where we start

Unit 14's contest — the draught snuffs lamps, but passes through the lamplighter harmlessly. We make the two of them dangerous to each other.

Two sprites, one cell: collision

A collision is the question are the lamplighter and the draught trying to stand on the same cell? — a comparison of their columns and rows. But there's a trap. If we let them overlap, their save/restore buffers tangle: each sprite assumes it's the only thing on its cell, and two sharing one cell corrupts what each thinks is "underneath".

So we don't let them overlap. Instead, a move onto the other's cell is treated like a move into a wall — refused — and counted as a hit. We check it both ways: when the lamplighter steps toward the draught, and when the draught drifts toward the lamplighter.

; about to move onto the other's cell?
ld   a, (tcol)
ld   hl, draught_col
cp   (hl)
jr   nz, .clear
ld   a, (trow)
ld   hl, draught_row
cp   (hl)
jr   nz, .clear
call lose_life      ; same cell — collision, don't move

They never share a cell, so the engine stays clean. (Drawing two sprites that genuinely overlap — compositing one over the other — is a real technique a later game builds. Here we sidestep it, and the game's none the poorer.)

Lives, as pips again

Lives are shown the way everything is shown in this game: as coloured cells. Three red pips sit at the right of the HUD, and lose_life removes one each hit — the lamp tally's machinery, run in reverse. While lives remain, a hit sends the lamplighter back to the start, clear of the draught, and play continues. The reset is also what keeps the two sprites apart after a collision, with no overlap to clean up.

The fall of night

When the last life goes, lose_life jumps to lose, and night takes the square. It's the win's mirror image: where the win prints its line over the lit board, the loss first washes every cell to black — one LDIR, the whole attribute map gone dark — then prints NIGHT FALLS into the void, and holds. Two end states now, one bright and one dark, each a place the game comes to rest.

Milestone — the lose state

We add the same-cell check to both player_step and draught_step, a lose_life that drains a red pip and resets the lamplighter (or jumps to lose at zero), and a lose routine that blacks the board and prints NIGHT FALLS.

Step 1: collision, lives, and the fall of night
+131-34
11 ; Gloaming — Unit 15: Lives, and the Fall of Night
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 14's contest — the draught snuffs lamps, but can't touch you.
3+; step-01 adds collision, lives, and the lose state — night falls at zero lives.
44
55 org 32768
66
...
1818 PIP_LIT equ %01110000
1919 PIP_BASE equ $5800 + 12
2020 NUM_LAMPS equ 8
21+
22+LIVES equ 3
23+LIFE_PIP equ %01010000 ; BRIGHT, PAPER red — a life
24+LIFE_BASE equ $5800 + 28 ; row 0, columns 28-30
2125
2226 MSG_ATTR equ %01000111
2327 MSG_ROW equ 11
24-MSG_COL equ 7
28+WIN_COL equ 7
29+LOSE_COL equ 10
2530 FONT equ $3C00
2631
2732 START_COL equ 15
2833 START_ROW equ 11
29-DRAUGHT_COL0 equ 18 ; its diagonal now crosses the lamp at (22,7)
34+DRAUGHT_COL0 equ 18
3035 DRAUGHT_ROW0 equ 3
3136
3237 KEYS_OP equ $DFFE
...
7479 djnz .sides
7580
7681 call draw_pips
82+ call draw_lives
7783 call draw_lamps
7884 call save_under
7985 call draw_lamp
...
96102 jr game_loop
97103
98104 ; ----------------------------------------------------------------------------
99-; player_step — move the lamplighter, light lamps (Unit 13).
105+; player_step — move the lamplighter; a step onto the draught costs a life.
100106 ; ----------------------------------------------------------------------------
101107 player_step:
102108 ld a, (lamp_col)
...
141147 ld a, (tcol)
142148 ld c, a
143149 call wall_at
144- ret nz
150+ ret nz ; wall blocks
151+
152+ ; would the step land on the draught? then it's a collision
153+ ld a, (tcol)
154+ ld hl, draught_col
155+ cp (hl)
156+ jr nz, .pcommit
157+ ld a, (trow)
158+ ld hl, draught_row
159+ cp (hl)
160+ jr nz, .pcommit
161+ call lose_life
162+ ret
145163
164+.pcommit:
146165 call restore_under
147166 ld a, (tcol)
148167 ld (lamp_col), a
...
160179 ret
161180
162181 ; ----------------------------------------------------------------------------
163-; draught_step — drift, bounce, and SNUFF any lit lamp stepped onto.
182+; draught_step — drift, bounce, snuff; a step onto the lamplighter costs a life.
164183 ; ----------------------------------------------------------------------------
165184 draught_step:
166185 ld a, (draught_timer)
...
196215 neg
197216 ld (draught_dy), a
198217 .vok:
199- call restore_draught
218+ ; work out the target cell
200219 ld a, (draught_col)
201220 ld b, a
202221 ld a, (draught_dx)
203222 add a, b
204- ld (draught_col), a
223+ ld (dtcol), a
205224 ld a, (draught_row)
206225 ld b, a
207226 ld a, (draught_dy)
208227 add a, b
228+ ld (dtrow), a
229+
230+ ; would it land on the lamplighter? then it's a collision
231+ ld a, (dtcol)
232+ ld hl, lamp_col
233+ cp (hl)
234+ jr nz, .dmove
235+ ld a, (dtrow)
236+ ld hl, lamp_row
237+ cp (hl)
238+ jr nz, .dmove
239+ call lose_life
240+ ret ; don't move onto him
241+
242+.dmove:
243+ call restore_draught
244+ ld a, (dtcol)
245+ ld (draught_col), a
246+ ld a, (dtrow)
209247 ld (draught_row), a
210248 call save_draught
211-
212- ; --- snuff it: if the saved cell is a lit lamp, cool it ---
213249 ld a, (under_draught + 8)
214250 cp LAMP_LIT
215251 jr nz, .nosnuff
216252 ld a, LAMP_UNLIT
217- ld (under_draught + 8), a ; restored cold when the draught leaves
253+ ld (under_draught + 8), a
218254 call unlight_pip
219255 .nosnuff:
220256 call draw_draught
221257 ret
222258
223259 ; ----------------------------------------------------------------------------
224-; unlight_pip — drop the tally by one and cool the top pip (mirror of light_pip).
260+; lose_life — drop a life pip; reset the lamplighter, or fall to night.
225261 ; ----------------------------------------------------------------------------
226-unlight_pip:
227- ld a, (lit_count)
262+lose_life:
263+ ld a, (lives)
228264 dec a
229- ld (lit_count), a ; new, lower count
230- ld e, a ; index of the pip that was on top
265+ ld (lives), a
266+ ld e, a ; index of the life pip to remove
231267 ld d, 0
232- ld hl, PIP_BASE
268+ ld hl, LIFE_BASE
233269 add hl, de
234- ld (hl), PIP_UNLIT
270+ ld (hl), COBBLE ; the pip goes dark
271+ ld a, (lives)
272+ or a
273+ jp z, lose ; out of lives — night falls
274+
275+ ; otherwise send the lamplighter back to the start, clear of danger
276+ call restore_under
277+ ld a, START_COL
278+ ld (lamp_col), a
279+ ld a, START_ROW
280+ ld (lamp_row), a
281+ call save_under
282+ call draw_lamp
235283 ret
236284
237285 ; ----------------------------------------------------------------------------
238-; win / draw_message / print_char (Unit 12).
286+; win / lose — each prints a line and holds.
239287 ; ----------------------------------------------------------------------------
240288 win:
241289 call restore_under
242- call draw_message
243-.hold:
290+ ld hl, win_text
291+ ld b, MSG_ROW
292+ ld c, WIN_COL
293+ call print_string
294+.whold:
244295 halt
245- jr .hold
296+ jr .whold
246297
247-draw_message:
248- ld hl, msg_text
249- ld c, MSG_COL
250-.dm:
298+lose:
299+ ld hl, $5800 ; night falls — wash the square to black
300+ ld de, $5801
301+ ld (hl), %00000000
302+ ld bc, 767
303+ ldir
304+ ld hl, lose_text
305+ ld b, MSG_ROW
306+ ld c, LOSE_COL
307+ call print_string
308+.lhold:
309+ halt
310+ jr .lhold
311+
312+; ----------------------------------------------------------------------------
313+; print_string — HL=string ($FF-terminated), B=row, C=col.
314+; ----------------------------------------------------------------------------
315+print_string:
316+.ps:
251317 ld a, (hl)
252318 cp $FF
253319 ret z
254320 push hl
255- ld b, MSG_ROW
321+ push bc
256322 call print_char
323+ pop bc
257324 pop hl
258325 inc hl
259326 inc c
260- jr .dm
327+ jr .ps
261328
262329 print_char:
263330 ld l, a
...
283350 ret
284351
285352 ; ----------------------------------------------------------------------------
286-; light_pip / draw_pips (Unit 11).
353+; light_pip / unlight_pip / draw_pips / draw_lives.
287354 ; ----------------------------------------------------------------------------
288355 light_pip:
289356 ld a, (lit_count)
...
294361 ld hl, PIP_BASE
295362 add hl, de
296363 ld (hl), PIP_LIT
364+ ret
365+
366+unlight_pip:
367+ ld a, (lit_count)
368+ dec a
369+ ld (lit_count), a
370+ ld e, a
371+ ld d, 0
372+ ld hl, PIP_BASE
373+ add hl, de
374+ ld (hl), PIP_UNLIT
297375 ret
298376
299377 draw_pips:
...
304382 ld (hl), a
305383 inc hl
306384 djnz .dp
385+ ret
386+
387+draw_lives:
388+ ld hl, LIFE_BASE
389+ ld b, LIVES
390+ ld a, LIFE_PIP
391+.dlv:
392+ ld (hl), a
393+ inc hl
394+ djnz .dlv
307395 ret
308396
309397 ; ----------------------------------------------------------------------------
310-; draw_lamps / draw_lantern (Unit 9).
398+; draw_lamps / draw_lantern.
311399 ; ----------------------------------------------------------------------------
312400 draw_lamps:
313401 ld hl, lamp_data
...
339427 ret
340428
341429 ; ----------------------------------------------------------------------------
342-; scr_addr_cr / attr_addr_cr / wall_at (Unit 8).
430+; scr_addr_cr / attr_addr_cr / wall_at.
343431 ; ----------------------------------------------------------------------------
344432 scr_addr_cr:
345433 ld a, b
...
378466 ret
379467
380468 ; ----------------------------------------------------------------------------
381-; The lamplighter's save / restore / draw (Unit 8).
469+; The lamplighter's save / restore / draw.
382470 ; ----------------------------------------------------------------------------
383471 pos_bc:
384472 ld a, (lamp_row)
...
438526 ret
439527
440528 ; ----------------------------------------------------------------------------
441-; The draught's save / restore / draw (Unit 13).
529+; The draught's save / restore / draw.
442530 ; ----------------------------------------------------------------------------
443531 dpos_bc:
444532 ld a, (draught_row)
...
521609 defb 0
522610 lit_count:
523611 defb 0
612+lives:
613+ defb LIVES
524614
525615 draught_col:
526616 defb DRAUGHT_COL0
...
532622 defb 1
533623 draught_timer:
534624 defb DRAUGHT_SPEED
625+dtcol:
626+ defb 0
627+dtrow:
628+ defb 0
535629
536630 under_lamp:
537631 defb 0, 0, 0, 0, 0, 0, 0, 0, 0
...
568662 defb %00111100
569663 defb %00000000
570664
571-msg_text:
665+win_text:
572666 defb "THE NIGHT IS HELD"
667+ defb $FF
668+lose_text:
669+ defb "NIGHT FALLS"
573670 defb $FF
574671
575672 end start
The complete program
; Gloaming — Unit 15: Lives, and the Fall of Night
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 adds collision, lives, and the lose state — night falls at zero lives.

            org     32768

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

DRAUGHT_ATTR  equ   %01000101
DRAUGHT_SPEED equ   8

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

LIVES       equ     3
LIFE_PIP    equ     %01010000       ; BRIGHT, PAPER red — a life
LIFE_BASE   equ     $5800 + 28      ; row 0, columns 28-30

MSG_ATTR    equ     %01000111
MSG_ROW     equ     11
WIN_COL     equ     7
LOSE_COL    equ     10
FONT        equ     $3C00

START_COL   equ     15
START_ROW   equ     11
DRAUGHT_COL0 equ    18
DRAUGHT_ROW0 equ    3

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
            ld      b, 32
.top:
            ld      (hl), WALL
            inc     hl
            djnz    .top

            ld      hl, $5AE0
            ld      b, 32
.bottom:
            ld      (hl), WALL
            inc     hl
            djnz    .bottom

            ld      hl, $5820
            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_lives
            call    draw_lamps
            call    save_under
            call    draw_lamp
            call    save_draught
            call    draw_draught

; ============================================================================
; THE HEARTBEAT.
; ============================================================================
            im      1
            ei

game_loop:
            halt
            call    player_step
            call    draught_step
            ld      a, (lit_count)
            cp      NUM_LAMPS
            jp      z, win
            jr      game_loop

; ----------------------------------------------------------------------------
; player_step — move the lamplighter; a step onto the draught costs a life.
; ----------------------------------------------------------------------------
player_step:
            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, .pleft
            bit     0, a
            jr      z, .pright
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a
            jr      z, .pup
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a
            jr      z, .pdown
            ret

.pleft:
            ld      hl, tcol
            dec     (hl)
            jr      .pmove
.pright:
            ld      hl, tcol
            inc     (hl)
            jr      .pmove
.pup:
            ld      hl, trow
            dec     (hl)
            jr      .pmove
.pdown:
            ld      hl, trow
            inc     (hl)
.pmove:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz              ; wall blocks

            ; would the step land on the draught? then it's a collision
            ld      a, (tcol)
            ld      hl, draught_col
            cp      (hl)
            jr      nz, .pcommit
            ld      a, (trow)
            ld      hl, draught_row
            cp      (hl)
            jr      nz, .pcommit
            call    lose_life
            ret

.pcommit:
            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, .pdrawn
            ld      a, LAMP_LIT
            ld      (under_lamp + 8), a
            call    light_pip
.pdrawn:
            call    draw_lamp
            ret

; ----------------------------------------------------------------------------
; draught_step — drift, bounce, snuff; a step onto the lamplighter costs a life.
; ----------------------------------------------------------------------------
draught_step:
            ld      a, (draught_timer)
            dec     a
            ld      (draught_timer), a
            ret     nz
            ld      a, DRAUGHT_SPEED
            ld      (draught_timer), a

            ld      a, (draught_col)
            ld      b, a
            ld      a, (draught_dx)
            add     a, b
            ld      c, a
            ld      a, (draught_row)
            ld      b, a
            call    wall_at
            jr      z, .hok
            ld      a, (draught_dx)
            neg
            ld      (draught_dx), a
.hok:
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_dy)
            add     a, b
            ld      b, a
            ld      a, (draught_col)
            ld      c, a
            call    wall_at
            jr      z, .vok
            ld      a, (draught_dy)
            neg
            ld      (draught_dy), a
.vok:
            ; work out the target cell
            ld      a, (draught_col)
            ld      b, a
            ld      a, (draught_dx)
            add     a, b
            ld      (dtcol), a
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_dy)
            add     a, b
            ld      (dtrow), a

            ; would it land on the lamplighter? then it's a collision
            ld      a, (dtcol)
            ld      hl, lamp_col
            cp      (hl)
            jr      nz, .dmove
            ld      a, (dtrow)
            ld      hl, lamp_row
            cp      (hl)
            jr      nz, .dmove
            call    lose_life
            ret                     ; don't move onto him

.dmove:
            call    restore_draught
            ld      a, (dtcol)
            ld      (draught_col), a
            ld      a, (dtrow)
            ld      (draught_row), a
            call    save_draught
            ld      a, (under_draught + 8)
            cp      LAMP_LIT
            jr      nz, .nosnuff
            ld      a, LAMP_UNLIT
            ld      (under_draught + 8), a
            call    unlight_pip
.nosnuff:
            call    draw_draught
            ret

; ----------------------------------------------------------------------------
; lose_life — drop a life pip; reset the lamplighter, or fall to night.
; ----------------------------------------------------------------------------
lose_life:
            ld      a, (lives)
            dec     a
            ld      (lives), a
            ld      e, a            ; index of the life pip to remove
            ld      d, 0
            ld      hl, LIFE_BASE
            add     hl, de
            ld      (hl), COBBLE    ; the pip goes dark
            ld      a, (lives)
            or      a
            jp      z, lose         ; out of lives — night falls

            ; otherwise send the lamplighter back to the start, clear of danger
            call    restore_under
            ld      a, START_COL
            ld      (lamp_col), a
            ld      a, START_ROW
            ld      (lamp_row), a
            call    save_under
            call    draw_lamp
            ret

; ----------------------------------------------------------------------------
; win / lose — each prints a line and holds.
; ----------------------------------------------------------------------------
win:
            call    restore_under
            ld      hl, win_text
            ld      b, MSG_ROW
            ld      c, WIN_COL
            call    print_string
.whold:
            halt
            jr      .whold

lose:
            ld      hl, $5800       ; night falls — wash the square to black
            ld      de, $5801
            ld      (hl), %00000000
            ld      bc, 767
            ldir
            ld      hl, lose_text
            ld      b, MSG_ROW
            ld      c, LOSE_COL
            call    print_string
.lhold:
            halt
            jr      .lhold

; ----------------------------------------------------------------------------
; print_string — HL=string ($FF-terminated), B=row, C=col.
; ----------------------------------------------------------------------------
print_string:
.ps:
            ld      a, (hl)
            cp      $FF
            ret     z
            push    hl
            push    bc
            call    print_char
            pop     bc
            pop     hl
            inc     hl
            inc     c
            jr      .ps

print_char:
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, FONT
            add     hl, de
            ex      de, hl
            push    de
            call    attr_addr_cr
            ld      (hl), MSG_ATTR
            call    scr_addr_cr
            pop     de
            ld      b, 8
.pc:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .pc
            ret

; ----------------------------------------------------------------------------
; light_pip / unlight_pip / draw_pips / draw_lives.
; ----------------------------------------------------------------------------
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

unlight_pip:
            ld      a, (lit_count)
            dec     a
            ld      (lit_count), a
            ld      e, a
            ld      d, 0
            ld      hl, PIP_BASE
            add     hl, de
            ld      (hl), PIP_UNLIT
            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_lives:
            ld      hl, LIFE_BASE
            ld      b, LIVES
            ld      a, LIFE_PIP
.dlv:
            ld      (hl), a
            inc     hl
            djnz    .dlv
            ret

; ----------------------------------------------------------------------------
; draw_lamps / draw_lantern.
; ----------------------------------------------------------------------------
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.
; ----------------------------------------------------------------------------
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

; ----------------------------------------------------------------------------
; The lamplighter's save / restore / draw.
; ----------------------------------------------------------------------------
pos_bc:
            ld      a, (lamp_row)
            ld      b, a
            ld      a, (lamp_col)
            ld      c, a
            ret

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

; ----------------------------------------------------------------------------
; The draught's save / restore / draw.
; ----------------------------------------------------------------------------
dpos_bc:
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_col)
            ld      c, a
            ret

save_draught:
            call    dpos_bc
            call    scr_addr_cr
            ld      de, under_draught
            ld      b, 8
.sd:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .sd
            call    dpos_bc
            call    attr_addr_cr
            ld      a, (hl)
            ld      (under_draught + 8), a
            ret

restore_draught:
            call    dpos_bc
            call    scr_addr_cr
            ld      de, under_draught
            ld      b, 8
.rd:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .rd
            call    dpos_bc
            call    attr_addr_cr
            ld      a, (under_draught + 8)
            ld      (hl), a
            ret

draw_draught:
            call    dpos_bc
            call    attr_addr_cr
            ld      (hl), DRAUGHT_ATTR
            call    dpos_bc
            call    scr_addr_cr
            ld      de, draught_glyph
            ld      b, 8
.dd:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .dd
            ret

; ----------------------------------------------------------------------------
; Level data, state, buffers, 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
lives:
            defb    LIVES

draught_col:
            defb    DRAUGHT_COL0
draught_row:
            defb    DRAUGHT_ROW0
draught_dx:
            defb    1
draught_dy:
            defb    1
draught_timer:
            defb    DRAUGHT_SPEED
dtcol:
            defb    0
dtrow:
            defb    0

under_lamp:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
under_draught:
            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

draught_glyph:
            defb    %00000000
            defb    %00111100
            defb    %01111110
            defb    %11111111
            defb    %11111111
            defb    %01111110
            defb    %00111100
            defb    %00000000

win_text:
            defb    "THE NIGHT IS HELD"
            defb    $FF
lose_text:
            defb    "NIGHT FALLS"
            defb    $FF

            end     start

The HUD now carries your lives — three red pips at the top right, beside the lamp tally:

The square in play, with the cyan lamp tally at the top left of the HUD and three red life pips at the top right.
Three lives, shown as red pips at the right of the HUD. Each time the draught catches you, one winks out — and the lamplighter is thrown back to the start.

Let the draught catch you three times and the square is swallowed whole — the lose state, earned by losing:

A black screen with the words NIGHT FALLS in white across the middle.
Night falls: the whole attribute map washed to black and two cold words left behind. Reached by play — three collisions, lives counted down to zero in memory. The win's mirror image.

When it's wrong, see why

Collision and the lose state fail in their own ways:

  • No collisions ever register. The same-cell test is missing or only in one place. Check both player_step and draught_step compare the two positions before committing a move.
  • The sprites overlap and garble. You let a move onto the shared cell go through. The check must come before the move commits and refuse it, exactly like the wall test.
  • Lives go wrong or the count underflows. lose_life decrements then indexes LIFE_BASE + lives; make sure it's only ever called on a real hit.
  • Night never falls. Lives aren't reaching zero — either collisions aren't registering, or the draught can't reach the lamplighter. Lower DRAUGHT_SPEED while testing.

Before and after

You started with a game you could only win and finished with one you can also lose — and both stakes are the same machinery you already had. Collision is the wall test pointed at another sprite; lives are the tally in reverse; the lose state is the win state dressed in black. The two sprites never overlap, so nothing about the engine had to change to make them deadly — refusing the shared cell was enough. Gloaming has a goal, a threat, and two ways to end.

Try this: more or fewer lives

LIVES sets how many pips you start with. Try 1 for a brutal single-life run, or 5 for a gentler game. The pips draw themselves from that one number — change it and the HUD follows. How forgiving the game feels is yours to set.

Try this: a jolt when caught

A lost life should land. In lose_life, before the reset, flash the border red for a few frames (ld a,2 / out ($FE),a, a short delay, then ld a,0). The hit becomes something you feel, not a pip quietly going out — and it buys a beat before you're thrown back to the start.

Try this: only the draught is deadly

Right now you lose a life whether you walk into the draught or it walks into you. Remove the check in player_step and keep only the one in draught_step: now you can brush past the draught safely, and only its own movement onto you is fatal. It's a subtler, fairer feel — and a single deleted block changes the whole character of the threat.

What you've learnt

  • Entity-versus-entity collision is a same-cell test — and blocking the overlap keeps two sprites' save/restore buffers clean, sidestepping the harder problem of drawing them on top of each other.
  • Lives are another pip readout, depleted like the tally in reverse.
  • A lose state mirrors the win state — an end the game rests in, here dressed as the dark taking the square.
  • Gloaming is now a complete game: a goal, a threat, and both a win and a loss.

What's next

The mechanics are all here — you can win, and you can lose. What's left is to make it feel like a game you sit down to play, and that's Phase E. In Unit 16, "The Title", we add a title screen and the state machine that ties everything together: title → play → win or lose → back to the title. Gloaming stops being a program that starts in the middle and becomes one with a front door.