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

A Keep with Character

Compose the whole keep as one place. Let a room hold more than one torch — each floor cell lit by its nearest flame — and use that with per-room falloff to give every chamber its own character from one coherent toolset.

84% of Shadowkeep

We’ve given rooms moods one at a time. This unit pulls back and treats the whole keep as a single composition: three chambers that read as parts of one place, each lit to its own character, all from the same coherent set of tools. It’s the unit that turns “rooms with lighting” into “a keep you believe in”, and it closes the Mood and Light sub-arc.

The capability that makes it possible is a small but real upgrade: a room can have more than one torch.

What you’ll see by the end

The Hall lit by three sconces — one high in the centre wall and one on each side wall — their pools of light overlapping across the upper hall around the statue and banners, the floor falling into shadow below; the hooded thief and the pale Warden in the light, coins on the right.
The Hall, grandly lit: three sconces, their pools overlapping into a broad wash across the upper chamber, the statue and banners standing in it, the thief and Warden moving through it, the floor sinking to shadow at the back. The light is no longer one circle but as many as the room has fires.

The Hall, grandly lit now — three flames, their pools overlapping into a broad wash of light across the upper chamber, the statue and its banners standing in it, the thief and the Warden moving through it, the floor sinking to shadow toward the back. Walk to the Vault and it’s still a single flame in the black; the Gallery, two torches and a medium glow. One keep, one engine, three characters — and they feel like the same keep, lit by the same hand.

Many flames, nearest wins

Until now a room had one torch and find_torch looked for it. Now find_torches gathers every T in the room into a little list:

find_torches:
            ; ... scan the map; for each 'T', record (col, row) ...
            ; ... up to MAX_TORCHES, counting them in torch_count ...

And a floor cell takes its light from whichever flame is closestshade_for_cell measures the distance to each torch and keeps the smallest:

.sf_loop:
            ; distance from this cell to torch[i] ...
            ld      e, a            ; this torch's distance
            ld      a, (min_dist)
            cp      e
            jr      c, .sf_keep     ; already nearer
            ld      a, e
            ld      (min_dist), a   ; new nearest
.sf_keep:
            djnz    .sf_loop
            ; ... then shift by the room's falloff and clamp, as before ...

Two torches make two pools that overlap where they meet; three light a whole hall. The light stopped being one circle and became as many as the room has fires.

Character from a shared toolset

Now each room is composed with the same two dials — how many flames, and how fast they fade — turned to a different setting:

room_falloff:
            defb    2               ; Hall  — 3 sconces, broad: grand and bright
            defb    1               ; Gallery — 2 torches, medium
            defb    0               ; Vault — 1 flame on the altar, tight: a crypt

The Hall is generous and public — three sconces, soft falloff, light everywhere you’d gather. The Vault is private and fearful — one flame, sharp falloff, dark everywhere you’re not meant to linger. They’re opposites, and yet they’re plainly the same building, because they’re drawn with the same stone, the same dither ramp, the same flames. That coherence — variety within one clear hand — is what “character” means for a place. You’re no longer lighting rooms; you’re lighting a keep.

The Vault, lit by a single flame on the central altar: a tight pool of light around the altar, the rest of the room near-black, the coins and Warden faint in the dark, the hooded thief entering from the south doorway below.
The same keep, the opposite character: the Vault's one cold flame and sharp falloff against the Hall's three sconces and broad wash. Same stone, same dither ramp, same fire — one number each sets them apart, and they still read as one building.

Milestone — light a keep, not a room

find_torch becomes find_torches: it gathers every T in a room into a short list. shade_for_cell then measures a floor cell’s distance to each flame and keeps the smallest, so two torches make two overlapping pools and three light a whole hall. Paired with the per-room falloff from Unit 15, two dials — count of flames and falloff — compose the grand Hall, the working Gallery and the crypt-dark Vault from one shared toolset.

