Skip to content
Game 1 Unit 14 of 128 1 hr learning time

Room Complete

Two conditions tested in sequence: player on the exit AND all treasures collected. The game has a win condition for the first time — a four-note fanfare and ROOM COMPLETE on screen.

11% of Shadowkeep

The room has an exit. But walking onto it does nothing. The cyan cell sits there, inert. The game needs a win condition — a moment where the room is complete and the player knows they’ve succeeded.

The rule: collect all three treasures, then walk to the exit. Both conditions must be true. Miss a treasure and the exit ignores you. Stand on the exit without collecting everything and nothing happens. Only when both are satisfied does the game end.

Two Conditions, One at a Time

The Z80 can test one thing per instruction. “Is the player on the exit?” is one test. “Are all treasures collected?” is another. To check both, test them in sequence — and bail out early if the first fails.

; Win condition — two tests in sequence
;
; The Z80 can only test one thing at a time. To check two
; conditions, test the first and skip ahead if it fails.
; Then test the second.

            ; Is the player standing on the exit?
            ld      a, (player_under)
            cp      EXIT                ; Cyan cell?
            jr      nz, .not_on_exit    ; No — skip

            ; Yes — are all treasures collected?
            ld      a, (treasure_count)
            cp      TOTAL_TREASURE
            jr      z, .room_complete   ; Both true — win!

.not_on_exit:
            ; Game continues...

This is how all multi-condition logic works in assembly. There’s no && operator, no boolean AND that combines two expressions. You test the first condition with CP. If it fails, JR NZ skips past the second test entirely. If the first passes, execution falls through to the second CP. If that passes too, execution falls through to the win code.

The order matters for efficiency. Test the condition that fails most often first. The player is rarely on the exit, so that check comes first — most frames skip the second test entirely.

What Triggers “On the Exit”

When the player moves onto a cell, check_collect saves the target attribute to player_under. If the player walked onto the exit, player_under holds $28 (the EXIT constant). The comparison is a single CP:

            ld      a, (player_under)
            cp      EXIT                ; $28 = cyan paper?

This works because player_under stores the exact attribute byte. No masking needed — $28 is a unique value that only appears on exit cells.

The Green Border Means “Door Open”

The green border from Unit 10 now has a purpose. When all treasures are collected, the border turns green — a visual signal that the exit is active. Walk to the cyan door while the border is green and the room is complete.

            ; 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

The same code from Unit 10, but now it has meaning. The border isn’t just a congratulation — it tells the player the exit is ready.

Victory

; Victory sequence — sound, message, halt
;
; The game leaves the main loop permanently. A four-note
; fanfare plays, the score line shows ROOM COMPLETE!, and
; the program enters an infinite halt loop. The game is over.

.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

The victory sequence does three things:

  1. Sound — a four-note ascending fanfare, longer than the collect chirp. The last note sustains (duration 300 vs 100) to signal finality.
  2. Message — “ROOM COMPLETE!” overwrites the score line. print_str writes it at the same screen position, replacing “TREASURE 3/3”.
  3. Halt — an infinite loop of HALT / JR .victory. The game is over. The CPU sleeps between interrupts, the green border stays, and the message remains on screen.

The Halt Loop

.victory:   halt
            jr      .victory

HALT stops the CPU until the next interrupt (which fires every frame at 50Hz). JR .victory jumps back to halt again. The CPU does almost nothing — it wakes briefly each frame to execute the jump, then sleeps again. This is the standard way to end a Spectrum program cleanly.

The Complete Code

; ============================================================================
; SHADOWKEEP — Unit 14: Room Complete
; ============================================================================
; The room finally has a win condition. Collect all three treasures, walk
; to the cyan exit door, and the room is complete.
;
; Two conditions must be true: player is on the exit AND all treasures
; are collected. The Z80 tests one at a time — check the first, skip
; if false, then check the second.
;
; Victory: a four-note fanfare, "ROOM COMPLETE!" on the score line,
; and the game halts. Shadowkeep has an ending for the first time.
; ============================================================================

            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

; 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     15              ; Wide enough for "ROOM COMPLETE!"

; 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 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

; ============================================================================
; 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" 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

            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
win_text:   db      "ROOM COMPLETE!", 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

            end     start

Shadowkeep Unit 14

