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.
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.
| 1 | 1 | ; Gloaming — Unit 16: The Title | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 20 | 20 | NUM_LAMPS equ 8 | |
| 21 | 21 | | |
| 22 | 22 | LIVES equ 3 | |
| 23 | - | LIFE_PIP equ %01010000 ; BRIGHT, PAPER red — a life | |
| 24 | - | LIFE_BASE equ $5800 + 28 ; row 0, columns 28-30 | |
| 23 | + | LIFE_PIP equ %01010000 | |
| 24 | + | LIFE_BASE equ $5800 + 28 | |
| 25 | 25 | | |
| 26 | 26 | MSG_ATTR equ %01000111 | |
| 27 | 27 | MSG_ROW equ 11 | |
| 28 | 28 | WIN_COL equ 7 | |
| 29 | 29 | 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 | |
| 30 | 34 | 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 | |
| 31 | 41 | | |
| 32 | 42 | START_COL equ 15 | |
| 33 | 43 | START_ROW equ 11 | |
| ... | |||
| 37 | 47 | KEYS_OP equ $DFFE | |
| 38 | 48 | KEYS_Q equ $FBFE | |
| 39 | 49 | KEYS_A equ $FDFE | |
| 50 | + | KEYS_SPACE equ $7FFE ; SPACE is bit 0 of this half-row | |
| 40 | 51 | | |
| 41 | 52 | ; ============================================================================ | |
| 42 | - | ; SETUP — runs once. | |
| 53 | + | ; SETUP — border, the title screen, then start the state-machine loop. | |
| 43 | 54 | ; ============================================================================ | |
| 44 | 55 | start: | |
| 45 | 56 | ld a, 0 | |
| 46 | 57 | out ($FE), a | |
| 47 | 58 | | |
| 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 | |
| 49 | 142 | ld de, $5801 | |
| 50 | 143 | ld (hl), COBBLE | |
| 51 | 144 | ld bc, 767 | |
| 52 | 145 | ldir | |
| 53 | 146 | | |
| 54 | - | ld hl, $5820 | |
| 147 | + | ld hl, $5820 ; top wall (row 1) | |
| 55 | 148 | ld b, 32 | |
| 56 | - | .top: | |
| 149 | + | .iwt: | |
| 57 | 150 | ld (hl), WALL | |
| 58 | 151 | inc hl | |
| 59 | - | djnz .top | |
| 60 | - | | |
| 61 | - | ld hl, $5AE0 | |
| 152 | + | djnz .iwt | |
| 153 | + | ld hl, $5AE0 ; bottom wall (row 23) | |
| 62 | 154 | ld b, 32 | |
| 63 | - | .bottom: | |
| 155 | + | .iwb: | |
| 64 | 156 | ld (hl), WALL | |
| 65 | 157 | inc hl | |
| 66 | - | djnz .bottom | |
| 67 | - | | |
| 68 | - | ld hl, $5820 | |
| 158 | + | djnz .iwb | |
| 159 | + | ld hl, $5820 ; sides | |
| 69 | 160 | ld b, 23 | |
| 70 | - | .sides: | |
| 161 | + | .iws: | |
| 71 | 162 | ld (hl), WALL | |
| 72 | 163 | push hl | |
| 73 | 164 | ld de, 31 | |
| ... | |||
| 76 | 167 | pop hl | |
| 77 | 168 | ld de, 32 | |
| 78 | 169 | add hl, de | |
| 79 | - | djnz .sides | |
| 170 | + | djnz .iws | |
| 80 | 171 | | |
| 81 | 172 | call draw_pips | |
| 82 | 173 | call draw_lives | |
| ... | |||
| 85 | 176 | call draw_lamp | |
| 86 | 177 | call save_draught | |
| 87 | 178 | call draw_draught | |
| 179 | + | ret | |
| 88 | 180 | | |
| 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 | |
| 94 | 200 | | |
| 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 | |
| 103 | 229 | | |
| 104 | 230 | ; ---------------------------------------------------------------------------- | |
| 105 | - | ; player_step — move the lamplighter; a step onto the draught costs a life. | |
| 231 | + | ; player_step. | |
| 106 | 232 | ; ---------------------------------------------------------------------------- | |
| 107 | 233 | player_step: | |
| 108 | 234 | ld a, (lamp_col) | |
| ... | |||
| 147 | 273 | ld a, (tcol) | |
| 148 | 274 | ld c, a | |
| 149 | 275 | call wall_at | |
| 150 | - | ret nz ; wall blocks | |
| 276 | + | ret nz | |
| 151 | 277 | | |
| 152 | - | ; would the step land on the draught? then it's a collision | |
| 153 | 278 | ld a, (tcol) | |
| 154 | 279 | ld hl, draught_col | |
| 155 | 280 | cp (hl) | |
| ... | |||
| 179 | 304 | ret | |
| 180 | 305 | | |
| 181 | 306 | ; ---------------------------------------------------------------------------- | |
| 182 | - | ; draught_step — drift, bounce, snuff; a step onto the lamplighter costs a life. | |
| 307 | + | ; draught_step. | |
| 183 | 308 | ; ---------------------------------------------------------------------------- | |
| 184 | 309 | draught_step: | |
| 185 | 310 | ld a, (draught_timer) | |
| ... | |||
| 215 | 340 | neg | |
| 216 | 341 | ld (draught_dy), a | |
| 217 | 342 | .vok: | |
| 218 | - | ; work out the target cell | |
| 219 | 343 | ld a, (draught_col) | |
| 220 | 344 | ld b, a | |
| 221 | 345 | ld a, (draught_dx) | |
| ... | |||
| 227 | 351 | add a, b | |
| 228 | 352 | ld (dtrow), a | |
| 229 | 353 | | |
| 230 | - | ; would it land on the lamplighter? then it's a collision | |
| 231 | 354 | ld a, (dtcol) | |
| 232 | 355 | ld hl, lamp_col | |
| 233 | 356 | cp (hl) | |
| ... | |||
| 237 | 360 | cp (hl) | |
| 238 | 361 | jr nz, .dmove | |
| 239 | 362 | call lose_life | |
| 240 | - | ret ; don't move onto him | |
| 363 | + | ret | |
| 241 | 364 | | |
| 242 | 365 | .dmove: | |
| 243 | 366 | call restore_draught | |
| ... | |||
| 257 | 380 | ret | |
| 258 | 381 | | |
| 259 | 382 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 261 | 384 | ; ---------------------------------------------------------------------------- | |
| 262 | 385 | lose_life: | |
| 263 | 386 | ld a, (lives) | |
| 264 | 387 | dec a | |
| 265 | 388 | ld (lives), a | |
| 266 | - | ld e, a ; index of the life pip to remove | |
| 389 | + | ld e, a | |
| 267 | 390 | ld d, 0 | |
| 268 | 391 | ld hl, LIFE_BASE | |
| 269 | 392 | add hl, de | |
| 270 | - | ld (hl), COBBLE ; the pip goes dark | |
| 393 | + | ld (hl), COBBLE | |
| 271 | 394 | ld a, (lives) | |
| 272 | 395 | or a | |
| 273 | - | jp z, lose ; out of lives — night falls | |
| 396 | + | jr z, .gone | |
| 274 | 397 | | |
| 275 | - | ; otherwise send the lamplighter back to the start, clear of danger | |
| 276 | 398 | call restore_under | |
| 277 | 399 | ld a, START_COL | |
| 278 | 400 | ld (lamp_col), a | |
| ... | |||
| 281 | 403 | call save_under | |
| 282 | 404 | call draw_lamp | |
| 283 | 405 | 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 | |
| 311 | 411 | | |
| 312 | 412 | ; ---------------------------------------------------------------------------- | |
| 313 | - | ; print_string — HL=string ($FF-terminated), B=row, C=col. | |
| 413 | + | ; print_string / print_char. | |
| 314 | 414 | ; ---------------------------------------------------------------------------- | |
| 315 | 415 | print_string: | |
| 316 | 416 | .ps: | |
| ... | |||
| 588 | 688 | ; ---------------------------------------------------------------------------- | |
| 589 | 689 | ; Level data, state, buffers, and shapes. | |
| 590 | 690 | ; ---------------------------------------------------------------------------- | |
| 691 | + | game_state: | |
| 692 | + | defb STATE_TITLE | |
| 693 | + | | |
| 591 | 694 | lamp_data: | |
| 592 | 695 | defb 4, 3 | |
| 593 | 696 | defb 27, 3 | |
| ... | |||
| 662 | 765 | defb %00111100 | |
| 663 | 766 | defb %00000000 | |
| 664 | 767 | | |
| 768 | + | title_text: | |
| 769 | + | defb "GLOAMING" | |
| 770 | + | defb $FF | |
| 771 | + | prompt_text: | |
| 772 | + | defb "PRESS SPACE" | |
| 773 | + | defb $FF | |
| 665 | 774 | win_text: | |
| 666 | 775 | defb "THE NIGHT IS HELD" | |
| 667 | 776 | 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:
Press space and the square assembles itself fresh — init_game builds a clean board, and the state flips to PLAY:
When it's wrong, see why
A second screen surfaces second-screen bugs:
- The title's letters ghost through the game in blue.
init_gamerepainted the colours but not the pixels. Clear the bitmap ($4000–$57FF) before drawing the board. - Pressing space does nothing.
title_stepisn't being reached, or reads the wrong half-row. Space is bit 0 of$7FFE, andgame_statemust start asSTATE_TITLE. - The game starts immediately, skipping the title.
game_statewasn't initialised toSTATE_TITLE, or the dispatcher doesn't branch on it. - Win/lose now breaks the game. Those states must only set
game_stateand draw their screen once — not loop forever insideplay_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.