Step 1: many torches per room, each cell lit by its nearest flame
+79-42
11 ; Shadowkeep — Unit 16: A Keep with Character
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 15's end: each room one torch; now a room may hold many.
3+; step-01 lets a room hold many torches — each floor cell lit by its nearest flame.
44
55 org 32768
66
...
1111 STATUE_ATTR equ %01001111 ; BRIGHT white on blue — pale stone (solid)
1212 BANNER_ATTR equ %01001011 ; BRIGHT magenta on blue — a hanging (solid)
1313 RUBBLE_ATTR equ %00001000 ; dim, like floor — walkable broken stone
14-NO_TORCH equ $FF
1514 MAX_SHADE equ 4
15+MAX_TORCHES equ 4
1616 THIEF equ %01001010
1717 GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold
1818 TOTAL_GOLD equ 6
...
154154 ; find_torch — scan the current room's map for 'T', remember where it is (or
155155 ; NO_TORCH if the room is unlit).
156156 ; ----------------------------------------------------------------------------
157-find_torch:
158- ld a, NO_TORCH
159- ld (torch_col), a
160- ld (torch_row), a
157+; find_torches — collect every 'T' in the current room (up to MAX_TORCHES) into
158+; torch_list as (col, row) pairs; torch_count says how many.
159+find_torches:
160+ xor a
161+ ld (torch_count), a
161162 call room_entry_addr
162163 ld a, (hl)
163164 inc hl
164165 ld h, (hl)
165166 ld l, a
166167 ld b, 0
167-.ft_row:
168+.fr_row:
168169 ld c, 0
169-.ft_col:
170+.fr_col:
170171 ld a, (hl)
171172 cp 'T'
172- jr nz, .ft_skip
173- ld a, c
174- ld (torch_col), a
175- ld a, b
176- ld (torch_row), a
177-.ft_skip:
173+ jr nz, .fr_skip
174+ ld a, (torch_count)
175+ cp MAX_TORCHES
176+ jr nc, .fr_skip
177+ push hl
178+ add a, a ; count * 2
179+ ld e, a
180+ ld d, 0
181+ ld hl, torch_list
182+ add hl, de
183+ ld (hl), c ; column
184+ inc hl
185+ ld (hl), b ; row
186+ pop hl
187+ ld a, (torch_count)
188+ inc a
189+ ld (torch_count), a
190+.fr_skip:
178191 inc hl
179192 inc c
180193 ld a, c
181194 cp 32
182- jr nz, .ft_col
195+ jr nz, .fr_col
183196 inc b
184197 ld a, b
185198 cp 24
186- jr nz, .ft_row
199+ jr nz, .fr_row
187200 ret
188201
189202 ; ----------------------------------------------------------------------------
...
193206 ; ----------------------------------------------------------------------------
194207 shade_for_cell:
195208 push bc
196- ld a, (torch_col)
197- cp NO_TORCH
198- jr z, .sf_dark
199209 ld a, b
200- ld hl, torch_row
201- sub (hl)
202- jr nc, .sf_rpos
203- neg
204-.sf_rpos:
205- ld d, a ; |row - torch_row|
210+ ld (cell_row), a
206211 ld a, c
207- ld hl, torch_col
212+ ld (cell_col), a
213+ ld a, (torch_count)
214+ or a
215+ jr z, .sf_dark
216+ ld a, 255
217+ ld (min_dist), a
218+ ld hl, torch_list
219+ ld a, (torch_count)
220+ ld b, a ; loop count
221+.sf_loop:
222+ ld a, (cell_col)
223+ sub (hl) ; - torch col
224+ jr nc, .sf_dc
225+ neg
226+.sf_dc:
227+ ld d, a ; |dc|
228+ inc hl ; -> torch row
229+ ld a, (cell_row)
208230 sub (hl)
209- jr nc, .sf_cpos
231+ jr nc, .sf_dr
210232 neg
211-.sf_cpos:
212- cp d ; A = |dc|; max(|dc|, |dr|)
213- jr nc, .sf_max
233+.sf_dr:
234+ inc hl ; -> next torch pair
235+ cp d ; A = |dr|; take the larger
236+ jr nc, .sf_mx
214237 ld a, d
215-.sf_max:
216- ld e, a ; E = Chebyshev distance
217- ld a, (current_room) ; shift it by THIS room's falloff
238+.sf_mx:
239+ ld e, a ; this torch's distance
240+ ld a, (min_dist)
241+ cp e
242+ jr c, .sf_keep ; min already smaller
243+ ld a, e
244+ ld (min_dist), a
245+.sf_keep:
246+ djnz .sf_loop
247+
248+ ld a, (current_room)
218249 ld c, a
219250 ld b, 0
220251 ld hl, room_falloff
221252 add hl, bc
222- ld b, (hl) ; B = this room's falloff
223- ld a, e
253+ ld b, (hl)
254+ ld a, (min_dist)
224255 inc b
225256 .sf_shift:
226257 dec b
...
240271 ret
241272
242273 draw_room:
243- call find_torch
274+ call find_torches
244275 call room_entry_addr
245276 ld a, (hl)
246277 inc hl
...
11221153 defb "B..............S...............B"
11231154 defb "#.............ooo.........G....#"
11241155 defb "#..............................#"
1125- defb "#........##..........##........#"
1156+ defb "T........##..........##........T"
11261157 defb "#........##..........##........#"
11271158 defb "#..............................#"
11281159 defb "#..............................."
...
11431174 ; back to the Hall; north door (column 15) up to the Vault.
11441175 room1_template:
11451176 defb "###############.################"
1146- defb "#..............................#"
11471177 defb "#..............................#"
11481178 defb "#..............................#"
11491179 defb "#..............................#"
1180+ defb "#..............................T"
11501181 defb "#..............................#"
11511182 defb "#..............................#"
11521183 defb "#..............................#"
...
12711302 defb WARDEN_GATHER
12721303 caught:
12731304 defb 0
1274-torch_col:
1275- defb NO_TORCH
1276-torch_row:
1277- defb NO_TORCH
1305+torch_count:
1306+ defb 0
1307+cell_row:
1308+ defb 0
1309+cell_col:
1310+ defb 0
1311+min_dist:
1312+ defb 0
1313+torch_list:
1314+ defs MAX_TORCHES * 2
12781315
12791316 room0_state:
12801317 defs 768
The complete program
; Shadowkeep — Unit 16: A Keep with Character
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 lets a room hold many torches — each floor cell lit by its nearest flame.

            org     32768

