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

The Title

Give the game a front door. A sparse GLOAMING title screen, a press-to-begin prompt, and the state machine — TITLE, PLAY, WIN, LOSE — that ties the whole game together and decides what happens each frame.

80% of Gloaming

Gloaming plays, but it starts in the middle — load it and you're already wandering the square. Real games have a front door: a title that waits for you. Phase E is about making the game feel finished, and it starts here, with a title screen and the idea that quietly organises every game ever written — a state machine.

Where we start

Unit 15's complete game — a goal, a threat, a win and a loss — that drops you straight into play with no beginning and no way to start over. We give it a front door and a structure to hang the rest on.

A game is a handful of states

At any moment, Gloaming is doing one of four things:

  • TITLE — showing its name, waiting for you to start.
  • PLAY — the game proper: moving, lighting, dodging.
  • WIN — all lamps lit, holding the closing line.
  • LOSE — out of lives, night fallen.

A state machine is a byte that remembers which — game_state — and a loop that, each frame, does the right thing for that state:

main_loop:
    halt
    ld   a, (game_state)
    cp   STATE_TITLE
    jr   z, .do_title
    cp   STATE_PLAY
    jr   z, .do_play
    jr   main_loop          ; WIN / LOSE — just hold
.do_title:
    call title_step
    jr   main_loop
.do_play:
    call play_step
    jr   main_loop

Everything you built becomes play_step — one state among four. The win and lose checks no longer freeze the machine; they just change the state, and the loop carries on, now doing whatever that new state needs. This is how a program stops being one long script and becomes a system that can be in different modes and move between them.

Setup becomes "start a game"

For the title to mean anything, pressing the key has to build a fresh game — a different job from "draw the scene once at boot". So all the world-building moves into init_game: reset the lives and the tally, place the lamplighter and the draught at their starts, draw the square, the walls, the lamps. title_step waits for space and calls it:

title_step:
    ld   bc, KEYS_SPACE
    in   a, (c)
    bit  0, a          ; SPACE down?
    ret  nz            ; no — keep waiting
    call init_game     ; yes — build a fresh game
    ld   a, STATE_PLAY
    ld   (game_state), a
    ret

Because init_game builds everything from scratch, it's also exactly what "play again" will need later — pressing start and starting over are the same act.

A wrinkle: clearing the pixels

The title is text — pixels written into the bitmap. When the game starts, init_game repaints the colour of every cell (the cobbles), but colour isn't pixels: the title's letters are still sitting in the bitmap, and they'd ghost through the new floor in faint blue. The fix is to wipe the pixel area too — a single LDIR clearing $4000$57FF — before drawing the board. It's a small thing, but it's the kind of bug that only appears once a game has more than one screen, and worth meeting now.

Milestone — the front door

We add a game_state byte and a dispatch loop, move all the boot-time world-building into init_game, add a title_step that draws the name and waits for space, and turn the win and lose checks into state changes, not freezes. A bitmap clear in init_game stops the title ghosting through.

