Lives, and the Fall of Night
Phase D's finale. The draught and the lamplighter can now collide — each hit costs a life from a row of pips — and when the lives run out, night falls: the screen goes dark and the game ends. Gloaming can be lost as well as won.
The draught can undo your work, but it still can't touch you. This unit lets it — and gives the game its stakes. The two can collide, each collision costs a life, and when the lives run out, night falls: the square goes dark and the game ends. With this, Gloaming becomes a real game, one you can lose as well as win — and that closes Phase D.
Where we start
Unit 14's contest — the draught snuffs lamps, but passes through the lamplighter harmlessly. We make the two of them dangerous to each other.
Two sprites, one cell: collision
A collision is the question are the lamplighter and the draught trying to stand on the same cell? — a comparison of their columns and rows. But there's a trap. If we let them overlap, their save/restore buffers tangle: each sprite assumes it's the only thing on its cell, and two sharing one cell corrupts what each thinks is "underneath".
So we don't let them overlap. Instead, a move onto the other's cell is treated like a move into a wall — refused — and counted as a hit. We check it both ways: when the lamplighter steps toward the draught, and when the draught drifts toward the lamplighter.
; about to move onto the other's cell?
ld a, (tcol)
ld hl, draught_col
cp (hl)
jr nz, .clear
ld a, (trow)
ld hl, draught_row
cp (hl)
jr nz, .clear
call lose_life ; same cell — collision, don't move
They never share a cell, so the engine stays clean. (Drawing two sprites that genuinely overlap — compositing one over the other — is a real technique a later game builds. Here we sidestep it, and the game's none the poorer.)
Lives, as pips again
Lives are shown the way everything is shown in this game: as coloured cells. Three red pips sit at the right of the HUD, and lose_life removes one each hit — the lamp tally's machinery, run in reverse. While lives remain, a hit sends the lamplighter back to the start, clear of the draught, and play continues. The reset is also what keeps the two sprites apart after a collision, with no overlap to clean up.
The fall of night
When the last life goes, lose_life jumps to lose, and night takes the square. It's the win's mirror image: where the win prints its line over the lit board, the loss first washes every cell to black — one LDIR, the whole attribute map gone dark — then prints NIGHT FALLS into the void, and holds. Two end states now, one bright and one dark, each a place the game comes to rest.
Milestone — the lose state
We add the same-cell check to both player_step and draught_step, a lose_life that drains a red pip and resets the lamplighter (or jumps to lose at zero), and a lose routine that blacks the board and prints NIGHT FALLS.
| 1 | 1 | ; Gloaming — Unit 15: Lives, and the Fall of Night | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 is Unit 14's contest — the draught snuffs lamps, but can't touch you. | |
| 3 | + | ; step-01 adds collision, lives, and the lose state — night falls at zero lives. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 18 | 18 | PIP_LIT equ %01110000 | |
| 19 | 19 | PIP_BASE equ $5800 + 12 | |
| 20 | 20 | NUM_LAMPS equ 8 | |
| 21 | + | | |
| 22 | + | LIVES equ 3 | |
| 23 | + | LIFE_PIP equ %01010000 ; BRIGHT, PAPER red — a life | |
| 24 | + | LIFE_BASE equ $5800 + 28 ; row 0, columns 28-30 | |
| 21 | 25 | | |
| 22 | 26 | MSG_ATTR equ %01000111 | |
| 23 | 27 | MSG_ROW equ 11 | |
| 24 | - | MSG_COL equ 7 | |
| 28 | + | WIN_COL equ 7 | |
| 29 | + | LOSE_COL equ 10 | |
| 25 | 30 | FONT equ $3C00 | |
| 26 | 31 | | |
| 27 | 32 | START_COL equ 15 | |
| 28 | 33 | START_ROW equ 11 | |
| 29 | - | DRAUGHT_COL0 equ 18 ; its diagonal now crosses the lamp at (22,7) | |
| 34 | + | DRAUGHT_COL0 equ 18 | |
| 30 | 35 | DRAUGHT_ROW0 equ 3 | |
| 31 | 36 | | |
| 32 | 37 | KEYS_OP equ $DFFE | |
| ... | |||
| 74 | 79 | djnz .sides | |
| 75 | 80 | | |
| 76 | 81 | call draw_pips | |
| 82 | + | call draw_lives | |
| 77 | 83 | call draw_lamps | |
| 78 | 84 | call save_under | |
| 79 | 85 | call draw_lamp | |
| ... | |||
| 96 | 102 | jr game_loop | |
| 97 | 103 | | |
| 98 | 104 | ; ---------------------------------------------------------------------------- | |
| 99 | - | ; player_step — move the lamplighter, light lamps (Unit 13). | |
| 105 | + | ; player_step — move the lamplighter; a step onto the draught costs a life. | |
| 100 | 106 | ; ---------------------------------------------------------------------------- | |
| 101 | 107 | player_step: | |
| 102 | 108 | ld a, (lamp_col) | |
| ... | |||
| 141 | 147 | ld a, (tcol) | |
| 142 | 148 | ld c, a | |
| 143 | 149 | call wall_at | |
| 144 | - | ret nz | |
| 150 | + | ret nz ; wall blocks | |
| 151 | + | | |
| 152 | + | ; would the step land on the draught? then it's a collision | |
| 153 | + | ld a, (tcol) | |
| 154 | + | ld hl, draught_col | |
| 155 | + | cp (hl) | |
| 156 | + | jr nz, .pcommit | |
| 157 | + | ld a, (trow) | |
| 158 | + | ld hl, draught_row | |
| 159 | + | cp (hl) | |
| 160 | + | jr nz, .pcommit | |
| 161 | + | call lose_life | |
| 162 | + | ret | |
| 145 | 163 | | |
| 164 | + | .pcommit: | |
| 146 | 165 | call restore_under | |
| 147 | 166 | ld a, (tcol) | |
| 148 | 167 | ld (lamp_col), a | |
| ... | |||
| 160 | 179 | ret | |
| 161 | 180 | | |
| 162 | 181 | ; ---------------------------------------------------------------------------- | |
| 163 | - | ; draught_step — drift, bounce, and SNUFF any lit lamp stepped onto. | |
| 182 | + | ; draught_step — drift, bounce, snuff; a step onto the lamplighter costs a life. | |
| 164 | 183 | ; ---------------------------------------------------------------------------- | |
| 165 | 184 | draught_step: | |
| 166 | 185 | ld a, (draught_timer) | |
| ... | |||
| 196 | 215 | neg | |
| 197 | 216 | ld (draught_dy), a | |
| 198 | 217 | .vok: | |
| 199 | - | call restore_draught | |
| 218 | + | ; work out the target cell | |
| 200 | 219 | ld a, (draught_col) | |
| 201 | 220 | ld b, a | |
| 202 | 221 | ld a, (draught_dx) | |
| 203 | 222 | add a, b | |
| 204 | - | ld (draught_col), a | |
| 223 | + | ld (dtcol), a | |
| 205 | 224 | ld a, (draught_row) | |
| 206 | 225 | ld b, a | |
| 207 | 226 | ld a, (draught_dy) | |
| 208 | 227 | add a, b | |
| 228 | + | ld (dtrow), a | |
| 229 | + | | |
| 230 | + | ; would it land on the lamplighter? then it's a collision | |
| 231 | + | ld a, (dtcol) | |
| 232 | + | ld hl, lamp_col | |
| 233 | + | cp (hl) | |
| 234 | + | jr nz, .dmove | |
| 235 | + | ld a, (dtrow) | |
| 236 | + | ld hl, lamp_row | |
| 237 | + | cp (hl) | |
| 238 | + | jr nz, .dmove | |
| 239 | + | call lose_life | |
| 240 | + | ret ; don't move onto him | |
| 241 | + | | |
| 242 | + | .dmove: | |
| 243 | + | call restore_draught | |
| 244 | + | ld a, (dtcol) | |
| 245 | + | ld (draught_col), a | |
| 246 | + | ld a, (dtrow) | |
| 209 | 247 | ld (draught_row), a | |
| 210 | 248 | call save_draught | |
| 211 | - | | |
| 212 | - | ; --- snuff it: if the saved cell is a lit lamp, cool it --- | |
| 213 | 249 | ld a, (under_draught + 8) | |
| 214 | 250 | cp LAMP_LIT | |
| 215 | 251 | jr nz, .nosnuff | |
| 216 | 252 | ld a, LAMP_UNLIT | |
| 217 | - | ld (under_draught + 8), a ; restored cold when the draught leaves | |
| 253 | + | ld (under_draught + 8), a | |
| 218 | 254 | call unlight_pip | |
| 219 | 255 | .nosnuff: | |
| 220 | 256 | call draw_draught | |
| 221 | 257 | ret | |
| 222 | 258 | | |
| 223 | 259 | ; ---------------------------------------------------------------------------- | |
| 224 | - | ; unlight_pip — drop the tally by one and cool the top pip (mirror of light_pip). | |
| 260 | + | ; lose_life — drop a life pip; reset the lamplighter, or fall to night. | |
| 225 | 261 | ; ---------------------------------------------------------------------------- | |
| 226 | - | unlight_pip: | |
| 227 | - | ld a, (lit_count) | |
| 262 | + | lose_life: | |
| 263 | + | ld a, (lives) | |
| 228 | 264 | dec a | |
| 229 | - | ld (lit_count), a ; new, lower count | |
| 230 | - | ld e, a ; index of the pip that was on top | |
| 265 | + | ld (lives), a | |
| 266 | + | ld e, a ; index of the life pip to remove | |
| 231 | 267 | ld d, 0 | |
| 232 | - | ld hl, PIP_BASE | |
| 268 | + | ld hl, LIFE_BASE | |
| 233 | 269 | add hl, de | |
| 234 | - | ld (hl), PIP_UNLIT | |
| 270 | + | ld (hl), COBBLE ; the pip goes dark | |
| 271 | + | ld a, (lives) | |
| 272 | + | or a | |
| 273 | + | jp z, lose ; out of lives — night falls | |
| 274 | + | | |
| 275 | + | ; otherwise send the lamplighter back to the start, clear of danger | |
| 276 | + | call restore_under | |
| 277 | + | ld a, START_COL | |
| 278 | + | ld (lamp_col), a | |
| 279 | + | ld a, START_ROW | |
| 280 | + | ld (lamp_row), a | |
| 281 | + | call save_under | |
| 282 | + | call draw_lamp | |
| 235 | 283 | ret | |
| 236 | 284 | | |
| 237 | 285 | ; ---------------------------------------------------------------------------- | |
| 238 | - | ; win / draw_message / print_char (Unit 12). | |
| 286 | + | ; win / lose — each prints a line and holds. | |
| 239 | 287 | ; ---------------------------------------------------------------------------- | |
| 240 | 288 | win: | |
| 241 | 289 | call restore_under | |
| 242 | - | call draw_message | |
| 243 | - | .hold: | |
| 290 | + | ld hl, win_text | |
| 291 | + | ld b, MSG_ROW | |
| 292 | + | ld c, WIN_COL | |
| 293 | + | call print_string | |
| 294 | + | .whold: | |
| 244 | 295 | halt | |
| 245 | - | jr .hold | |
| 296 | + | jr .whold | |
| 246 | 297 | | |
| 247 | - | draw_message: | |
| 248 | - | ld hl, msg_text | |
| 249 | - | ld c, MSG_COL | |
| 250 | - | .dm: | |
| 298 | + | lose: | |
| 299 | + | ld hl, $5800 ; night falls — wash the square to black | |
| 300 | + | ld de, $5801 | |
| 301 | + | ld (hl), %00000000 | |
| 302 | + | ld bc, 767 | |
| 303 | + | ldir | |
| 304 | + | ld hl, lose_text | |
| 305 | + | ld b, MSG_ROW | |
| 306 | + | ld c, LOSE_COL | |
| 307 | + | call print_string | |
| 308 | + | .lhold: | |
| 309 | + | halt | |
| 310 | + | jr .lhold | |
| 311 | + | | |
| 312 | + | ; ---------------------------------------------------------------------------- | |
| 313 | + | ; print_string — HL=string ($FF-terminated), B=row, C=col. | |
| 314 | + | ; ---------------------------------------------------------------------------- | |
| 315 | + | print_string: | |
| 316 | + | .ps: | |
| 251 | 317 | ld a, (hl) | |
| 252 | 318 | cp $FF | |
| 253 | 319 | ret z | |
| 254 | 320 | push hl | |
| 255 | - | ld b, MSG_ROW | |
| 321 | + | push bc | |
| 256 | 322 | call print_char | |
| 323 | + | pop bc | |
| 257 | 324 | pop hl | |
| 258 | 325 | inc hl | |
| 259 | 326 | inc c | |
| 260 | - | jr .dm | |
| 327 | + | jr .ps | |
| 261 | 328 | | |
| 262 | 329 | print_char: | |
| 263 | 330 | ld l, a | |
| ... | |||
| 283 | 350 | ret | |
| 284 | 351 | | |
| 285 | 352 | ; ---------------------------------------------------------------------------- | |
| 286 | - | ; light_pip / draw_pips (Unit 11). | |
| 353 | + | ; light_pip / unlight_pip / draw_pips / draw_lives. | |
| 287 | 354 | ; ---------------------------------------------------------------------------- | |
| 288 | 355 | light_pip: | |
| 289 | 356 | ld a, (lit_count) | |
| ... | |||
| 294 | 361 | ld hl, PIP_BASE | |
| 295 | 362 | add hl, de | |
| 296 | 363 | ld (hl), PIP_LIT | |
| 364 | + | ret | |
| 365 | + | | |
| 366 | + | unlight_pip: | |
| 367 | + | ld a, (lit_count) | |
| 368 | + | dec a | |
| 369 | + | ld (lit_count), a | |
| 370 | + | ld e, a | |
| 371 | + | ld d, 0 | |
| 372 | + | ld hl, PIP_BASE | |
| 373 | + | add hl, de | |
| 374 | + | ld (hl), PIP_UNLIT | |
| 297 | 375 | ret | |
| 298 | 376 | | |
| 299 | 377 | draw_pips: | |
| ... | |||
| 304 | 382 | ld (hl), a | |
| 305 | 383 | inc hl | |
| 306 | 384 | djnz .dp | |
| 385 | + | ret | |
| 386 | + | | |
| 387 | + | draw_lives: | |
| 388 | + | ld hl, LIFE_BASE | |
| 389 | + | ld b, LIVES | |
| 390 | + | ld a, LIFE_PIP | |
| 391 | + | .dlv: | |
| 392 | + | ld (hl), a | |
| 393 | + | inc hl | |
| 394 | + | djnz .dlv | |
| 307 | 395 | ret | |
| 308 | 396 | | |
| 309 | 397 | ; ---------------------------------------------------------------------------- | |
| 310 | - | ; draw_lamps / draw_lantern (Unit 9). | |
| 398 | + | ; draw_lamps / draw_lantern. | |
| 311 | 399 | ; ---------------------------------------------------------------------------- | |
| 312 | 400 | draw_lamps: | |
| 313 | 401 | ld hl, lamp_data | |
| ... | |||
| 339 | 427 | ret | |
| 340 | 428 | | |
| 341 | 429 | ; ---------------------------------------------------------------------------- | |
| 342 | - | ; scr_addr_cr / attr_addr_cr / wall_at (Unit 8). | |
| 430 | + | ; scr_addr_cr / attr_addr_cr / wall_at. | |
| 343 | 431 | ; ---------------------------------------------------------------------------- | |
| 344 | 432 | scr_addr_cr: | |
| 345 | 433 | ld a, b | |
| ... | |||
| 378 | 466 | ret | |
| 379 | 467 | | |
| 380 | 468 | ; ---------------------------------------------------------------------------- | |
| 381 | - | ; The lamplighter's save / restore / draw (Unit 8). | |
| 469 | + | ; The lamplighter's save / restore / draw. | |
| 382 | 470 | ; ---------------------------------------------------------------------------- | |
| 383 | 471 | pos_bc: | |
| 384 | 472 | ld a, (lamp_row) | |
| ... | |||
| 438 | 526 | ret | |
| 439 | 527 | | |
| 440 | 528 | ; ---------------------------------------------------------------------------- | |
| 441 | - | ; The draught's save / restore / draw (Unit 13). | |
| 529 | + | ; The draught's save / restore / draw. | |
| 442 | 530 | ; ---------------------------------------------------------------------------- | |
| 443 | 531 | dpos_bc: | |
| 444 | 532 | ld a, (draught_row) | |
| ... | |||
| 521 | 609 | defb 0 | |
| 522 | 610 | lit_count: | |
| 523 | 611 | defb 0 | |
| 612 | + | lives: | |
| 613 | + | defb LIVES | |
| 524 | 614 | | |
| 525 | 615 | draught_col: | |
| 526 | 616 | defb DRAUGHT_COL0 | |
| ... | |||
| 532 | 622 | defb 1 | |
| 533 | 623 | draught_timer: | |
| 534 | 624 | defb DRAUGHT_SPEED | |
| 625 | + | dtcol: | |
| 626 | + | defb 0 | |
| 627 | + | dtrow: | |
| 628 | + | defb 0 | |
| 535 | 629 | | |
| 536 | 630 | under_lamp: | |
| 537 | 631 | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| ... | |||
| 568 | 662 | defb %00111100 | |
| 569 | 663 | defb %00000000 | |
| 570 | 664 | | |
| 571 | - | msg_text: | |
| 665 | + | win_text: | |
| 572 | 666 | defb "THE NIGHT IS HELD" | |
| 667 | + | defb $FF | |
| 668 | + | lose_text: | |
| 669 | + | defb "NIGHT FALLS" | |
| 573 | 670 | defb $FF | |
| 574 | 671 | | |
| 575 | 672 | end start |
The complete program
; Gloaming — Unit 15: Lives, and the Fall of Night
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 adds collision, lives, and the lose state — night falls at zero lives.
org 32768
COBBLE equ %00000001
WALL equ %00001111
LAMP_ATTR equ %01000111
LAMP_UNLIT equ %00000101
LAMP_LIT equ %01000110
WALL_BIT equ 3
DRAUGHT_ATTR equ %01000101
DRAUGHT_SPEED equ 8
PIP_UNLIT equ %00101000
PIP_LIT equ %01110000
PIP_BASE equ $5800 + 12
NUM_LAMPS equ 8
LIVES equ 3
LIFE_PIP equ %01010000 ; BRIGHT, PAPER red — a life
LIFE_BASE equ $5800 + 28 ; row 0, columns 28-30
MSG_ATTR equ %01000111
MSG_ROW equ 11
WIN_COL equ 7
LOSE_COL equ 10
FONT equ $3C00
START_COL equ 15
START_ROW equ 11
DRAUGHT_COL0 equ 18
DRAUGHT_ROW0 equ 3
KEYS_OP equ $DFFE
KEYS_Q equ $FBFE
KEYS_A equ $FDFE
; ============================================================================
; SETUP — runs once.
; ============================================================================
start:
ld a, 0
out ($FE), a
ld hl, $5800
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
ld hl, $5820
ld b, 32
.top:
ld (hl), WALL
inc hl
djnz .top
ld hl, $5AE0
ld b, 32
.bottom:
ld (hl), WALL
inc hl
djnz .bottom
ld hl, $5820
ld b, 23
.sides:
ld (hl), WALL
push hl
ld de, 31
add hl, de
ld (hl), WALL
pop hl
ld de, 32
add hl, de
djnz .sides
call draw_pips
call draw_lives
call draw_lamps
call save_under
call draw_lamp
call save_draught
call draw_draught
; ============================================================================
; THE HEARTBEAT.
; ============================================================================
im 1
ei
game_loop:
halt
call player_step
call draught_step
ld a, (lit_count)
cp NUM_LAMPS
jp z, win
jr game_loop
; ----------------------------------------------------------------------------
; player_step — move the lamplighter; a step onto the draught costs a life.
; ----------------------------------------------------------------------------
player_step:
ld a, (lamp_col)
ld (tcol), a
ld a, (lamp_row)
ld (trow), a
ld bc, KEYS_OP
in a, (c)
bit 1, a
jr z, .pleft
bit 0, a
jr z, .pright
ld bc, KEYS_Q
in a, (c)
bit 0, a
jr z, .pup
ld bc, KEYS_A
in a, (c)
bit 0, a
jr z, .pdown
ret
.pleft:
ld hl, tcol
dec (hl)
jr .pmove
.pright:
ld hl, tcol
inc (hl)
jr .pmove
.pup:
ld hl, trow
dec (hl)
jr .pmove
.pdown:
ld hl, trow
inc (hl)
.pmove:
ld a, (trow)
ld b, a
ld a, (tcol)
ld c, a
call wall_at
ret nz ; wall blocks
; would the step land on the draught? then it's a collision
ld a, (tcol)
ld hl, draught_col
cp (hl)
jr nz, .pcommit
ld a, (trow)
ld hl, draught_row
cp (hl)
jr nz, .pcommit
call lose_life
ret
.pcommit:
call restore_under
ld a, (tcol)
ld (lamp_col), a
ld a, (trow)
ld (lamp_row), a
call save_under
ld a, (under_lamp + 8)
cp LAMP_UNLIT
jr nz, .pdrawn
ld a, LAMP_LIT
ld (under_lamp + 8), a
call light_pip
.pdrawn:
call draw_lamp
ret
; ----------------------------------------------------------------------------
; draught_step — drift, bounce, snuff; a step onto the lamplighter costs a life.
; ----------------------------------------------------------------------------
draught_step:
ld a, (draught_timer)
dec a
ld (draught_timer), a
ret nz
ld a, DRAUGHT_SPEED
ld (draught_timer), a
ld a, (draught_col)
ld b, a
ld a, (draught_dx)
add a, b
ld c, a
ld a, (draught_row)
ld b, a
call wall_at
jr z, .hok
ld a, (draught_dx)
neg
ld (draught_dx), a
.hok:
ld a, (draught_row)
ld b, a
ld a, (draught_dy)
add a, b
ld b, a
ld a, (draught_col)
ld c, a
call wall_at
jr z, .vok
ld a, (draught_dy)
neg
ld (draught_dy), a
.vok:
; work out the target cell
ld a, (draught_col)
ld b, a
ld a, (draught_dx)
add a, b
ld (dtcol), a
ld a, (draught_row)
ld b, a
ld a, (draught_dy)
add a, b
ld (dtrow), a
; would it land on the lamplighter? then it's a collision
ld a, (dtcol)
ld hl, lamp_col
cp (hl)
jr nz, .dmove
ld a, (dtrow)
ld hl, lamp_row
cp (hl)
jr nz, .dmove
call lose_life
ret ; don't move onto him
.dmove:
call restore_draught
ld a, (dtcol)
ld (draught_col), a
ld a, (dtrow)
ld (draught_row), a
call save_draught
ld a, (under_draught + 8)
cp LAMP_LIT
jr nz, .nosnuff
ld a, LAMP_UNLIT
ld (under_draught + 8), a
call unlight_pip
.nosnuff:
call draw_draught
ret
; ----------------------------------------------------------------------------
; lose_life — drop a life pip; reset the lamplighter, or fall to night.
; ----------------------------------------------------------------------------
lose_life:
ld a, (lives)
dec a
ld (lives), a
ld e, a ; index of the life pip to remove
ld d, 0
ld hl, LIFE_BASE
add hl, de
ld (hl), COBBLE ; the pip goes dark
ld a, (lives)
or a
jp z, lose ; out of lives — night falls
; otherwise send the lamplighter back to the start, clear of danger
call restore_under
ld a, START_COL
ld (lamp_col), a
ld a, START_ROW
ld (lamp_row), a
call save_under
call draw_lamp
ret
; ----------------------------------------------------------------------------
; win / lose — each prints a line and holds.
; ----------------------------------------------------------------------------
win:
call restore_under
ld hl, win_text
ld b, MSG_ROW
ld c, WIN_COL
call print_string
.whold:
halt
jr .whold
lose:
ld hl, $5800 ; night falls — wash the square to black
ld de, $5801
ld (hl), %00000000
ld bc, 767
ldir
ld hl, lose_text
ld b, MSG_ROW
ld c, LOSE_COL
call print_string
.lhold:
halt
jr .lhold
; ----------------------------------------------------------------------------
; print_string — HL=string ($FF-terminated), B=row, C=col.
; ----------------------------------------------------------------------------
print_string:
.ps:
ld a, (hl)
cp $FF
ret z
push hl
push bc
call print_char
pop bc
pop hl
inc hl
inc c
jr .ps
print_char:
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
ld de, FONT
add hl, de
ex de, hl
push de
call attr_addr_cr
ld (hl), MSG_ATTR
call scr_addr_cr
pop de
ld b, 8
.pc:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .pc
ret
; ----------------------------------------------------------------------------
; light_pip / unlight_pip / draw_pips / draw_lives.
; ----------------------------------------------------------------------------
light_pip:
ld a, (lit_count)
ld e, a
ld d, 0
inc a
ld (lit_count), a
ld hl, PIP_BASE
add hl, de
ld (hl), PIP_LIT
ret
unlight_pip:
ld a, (lit_count)
dec a
ld (lit_count), a
ld e, a
ld d, 0
ld hl, PIP_BASE
add hl, de
ld (hl), PIP_UNLIT
ret
draw_pips:
ld hl, PIP_BASE
ld b, NUM_LAMPS
ld a, PIP_UNLIT
.dp:
ld (hl), a
inc hl
djnz .dp
ret
draw_lives:
ld hl, LIFE_BASE
ld b, LIVES
ld a, LIFE_PIP
.dlv:
ld (hl), a
inc hl
djnz .dlv
ret
; ----------------------------------------------------------------------------
; draw_lamps / draw_lantern.
; ----------------------------------------------------------------------------
draw_lamps:
ld hl, lamp_data
.next:
ld a, (hl)
cp $FF
ret z
ld c, a
inc hl
ld b, (hl)
inc hl
push hl
call draw_lantern
pop hl
jr .next
draw_lantern:
call attr_addr_cr
ld (hl), LAMP_UNLIT
call scr_addr_cr
ld de, lantern
ld b, 8
.dlt:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dlt
ret
; ----------------------------------------------------------------------------
; scr_addr_cr / attr_addr_cr / wall_at.
; ----------------------------------------------------------------------------
scr_addr_cr:
ld a, b
and %00011000
or %01000000
ld h, a
ld a, b
and %00000111
rrca
rrca
rrca
or c
ld l, a
ret
attr_addr_cr:
ld a, b
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
add hl, hl
add hl, hl
ld de, $5800
add hl, de
ld a, c
ld e, a
ld d, 0
add hl, de
ret
wall_at:
call attr_addr_cr
bit WALL_BIT, (hl)
ret
; ----------------------------------------------------------------------------
; The lamplighter's save / restore / draw.
; ----------------------------------------------------------------------------
pos_bc:
ld a, (lamp_row)
ld b, a
ld a, (lamp_col)
ld c, a
ret
save_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.su:
ld a, (hl)
ld (de), a
inc de
inc h
djnz .su
call pos_bc
call attr_addr_cr
ld a, (hl)
ld (under_lamp + 8), a
ret
restore_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.ru:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .ru
call pos_bc
call attr_addr_cr
ld a, (under_lamp + 8)
ld (hl), a
ret
draw_lamp:
call pos_bc
call attr_addr_cr
ld (hl), LAMP_ATTR
call pos_bc
call scr_addr_cr
ld de, lamplighter
ld b, 8
.dl:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dl
ret
; ----------------------------------------------------------------------------
; The draught's save / restore / draw.
; ----------------------------------------------------------------------------
dpos_bc:
ld a, (draught_row)
ld b, a
ld a, (draught_col)
ld c, a
ret
save_draught:
call dpos_bc
call scr_addr_cr
ld de, under_draught
ld b, 8
.sd:
ld a, (hl)
ld (de), a
inc de
inc h
djnz .sd
call dpos_bc
call attr_addr_cr
ld a, (hl)
ld (under_draught + 8), a
ret
restore_draught:
call dpos_bc
call scr_addr_cr
ld de, under_draught
ld b, 8
.rd:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .rd
call dpos_bc
call attr_addr_cr
ld a, (under_draught + 8)
ld (hl), a
ret
draw_draught:
call dpos_bc
call attr_addr_cr
ld (hl), DRAUGHT_ATTR
call dpos_bc
call scr_addr_cr
ld de, draught_glyph
ld b, 8
.dd:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dd
ret
; ----------------------------------------------------------------------------
; Level data, state, buffers, and shapes.
; ----------------------------------------------------------------------------
lamp_data:
defb 4, 3
defb 27, 3
defb 9, 7
defb 22, 7
defb 6, 15
defb 25, 15
defb 13, 20
defb 18, 20
defb $FF
lamp_col:
defb START_COL
lamp_row:
defb START_ROW
tcol:
defb 0
trow:
defb 0
lit_count:
defb 0
lives:
defb LIVES
draught_col:
defb DRAUGHT_COL0
draught_row:
defb DRAUGHT_ROW0
draught_dx:
defb 1
draught_dy:
defb 1
draught_timer:
defb DRAUGHT_SPEED
dtcol:
defb 0
dtrow:
defb 0
under_lamp:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
under_draught:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
lantern:
defb %00011000
defb %00100100
defb %01111110
defb %01111110
defb %01011010
defb %01111110
defb %01111110
defb %00111100
draught_glyph:
defb %00000000
defb %00111100
defb %01111110
defb %11111111
defb %11111111
defb %01111110
defb %00111100
defb %00000000
win_text:
defb "THE NIGHT IS HELD"
defb $FF
lose_text:
defb "NIGHT FALLS"
defb $FF
end start
The HUD now carries your lives — three red pips at the top right, beside the lamp tally:
Let the draught catch you three times and the square is swallowed whole — the lose state, earned by losing:
When it's wrong, see why
Collision and the lose state fail in their own ways:
- No collisions ever register. The same-cell test is missing or only in one place. Check both
player_stepanddraught_stepcompare the two positions before committing a move. - The sprites overlap and garble. You let a move onto the shared cell go through. The check must come before the move commits and refuse it, exactly like the wall test.
- Lives go wrong or the count underflows.
lose_lifedecrements then indexesLIFE_BASE + lives; make sure it's only ever called on a real hit. - Night never falls. Lives aren't reaching zero — either collisions aren't registering, or the draught can't reach the lamplighter. Lower
DRAUGHT_SPEEDwhile testing.
Before and after
You started with a game you could only win and finished with one you can also lose — and both stakes are the same machinery you already had. Collision is the wall test pointed at another sprite; lives are the tally in reverse; the lose state is the win state dressed in black. The two sprites never overlap, so nothing about the engine had to change to make them deadly — refusing the shared cell was enough. Gloaming has a goal, a threat, and two ways to end.
Try this: more or fewer lives
LIVES sets how many pips you start with. Try 1 for a brutal single-life run, or 5 for a gentler game. The pips draw themselves from that one number — change it and the HUD follows. How forgiving the game feels is yours to set.
Try this: a jolt when caught
A lost life should land. In lose_life, before the reset, flash the border red for a few frames (ld a,2 / out ($FE),a, a short delay, then ld a,0). The hit becomes something you feel, not a pip quietly going out — and it buys a beat before you're thrown back to the start.
Try this: only the draught is deadly
Right now you lose a life whether you walk into the draught or it walks into you. Remove the check in player_step and keep only the one in draught_step: now you can brush past the draught safely, and only its own movement onto you is fatal. It's a subtler, fairer feel — and a single deleted block changes the whole character of the threat.
What you've learnt
- Entity-versus-entity collision is a same-cell test — and blocking the overlap keeps two sprites' save/restore buffers clean, sidestepping the harder problem of drawing them on top of each other.
- Lives are another pip readout, depleted like the tally in reverse.
- A lose state mirrors the win state — an end the game rests in, here dressed as the dark taking the square.
- Gloaming is now a complete game: a goal, a threat, and both a win and a loss.
What's next
The mechanics are all here — you can win, and you can lose. What's left is to make it feel like a game you sit down to play, and that's Phase E. In Unit 16, "The Title", we add a title screen and the state machine that ties everything together: title → play → win or lose → back to the title. Gloaming stops being a program that starts in the middle and becomes one with a front door.