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

Something in the Dark

The keep has never had anyone in it but you. Give it a second figure — a Warden, drawn the same way as the thief, with its own private patch of screen to save and restore — and set it walking a slow, fixed route of its own.

53% of Shadowkeep

The keep can be cleared now — six coins, a win, a reason to move. But you are alone in it, and nothing you do is ever risky. You collect at your leisure; the worst that happens is nothing. A keep with treasure and no danger is a shopping trip.

So we put something else in the keep. Not a hazard yet — first, an inhabitant: a Warden, a second figure that moves in the Hall on business of its own. You already know how to move one figure — the thief is drawn, saved, and restored every step. The whole of this unit is the realisation that there is nothing special about the thief. A second mover is the same handful of routines with its own name and its own patch of memory. Build one, and the keep is no longer empty.

What you’ll see by the end

The Hall, with the hooded thief near the centre and, to the right, a second figure drawn in pale cyan — a cowled sentinel standing on the same column as the two coins.
A second figure in the keep: the Warden, a pale cyan sentinel, standing where the thief cannot. Same drawing routines as the thief, its own patch of screen to keep — one cell, one occupant.

A second figure stands in the Hall, cowled and pale cyan, on the column where the coins hang. Watch a moment and it moves — one slow step, then another, up its column and back down, reversing when it meets a wall. It does not chase you. It does not, yet, even notice you: walk right up to it and nothing happens. It is a presence, a patrol, a thing that shares the room — and building it teaches the pattern every enemy, ally and moving object in every later game is cut from.

One cell, one occupant

The thief occupies a cell, and to move he does four things: save the eight bytes and one attribute beneath him, restore them when he leaves, and draw his glyph where he lands. The Warden is the identical dance with its own name and its own nine-byte buffer:

under_warden:
            defs    9                 ; the ground the Warden stands on

save_warden:    ; grab the screen under the Warden into under_warden
restore_warden: ; put it back when the Warden moves off
draw_warden:    ; stamp the Warden's glyph and attribute

Nothing here is new — it is save_under / restore_under / draw_thief with warden swapped for thief. That is the lesson: the two-mover shape scales. Give a thing a position, a buffer, and these three routines, and it is a mover. The keep could hold a dozen this way.

The glyph is its own — a hooded, faceless sentinel, drawn in bright cyan so it reads cold against the warm thief and the yellow gold:

warden_glyph:
            defb    %00111100         ; the cowl
            defb    %01111110
            defb    %01100110         ; dark eye-slits
            defb    %01111110
            defb    %00111100
            defb    %01111110
            defb    %01111110
            defb    %01011010         ; a ragged hem

A route, not a chase

In The Long Night the dark hunted you — it read your position and came for it. The Warden is deliberately the opposite, and the difference matters: it walks a fixed route it does not deviate from, one cell every WARDEN_SPEED frames, up its column until a wall turns it back. This is Sabre Wulf’s roaming beasts, not a hunter — a pattern you can learn and time, not a pursuer you must flee:

warden_step:
            ; ... once every WARDEN_SPEED frames ...
            ; propose the next cell up/down the column, in the current direction
            call    wall_at
            jr      z, .commit       ; floor ahead — restore, move, save, draw
            ld      a, (warden_dir)  ; wall ahead — reverse and wait
            neg
            ld      (warden_dir), a
            ret

The step is the thief’s move in miniature: propose a cell, ask wall_at, and either take it (restoring the old cell, drawing the new) or turn around. Because it reads walls the same way the thief does, it also reads the thief as a wall — his cell is bright, and bright means solid — so for now it turns aside when it meets him. It cannot pass through him, and it cannot yet catch him. That comes next unit; here it is only learning to walk.

A beat before it begins

When a room is entered, the Warden does not move for WARDEN_GATHER frames — a held breath, long enough for you to see where it is before it starts. It is a small courtesy that will matter a great deal once the Warden is dangerous: you should never be caught by something you were given no moment to read.

Milestone — a second mover, walking

The Warden is the thief’s own save / restore / draw, given a second name and a nine-byte buffer of its own — proof that a mover is a pattern, not a special case. It stands in the Hall (step 1), then walks a fixed column one cell at a time, reversing at walls after a gathering beat (step 2). It reads the thief as solid and turns aside at him: a presence, not yet a threat.