WALL_ATTR   equ     %01001000
FLOOR_ATTR  equ     %00001000
MARK_ATTR   equ     %00001111
TORCH_ATTR  equ     %01001110         ; BRIGHT, blue paper, yellow ink — a lit sconce
STATUE_ATTR equ     %01001111         ; BRIGHT white on blue — pale stone (solid)
BANNER_ATTR equ     %01001011         ; BRIGHT magenta on blue — a hanging (solid)
RUBBLE_ATTR equ     %00001000         ; dim, like floor — walkable broken stone
MAX_SHADE   equ     4
MAX_TORCHES equ     4
THIEF       equ     %01001010
GOLD_ATTR   equ     %00000110         ; yellow ink, no BRIGHT -> walkable gold
TOTAL_GOLD  equ     6
WARDEN      equ     %01000101         ; BRIGHT cyan on black — the sentinel
WARDEN_COL0 equ     26                ; the Hall's gold column
WARDEN_ROW0 equ     11
WARDEN_SPEED equ    10                ; frames between patrol steps
WARDEN_GATHER equ   50                ; a beat before it begins to move
AXIS_HORIZ  equ     0                 ; the Warden varies its column (walks a row)
AXIS_VERT   equ     1                 ; the Warden varies its row (walks a column)
WALL_BIT    equ     6

