Skip to content
Game 2Unit 11 of 191 hr learning time

Join the Sleepers

The Warden walks, but it cannot touch you. Give its touch a consequence — the one the keep has lacked since the first room — and the Place stops being a walk and becomes a game: step where it steps, and you join the sleepers.

58% of Shadowkeep

Here is the unit the whole game has been walking toward. You have a keep, treasure, a way to win, and now a figure that moves. Everything is in place except the one thing that makes any of it matter: a way to lose.

It is worth saying plainly, because it is the most important idea in the course, and your first game taught it too: a game needs a way to lose. Without it, a win is not earned — you were always going to get there, it was only a question of when you could be bothered. The coins are a chore-list; the Warden is scenery. Give the Warden’s touch a consequence, and everything tightens at once. The route you took without thinking becomes a route you must plan. The coin on the Warden’s column becomes a coin you must time. Nothing on screen changes. The stakes change, and the stakes are the game.

What you’ll see by the end

A black screen, centred white text reading THE KEEP SLEEPS, and below it PRESS SPACE TO RETURN.
The other ending. Where THE KEEP STANDS is the win you earn by clearing every coin, THE KEEP SLEEPS is the loss you earn by sharing a cell with the Warden — caught, frozen, one of the keep's sleepers now.

Walk into the Warden’s path and it will step onto you — and the screen turns black with two cold words: THE KEEP SLEEPS. A falling toll, and you are back at the title. You were caught; you froze mid-stride; you joined the sleepers the keep is full of. The keep now has two endings, and the difference between them is whether you were careful.

You cannot walk into the Warden

There is a subtlety here worth dwelling on, because it shapes the whole feel of the danger. The Warden is drawn bright, and bright reads as solid to wall_at — so when you try to step onto the Warden’s cell, the thief’s own movement code refuses, exactly as it refuses a wall. You can never walk into the Warden.

So the catch is never yours to make by accident. It is always the Warden stepping onto you. That is a fairer, colder kind of danger: you cannot blunder into it, but if you stand in its path and let it come, it will. The threat is the patrol’s motion, not a wall you might brush.

In warden_step, then, before the Warden asks wall_at about the cell ahead, it asks a different question first — is the thief there? — because the thief’s cell would read as a wall and turn the Warden aside, when what we want is for it to catch him:

warden_step:
            ; ... propose the cell ahead ...
            ld      a, b             ; is the thief in the cell we're stepping into?
            ld      hl, thief_row
            cp      (hl)
            jr      nz, .not_thief
            ld      a, c
            ld      hl, thief_col
            cp      (hl)
            jr      nz, .not_thief
            ld      a, 1
            ld      (caught), a      ; caught — the keep sleeps
            ret
.not_thief:
            call    wall_at          ; otherwise, wall or floor as before

A flag the loop already knows how to watch

You have done this once before, this unit’s real trick. To win, the last coin raises a won flag and the loop, seeing it, breaks to the flourish. To lose is the mirror image: capture raises a caught flag, and the loop, seeing that, breaks the other way:

.game_loop:
            halt
            ld      a, (won)
            or      a
            jr      nz, .won         ; cleared -> THE KEEP STANDS
            call    player_step
            call    warden_step
            ld      a, (caught)
            or      a
            jr      nz, .lost        ; caught -> THE KEEP SLEEPS
            jr      .game_loop

The loop that once did one thing forever now has three exits: play on, win, or lose. That third exit is all a lose-state is — not a new machine, just one more way out of the loop you already have. new_game clears caught alongside won, and the keep begins again, fair each time.

The sound of stasis

A loss deserves a sound as surely as a win does — and the opposite sound. Where the win flourish climbs, the capture falls: a quick slide down the pitches, then one deep, held toll, like stone closing over you. It plays on the black screen, under the words, the last thing you hear before the title returns:

sfx_caught:
            ; a fast fall down the pitches...
            ; ... then one long, deep toll — the stone closing

Milestone — the keep can be lost

The Warden’s step onto the thief raises a caught flag; the main loop, which already breaks to the flourish when won is set, now breaks the other way when caught is set — a third exit, THE KEEP SLEEPS, with a falling toll for a sound. You can never walk into the Warden (it reads as solid); the catch is always the Warden walking into you. First the capture and the reset (step 1), then the screen and the sting that give the loss a face (step 2).

