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

The Draught

The threat arrives. A cold wisp drifts through the square on its own — a second cell sprite running the same save/restore/draw engine the lamplighter uses, steered by a tiny bounce-off-walls patrol rule.

65% of Gloaming

Gloaming can be won but not lost — and a game with no danger has no tension. Phase D brings the dark to life. It starts here, with a cold draught: a wisp that drifts through the square under its own steam. This unit sets it moving; Unit 14 lets it snuff lamps, and Unit 15 makes it cost you.

The reason this unit is short is the best news in the whole game: the draught is a second cell sprite, and it runs the same engine you built for the lamplighter over Units 5–8. You spent four units making one character move and behave; a second one is almost free.

Where we start

Unit 12's finished game — one mover, steered by you, and no threat. We add a second mover that steers itself.

A second sprite is the same sprite

The draught keeps its own state — draught_col, draught_row — its own nine-byte buffer, under_draught, and its own shape. And the three things it does are the three things the lamplighter does: save what it's about to cover, draw itself, restore when it leaves. The routines save_draught / restore_draught / draw_draught are line-for-line the same shape as the lamplighter's, pointed at the draught's data.

That repetition is the lesson. (In a larger game you'd fold the pair into one routine that takes "which sprite" as a parameter — and a later game does. Seeing them side by side first makes it plain they're the same machine, run twice.) The costly part — the engine — was built once and now carries as many sprites as you give it data for.

Steering itself: a tiny patrol rule

The lamplighter is steered by your keys; the draught steers itself, with about the smallest rule that still looks alive. It carries a velocity — one step in x, one in y (draught_dx, draught_dy, each +1 or -1) — and drifts diagonally. When a wall lies directly ahead on an axis, it reverses that axis and bounces:

; if (row, col+dx) is a wall, flip dx
; if (row+dy, col) is a wall, flip dy
; then move by the (maybe flipped) dx, dy

It's the same wall_at test the lamplighter obeys — the draught is penned in the square by the same walls. No pursuit, no cleverness; a bounce. (It's plenty for a wisp, and the gentle-ramp forbids reaching for AI here.)

A drift, not a race, and a two-actor loop

If the draught moved every frame it would streak around fifty cells a second. So it moves on a little timer — once every few frames — giving it a slow, cold drift. Each sprite can keep its own pace this way.

And the loop grows up. Instead of doing everything inline, it now calls two steps in turn:

game_loop:
    halt
    call player_step      ; you
    call draught_step      ; the dark
    ... win check ...
    jr game_loop

One frame, two actors, each given its turn. That shape — a loop that steps every moving thing once per frame — is how every game with more than one moving part is built.

Milestone — set the dark drifting

We give the draught its own state, buffer and shape, its save/restore/draw trio, a bounce-off-walls draught_step on a timer, and split the loop into player_step and draught_step. The lamplighter's code is untouched — the engine just runs twice now.