START_COL   equ     15
START_ROW   equ     11
NO_EXIT     equ     $FF

KEYS_OP     equ     $DFFE
KEYS_Q      equ     $FBFE
KEYS_A      equ     $FDFE
KEYS_SPACE  equ     $7FFE

start:
            ld      a, 0
            out     ($FE), a
            im      1

; --- TITLE -> PLAY -> WIN -> TITLE ------------------------------------------
main_title:
            call    show_title       ; returns (interrupts off) when SPACE pressed
            call    new_game         ; reset the keep, place the gold, draw
            ei
.game_loop:
            halt
            ld      a, (won)
            or      a
            jr      nz, .won
            call    player_step
            call    warden_step      ; every room has its Warden now
            ld      a, (caught)
            or      a
            jr      nz, .lost
            call    mark_step
            jr      .game_loop
.lost:
            call    show_lose        ; "THE KEEP SLEEPS", the sting, wait for SPACE
            jr      main_title
.won:
            call    show_win         ; "THE KEEP STANDS", flourish, wait for SPACE
            jr      main_title       ; round again

; new_game — copy the room templates back into working state (so collected gold
; returns), reset the thief, the gold counter and the win flag, draw the keep.
new_game:
            ld      hl, room0_template
            ld      de, room0_state
            ld      bc, 768
            ldir
            ld      hl, room1_template
            ld      de, room1_state
            ld      bc, 768
            ldir
            ld      hl, room2_template
            ld      de, room2_state
            ld      bc, 768
            ldir

            xor     a
            ld      (current_room), a
            ld      (won), a
            ld      (caught), a
            ld      a, TOTAL_GOLD
            ld      (gold_remaining), a
            ld      a, START_COL
            ld      (thief_col), a
            ld      a, START_ROW
            ld      (thief_row), a

            call    draw_room
            call    save_under
            call    draw_thief
            call    warden_enter     ; this room's Warden, from the table
            call    save_warden
            call    draw_warden
            ret

mark_step:
            ld      bc, KEYS_SPACE
            in      a, (c)
            bit     0, a
            ret     nz
            call    cell_state_addr
            ld      (hl), '+'
            ld      hl, mark_tile
            ld      de, under_thief
            ld      bc, 8
            ldir
            ld      a, MARK_ATTR
            ld      (under_thief + 8), a
            ret

cell_state_addr:
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            push    hl
            ld      a, (thief_row)
            ld      l, a
            ld      h, 0
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            add     hl, hl
            ld      a, (thief_col)
            ld      e, a
            ld      d, 0
            add     hl, de
            pop     de
            add     hl, de
            ret

room_entry_addr:
            ld      a, (current_room)
            ld      l, a
            ld      h, 0
            ld      d, h
            ld      e, l
            add     hl, hl
            add     hl, de
            add     hl, hl
            ld      de, rooms
            add     hl, de
            ret


; ----------------------------------------------------------------------------
; find_torch — scan the current room's map for 'T', remember where it is (or
; NO_TORCH if the room is unlit).
; ----------------------------------------------------------------------------
; find_torches — collect every 'T' in the current room (up to MAX_TORCHES) into
; torch_list as (col, row) pairs; torch_count says how many.
find_torches:
            xor     a
            ld      (torch_count), a
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      b, 0
.fr_row:
            ld      c, 0
.fr_col:
            ld      a, (hl)
            cp      'T'
            jr      nz, .fr_skip
            ld      a, (torch_count)
            cp      MAX_TORCHES
            jr      nc, .fr_skip
            push    hl
            add     a, a            ; count * 2
            ld      e, a
            ld      d, 0
            ld      hl, torch_list
            add     hl, de
            ld      (hl), c         ; column
            inc     hl
            ld      (hl), b         ; row
            pop     hl
            ld      a, (torch_count)
            inc     a
            ld      (torch_count), a
