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.
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.
| 1 | 1 | ; Gloaming — Unit 13: The Draught | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 10 | 10 | LAMP_UNLIT equ %00000101 | |
| 11 | 11 | LAMP_LIT equ %01000110 | |
| 12 | 12 | 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 | |
| 13 | 16 | | |
| 14 | 17 | PIP_UNLIT equ %00101000 | |
| 15 | 18 | PIP_LIT equ %01110000 | |
| 16 | 19 | PIP_BASE equ $5800 + 12 | |
| 17 | 20 | NUM_LAMPS equ 8 | |
| 18 | 21 | | |
| 19 | - | MSG_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the closing line | |
| 22 | + | MSG_ATTR equ %01000111 | |
| 20 | 23 | 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 | |
| 23 | 26 | | |
| 24 | 27 | START_COL equ 15 | |
| 25 | 28 | START_ROW equ 11 | |
| 29 | + | DRAUGHT_COL0 equ 15 | |
| 30 | + | DRAUGHT_ROW0 equ 3 | |
| 26 | 31 | | |
| 27 | 32 | KEYS_OP equ $DFFE | |
| 28 | 33 | KEYS_Q equ $FBFE | |
| ... | |||
| 41 | 46 | ld bc, 767 | |
| 42 | 47 | ldir | |
| 43 | 48 | | |
| 44 | - | ld hl, $5820 ; top wall — row 1 | |
| 49 | + | ld hl, $5820 | |
| 45 | 50 | ld b, 32 | |
| 46 | 51 | .top: | |
| 47 | 52 | ld (hl), WALL | |
| 48 | 53 | inc hl | |
| 49 | 54 | djnz .top | |
| 50 | 55 | | |
| 51 | - | ld hl, $5AE0 ; bottom wall — row 23 | |
| 56 | + | ld hl, $5AE0 | |
| 52 | 57 | ld b, 32 | |
| 53 | 58 | .bottom: | |
| 54 | 59 | ld (hl), WALL | |
| 55 | 60 | inc hl | |
| 56 | 61 | djnz .bottom | |
| 57 | 62 | | |
| 58 | - | ld hl, $5820 ; sides, rows 1..23 | |
| 63 | + | ld hl, $5820 | |
| 59 | 64 | ld b, 23 | |
| 60 | 65 | .sides: | |
| 61 | 66 | ld (hl), WALL | |
| ... | |||
| 72 | 77 | call draw_lamps | |
| 73 | 78 | call save_under | |
| 74 | 79 | call draw_lamp | |
| 80 | + | call save_draught ; the draught starts up too | |
| 81 | + | call draw_draught | |
| 75 | 82 | | |
| 76 | 83 | ; ============================================================================ | |
| 77 | - | ; THE HEARTBEAT — move, light, tally, and check for the win. | |
| 84 | + | ; THE HEARTBEAT — step the player, then step the draught. | |
| 78 | 85 | ; ============================================================================ | |
| 79 | 86 | im 1 | |
| 80 | 87 | ei | |
| 81 | 88 | | |
| 82 | 89 | game_loop: | |
| 83 | 90 | 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 | |
| 84 | 97 | | |
| 98 | + | ; ---------------------------------------------------------------------------- | |
| 99 | + | ; player_step — read QAOP, move the lamplighter if clear, light lamps. | |
| 100 | + | ; ---------------------------------------------------------------------------- | |
| 101 | + | player_step: | |
| 85 | 102 | ld a, (lamp_col) | |
| 86 | 103 | ld (tcol), a | |
| 87 | 104 | ld a, (lamp_row) | |
| ... | |||
| 90 | 107 | ld bc, KEYS_OP | |
| 91 | 108 | in a, (c) | |
| 92 | 109 | bit 1, a | |
| 93 | - | jr z, .left | |
| 110 | + | jr z, .pleft | |
| 94 | 111 | bit 0, a | |
| 95 | - | jr z, .right | |
| 112 | + | jr z, .pright | |
| 96 | 113 | ld bc, KEYS_Q | |
| 97 | 114 | in a, (c) | |
| 98 | 115 | bit 0, a | |
| 99 | - | jr z, .up | |
| 116 | + | jr z, .pup | |
| 100 | 117 | ld bc, KEYS_A | |
| 101 | 118 | in a, (c) | |
| 102 | 119 | bit 0, a | |
| 103 | - | jr z, .down | |
| 104 | - | jr game_loop | |
| 120 | + | jr z, .pdown | |
| 121 | + | ret ; nothing held | |
| 105 | 122 | | |
| 106 | - | .left: | |
| 123 | + | .pleft: | |
| 107 | 124 | ld hl, tcol | |
| 108 | 125 | dec (hl) | |
| 109 | - | jr .try | |
| 110 | - | .right: | |
| 126 | + | jr .pmove | |
| 127 | + | .pright: | |
| 111 | 128 | ld hl, tcol | |
| 112 | 129 | inc (hl) | |
| 113 | - | jr .try | |
| 114 | - | .up: | |
| 130 | + | jr .pmove | |
| 131 | + | .pup: | |
| 115 | 132 | ld hl, trow | |
| 116 | 133 | dec (hl) | |
| 117 | - | jr .try | |
| 118 | - | .down: | |
| 134 | + | jr .pmove | |
| 135 | + | .pdown: | |
| 119 | 136 | ld hl, trow | |
| 120 | 137 | inc (hl) | |
| 121 | - | .try: | |
| 138 | + | .pmove: | |
| 122 | 139 | ld a, (trow) | |
| 123 | 140 | ld b, a | |
| 124 | 141 | ld a, (tcol) | |
| 125 | 142 | ld c, a | |
| 126 | 143 | call wall_at | |
| 127 | - | jr nz, game_loop | |
| 144 | + | ret nz ; blocked | |
| 128 | 145 | | |
| 129 | 146 | call restore_under | |
| 130 | 147 | ld a, (tcol) | |
| ... | |||
| 132 | 149 | ld a, (trow) | |
| 133 | 150 | ld (lamp_row), a | |
| 134 | 151 | call save_under | |
| 135 | - | | |
| 136 | 152 | ld a, (under_lamp + 8) | |
| 137 | 153 | cp LAMP_UNLIT | |
| 138 | - | jr nz, .not_lamp | |
| 154 | + | jr nz, .pdrawn | |
| 139 | 155 | ld a, LAMP_LIT | |
| 140 | 156 | ld (under_lamp + 8), a | |
| 141 | 157 | call light_pip | |
| 142 | - | .not_lamp: | |
| 158 | + | .pdrawn: | |
| 143 | 159 | call draw_lamp | |
| 160 | + | ret | |
| 144 | 161 | | |
| 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 | |
| 149 | 216 | | |
| 150 | 217 | ; ---------------------------------------------------------------------------- | |
| 151 | - | ; win — reveal the last lamp, print the closing line, hold the end state. | |
| 218 | + | ; win / draw_message / print_char (Unit 12). | |
| 152 | 219 | ; ---------------------------------------------------------------------------- | |
| 153 | 220 | win: | |
| 154 | - | call restore_under ; the lamplighter steps aside; last lamp shows | |
| 221 | + | call restore_under | |
| 155 | 222 | call draw_message | |
| 156 | 223 | .hold: | |
| 157 | 224 | halt | |
| 158 | 225 | jr .hold | |
| 159 | 226 | | |
| 160 | - | ; ---------------------------------------------------------------------------- | |
| 161 | - | ; draw_message — print msg_text from (MSG_ROW, MSG_COL), ended by $FF. | |
| 162 | - | ; ---------------------------------------------------------------------------- | |
| 163 | 227 | draw_message: | |
| 164 | 228 | ld hl, msg_text | |
| 165 | 229 | ld c, MSG_COL | |
| ... | |||
| 169 | 233 | ret z | |
| 170 | 234 | push hl | |
| 171 | 235 | ld b, MSG_ROW | |
| 172 | - | call print_char ; A=char, B=row, C=col; preserves C | |
| 236 | + | call print_char | |
| 173 | 237 | pop hl | |
| 174 | 238 | inc hl | |
| 175 | - | inc c ; next column | |
| 239 | + | inc c | |
| 176 | 240 | jr .dm | |
| 177 | 241 | | |
| 178 | - | ; ---------------------------------------------------------------------------- | |
| 179 | - | ; print_char — A=char code, B=row, C=col. Copy the ROM-font glyph into the cell. | |
| 180 | - | ; ---------------------------------------------------------------------------- | |
| 181 | 242 | print_char: | |
| 182 | - | ld l, a ; HL = code * 8 | |
| 243 | + | ld l, a | |
| 183 | 244 | ld h, 0 | |
| 184 | 245 | add hl, hl | |
| 185 | 246 | add hl, hl | |
| 186 | 247 | add hl, hl | |
| 187 | 248 | ld de, FONT | |
| 188 | 249 | add hl, de | |
| 189 | - | ex de, hl ; DE = glyph address in ROM | |
| 250 | + | ex de, hl | |
| 190 | 251 | push de | |
| 191 | - | call attr_addr_cr ; HL = attribute of (B,C); BC preserved | |
| 252 | + | call attr_addr_cr | |
| 192 | 253 | 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 | |
| 195 | 256 | ld b, 8 | |
| 196 | 257 | .pc: | |
| 197 | 258 | ld a, (de) | |
| ... | |||
| 258 | 319 | ret | |
| 259 | 320 | | |
| 260 | 321 | ; ---------------------------------------------------------------------------- | |
| 261 | - | ; scr_addr_cr / attr_addr_cr / wall_at / pos_bc (Unit 8). | |
| 322 | + | ; scr_addr_cr / attr_addr_cr / wall_at (Unit 8). | |
| 262 | 323 | ; ---------------------------------------------------------------------------- | |
| 263 | 324 | scr_addr_cr: | |
| 264 | 325 | ld a, b | |
| ... | |||
| 296 | 357 | bit WALL_BIT, (hl) | |
| 297 | 358 | ret | |
| 298 | 359 | | |
| 360 | + | ; ---------------------------------------------------------------------------- | |
| 361 | + | ; The lamplighter's save / restore / draw (Unit 8). | |
| 362 | + | ; ---------------------------------------------------------------------------- | |
| 299 | 363 | pos_bc: | |
| 300 | 364 | ld a, (lamp_row) | |
| 301 | 365 | ld b, a | |
| ... | |||
| 303 | 367 | ld c, a | |
| 304 | 368 | ret | |
| 305 | 369 | | |
| 306 | - | ; ---------------------------------------------------------------------------- | |
| 307 | - | ; save_under / restore_under / draw_lamp (Unit 8). | |
| 308 | - | ; ---------------------------------------------------------------------------- | |
| 309 | 370 | save_under: | |
| 310 | 371 | call pos_bc | |
| 311 | 372 | call scr_addr_cr | |
| ... | |||
| 357 | 418 | ret | |
| 358 | 419 | | |
| 359 | 420 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 361 | 482 | ; ---------------------------------------------------------------------------- | |
| 362 | 483 | lamp_data: | |
| 363 | 484 | defb 4, 3 | |
| ... | |||
| 380 | 501 | defb 0 | |
| 381 | 502 | lit_count: | |
| 382 | 503 | 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 | |
| 383 | 515 | | |
| 384 | 516 | under_lamp: | |
| 517 | + | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 518 | + | under_draught: | |
| 385 | 519 | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 386 | 520 | | |
| 387 | 521 | lamplighter: | |
| ... | |||
| 400 | 534 | defb %01111110 | |
| 401 | 535 | defb %01111110 | |
| 402 | 536 | defb %01011010 | |
| 537 | + | defb %01111110 | |
| 538 | + | defb %01111110 | |
| 539 | + | defb %00111100 | |
| 540 | + | | |
| 541 | + | draught_glyph: | |
| 542 | + | defb %00000000 | |
| 543 | + | defb %00111100 | |
| 403 | 544 | defb %01111110 | |
| 545 | + | defb %11111111 | |
| 546 | + | defb %11111111 | |
| 404 | 547 | defb %01111110 | |
| 405 | 548 | defb %00111100 | |
| 549 | + | defb %00000000 | |
| 406 | 550 | | |
| 407 | 551 | msg_text: | |
| 408 | 552 | 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:
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_stepisn't called. Check thedec/ 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 confirmnegflips the direction byte. - It leaves a trail of wisps.
restore_draughtisn'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_atcollision. - 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.