Step 1: a second self-steering cell sprite, and a two-actor loop
+193-49
11 ; Gloaming — Unit 13: The Draught
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 12's finished, winnable game — one mover, no threat.
3+; step-01 adds a second cell sprite — a wisp that drifts and bounces on its own.
44
55 org 32768
66
...
1010 LAMP_UNLIT equ %00000101
1111 LAMP_LIT equ %01000110
1212 WALL_BIT equ 3
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
1316
1417 PIP_UNLIT equ %00101000
1518 PIP_LIT equ %01110000
1619 PIP_BASE equ $5800 + 12
1720 NUM_LAMPS equ 8
1821
19-MSG_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the closing line
22+MSG_ATTR equ %01000111
2023 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
24+MSG_COL equ 7
25+FONT equ $3C00
2326
2427 START_COL equ 15
2528 START_ROW equ 11
29+DRAUGHT_COL0 equ 15
30+DRAUGHT_ROW0 equ 3
2631
2732 KEYS_OP equ $DFFE
2833 KEYS_Q equ $FBFE
...
4146 ld bc, 767
4247 ldir
4348
44- ld hl, $5820 ; top wall — row 1
49+ ld hl, $5820
4550 ld b, 32
4651 .top:
4752 ld (hl), WALL
4853 inc hl
4954 djnz .top
5055
51- ld hl, $5AE0 ; bottom wall — row 23
56+ ld hl, $5AE0
5257 ld b, 32
5358 .bottom:
5459 ld (hl), WALL
5560 inc hl
5661 djnz .bottom
5762
58- ld hl, $5820 ; sides, rows 1..23
63+ ld hl, $5820
5964 ld b, 23
6065 .sides:
6166 ld (hl), WALL
...
7277 call draw_lamps
7378 call save_under
7479 call draw_lamp
80+ call save_draught ; the draught starts up too
81+ call draw_draught
7582
7683 ; ============================================================================
77-; THE HEARTBEAT — move, light, tally, and check for the win.
84+; THE HEARTBEAT — step the player, then step the draught.
7885 ; ============================================================================
7986 im 1
8087 ei
8188
8289 game_loop:
8390 halt
91+ call player_step
92+ call draught_step
93+ ld a, (lit_count)
94+ cp NUM_LAMPS
95+ jp z, win
96+ jr game_loop
8497
98+; ----------------------------------------------------------------------------
99+; player_step — read QAOP, move the lamplighter if clear, light lamps.
100+; ----------------------------------------------------------------------------
101+player_step:
85102 ld a, (lamp_col)
86103 ld (tcol), a
87104 ld a, (lamp_row)
...
90107 ld bc, KEYS_OP
91108 in a, (c)
92109 bit 1, a
93- jr z, .left
110+ jr z, .pleft
94111 bit 0, a
95- jr z, .right
112+ jr z, .pright
96113 ld bc, KEYS_Q
97114 in a, (c)
98115 bit 0, a
99- jr z, .up
116+ jr z, .pup
100117 ld bc, KEYS_A
101118 in a, (c)
102119 bit 0, a
103- jr z, .down
104- jr game_loop
120+ jr z, .pdown
121+ ret ; nothing held
105122
106-.left:
123+.pleft:
107124 ld hl, tcol
108125 dec (hl)
109- jr .try
110-.right:
126+ jr .pmove
127+.pright:
111128 ld hl, tcol
112129 inc (hl)
113- jr .try
114-.up:
130+ jr .pmove
131+.pup:
115132 ld hl, trow
116133 dec (hl)
117- jr .try
118-.down:
134+ jr .pmove
135+.pdown:
119136 ld hl, trow
120137 inc (hl)
121-.try:
138+.pmove:
122139 ld a, (trow)
123140 ld b, a
124141 ld a, (tcol)
125142 ld c, a
126143 call wall_at
127- jr nz, game_loop
144+ ret nz ; blocked
128145
129146 call restore_under
130147 ld a, (tcol)
...
132149 ld a, (trow)
133150 ld (lamp_row), a
134151 call save_under
135-
136152 ld a, (under_lamp + 8)
137153 cp LAMP_UNLIT
138- jr nz, .not_lamp
154+ jr nz, .pdrawn
139155 ld a, LAMP_LIT
140156 ld (under_lamp + 8), a
141157 call light_pip
142-.not_lamp:
158+.pdrawn:
143159 call draw_lamp
160+ ret
144161
145- ld a, (lit_count) ; all lamps lit?
146- cp NUM_LAMPS
147- jp z, win
148- jr game_loop
162+; ----------------------------------------------------------------------------
163+; draught_step — on its timer, bounce off walls and drift one cell.
164+; ----------------------------------------------------------------------------
165+draught_step:
166+ ld a, (draught_timer)
167+ dec a
168+ ld (draught_timer), a
169+ ret nz ; not time to move yet
170+ ld a, DRAUGHT_SPEED
171+ ld (draught_timer), a
172+
173+ ; horizontal: is (row, col+dx) a wall? if so, reverse dx
174+ ld a, (draught_col)
175+ ld b, a
176+ ld a, (draught_dx)
177+ add a, b
178+ ld c, a ; C = col + dx
179+ ld a, (draught_row)
180+ ld b, a ; B = row
181+ call wall_at
182+ jr z, .hok
183+ ld a, (draught_dx)
184+ neg
185+ ld (draught_dx), a
186+.hok:
187+ ; vertical: is (row+dy, col) a wall? if so, reverse dy
188+ ld a, (draught_row)
189+ ld b, a
190+ ld a, (draught_dy)
191+ add a, b
192+ ld b, a ; B = row + dy
193+ ld a, (draught_col)
194+ ld c, a ; C = col
195+ call wall_at
196+ jr z, .vok
197+ ld a, (draught_dy)
198+ neg
199+ ld (draught_dy), a
200+.vok:
201+ ; move by the (possibly reversed) velocity
202+ call restore_draught
203+ ld a, (draught_col)
204+ ld b, a
205+ ld a, (draught_dx)
206+ add a, b
207+ ld (draught_col), a
208+ ld a, (draught_row)
209+ ld b, a
210+ ld a, (draught_dy)
211+ add a, b
212+ ld (draught_row), a
213+ call save_draught
214+ call draw_draught
215+ ret
149216
150217 ; ----------------------------------------------------------------------------
151-; win — reveal the last lamp, print the closing line, hold the end state.
218+; win / draw_message / print_char (Unit 12).
152219 ; ----------------------------------------------------------------------------
153220 win:
154- call restore_under ; the lamplighter steps aside; last lamp shows
221+ call restore_under
155222 call draw_message
156223 .hold:
157224 halt
158225 jr .hold
159226
160-; ----------------------------------------------------------------------------
161-; draw_message — print msg_text from (MSG_ROW, MSG_COL), ended by $FF.
162-; ----------------------------------------------------------------------------
163227 draw_message:
164228 ld hl, msg_text
165229 ld c, MSG_COL
...
169233 ret z
170234 push hl
171235 ld b, MSG_ROW
172- call print_char ; A=char, B=row, C=col; preserves C
236+ call print_char
173237 pop hl
174238 inc hl
175- inc c ; next column
239+ inc c
176240 jr .dm
177241
178-; ----------------------------------------------------------------------------
179-; print_char — A=char code, B=row, C=col. Copy the ROM-font glyph into the cell.
180-; ----------------------------------------------------------------------------
181242 print_char:
182- ld l, a ; HL = code * 8
243+ ld l, a
183244 ld h, 0
184245 add hl, hl
185246 add hl, hl
186247 add hl, hl
187248 ld de, FONT
188249 add hl, de
189- ex de, hl ; DE = glyph address in ROM
250+ ex de, hl
190251 push de
191- call attr_addr_cr ; HL = attribute of (B,C); BC preserved
252+ call attr_addr_cr
192253 ld (hl), MSG_ATTR
193- call scr_addr_cr ; HL = screen of (B,C)
194- pop de ; DE = glyph address
254+ call scr_addr_cr
255+ pop de
195256 ld b, 8
196257 .pc:
197258 ld a, (de)
...
258319 ret
259320
260321 ; ----------------------------------------------------------------------------
261-; scr_addr_cr / attr_addr_cr / wall_at / pos_bc (Unit 8).
322+; scr_addr_cr / attr_addr_cr / wall_at (Unit 8).
262323 ; ----------------------------------------------------------------------------
263324 scr_addr_cr:
264325 ld a, b
...
296357 bit WALL_BIT, (hl)
297358 ret
298359
360+; ----------------------------------------------------------------------------
361+; The lamplighter's save / restore / draw (Unit 8).
362+; ----------------------------------------------------------------------------
299363 pos_bc:
300364 ld a, (lamp_row)
301365 ld b, a
...
303367 ld c, a
304368 ret
305369
306-; ----------------------------------------------------------------------------
307-; save_under / restore_under / draw_lamp (Unit 8).
308-; ----------------------------------------------------------------------------
309370 save_under:
310371 call pos_bc
311372 call scr_addr_cr
...
357418 ret
358419
359420 ; ----------------------------------------------------------------------------
360-; Level data, state, buffer, shapes, and the closing line.
421+; The draught's save / restore / draw — the same dance, its own data.
422+; ----------------------------------------------------------------------------
423+dpos_bc:
424+ ld a, (draught_row)
425+ ld b, a
426+ ld a, (draught_col)
427+ ld c, a
428+ ret
429+
430+save_draught:
431+ call dpos_bc
432+ call scr_addr_cr
433+ ld de, under_draught
434+ ld b, 8
435+.sd:
436+ ld a, (hl)
437+ ld (de), a
438+ inc de
439+ inc h
440+ djnz .sd
441+ call dpos_bc
442+ call attr_addr_cr
443+ ld a, (hl)
444+ ld (under_draught + 8), a
445+ ret
446+
447+restore_draught:
448+ call dpos_bc
449+ call scr_addr_cr
450+ ld de, under_draught
451+ ld b, 8
452+.rd:
453+ ld a, (de)
454+ ld (hl), a
455+ inc de
456+ inc h
457+ djnz .rd
458+ call dpos_bc
459+ call attr_addr_cr
460+ ld a, (under_draught + 8)
461+ ld (hl), a
462+ ret
463+
464+draw_draught:
465+ call dpos_bc
466+ call attr_addr_cr
467+ ld (hl), DRAUGHT_ATTR
468+ call dpos_bc
469+ call scr_addr_cr
470+ ld de, draught_glyph
471+ ld b, 8
472+.dd:
473+ ld a, (de)
474+ ld (hl), a
475+ inc de
476+ inc h
477+ djnz .dd
478+ ret
479+
480+; ----------------------------------------------------------------------------
481+; Level data, state, buffers, and shapes.
361482 ; ----------------------------------------------------------------------------
362483 lamp_data:
363484 defb 4, 3
...
380501 defb 0
381502 lit_count:
382503 defb 0
504+
505+draught_col:
506+ defb DRAUGHT_COL0
507+draught_row:
508+ defb DRAUGHT_ROW0
509+draught_dx:
510+ defb 1
511+draught_dy:
512+ defb 1
513+draught_timer:
514+ defb DRAUGHT_SPEED
383515
384516 under_lamp:
517+ defb 0, 0, 0, 0, 0, 0, 0, 0, 0
518+under_draught:
385519 defb 0, 0, 0, 0, 0, 0, 0, 0, 0
386520
387521 lamplighter:
...
400534 defb %01111110
401535 defb %01111110
402536 defb %01011010
537+ defb %01111110
538+ defb %01111110
539+ defb %00111100
540+
541+draught_glyph:
542+ defb %00000000
543+ defb %00111100
403544 defb %01111110
545+ defb %11111111
546+ defb %11111111
404547 defb %01111110
405548 defb %00111100
549+ defb %00000000
406550
407551 msg_text:
408552 defb "THE NIGHT IS HELD"
The complete program
; Gloaming — Unit 13: The Draught
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 adds a second cell sprite — a wisp that drifts and bounces on its own.

            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       ; BRIGHT, PAPER black, INK cyan — a cold wisp
