Again
Close the loop. Win or lose, a key returns you to the title; pressing start there builds a brand-new game. The state machine comes full circle — and a short input lock stops a held key from skipping screens.
Gloaming is a finished game — but it ends and stays ended. A real game loops: you finish, and it offers you another go. This unit closes that loop. Win or lose, a key press returns you to the title; press start there and a brand-new game begins. Title → play → win or lose → title → play, round and round, the way an arcade machine waits for the next coin.
Where we start
Unit 18's game — it plays, scores, threatens, sounds, sets a mood, and reaches a win or a lose screen that then holds forever. We turn those dead ends into doorways back to the title.
The loop was almost already built
The state machine from Unit 16 did the hard part. The WIN and LOSE states used to be dead ends — they drew their screen and held forever. Now each watches for a key and hands control back to TITLE:
end_step: ; runs in WIN and LOSE
... wait out the input lock ...
read SPACE
if pressed:
draw the title
game_state = TITLE
That is the whole closure. A dead-end state learning to hand control on is all "play again" is. And because title_step already turns a key press into init_game + PLAY, the loop completes itself.
"Play again" and "start" are the same act
The reason this works cleanly is init_game. Starting from the title does not resume — it rebuilds: lamps unlit, tally cleared, three lives, walls cold and blue, the lamplighter and draught at their starts, and (the detail from Unit 16) the bitmap wiped so nothing ghosts through. So "start" and "play again" call the exact same routine, and a replayed game is genuinely new, not a half-cleared echo of the last one. Per-game state lives in the variables init_game resets; a score you wanted to survive across games would live in a variable init_game leaves alone.
Debounce: the held-key trap
One real-world snag. When you win, the key you were holding to play is most likely still down. Without care, that key would dismiss the win screen and start a new game in the same instant — you would never see the screen you earned. So on entering any screen that waits for a key, we set a short input lock (a frame countdown) and ignore input until it expires — a beat to let go and read the screen. It is a tiny thing that separates a game that feels considered from one that feels twitchy. (We do not lock at boot — there is no held key to guard against there.)
Milestone — close the loop
We route the WIN and LOSE states to a new end_step that waits out an input lock, then sends a key press back to the title; we add the lock countdown to title_step too, and set the lock whenever we enter a waiting screen. The win and lose screens gain a PRESS SPACE line. Everything else is the game you already had — the loop closes with a handful of instructions.
| 1 | 1 | ; Gloaming — Unit 19: Again | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 = Unit 18's end: warming walls, but a win or loss is a dead end. | |
| 3 | + | ; step-01 closes the loop: a key on the end screen returns to the title, debounced. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 31 | 31 | PROMPT_COL equ 10 | |
| 32 | 32 | TITLE_ROW equ 8 | |
| 33 | 33 | PROMPT_ROW equ 14 | |
| 34 | + | CONT_ROW equ 16 ; "PRESS SPACE" under a win/lose line | |
| 34 | 35 | FONT equ $3C00 | |
| 35 | 36 | | |
| 36 | 37 | SPEAKER equ %00010000 | |
| ... | |||
| 39 | 40 | STATE_PLAY equ 1 | |
| 40 | 41 | STATE_WIN equ 2 | |
| 41 | 42 | STATE_LOSE equ 3 | |
| 43 | + | LOCK equ 25 ; input-lock frames after entering a screen | |
| 42 | 44 | | |
| 43 | 45 | START_COL equ 15 | |
| 44 | 46 | START_ROW equ 11 | |
| ... | |||
| 58 | 60 | out ($FE), a | |
| 59 | 61 | ld a, STATE_TITLE | |
| 60 | 62 | ld (game_state), a | |
| 61 | - | call draw_title_screen | |
| 63 | + | call draw_title_screen ; no startup lock — nothing to debounce yet | |
| 62 | 64 | im 1 | |
| 63 | 65 | ei | |
| 64 | 66 | | |
| ... | |||
| 69 | 71 | jr z, .do_title | |
| 70 | 72 | cp STATE_PLAY | |
| 71 | 73 | jr z, .do_play | |
| 74 | + | call end_step ; WIN or LOSE — wait for a key, then title | |
| 72 | 75 | jr main_loop | |
| 73 | 76 | .do_title: | |
| 74 | 77 | call title_step | |
| ... | |||
| 77 | 80 | call play_step | |
| 78 | 81 | jr main_loop | |
| 79 | 82 | | |
| 83 | + | ; ---------------------------------------------------------------------------- | |
| 84 | + | ; title_step — after the lock, SPACE starts a fresh game. | |
| 85 | + | ; ---------------------------------------------------------------------------- | |
| 80 | 86 | title_step: | |
| 87 | + | ld a, (input_lock) | |
| 88 | + | or a | |
| 89 | + | jr z, .tready | |
| 90 | + | dec a | |
| 91 | + | ld (input_lock), a | |
| 92 | + | ret | |
| 93 | + | .tready: | |
| 81 | 94 | ld bc, KEYS_SPACE | |
| 82 | 95 | in a, (c) | |
| 83 | 96 | bit 0, a | |
| 84 | 97 | ret nz | |
| 85 | 98 | call init_game | |
| 86 | 99 | ld a, STATE_PLAY | |
| 100 | + | ld (game_state), a | |
| 101 | + | ret | |
| 102 | + | | |
| 103 | + | ; ---------------------------------------------------------------------------- | |
| 104 | + | ; end_step — WIN/LOSE: after the lock, SPACE returns to the title. | |
| 105 | + | ; ---------------------------------------------------------------------------- | |
| 106 | + | end_step: | |
| 107 | + | ld a, (input_lock) | |
| 108 | + | or a | |
| 109 | + | jr z, .eready | |
| 110 | + | dec a | |
| 111 | + | ld (input_lock), a | |
| 112 | + | ret | |
| 113 | + | .eready: | |
| 114 | + | ld bc, KEYS_SPACE | |
| 115 | + | in a, (c) | |
| 116 | + | bit 0, a | |
| 117 | + | ret nz | |
| 118 | + | call draw_title_screen | |
| 119 | + | ld a, LOCK | |
| 120 | + | ld (input_lock), a | |
| 121 | + | ld a, STATE_TITLE | |
| 87 | 122 | ld (game_state), a | |
| 88 | 123 | ret | |
| 89 | 124 | | |
| ... | |||
| 100 | 135 | cp NUM_LAMPS | |
| 101 | 136 | ret nz | |
| 102 | 137 | call draw_win_screen | |
| 138 | + | ld a, LOCK | |
| 139 | + | ld (input_lock), a | |
| 103 | 140 | ld a, STATE_WIN | |
| 104 | 141 | ld (game_state), a | |
| 105 | 142 | ret | |
| ... | |||
| 124 | 161 | ld (draught_timer), a | |
| 125 | 162 | | |
| 126 | 163 | call clear_bitmap | |
| 127 | - | | |
| 128 | 164 | ld hl, $5800 | |
| 129 | 165 | ld de, $5801 | |
| 130 | 166 | ld (hl), COBBLE | |
| 131 | 167 | ld bc, 767 | |
| 132 | 168 | ldir | |
| 133 | - | | |
| 134 | - | call warm_walls ; draw the frame at the current (cold) warmth | |
| 135 | - | | |
| 169 | + | call warm_walls | |
| 136 | 170 | call draw_pips | |
| 137 | 171 | call draw_lives | |
| 138 | 172 | call draw_lamps | |
| ... | |||
| 142 | 176 | call draw_draught | |
| 143 | 177 | ret | |
| 144 | 178 | | |
| 145 | - | ; ---------------------------------------------------------------------------- | |
| 146 | - | ; warm_walls — repaint the frame in the colour for the current lamp count. | |
| 147 | - | ; wall_ramp[lit_count] -> the wall attribute. Every entry keeps PAPER bit 0 | |
| 148 | - | ; set, so the walls stay solid to collision. | |
| 149 | - | ; ---------------------------------------------------------------------------- | |
| 150 | 179 | warm_walls: | |
| 151 | 180 | ld a, (lit_count) | |
| 152 | 181 | ld e, a | |
| 153 | 182 | ld d, 0 | |
| 154 | 183 | ld hl, wall_ramp | |
| 155 | 184 | add hl, de | |
| 156 | - | ld c, (hl) ; C = wall colour for this progress | |
| 157 | - | | |
| 158 | - | ld hl, $5820 ; top wall (row 1) | |
| 185 | + | ld c, (hl) | |
| 186 | + | ld hl, $5820 | |
| 159 | 187 | ld b, 32 | |
| 160 | 188 | .wt: | |
| 161 | 189 | ld (hl), c | |
| 162 | 190 | inc hl | |
| 163 | 191 | djnz .wt | |
| 164 | - | ld hl, $5AE0 ; bottom wall (row 23) | |
| 192 | + | ld hl, $5AE0 | |
| 165 | 193 | ld b, 32 | |
| 166 | 194 | .wb: | |
| 167 | 195 | ld (hl), c | |
| 168 | 196 | inc hl | |
| 169 | 197 | djnz .wb | |
| 170 | - | ld hl, $5820 ; sides, rows 1..23 | |
| 198 | + | ld hl, $5820 | |
| 171 | 199 | ld b, 23 | |
| 172 | 200 | .ws: | |
| 173 | 201 | ld (hl), c | |
| ... | |||
| 211 | 239 | jp beep | |
| 212 | 240 | | |
| 213 | 241 | ; ---------------------------------------------------------------------------- | |
| 214 | - | ; Screens. | |
| 242 | + | ; Screens. Win and lose now invite another go. | |
| 215 | 243 | ; ---------------------------------------------------------------------------- | |
| 216 | 244 | draw_title_screen: | |
| 217 | 245 | call clear_bitmap | |
| ... | |||
| 235 | 263 | ld hl, win_text | |
| 236 | 264 | ld b, MSG_ROW | |
| 237 | 265 | ld c, WIN_COL | |
| 266 | + | call print_string | |
| 267 | + | ld hl, prompt_text | |
| 268 | + | ld b, CONT_ROW | |
| 269 | + | ld c, PROMPT_COL | |
| 238 | 270 | call print_string | |
| 239 | 271 | ret | |
| 240 | 272 | | |
| ... | |||
| 247 | 279 | ld hl, lose_text | |
| 248 | 280 | ld b, MSG_ROW | |
| 249 | 281 | ld c, LOSE_COL | |
| 282 | + | call print_string | |
| 283 | + | ld hl, prompt_text | |
| 284 | + | ld b, CONT_ROW | |
| 285 | + | ld c, PROMPT_COL | |
| 250 | 286 | call print_string | |
| 251 | 287 | ret | |
| 252 | 288 | | |
| ... | |||
| 259 | 295 | ret | |
| 260 | 296 | | |
| 261 | 297 | ; ---------------------------------------------------------------------------- | |
| 262 | - | ; player_step — light a lamp: blip and warm the walls. | |
| 298 | + | ; player_step. | |
| 263 | 299 | ; ---------------------------------------------------------------------------- | |
| 264 | 300 | player_step: | |
| 265 | 301 | ld a, (lamp_col) | |
| ... | |||
| 331 | 367 | ld (under_lamp + 8), a | |
| 332 | 368 | call light_pip | |
| 333 | 369 | call blip_lit | |
| 334 | - | call warm_walls ; the stone catches the new light | |
| 370 | + | call warm_walls | |
| 335 | 371 | .pdrawn: | |
| 336 | 372 | call draw_lamp | |
| 337 | 373 | ret | |
| 338 | 374 | | |
| 339 | 375 | ; ---------------------------------------------------------------------------- | |
| 340 | - | ; draught_step — snuff a lamp: blip and cool the walls. | |
| 376 | + | ; draught_step. | |
| 341 | 377 | ; ---------------------------------------------------------------------------- | |
| 342 | 378 | draught_step: | |
| 343 | 379 | ld a, (draught_timer) | |
| ... | |||
| 409 | 445 | ld (under_draught + 8), a | |
| 410 | 446 | call unlight_pip | |
| 411 | 447 | call blip_snuff | |
| 412 | - | call warm_walls ; the stone cools as the dark returns | |
| 448 | + | call warm_walls | |
| 413 | 449 | .nosnuff: | |
| 414 | 450 | call draw_draught | |
| 415 | 451 | ret | |
| ... | |||
| 436 | 472 | ret | |
| 437 | 473 | .gone: | |
| 438 | 474 | call draw_lose_screen | |
| 475 | + | ld a, LOCK | |
| 476 | + | ld (input_lock), a | |
| 439 | 477 | ld a, STATE_LOSE | |
| 440 | 478 | ld (game_state), a | |
| 441 | 479 | ret | |
| ... | |||
| 721 | 759 | ; ---------------------------------------------------------------------------- | |
| 722 | 760 | game_state: | |
| 723 | 761 | defb STATE_TITLE | |
| 762 | + | input_lock: | |
| 763 | + | defb 0 | |
| 724 | 764 | | |
| 725 | - | ; wall colour by lamps lit (0..8). Cold blue -> bright blue -> magenta -> | |
| 726 | - | ; bright magenta. Every PAPER here has bit 0 set, so walls stay solid. | |
| 727 | 765 | wall_ramp: | |
| 728 | - | defb %00001111 ; 0 — blue, cold | |
| 729 | - | defb %00001111 ; 1 | |
| 730 | - | defb %01001111 ; 2 — BRIGHT blue | |
| 731 | - | defb %01001111 ; 3 | |
| 732 | - | defb %00011111 ; 4 — magenta | |
| 733 | - | defb %00011111 ; 5 | |
| 734 | - | defb %01011111 ; 6 — BRIGHT magenta | |
| 735 | - | defb %01011111 ; 7 | |
| 736 | - | defb %01011111 ; 8 — fully warmed | |
| 766 | + | defb %00001111 | |
| 767 | + | defb %00001111 | |
| 768 | + | defb %01001111 | |
| 769 | + | defb %01001111 | |
| 770 | + | defb %00011111 | |
| 771 | + | defb %00011111 | |
| 772 | + | defb %01011111 | |
| 773 | + | defb %01011111 | |
| 774 | + | defb %01011111 | |
| 737 | 775 | | |
| 738 | 776 | lamp_data: | |
| 739 | 777 | defb 4, 3 |
The complete program
; Gloaming — Unit 19: Again
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 closes the loop: a key on the end screen returns to the title, debounced.
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
LIFE_BASE equ $5800 + 28
MSG_ATTR equ %01000111
MSG_ROW equ 11
WIN_COL equ 7
LOSE_COL equ 10
TITLE_COL equ 12
PROMPT_COL equ 10
TITLE_ROW equ 8
PROMPT_ROW equ 14
CONT_ROW equ 16 ; "PRESS SPACE" under a win/lose line
FONT equ $3C00
SPEAKER equ %00010000
STATE_TITLE equ 0
STATE_PLAY equ 1
STATE_WIN equ 2
STATE_LOSE equ 3
LOCK equ 25 ; input-lock frames after entering a screen
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
KEYS_SPACE equ $7FFE
; ============================================================================
; SETUP.
; ============================================================================
start:
ld a, 0
out ($FE), a
ld a, STATE_TITLE
ld (game_state), a
call draw_title_screen ; no startup lock — nothing to debounce yet
im 1
ei
main_loop:
halt
ld a, (game_state)
cp STATE_TITLE
jr z, .do_title
cp STATE_PLAY
jr z, .do_play
call end_step ; WIN or LOSE — wait for a key, then title
jr main_loop
.do_title:
call title_step
jr main_loop
.do_play:
call play_step
jr main_loop
; ----------------------------------------------------------------------------
; title_step — after the lock, SPACE starts a fresh game.
; ----------------------------------------------------------------------------
title_step:
ld a, (input_lock)
or a
jr z, .tready
dec a
ld (input_lock), a
ret
.tready:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
ret nz
call init_game
ld a, STATE_PLAY
ld (game_state), a
ret
; ----------------------------------------------------------------------------
; end_step — WIN/LOSE: after the lock, SPACE returns to the title.
; ----------------------------------------------------------------------------
end_step:
ld a, (input_lock)
or a
jr z, .eready
dec a
ld (input_lock), a
ret
.eready:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
ret nz
call draw_title_screen
ld a, LOCK
ld (input_lock), a
ld a, STATE_TITLE
ld (game_state), a
ret
play_step:
call player_step
ld a, (game_state)
cp STATE_PLAY
ret nz
call draught_step
ld a, (game_state)
cp STATE_PLAY
ret nz
ld a, (lit_count)
cp NUM_LAMPS
ret nz
call draw_win_screen
ld a, LOCK
ld (input_lock), a
ld a, STATE_WIN
ld (game_state), a
ret
init_game:
xor a
ld (lit_count), a
ld a, LIVES
ld (lives), a
ld a, START_COL
ld (lamp_col), a
ld a, START_ROW
ld (lamp_row), a
ld a, DRAUGHT_COL0
ld (draught_col), a
ld a, DRAUGHT_ROW0
ld (draught_row), a
ld a, 1
ld (draught_dx), a
ld (draught_dy), a
ld a, DRAUGHT_SPEED
ld (draught_timer), a
call clear_bitmap
ld hl, $5800
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
call warm_walls
call draw_pips
call draw_lives
call draw_lamps
call save_under
call draw_lamp
call save_draught
call draw_draught
ret
warm_walls:
ld a, (lit_count)
ld e, a
ld d, 0
ld hl, wall_ramp
add hl, de
ld c, (hl)
ld hl, $5820
ld b, 32
.wt:
ld (hl), c
inc hl
djnz .wt
ld hl, $5AE0
ld b, 32
.wb:
ld (hl), c
inc hl
djnz .wb
ld hl, $5820
ld b, 23
.ws:
ld (hl), c
push hl
ld de, 31
add hl, de
ld (hl), c
pop hl
ld de, 32
add hl, de
djnz .ws
ret
beep:
di
.bcyc:
ld a, SPEAKER
out ($FE), a
ld a, c
.bd1:
dec a
jr nz, .bd1
xor a
out ($FE), a
ld a, c
.bd2:
dec a
jr nz, .bd2
djnz .bcyc
ei
ret
blip_lit:
ld b, $20
ld c, $18
jp beep
blip_snuff:
ld b, $1A
ld c, $40
jp beep
; ----------------------------------------------------------------------------
; Screens. Win and lose now invite another go.
; ----------------------------------------------------------------------------
draw_title_screen:
call clear_bitmap
ld hl, $5800
ld de, $5801
ld (hl), %00000000
ld bc, 767
ldir
ld hl, title_text
ld b, TITLE_ROW
ld c, TITLE_COL
call print_string
ld hl, prompt_text
ld b, PROMPT_ROW
ld c, PROMPT_COL
call print_string
ret
draw_win_screen:
call restore_under
ld hl, win_text
ld b, MSG_ROW
ld c, WIN_COL
call print_string
ld hl, prompt_text
ld b, CONT_ROW
ld c, PROMPT_COL
call print_string
ret
draw_lose_screen:
ld hl, $5800
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
ld hl, prompt_text
ld b, CONT_ROW
ld c, PROMPT_COL
call print_string
ret
clear_bitmap:
ld hl, $4000
ld de, $4001
ld (hl), 0
ld bc, 6143
ldir
ret
; ----------------------------------------------------------------------------
; player_step.
; ----------------------------------------------------------------------------
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
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
call blip_lit
call warm_walls
.pdrawn:
call draw_lamp
ret
; ----------------------------------------------------------------------------
; draught_step.
; ----------------------------------------------------------------------------
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:
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
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
.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
call blip_snuff
call warm_walls
.nosnuff:
call draw_draught
ret
lose_life:
ld a, (lives)
dec a
ld (lives), a
ld e, a
ld d, 0
ld hl, LIFE_BASE
add hl, de
ld (hl), COBBLE
ld a, (lives)
or a
jr z, .gone
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
.gone:
call draw_lose_screen
ld a, LOCK
ld (input_lock), a
ld a, STATE_LOSE
ld (game_state), a
ret
; ----------------------------------------------------------------------------
; print_string / print_char.
; ----------------------------------------------------------------------------
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
; ----------------------------------------------------------------------------
; Data.
; ----------------------------------------------------------------------------
game_state:
defb STATE_TITLE
input_lock:
defb 0
wall_ramp:
defb %00001111
defb %00001111
defb %01001111
defb %01001111
defb %00011111
defb %00011111
defb %01011111
defb %01011111
defb %01011111
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
title_text:
defb "GLOAMING"
defb $FF
prompt_text:
defb "PRESS SPACE"
defb $FF
win_text:
defb "THE NIGHT IS HELD"
defb $FF
lose_text:
defb "NIGHT FALLS"
defb $FF
end start
Every end screen now invites another go. Here is the lose screen, carrying the new prompt (the win screen carries the same one):
Press it and the whole loop turns — the end screen hands back to the title, and the title builds a fresh game from scratch:
When it's wrong, see why
The loop and its debounce fail in their own ways:
- A key on the end screen does nothing.
end_stepis not being called forWIN/LOSE, or the input lock never reaches zero. The dispatcher must route those states toend_step, which counts the lock down. - The end screen flashes past before you can read it. No input lock — a held key carried straight through. Set the lock when you enter the screen.
- A replayed game keeps old lamps lit, warm walls, or lost lives.
init_gameis not a total reset, or the bitmap wipe is missing. Starting over must rebuild everything. - The title instantly starts a game after a win. The lock set when returning to the title is missing, so the still-held key triggers
title_stepat once.
Before and after
You started with a game that ended and stopped and finished with one that ends and offers another go — and the closure cost almost nothing, because the state machine was already there. A dead-end state only had to learn to hand control on; init_game was already a full rebuild, so "play again" and "start" became the same act; and one short per-screen lock kept a held key from skipping the screen you earned. That is the shape of every arcade game: a loop with a clean way in and a clean way round again.
Try this: press any key to continue
Space is fine, but arcades say "press any key". In end_step, read a few half-rows and continue if any shows a held key (combine the rows; if the low five bits are not all 1, something is down). Friendlier, and it reuses the keyboard reading you already know.
Try this: tune the lock
LOCK is the debounce length in frames. Set it to 1 and you will sometimes skip the win screen by still-holding a key; set it to 100 and the screen feels unresponsive. Find the value that reads as "deliberate, not sticky" — somewhere near half a second. Feel is made of numbers like this.
Try this: a games-played counter
Add a byte that init_game does not reset, and increment it each time a game starts; print it on the title as "GAME N". You will have drawn the line between per-game state (everything init_game clears) and persistent state (what survives the loop) — exactly where a high score would live.
What you've learnt
- Closing the loop is cheap once a state machine exists — a dead-end state just learns to hand control to another.
- "Play again" is "start" — one
init_gamethat rebuilds everything, so a replay is truly fresh. - Debounce shared keys with a short per-screen input lock, so a held key does not skip the screen you just earned.
- Per-game state vs persistent state: what
init_gameresets versus what survives the loop (a score, a counter).
What's next
Gloaming is complete — a full arcade loop, played from a title and back again. There is one truth left to confirm, and it is the one that matters most: does it run on a real Spectrum? In Unit 20, "On Real Iron", we take the finished game off the emulator and onto actual hardware — built to a tape with an auto-running loader — and verify that everything you built behaves exactly as it has on screen. The last step of every real project: trust, but verify, on the metal.