Step 1: the Warden's touch is a loss — caught, back to the title
+24-2
11 ; Shadowkeep — Unit 11: Join the Sleepers
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 10's end: a Warden patrols the Hall, but cannot yet harm you.
3+; step-01 makes the Warden's touch a loss — the Place becomes a game.
44
55 org 32768
66
...
4444 call player_step
4545 ld a, (current_room)
4646 or a
47- call z, warden_step ; the Warden haunts the Hall (room 0)
47+ jr nz, .skip_warden ; the Warden haunts the Hall (room 0)
48+ call warden_step ; it may step onto the thief
49+ ld a, (caught)
50+ or a
51+ jr nz, .lost
52+.skip_warden:
4853 call mark_step
4954 jr .game_loop
55+.lost:
56+ jr main_title ; caught — step-02 gives it a screen
5057 .won:
5158 call show_win ; "THE KEEP STANDS", flourish, wait for SPACE
5259 jr main_title ; round again
...
7077 xor a
7178 ld (current_room), a
7279 ld (won), a
80+ ld (caught), a
7381 ld a, TOTAL_GOLD
7482 ld (gold_remaining), a
7583 ld a, START_COL
...
484492 ld hl, warden_row
485493 add a, (hl)
486494 ld b, a ; B = next row
495+ ld a, b ; the thief there? caught — check BEFORE
496+ ld hl, thief_row ; wall_at, because his own cell reads as wall
497+ cp (hl)
498+ jr nz, .ws_wall
499+ ld a, c
500+ ld hl, thief_col
501+ cp (hl)
502+ jr nz, .ws_wall
503+ ld a, 1
504+ ld (caught), a
505+ ret
506+.ws_wall:
487507 push bc
488508 call wall_at
489509 pop bc
...
920940 defb -1
921941 warden_timer:
922942 defb WARDEN_GATHER
943+caught:
944+ defb 0
923945
924946 room0_state:
925947 defs 768
Step 2: THE KEEP SLEEPS — a screen and a falling toll
+53-2
11 ; Shadowkeep — Unit 11: Join the Sleepers
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-01 makes the Warden's touch a loss — the Place becomes a game.
3+; step-02 gives the loss a face: THE KEEP SLEEPS, and a plunge-then-lock sting.
44
55 org 32768
66
...
5353 call mark_step
5454 jr .game_loop
5555 .lost:
56- jr main_title ; caught — step-02 gives it a screen
56+ call show_lose ; "THE KEEP SLEEPS", the sting, wait for SPACE
57+ jr main_title
5758 .won:
5859 call show_win ; "THE KEEP STANDS", flourish, wait for SPACE
5960 jr main_title ; round again
...
727728 defb "THE KEEP STANDS", 0
728729 str_again:
729730 defb "PRESS SPACE TO RETURN", 0
731+
732+; ----------------------------------------------------------------------------
733+; show_lose — the mirror of show_win. Black screen, the words, the freezing
734+; sting, then wait for SPACE (and release) to return to the title.
735+; ----------------------------------------------------------------------------
736+show_lose:
737+ di
738+ call clear_screen
739+ ld hl, str_lost
740+ ld b, 9
741+ ld c, 8
742+ call print_string
743+ ld hl, str_again
744+ ld b, 15
745+ ld c, 5
746+ call print_string
747+ call sfx_caught
748+.slo_wait:
749+ ld bc, KEYS_SPACE
750+ in a, (c)
751+ bit 0, a
752+ jr nz, .slo_wait
753+.slo_rel:
754+ ld bc, KEYS_SPACE
755+ in a, (c)
756+ bit 0, a
757+ jr z, .slo_rel
758+ ret
759+
760+; sfx_caught — the plunge into stasis: a quick fall, then a deep, long toll as
761+; the stone closes over you. The opposite of the win flourish.
762+sfx_caught:
763+ ld c, 16
764+.sca_fall:
765+ ld b, 5
766+ push bc
767+ call beep
768+ pop bc
769+ ld a, c
770+ add a, 7
771+ ld c, a
772+ cp 100
773+ jr c, .sca_fall
774+ ld b, 60
775+ ld c, 130
776+ call beep
777+ ret
778+
779+str_lost:
780+ defb "THE KEEP SLEEPS", 0
730781
731782 scr_addr_cr:
732783 ld a, b
The complete program
; Shadowkeep — Unit 11: Join the Sleepers
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 gives the loss a face: THE KEEP SLEEPS, and a plunge-then-lock sting.

            org     32768