DRAUGHT_SPEED equ   8               ; move once every this many frames

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    15
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    ; the draught starts up too
            call    draw_draught

; ============================================================================
; THE HEARTBEAT — step the player, then step the draught.
; ============================================================================
            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 — read QAOP, move the lamplighter if clear, light lamps.
; ----------------------------------------------------------------------------
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                     ; nothing held

.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              ; blocked

            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 — on its timer, bounce off walls and drift one cell.
; ----------------------------------------------------------------------------
draught_step:
            ld      a, (draught_timer)
            dec     a
            ld      (draught_timer), a
            ret     nz              ; not time to move yet
            ld      a, DRAUGHT_SPEED
            ld      (draught_timer), a

            ; horizontal: is (row, col+dx) a wall? if so, reverse dx
            ld      a, (draught_col)
            ld      b, a
            ld      a, (draught_dx)
            add     a, b
            ld      c, a            ; C = col + dx
            ld      a, (draught_row)
            ld      b, a            ; B = row
            call    wall_at
            jr      z, .hok
            ld      a, (draught_dx)
            neg
            ld      (draught_dx), a
.hok:
            ; vertical: is (row+dy, col) a wall? if so, reverse dy
            ld      a, (draught_row)
            ld      b, a
            ld      a, (draught_dy)
            add     a, b
            ld      b, a            ; B = row + dy
            ld      a, (draught_col)
            ld      c, a            ; C = col
            call    wall_at
            jr      z, .vok
            ld      a, (draught_dy)
            neg
            ld      (draught_dy), a
