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

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).
| 1 | 1 | ; Shadowkeep — Unit 11: Join the Sleepers | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 44 | 44 | call player_step | |
| 45 | 45 | ld a, (current_room) | |
| 46 | 46 | 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: | |
| 48 | 53 | call mark_step | |
| 49 | 54 | jr .game_loop | |
| 55 | + | .lost: | |
| 56 | + | jr main_title ; caught — step-02 gives it a screen | |
| 50 | 57 | .won: | |
| 51 | 58 | call show_win ; "THE KEEP STANDS", flourish, wait for SPACE | |
| 52 | 59 | jr main_title ; round again | |
| ... | |||
| 70 | 77 | xor a | |
| 71 | 78 | ld (current_room), a | |
| 72 | 79 | ld (won), a | |
| 80 | + | ld (caught), a | |
| 73 | 81 | ld a, TOTAL_GOLD | |
| 74 | 82 | ld (gold_remaining), a | |
| 75 | 83 | ld a, START_COL | |
| ... | |||
| 484 | 492 | ld hl, warden_row | |
| 485 | 493 | add a, (hl) | |
| 486 | 494 | 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: | |
| 487 | 507 | push bc | |
| 488 | 508 | call wall_at | |
| 489 | 509 | pop bc | |
| ... | |||
| 920 | 940 | defb -1 | |
| 921 | 941 | warden_timer: | |
| 922 | 942 | defb WARDEN_GATHER | |
| 943 | + | caught: | |
| 944 | + | defb 0 | |
| 923 | 945 | | |
| 924 | 946 | room0_state: | |
| 925 | 947 | defs 768 |
| 1 | 1 | ; Shadowkeep — Unit 11: Join the Sleepers | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 53 | 53 | call mark_step | |
| 54 | 54 | jr .game_loop | |
| 55 | 55 | .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 | |
| 57 | 58 | .won: | |
| 58 | 59 | call show_win ; "THE KEEP STANDS", flourish, wait for SPACE | |
| 59 | 60 | jr main_title ; round again | |
| ... | |||
| 727 | 728 | defb "THE KEEP STANDS", 0 | |
| 728 | 729 | str_again: | |
| 729 | 730 | 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 | |
| 730 | 781 | | |
| 731 | 782 | scr_addr_cr: | |
| 732 | 783 | 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:
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 askingwall_at. - Caught, but the game keeps playing. The loop isn’t testing
caughtafterwarden_step, or.lostfalls back into the loop. It must break out to the lose screen, like.wonbreaks to the win. - You lose the instant a new game starts.
caughtisn’t cleared innew_game. Reset it besidewon, 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.