Step 1: the Warden's body — a second sprite, saved and drawn
+78-1
11 ; Shadowkeep — Unit 10: Something in the Dark
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 9's end: gold and a win, but the keep is empty and safe.
3+; step-01 gives the Hall a Warden — a second mover with a body, not yet moving.
44
55 org 32768
66
...
1010 THIEF equ %01001010
1111 GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold
1212 TOTAL_GOLD equ 6
13+WARDEN equ %01000101 ; BRIGHT cyan on black — the sentinel
14+WARDEN_COL0 equ 26 ; the Hall's gold column
15+WARDEN_ROW0 equ 11
1316 WALL_BIT equ 6
1417
1518 START_COL equ 15
...
7275 call draw_room
7376 call save_under
7477 call draw_thief
78+ ld a, WARDEN_COL0
79+ ld (warden_col), a
80+ ld a, WARDEN_ROW0
81+ ld (warden_row), a
82+ call save_warden
83+ call draw_warden
7584 ret
7685
7786 mark_step:
...
311320 call draw_room
312321 call save_under
313322 call draw_thief
323+ ld a, (current_room)
324+ or a
325+ ret nz ; the Warden is only in the Hall
326+ call save_warden
327+ call draw_warden
314328 ret
315329
316330 pos_bc:
...
368382 inc de
369383 inc h
370384 djnz .thief_row
385+ ret
386+
387+; ----------------------------------------------------------------------------
388+; The Warden — something shares the keep. A second mover, drawn exactly like the
389+; thief: a private under-buffer and its own save / draw, a spectral hooded
390+; sentinel. First we simply give it a body (it neither moves nor harms yet).
391+; ----------------------------------------------------------------------------
392+wpos_bc:
393+ ld a, (warden_row)
394+ ld b, a
395+ ld a, (warden_col)
396+ ld c, a
397+ ret
398+
399+save_warden:
400+ call wpos_bc
401+ call scr_addr_cr
402+ ld de, under_warden
403+ ld b, 8
404+.svw_row:
405+ ld a, (hl)
406+ ld (de), a
407+ inc de
408+ inc h
409+ djnz .svw_row
410+ call wpos_bc
411+ call attr_addr_cr
412+ ld a, (hl)
413+ ld (under_warden + 8), a
414+ ret
415+
416+draw_warden:
417+ call wpos_bc
418+ call attr_addr_cr
419+ ld (hl), WARDEN
420+ call wpos_bc
421+ call scr_addr_cr
422+ ld de, warden_glyph
423+ ld b, 8
424+.drw_row:
425+ ld a, (de)
426+ ld (hl), a
427+ inc de
428+ inc h
429+ djnz .drw_row
371430 ret
431+
432+; A spectral hooded sentinel: a cowl with dark eye-slits over a robe that frays
433+; at the hem. Cold, faceless, unmistakably a figure.
434+warden_glyph:
435+ defb %00111100
436+ defb %01111110
437+ defb %01100110
438+ defb %01111110
439+ defb %00111100
440+ defb %01111110
441+ defb %01111110
442+ defb %01011010
372443
373444 ; ----------------------------------------------------------------------------
374445 ; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns
...
778849 defb TOTAL_GOLD
779850 won:
780851 defb 0
852+under_warden:
853+ defb 0, 0, 0, 0, 0, 0, 0, 0, 0
854+warden_col:
855+ defb WARDEN_COL0
856+warden_row:
857+ defb WARDEN_ROW0
781858
782859 room0_state:
783860 defs 768
Step 2: the patrol — one cell at a time, reversing at walls
+66-1
11 ; Shadowkeep — Unit 10: Something in the Dark
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-01 gives the Hall a Warden — a second mover with a body, not yet moving.
3+; step-02 sets the Warden walking — a deterministic patrol up and down its column.
44
55 org 32768
66
...
1313 WARDEN equ %01000101 ; BRIGHT cyan on black — the sentinel
1414 WARDEN_COL0 equ 26 ; the Hall's gold column
1515 WARDEN_ROW0 equ 11
16+WARDEN_SPEED equ 10 ; frames between patrol steps
17+WARDEN_GATHER equ 50 ; a beat before it begins to move
1618 WALL_BIT equ 6
1719
1820 START_COL equ 15
...
4042 or a
4143 jr nz, .won
4244 call player_step
45+ ld a, (current_room)
46+ or a
47+ call z, warden_step ; the Warden haunts the Hall (room 0)
4348 call mark_step
4449 jr .game_loop
4550 .won:
...
7984 ld (warden_col), a
8085 ld a, WARDEN_ROW0
8186 ld (warden_row), a
87+ ld a, -1
88+ ld (warden_dir), a
89+ ld a, WARDEN_GATHER
90+ ld (warden_timer), a
8291 call save_warden
8392 call draw_warden
8493 ret
...
440449 defb %01111110
441450 defb %01111110
442451 defb %01011010
452+
453+restore_warden:
454+ call wpos_bc
455+ call scr_addr_cr
456+ ld de, under_warden
457+ ld b, 8
458+.rsw_row:
459+ ld a, (de)
460+ ld (hl), a
461+ inc de
462+ inc h
463+ djnz .rsw_row
464+ call wpos_bc
465+ call attr_addr_cr
466+ ld a, (under_warden + 8)
467+ ld (hl), a
468+ ret
469+
470+; warden_step — the patrol. Walks its column one cell every WARDEN_SPEED frames,
471+; after a WARDEN_GATHER beat, reversing at walls. Deterministic: a route you learn
472+; and time. It reads the thief's cell as a wall (his attribute has WALL_BIT set),
473+; so it turns aside at him for now — the catch is Unit 11.
474+warden_step:
475+ ld a, (warden_timer)
476+ dec a
477+ ld (warden_timer), a
478+ ret nz
479+ ld a, WARDEN_SPEED
480+ ld (warden_timer), a
481+ ld a, (warden_col)
482+ ld c, a ; C = column (fixed)
483+ ld a, (warden_dir)
484+ ld hl, warden_row
485+ add a, (hl)
486+ ld b, a ; B = next row
487+ push bc
488+ call wall_at
489+ pop bc
490+ jr z, .ws_commit ; floor ahead — step onto it
491+ ld a, (warden_dir) ; wall ahead — reverse, wait a beat
492+ neg
493+ ld (warden_dir), a
494+ ret
495+.ws_commit:
496+ push bc ; keep the proposed row — restore heals the OLD cell
497+ call restore_warden
498+ pop bc
499+ ld a, b
500+ ld (warden_row), a
501+ call save_warden ; photograph the new cell's ground
502+ call draw_warden
503+ ret
443504
444505 ; ----------------------------------------------------------------------------
445506 ; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns
...
855916 defb WARDEN_COL0
856917 warden_row:
857918 defb WARDEN_ROW0
919+warden_dir:
920+ defb -1
921+warden_timer:
922+ defb WARDEN_GATHER
858923
859924 room0_state:
860925 defs 768
The complete program
; Shadowkeep — Unit 10: Something in the Dark
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 sets the Warden walking — a deterministic patrol up and down its column.

            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
            call    z, warden_step   ; the Warden haunts the Hall (room 0)
            call    mark_step
            jr      .game_loop