WALL_ATTR   equ     %01001000
FLOOR_ATTR  equ     %00001000
MARK_ATTR   equ     %00001111
THIEF       equ     %01001010
GOLD_ATTR   equ     %00000110         ; yellow ink, no BRIGHT -> walkable gold
TOTAL_GOLD  equ     6
WARDEN      equ     %01000101         ; BRIGHT cyan on black — the sentinel
WARDEN_COL0 equ     26                ; the Hall's gold column
WARDEN_ROW0 equ     11
WARDEN_SPEED equ    10                ; frames between patrol steps
WARDEN_GATHER equ   50                ; a beat before it begins to move
WALL_BIT    equ     6

START_COL   equ     15
START_ROW   equ     11
NO_EXIT     equ     $FF

KEYS_OP     equ     $DFFE
KEYS_Q      equ     $FBFE
KEYS_A      equ     $FDFE
KEYS_SPACE  equ     $7FFE

start:
            ld      a, 0
            out     ($FE), a
            im      1

; --- TITLE -> PLAY -> WIN -> TITLE ------------------------------------------
main_title:
            call    show_title       ; returns (interrupts off) when SPACE pressed
            call    new_game         ; reset the keep, place the gold, draw
            ei
.game_loop:
            halt
            ld      a, (won)
            or      a
            jr      nz, .won
            call    player_step
            ld      a, (current_room)
            or      a
            jr      nz, .skip_warden ; the Warden haunts the Hall (room 0)
            call    warden_step      ; it may step onto the thief
            ld      a, (caught)
            or      a
            jr      nz, .lost
.skip_warden:
            call    mark_step
            jr      .game_loop
.lost:
            call    show_lose        ; "THE KEEP SLEEPS", the sting, wait for SPACE
            jr      main_title
.won:
            call    show_win         ; "THE KEEP STANDS", flourish, wait for SPACE
            jr      main_title       ; round again

; new_game — copy the room templates back into working state (so collected gold
; returns), reset the thief, the gold counter and the win flag, draw the keep.
new_game:
            ld      hl, room0_template
            ld      de, room0_state
            ld      bc, 768
            ldir
            ld      hl, room1_template
            ld      de, room1_state
            ld      bc, 768
            ldir
            ld      hl, room2_template
            ld      de, room2_state
            ld      bc, 768
            ldir

            xor     a
            ld      (current_room), a
            ld      (won), a
            ld      (caught), a
            ld      a, TOTAL_GOLD
            ld      (gold_remaining), a
            ld      a, START_COL
            ld      (thief_col), a
            ld      a, START_ROW
            ld      (thief_row), a

            call    draw_room
            call    save_under
            call    draw_thief
            ld      a, WARDEN_COL0
            ld      (warden_col), a
            ld      a, WARDEN_ROW0
            ld      (warden_row), a
            ld      a, -1
            ld      (warden_dir), a
            ld      a, WARDEN_GATHER
            ld      (warden_timer), a
            call    save_warden
            call    draw_warden
            ret

mark_step:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            ret     nz
            call    cell_state_addr
            ld      (hl), '+'
            ld      hl, mark_tile
            ld      de, under_thief
            ld      bc, 8
            ldir
            ld      a, MARK_ATTR
            ld      (under_thief + 8), a
            ret

cell_state_addr:
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            push    hl
            ld      a, (thief_row)
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, (thief_col)
            ld      e, a
            ld      d, 0
            add     hl, de
            pop     de
            add     hl, de
            ret

room_entry_addr:
            ld      a, (current_room)
            ld      l, a
            ld      h, 0
            ld      d, h
            ld      e, l
            add     hl, hl
            add     hl, de
            add     hl, hl
            ld      de, rooms
            add     hl, de
            ret

draw_room:
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      (map_ptr), hl
            ld      b, 0
.room_row:
            ld      c, 0
.room_col:
            ld      hl, (map_ptr)
            ld      a, (hl)
            call    lookup_tile
            call    draw_tile
            ld      hl, (map_ptr)
            inc     hl
            ld      (map_ptr), hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .room_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .room_row
            ret

lookup_tile:
            ld      hl, palette
.scan:
            cp      (hl)
            jr      z, .found
            inc     hl
            inc     hl
            inc     hl
            inc     hl
            jr      .scan
.found:
            inc     hl
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            inc     hl
            ld      a, (hl)
            ld      (tile_attr), a
            ret

