Hazards and Lives
The FLASH hazard cell finally hurts. BIT 7 detects danger, DEC (HL) counts down lives, and the player resets to start on contact. Three lives — then game over.
The red flashing cell has been in the room since Unit 8. It looks dangerous — FLASH makes it alternate between red and black. But walking onto it does nothing. The hazard is decoration. That changes now.
Step on the hazard and you lose a life. Three lives, shown on the score line. When they’re gone, it’s game over. The room has consequences for the first time.
BIT 7 — Testing FLASH
You’ve used BIT 6 to detect BRIGHT (treasure). The FLASH bit is bit 7 — one position higher. Same instruction, different bit number:
ld a, (player_under)
bit 7, a ; FLASH = hazard?
jr z, .not_hazard ; No FLASH — safe
The pattern is identical to treasure detection. Read the attribute, test a single bit, branch on the result. The attribute byte encodes the entire game world — each bit has a meaning:
| Bit | Name | Test | Meaning |
|---|---|---|---|
| 7 | FLASH | BIT 7 | Hazard — lose a life |
| 6 | BRIGHT | BIT 6 | Treasure — collect it |
| 5-3 | PAPER | AND $38 | Background colour |
| 2-0 | INK | AND $07 | Foreground colour (wall check) |
Four different checks on one byte. No lookup tables, no sprite collision boxes. The Spectrum’s attribute system is the collision system.
When to Check
The hazard check happens after the player is drawn at their new position. If player_under has FLASH set, the player stepped on danger:
; Hazard detection — BIT 7 tests the FLASH bit
;
; FLASH (bit 7) means danger. After drawing the player,
; check what's underneath. If it's flashing, the player
; stepped on a hazard.
ld a, (player_under)
bit 7, a ; FLASH = hazard?
jr z, .not_hazard
; Erase player from hazard cell
ld hl, (player_scr)
ld b, 8
ld a, 0
.derase: ld (hl), a
inc h
djnz .derase
ld hl, (player_att)
ld a, (player_under)
ld (hl), a ; Restore hazard attribute
; Lose a life
ld hl, lives
dec (hl) ; DEC (HL) sets flags!
jp z, .game_over ; Z = lives reached zero
; ... reset player to start ...
The sequence: erase the player from the hazard cell (restore its attribute), then decrement lives. This order matters — the hazard cell needs its FLASH attribute back before the player vanishes, otherwise the flashing stops.
DEC (HL) Sets Flags
; DEC (HL) vs DEC HL — flags
;
; DEC HL (16-bit) does NOT set flags. You need the
; three-instruction idiom: DEC HL / LD A,H / OR L.
;
; DEC (HL) (8-bit memory) DOES set flags. Z is set
; when the byte at HL reaches zero. One instruction
; to decrement and test.
ld hl, lives
dec (hl) ; Decrement byte, set Z if zero
jr z, .game_over ; Lives exhausted
; Compare:
; DEC HL — 16-bit, no flags (Unit 12: testing HL counter)
; DEC (HL) — 8-bit memory, flags (this unit: testing lives)
; DEC A — 8-bit register, flags
DEC (HL) decrements the byte at address HL and sets the Z flag if the result is zero. One instruction to decrement and test. This is unlike DEC HL (the 16-bit version from Unit 12) which does NOT set flags and needs the LD A, H / OR L idiom.
The distinction matters. The Z80 has two kinds of DEC:
DEC randDEC (HL)— 8-bit, sets flags (Z, S, H, N)DEC rr— 16-bit register pair, does NOT set flags
The same pattern applies to INC. INC (HL) (used for treasure count in Unit 10) also sets flags, though we didn’t need them then.
Death and Reset
When the player touches a hazard:
- Erase — clear the player’s bitmap from the hazard cell, restore the hazard attribute
- Decrement —
DEC (HL)on the lives counter - Check zero —
JP Z, .game_overif no lives remain - Sound — a short descending two-note tone (the opposite of the collect chirp)
- Reset — restore all position variables to their starting values
; Reset to start position
ld a, START_ROW
ld (player_row), a
ld a, START_COL
ld (player_col), a
ld hl, START_SCR
ld (player_scr), hl
ld hl, START_ATT
ld (player_att), hl
ld a, (START_ATT)
ld (player_under), a
Six variables need resetting: row, column, screen address, attribute address, and player_under. Miss any one and the player appears at the wrong position or corrupts a cell. The reset is verbose but simple — load constant, store to variable, repeat.
The last line reads the current value at START_ATT and saves it as player_under. This is the same pattern used during initialisation — save what’s underneath before the player is drawn on top.
Game Over
When lives reaches zero, the game ends differently from victory:
.game_over:
; Four descending notes — the opposite of the victory fanfare
ld hl, 100
ld e, 18 ; High
call beep
ld hl, 100
ld e, 25 ; Mid-high
call beep
ld hl, 100
ld e, 35 ; Mid-low
call beep
ld hl, 300
ld e, 50 ; Sustained low
call beep
The descending fanfare mirrors the ascending victory sound. Same structure (four notes, last one sustained), reversed direction. The border turns red instead of green. “GAME OVER!” replaces the score line.
Two ways to leave the main loop, two endings:
| Ending | Condition | Sound | Border | Message |
|---|---|---|---|---|
| Victory | Exit + all treasure | Ascending | Green | ROOM COMPLETE! |
| Game over | Lives = 0 | Descending | Red | GAME OVER! |
Lives on the Score Line
The score display now shows both treasure and lives:
; "TREASURE 0/3 L:3"
ld hl, score_text ; "TREASURE "
call print_str
; ... treasure count ...
ld hl, lives_text ; "L:"
call print_str
ld a, (lives)
add a, '0'
call print_char
print_score prints 17 characters total. The SCORE_LEN constant (which sets how many attribute cells get the white-on-black treatment) is now 18 to cover the full width.
The victory and game over messages are padded with spaces to overwrite all 17 characters cleanly. Without padding, leftover characters from the score would remain visible.
The Complete Code
; ============================================================================
; SHADOWKEEP — Unit 15: Hazards and Lives
; ============================================================================
; The FLASH hazard cell finally bites. Step on it and you lose a life.
; Three lives, shown on the score line. When they're gone, the game is
; over. The room has stakes for the first time.
;
; BIT 7 tests the FLASH bit — the same pattern as BIT 6 for BRIGHT.
; DEC (HL) decrements the lives counter and sets flags in one
; instruction. Unlike DEC HL (16-bit, no flags), DEC (HL) is an
; 8-bit memory operation that sets Z when the result is zero.
; ============================================================================
org 32768
; Attribute values
WALL equ $09 ; PAPER 1 (blue) + INK 1
FLOOR equ $38 ; PAPER 7 (white) + INK 0
TREASURE equ $70 ; BRIGHT + PAPER 6 (yellow) + INK 0
HAZARD equ $90 ; FLASH + PAPER 2 (red) + INK 0
EXIT equ $28 ; PAPER 5 (cyan) + INK 0
PLAYER equ $3a ; PAPER 7 (white) + INK 2 (red)
; Collision
WALL_INK equ 1 ; INK colour that means "wall"
; Room
ROOM_TOP equ 10
ROOM_LEFT equ 12
ROOM_WIDTH equ 9
ROOM_HEIGHT equ 5
ROW_SKIP equ 23 ; 32 - ROOM_WIDTH
TOTAL_TREASURE equ 3 ; Treasures in the room
START_LIVES equ 3
; Screen addresses for starting position (row 12, col 13)
START_ROW equ 12
START_COL equ 13
START_SCR equ $488d
START_ATT equ $598d
; Score display position (row 23, col 10)
SCORE_SCR equ $50ea ; Screen bitmap: row 23, col 10
SCORE_ATT equ $5aea ; Attribute: row 23, col 10
SCORE_LEN equ 18 ; Wide enough for "TREASURE 0/3 L:3"
; Keyboard rows
KEY_ROW_QT equ $fb ; Q, W, E, R, T
KEY_ROW_AG equ $fd ; A, S, D, F, G
KEY_ROW_PY equ $df ; P, O, I, U, Y
; ROM font
FONT_BASE equ $3c00 ; Character set in ROM
; ----------------------------------------------------------------------------
; Entry point
; ----------------------------------------------------------------------------
start:
ld a, 0
out ($fe), a
; Clear screen
ld hl, $4000
ld de, $4001
ld bc, 6911
ld (hl), 0
ldir
; ==================================================================
; Draw room from data table
; ==================================================================
ld hl, $594c ; Attribute address: row 10, col 12
ld de, room_data
ld c, ROOM_HEIGHT
.row: ld b, ROOM_WIDTH
.cell: ld a, (de)
ld (hl), a
inc de
inc hl
djnz .cell
push de
ld de, ROW_SKIP
add hl, de
pop de
dec c
jr nz, .row
; ==================================================================
; Draw the player at starting position
; ==================================================================
ld a, (START_ATT)
ld (player_under), a
ld hl, START_SCR
ld de, player_gfx
ld b, 8
.initdraw: ld a, (de)
ld (hl), a
inc de
inc h
djnz .initdraw
ld a, PLAYER
ld (START_ATT), a
; Set attributes for score line
ld hl, SCORE_ATT
ld a, $47 ; BRIGHT + INK 7 (white on black)
ld b, SCORE_LEN
.sattr: ld (hl), a
inc hl
djnz .sattr
call print_score ; Show initial status
; ==================================================================
; Main loop
; ==================================================================
.loop: halt
; --- Erase player at current position ---
ld hl, (player_scr)
ld b, 8
ld a, 0
.erase: ld (hl), a
inc h
djnz .erase
ld hl, (player_att)
ld a, (player_under)
ld (hl), a
; --- Check Q (up) ---
ld a, KEY_ROW_QT
in a, ($fe)
bit 0, a
jr nz, .not_q
ld hl, (player_att)
ld de, $ffe0 ; -32 (one row up)
add hl, de
ld a, (hl)
and $07
cp WALL_INK
jr z, .not_q
call check_collect
ld (player_att), hl
ld hl, (player_scr)
ld de, $ffe0
add hl, de
ld (player_scr), hl
ld a, (player_row)
dec a
ld (player_row), a
.not_q:
; --- Check A (down) ---
ld a, KEY_ROW_AG
in a, ($fe)
bit 0, a
jr nz, .not_a
ld hl, (player_att)
ld de, 32
add hl, de
ld a, (hl)
and $07
cp WALL_INK
jr z, .not_a
call check_collect
ld (player_att), hl
ld hl, (player_scr)
ld de, 32
add hl, de
ld (player_scr), hl
ld a, (player_row)
inc a
ld (player_row), a
.not_a:
; --- Check O (left) ---
ld a, KEY_ROW_PY
in a, ($fe)
bit 1, a
jr nz, .not_o
ld hl, (player_att)
dec hl
ld a, (hl)
and $07
cp WALL_INK
jr z, .not_o
call check_collect
ld (player_att), hl
ld hl, (player_scr)
dec hl
ld (player_scr), hl
ld a, (player_col)
dec a
ld (player_col), a
.not_o:
; --- Check P (right) ---
ld a, KEY_ROW_PY
in a, ($fe)
bit 0, a
jr nz, .not_p
ld hl, (player_att)
inc hl
ld a, (hl)
and $07
cp WALL_INK
jr z, .not_p
call check_collect
ld (player_att), hl
ld hl, (player_scr)
inc hl
ld (player_scr), hl
ld a, (player_col)
inc a
ld (player_col), a
.not_p:
; --- Draw player at current position ---
ld hl, (player_scr)
ld de, player_gfx
ld b, 8
.draw: ld a, (de)
ld (hl), a
inc de
inc h
djnz .draw
ld hl, (player_att)
ld a, PLAYER
ld (hl), a
; --- Update score display ---
call print_score
; --- Check hazard ---
ld a, (player_under)
bit 7, a ; FLASH = hazard?
jr z, .not_hazard
; Erase player from hazard cell
ld hl, (player_scr)
ld b, 8
ld a, 0
.derase: ld (hl), a
inc h
djnz .derase
ld hl, (player_att)
ld a, (player_under)
ld (hl), a ; Restore hazard attribute
; Lose a life
ld hl, lives
dec (hl) ; DEC (HL) sets Z flag!
jp z, .game_over
; Death sound — short descending tone
ld hl, 80
ld e, 20 ; High
call beep
ld hl, 80
ld e, 40 ; Low
call beep
; Reset to start position
ld a, START_ROW
ld (player_row), a
ld a, START_COL
ld (player_col), a
ld hl, START_SCR
ld (player_scr), hl
ld hl, START_ATT
ld (player_att), hl
; Save what's under start position
ld a, (START_ATT)
ld (player_under), a
call print_score ; Show reduced lives
jp .loop
.not_hazard:
; --- Check win condition ---
ld a, (player_under)
cp EXIT ; Standing on the exit?
jr nz, .not_on_exit
ld a, (treasure_count)
cp TOTAL_TREASURE ; All treasures collected?
jr z, .room_complete
.not_on_exit:
; --- Border shows progress ---
ld a, (treasure_count)
cp TOTAL_TREASURE
jr nz, .not_all
ld a, 4 ; Green border — door is open
out ($fe), a
jp .loop
.not_all:
ld a, 0 ; Black border
out ($fe), a
jp .loop
; ==================================================================
; Room complete — victory sequence
; ==================================================================
.room_complete:
; Victory fanfare — four ascending notes
ld hl, 100
ld e, 45 ; Low
call beep
ld hl, 100
ld e, 35 ; Mid
call beep
ld hl, 100
ld e, 25 ; High
call beep
ld hl, 300
ld e, 18 ; Sustained high
call beep
; Overwrite score line with victory message
ld de, SCORE_SCR
ld hl, win_text
call print_str
; Green border — permanent
ld a, 4
out ($fe), a
; Halt forever
.victory: halt
jr .victory
; ==================================================================
; Game over — death sequence
; ==================================================================
.game_over:
; Game over sound — four descending notes
ld hl, 100
ld e, 18 ; High
call beep
ld hl, 100
ld e, 25 ; Mid-high
call beep
ld hl, 100
ld e, 35 ; Mid-low
call beep
ld hl, 300
ld e, 50 ; Sustained low
call beep
; Overwrite score line with game over message
ld de, SCORE_SCR
ld hl, lose_text
call print_str
; Red border — permanent
ld a, 2
out ($fe), a
; Halt forever
.dead: halt
jr .dead
; ============================================================================
; Subroutines
; ============================================================================
; ----------------------------------------------------------------------------
; check_collect — save target cell, check for treasure, collect if found
; Entry: HL = target attribute address
; Exit: HL preserved, player_under updated
; ----------------------------------------------------------------------------
check_collect:
ld a, (hl)
ld (player_under), a
bit 6, a ; BRIGHT = treasure?
ret z ; No — done
res 6, a ; Clear BRIGHT (collected)
ld (player_under), a
push hl
ld hl, treasure_count
inc (hl)
pop hl
; Play collect sound — short rising tone
push hl
push de
ld hl, 80 ; Duration (cycles)
ld e, 40 ; Pitch (delay — lower = higher)
call beep
ld hl, 80
ld e, 30 ; Higher pitch
call beep
ld hl, 80
ld e, 20 ; Highest pitch
call beep
pop de
pop hl
ret
; ----------------------------------------------------------------------------
; beep — generate a tone on the speaker
; Entry: HL = duration (number of wave cycles), E = pitch (delay per half)
; Exit: speaker off, border black
; Destroys: A, B, HL
; ----------------------------------------------------------------------------
beep:
ld a, $10 ; Bit 4 high — speaker on
.on: out ($fe), a ; Push speaker cone out
ld b, e ; Delay counter = pitch
.delay1: djnz .delay1 ; Wait
xor $10 ; Toggle bit 4
out ($fe), a ; Pull speaker cone back
ld b, e ; Same delay
.delay2: djnz .delay2 ; Wait
xor $10 ; Toggle bit 4 back
dec hl ; One cycle done
ld a, h
or l ; HL = 0?
ld a, $10 ; Reload (doesn't affect flags)
jr nz, .on ; More cycles — continue
xor a ; A = 0 — speaker off, border black
out ($fe), a
ret
; ----------------------------------------------------------------------------
; print_score — display "TREASURE n/3 L:n" at row 23
; Destroys: A, BC, DE, HL
; ----------------------------------------------------------------------------
print_score:
ld de, SCORE_SCR
ld hl, score_text
call print_str
ld a, (treasure_count)
add a, '0'
call print_char
ld a, '/'
call print_char
ld a, TOTAL_TREASURE
add a, '0'
call print_char
ld a, ' '
call print_char
ld a, ' '
call print_char
ld hl, lives_text
call print_str
ld a, (lives)
add a, '0'
call print_char
ret
; ----------------------------------------------------------------------------
; print_str — print null-terminated string at screen address DE
; Entry: HL = string address, DE = screen address
; Exit: HL past null terminator, DE advanced past last character
; ----------------------------------------------------------------------------
print_str:
ld a, (hl)
or a
ret z
push hl
call print_char
pop hl
inc hl
jr print_str
; ----------------------------------------------------------------------------
; print_char — draw one character to screen memory using ROM font
; Entry: A = character (32-127), DE = screen address (pixel row 0)
; Exit: DE advanced to next column (E incremented)
; ----------------------------------------------------------------------------
print_char:
push de
ld l, a
ld h, 0
add hl, hl
add hl, hl
add hl, hl
ld bc, FONT_BASE
add hl, bc
ld b, 8
.pchar: ld a, (hl)
ld (de), a
inc hl
inc d
djnz .pchar
pop de
inc e
ret
; ============================================================================
; Room data — one byte per cell
; ============================================================================
;
; W W W W W W W W W
; W . W . . . T . W T = treasure
; W . T . . . W . W H = hazard
; W . . . H . . T W X = exit
; W W W W W X W W W
;
room_data:
db WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL
db WALL, FLOOR, WALL, FLOOR, FLOOR, FLOOR, TREASURE, FLOOR, WALL
db WALL, FLOOR, TREASURE, FLOOR, FLOOR, FLOOR, WALL, FLOOR, WALL
db WALL, FLOOR, FLOOR, FLOOR, HAZARD, FLOOR, FLOOR, TREASURE, WALL
db WALL, WALL, WALL, WALL, WALL, EXIT, WALL, WALL, WALL
; ============================================================================
; String data
; ============================================================================
score_text: db "TREASURE ", 0
lives_text: db "L:", 0
win_text: db "ROOM COMPLETE! ", 0
lose_text: db " GAME OVER! ", 0
; ============================================================================
; Player data
; ============================================================================
player_gfx: db $18, $3c, $7e, $ff
db $ff, $7e, $3c, $18
player_row: db START_ROW
player_col: db START_COL
player_scr: dw START_SCR
player_att: dw START_ATT
player_under:
db FLOOR
treasure_count:
db 0
lives: db START_LIVES
end start

The score line now reads “TREASURE 0/3 L:3”. Walk onto the flashing red hazard cell and you’ll hear a descending tone, lose a life (L:2), and reappear at the starting position. Lose all three lives and the game ends with “GAME OVER!” and a red border. Collect all treasure and reach the exit to win.
Try This: More Hazards
Add more hazard cells to make the room harder:
room_data:
db WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL, WALL
db WALL, FLOOR, WALL, FLOOR, HAZARD, FLOOR, TREASURE, FLOOR, WALL
db WALL, FLOOR, TREASURE, FLOOR, FLOOR, FLOOR, WALL, FLOOR, WALL
db WALL, FLOOR, HAZARD, FLOOR, HAZARD, FLOOR, FLOOR, TREASURE, WALL
db WALL, WALL, WALL, WALL, WALL, EXIT, WALL, WALL, WALL
Three hazards instead of one. The player must weave between them to collect treasure and reach the exit. With only three lives, every move towards the hazards is a risk.
Try This: Extra Lives
Change the starting lives and display:
START_LIVES equ 5 ; More forgiving
With more lives, the room is easier. With fewer (try 1), every hazard touch is fatal. The balance between lives and hazard placement defines the difficulty.
Try This: Invincibility Frames
After dying, the player is vulnerable immediately. Add a brief grace period:
; After reset, set an invincibility counter
ld a, 30 ; 30 frames = 0.6 seconds
ld (invuln), a
; In hazard check, skip if invulnerable
ld a, (invuln)
or a
jr nz, .not_hazard ; Still invulnerable
; In main loop, decrement counter each frame
ld a, (invuln)
or a
jr z, .no_invuln
dec a
ld (invuln), a
.no_invuln:
This prevents the player from dying repeatedly on the same hazard. A common pattern in action games — a moment of safety after taking damage.
If It Doesn’t Work
- Hazard doesn’t hurt? Check that
BIT 7, Ais testingplayer_under, not the target cell before movement. The hazard check happens after the player is drawn at the new position. - Lives don’t decrease? Check
DEC (HL)where HL points tolives. If HL points elsewhere, you’re decrementing the wrong byte. - Player doesn’t reset? All six position variables must be restored:
player_row,player_col,player_scr,player_att, andplayer_under. Miss one and the player appears at the wrong position. - Game over triggers immediately? Make sure
livesstarts atSTART_LIVES(3), not 0. Check thatDEC (HL)is followed byJP Z(notJR Z— the game over code is too far for a relative jump). - Score line shows wrong lives? Check that
livesis decremented withDEC (HL), notDEC A. The counter must be in memory forprint_scoreto read it later.
What You’ve Learnt
- BIT 7 for FLASH — hazard detection, completing the attribute bit tests: BIT 6 = treasure, BIT 7 = hazard, AND $07 = wall.
- DEC (HL) sets flags — 8-bit memory decrement sets Z when the result is zero. Unlike DEC HL (16-bit, no flags), DEC (HL) combines decrement and test in one instruction.
- JP vs JR —
JP Zwhen the target is far away (game over code),JR Zwhen it’s close. JR is smaller (2 bytes vs 3) but limited to -128/+127 bytes. - Death and reset — erase the player, restore the cell, reset six position variables, save what’s under the new position. The same initialisation pattern, repeated on death.
- Two exit conditions — the main loop can end in victory or game over. Two paths out, two endings, two sounds. The game has both success and failure.
What’s Next
Unit 16 is the final unit of Phase 1. The single room is playable — you can win or lose. But there are rough edges: the player starts on top of existing content, there’s no title screen, and restarting requires reloading. Unit 16 integrates everything into a polished single-room experience.