Step 1: a TITLE/PLAY/WIN/LOSE state machine and a title screen
+178-69
11 ; Gloaming — Unit 16: The Title
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 15's complete game — but it starts in the middle, no front door.
3+; step-01 adds a title screen and a TITLE/PLAY/WIN/LOSE state machine.
44
55 org 32768
66
...
2020 NUM_LAMPS equ 8
2121
2222 LIVES equ 3
23-LIFE_PIP equ %01010000 ; BRIGHT, PAPER red — a life
24-LIFE_BASE equ $5800 + 28 ; row 0, columns 28-30
23+LIFE_PIP equ %01010000
24+LIFE_BASE equ $5800 + 28
2525
2626 MSG_ATTR equ %01000111
2727 MSG_ROW equ 11
2828 WIN_COL equ 7
2929 LOSE_COL equ 10
30+TITLE_COL equ 12 ; "GLOAMING" centred
31+PROMPT_COL equ 10 ; "PRESS SPACE" centred
32+TITLE_ROW equ 8
33+PROMPT_ROW equ 14
3034 FONT equ $3C00
35+
36+; game states
37+STATE_TITLE equ 0
38+STATE_PLAY equ 1
39+STATE_WIN equ 2
40+STATE_LOSE equ 3
3141
3242 START_COL equ 15
3343 START_ROW equ 11
...
3747 KEYS_OP equ $DFFE
3848 KEYS_Q equ $FBFE
3949 KEYS_A equ $FDFE
50+KEYS_SPACE equ $7FFE ; SPACE is bit 0 of this half-row
4051
4152 ; ============================================================================
42-; SETUP — runs once.
53+; SETUP — border, the title screen, then start the state-machine loop.
4354 ; ============================================================================
4455 start:
4556 ld a, 0
4657 out ($FE), a
4758
48- ld hl, $5800
59+ ld a, STATE_TITLE
60+ ld (game_state), a
61+ call draw_title_screen
62+
63+ im 1
64+ ei
65+
66+; ----------------------------------------------------------------------------
67+; THE STATE MACHINE — each frame, do whatever the current state needs.
68+; ----------------------------------------------------------------------------
69+main_loop:
70+ halt
71+ ld a, (game_state)
72+ cp STATE_TITLE
73+ jr z, .do_title
74+ cp STATE_PLAY
75+ jr z, .do_play
76+ jr main_loop ; WIN / LOSE — just hold the screen
77+.do_title:
78+ call title_step
79+ jr main_loop
80+.do_play:
81+ call play_step
82+ jr main_loop
83+
84+; ----------------------------------------------------------------------------
85+; title_step — wait for SPACE; when pressed, build a fresh game and play.
86+; ----------------------------------------------------------------------------
87+title_step:
88+ ld bc, KEYS_SPACE
89+ in a, (c)
90+ bit 0, a ; SPACE down? (0 = pressed)
91+ ret nz ; not yet — stay on the title
92+ call init_game
93+ ld a, STATE_PLAY
94+ ld (game_state), a
95+ ret
96+
97+; ----------------------------------------------------------------------------
98+; play_step — one frame of the game; may transition to WIN or LOSE.
99+; ----------------------------------------------------------------------------
100+play_step:
101+ call player_step
102+ ld a, (game_state)
103+ cp STATE_PLAY
104+ ret nz ; a collision ended the game
105+ call draught_step
106+ ld a, (game_state)
107+ cp STATE_PLAY
108+ ret nz
109+ ld a, (lit_count)
110+ cp NUM_LAMPS
111+ ret nz
112+ call draw_win_screen
113+ ld a, STATE_WIN
114+ ld (game_state), a
115+ ret
116+
117+; ----------------------------------------------------------------------------
118+; init_game — build a fresh play session (called by "press a key to begin").
119+; ----------------------------------------------------------------------------
120+init_game:
121+ xor a
122+ ld (lit_count), a
123+ ld a, LIVES
124+ ld (lives), a
125+ ld a, START_COL
126+ ld (lamp_col), a
127+ ld a, START_ROW
128+ ld (lamp_row), a
129+ ld a, DRAUGHT_COL0
130+ ld (draught_col), a
131+ ld a, DRAUGHT_ROW0
132+ ld (draught_row), a
133+ ld a, 1
134+ ld (draught_dx), a
135+ ld (draught_dy), a
136+ ld a, DRAUGHT_SPEED
137+ ld (draught_timer), a
138+
139+ call clear_bitmap ; wipe any leftover pixels (e.g. the title)
140+
141+ ld hl, $5800 ; cobbles
49142 ld de, $5801
50143 ld (hl), COBBLE
51144 ld bc, 767
52145 ldir
53146
54- ld hl, $5820
147+ ld hl, $5820 ; top wall (row 1)
55148 ld b, 32
56-.top:
149+.iwt:
57150 ld (hl), WALL
58151 inc hl
59- djnz .top
60-
61- ld hl, $5AE0
152+ djnz .iwt
153+ ld hl, $5AE0 ; bottom wall (row 23)
62154 ld b, 32
63-.bottom:
155+.iwb:
64156 ld (hl), WALL
65157 inc hl
66- djnz .bottom
67-
68- ld hl, $5820
158+ djnz .iwb
159+ ld hl, $5820 ; sides
69160 ld b, 23
70-.sides:
161+.iws:
71162 ld (hl), WALL
72163 push hl
73164 ld de, 31
...
76167 pop hl
77168 ld de, 32
78169 add hl, de
79- djnz .sides
170+ djnz .iws
80171
81172 call draw_pips
82173 call draw_lives
...
85176 call draw_lamp
86177 call save_draught
87178 call draw_draught
179+ ret
88180
89-; ============================================================================
90-; THE HEARTBEAT.
91-; ============================================================================
92- im 1
93- ei
181+; ----------------------------------------------------------------------------
182+; Screens: title, win, lose.
183+; ----------------------------------------------------------------------------
184+draw_title_screen:
185+ call clear_bitmap ; wipe leftover pixels (a finished game's board)
186+ ld hl, $5800 ; black field
187+ ld de, $5801
188+ ld (hl), %00000000
189+ ld bc, 767
190+ ldir
191+ ld hl, title_text
192+ ld b, TITLE_ROW
193+ ld c, TITLE_COL
194+ call print_string
195+ ld hl, prompt_text
196+ ld b, PROMPT_ROW
197+ ld c, PROMPT_COL
198+ call print_string
199+ ret
94200
95-game_loop:
96- halt
97- call player_step
98- call draught_step
99- ld a, (lit_count)
100- cp NUM_LAMPS
101- jp z, win
102- jr game_loop
201+draw_win_screen:
202+ call restore_under
203+ ld hl, win_text
204+ ld b, MSG_ROW
205+ ld c, WIN_COL
206+ call print_string
207+ ret
208+
209+draw_lose_screen:
210+ ld hl, $5800
211+ ld de, $5801
212+ ld (hl), %00000000
213+ ld bc, 767
214+ ldir
215+ ld hl, lose_text
216+ ld b, MSG_ROW
217+ ld c, LOSE_COL
218+ call print_string
219+ ret
220+
221+; clear_bitmap — zero the pixel area $4000-$57FF (6144 bytes).
222+clear_bitmap:
223+ ld hl, $4000
224+ ld de, $4001
225+ ld (hl), 0
226+ ld bc, 6143
227+ ldir
228+ ret
103229
104230 ; ----------------------------------------------------------------------------
105-; player_step — move the lamplighter; a step onto the draught costs a life.
231+; player_step.
106232 ; ----------------------------------------------------------------------------
107233 player_step:
108234 ld a, (lamp_col)
...
147273 ld a, (tcol)
148274 ld c, a
149275 call wall_at
150- ret nz ; wall blocks
276+ ret nz
151277
152- ; would the step land on the draught? then it's a collision
153278 ld a, (tcol)
154279 ld hl, draught_col
155280 cp (hl)
...
179304 ret
180305
181306 ; ----------------------------------------------------------------------------
182-; draught_step — drift, bounce, snuff; a step onto the lamplighter costs a life.
307+; draught_step.
183308 ; ----------------------------------------------------------------------------
184309 draught_step:
185310 ld a, (draught_timer)
...
215340 neg
216341 ld (draught_dy), a
217342 .vok:
218- ; work out the target cell
219343 ld a, (draught_col)
220344 ld b, a
221345 ld a, (draught_dx)
...
227351 add a, b
228352 ld (dtrow), a
229353
230- ; would it land on the lamplighter? then it's a collision
231354 ld a, (dtcol)
232355 ld hl, lamp_col
233356 cp (hl)
...
237360 cp (hl)
238361 jr nz, .dmove
239362 call lose_life
240- ret ; don't move onto him
363+ ret
241364
242365 .dmove:
243366 call restore_draught
...
257380 ret
258381
259382 ; ----------------------------------------------------------------------------
260-; lose_life — drop a life pip; reset the lamplighter, or fall to night.
383+; lose_life — drop a life pip; reset the lamplighter, or enter the lose state.
261384 ; ----------------------------------------------------------------------------
262385 lose_life:
263386 ld a, (lives)
264387 dec a
265388 ld (lives), a
266- ld e, a ; index of the life pip to remove
389+ ld e, a
267390 ld d, 0
268391 ld hl, LIFE_BASE
269392 add hl, de
270- ld (hl), COBBLE ; the pip goes dark
393+ ld (hl), COBBLE
271394 ld a, (lives)
272395 or a
273- jp z, lose ; out of lives — night falls
396+ jr z, .gone
274397
275- ; otherwise send the lamplighter back to the start, clear of danger
276398 call restore_under
277399 ld a, START_COL
278400 ld (lamp_col), a
...
281403 call save_under
282404 call draw_lamp
283405 ret
284-
285-; ----------------------------------------------------------------------------
286-; win / lose — each prints a line and holds.
287-; ----------------------------------------------------------------------------
288-win:
289- call restore_under
290- ld hl, win_text
291- ld b, MSG_ROW
292- ld c, WIN_COL
293- call print_string
294-.whold:
295- halt
296- jr .whold
297-
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
406+.gone:
407+ call draw_lose_screen
408+ ld a, STATE_LOSE
409+ ld (game_state), a
410+ ret
311411
312412 ; ----------------------------------------------------------------------------
313-; print_string — HL=string ($FF-terminated), B=row, C=col.
413+; print_string / print_char.
314414 ; ----------------------------------------------------------------------------
315415 print_string:
316416 .ps:
...
588688 ; ----------------------------------------------------------------------------
589689 ; Level data, state, buffers, and shapes.
590690 ; ----------------------------------------------------------------------------
691+game_state:
692+ defb STATE_TITLE
693+
591694 lamp_data:
592695 defb 4, 3
593696 defb 27, 3
...
662765 defb %00111100
663766 defb %00000000
664767
768+title_text:
769+ defb "GLOAMING"
770+ defb $FF
771+prompt_text:
772+ defb "PRESS SPACE"
773+ defb $FF
665774 win_text:
666775 defb "THE NIGHT IS HELD"
667776 defb $FF
The complete program
; Gloaming — Unit 16: The Title
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 adds a title screen and a TITLE/PLAY/WIN/LOSE state machine.

            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              ; "GLOAMING" centred
PROMPT_COL  equ     10              ; "PRESS SPACE" centred
TITLE_ROW   equ     8
PROMPT_ROW  equ     14
FONT        equ     $3C00

; game states
STATE_TITLE equ     0
STATE_PLAY  equ     1
STATE_WIN   equ     2
STATE_LOSE  equ     3

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           ; SPACE is bit 0 of this half-row

; ============================================================================
; SETUP — border, the title screen, then start the state-machine loop.
; ============================================================================
start:
            ld      a, 0
            out     ($FE), a

            ld      a, STATE_TITLE
            ld      (game_state), a
            call    draw_title_screen

            im      1
            ei

; ----------------------------------------------------------------------------
; THE STATE MACHINE — each frame, do whatever the current state needs.
; ----------------------------------------------------------------------------
main_loop:
            halt
            ld      a, (game_state)
            cp      STATE_TITLE
            jr      z, .do_title
            cp      STATE_PLAY
            jr      z, .do_play
            jr      main_loop       ; WIN / LOSE — just hold the screen
.do_title:
            call    title_step
            jr      main_loop
.do_play:
            call    play_step
            jr      main_loop

; ----------------------------------------------------------------------------
; title_step — wait for SPACE; when pressed, build a fresh game and play.
; ----------------------------------------------------------------------------
title_step:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a            ; SPACE down? (0 = pressed)
            ret     nz              ; not yet — stay on the title
            call    init_game
            ld      a, STATE_PLAY
            ld      (game_state), a
            ret

; ----------------------------------------------------------------------------
; play_step — one frame of the game; may transition to WIN or LOSE.
; ----------------------------------------------------------------------------
play_step:
            call    player_step
            ld      a, (game_state)
            cp      STATE_PLAY
            ret     nz              ; a collision ended the game
            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, STATE_WIN
            ld      (game_state), a
            ret

; ----------------------------------------------------------------------------
; init_game — build a fresh play session (called by "press a key to begin").
; ----------------------------------------------------------------------------
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    ; wipe any leftover pixels (e.g. the title)

            ld      hl, $5800       ; cobbles
            ld      de, $5801
            ld      (hl), COBBLE
            ld      bc, 767
            ldir

            ld      hl, $5820       ; top wall (row 1)
            ld      b, 32
.iwt:
            ld      (hl), WALL
            inc     hl
            djnz    .iwt
            ld      hl, $5AE0       ; bottom wall (row 23)
            ld      b, 32
.iwb:
            ld      (hl), WALL
            inc     hl
            djnz    .iwb
            ld      hl, $5820       ; sides
            ld      b, 23
.iws:
            ld      (hl), WALL
            push    hl
            ld      de, 31
            add     hl, de
            ld      (hl), WALL
            pop     hl
            ld      de, 32
            add     hl, de
            djnz    .iws

            call    draw_pips
            call    draw_lives
            call    draw_lamps
            call    save_under
            call    draw_lamp
            call    save_draught
            call    draw_draught
            ret

; ----------------------------------------------------------------------------
; Screens: title, win, lose.
; ----------------------------------------------------------------------------
draw_title_screen:
            call    clear_bitmap    ; wipe leftover pixels (a finished game's board)
            ld      hl, $5800       ; black field
            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
            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
            ret

; clear_bitmap — zero the pixel area $4000-$57FF (6144 bytes).
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
.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
.nosnuff:
            call    draw_draught
            ret

; ----------------------------------------------------------------------------
; lose_life — drop a life pip; reset the lamplighter, or enter the lose state.
; ----------------------------------------------------------------------------
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, 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

; ----------------------------------------------------------------------------
; Level data, state, buffers, and shapes.
; ----------------------------------------------------------------------------
game_state:
            defb    STATE_TITLE

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

Load it and the title waits, patient, for you:

A black screen with GLOAMING centred in white and PRESS SPACE below it.
The front door: the name, and an invitation. The TITLE state does nothing but show this and watch for space.

Press space and the square assembles itself fresh — init_game builds a clean board, and the state flips to PLAY:

The title holds until space is pressed; then init_game clears the screen, builds the square, lamps, lives and draught, and the state machine switches to PLAY. No ghost of the title bleeds through — the bitmap was wiped before the board was drawn.

When it's wrong, see why

A second screen surfaces second-screen bugs:

  • The title's letters ghost through the game in blue. init_game repainted the colours but not the pixels. Clear the bitmap ($4000$57FF) before drawing the board.
  • Pressing space does nothing. title_step isn't being reached, or reads the wrong half-row. Space is bit 0 of $7FFE, and game_state must start as STATE_TITLE.
  • The game starts immediately, skipping the title. game_state wasn't initialised to STATE_TITLE, or the dispatcher doesn't branch on it.
  • Win/lose now breaks the game. Those states must only set game_state and draw their screen once — not loop forever inside play_step. The dispatcher does the holding.

Before and after

You started with a game that began in the middle and finished with one that begins at a title and enters play on a keypress — and the change is structural, not cosmetic. A byte names the mode; a dispatch loop runs the right step for it; the whole game you built is one of those modes. Win and lose stopped being dead ends and became states the machine moves to. That structure is what the next units hang sound, a "play again", and the rest of the polish on.

Try this: warm the title

The title is white on black. Print it in a warmer colour to set the mood — give print_char a parameter for the attribute, or add a second loop that paints the title cells amber (%01000110) after they're drawn. A title screen is the game's first impression; a little warmth in the word "GLOAMING" goes a long way.

Try this: any key, not just space

title_step only watches space. Make it start on any key: read several half-rows and begin if any shows a held key. (A neat way: read each row in turn and combine the results — if the low five bits aren't all 1, something's pressed.) "Press any key" is friendlier than hunting for one.

Try this: a third state — paused

Add a STATE_PAUSE. During PLAY, watch for a pause key; when pressed, switch to STATE_PAUSE, which does nothing but watch for the key again to switch back. You'll see how cheap a new mode is once the machine exists: a state, a way in, a way out.

What you've learnt

  • A state machine — one byte plus a dispatch loop — lets a program be in distinct modes and move between them cleanly.
  • Your whole game becomes one state (PLAY) among several; win and lose just change the state.
  • Setup becomes init_game, a routine that builds a fresh session — the seed of "play again".
  • A second screen surfaces a real bug: colour isn't pixels, so a new screen must clear the bitmap too.

What's next

The game has a front door and a name. In Unit 17, "A Small Sound", it gets a voice: a short beeper blip when a lamp lights, a colder note when one is snuffed — sound straight from the Spectrum's speaker via OUT ($FE),A. It's the honest, single-blip "before" of the sound driver a later game builds, and the first time Gloaming makes a noise.