draw_tile:
            push    bc
            call    attr_addr_cr
            ld      a, (tile_attr)
            ld      (hl), a
            call    scr_addr_cr
            ld      de, (tile_ptr)
            ld      b, 8
.tile_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .tile_row
            pop     bc
            ret

player_step:
            ld      a, (thief_col)
            ld      (tcol), a
            ld      a, (thief_row)
            ld      (trow), a

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a
            jr      z, .left
            bit     0, a
            jr      z, .right
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a
            jr      z, .up
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a
            jr      z, .down
            ret
.left:
            ld      hl, tcol
            dec     (hl)
            jr      .move
.right:
            ld      hl, tcol
            inc     (hl)
            jr      .move
.up:
            ld      hl, trow
            dec     (hl)
            jr      .move
.down:
            ld      hl, trow
            inc     (hl)
.move:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz

            call    restore_under
            ld      a, (tcol)
            ld      (thief_col), a
            ld      a, (trow)
            ld      (thief_row), a
            call    collect_gold     ; the new cell might be gold — lift it first
            call    save_under
            call    draw_thief
            call    check_exit
            ret

wall_at:
            call    attr_addr_cr
            bit     WALL_BIT, (hl)
            ret

check_exit:
            ld      a, (thief_col)
            or      a
            jr      z, .west
            cp      31
            jr      z, .east
            ld      a, (thief_row)
            or      a
            jr      z, .north
            cp      23
            jr      z, .south
            ret
.east:
            call    room_entry_addr
            ld      de, 4
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1
            ld      (thief_col), a
            jr      .enter
.west:
            call    room_entry_addr
            ld      de, 5
            add     hl, de
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 30
            ld      (thief_col), a
            jr      .enter
.north:
            call    room_entry_addr
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 22
            ld      (thief_row), a
            jr      .enter
.south:
            call    room_entry_addr
            inc     hl
            inc     hl
            inc     hl
            ld      a, (hl)
            cp      NO_EXIT
            ret     z
            ld      (current_room), a
            ld      a, 1
            ld      (thief_row), a
.enter:
            call    draw_room
            call    save_under
            call    draw_thief
            ld      a, (current_room)
            or      a
            ret     nz               ; the Warden is only in the Hall
            call    save_warden
            call    draw_warden
            ret

pos_bc:
            ld      a, (thief_row)
            ld      b, a
            ld      a, (thief_col)
            ld      c, a
            ret

save_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.save_row:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .save_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (hl)
            ld      (under_thief + 8), a
            ret

restore_under:
            call    pos_bc
            call    scr_addr_cr
            ld      de, under_thief
            ld      b, 8
.restore_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .restore_row
            call    pos_bc
            call    attr_addr_cr
            ld      a, (under_thief + 8)
            ld      (hl), a
            ret

draw_thief:
            call    pos_bc
            call    attr_addr_cr
            ld      (hl), THIEF
            call    pos_bc
            call    scr_addr_cr
            ld      de, thief
            ld      b, 8
.thief_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .thief_row
            ret

; ----------------------------------------------------------------------------
; The Warden — something shares the keep. A second mover, drawn exactly like the
; thief: a private under-buffer and its own save / draw, a spectral hooded
; sentinel. First we simply give it a body (it neither moves nor harms yet).
; ----------------------------------------------------------------------------
wpos_bc:
            ld      a, (warden_row)
            ld      b, a
            ld      a, (warden_col)
            ld      c, a
            ret

save_warden:
            call    wpos_bc
            call    scr_addr_cr
            ld      de, under_warden
            ld      b, 8
.svw_row:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .svw_row
            call    wpos_bc
            call    attr_addr_cr
            ld      a, (hl)
            ld      (under_warden + 8), a
            ret

draw_warden:
            call    wpos_bc
            call    attr_addr_cr
            ld      (hl), WARDEN
            call    wpos_bc
            call    scr_addr_cr
            ld      de, warden_glyph
            ld      b, 8
.drw_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .drw_row
            ret

; A spectral hooded sentinel: a cowl with dark eye-slits over a robe that frays
; at the hem. Cold, faceless, unmistakably a figure.
warden_glyph:
            defb    %00111100
            defb    %01111110
            defb    %01100110
            defb    %01111110
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01011010

restore_warden:
            call    wpos_bc
            call    scr_addr_cr
            ld      de, under_warden
            ld      b, 8