.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      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
            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

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

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

            end     start

Watch it walk its column, up and back, while the thief collects around it — and note it never smears the coins or the stone it passes over, because it lays each cell back exactly as the thief does:

The Warden walks a fixed column, reversing at the walls, one cell every few frames. It saves and restores the ground beneath it just as the thief does — so the coins it passes over reappear untouched. A route to learn, not a hunter to flee.

Try this: send it sideways

The Warden walks a column because its step changes the row. Change it to step the column instead and it walks a row — a sentinel pacing left and right. (You are one unit early: next unit the patrol becomes data, and either direction is a table entry. Here, feel the one-line difference by hand.)

Try this: change its pace

WARDEN_SPEED is frames per step. Make it small and the Warden strides; make it large and it drifts, ponderous and stately. A slow patrol is a rhythm you learn, eerie to watch; a fast one is urgent. The number is a character trait — pick the one that suits a thing that turns you to stone.

Try this: a second Warden, by hand

Copy warden_col / warden_row / the buffer to a warden2_* set, give it a start cell, and call its step too. Two patrols in one room. It is clumsy done this way — which is exactly why the next unit but one turns the Warden into a table, so a whole keep of them costs no extra code.

When it’s wrong, see why

  • The Warden smears a trail behind it. It is drawing without restoring — restore_warden isn’t called before it moves, so the old cell keeps its glyph. Restore the ground, then move.
  • The Warden eats a coin permanently. Its buffer and the thief’s have been crossed, or save_warden runs before the room is drawn, so it saves rubbish and lays rubbish down. Save the ground once the room is painted.
  • The Warden flies through walls. warden_step isn’t consulting wall_at, or is testing the current cell instead of the proposed one. It must ask about the cell it is about to enter.
  • The Warden vanishes when you leave the Hall and return. It is only redrawn in its own room — re-stamp it on entry, or (next unit) instantiate it from its table.
  • The Warden and thief flicker over each other. They are being drawn in the wrong order, or one restores after the other draws. The Warden reads the thief as a wall and should never share his cell in the first place.

Before and after

You began with a keep that held only you, and finished with a keep that holds someone else — a second figure that moves, keeps its own patch of screen, and walks a route you can watch and learn. And you wrote almost nothing new to do it: the Warden is the thief’s own movement, renamed. That is the quiet, powerful idea under every crowd of sprites you will ever draw — a mover is a pattern you stamp out, not a hero you hand-build. The keep is inhabited now. Next, it becomes dangerous.

What you’ve learnt

  • A mover is a pattern, not a special case. Position, a buffer, save / restore / draw — give any thing those four and it moves like the hero.
  • A patrol is a route, not a chase. A fixed path you can learn and time reads utterly differently from a pursuer — and it is far simpler code.
  • Read walls, reverse at them. The same wall_at the thief uses turns the patrol at the room’s edges — and makes it treat the thief, for now, as just another wall.
  • Give the player a beat to read. A gather before the patrol begins is the courtesy that makes danger fair.

What’s next

The Warden walks, but it is harmless — it turns aside at you like a wall. That is about to change. In Unit 11, “Join the Sleepers,” the Warden’s touch becomes the thing the keep has lacked since the beginning: a way to lose. Step where it steps, and you join the sleepers — frozen mid-stride, the keep asleep. The moment the Place can be lost is the moment it truly becomes a game.