Stand or Sleep
Everything is here — the game plays, sounds, lights and sings. This unit seals it: a hand-pixelled crown of stone over the title, and the course's own proof met head-on — a run to each ending, THE KEEP STANDS and THE KEEP SLEEPS, both reachable, both real. And the proof earns its keep: driven to the edge, it catches a tie that read a won game as a lost one — the same ordering bug that hid in Gloaming and The Long Night — and closes it.
There is nothing left to add. The keep is a game — you want the gold, the Wardens threaten it, you win or you sleep. It is a place — lit, furnished, composed, three chambers of one character. And it has a voice — footsteps, doors, a theme that loops on the title. Everything the Place was ever meant to be is on the screen and in the speaker. This unit does not build a new thing; it seals the thing you built — crowns the title with a band of stone, and then does what this course has asked of every game since your first: proves it. Not “it looks finished.” Run it to each of its endings, and watch both arrive.
What you’ll see by the end

The title wears a crown of stone now — a row of battlements pixelled above the name, the theme playing under it. Press SPACE and you play the whole finished thing: read the patrols, take the gold, clear all six and THE KEEP STANDS rises in white; or misjudge a Warden and THE KEEP SLEEPS falls with its toll. Two endings, both real, both earned. That is a game.
A crown, pixel by pixel
The title has read in the ROM’s typeface until now — perfectly good, but plain. A game deserves a face of its own, and the cheapest, oldest way to give it one is a strip of hand-pixelled art. We draw a single tile — a merlon, the tooth of a battlement — and stamp a row of it above the name:
battlement_tile: ; crenellations: merlons over solid wall
defb %11011011
defb %11011011
defb %11011011
defb %11111111
; ... solid below ...
draw_battlements:
; ... stamp battlement_tile across a row, above the title text ...
It is eight bytes and a short loop, and it turns a line of type into the top of a keep. That is the whole trick of a title: one distinctive stroke, drawn by hand, that says this is a place, and someone made it.
The proof the course demands
Since your first game, the rule has held: a game is not done because it looks done. It is done when you can drive it to its endings and watch them arrive. The keep has two, and this is where we meet them both, deliberately, the way the winnability gate asks:
- THE KEEP STANDS — a scripted run that reads every patrol, lifts all six coins across the three rooms, and reaches the win. Proof the keep is winnable: there is a way through.
- THE KEEP SLEEPS — a scripted run that steps onto a Warden’s ground and is caught. Proof the keep is losable: the danger is real, not decorative.
A game with only the first is a walk. A game with only the second is unfair. A game with both, each reachable, is the thing you set out to build in Unit 9 when you first asked what the keep was for. Run each; watch each land. The Place is proven.
Milestone — the Place, sealed
A hand-pixelled battlement_tile and a draw_battlements loop crown the title with
a row of stone. Nothing else is added — the game was whole at Unit 18 — but the
per-game winnability gate is met head-on: a scripted run to THE KEEP STANDS (all six
coins, every patrol read) and a scripted run to THE KEEP SLEEPS (caught on a Warden’s
ground), both reachable, both real. “A complete, finishable game” is now true out
loud: losable and winnable.
| 1 | 1 | ; Shadowkeep — Unit 19: Stand or Sleep | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 = Unit 18's end: the whole game, its title bare above the keep. | |
| 3 | + | ; step-01 crowns the title with stone and seals the complete, winnable-and-losable game. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 855 | 855 | defb 50, 8 ; A5 | |
| 856 | 856 | defb 75, 16 ; D5 (home) | |
| 857 | 857 | defb $FF ; end | |
| 858 | + | | |
| 859 | + | ; draw_battlements — a crown of stone merlons across the title, the keep's | |
| 860 | + | ; silhouette above its name. Drawn in wall stone at row 6, columns 8..23. | |
| 861 | + | draw_battlements: | |
| 862 | + | ld b, 6 | |
| 863 | + | ld c, 8 | |
| 864 | + | .db_loop: | |
| 865 | + | push bc | |
| 866 | + | ld hl, battlement_tile | |
| 867 | + | ld (tile_ptr), hl | |
| 868 | + | ld a, WALL_ATTR | |
| 869 | + | ld (tile_attr), a | |
| 870 | + | call draw_tile | |
| 871 | + | pop bc | |
| 872 | + | inc c | |
| 873 | + | ld a, c | |
| 874 | + | cp 24 | |
| 875 | + | jr nz, .db_loop | |
| 876 | + | ret | |
| 877 | + | | |
| 878 | + | battlement_tile: ; crenellations — merlons over solid wall | |
| 879 | + | defb %11011011 | |
| 880 | + | defb %11011011 | |
| 881 | + | defb %11011011 | |
| 882 | + | defb %11111111 | |
| 883 | + | defb %11111111 | |
| 884 | + | defb %11111111 | |
| 885 | + | defb %11111111 | |
| 886 | + | defb %11111111 | |
| 858 | 887 | | |
| 859 | 888 | gold_tile: | |
| 860 | 889 | defb %00000000 | |
| ... | |||
| 974 | 1003 | show_title: | |
| 975 | 1004 | di | |
| 976 | 1005 | call clear_screen | |
| 1006 | + | call draw_battlements | |
| 977 | 1007 | ld hl, str_title | |
| 978 | 1008 | ld b, 8 | |
| 979 | 1009 | ld c, 11 |
The complete program
; Shadowkeep — Unit 19: Stand or Sleep
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 crowns the title with stone and seals the complete, winnable-and-losable game.
org 32768
WALL_ATTR equ %01001000
FLOOR_ATTR equ %00001000
MARK_ATTR equ %00001111
TORCH_ATTR equ %01001110 ; BRIGHT, blue paper, yellow ink — a lit sconce
STATUE_ATTR equ %01001111 ; BRIGHT white on blue — pale stone (solid)
BANNER_ATTR equ %01001011 ; BRIGHT magenta on blue — a hanging (solid)
RUBBLE_ATTR equ %00001000 ; dim, like floor — walkable broken stone
MAX_SHADE equ 4
MAX_TORCHES equ 4
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
AXIS_HORIZ equ 0 ; the Warden varies its column (walks a row)
AXIS_VERT equ 1 ; the Warden varies its row (walks a column)
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
call warden_step ; every room has its Warden now
ld a, (caught)
or a
jr nz, .lost
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
call warden_enter ; this room's Warden, from the table
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
; ----------------------------------------------------------------------------
; find_torch — scan the current room's map for 'T', remember where it is (or
; NO_TORCH if the room is unlit).
; ----------------------------------------------------------------------------
; find_torches — collect every 'T' in the current room (up to MAX_TORCHES) into
; torch_list as (col, row) pairs; torch_count says how many.
find_torches:
xor a
ld (torch_count), a
call room_entry_addr
ld a, (hl)
inc hl
ld h, (hl)
ld l, a
ld b, 0
.fr_row:
ld c, 0
.fr_col:
ld a, (hl)
cp 'T'
jr nz, .fr_skip
ld a, (torch_count)
cp MAX_TORCHES
jr nc, .fr_skip
push hl
add a, a ; count * 2
ld e, a
ld d, 0
ld hl, torch_list
add hl, de
ld (hl), c ; column
inc hl
ld (hl), b ; row
pop hl
ld a, (torch_count)
inc a
ld (torch_count), a
.fr_skip:
inc hl
inc c
ld a, c
cp 32
jr nz, .fr_col
inc b
ld a, b
cp 24
jr nz, .fr_row
ret
; ----------------------------------------------------------------------------
; shade_for_cell — row in B, column in C. Returns a shade 0..4 in A: the
; Chebyshev distance to the torch, halved and clamped. Near the flame = 0
; (lightest); far away = 4 (darkest). No torch = darkest everywhere.
; ----------------------------------------------------------------------------
shade_for_cell:
push bc
ld a, b
ld (cell_row), a
ld a, c
ld (cell_col), a
ld a, (torch_count)
or a
jr z, .sf_dark
ld a, 255
ld (min_dist), a
ld hl, torch_list
ld a, (torch_count)
ld b, a ; loop count
.sf_loop:
ld a, (cell_col)
sub (hl) ; - torch col
jr nc, .sf_dc
neg
.sf_dc:
ld d, a ; |dc|
inc hl ; -> torch row
ld a, (cell_row)
sub (hl)
jr nc, .sf_dr
neg
.sf_dr:
inc hl ; -> next torch pair
cp d ; A = |dr|; take the larger
jr nc, .sf_mx
ld a, d
.sf_mx:
ld e, a ; this torch's distance
ld a, (min_dist)
cp e
jr c, .sf_keep ; min already smaller
ld a, e
ld (min_dist), a
.sf_keep:
djnz .sf_loop
ld a, (current_room)
ld c, a
ld b, 0
ld hl, room_falloff
add hl, bc
ld b, (hl)
ld a, (min_dist)
inc b
.sf_shift:
dec b
jr z, .sf_clamp
srl a
jr .sf_shift
.sf_clamp:
cp MAX_SHADE + 1
jr c, .sf_done
ld a, MAX_SHADE
.sf_done:
pop bc
ret
.sf_dark:
ld a, MAX_SHADE
pop bc
ret
draw_room:
call find_torches
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)
cp '.'
jr nz, .not_floor
call shade_for_cell
add a, a
ld e, a
ld d, 0
ld hl, shade_tiles
add hl, de
ld e, (hl)
inc hl
ld d, (hl)
ld (tile_ptr), de
ld a, FLOOR_ATTR
ld (tile_attr), a
call draw_tile
jr .cell_done
.not_floor:
call lookup_tile
call draw_tile
.cell_done:
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 sfx_step
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 sfx_door
call draw_room
call save_under
call draw_thief
call warden_enter ; this room's Warden, clear of the entry cell
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
; propose the next cell along the patrol axis, in the current dir
ld a, (warden_axis)
or a
jr nz, .ws_vert
.ws_horiz:
ld a, (warden_row)
ld b, a ; B = row (fixed)
ld a, (warden_dir)
ld hl, warden_col
add a, (hl)
ld c, a ; C = next col
jr .ws_have
.ws_vert:
ld a, (warden_col)
ld c, a ; C = col (fixed)
ld a, (warden_dir)
ld hl, warden_row
add a, (hl)
ld b, a ; B = next row
.ws_have:
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 B, col C)
call restore_warden
pop bc
ld a, c
ld (warden_col), a
ld a, b
ld (warden_row), a
call save_warden ; photograph the new cell's ground
call draw_warden
ret
; ----------------------------------------------------------------------------
; warden_enter — instantiate the current room's Warden from warden_table: place
; it at the route-start (chosen clear of the entry cell), set its axis, and start
; its gather. Called on new_game and on every room change. A second mover is a
; table entry — like the torches and the gold.
; ----------------------------------------------------------------------------
warden_enter:
ld a, (current_room)
add a, a
add a, a ; * 4 bytes per table row
ld e, a
ld d, 0
ld hl, warden_table
add hl, de
ld a, (hl)
ld (warden_col), a
inc hl
ld a, (hl)
ld (warden_row), a
inc hl
ld a, (hl)
ld (warden_dir), a
inc hl
ld a, (hl)
ld (warden_axis), a
ld a, WARDEN_GATHER
ld (warden_timer), a
ret
; warden_table — one row per room: start col, start row, dir (+1/-1), axis.
; Each route crosses that room's gold and starts clear of the entry cell.
warden_table:
defb WARDEN_COL0, WARDEN_ROW0, -1, AXIS_VERT ; Hall — the gold's column
defb 24, 15, -1, AXIS_VERT ; Gallery — column 24, lower chamber
defb 15, 20, -1, AXIS_HORIZ ; Vault — row 20, between the coins
; ----------------------------------------------------------------------------
; 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:
call shade_for_cell
add a, a
ld e, a
ld d, 0
ld hl, shade_tiles
add hl, de
ld e, (hl)
inc hl
ld d, (hl)
ld (tile_ptr), de
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
; sfx_step — a footfall: short and low, gone almost before you notice it. Played
; under every successful move.
sfx_step:
ld b, 5
ld c, 90
call beep
ret
; sfx_door — a creak: a tone that falls in pitch as it plays, so the note groans
; downward — a heavy door swinging on its hinges.
sfx_door:
ld c, 36 ; start higher
.sd_sweep:
ld b, 3
push bc
call beep
pop bc
inc c ; lower the pitch a little
inc c
ld a, c
cp 130 ; until it has groaned down low
jr c, .sd_sweep
ret
; ----------------------------------------------------------------------------
; The music player — a note-table interpreter for the title theme.
; play_note — C = pitch (beep delay), D = duration in chunks. A note is the beep
; primitive run for D fixed-size chunks, so a note can be long though one beep's
; cycle count is a single byte.
; ----------------------------------------------------------------------------
play_note:
.pn_loop:
ld b, 24 ; one chunk = 24 square-wave cycles
push de
call beep ; B cycles at pitch C
pop de
dec d
jr nz, .pn_loop
ret
; rest_chunks — D chunks of silence, timed to roughly match a note chunk so rests
; sit in the rhythm.
rest_chunks:
.rc_loop:
ld b, 24
.rc_inner:
ld e, 75
.rc_wait:
dec e
jr nz, .rc_wait
djnz .rc_inner
dec d
jr nz, .rc_loop
ret
; The theme — a short, solemn phrase in D minor. Pitch values are beep delay
; constants (larger = lower); durations are chunk counts. One voice, no harmony:
; the melody carries the whole mood by itself.
theme:
defb 75, 8 ; D5
defb 63, 8 ; F5
defb 50, 8 ; A5
defb 50, 12 ; A5 (held)
defb 56, 8 ; G5
defb 63, 8 ; F5
defb 66, 8 ; E5
defb 75, 16 ; D5 (resolve)
defb 0, 6 ; breath
defb 56, 8 ; G5
defb 63, 8 ; F5
defb 50, 8 ; A5
defb 75, 16 ; D5 (home)
defb $FF ; end
; draw_battlements — a crown of stone merlons across the title, the keep's
; silhouette above its name. Drawn in wall stone at row 6, columns 8..23.
draw_battlements:
ld b, 6
ld c, 8
.db_loop:
push bc
ld hl, battlement_tile
ld (tile_ptr), hl
ld a, WALL_ATTR
ld (tile_attr), a
call draw_tile
pop bc
inc c
ld a, c
cp 24
jr nz, .db_loop
ret
battlement_tile: ; crenellations — merlons over solid wall
defb %11011011
defb %11011011
defb %11011011
defb %11111111
defb %11111111
defb %11111111
defb %11111111
defb %11111111
gold_tile:
defb %00000000
defb %00111100
defb %01111110
defb %01111110
defb %01111110
defb %01111110
defb %00111100
defb %00000000
; One byte of mood per room: how slowly its light fades.
room_falloff:
defb 2 ; Hall — broad, soft pool
defb 1 ; Gallery — medium
defb 0 ; Vault — tight pool, deep dark
; The shade ramp: lightest (lit) to darkest (deep shadow), all blue/black dither.
shade_tiles:
defw shade0_tile
defw shade1_tile
defw shade2_tile
defw shade3_tile
defw shade4_tile
shade0_tile: ; lit — nearly all blue
defb %00000000
defb %00100010
defb %00000000
defb %00000000
defb %00000000
defb %10001000
defb %00000000
defb %00000000
shade1_tile:
defb %00100010
defb %00000000
defb %10001000
defb %00000000
defb %00100010
defb %00000000
defb %10001000
defb %00000000
shade2_tile: ; the half-and-half slate from Unit 2
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
shade3_tile:
defb %10101010
defb %11111111
defb %01010101
defb %11111111
defb %10101010
defb %11111111
defb %01010101
defb %11111111
shade4_tile: ; deep shadow — nearly all black
defb %11111111
defb %11101110
defb %11111111
defb %10111011
defb %11111111
defb %11101110
defb %11111111
defb %10111011
torch_tile: ; a flame in its sconce
defb %00010000
defb %00111000
defb %00111000
defb %01111100
defb %01111100
defb %01111100
defb %00111000
defb %00010000
statue_tile:
defb %00111100
defb %01111110
defb %00111100
defb %00011000
defb %00011000
defb %00111100
defb %01111110
defb %01111110
banner_tile:
defb %01111110
defb %01111110
defb %01011010
defb %01011010
defb %01111110
defb %01111110
defb %00111100
defb %00011000
rubble_tile:
defb %00000000
defb %01100000
defb %01100000
defb %00000110
defb %00000110
defb %00011000
defb %00011000
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
call draw_battlements
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_tune:
ld hl, theme
.tt_note:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
jr z, .tt_pressed ; SPACE down -> leave
ld a, (hl)
inc hl
cp $FF
jr z, .tt_tune ; end of tune -> loop it
ld c, a
ld a, (hl)
inc hl
ld d, a
ld a, c
or a
jr z, .tt_rest
push hl
call play_note
pop hl
jr .tt_note
.tt_rest:
push hl
call rest_chunks
pop hl
jr .tt_note
.tt_pressed:
ld bc, KEYS_SPACE ; debounce: wait for release
in a, (c)
bit 0, a
jr z, .tt_pressed
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
defb 'T'
defw torch_tile
defb TORCH_ATTR
defb 'S'
defw statue_tile
defb STATUE_ATTR
defb 'B'
defw banner_tile
defb BANNER_ATTR
defb 'o'
defw rubble_tile
defb RUBBLE_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 "###############T################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "B..............S...............B"
defb "#.............ooo.........G....#"
defb "#..............................#"
defb "T........##..........##........T"
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 "#..............................T"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "........................G......#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "B..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#........................ooo...#"
defb "#..............................#"
defb "#.......................G......#"
defb "#..............................#"
defb "#..............................#"
defb "###############T################"
; 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 "#.............#T##.............#"
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_axis:
defb AXIS_VERT
warden_timer:
defb WARDEN_GATHER
caught:
defb 0
torch_count:
defb 0
cell_row:
defb 0
cell_col:
defb 0
min_dist:
defb 0
torch_list:
defs MAX_TORCHES * 2
room0_state:
defs 768
room1_state:
defs 768
room2_state:
defs 768
end start
Both endings, back to back — the keep proven to stand and proven to sleep:
The tie the proof found
Both endings reachable is the bar — and driving the win to its exact edge is how you find the place the bar bends. Take the sixth and last coin on a Warden’s own ground, on the very frame the Warden steps onto you, and the shipped keep hands down a verdict it hasn’t earned the right to give:

The fault is one of order, the same one that hid in Gloaming and The Long Night before it — Shadowkeep runs a cousin of that engine. The loop moves the thief, moves the Warden, then asks whether you were caught — and only on the next frame does it re-ask whether you’d won. So on the tie, player_step lifts the last coin (won is set), warden_step lands on you (caught is set), and the caught test fires first. The theft you completed is overruled by a catch that arrived at the same instant.
The keep isn’t built on the draught’s attribute trick, so it never had Gloaming’s phase-through-wall bug — its Warden checks “is the thief on my square?” before it consults the wall at all. But this fault it shares, and the fix is the family’s: ask the win before the catch.
.game_loop:
halt
call player_step
call warden_step
ld a, (won) ; the win, asked BEFORE the catch: a theft
or a ; completed the same frame the Warden lands
jr nz, .won ; still stands
ld a, (caught)
or a
jr nz, .lost
...
The won test moves ahead of caught, so a coin lifted on the frame a Warden lands resolves as the win it is. Same tie, same last coin:

| 46 | 46 | ei | |
| 47 | 47 | .game_loop: | |
| 48 | 48 | halt | |
| 49 | + | call player_step | |
| 50 | + | call warden_step ; every room has its Warden now | |
| 51 | + | ; the win is checked BEFORE the catch. If the last coin lifted this | |
| 52 | + | ; frame (in player_step) and a Warden landed on the thief the same | |
| 53 | + | ; frame (in warden_step), the theft was already complete — the keep | |
| 54 | + | ; stands. Ask caught first, as the shipped loop did, and that tie | |
| 55 | + | ; steals a won game: THE KEEP SLEEPS on the frame you finished. | |
| 49 | 56 | ld a, (won) | |
| 50 | 57 | or a | |
| 51 | 58 | jr nz, .won | |
| 52 | - | call player_step | |
| 53 | - | call warden_step ; every room has its Warden now | |
| 54 | 59 | ld a, (caught) | |
| 55 | 60 | or a | |
| 56 | 61 | jr nz, .lost |
The complete corrected program
; Shadowkeep — Unit 19: Stand or Sleep
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 crowns the title with stone and seals the complete, winnable-and-losable game.
org 32768
WALL_ATTR equ %01001000
FLOOR_ATTR equ %00001000
MARK_ATTR equ %00001111
TORCH_ATTR equ %01001110 ; BRIGHT, blue paper, yellow ink — a lit sconce
STATUE_ATTR equ %01001111 ; BRIGHT white on blue — pale stone (solid)
BANNER_ATTR equ %01001011 ; BRIGHT magenta on blue — a hanging (solid)
RUBBLE_ATTR equ %00001000 ; dim, like floor — walkable broken stone
MAX_SHADE equ 4
MAX_TORCHES equ 4
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
AXIS_HORIZ equ 0 ; the Warden varies its column (walks a row)
AXIS_VERT equ 1 ; the Warden varies its row (walks a column)
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
call player_step
call warden_step ; every room has its Warden now
; the win is checked BEFORE the catch. If the last coin lifted this
; frame (in player_step) and a Warden landed on the thief the same
; frame (in warden_step), the theft was already complete — the keep
; stands. Ask caught first, as the shipped loop did, and that tie
; steals a won game: THE KEEP SLEEPS on the frame you finished.
ld a, (won)
or a
jr nz, .won
ld a, (caught)
or a
jr nz, .lost
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
call warden_enter ; this room's Warden, from the table
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
; ----------------------------------------------------------------------------
; find_torch — scan the current room's map for 'T', remember where it is (or
; NO_TORCH if the room is unlit).
; ----------------------------------------------------------------------------
; find_torches — collect every 'T' in the current room (up to MAX_TORCHES) into
; torch_list as (col, row) pairs; torch_count says how many.
find_torches:
xor a
ld (torch_count), a
call room_entry_addr
ld a, (hl)
inc hl
ld h, (hl)
ld l, a
ld b, 0
.fr_row:
ld c, 0
.fr_col:
ld a, (hl)
cp 'T'
jr nz, .fr_skip
ld a, (torch_count)
cp MAX_TORCHES
jr nc, .fr_skip
push hl
add a, a ; count * 2
ld e, a
ld d, 0
ld hl, torch_list
add hl, de
ld (hl), c ; column
inc hl
ld (hl), b ; row
pop hl
ld a, (torch_count)
inc a
ld (torch_count), a
.fr_skip:
inc hl
inc c
ld a, c
cp 32
jr nz, .fr_col
inc b
ld a, b
cp 24
jr nz, .fr_row
ret
; ----------------------------------------------------------------------------
; shade_for_cell — row in B, column in C. Returns a shade 0..4 in A: the
; Chebyshev distance to the torch, halved and clamped. Near the flame = 0
; (lightest); far away = 4 (darkest). No torch = darkest everywhere.
; ----------------------------------------------------------------------------
shade_for_cell:
push bc
ld a, b
ld (cell_row), a
ld a, c
ld (cell_col), a
ld a, (torch_count)
or a
jr z, .sf_dark
ld a, 255
ld (min_dist), a
ld hl, torch_list
ld a, (torch_count)
ld b, a ; loop count
.sf_loop:
ld a, (cell_col)
sub (hl) ; - torch col
jr nc, .sf_dc
neg
.sf_dc:
ld d, a ; |dc|
inc hl ; -> torch row
ld a, (cell_row)
sub (hl)
jr nc, .sf_dr
neg
.sf_dr:
inc hl ; -> next torch pair
cp d ; A = |dr|; take the larger
jr nc, .sf_mx
ld a, d
.sf_mx:
ld e, a ; this torch's distance
ld a, (min_dist)
cp e
jr c, .sf_keep ; min already smaller
ld a, e
ld (min_dist), a
.sf_keep:
djnz .sf_loop
ld a, (current_room)
ld c, a
ld b, 0
ld hl, room_falloff
add hl, bc
ld b, (hl)
ld a, (min_dist)
inc b
.sf_shift:
dec b
jr z, .sf_clamp
srl a
jr .sf_shift
.sf_clamp:
cp MAX_SHADE + 1
jr c, .sf_done
ld a, MAX_SHADE
.sf_done:
pop bc
ret
.sf_dark:
ld a, MAX_SHADE
pop bc
ret
draw_room:
call find_torches
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)
cp '.'
jr nz, .not_floor
call shade_for_cell
add a, a
ld e, a
ld d, 0
ld hl, shade_tiles
add hl, de
ld e, (hl)
inc hl
ld d, (hl)
ld (tile_ptr), de
ld a, FLOOR_ATTR
ld (tile_attr), a
call draw_tile
jr .cell_done
.not_floor:
call lookup_tile
call draw_tile
.cell_done:
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 sfx_step
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 sfx_door
call draw_room
call save_under
call draw_thief
call warden_enter ; this room's Warden, clear of the entry cell
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
; propose the next cell along the patrol axis, in the current dir
ld a, (warden_axis)
or a
jr nz, .ws_vert
.ws_horiz:
ld a, (warden_row)
ld b, a ; B = row (fixed)
ld a, (warden_dir)
ld hl, warden_col
add a, (hl)
ld c, a ; C = next col
jr .ws_have
.ws_vert:
ld a, (warden_col)
ld c, a ; C = col (fixed)
ld a, (warden_dir)
ld hl, warden_row
add a, (hl)
ld b, a ; B = next row
.ws_have:
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 B, col C)
call restore_warden
pop bc
ld a, c
ld (warden_col), a
ld a, b
ld (warden_row), a
call save_warden ; photograph the new cell's ground
call draw_warden
ret
; ----------------------------------------------------------------------------
; warden_enter — instantiate the current room's Warden from warden_table: place
; it at the route-start (chosen clear of the entry cell), set its axis, and start
; its gather. Called on new_game and on every room change. A second mover is a
; table entry — like the torches and the gold.
; ----------------------------------------------------------------------------
warden_enter:
ld a, (current_room)
add a, a
add a, a ; * 4 bytes per table row
ld e, a
ld d, 0
ld hl, warden_table
add hl, de
ld a, (hl)
ld (warden_col), a
inc hl
ld a, (hl)
ld (warden_row), a
inc hl
ld a, (hl)
ld (warden_dir), a
inc hl
ld a, (hl)
ld (warden_axis), a
ld a, WARDEN_GATHER
ld (warden_timer), a
ret
; warden_table — one row per room: start col, start row, dir (+1/-1), axis.
; Each route crosses that room's gold and starts clear of the entry cell.
warden_table:
defb WARDEN_COL0, WARDEN_ROW0, -1, AXIS_VERT ; Hall — the gold's column
defb 24, 15, -1, AXIS_VERT ; Gallery — column 24, lower chamber
defb 15, 20, -1, AXIS_HORIZ ; Vault — row 20, between the coins
; ----------------------------------------------------------------------------
; 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:
call shade_for_cell
add a, a
ld e, a
ld d, 0
ld hl, shade_tiles
add hl, de
ld e, (hl)
inc hl
ld d, (hl)
ld (tile_ptr), de
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
; sfx_step — a footfall: short and low, gone almost before you notice it. Played
; under every successful move.
sfx_step:
ld b, 5
ld c, 90
call beep
ret
; sfx_door — a creak: a tone that falls in pitch as it plays, so the note groans
; downward — a heavy door swinging on its hinges.
sfx_door:
ld c, 36 ; start higher
.sd_sweep:
ld b, 3
push bc
call beep
pop bc
inc c ; lower the pitch a little
inc c
ld a, c
cp 130 ; until it has groaned down low
jr c, .sd_sweep
ret
; ----------------------------------------------------------------------------
; The music player — a note-table interpreter for the title theme.
; play_note — C = pitch (beep delay), D = duration in chunks. A note is the beep
; primitive run for D fixed-size chunks, so a note can be long though one beep's
; cycle count is a single byte.
; ----------------------------------------------------------------------------
play_note:
.pn_loop:
ld b, 24 ; one chunk = 24 square-wave cycles
push de
call beep ; B cycles at pitch C
pop de
dec d
jr nz, .pn_loop
ret
; rest_chunks — D chunks of silence, timed to roughly match a note chunk so rests
; sit in the rhythm.
rest_chunks:
.rc_loop:
ld b, 24
.rc_inner:
ld e, 75
.rc_wait:
dec e
jr nz, .rc_wait
djnz .rc_inner
dec d
jr nz, .rc_loop
ret
; The theme — a short, solemn phrase in D minor. Pitch values are beep delay
; constants (larger = lower); durations are chunk counts. One voice, no harmony:
; the melody carries the whole mood by itself.
theme:
defb 75, 8 ; D5
defb 63, 8 ; F5
defb 50, 8 ; A5
defb 50, 12 ; A5 (held)
defb 56, 8 ; G5
defb 63, 8 ; F5
defb 66, 8 ; E5
defb 75, 16 ; D5 (resolve)
defb 0, 6 ; breath
defb 56, 8 ; G5
defb 63, 8 ; F5
defb 50, 8 ; A5
defb 75, 16 ; D5 (home)
defb $FF ; end
; draw_battlements — a crown of stone merlons across the title, the keep's
; silhouette above its name. Drawn in wall stone at row 6, columns 8..23.
draw_battlements:
ld b, 6
ld c, 8
.db_loop:
push bc
ld hl, battlement_tile
ld (tile_ptr), hl
ld a, WALL_ATTR
ld (tile_attr), a
call draw_tile
pop bc
inc c
ld a, c
cp 24
jr nz, .db_loop
ret
battlement_tile: ; crenellations — merlons over solid wall
defb %11011011
defb %11011011
defb %11011011
defb %11111111
defb %11111111
defb %11111111
defb %11111111
defb %11111111
gold_tile:
defb %00000000
defb %00111100
defb %01111110
defb %01111110
defb %01111110
defb %01111110
defb %00111100
defb %00000000
; One byte of mood per room: how slowly its light fades.
room_falloff:
defb 2 ; Hall — broad, soft pool
defb 1 ; Gallery — medium
defb 0 ; Vault — tight pool, deep dark
; The shade ramp: lightest (lit) to darkest (deep shadow), all blue/black dither.
shade_tiles:
defw shade0_tile
defw shade1_tile
defw shade2_tile
defw shade3_tile
defw shade4_tile
shade0_tile: ; lit — nearly all blue
defb %00000000
defb %00100010
defb %00000000
defb %00000000
defb %00000000
defb %10001000
defb %00000000
defb %00000000
shade1_tile:
defb %00100010
defb %00000000
defb %10001000
defb %00000000
defb %00100010
defb %00000000
defb %10001000
defb %00000000
shade2_tile: ; the half-and-half slate from Unit 2
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
defb %10101010
defb %01010101
shade3_tile:
defb %10101010
defb %11111111
defb %01010101
defb %11111111
defb %10101010
defb %11111111
defb %01010101
defb %11111111
shade4_tile: ; deep shadow — nearly all black
defb %11111111
defb %11101110
defb %11111111
defb %10111011
defb %11111111
defb %11101110
defb %11111111
defb %10111011
torch_tile: ; a flame in its sconce
defb %00010000
defb %00111000
defb %00111000
defb %01111100
defb %01111100
defb %01111100
defb %00111000
defb %00010000
statue_tile:
defb %00111100
defb %01111110
defb %00111100
defb %00011000
defb %00011000
defb %00111100
defb %01111110
defb %01111110
banner_tile:
defb %01111110
defb %01111110
defb %01011010
defb %01011010
defb %01111110
defb %01111110
defb %00111100
defb %00011000
rubble_tile:
defb %00000000
defb %01100000
defb %01100000
defb %00000110
defb %00000110
defb %00011000
defb %00011000
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
call draw_battlements
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_tune:
ld hl, theme
.tt_note:
ld bc, KEYS_SPACE
in a, (c)
bit 0, a
jr z, .tt_pressed ; SPACE down -> leave
ld a, (hl)
inc hl
cp $FF
jr z, .tt_tune ; end of tune -> loop it
ld c, a
ld a, (hl)
inc hl
ld d, a
ld a, c
or a
jr z, .tt_rest
push hl
call play_note
pop hl
jr .tt_note
.tt_rest:
push hl
call rest_chunks
pop hl
jr .tt_note
.tt_pressed:
ld bc, KEYS_SPACE ; debounce: wait for release
in a, (c)
bit 0, a
jr z, .tt_pressed
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
defb 'T'
defw torch_tile
defb TORCH_ATTR
defb 'S'
defw statue_tile
defb STATUE_ATTR
defb 'B'
defw banner_tile
defb BANNER_ATTR
defb 'o'
defw rubble_tile
defb RUBBLE_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 "###############T################"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "B..............S...............B"
defb "#.............ooo.........G....#"
defb "#..............................#"
defb "T........##..........##........T"
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 "#..............................T"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "###############.################"
defb "#..............................#"
defb "#..............................#"
defb "........................G......#"
defb "#..............................#"
defb "#..............................#"
defb "#..............................#"
defb "B..............................#"
defb "#..............................#"
defb "#..............................#"
defb "#........................ooo...#"
defb "#..............................#"
defb "#.......................G......#"
defb "#..............................#"
defb "#..............................#"
defb "###############T################"
; 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 "#.............#T##.............#"
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_axis:
defb AXIS_VERT
warden_timer:
defb WARDEN_GATHER
caught:
defb 0
torch_count:
defb 0
cell_row:
defb 0
cell_col:
defb 0
min_dist:
defb 0
torch_list:
defs MAX_TORCHES * 2
room0_state:
defs 768
room1_state:
defs 768
room2_state:
defs 768
end start
This is what the winnability gate is for. “Drive it to both endings” isn’t a ceremony you perform once and file away — it’s a net, and this is the frame it caught: a third outcome hiding at the seam between the other two, where a won game read as a lost one. step-02 closes it, and THE KEEP STANDS means what it says on every frame, including the last.
Try this: your own crown
Redraw battlement_tile. Make the merlons taller, or add an arrow-slit, or a banner hanging between the teeth. It is eight bytes; you can redesign the top of your keep in a minute. A title is the one place in the whole game where you are pure artist — spend a little time making it yours.
Try this: a subtitle
Print a second line under SHADOWKEEP — “a thief, a keep, a dark that watches” — in a dimmer attribute. A single line of flavour under the name sets the tone before a key is pressed. Keep it short; a title says one thing well.
Try this: prove it yourself
Write your own capture scripts — one that wins, one that loses — and run them. It is the discipline that separates “I think it works” from “I watched it work.” A game you can drive to both endings on demand is a game you can trust, and trust is what lets you change it without fear.
When it’s wrong, see why
- The battlements draw over the title text. They share a row with the name — move the band up a row, clear of the letters.
- The crown is a solid bar, not crenellated. The tile has no gaps — the merlon pattern needs
0bits between the teeth, or every cell reads as full wall. - The win run is caught, or the loss run wins. Your capture script’s timing drifted — the patrols are deterministic, so a run that misses is a run mistimed. Read the Warden’s position and try the gap again.
- Only one ending ever fires. The loop’s win and lose exits aren’t both wired — check both
wonandcaughtare tested and both break out. You proved this in Unit 11; confirm it survived. - A cleared keep reads THE KEEP SLEEPS. The
caughttest runs beforewonis re-checked, so lifting the last coin on the frame a Warden lands resolves as a capture. Order thewontest ahead ofcaughtin the loop — a completed theft is a win, whatever arrives the same instant.
Before and after — the Place, complete
You began this game, nineteen units ago, with a hooded thief who could walk a single room. You end it with a keep: three chambers of lit, furnished, composed stone; gold to steal and Wardens to steal it past; a voice of footsteps and doors and a theme; two endings, both proven. Every piece was built on the one before, seam by seam, and every one of them still runs on its own. That is what a finished game looks like from the inside — not a burst of magic, but a long chain of small, sure steps, each leaving the thing playable.
The keep stands, or the keep sleeps. Either way, you built the place it happens in.
What you’ve learnt
- Sealing is a phase, not an afterthought. A crown on the title and a proof of both endings turn “it works” into “it’s finished.”
- A title is where you are the artist. One hand-pixelled stroke — eight bytes of merlon — gives a game a face of its own.
- A game is proven at its endings. Winnable and losable, each reachable on demand, is the bar; drive it there and watch it arrive.
- And the proof finds bugs. Driving the win to its exact edge — the last coin on a Warden’s ground, caught the same frame — surfaced a tie the loop resolved the wrong way. Ordering the win before the catch closed it. The gate isn’t ceremony; it’s a net.
- A big game is a chain of small sure steps. Nineteen units, each built on the last, each runnable — that is how something this whole gets made.
What’s next
The Place is complete — the first full chapter of Shadowkeep, a keep you can enter, clear, and lose. But its story isn’t over. The sleepers you saw frozen in the dark were people once; the curse that took them can be lifted, not only fled. In a later return — the Waking — the keep grows: inhabitants to free, allies who stand in for the lives you’re spending, a dark with a history. For now, the keep stands. Step out of it, and on to the next machine and the next game — you have built a place, whole and your own, and that is the thing this whole track was for.