.rsw_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .rsw_row
            call    wpos_bc
            call    attr_addr_cr
            ld      a, (under_warden + 8)
            ld      (hl), a
            ret

; warden_step — the patrol. Walks its column one cell every WARDEN_SPEED frames,
; after a WARDEN_GATHER beat, reversing at walls. Deterministic: a route you learn
; and time. It reads the thief's cell as a wall (his attribute has WALL_BIT set),
; so it turns aside at him for now — the catch is Unit 11.
warden_step:
            ld      a, (warden_timer)
            dec     a
            ld      (warden_timer), a
            ret     nz
            ld      a, WARDEN_SPEED
            ld      (warden_timer), a
            ld      a, (warden_col)
            ld      c, a             ; C = column (fixed)
            ld      a, (warden_dir)
            ld      hl, warden_row
            add     a, (hl)
            ld      b, a             ; B = next row
            ld      a, b             ; the thief there? caught — check BEFORE
            ld      hl, thief_row    ; wall_at, because his own cell reads as wall
            cp      (hl)
            jr      nz, .ws_wall
            ld      a, c
            ld      hl, thief_col
            cp      (hl)
            jr      nz, .ws_wall
            ld      a, 1
            ld      (caught), a
            ret
.ws_wall:
            push    bc
            call    wall_at
            pop     bc
            jr      z, .ws_commit    ; floor ahead — step onto it
            ld      a, (warden_dir)  ; wall ahead — reverse, wait a beat
            neg
            ld      (warden_dir), a
            ret
.ws_commit:
            push    bc               ; keep the proposed row — restore heals the OLD cell
            call    restore_warden
            pop     bc
            ld      a, b
            ld      (warden_row), a
            call    save_warden      ; photograph the new cell's ground
            call    draw_warden
            ret

; ----------------------------------------------------------------------------
; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns
; to floor (gone for good), the cell repaints as plain floor (no lighting yet),
; a chime sounds, and the counter ticks down.
; ----------------------------------------------------------------------------
collect_gold:
            call    cell_state_addr  ; HL -> map cell at the thief's position
            ld      a, (hl)
            cp      'G'
            ret     nz
            ld      (hl), '.'        ; the gold is taken — floor from now on
            call    pos_bc           ; B = row, C = col
            call    draw_floor_cell  ; repaint the cell as plain floor
            call    sfx_pickup
            ld      hl, gold_remaining
            dec     (hl)
            ld      a, (hl)
            or      a
            call    z, win           ; the last coin — the keep stands
            ret

; draw_floor_cell — paint the floor tile at (row B, col C). Lighting (Unit 13)
; will replace this with a shade chosen by distance from the nearest torch.
draw_floor_cell:
            ld      hl, floor_tile
            ld      (tile_ptr), hl
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            call    draw_tile
            ret

; ----------------------------------------------------------------------------
; beep — the one tone primitive (the tiny game's blip, reused). B = cycles,
; C = the delay between speaker toggles (the pitch; larger C = lower).
; ----------------------------------------------------------------------------
beep:
.bp_cycle:
            ld      a, %00010000
            out     ($FE), a
            ld      e, c
.bp_hi:
            dec     e
            jr      nz, .bp_hi
            xor     a
            out     ($FE), a
            ld      e, c
.bp_lo:
            dec     e
            jr      nz, .bp_lo
            djnz    .bp_cycle
            ret

; sfx_pickup — a bright two-blip chime: the sound of gold.
sfx_pickup:
            ld      b, 8
            ld      c, 20
            call    beep
            ld      b, 8
            ld      c, 14
            call    beep
            ret

gold_tile:
            defb    %00000000
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00000000

; ----------------------------------------------------------------------------
; show_title — black screen, the keep's name and a prompt; wait for SPACE (and
; release). Silent for now — the composed theme arrives in Unit 18. Words use the
; ROM's own 8x8 font at $3D00.
; ----------------------------------------------------------------------------
show_title:
            di
            call    clear_screen
            ld      hl, str_title
            ld      b, 8
            ld      c, 11
            call    print_string
            ld      hl, str_prompt
            ld      b, 16
            ld      c, 6
            call    print_string
.tt_wait:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            jr      nz, .tt_wait
.tt_rel:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            jr      z, .tt_rel
            ret

; show_win — the keep is cleared. Black screen, the words, the flourish, then
; wait for SPACE (and release) to return to the title.
show_win:
            di
            call    clear_screen
            ld      hl, str_won
            ld      b, 9
            ld      c, 8
            call    print_string
            ld      hl, str_again
            ld      b, 15
            ld      c, 5
            call    print_string
            call    sfx_win