.fr_skip:
            inc     hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .fr_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .fr_row
            ret

; ----------------------------------------------------------------------------
; shade_for_cell — row in B, column in C. Returns a shade 0..4 in A: the
; Chebyshev distance to the torch, halved and clamped. Near the flame = 0
; (lightest); far away = 4 (darkest). No torch = darkest everywhere.
; ----------------------------------------------------------------------------
shade_for_cell:
            push    bc
            ld      a, b
            ld      (cell_row), a
            ld      a, c
            ld      (cell_col), a
            ld      a, (torch_count)
            or      a
            jr      z, .sf_dark
            ld      a, 255
            ld      (min_dist), a
            ld      hl, torch_list
            ld      a, (torch_count)
            ld      b, a            ; loop count
.sf_loop:
            ld      a, (cell_col)
            sub     (hl)            ; - torch col
            jr      nc, .sf_dc
            neg
.sf_dc:
            ld      d, a            ; |dc|
            inc     hl              ; -> torch row
            ld      a, (cell_row)
            sub     (hl)
            jr      nc, .sf_dr
            neg
.sf_dr:
            inc     hl              ; -> next torch pair
            cp      d               ; A = |dr|; take the larger
            jr      nc, .sf_mx
            ld      a, d
.sf_mx:
            ld      e, a            ; this torch's distance
            ld      a, (min_dist)
            cp      e
            jr      c, .sf_keep     ; min already smaller
            ld      a, e
            ld      (min_dist), a
.sf_keep:
            djnz    .sf_loop

            ld      a, (current_room)
            ld      c, a
            ld      b, 0
            ld      hl, room_falloff
            add     hl, bc
            ld      b, (hl)
            ld      a, (min_dist)
            inc     b
.sf_shift:
            dec     b
            jr      z, .sf_clamp
            srl     a
            jr      .sf_shift
.sf_clamp:
            cp      MAX_SHADE + 1
            jr      c, .sf_done
            ld      a, MAX_SHADE
.sf_done:
            pop     bc
            ret
.sf_dark:
            ld      a, MAX_SHADE
            pop     bc
            ret

draw_room:
            call    find_torches
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      (map_ptr), hl
            ld      b, 0
.room_row:
            ld      c, 0
.room_col:
            ld      hl, (map_ptr)
            ld      a, (hl)
            cp      '.'
            jr      nz, .not_floor
            call    shade_for_cell
            add     a, a
            ld      e, a
            ld      d, 0
            ld      hl, shade_tiles
            add     hl, de
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            call    draw_tile
            jr      .cell_done
.not_floor:
            call    lookup_tile
            call    draw_tile
.cell_done:
            ld      hl, (map_ptr)
            inc     hl
            ld      (map_ptr), hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .room_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .room_row
            ret

lookup_tile:
            ld      hl, palette
.scan:
            cp      (hl)
            jr      z, .found
            inc     hl
            inc     hl
            inc     hl
            inc     hl
            jr      .scan
.found:
            inc     hl
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            inc     hl
            ld      a, (hl)
            ld      (tile_attr), a
            ret

draw_tile:
            push    bc
            call    attr_addr_cr
            ld      a, (tile_attr)
            ld      (hl), a
            call    scr_addr_cr
            ld      de, (tile_ptr)
            ld      b, 8
.tile_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .tile_row
            pop     bc
            ret

player_step:
            ld      a, (thief_col)
            ld      (tcol), a
            ld      a, (thief_row)
            ld      (trow), a

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a
            jr      z, .left
            bit     0, a
            jr      z, .right
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a
            jr      z, .up
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a
            jr      z, .down
            ret
.left:
            ld      hl, tcol
            dec     (hl)
            jr      .move
.right:
            ld      hl, tcol
            inc     (hl)
            jr      .move
.up:
            ld      hl, trow
            dec     (hl)
            jr      .move
