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

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.
| 1 | 1 | ; Shadowkeep — Unit 10: Something in the Dark | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 10 | 10 | THIEF equ %01001010 | |
| 11 | 11 | GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold | |
| 12 | 12 | 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 | |
| 13 | 16 | WALL_BIT equ 6 | |
| 14 | 17 | | |
| 15 | 18 | START_COL equ 15 | |
| ... | |||
| 72 | 75 | call draw_room | |
| 73 | 76 | call save_under | |
| 74 | 77 | 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 | |
| 75 | 84 | ret | |
| 76 | 85 | | |
| 77 | 86 | mark_step: | |
| ... | |||
| 311 | 320 | call draw_room | |
| 312 | 321 | call save_under | |
| 313 | 322 | 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 | |
| 314 | 328 | ret | |
| 315 | 329 | | |
| 316 | 330 | pos_bc: | |
| ... | |||
| 368 | 382 | inc de | |
| 369 | 383 | inc h | |
| 370 | 384 | 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 | |
| 371 | 430 | 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 | |
| 372 | 443 | | |
| 373 | 444 | ; ---------------------------------------------------------------------------- | |
| 374 | 445 | ; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns | |
| ... | |||
| 778 | 849 | defb TOTAL_GOLD | |
| 779 | 850 | won: | |
| 780 | 851 | 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 | |
| 781 | 858 | | |
| 782 | 859 | room0_state: | |
| 783 | 860 | defs 768 |
| 1 | 1 | ; Shadowkeep — Unit 10: Something in the Dark | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 13 | 13 | WARDEN equ %01000101 ; BRIGHT cyan on black — the sentinel | |
| 14 | 14 | WARDEN_COL0 equ 26 ; the Hall's gold column | |
| 15 | 15 | 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 | |
| 16 | 18 | WALL_BIT equ 6 | |
| 17 | 19 | | |
| 18 | 20 | START_COL equ 15 | |
| ... | |||
| 40 | 42 | or a | |
| 41 | 43 | jr nz, .won | |
| 42 | 44 | call player_step | |
| 45 | + | ld a, (current_room) | |
| 46 | + | or a | |
| 47 | + | call z, warden_step ; the Warden haunts the Hall (room 0) | |
| 43 | 48 | call mark_step | |
| 44 | 49 | jr .game_loop | |
| 45 | 50 | .won: | |
| ... | |||
| 79 | 84 | ld (warden_col), a | |
| 80 | 85 | ld a, WARDEN_ROW0 | |
| 81 | 86 | ld (warden_row), a | |
| 87 | + | ld a, -1 | |
| 88 | + | ld (warden_dir), a | |
| 89 | + | ld a, WARDEN_GATHER | |
| 90 | + | ld (warden_timer), a | |
| 82 | 91 | call save_warden | |
| 83 | 92 | call draw_warden | |
| 84 | 93 | ret | |
| ... | |||
| 440 | 449 | defb %01111110 | |
| 441 | 450 | defb %01111110 | |
| 442 | 451 | 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 | |
| 443 | 504 | | |
| 444 | 505 | ; ---------------------------------------------------------------------------- | |
| 445 | 506 | ; The gold — the keep's goal. Step onto a coin and it lifts: the map cell turns | |
| ... | |||
| 855 | 916 | defb WARDEN_COL0 | |
| 856 | 917 | warden_row: | |
| 857 | 918 | defb WARDEN_ROW0 | |
| 919 | + | warden_dir: | |
| 920 | + | defb -1 | |
| 921 | + | warden_timer: | |
| 922 | + | defb WARDEN_GATHER | |
| 858 | 923 | | |
| 859 | 924 | room0_state: | |
| 860 | 925 | 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:
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_wardenisn’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_wardenruns 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_stepisn’t consultingwall_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_atthe 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.