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

It Snuffs the Light

The draught earns its menace. When it crosses a lit lamp it puts it out — editing its own saved buffer back to cold cyan, the exact mirror of the lamplighter's lighting — and the tally drops. Two moving things now fight over the same ground.

70% of Gloaming

The draught drifts, but does no harm. This unit gives it its purpose: when it crosses a lit lamp, it snuffs it — cold cyan again — and the tally falls. The dark starts winning back the ground you've covered, and for the first time you have to think about where the draught is going.

The neat part: you already know how to do this. It's Unit 10's lighting, run backwards.

Where we start

Unit 13's draught — a wisp that drifts and bounces but leaves the world untouched. We give it teeth.

Snuffing is lighting, mirrored

In Unit 10, when the lamplighter stepped onto a cold lamp, he edited his saved buffer from cyan to yellow, so restore painted it lit. The draught does the opposite. When it steps onto a cell, it saves the nine bytes as always — and if what it saved is a lit lamp, it rewrites the saved attribute back to unlit:

ld   a, (under_draught + 8)   ; what the draught just covered
cp   LAMP_LIT                ; a lit lamp?
jr   nz, .nosnuff
ld   a, LAMP_UNLIT           ; then cool the SAVED copy
ld   (under_draught + 8), a
call unlight_pip             ; and drop the tally
.nosnuff:

When the draught drifts off, restore_draught writes that cooled lamp back to the screen. The lamplighter edits his buffer to light; the draught edits its buffer to snuff. Same mechanism, opposite sign — and unlight_pip is light_pip run backwards, dropping the count and cooling the top pip.

Two things fighting over one world

This is the first entity-versus-world interaction in the game: one moving thing changing a piece of the world that another moving thing cares about. The lamps aren't the lamplighter's, and they aren't the draught's — they're contested ground, and the state of each cell is the running score of who reached it last. The lamplighter lights; the draught snuffs; the board is the argument between them.

It changes how the game plays, too. Lighting a lamp is no longer permanent, so the win — all eight lit at once — becomes a real test. You have to light faster than the draught can eat, and steer your route to stay ahead of its drift. Mechanics just became a contest.

Milestone — let it snuff

Right after save_draught, we read what the draught covered, and if it's a lit lamp, cool the saved copy and drop the tally with unlight_pip. That's the whole change — the mirror image of Unit 10's six lines.