The screenshot shows the starting state — the room with its exit, treasures uncollected. To see the win condition in action: collect all three bright yellow treasures (the border turns green), then walk down to the cyan exit. A four-note fanfare plays and “ROOM COMPLETE!” appears on the score line.

The Game Loop So Far

Each frame now follows this sequence:

  1. HALT — wait for the next frame
  2. Erase — clear the player’s bitmap, restore the attribute under them
  3. Input — read keyboard, check collision, move if allowed, collect treasure
  4. Draw — write the player’s bitmap and attribute at the new position
  5. Score — update the treasure display
  6. Win check — test for exit + all treasures → victory
  7. Border — green if all collected, black otherwise

Steps 1–5 existed in previous units. Step 6 is new — a test that can break out of the loop permanently. The main loop isn’t just repeating forever any more. It has an exit condition.

Try This: Immediate Exit

Remove the treasure requirement — the exit works immediately:

            ld      a, (player_under)
            cp      EXIT
            jr      z, .room_complete   ; No treasure check — exit always works

Now you can walk straight to the exit. This is useful for testing, but makes the room trivial — no challenge, no reason to explore.

Try This: Door Opens Visually

When all treasures are collected, make the exit BRIGHT (cyan becomes bright cyan):

            ; After confirming all treasures collected:
EXIT_ATT    equ     $59d1           ; Exit cell: row 14, col 17

            ld      a, (treasure_count)
            cp      TOTAL_TREASURE
            jr      nz, .not_all
            ld      hl, EXIT_ATT
            set     6, (hl)             ; Make exit BRIGHT — visually "open"

SET 6, (HL) sets the BRIGHT bit directly in attribute memory. The exit changes from solid cyan to bright cyan — a visible signal that the door has opened. This is SET applied to memory rather than a register, a single instruction that modifies one bit at an address.

Be careful: if the player is standing on the exit when you modify it, you’re changing the PLAYER attribute instead (since the player’s attribute overwrites the cell). The simple workaround: only SET the bit if the player isn’t on the exit.

Try This: Victory Border Stripes

Flash the border through multiple colours before settling on green:

.room_complete:
            ld      c, 8                ; 8 colour changes
            ld      b, 0                ; Starting colour
.flash:     ld      a, b
            out     ($fe), a
            push    bc
            ld      hl, 50
            ld      e, 25
            call    beep                ; Each beep acts as a delay
            pop     bc
            inc     b
            dec     c
            jr      nz, .flash

Each beep takes roughly the same time, creating a rhythmic colour change. The border cycles through all 8 colours while playing ascending tones — a more celebratory victory.

If It Doesn’t Work

  • Exit doesn’t trigger? Check that player_under is being compared to EXIT ($28). If you’re comparing to the wrong value, the condition never matches.
  • Win triggers without all treasures? Make sure the treasure check comes AFTER the exit check. Both CP instructions and JR conditions must be correct — JR NZ skips when NOT equal, JR Z continues when equal.
  • “ROOM COMPLETE!” text is garbled? Check that SCORE_LEN is at least 15 (14 characters + safety margin). If the attribute area is too narrow, some characters will appear on a black background.
  • Game doesn’t stop after winning? The .victory label must have both HALT and JR .victory. Without the jump, execution falls through into whatever code follows.
  • Fanfare sounds wrong? Check the pitch values descend (45, 35, 25, 18) — lower values mean higher pitch. The last note should be duration 300 for the sustained effect.

What You’ve Learnt

  • Multi-condition logic — test one condition, skip if false, test the next. No boolean operators in assembly — just sequential comparisons and branches.
  • Test order matters — check the condition that fails most often first. Most frames the player isn’t on the exit, so that test comes first and the second is skipped.
  • Game state change — the main loop has an exit condition. The game transitions from “playing” to “complete.” This is the simplest form of a state machine.
  • Victory feedback — sound, text, and colour together signal success. Multiple forms of feedback make the moment feel significant.
  • HALT loopHALT / JR is the standard way to end a Spectrum program. The CPU sleeps between interrupts, consuming minimal power.

What’s Next

The room is completable but has no penalty. In Unit 15, the FLASH attribute finally becomes dangerous — touching a hazard costs a life. Three lives, and the game can end in failure as well as success. Stakes make the navigation matter.