A Small Sound
Give Gloaming a voice. The Spectrum's beeper is bit 4 of port $FE — toggle it as a square wave for a tone. A bright blip when a lamp lights, a colder note when one is snuffed. The honest single-blip 'before' of a sound driver.
The game has a front door, a goal, and a threat — but it has never made a sound. This unit gives it the smallest voice the Spectrum has: the beeper. A bright little blip when you light a lamp, a colder, lower note when the draught snuffs one. Two short sounds, a handful of bytes each, and the square suddenly feels alive.
Where we start
Unit 16's titled game — it opens, plays, wins and loses, but it does all of it in silence. We tie a sound to the two moments that matter most: a lamp catching, and a lamp going cold.
The speaker is one bit
The Spectrum's speaker is bit 4 of port $FE — the same port we set the border with. Write a 1 to bit 4 and the cone pushes out; write a 0 and it pulls back. A single write is a click. To make a tone, you flip the bit on and off, over and over: each on-off pair is one cycle of a square wave, and how long you wait between flips sets the pitch — shorter waits, higher note.
So a beep needs two numbers:
beep: B = how many cycles (the length of the sound)
C = the delay between flips (the pitch — smaller is higher)
The routine writes %00010000 (speaker out, border still black), waits C, writes 0, waits C, and repeats B times. That is the whole sound engine.
Blocking, and on purpose
The blip is blocking: while it sounds, the game does nothing else — the loop pauses for those few milliseconds. That is exactly how beeper games worked, and you can hear it as a tiny hitch when a lamp lights. It is the honest "before" of a real sound driver — one that plays music while the game keeps running, which a later, bigger game builds. For a single blip, blocking is right.
One detail: we turn interrupts off (di) around the beep and back on (ei) after. Without that, the 50 Hz frame interrupt would fire mid-tone and jitter it into a warble. Off for the blip, on for the game.
Two notes, two events
The sound is feedback — it is tied to things that happen. blip_lit (short, bright) plays in the same place the lamp lights; blip_snuff (lower, colder) plays where the draught puts one out. Pick different B/C for each and the two events sound different — you can play with your eyes half-closed and still know what just happened.
Milestone — give it a voice
We add a beep routine that toggles bit 4 of $FE as a square wave, a blip_lit and a blip_snuff that call it with their own length and pitch, and one call to each in the branches that already light and snuff a lamp. Nothing else about the game changes — the sound rides on the events that were there all along.
| 1 | 1 | ; Gloaming — Unit 17: A Small Sound | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 is Unit 16's titled game — it plays, but it makes no sound. | |
| 3 | + | ; step-01 gives the game a voice: a beeper blip on light, a colder note on snuff. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 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 | |
| 30 | + | TITLE_COL equ 12 | |
| 31 | + | PROMPT_COL equ 10 | |
| 32 | 32 | TITLE_ROW equ 8 | |
| 33 | 33 | PROMPT_ROW equ 14 | |
| 34 | 34 | FONT equ $3C00 | |
| 35 | 35 | | |
| 36 | - | ; game states | |
| 36 | + | SPEAKER equ %00010000 ; bit 4 of port $FE, border black | |
| 37 | + | | |
| 37 | 38 | STATE_TITLE equ 0 | |
| 38 | 39 | STATE_PLAY equ 1 | |
| 39 | 40 | STATE_WIN equ 2 | |
| ... | |||
| 47 | 48 | KEYS_OP equ $DFFE | |
| 48 | 49 | KEYS_Q equ $FBFE | |
| 49 | 50 | KEYS_A equ $FDFE | |
| 50 | - | KEYS_SPACE equ $7FFE ; SPACE is bit 0 of this half-row | |
| 51 | + | KEYS_SPACE equ $7FFE | |
| 51 | 52 | | |
| 52 | 53 | ; ============================================================================ | |
| 53 | - | ; SETUP — border, the title screen, then start the state-machine loop. | |
| 54 | + | ; SETUP. | |
| 54 | 55 | ; ============================================================================ | |
| 55 | 56 | start: | |
| 56 | 57 | ld a, 0 | |
| 57 | 58 | out ($FE), a | |
| 58 | - | | |
| 59 | 59 | ld a, STATE_TITLE | |
| 60 | 60 | ld (game_state), a | |
| 61 | 61 | call draw_title_screen | |
| 62 | - | | |
| 63 | 62 | im 1 | |
| 64 | 63 | ei | |
| 65 | 64 | | |
| 66 | - | ; ---------------------------------------------------------------------------- | |
| 67 | - | ; THE STATE MACHINE — each frame, do whatever the current state needs. | |
| 68 | - | ; ---------------------------------------------------------------------------- | |
| 69 | 65 | main_loop: | |
| 70 | 66 | halt | |
| 71 | 67 | ld a, (game_state) | |
| ... | |||
| 73 | 69 | jr z, .do_title | |
| 74 | 70 | cp STATE_PLAY | |
| 75 | 71 | jr z, .do_play | |
| 76 | - | jr main_loop ; WIN / LOSE — just hold the screen | |
| 72 | + | jr main_loop | |
| 77 | 73 | .do_title: | |
| 78 | 74 | call title_step | |
| 79 | 75 | jr main_loop | |
| ... | |||
| 81 | 77 | call play_step | |
| 82 | 78 | jr main_loop | |
| 83 | 79 | | |
| 84 | - | ; ---------------------------------------------------------------------------- | |
| 85 | - | ; title_step — wait for SPACE; when pressed, build a fresh game and play. | |
| 86 | - | ; ---------------------------------------------------------------------------- | |
| 87 | 80 | title_step: | |
| 88 | 81 | ld bc, KEYS_SPACE | |
| 89 | 82 | in a, (c) | |
| 90 | - | bit 0, a ; SPACE down? (0 = pressed) | |
| 91 | - | ret nz ; not yet — stay on the title | |
| 83 | + | bit 0, a | |
| 84 | + | ret nz | |
| 92 | 85 | call init_game | |
| 93 | 86 | ld a, STATE_PLAY | |
| 94 | 87 | ld (game_state), a | |
| 95 | 88 | ret | |
| 96 | 89 | | |
| 97 | - | ; ---------------------------------------------------------------------------- | |
| 98 | - | ; play_step — one frame of the game; may transition to WIN or LOSE. | |
| 99 | - | ; ---------------------------------------------------------------------------- | |
| 100 | 90 | play_step: | |
| 101 | 91 | call player_step | |
| 102 | 92 | ld a, (game_state) | |
| 103 | 93 | cp STATE_PLAY | |
| 104 | - | ret nz ; a collision ended the game | |
| 94 | + | ret nz | |
| 105 | 95 | call draught_step | |
| 106 | 96 | ld a, (game_state) | |
| 107 | 97 | cp STATE_PLAY | |
| ... | |||
| 114 | 104 | ld (game_state), a | |
| 115 | 105 | ret | |
| 116 | 106 | | |
| 117 | - | ; ---------------------------------------------------------------------------- | |
| 118 | - | ; init_game — build a fresh play session (called by "press a key to begin"). | |
| 119 | - | ; ---------------------------------------------------------------------------- | |
| 120 | 107 | init_game: | |
| 121 | 108 | xor a | |
| 122 | 109 | ld (lit_count), a | |
| ... | |||
| 136 | 123 | ld a, DRAUGHT_SPEED | |
| 137 | 124 | ld (draught_timer), a | |
| 138 | 125 | | |
| 139 | - | call clear_bitmap ; wipe any leftover pixels (e.g. the title) | |
| 126 | + | call clear_bitmap | |
| 140 | 127 | | |
| 141 | - | ld hl, $5800 ; cobbles | |
| 128 | + | ld hl, $5800 | |
| 142 | 129 | ld de, $5801 | |
| 143 | 130 | ld (hl), COBBLE | |
| 144 | 131 | ld bc, 767 | |
| 145 | 132 | ldir | |
| 146 | 133 | | |
| 147 | - | ld hl, $5820 ; top wall (row 1) | |
| 134 | + | ld hl, $5820 | |
| 148 | 135 | ld b, 32 | |
| 149 | 136 | .iwt: | |
| 150 | 137 | ld (hl), WALL | |
| 151 | 138 | inc hl | |
| 152 | 139 | djnz .iwt | |
| 153 | - | ld hl, $5AE0 ; bottom wall (row 23) | |
| 140 | + | ld hl, $5AE0 | |
| 154 | 141 | ld b, 32 | |
| 155 | 142 | .iwb: | |
| 156 | 143 | ld (hl), WALL | |
| 157 | 144 | inc hl | |
| 158 | 145 | djnz .iwb | |
| 159 | - | ld hl, $5820 ; sides | |
| 146 | + | ld hl, $5820 | |
| 160 | 147 | ld b, 23 | |
| 161 | 148 | .iws: | |
| 162 | 149 | ld (hl), WALL | |
| ... | |||
| 179 | 166 | ret | |
| 180 | 167 | | |
| 181 | 168 | ; ---------------------------------------------------------------------------- | |
| 182 | - | ; Screens: title, win, lose. | |
| 169 | + | ; beep — B = cycles, C = pitch delay. Bit 4 of $FE is the speaker. | |
| 170 | + | ; ---------------------------------------------------------------------------- | |
| 171 | + | beep: | |
| 172 | + | di | |
| 173 | + | .bcyc: | |
| 174 | + | ld a, SPEAKER ; speaker out, border black | |
| 175 | + | out ($FE), a | |
| 176 | + | ld a, c | |
| 177 | + | .bd1: | |
| 178 | + | dec a | |
| 179 | + | jr nz, .bd1 | |
| 180 | + | xor a ; speaker back (and border black) | |
| 181 | + | out ($FE), a | |
| 182 | + | ld a, c | |
| 183 | + | .bd2: | |
| 184 | + | dec a | |
| 185 | + | jr nz, .bd2 | |
| 186 | + | djnz .bcyc | |
| 187 | + | ei | |
| 188 | + | ret | |
| 189 | + | | |
| 190 | + | blip_lit: | |
| 191 | + | ld b, $20 | |
| 192 | + | ld c, $18 ; short, bright | |
| 193 | + | jp beep | |
| 194 | + | | |
| 195 | + | blip_snuff: | |
| 196 | + | ld b, $1A | |
| 197 | + | ld c, $40 ; lower, colder | |
| 198 | + | jp beep | |
| 199 | + | | |
| 200 | + | ; ---------------------------------------------------------------------------- | |
| 201 | + | ; Screens. | |
| 183 | 202 | ; ---------------------------------------------------------------------------- | |
| 184 | 203 | draw_title_screen: | |
| 185 | - | call clear_bitmap ; wipe leftover pixels (a finished game's board) | |
| 186 | - | ld hl, $5800 ; black field | |
| 204 | + | call clear_bitmap | |
| 205 | + | ld hl, $5800 | |
| 187 | 206 | ld de, $5801 | |
| 188 | 207 | ld (hl), %00000000 | |
| 189 | 208 | ld bc, 767 | |
| ... | |||
| 218 | 237 | call print_string | |
| 219 | 238 | ret | |
| 220 | 239 | | |
| 221 | - | ; clear_bitmap — zero the pixel area $4000-$57FF (6144 bytes). | |
| 222 | 240 | clear_bitmap: | |
| 223 | 241 | ld hl, $4000 | |
| 224 | 242 | ld de, $4001 | |
| ... | |||
| 228 | 246 | ret | |
| 229 | 247 | | |
| 230 | 248 | ; ---------------------------------------------------------------------------- | |
| 231 | - | ; player_step. | |
| 249 | + | ; player_step — now blips when a lamp lights. | |
| 232 | 250 | ; ---------------------------------------------------------------------------- | |
| 233 | 251 | player_step: | |
| 234 | 252 | ld a, (lamp_col) | |
| ... | |||
| 299 | 317 | ld a, LAMP_LIT | |
| 300 | 318 | ld (under_lamp + 8), a | |
| 301 | 319 | call light_pip | |
| 320 | + | call blip_lit ; a bright little note | |
| 302 | 321 | .pdrawn: | |
| 303 | 322 | call draw_lamp | |
| 304 | 323 | ret | |
| 305 | 324 | | |
| 306 | 325 | ; ---------------------------------------------------------------------------- | |
| 307 | - | ; draught_step. | |
| 326 | + | ; draught_step — now blips a colder note when it snuffs a lamp. | |
| 308 | 327 | ; ---------------------------------------------------------------------------- | |
| 309 | 328 | draught_step: | |
| 310 | 329 | ld a, (draught_timer) | |
| ... | |||
| 375 | 394 | ld a, LAMP_UNLIT | |
| 376 | 395 | ld (under_draught + 8), a | |
| 377 | 396 | call unlight_pip | |
| 397 | + | call blip_snuff ; a colder note | |
| 378 | 398 | .nosnuff: | |
| 379 | 399 | call draw_draught | |
| 380 | 400 | ret | |
| 381 | 401 | | |
| 382 | - | ; ---------------------------------------------------------------------------- | |
| 383 | - | ; lose_life — drop a life pip; reset the lamplighter, or enter the lose state. | |
| 384 | - | ; ---------------------------------------------------------------------------- | |
| 385 | 402 | lose_life: | |
| 386 | 403 | ld a, (lives) | |
| 387 | 404 | dec a | |
| ... | |||
| 394 | 411 | ld a, (lives) | |
| 395 | 412 | or a | |
| 396 | 413 | jr z, .gone | |
| 397 | - | | |
| 398 | 414 | call restore_under | |
| 399 | 415 | ld a, START_COL | |
| 400 | 416 | ld (lamp_col), a | |
| ... | |||
| 686 | 702 | ret | |
| 687 | 703 | | |
| 688 | 704 | ; ---------------------------------------------------------------------------- | |
| 689 | - | ; Level data, state, buffers, and shapes. | |
| 705 | + | ; Data. | |
| 690 | 706 | ; ---------------------------------------------------------------------------- | |
| 691 | 707 | game_state: | |
| 692 | 708 | defb STATE_TITLE |
The complete program
; Gloaming — Unit 17: A Small Sound
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 gives the game a voice: a beeper blip on light, a colder note on snuff.
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
FONT equ $3C00
SPEAKER equ %00010000 ; bit 4 of port $FE, border black
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
; ============================================================================
; SETUP.
; ============================================================================
start:
ld a, 0
out ($FE), a
ld a, STATE_TITLE
ld (game_state), a
call draw_title_screen
im 1
ei
main_loop:
halt
ld a, (game_state)
cp STATE_TITLE
jr z, .do_title
cp STATE_PLAY
jr z, .do_play
jr main_loop
.do_title:
call title_step
jr main_loop
.do_play:
call play_step
jr main_loop
title_step:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
ret nz
call init_game
ld a, STATE_PLAY
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, 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
ld hl, $5820
ld b, 32
.iwt:
ld (hl), WALL
inc hl
djnz .iwt
ld hl, $5AE0
ld b, 32
.iwb:
ld (hl), WALL
inc hl
djnz .iwb
ld hl, $5820
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
; ----------------------------------------------------------------------------
; beep — B = cycles, C = pitch delay. Bit 4 of $FE is the speaker.
; ----------------------------------------------------------------------------
beep:
di
.bcyc:
ld a, SPEAKER ; speaker out, border black
out ($FE), a
ld a, c
.bd1:
dec a
jr nz, .bd1
xor a ; speaker back (and border black)
out ($FE), a
ld a, c
.bd2:
dec a
jr nz, .bd2
djnz .bcyc
ei
ret
blip_lit:
ld b, $20
ld c, $18 ; short, bright
jp beep
blip_snuff:
ld b, $1A
ld c, $40 ; lower, colder
jp beep
; ----------------------------------------------------------------------------
; Screens.
; ----------------------------------------------------------------------------
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
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:
ld hl, $4000
ld de, $4001
ld (hl), 0
ld bc, 6143
ldir
ret
; ----------------------------------------------------------------------------
; player_step — now blips when a lamp lights.
; ----------------------------------------------------------------------------
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 ; a bright little note
.pdrawn:
call draw_lamp
ret
; ----------------------------------------------------------------------------
; draught_step — now blips a colder note when it snuffs a lamp.
; ----------------------------------------------------------------------------
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 ; a colder note
.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, 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
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
The screen looks like any moment of play — a lamp gold beside the lamplighter, the draught adrift, the tally and lives in the HUD — but now each lamp you light announces itself:
Here are the two blips from lighting two lamps — bright, short, and now resting on true silence:
When it's wrong, see why
A one-bit speaker fails in a few tell-tale ways:
- No sound at all. The speaker is bit 4 of
$FE. Confirm the beep writes%00010000then%00000000, and thatblip_litis reached in the lamp-lit branch. - A click, not a note. You are toggling once, not repeatedly. The tone comes from the
Bloop flipping the bit many times — check thedjnz. - The tone warbles. The frame interrupt is jittering it. Wrap the beep in
di…ei. - The game hitches hard on a blip. The sound is too long — shrink
B. (Some hitch is expected; the blip blocks by design.)
Before and after
You started with a game that played in silence and finished with one that speaks at the two moments that carry its stakes — a lamp lit, a lamp lost. The change is small in bytes and large in feel: a square wave is one bit toggled on a delay, the pitch is the delay, the length is the loop count, and tying a distinct note to a distinct event turns a silent diagram into something that responds. The sound rode on events you already had — it needed a voice, not new machinery.
Try this: tune the blip
The blip is two numbers. In blip_lit, shrink C for a higher note, grow B for a longer one. Find a tick that feels bright without being shrill. A few values in and you will have an ear for what the pair does — that is the whole of beeper sound design.
Try this: a two-tone chime
Make lighting a lamp a little richer: call beep twice in a row with two different pitches — a quick low-then-high gives a cheerful "ding". Chaining a few short beeps is how beeper games built their fanfares, all from this one routine.
Try this: a win fanfare
On a win, play something celebratory before the line prints. In the win routine, chain three or four rising beeps (drop C a little each call) for a short triumphant run. It is the same beep, just sequenced — and a tiny tune is a big lift at the end of a game.
What you've learnt
- The beeper is bit 4 of port
$FE; a tone is that bit toggled as a square wave, the flip-delay setting the pitch. - A blocking blip pauses the game for a few milliseconds — authentic, and the honest "before" of a non-blocking sound driver.
di/eiaround the beep keeps the 50 Hz interrupt from warbling the tone.- Sound is feedback — tie distinct notes to distinct events and the game speaks.
What's next
Gloaming looks and sounds like a game now. Unit 18, "The Square Warms", adds the last layer of polish to the play itself: atmosphere. As lamps light, the square warms — small attribute touches that make the lit town feel different from the cold, dark one you started in. It is a first taste of the mood-making that a later, larger game makes its whole craft, built from the colour writes you have used since Unit 1.