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

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.
| 1 | 1 | ; Shadowkeep — Unit 12: A Warden for Every Room | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 15 | 15 | WARDEN_ROW0 equ 11 | |
| 16 | 16 | WARDEN_SPEED equ 10 ; frames between patrol steps | |
| 17 | 17 | 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) | |
| 18 | 20 | WALL_BIT equ 6 | |
| 19 | 21 | | |
| 20 | 22 | START_COL equ 15 | |
| ... | |||
| 487 | 489 | ret nz | |
| 488 | 490 | ld a, WARDEN_SPEED | |
| 489 | 491 | 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: | |
| 490 | 505 | ld a, (warden_col) | |
| 491 | - | ld c, a ; C = column (fixed) | |
| 506 | + | ld c, a ; C = col (fixed) | |
| 492 | 507 | ld a, (warden_dir) | |
| 493 | 508 | ld hl, warden_row | |
| 494 | 509 | add a, (hl) | |
| 495 | 510 | ld b, a ; B = next row | |
| 511 | + | .ws_have: | |
| 496 | 512 | ld a, b ; the thief there? caught — check BEFORE | |
| 497 | 513 | ld hl, thief_row ; wall_at, because his own cell reads as wall | |
| 498 | 514 | cp (hl) | |
| ... | |||
| 514 | 530 | ld (warden_dir), a | |
| 515 | 531 | ret | |
| 516 | 532 | .ws_commit: | |
| 517 | - | push bc ; keep the proposed row — restore heals the OLD cell | |
| 533 | + | push bc ; keep the proposed (row B, col C) | |
| 518 | 534 | call restore_warden | |
| 519 | 535 | pop bc | |
| 536 | + | ld a, c | |
| 537 | + | ld (warden_col), a | |
| 520 | 538 | ld a, b | |
| 521 | 539 | ld (warden_row), a | |
| 522 | 540 | call save_warden ; photograph the new cell's ground | |
| ... | |||
| 989 | 1007 | defb WARDEN_ROW0 | |
| 990 | 1008 | warden_dir: | |
| 991 | 1009 | defb -1 | |
| 1010 | + | warden_axis: | |
| 1011 | + | defb AXIS_VERT | |
| 992 | 1012 | warden_timer: | |
| 993 | 1013 | defb WARDEN_GATHER | |
| 994 | 1014 | caught: |
| 1 | 1 | ; Shadowkeep — Unit 12: A Warden for Every Room | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 44 | 44 | or a | |
| 45 | 45 | jr nz, .won | |
| 46 | 46 | 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 | |
| 51 | 48 | ld a, (caught) | |
| 52 | 49 | or a | |
| 53 | 50 | jr nz, .lost | |
| 54 | - | .skip_warden: | |
| 55 | 51 | call mark_step | |
| 56 | 52 | jr .game_loop | |
| 57 | 53 | .lost: | |
| ... | |||
| 91 | 87 | call draw_room | |
| 92 | 88 | call save_under | |
| 93 | 89 | 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 | |
| 102 | 91 | call save_warden | |
| 103 | 92 | call draw_warden | |
| 104 | 93 | ret | |
| ... | |||
| 340 | 329 | call draw_room | |
| 341 | 330 | call save_under | |
| 342 | 331 | 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 | |
| 346 | 333 | call save_warden | |
| 347 | 334 | call draw_warden | |
| 348 | 335 | ret | |
| ... | |||
| 539 | 526 | ld (warden_row), a | |
| 540 | 527 | call save_warden ; photograph the new cell's ground | |
| 541 | 528 | 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 | |
| 542 | 558 | 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 | |
| 543 | 566 | | |
| 544 | 567 | ; ---------------------------------------------------------------------------- | |
| 545 | 568 | ; 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:
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_enterisn’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_VERTwhere you meantAXIS_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 * 4for 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.