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

A Warden for Every Room

One Warden in one room is a demo. Turn the Warden into data — a table with a line per room — so every room instantiates its own patrol on the coins' ground, and the whole keep, at last, is a game.

63% of Shadowkeep

The keep can be lost — but only in the Hall. Cross to the Gallery or the Vault and the danger evaporates; you collect four of the six coins with no more risk than a walk. A game where two-thirds of the rooms are safe isn’t finished. It has a dangerous level and two empty ones.

The fix is not to write two more Wardens by hand. You met that temptation last unit and felt how clumsy it was — copy the routines, copy the variables, call each step. The keep already knows a better way, because you already store the gold and the torches as data: a letter in the map, read at run time. This unit does the same for the Warden. One table, a line per room — where it starts, which way it walks, along a row or a column — and every room, on entry, instantiates the patrol that belongs to it. Density from data, not from duplicated code. And because the coins were placed on the Wardens’ routes from the beginning, nothing has to move: the guards arrive on the treasure’s ground.

What you’ll see by the end

The Vault, its central altar of stone, with the two coins low on the floor and a cyan Warden pacing the row between them — the thief entering from the doorway below.
The Vault has its own Warden now — and this one paces a row, not a column, guarding the two coins strung along it. One table gave every room its guard; a line of data is all a new patrol costs.

Every room has its guard. The Hall’s Warden walks its column as before; the Gallery’s paces a column of its own; the Vault’s strides a row, back and forth between the two coins on the floor. Each stands where the gold is, so every coin in the keep is now a coin you must read a patrol to reach. Clear all six and THE KEEP STANDS; touch any Warden and THE KEEP SLEEPS. The whole keep is the game now, not one room of it.

A Warden is a line in a table

Here is the table — one row per room, four bytes each: the start column, the start row, the direction, and the axis (does it walk a row or a column?):

warden_table:
            defb    26, 11, -1, AXIS_VERT    ; Hall — up the coins' column
            defb    24, 15, -1, AXIS_VERT    ; Gallery — its own column
            defb    15, 20, -1, AXIS_HORIZ   ; Vault — a row between the coins

On entering a room, warden_enter reads its line and sets the Warden up from it — position, direction, axis, and a fresh gathering beat:

warden_enter:
            ld      a, (current_room)
            ; ... index warden_table by room * 4 ...
            ; ... load warden_col, warden_row, warden_dir, warden_axis ...
            ld      a, WARDEN_GATHER
            ld      (warden_timer), a        ; a beat before it moves
            ret

This is exactly how the keep already reads its rooms, its gold, its torches: data indexed by where you are. Adding a fourth room’s Warden is now a fourth line — no new code at all.

One patrol, either axis

Last unit the Warden walked a column because its step changed the row. To let a room’s guard walk either way, the step now reads its axis and changes the row or the column accordingly:

warden_step:
            ld      a, (warden_axis)
            or      a
            jr      nz, .vertical
.horizontal:
            ; ... propose the next COLUMN along the row ...
            jr      .have_cell
.vertical:
            ; ... propose the next ROW along the column ...
.have_cell:
            ; ... thief there? caught. wall ahead? reverse. else step. ...

The rest — catch the thief, reverse at a wall, restore-move-save-draw — is unchanged. One routine, two kinds of patrol, chosen by a byte in the table.

The gather earns its keep

Now that every room hands you a Warden the moment you enter, the gathering beat you built two units ago stops being a courtesy and becomes essential. Each Warden’s start cell is chosen clear of the doorway you arrive through, and it holds still for WARDEN_GATHER frames before it moves — so you never materialise onto a guard, and you always get a breath to read the room before it turns dangerous. Fair danger is danger you were shown before it could touch you.

Milestone — the whole keep is a game

The Warden becomes a table — a line per room giving its start, direction, and axis — and warden_enter instantiates the right patrol on every room change; the step reads the axis to walk a row or a column. The Gallery and the Vault get their guards for two lines of data, each standing on gold placed there since Unit 9. The keep is winnable and losable in every room: drive it to THE KEEP STANDS and to THE KEEP SLEEPS to prove it.