Step 1: the draught cools a lit lamp it crosses
+39-19
11 ; Gloaming — Unit 14: It Snuffs the Light
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 13's harmless draught — it drifts, but changes nothing.
3+; step-01 lets the draught snuff a lit lamp — Unit 10's lighting, mirrored.
44
55 org 32768
66
...
1111 LAMP_LIT equ %01000110
1212 WALL_BIT equ 3
1313
14-DRAUGHT_ATTR equ %01000101 ; BRIGHT, PAPER black, INK cyan — a cold wisp
15-DRAUGHT_SPEED equ 8 ; move once every this many frames
14+DRAUGHT_ATTR equ %01000101
15+DRAUGHT_SPEED equ 8
1616
1717 PIP_UNLIT equ %00101000
1818 PIP_LIT equ %01110000
...
2626
2727 START_COL equ 15
2828 START_ROW equ 11
29-DRAUGHT_COL0 equ 15
29+DRAUGHT_COL0 equ 18 ; its diagonal now crosses the lamp at (22,7)
3030 DRAUGHT_ROW0 equ 3
3131
3232 KEYS_OP equ $DFFE
...
7777 call draw_lamps
7878 call save_under
7979 call draw_lamp
80- call save_draught ; the draught starts up too
80+ call save_draught
8181 call draw_draught
8282
8383 ; ============================================================================
84-; THE HEARTBEAT — step the player, then step the draught.
84+; THE HEARTBEAT.
8585 ; ============================================================================
8686 im 1
8787 ei
...
9696 jr game_loop
9797
9898 ; ----------------------------------------------------------------------------
99-; player_step — read QAOP, move the lamplighter if clear, light lamps.
99+; player_step — move the lamplighter, light lamps (Unit 13).
100100 ; ----------------------------------------------------------------------------
101101 player_step:
102102 ld a, (lamp_col)
...
118118 in a, (c)
119119 bit 0, a
120120 jr z, .pdown
121- ret ; nothing held
121+ ret
122122
123123 .pleft:
124124 ld hl, tcol
...
141141 ld a, (tcol)
142142 ld c, a
143143 call wall_at
144- ret nz ; blocked
144+ ret nz
145145
146146 call restore_under
147147 ld a, (tcol)
...
160160 ret
161161
162162 ; ----------------------------------------------------------------------------
163-; draught_step — on its timer, bounce off walls and drift one cell.
163+; draught_step — drift, bounce, and SNUFF any lit lamp stepped onto.
164164 ; ----------------------------------------------------------------------------
165165 draught_step:
166166 ld a, (draught_timer)
167167 dec a
168168 ld (draught_timer), a
169- ret nz ; not time to move yet
169+ ret nz
170170 ld a, DRAUGHT_SPEED
171171 ld (draught_timer), a
172172
173- ; horizontal: is (row, col+dx) a wall? if so, reverse dx
174173 ld a, (draught_col)
175174 ld b, a
176175 ld a, (draught_dx)
177176 add a, b
178- ld c, a ; C = col + dx
177+ ld c, a
179178 ld a, (draught_row)
180- ld b, a ; B = row
179+ ld b, a
181180 call wall_at
182181 jr z, .hok
183182 ld a, (draught_dx)
184183 neg
185184 ld (draught_dx), a
186185 .hok:
187- ; vertical: is (row+dy, col) a wall? if so, reverse dy
188186 ld a, (draught_row)
189187 ld b, a
190188 ld a, (draught_dy)
191189 add a, b
192- ld b, a ; B = row + dy
190+ ld b, a
193191 ld a, (draught_col)
194- ld c, a ; C = col
192+ ld c, a
195193 call wall_at
196194 jr z, .vok
197195 ld a, (draught_dy)
198196 neg
199197 ld (draught_dy), a
200198 .vok:
201- ; move by the (possibly reversed) velocity
202199 call restore_draught
203200 ld a, (draught_col)
204201 ld b, a
...
211208 add a, b
212209 ld (draught_row), a
213210 call save_draught
211+
212+ ; --- snuff it: if the saved cell is a lit lamp, cool it ---
213+ ld a, (under_draught + 8)
214+ cp LAMP_LIT
215+ jr nz, .nosnuff
216+ ld a, LAMP_UNLIT
217+ ld (under_draught + 8), a ; restored cold when the draught leaves
218+ call unlight_pip
219+.nosnuff:
214220 call draw_draught
221+ ret
222+
223+; ----------------------------------------------------------------------------
224+; unlight_pip — drop the tally by one and cool the top pip (mirror of light_pip).
225+; ----------------------------------------------------------------------------
226+unlight_pip:
227+ ld a, (lit_count)
228+ dec a
229+ ld (lit_count), a ; new, lower count
230+ ld e, a ; index of the pip that was on top
231+ ld d, 0
232+ ld hl, PIP_BASE
233+ add hl, de
234+ ld (hl), PIP_UNLIT
215235 ret
216236
217237 ; ----------------------------------------------------------------------------
...
418438 ret
419439
420440 ; ----------------------------------------------------------------------------
421-; The draught's save / restore / draw — the same dance, its own data.
441+; The draught's save / restore / draw (Unit 13).
422442 ; ----------------------------------------------------------------------------
423443 dpos_bc:
424444 ld a, (draught_row)
The complete program
; Gloaming — Unit 14: It Snuffs the Light
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 lets the draught snuff a lit lamp — Unit 10's lighting, mirrored.

            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

MSG_ATTR    equ     %01000111
MSG_ROW     equ     11
MSG_COL     equ     7
FONT        equ     $3C00

START_COL   equ     15
START_ROW   equ     11
DRAUGHT_COL0 equ    18              ; its diagonal now crosses the lamp at (22,7)
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_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, light lamps  (Unit 13).
; ----------------------------------------------------------------------------
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

            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, and SNUFF any lit lamp stepped onto.
; ----------------------------------------------------------------------------
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:
            call    restore_draught
            ld      a, (draught_col)
            ld      b, a
            ld      a, (draught_dx)
            add     a, b
            ld      (draught_col), a
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_dy)
            add     a, b
            ld      (draught_row), a
            call    save_draught

            ; --- snuff it: if the saved cell is a lit lamp, cool it ---
            ld      a, (under_draught + 8)
            cp      LAMP_LIT
            jr      nz, .nosnuff
            ld      a, LAMP_UNLIT
            ld      (under_draught + 8), a   ; restored cold when the draught leaves
            call    unlight_pip
.nosnuff:
            call    draw_draught
            ret