.down:
            ld      hl, trow
            inc     (hl)
.move:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz

            call    restore_under
            ld      a, (tcol)
            ld      (thief_col), a
            ld      a, (trow)
            ld      (thief_row), a
            call    collect_gold     ; the new cell might be gold — lift it first
            call    save_under
            call    draw_thief
            call    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:
            call    shade_for_cell
            add     a, a
            ld      e, a
            ld      d, 0
            ld      hl, shade_tiles
            add     hl, de
            ld      e, (hl)
            inc     hl
            ld      d, (hl)
            ld      (tile_ptr), de
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            call    draw_tile
            ret

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

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

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

; One byte of mood per room: how slowly its light fades.
room_falloff:
            defb    2                   ; Hall — broad, soft pool
            defb    1                   ; Gallery — medium
            defb    0                   ; Vault — tight pool, deep dark

; The shade ramp: lightest (lit) to darkest (deep shadow), all blue/black dither.
shade_tiles:
            defw    shade0_tile
            defw    shade1_tile
            defw    shade2_tile
            defw    shade3_tile
            defw    shade4_tile

shade0_tile:                        ; lit — nearly all blue
            defb    %00000000
            defb    %00100010
            defb    %00000000
            defb    %00000000
            defb    %00000000
            defb    %10001000
            defb    %00000000
            defb    %00000000
shade1_tile:
            defb    %00100010
            defb    %00000000
            defb    %10001000
            defb    %00000000
            defb    %00100010
            defb    %00000000
            defb    %10001000
            defb    %00000000
shade2_tile:                        ; the half-and-half slate from Unit 2
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
shade3_tile:
            defb    %10101010
            defb    %11111111
            defb    %01010101
            defb    %11111111
            defb    %10101010
            defb    %11111111
            defb    %01010101
            defb    %11111111
shade4_tile:                        ; deep shadow — nearly all black
            defb    %11111111
            defb    %11101110
            defb    %11111111
            defb    %10111011
            defb    %11111111
            defb    %11101110
            defb    %11111111
            defb    %10111011

torch_tile:                         ; a flame in its sconce
            defb    %00010000
            defb    %00111000
            defb    %00111000
            defb    %01111100
            defb    %01111100
            defb    %01111100
            defb    %00111000
            defb    %00010000

statue_tile:
            defb    %00111100
            defb    %01111110
            defb    %00111100
            defb    %00011000
            defb    %00011000
            defb    %00111100
            defb    %01111110
            defb    %01111110

banner_tile:
            defb    %01111110
            defb    %01111110
            defb    %01011010
            defb    %01011010
            defb    %01111110
            defb    %01111110
            defb    %00111100
            defb    %00011000

rubble_tile:
            defb    %00000000
            defb    %01100000
            defb    %01100000
            defb    %00000110
            defb    %00000110
            defb    %00011000
            defb    %00011000
            defb    %00000000

; ----------------------------------------------------------------------------
; show_title — black screen, the keep's name and a prompt; wait for SPACE (and
; release). Silent for now — the composed theme arrives in Unit 18. Words use the
; ROM's own 8x8 font at $3D00.
; ----------------------------------------------------------------------------
show_title:
            di
            call    clear_screen
            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
            defb    'T'
            defw    torch_tile
            defb    TORCH_ATTR
            defb    'S'
            defw    statue_tile
            defb    STATUE_ATTR
            defb    'B'
            defw    banner_tile
            defb    BANNER_ATTR
            defb    'o'
            defw    rubble_tile
            defb    RUBBLE_ATTR

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

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

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

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

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

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

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

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

current_room:
            defb    0
thief_col:
            defb    START_COL
thief_row:
            defb    START_ROW
tcol:
            defb    0
trow:
            defb    0
map_ptr:
            defw    0
tile_ptr:
            defw    0
tile_attr:
            defb    0
under_thief:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
gold_remaining:
            defb    TOTAL_GOLD