Step 1: a patrol that walks a row or a column (the axis byte)
+23-3
11 ; Shadowkeep — Unit 12: A Warden for Every Room
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 11's end: one Warden, in the Hall alone; the other rooms are safe.
3+; step-01 lets a Warden walk a row or a column — patrol on either axis.
44
55 org 32768
66
...
1515 WARDEN_ROW0 equ 11
1616 WARDEN_SPEED equ 10 ; frames between patrol steps
1717 WARDEN_GATHER equ 50 ; a beat before it begins to move
18+AXIS_HORIZ equ 0 ; the Warden varies its column (walks a row)
19+AXIS_VERT equ 1 ; the Warden varies its row (walks a column)
1820 WALL_BIT equ 6
1921
2022 START_COL equ 15
...
487489 ret nz
488490 ld a, WARDEN_SPEED
489491 ld (warden_timer), a
492+ ; propose the next cell along the patrol axis, in the current dir
493+ ld a, (warden_axis)
494+ or a
495+ jr nz, .ws_vert
496+.ws_horiz:
497+ ld a, (warden_row)
498+ ld b, a ; B = row (fixed)
499+ ld a, (warden_dir)
500+ ld hl, warden_col
501+ add a, (hl)
502+ ld c, a ; C = next col
503+ jr .ws_have
504+.ws_vert:
490505 ld a, (warden_col)
491- ld c, a ; C = column (fixed)
506+ ld c, a ; C = col (fixed)
492507 ld a, (warden_dir)
493508 ld hl, warden_row
494509 add a, (hl)
495510 ld b, a ; B = next row
511+.ws_have:
496512 ld a, b ; the thief there? caught — check BEFORE
497513 ld hl, thief_row ; wall_at, because his own cell reads as wall
498514 cp (hl)
...
514530 ld (warden_dir), a
515531 ret
516532 .ws_commit:
517- push bc ; keep the proposed row — restore heals the OLD cell
533+ push bc ; keep the proposed (row B, col C)
518534 call restore_warden
519535 pop bc
536+ ld a, c
537+ ld (warden_col), a
520538 ld a, b
521539 ld (warden_row), a
522540 call save_warden ; photograph the new cell's ground
...
9891007 defb WARDEN_ROW0
9901008 warden_dir:
9911009 defb -1
1010+warden_axis:
1011+ defb AXIS_VERT
9921012 warden_timer:
9931013 defb WARDEN_GATHER
9941014 caught:
Step 2: a Warden for every room, from a table
+40-17
11 ; Shadowkeep — Unit 12: A Warden for Every Room
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-01 lets a Warden walk a row or a column — patrol on either axis.
3+; step-02 gives every room its own Warden, instantiated from a table on entry.
44
55 org 32768
66
...
4444 or a
4545 jr nz, .won
4646 call player_step
47- ld a, (current_room)
48- or a
49- jr nz, .skip_warden ; the Warden haunts the Hall (room 0)
50- call warden_step ; it may step onto the thief
47+ call warden_step ; every room has its Warden now
5148 ld a, (caught)
5249 or a
5350 jr nz, .lost
54-.skip_warden:
5551 call mark_step
5652 jr .game_loop
5753 .lost:
...
9187 call draw_room
9288 call save_under
9389 call draw_thief
94- ld a, WARDEN_COL0
95- ld (warden_col), a
96- ld a, WARDEN_ROW0
97- ld (warden_row), a
98- ld a, -1
99- ld (warden_dir), a
100- ld a, WARDEN_GATHER
101- ld (warden_timer), a
90+ call warden_enter ; this room's Warden, from the table
10291 call save_warden
10392 call draw_warden
10493 ret
...
340329 call draw_room
341330 call save_under
342331 call draw_thief
343- ld a, (current_room)
344- or a
345- ret nz ; the Warden is only in the Hall
332+ call warden_enter ; this room's Warden, clear of the entry cell
346333 call save_warden
347334 call draw_warden
348335 ret
...
539526 ld (warden_row), a
540527 call save_warden ; photograph the new cell's ground
541528 call draw_warden
529+ ret
530+
531+; ----------------------------------------------------------------------------
532+; warden_enter — instantiate the current room's Warden from warden_table: place
533+; it at the route-start (chosen clear of the entry cell), set its axis, and start
534+; its gather. Called on new_game and on every room change. A second mover is a
535+; table entry — like the torches and the gold.
536+; ----------------------------------------------------------------------------
537+warden_enter:
538+ ld a, (current_room)
539+ add a, a
540+ add a, a ; * 4 bytes per table row
541+ ld e, a
542+ ld d, 0
543+ ld hl, warden_table
544+ add hl, de
545+ ld a, (hl)
546+ ld (warden_col), a
547+ inc hl
548+ ld a, (hl)
549+ ld (warden_row), a
550+ inc hl
551+ ld a, (hl)
552+ ld (warden_dir), a
553+ inc hl
554+ ld a, (hl)
555+ ld (warden_axis), a
556+ ld a, WARDEN_GATHER
557+ ld (warden_timer), a
542558 ret
559+
560+; warden_table — one row per room: start col, start row, dir (+1/-1), axis.
561+; Each route crosses that room's gold and starts clear of the entry cell.
562+warden_table:
563+ defb WARDEN_COL0, WARDEN_ROW0, -1, AXIS_VERT ; Hall — the gold's column
564+ defb 24, 15, -1, AXIS_VERT ; Gallery — column 24, lower chamber
565+ defb 15, 20, -1, AXIS_HORIZ ; Vault — row 20, between the coins
543566
544567 ; ----------------------------------------------------------------------------
545568 ; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns
The complete program
; Shadowkeep — Unit 12: A Warden for Every Room
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 gives every room its own Warden, instantiated from a table on entry.

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

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
            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:
            ld      hl, floor_tile
            ld      (tile_ptr), hl
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            call    draw_tile
            ret

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

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

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

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

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

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

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

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

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

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

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

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

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