; ----------------------------------------------------------------------------
; unlight_pip — drop the tally by one and cool the top pip (mirror of light_pip).
; ----------------------------------------------------------------------------
unlight_pip:
            ld      a, (lit_count)
            dec     a
            ld      (lit_count), a   ; new, lower count
            ld      e, a             ; index of the pip that was on top
            ld      d, 0
            ld      hl, PIP_BASE
            add     hl, de
            ld      (hl), PIP_UNLIT
            ret

; ----------------------------------------------------------------------------
; win / draw_message / print_char  (Unit 12).
; ----------------------------------------------------------------------------
win:
            call    restore_under
            call    draw_message
.hold:
            halt
            jr      .hold

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
            pop     hl
            inc     hl
            inc     c
            jr      .dm

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 / 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  (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

; ----------------------------------------------------------------------------
; The lamplighter's save / restore / draw  (Unit 8).
; ----------------------------------------------------------------------------
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  (Unit 13).
; ----------------------------------------------------------------------------
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

draught_col:
            defb    DRAUGHT_COL0
draught_row:
            defb    DRAUGHT_ROW0
draught_dx:
            defb    1
draught_dy:
            defb    1
draught_timer:
            defb    DRAUGHT_SPEED

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

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

            end     start

Light a lamp in the draught's path, and watch it wink back to cold as the draught drifts through — the tally pip cooling with it:

A lamp glows gold and the draught drifts straight into it — as it passes, the lamp cools back to cyan and the warm tally pip at the top goes cold with it (lit_count drops from 1 to 0). The dark pushing back, byte by byte.

Light more than one and the contest shows: a survivor glows on while the draught cools another beside it, the tally landing on what's left:

The square with one lamp lit gold at the left beside the lamplighter, the draught hovering by a freshly-cooled cyan lamp at the upper right, and the tally showing one.
Two lamps were lit; the draught crossed one and put it out (now cyan, by the draught at upper right). The survivor still glows gold at left, and the tally has dropped to one — confirmed by reading lit_count from memory.

When it's wrong, see why

The snuff is the lighting code's mirror, and it fails the same ways:

  • Lamps are never snuffed. The check is misplaced or compares the wrong value. Read under_draught + 8 after save_draught, and compare it with LAMP_LIT.
  • The snuffed lamp stays gold. You changed the live cell, or the wrong buffer. Cool the saved copy (under_draught + 8) so restore_draught paints it cold when the draught leaves.
  • The tally underflows or goes wrong. unlight_pip only ever runs when a lit lamp is snuffed, so the count can't drop below zero — confirm it's inside the cp LAMP_LIT branch.
  • You can never win now. That may be the game, not a bug: the draught can snuff faster than you light. Slow it (DRAUGHT_SPEED) while you test, or get quicker.

Before and after

You started with a draught that drifted past everything and finished with one that undoes your work — and the menace is six lines, the lighting mechanic with two constants flipped. The lamps became contested ground; the win became a race; the count can move both ways and still stays honest because the decrement only fires on a real loss. An antagonist didn't need new machinery — it needed permission to edit the same state you do.

Try this: a hungrier draught

Lower DRAUGHT_SPEED so the draught moves more often and snuffs faster. Find the point where the game tips from "tidy up at leisure" to "genuinely tense" — difficulty lives in that one number, and feeling it move is what tuning is.

Try this: a jolt when it bites

Give the snuff a moment of feedback. In the .nosnuff branch (just before it), flash the border for an instant — ld a,2 / out ($FE),a then back to 0 — so losing a lamp registers. A change the player can't see or feel barely counts; a flash of red sells the loss.

Try this: turn the enemy kind

Change the draught's cp LAMP_LIT to cp LAMP_UNLIT, and have it set LAMP_LIT (and call light_pip). Now the "draught" lights lamps as it wanders — a helper, not a threat. It's the same code with two constants swapped, and it shows the mechanic was never "good" or "evil": the rule is whatever you write.

What you've learnt

  • Entity-versus-world interaction: one actor changes shared world state that another actor cares about.
  • The snuff is the lighting mechanic mirrored — edit the saved buffer to the opposite value.
  • Keep paired counters honest (light_pip / unlight_pip) and guard the decrement so it only fires on a real loss.
  • An antagonist that edits the same state turns a set of mechanics into a contest.

What's next

The draught can undo your work — but it still can't touch you. In Unit 15, "Lives, and the Fall of Night", it can: if the draught and the lamplighter land on the same cell, you lose a life, and when the lives run out, night falls — the lose state. That's the last piece Phase D needs, and the moment Gloaming becomes a game you can not only win, but lose.