.vok:
            ; move by the (possibly reversed) velocity
            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
            call    draw_draught
            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 — the same dance, its own data.
; ----------------------------------------------------------------------------
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

Leave the keys alone and the draught roams on its own, sliding diagonally and bouncing off the walls — while the lamplighter waits where you left him:

No keys pressed: the pale-cyan draught drifts diagonally across the square and bounces off a wall, on its own timer, while the lamplighter stands still. Two independent movers, one loop — and the draught leaves every lamp untouched as it passes.
The square of unlit lamps with the lamplighter at the centre and a brighter cyan wisp drifting in the upper area.
The draught caught mid-drift, having wandered down from where it started. It can't hurt you yet — the menace is in what it's about to be allowed to do.

When it's wrong, see why

A self-moving sprite fails in its own ways:

  • The draught doesn't move. The timer never reaches zero, or draught_step isn't called. Check the dec / reset and that the loop calls it every frame.
  • It escapes the square. The bounce reversed the wrong axis. The horizontal test is (row, col+dx); the vertical is (row+dy, col) — keep them straight, and confirm neg flips the direction byte.
  • It leaves a trail of wisps. restore_draught isn't running, or it's using the wrong buffer. Each sprite needs its own save/restore buffer.
  • Lamps vanish where it passes. Its save/restore is broken — the draught should preserve lamps exactly as the lamplighter does. (Snuffing them is Unit 14, and deliberate.)