won:
            defb    0
under_warden:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0
warden_col:
            defb    WARDEN_COL0
warden_row:
            defb    WARDEN_ROW0
warden_dir:
            defb    -1
warden_axis:
            defb    AXIS_VERT
warden_timer:
            defb    WARDEN_GATHER
caught:
            defb    0
torch_count:
            defb    0
cell_row:
            defb    0
cell_col:
            defb    0
min_dist:
            defb    0
torch_list:
            defs    MAX_TORCHES * 2

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

            end     start

Walk the whole keep, end to end: the Hall opens out under its three flames, the Gallery settles to a working glow, the Vault closes in to its one cold light. Three rooms, one place — the two stills above are its grandest and its darkest, drawn by the same hand.

Try this: add a flame, take one away

Drop another T into a room’s map and watch a new pool bloom and overlap the others; delete one and watch a corner fall dark. You’re lighting by placing fires — the most physical way there is. Mind MAX_TORCHES; raise it if you want a room ablaze. (Keep them clear of a Warden’s route, or you light the danger and not the gold.)

Try this: a colonnade of light

Line a long room’s wall with a row of evenly-spaced sconces, falloff 1 or 2. The overlapping pools merge into a continuous band of light down the room — a lit gallery, a cathedral nave. Repetition of one element is its own kind of grandeur.

Try this: ruin the character

Give the Vault three torches and a falloff of 2. It floods with light and instantly stops being a crypt — it’s just another bright room. A useful failure: it shows that the Vault’s character was never the altar or the stone, it was the restraint. Put it back, and notice how much the darkness was doing.

When it’s wrong, see why

  • Only one torch lights a room. find_torches is stopping at the first T, or shade_for_cell isn’t looping over torch_count. Every T should be collected and every one measured.
  • The machine slows to a crawl entering a room. Many torches × 768 cells is real work; it’s a one-time cost on entry, but keep MAX_TORCHES modest (a handful) so the redraw stays a flick, not a freeze.
  • A room is all dark with torches present. torch_count came back zero — the map’s Ts aren’t being found, or min_dist isn’t being reset to a large value before the loop.
  • Two pools don’t overlap / one wins everywhere. The nearest test is backwards. Keep the smaller distance: update min_dist only when the new distance is less.
  • Garbage after adding torches. torch_list overflowed — you placed more than MAX_TORCHES and the count guard is missing. The cp MAX_TORCHES / jr nc check protects the list.

Before and after

You started with rooms that each held a single mood and finished with a whole keep composed as one place — the Hall grand under three sconces, the Vault a crypt with one cold flame, the Gallery somewhere between, and all three plainly the same building. The upgrade was small: a room gathers every torch it holds, and each cell takes its light from the nearest. With that and the falloff byte, two dials compose grand, working and fearful rooms from one shared toolset. You stopped lighting rooms and started lighting a keep — the difference between a set of screens and somewhere you believe in.

What you’ve learnt

  • Light is as many pools as there are flames. Collect every torch; light each cell from the nearest. Pools overlap into the shape of the room’s fires.
  • Character is variety within one hand. Two dials — count of flames and falloff — compose grand, working, and fearful rooms that still read as one building.
  • Restraint is a feature. The Vault’s darkness, not its altar, is its character. Over-light it and it vanishes.
  • A keep is a composition. You stopped lighting rooms and started lighting a place — the difference between a set of screens and somewhere to be.

What’s next

The keep looks like somewhere now — lit, furnished, composed — and it plays. But it’s near-silent: the coins chime, the win and the loss sound, and otherwise the stone is mute. A place you believe in has ambient sound — footsteps on stone, the creak of a door. The next sub-arc, The Keep’s Voice, gives it that. We begin in Unit 17, “Footsteps and Doors,” building on the beeper you rang the dusk bells with — turned now to stone and hinges — so the keep is heard as well as seen.