.sw_wait:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            jr      nz, .sw_wait
.sw_rel:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            jr      z, .sw_rel
            ret

; win — the last coin is gone. Flourish, and raise the flag the loop watches.
win:
            ld      a, 1
            ld      (won), a
            call    sfx_win
            ret

; sfx_win — an ascending flourish: a low note climbing in steps to a high one.
sfx_win:
            ld      c, 60
.swf_loop:
            ld      b, 12
            push    bc
            call    beep
            pop     bc
            ld      a, c
            sub     8
            ld      c, a
            cp      16
            jr      nc, .swf_loop
            ret

; clear_screen — fill bitmap and attributes with zero: a black screen.
clear_screen:
            ld      hl, $4000
            ld      (hl), 0
            ld      de, $4001
            ld      bc, 6911
            ldir
            ret

; print_string — HL -> zero-terminated text, B = row, C = col.
print_string:
.ps_loop:
            ld      a, (hl)
            or      a
            ret     z
            push    hl
            call    print_char
            pop     hl
            inc     hl
            inc     c
            jr      .ps_loop

; print_char — A = character (32..127), B = row, C = col. Copies the ROM font
; glyph at $3D00 + (char-32)*8 to the screen, bright white.
print_char:
            push    bc
            sub     32
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, $3D00
            add     hl, de
            push    hl
            call    scr_addr_cr
            pop     de
            ld      b, 8
.pc_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .pc_row
            pop     bc
            call    attr_addr_cr
            ld      (hl), %01000111
            ret

str_title:
            defb    "SHADOWKEEP", 0
str_prompt:
            defb    "PRESS SPACE TO ENTER", 0
str_won:
            defb    "THE KEEP STANDS", 0
str_again:
            defb    "PRESS SPACE TO RETURN", 0

; ----------------------------------------------------------------------------
; show_lose — the mirror of show_win. Black screen, the words, the freezing
; sting, then wait for SPACE (and release) to return to the title.
; ----------------------------------------------------------------------------
show_lose:
            di
            call    clear_screen
            ld      hl, str_lost
            ld      b, 9
            ld      c, 8
            call    print_string
            ld      hl, str_again
            ld      b, 15
            ld      c, 5
            call    print_string
            call    sfx_caught
.slo_wait:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            jr      nz, .slo_wait
.slo_rel:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            jr      z, .slo_rel
            ret

; sfx_caught — the plunge into stasis: a quick fall, then a deep, long toll as
; the stone closes over you. The opposite of the win flourish.
sfx_caught:
            ld      c, 16
.sca_fall:
            ld      b, 5
            push    bc
            call    beep
            pop     bc
            ld      a, c
            add     a, 7
            ld      c, a
            cp      100
            jr      c, .sca_fall
            ld      b, 60
            ld      c, 130
            call    beep
            ret

str_lost:
            defb    "THE KEEP SLEEPS", 0

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

palette:
            defb    '.'
            defw    floor_tile
            defb    FLOOR_ATTR
            defb    '#'
            defw    wall_tile
            defb    WALL_ATTR
            defb    '+'
            defw    mark_tile
            defb    MARK_ATTR
            defb    'G'
            defw    gold_tile
            defb    GOLD_ATTR

; ----------------------------------------------------------------------------
; The keep: three rooms. Hall -east-> Gallery -north-> Vault, and back.
;   entry: map ptr, North, South, East, West
; ----------------------------------------------------------------------------
rooms:
            defw    room0_state
            defb    NO_EXIT, NO_EXIT, 1, NO_EXIT      ; Hall: east -> Gallery
            defw    room1_state
            defb    2, NO_EXIT, NO_EXIT, 0            ; Gallery: north -> Vault, west -> Hall
            defw    room2_state
            defb    NO_EXIT, 1, NO_EXIT, NO_EXIT      ; Vault: south -> Gallery

; The Great Hall — four pillars, east door at row 11.
room0_template:
            defb    "################################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.........................G....#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................."
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.........................G....#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "################################"

; The Gallery — a dividing wall with one gap (column 15). West door (row 11)
; back to the Hall; north door (column 15) up to the Vault.
room1_template:
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "........................G......#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.......................G......#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "################################"