Before and after

You started with one mover and finished with two — and the second one cost almost nothing, because the engine you built over four units never assumed there was only one character. A sprite is its data plus the shared save/restore/draw; give it a velocity and a bounce and it steers itself; pace it with its own timer and step it in a loop beside the player. That two-actor loop is the skeleton every busier game hangs more movers on.

Try this: change its pace

DRAUGHT_SPEED is the frames between steps. Drop it to 3 and the draught hurries; raise it to 16 and it barely creeps. Find the speed that feels like a threat without being unfair — that's game-feel tuning, and it's one number.

Try this: send it another way

Change where it starts (DRAUGHT_COL0 / DRAUGHT_ROW0) and which way it sets off (draught_dx / draught_dy). Set draught_dy to 0 and it patrols straight left-and-right along one row instead of drifting diagonally. The patrol rule doesn't care — it bounces whatever it's given.

Try this: a second draught

Copy draught_* to a draught2_* set — its own position, velocity, timer, buffer — and call a second draught2_step in the loop. Now two wisps roam at once. The engine scales to as many sprites as you can spare memory for; nothing about it assumed there was only one.

What you've learnt

  • The cell-sprite engine carries more than one character — a second sprite is the same save/restore/draw, pointed at its own data.
  • Autonomous movement needs only a velocity and a rule; bounce-off-walls reuses the same wall_at collision.
  • A per-object timer paces a sprite independently of the 50 Hz frame rate.
  • The loop steps each actor in turn — the shape of every multi-sprite game.

What's next

The draught drifts harmlessly through lit and unlit lamps alike. In Unit 14, "It Snuffs the Light", that changes: when the draught crosses a lit lamp, it puts it out — cold cyan again — and the dark starts winning back ground you've covered. Suddenly where the draught is, and where it's heading, matters. The game gets a pulse.