str_lost:
            defb    "THE KEEP SLEEPS", 0

scr_addr_cr:
            ld      a, b
            and     %00011000
            or      %01000000
            ld      h, a
            ld      a, b
            and     %00000111
            rrca
            rrca
            rrca
            or      c
            ld      l, a
            ret

attr_addr_cr:
            ld      a, b
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      de, $5800
            add     hl, de
            ld      a, c
            ld      e, a
            ld      d, 0
            add     hl, de
            ret

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

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

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

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

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

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

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

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

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

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

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

            end     start

The whole run, all three rooms, every coin read past its guard — to THE KEEP STANDS:

Six coins across three guarded rooms: time the Hall's column, thread the Gallery, cross into the Vault and take the row between its coins. Every guard is a line of data; every coin is a patrol read and beaten. The keep, cleared — and losable at every step.

Try this: a fourth patrol

The table has a line per room. Give one room a second Warden by… well, you can’t — one active Warden is instantiated per room here, which keeps the memory tiny. But change a room’s line: send the Vault’s guard up a column instead of along its row, or start the Gallery’s at the other end. Feel how one edited line re-guards a whole room. Level design is table-editing now.

Try this: a coward’s route

With three guards, is the keep still winnable without timing any of them — by waiting at each doorway for the patrol to be at its far end, then dashing? If it is, the coins are too far from the guards. Nudge a G closer to its Warden’s route until the only way to it is through the patrol’s rhythm. That nudge is the difference between a keep you wait out and a keep you play.

Try this: read the table aloud

Look at warden_table and, without running it, describe each room’s danger: where the guard starts, which way it paces, which coins it threatens. Then run it and check. A good data table is one you can read like a level map — and learning to see the level in the data is most of what makes a data-driven game buildable.

When it’s wrong, see why

  • Only the Hall has a Warden. warden_enter isn’t called on room change, or the loop only steps the guard in room 0. Instantiate on every entry; step in every room.
  • A guard walks the wrong way. Its axis byte is crossed — AXIS_VERT where you meant AXIS_HORIZ, or the reverse. The axis says which coordinate the step changes.
  • You’re caught the instant you enter a room. A Warden’s start cell sits on the doorway you arrive through. Move its start clear of the entry, and trust the gather to give you a breath.
  • A guard drifts off its route. A room’s start cell or bounds put it where no wall turns it back, so it wanders into another corridor. Keep each patrol on a stretch walled at both ends.
  • The table reads the wrong room. The index is off — it must be room * 4 for four bytes a line. An off-by-one here hands a room the wrong guard.

Before and after

You began with one dangerous room and two safe ones, and finished with a keep where every room guards its gold. And the second and third guards cost you two lines of data — because you had already learned to store a mover, a light, a coin as data indexed by place, and a Warden is no different. This is the pattern that lets a small program hold a big world: don’t write the world, write a table and a reader. The game core is complete. The keep wants, threatens, and can be lost or won — everywhere. What remains is to make it beautiful.

What you’ve learnt

  • Duplicated behaviour wants to be a table. The moment you copy a routine to make a second one, reach for data instead — a line per instance, read at run time.
  • A byte can choose behaviour. One axis field turns a column-walker into a row-walker with no second routine.
  • Place the danger on the objective. A guard away from the gold is décor; a guard on the gold’s route is the game. The coins were positioned for this from the start.
  • Fair danger is instantiated fair. Start each patrol clear of the door and hold it for a gather, and the player is never caught by something they were given no chance to see.

What’s next

The keep is a complete game — and it is lit like a warehouse, every cell the same flat blue. Everything from here is atmosphere: making the finished game a place you feel. In Unit 13, “Light and Shadow,” we hang a torch in each room and let the dithering you met in Unit 2 become lighting — a pool of light that fades into a dark you’ll want to hide in. The game is built; now we make it beautiful.