; The Vault — a great altar of stone in the middle, south door (column 15)
; down to the Gallery.
room2_template:
            defb    "################################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.........G.........G..........#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############.################"

floor_tile:
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101

wall_tile:
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000

mark_tile:
            defb    %00000000
            defb    %00011000
            defb    %00011000
            defb    %01111110
            defb    %01111110
            defb    %00011000
            defb    %00011000
            defb    %00000000

thief:
            defb    %00011000
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00100100

current_room:
            defb    0
thief_col:
            defb    START_COL
thief_row:
            defb    START_ROW
tcol:
            defb    0
trow:
            defb    0
map_ptr:
            defw    0
tile_ptr:
            defw    0
tile_attr:
            defb    0
under_thief:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
gold_remaining:
            defb    TOTAL_GOLD
won:
            defb    0
under_warden:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
warden_col:
            defb    WARDEN_COL0
warden_row:
            defb    WARDEN_ROW0
warden_dir:
            defb    -1
warden_timer:
            defb    WARDEN_GATHER
caught:
            defb    0

room0_state:
            defs    768
room1_state:
            defs    768
room2_state:
            defs    768

            end     start

Stand on the Warden’s column and wait for it to come — one step, and the keep sleeps:

The thief steps onto the Warden's column and waits. The patrol arrives, steps onto his cell — and the screen falls to THE KEEP SLEEPS. The same keep as a minute ago, only now it can take something from you.

Try this: a narrow escape, tuned

Make the Warden faster or slower and feel how the danger changes. A slow patrol is a puzzle you solve once; a fast one is a nerve you hold. Somewhere between is the pace where a coin on its column is tempting but tense — reachable if you read the rhythm, punishing if you don’t. That tuning is level design.

Try this: a second chance (and why we didn’t)

Right now one touch sends you to the title — the whole run gone. That is harsh, harsher than the arcade games this descends from, which gave you lives. Add a lives counter: decrement on capture, and only go to the title at zero. It is a few lines. We hold it back on purpose — being alone is this keep’s fiction, and lives are a later chapter’s idea — but you should feel how a single number changes forgiveness.

Try this: make the toll yours

sfx_caught falls and then holds. Reshape it: a single sharp crack for a snap-freeze, or a long slow descent with no toll for a slow sinking. The sound of failure is teaching the player how to feel about failing — a comedy boing and a funeral bell are different games.

When it’s wrong, see why

  • You can walk onto the Warden and nothing happens. You can’t — it reads as a wall. If you think you should be able to, you have confused the two catches. The thief never enters the Warden’s cell; the Warden enters his.
  • The Warden turns aside at you instead of catching you. The thief-check runs after wall_at, so his solid cell reverses the patrol before the catch is tested. Ask “is the thief there?” before asking wall_at.
  • Caught, but the game keeps playing. The loop isn’t testing caught after warden_step, or .lost falls back into the loop. It must break out to the lose screen, like .won breaks to the win.
  • You lose the instant a new game starts. caught isn’t cleared in new_game. Reset it beside won, or you begin every game already frozen.
  • Both endings fire at once on the last coin. You collected the sixth coin and were caught on the same step. Decide the order deliberately — usually a win you earned should stand.

Before and after

You began with a keep you could clear at your leisure and finished with a keep that can freeze you where you stand. And the machinery was almost nothing: a flag, a check, a third way out of a loop you already had. That is the shape of every lose-state you will ever write — not a new system, but a consequence wired to the loop. The keep has both its endings now, STANDS and SLEEPS, and every step you take is measured against them. It was a place; it became a game the moment it could take something from you.

What you’ve learnt

  • A game needs a way to lose. A win you cannot fail to reach is not earned; the lose-state is what gives the win its weight.
  • A lose-state is a flag and a third exit. The same loop that breaks to the win breaks to the loss — no new machine, just one more way out.
  • Danger can be one-sided. You cannot walk into the Warden; it walks into you. Deciding who can catch whom shapes how the threat feels.
  • Failure has a sound. The falling toll teaches the player how to feel about being caught, as surely as the flourish teaches them to feel the win.

What’s next

One Warden, in one room — the Hall is dangerous, but the Gallery and the Vault are still a stroll. A keep with one guarded room and two safe ones isn’t a game yet; it’s a game with a demo level. In Unit 12, “A Warden for Every Room,” the Warden becomes data: a table with a line per room, so every room instantiates its own patrol on the coins’ ground — and the whole keep, at last, has something to fear in it.