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

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.

95% of Gloaming

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.

Step 1: end states hand back to the title, with a debounce lock
+70-32
11 ; Gloaming — Unit 19: Again
22 ; 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.
44
55 org 32768
66
...
3131 PROMPT_COL equ 10
3232 TITLE_ROW equ 8
3333 PROMPT_ROW equ 14
34+CONT_ROW equ 16 ; "PRESS SPACE" under a win/lose line
3435 FONT equ $3C00
3536
3637 SPEAKER equ %00010000
...
3940 STATE_PLAY equ 1
4041 STATE_WIN equ 2
4142 STATE_LOSE equ 3
43+LOCK equ 25 ; input-lock frames after entering a screen
4244
4345 START_COL equ 15
4446 START_ROW equ 11
...
5860 out ($FE), a
5961 ld a, STATE_TITLE
6062 ld (game_state), a
61- call draw_title_screen
63+ call draw_title_screen ; no startup lock — nothing to debounce yet
6264 im 1
6365 ei
6466
...
6971 jr z, .do_title
7072 cp STATE_PLAY
7173 jr z, .do_play
74+ call end_step ; WIN or LOSE — wait for a key, then title
7275 jr main_loop
7376 .do_title:
7477 call title_step
...
7780 call play_step
7881 jr main_loop
7982
83+; ----------------------------------------------------------------------------
84+; title_step — after the lock, SPACE starts a fresh game.
85+; ----------------------------------------------------------------------------
8086 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:
8194 ld bc, KEYS_SPACE
8295 in a, (c)
8396 bit 0, a
8497 ret nz
8598 call init_game
8699 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
87122 ld (game_state), a
88123 ret
89124
...
100135 cp NUM_LAMPS
101136 ret nz
102137 call draw_win_screen
138+ ld a, LOCK
139+ ld (input_lock), a
103140 ld a, STATE_WIN
104141 ld (game_state), a
105142 ret
...
124161 ld (draught_timer), a
125162
126163 call clear_bitmap
127-
128164 ld hl, $5800
129165 ld de, $5801
130166 ld (hl), COBBLE
131167 ld bc, 767
132168 ldir
133-
134- call warm_walls ; draw the frame at the current (cold) warmth
135-
169+ call warm_walls
136170 call draw_pips
137171 call draw_lives
138172 call draw_lamps
...
142176 call draw_draught
143177 ret
144178
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-; ----------------------------------------------------------------------------
150179 warm_walls:
151180 ld a, (lit_count)
152181 ld e, a
153182 ld d, 0
154183 ld hl, wall_ramp
155184 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
159187 ld b, 32
160188 .wt:
161189 ld (hl), c
162190 inc hl
163191 djnz .wt
164- ld hl, $5AE0 ; bottom wall (row 23)
192+ ld hl, $5AE0
165193 ld b, 32
166194 .wb:
167195 ld (hl), c
168196 inc hl
169197 djnz .wb
170- ld hl, $5820 ; sides, rows 1..23
198+ ld hl, $5820
171199 ld b, 23
172200 .ws:
173201 ld (hl), c
...
211239 jp beep
212240
213241 ; ----------------------------------------------------------------------------
214-; Screens.
242+; Screens. Win and lose now invite another go.
215243 ; ----------------------------------------------------------------------------
216244 draw_title_screen:
217245 call clear_bitmap
...
235263 ld hl, win_text
236264 ld b, MSG_ROW
237265 ld c, WIN_COL
266+ call print_string
267+ ld hl, prompt_text
268+ ld b, CONT_ROW
269+ ld c, PROMPT_COL
238270 call print_string
239271 ret
240272
...
247279 ld hl, lose_text
248280 ld b, MSG_ROW
249281 ld c, LOSE_COL
282+ call print_string
283+ ld hl, prompt_text
284+ ld b, CONT_ROW
285+ ld c, PROMPT_COL
250286 call print_string
251287 ret
252288
...
259295 ret
260296
261297 ; ----------------------------------------------------------------------------
262-; player_step — light a lamp: blip and warm the walls.
298+; player_step.
263299 ; ----------------------------------------------------------------------------
264300 player_step:
265301 ld a, (lamp_col)
...
331367 ld (under_lamp + 8), a
332368 call light_pip
333369 call blip_lit
334- call warm_walls ; the stone catches the new light
370+ call warm_walls
335371 .pdrawn:
336372 call draw_lamp
337373 ret
338374
339375 ; ----------------------------------------------------------------------------
340-; draught_step — snuff a lamp: blip and cool the walls.
376+; draught_step.
341377 ; ----------------------------------------------------------------------------
342378 draught_step:
343379 ld a, (draught_timer)
...
409445 ld (under_draught + 8), a
410446 call unlight_pip
411447 call blip_snuff
412- call warm_walls ; the stone cools as the dark returns
448+ call warm_walls
413449 .nosnuff:
414450 call draw_draught
415451 ret
...
436472 ret
437473 .gone:
438474 call draw_lose_screen
475+ ld a, LOCK
476+ ld (input_lock), a
439477 ld a, STATE_LOSE
440478 ld (game_state), a
441479 ret
...
721759 ; ----------------------------------------------------------------------------
722760 game_state:
723761 defb STATE_TITLE
762+input_lock:
763+ defb 0
724764
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.
727765 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
737775
738776 lamp_data:
739777 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):

A black screen with NIGHT FALLS and, below it, PRESS SPACE.
The lose screen, no longer a dead end: NIGHT FALLS, and an invitation back. The win screen carries the same PRESS SPACE under THE NIGHT IS HELD.

Press it and the whole loop turns — the end screen hands back to the title, and the title builds a fresh game from scratch:

The arcade loop, end to end. Night falls; a press of space returns to the GLOAMING title; another press rebuilds the square — cold blue again, every lamp dark, three lives restored. A replayed game is genuinely fresh, because start and play-again are the same init_game.

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_step is not being called for WIN/LOSE, or the input lock never reaches zero. The dispatcher must route those states to end_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_game is 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_step at 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_game that 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_game resets 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.