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

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.

85% of Gloaming

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.

Step 1: a beeper, and a blip on every lamp lit or snuffed
+56-40
11 ; Gloaming — Unit 17: A Small Sound
22 ; 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.
44
55 org 32768
66
...
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
30+TITLE_COL equ 12
31+PROMPT_COL equ 10
3232 TITLE_ROW equ 8
3333 PROMPT_ROW equ 14
3434 FONT equ $3C00
3535
36-; game states
36+SPEAKER equ %00010000 ; bit 4 of port $FE, border black
37+
3738 STATE_TITLE equ 0
3839 STATE_PLAY equ 1
3940 STATE_WIN equ 2
...
4748 KEYS_OP equ $DFFE
4849 KEYS_Q equ $FBFE
4950 KEYS_A equ $FDFE
50-KEYS_SPACE equ $7FFE ; SPACE is bit 0 of this half-row
51+KEYS_SPACE equ $7FFE
5152
5253 ; ============================================================================
53-; SETUP — border, the title screen, then start the state-machine loop.
54+; SETUP.
5455 ; ============================================================================
5556 start:
5657 ld a, 0
5758 out ($FE), a
58-
5959 ld a, STATE_TITLE
6060 ld (game_state), a
6161 call draw_title_screen
62-
6362 im 1
6463 ei
6564
66-; ----------------------------------------------------------------------------
67-; THE STATE MACHINE — each frame, do whatever the current state needs.
68-; ----------------------------------------------------------------------------
6965 main_loop:
7066 halt
7167 ld a, (game_state)
...
7369 jr z, .do_title
7470 cp STATE_PLAY
7571 jr z, .do_play
76- jr main_loop ; WIN / LOSE — just hold the screen
72+ jr main_loop
7773 .do_title:
7874 call title_step
7975 jr main_loop
...
8177 call play_step
8278 jr main_loop
8379
84-; ----------------------------------------------------------------------------
85-; title_step — wait for SPACE; when pressed, build a fresh game and play.
86-; ----------------------------------------------------------------------------
8780 title_step:
8881 ld bc, KEYS_SPACE
8982 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
9285 call init_game
9386 ld a, STATE_PLAY
9487 ld (game_state), a
9588 ret
9689
97-; ----------------------------------------------------------------------------
98-; play_step — one frame of the game; may transition to WIN or LOSE.
99-; ----------------------------------------------------------------------------
10090 play_step:
10191 call player_step
10292 ld a, (game_state)
10393 cp STATE_PLAY
104- ret nz ; a collision ended the game
94+ ret nz
10595 call draught_step
10696 ld a, (game_state)
10797 cp STATE_PLAY
...
114104 ld (game_state), a
115105 ret
116106
117-; ----------------------------------------------------------------------------
118-; init_game — build a fresh play session (called by "press a key to begin").
119-; ----------------------------------------------------------------------------
120107 init_game:
121108 xor a
122109 ld (lit_count), a
...
136123 ld a, DRAUGHT_SPEED
137124 ld (draught_timer), a
138125
139- call clear_bitmap ; wipe any leftover pixels (e.g. the title)
126+ call clear_bitmap
140127
141- ld hl, $5800 ; cobbles
128+ ld hl, $5800
142129 ld de, $5801
143130 ld (hl), COBBLE
144131 ld bc, 767
145132 ldir
146133
147- ld hl, $5820 ; top wall (row 1)
134+ ld hl, $5820
148135 ld b, 32
149136 .iwt:
150137 ld (hl), WALL
151138 inc hl
152139 djnz .iwt
153- ld hl, $5AE0 ; bottom wall (row 23)
140+ ld hl, $5AE0
154141 ld b, 32
155142 .iwb:
156143 ld (hl), WALL
157144 inc hl
158145 djnz .iwb
159- ld hl, $5820 ; sides
146+ ld hl, $5820
160147 ld b, 23
161148 .iws:
162149 ld (hl), WALL
...
179166 ret
180167
181168 ; ----------------------------------------------------------------------------
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.
183202 ; ----------------------------------------------------------------------------
184203 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
187206 ld de, $5801
188207 ld (hl), %00000000
189208 ld bc, 767
...
218237 call print_string
219238 ret
220239
221-; clear_bitmap — zero the pixel area $4000-$57FF (6144 bytes).
222240 clear_bitmap:
223241 ld hl, $4000
224242 ld de, $4001
...
228246 ret
229247
230248 ; ----------------------------------------------------------------------------
231-; player_step.
249+; player_step — now blips when a lamp lights.
232250 ; ----------------------------------------------------------------------------
233251 player_step:
234252 ld a, (lamp_col)
...
299317 ld a, LAMP_LIT
300318 ld (under_lamp + 8), a
301319 call light_pip
320+ call blip_lit ; a bright little note
302321 .pdrawn:
303322 call draw_lamp
304323 ret
305324
306325 ; ----------------------------------------------------------------------------
307-; draught_step.
326+; draught_step — now blips a colder note when it snuffs a lamp.
308327 ; ----------------------------------------------------------------------------
309328 draught_step:
310329 ld a, (draught_timer)
...
375394 ld a, LAMP_UNLIT
376395 ld (under_draught + 8), a
377396 call unlight_pip
397+ call blip_snuff ; a colder note
378398 .nosnuff:
379399 call draw_draught
380400 ret
381401
382-; ----------------------------------------------------------------------------
383-; lose_life — drop a life pip; reset the lamplighter, or enter the lose state.
384-; ----------------------------------------------------------------------------
385402 lose_life:
386403 ld a, (lives)
387404 dec a
...
394411 ld a, (lives)
395412 or a
396413 jr z, .gone
397-
398414 call restore_under
399415 ld a, START_COL
400416 ld (lamp_col), a
...
686702 ret
687703
688704 ; ----------------------------------------------------------------------------
689-; Level data, state, buffers, and shapes.
705+; Data.
690706 ; ----------------------------------------------------------------------------
691707 game_state:
692708 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:

The square in play: a gold lit lamp beside the white lamplighter, the cyan draught drifting at the upper right, and the HUD tally and red life pips along the top.
A lamp caught, mid-play. The picture is unchanged from Unit 16 — the difference is that lighting this lamp made a sound.

Here are the two blips from lighting two lamps — bright, short, and now resting on true silence:

ZX Spectrum beeper · port $FE bit 4
Lighting two lamps — the beeper blip

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 %00010000 then %00000000, and that blip_lit is reached in the lamp-lit branch.
  • A click, not a note. You are toggling once, not repeatedly. The tone comes from the B loop flipping the bit many times — check the djnz.
  • The tone warbles. The frame interrupt is jittering it. Wrap the beep in diei.
  • 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/ei around 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.