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.
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.
| 1 | 1 | ; Gloaming — Unit 14: It Snuffs the Light | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 11 | 11 | LAMP_LIT equ %01000110 | |
| 12 | 12 | WALL_BIT equ 3 | |
| 13 | 13 | | |
| 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 | |
| 16 | 16 | | |
| 17 | 17 | PIP_UNLIT equ %00101000 | |
| 18 | 18 | PIP_LIT equ %01110000 | |
| ... | |||
| 26 | 26 | | |
| 27 | 27 | START_COL equ 15 | |
| 28 | 28 | START_ROW equ 11 | |
| 29 | - | DRAUGHT_COL0 equ 15 | |
| 29 | + | DRAUGHT_COL0 equ 18 ; its diagonal now crosses the lamp at (22,7) | |
| 30 | 30 | DRAUGHT_ROW0 equ 3 | |
| 31 | 31 | | |
| 32 | 32 | KEYS_OP equ $DFFE | |
| ... | |||
| 77 | 77 | call draw_lamps | |
| 78 | 78 | call save_under | |
| 79 | 79 | call draw_lamp | |
| 80 | - | call save_draught ; the draught starts up too | |
| 80 | + | call save_draught | |
| 81 | 81 | call draw_draught | |
| 82 | 82 | | |
| 83 | 83 | ; ============================================================================ | |
| 84 | - | ; THE HEARTBEAT — step the player, then step the draught. | |
| 84 | + | ; THE HEARTBEAT. | |
| 85 | 85 | ; ============================================================================ | |
| 86 | 86 | im 1 | |
| 87 | 87 | ei | |
| ... | |||
| 96 | 96 | jr game_loop | |
| 97 | 97 | | |
| 98 | 98 | ; ---------------------------------------------------------------------------- | |
| 99 | - | ; player_step — read QAOP, move the lamplighter if clear, light lamps. | |
| 99 | + | ; player_step — move the lamplighter, light lamps (Unit 13). | |
| 100 | 100 | ; ---------------------------------------------------------------------------- | |
| 101 | 101 | player_step: | |
| 102 | 102 | ld a, (lamp_col) | |
| ... | |||
| 118 | 118 | in a, (c) | |
| 119 | 119 | bit 0, a | |
| 120 | 120 | jr z, .pdown | |
| 121 | - | ret ; nothing held | |
| 121 | + | ret | |
| 122 | 122 | | |
| 123 | 123 | .pleft: | |
| 124 | 124 | ld hl, tcol | |
| ... | |||
| 141 | 141 | ld a, (tcol) | |
| 142 | 142 | ld c, a | |
| 143 | 143 | call wall_at | |
| 144 | - | ret nz ; blocked | |
| 144 | + | ret nz | |
| 145 | 145 | | |
| 146 | 146 | call restore_under | |
| 147 | 147 | ld a, (tcol) | |
| ... | |||
| 160 | 160 | ret | |
| 161 | 161 | | |
| 162 | 162 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 164 | 164 | ; ---------------------------------------------------------------------------- | |
| 165 | 165 | draught_step: | |
| 166 | 166 | ld a, (draught_timer) | |
| 167 | 167 | dec a | |
| 168 | 168 | ld (draught_timer), a | |
| 169 | - | ret nz ; not time to move yet | |
| 169 | + | ret nz | |
| 170 | 170 | ld a, DRAUGHT_SPEED | |
| 171 | 171 | ld (draught_timer), a | |
| 172 | 172 | | |
| 173 | - | ; horizontal: is (row, col+dx) a wall? if so, reverse dx | |
| 174 | 173 | ld a, (draught_col) | |
| 175 | 174 | ld b, a | |
| 176 | 175 | ld a, (draught_dx) | |
| 177 | 176 | add a, b | |
| 178 | - | ld c, a ; C = col + dx | |
| 177 | + | ld c, a | |
| 179 | 178 | ld a, (draught_row) | |
| 180 | - | ld b, a ; B = row | |
| 179 | + | ld b, a | |
| 181 | 180 | call wall_at | |
| 182 | 181 | jr z, .hok | |
| 183 | 182 | ld a, (draught_dx) | |
| 184 | 183 | neg | |
| 185 | 184 | ld (draught_dx), a | |
| 186 | 185 | .hok: | |
| 187 | - | ; vertical: is (row+dy, col) a wall? if so, reverse dy | |
| 188 | 186 | ld a, (draught_row) | |
| 189 | 187 | ld b, a | |
| 190 | 188 | ld a, (draught_dy) | |
| 191 | 189 | add a, b | |
| 192 | - | ld b, a ; B = row + dy | |
| 190 | + | ld b, a | |
| 193 | 191 | ld a, (draught_col) | |
| 194 | - | ld c, a ; C = col | |
| 192 | + | ld c, a | |
| 195 | 193 | call wall_at | |
| 196 | 194 | jr z, .vok | |
| 197 | 195 | ld a, (draught_dy) | |
| 198 | 196 | neg | |
| 199 | 197 | ld (draught_dy), a | |
| 200 | 198 | .vok: | |
| 201 | - | ; move by the (possibly reversed) velocity | |
| 202 | 199 | call restore_draught | |
| 203 | 200 | ld a, (draught_col) | |
| 204 | 201 | ld b, a | |
| ... | |||
| 211 | 208 | add a, b | |
| 212 | 209 | ld (draught_row), a | |
| 213 | 210 | 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: | |
| 214 | 220 | 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 | |
| 215 | 235 | ret | |
| 216 | 236 | | |
| 217 | 237 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 418 | 438 | ret | |
| 419 | 439 | | |
| 420 | 440 | ; ---------------------------------------------------------------------------- | |
| 421 | - | ; The draught's save / restore / draw — the same dance, its own data. | |
| 441 | + | ; The draught's save / restore / draw (Unit 13). | |
| 422 | 442 | ; ---------------------------------------------------------------------------- | |
| 423 | 443 | dpos_bc: | |
| 424 | 444 | 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:
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:
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 + 8aftersave_draught, and compare it withLAMP_LIT. - The snuffed lamp stays gold. You changed the live cell, or the wrong buffer. Cool the saved copy (
under_draught + 8) sorestore_draughtpaints it cold when the draught leaves. - The tally underflows or goes wrong.
unlight_piponly ever runs when a lit lamp is snuffed, so the count can't drop below zero — confirm it's inside thecp LAMP_LITbranch. - 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.