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

Light and Shadow

The keep is a finished game — but lit like a warehouse, every cell the same cold blue. Hang a torch and let the dithering you met in Unit 2 become lighting: pick each floor cell's shade by its distance from the flame, so a pool of light fades into the dark.

68% of Shadowkeep

The keep is a game now. You want the gold, the Wardens threaten it, and every run ends in THE KEEP STANDS or THE KEEP SLEEPS. Everything that makes it play is in place. What is missing is everything that makes it a place — because it is lit like an office, every cell the same even blue, no dark corners, no sense of nightfall. From here on we stop adding mechanics and start adding atmosphere, and atmosphere starts with light. This unit hangs a torch in each room and lets it throw a pool of light into the gloom the Wardens patrol.

You have made light before — but not this way, and the difference is the lesson. In The Long Night, every lamp you lit warmed a pool around it: you made light by recolouring attributes, cobble turning to glow. That is one of the two ways this machine makes light, and you own it cold. Here is the other, and it could hardly be more different: not colour, but density. Back in Unit 2 you learned that dither density is shade — sparse pixels read light, dense read dark. Lighting is choosing that density by distance from a flame: close to the torch, sparse and bright; far, dense and near-black. The keep has no glow attributes to spare — its walls are already cold stone, its cells already spent on shape — so it lights the only way left to it: in the bitmap. Two independent routes to the same feeling, and a real Spectrum artist keeps both on the bench.

What you’ll see by the end

The Hall lit by a single torch set in the top wall: a bright pool of light fans out below the flame and fades through deepening shades of slate to near-black in the corners. The hooded thief stands in the shadow, the pale Warden pacing its column, two coins glinting on the right.
One torch in the top wall, and the floor lights itself: a pool of near-blue under the flame, thickening through slate to near-black in the corners. The same game as last unit — the same coins, the same Warden — now with somewhere dark to be.

A torch burns in the top wall, and the floor beneath it is lit — nearly all blue, the stone catching the flame. Step away and the dither thickens: half-and-half slate, then darker, then the far corners almost black. The thief, the Warden, the coins all move through the gradient; nothing about the game changed, but the keep became a place with a dark in it. Same rooms as last unit; an entirely different place to be in.

A torch is a glyph

A torch is just another tile in the palette — T, a yellow flame in its sconce — placed in the room map like any wall, and clear of the coins and the Wardens’ routes:

            defb    "###############T################"   ; the Hall's top wall

It’s drawn bright, so wall_at already treats it as solid stone — a fixture on the wall, not something you walk through. When a room is drawn, we first find its torch:

find_torch:
            ; ... scan the room map for 'T', record (torch_col, torch_row) ...
            ; ... or NO_TORCH if the room has none ...

Distance becomes shade

For each floor cell, how lit it is depends on how far it sits from that flame. We measure the distance — the larger of the row gap and the column gap, which gives a neat square-ish pool — halve it, and clamp it to give a shade from 0 (right by the flame) to 4 (deep dark):

shade_for_cell:                     ; row in B, column in C -> shade 0..4 in A
            ; ... |row - torch_row| and |col - torch_col| ...
            ; ... A = the larger of the two (the distance) ...
            srl     a               ; distance / 2 — a gentle falloff
            cp      MAX_SHADE + 1
            jr      c, .sf_done
            ld      a, MAX_SHADE     ; clamp to the darkest
.sf_done:
            ret

And the five shades are five floor tiles, the same blue and black at thickening densities — shade0 nearly all blue, shade2 the half-and-half slate from Unit 2, shade4 nearly all black.

Lighting as the room draws

draw_room now does the choosing as it paints. A floor cell is shaded by distance; a wall, torch, coin or Warden is drawn as before. And the same choice powers one more thing: when a coin is lifted, the cell it leaves is repainted with its lit shade, not flat floor — so the treasure vanishes into the lighting it sat in, seamlessly:

.room_col:
            ld      a, (hl)          ; the glyph here
            cp      '.'
            jr      nz, .not_floor
            call    shade_for_cell   ; floor: pick a shade by distance
            ; ... point tile_ptr at shade_tiles[shade] ...
            call    draw_tile

The light is baked — worked out once, when the room is drawn. The torch doesn’t move, so the pool doesn’t either. And it costs nothing as the thief walks: his save-and-restore already preserves whatever’s beneath him, lit shade and all — and so does the Warden’s. Both figures move through the gradient and leave it untouched.

Milestone — light the keep

find_torch scans each room’s map for a T; shade_for_cell turns a floor cell’s distance from that flame into a shade 0–4; and draw_room now picks a floor tile from a five-step density ramp as it paints, walls and torches flat. The collect repaint is lit too, so lifted coins vanish into the gradient. The light is baked once — and the thief’s and Warden’s save/restore carry each shade for free.

Step 1: a torch glyph, distance-to-shade, and a five-step floor ramp
+180-6
11 ; Shadowkeep — Unit 13: Light and Shadow
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 12's end: the game entire, but flatly lit — no atmosphere yet.
3+; step-01 hangs a torch in each room and shades the floor by distance from the flame.
44
55 org 32768
66
77 WALL_ATTR equ %01001000
88 FLOOR_ATTR equ %00001000
99 MARK_ATTR equ %00001111
10+TORCH_ATTR equ %01001110 ; BRIGHT, blue paper, yellow ink — a lit sconce
11+NO_TORCH equ $FF
12+MAX_SHADE equ 4
1013 THIEF equ %01001010
1114 GOLD_ATTR equ %00000110 ; yellow ink, no BRIGHT -> walkable gold
1215 TOTAL_GOLD equ 6
...
141144 add hl, hl
142145 ld de, rooms
143146 add hl, de
147+ ret
148+
149+
150+; ----------------------------------------------------------------------------
151+; find_torch — scan the current room's map for 'T', remember where it is (or
152+; NO_TORCH if the room is unlit).
153+; ----------------------------------------------------------------------------
154+find_torch:
155+ ld a, NO_TORCH
156+ ld (torch_col), a
157+ ld (torch_row), a
158+ call room_entry_addr
159+ ld a, (hl)
160+ inc hl
161+ ld h, (hl)
162+ ld l, a
163+ ld b, 0
164+.ft_row:
165+ ld c, 0
166+.ft_col:
167+ ld a, (hl)
168+ cp 'T'
169+ jr nz, .ft_skip
170+ ld a, c
171+ ld (torch_col), a
172+ ld a, b
173+ ld (torch_row), a
174+.ft_skip:
175+ inc hl
176+ inc c
177+ ld a, c
178+ cp 32
179+ jr nz, .ft_col
180+ inc b
181+ ld a, b
182+ cp 24
183+ jr nz, .ft_row
184+ ret
185+
186+; ----------------------------------------------------------------------------
187+; shade_for_cell — row in B, column in C. Returns a shade 0..4 in A: the
188+; Chebyshev distance to the torch, halved and clamped. Near the flame = 0
189+; (lightest); far away = 4 (darkest). No torch = darkest everywhere.
190+; ----------------------------------------------------------------------------
191+shade_for_cell:
192+ ld a, (torch_col)
193+ cp NO_TORCH
194+ jr z, .sf_dark
195+ ld a, b
196+ ld hl, torch_row
197+ sub (hl)
198+ jr nc, .sf_rpos
199+ neg
200+.sf_rpos:
201+ ld d, a ; |row - torch_row|
202+ ld a, c
203+ ld hl, torch_col
204+ sub (hl)
205+ jr nc, .sf_cpos
206+ neg
207+.sf_cpos:
208+ cp d ; A = |dc|; max(|dc|, |dr|)
209+ jr nc, .sf_max
210+ ld a, d
211+.sf_max:
212+ srl a ; distance / 2
213+ cp MAX_SHADE + 1
214+ jr c, .sf_done
215+ ld a, MAX_SHADE
216+.sf_done:
217+ ret
218+.sf_dark:
219+ ld a, MAX_SHADE
144220 ret
145221
146222 draw_room:
223+ call find_torch
147224 call room_entry_addr
148225 ld a, (hl)
149226 inc hl
...
156233 .room_col:
157234 ld hl, (map_ptr)
158235 ld a, (hl)
236+ cp '.'
237+ jr nz, .not_floor
238+ call shade_for_cell
239+ add a, a
240+ ld e, a
241+ ld d, 0
242+ ld hl, shade_tiles
243+ add hl, de
244+ ld e, (hl)
245+ inc hl
246+ ld d, (hl)
247+ ld (tile_ptr), de
248+ ld a, FLOOR_ATTR
249+ ld (tile_attr), a
250+ call draw_tile
251+ jr .cell_done
252+.not_floor:
159253 call lookup_tile
160254 call draw_tile
255+.cell_done:
161256 ld hl, (map_ptr)
162257 inc hl
163258 ld (map_ptr), hl
...
588683 ; draw_floor_cell — paint the floor tile at (row B, col C). Lighting (Unit 13)
589684 ; will replace this with a shade chosen by distance from the nearest torch.
590685 draw_floor_cell:
591- ld hl, floor_tile
592- ld (tile_ptr), hl
686+ call shade_for_cell
687+ add a, a
688+ ld e, a
689+ ld d, 0
690+ ld hl, shade_tiles
691+ add hl, de
692+ ld e, (hl)
693+ inc hl
694+ ld d, (hl)
695+ ld (tile_ptr), de
593696 ld a, FLOOR_ATTR
594697 ld (tile_attr), a
595698 call draw_tile
...
634737 defb %01111110
635738 defb %01111110
636739 defb %00111100
740+ defb %00000000
741+
742+; The shade ramp: lightest (lit) to darkest (deep shadow), all blue/black dither.
743+shade_tiles:
744+ defw shade0_tile
745+ defw shade1_tile
746+ defw shade2_tile
747+ defw shade3_tile
748+ defw shade4_tile
749+
750+shade0_tile: ; lit — nearly all blue
751+ defb %00000000
752+ defb %00100010
753+ defb %00000000
754+ defb %00000000
755+ defb %00000000
756+ defb %10001000
757+ defb %00000000
758+ defb %00000000
759+shade1_tile:
760+ defb %00100010
761+ defb %00000000
762+ defb %10001000
763+ defb %00000000
764+ defb %00100010
765+ defb %00000000
766+ defb %10001000
637767 defb %00000000
768+shade2_tile: ; the half-and-half slate from Unit 2
769+ defb %10101010
770+ defb %01010101
771+ defb %10101010
772+ defb %01010101
773+ defb %10101010
774+ defb %01010101
775+ defb %10101010
776+ defb %01010101
777+shade3_tile:
778+ defb %10101010
779+ defb %11111111
780+ defb %01010101
781+ defb %11111111
782+ defb %10101010
783+ defb %11111111
784+ defb %01010101
785+ defb %11111111
786+shade4_tile: ; deep shadow — nearly all black
787+ defb %11111111
788+ defb %11101110
789+ defb %11111111
790+ defb %10111011
791+ defb %11111111
792+ defb %11101110
793+ defb %11111111
794+ defb %10111011
795+
796+torch_tile: ; a flame in its sconce
797+ defb %00010000
798+ defb %00111000
799+ defb %00111000
800+ defb %01111100
801+ defb %01111100
802+ defb %01111100
803+ defb %00111000
804+ defb %00010000
638805
639806 ; ----------------------------------------------------------------------------
640807 ; show_title — black screen, the keep's name and a prompt; wait for SPACE (and
...
8641031 defb 'G'
8651032 defw gold_tile
8661033 defb GOLD_ATTR
1034+ defb 'T'
1035+ defw torch_tile
1036+ defb TORCH_ATTR
8671037
8681038 ; ----------------------------------------------------------------------------
8691039 ; The keep: three rooms. Hall -east-> Gallery -north-> Vault, and back.
...
8791049
8801050 ; The Great Hall — four pillars, east door at row 11.
8811051 room0_template:
882- defb "################################"
1052+ defb "###############T################"
8831053 defb "#..............................#"
8841054 defb "#..............................#"
8851055 defb "#..............................#"
...
9301100 defb "#.......................G......#"
9311101 defb "#..............................#"
9321102 defb "#..............................#"
933- defb "################################"
1103+ defb "###############T################"
9341104
9351105 ; The Vault — a great altar of stone in the middle, south door (column 15)
9361106 ; down to the Gallery.
9371107 room2_template:
938- defb "################################"
1108+ defb "###############T################"
9391109 defb "#..............................#"
9401110 defb "#..............................#"
9411111 defb "#..............................#"
...
10361206 defb WARDEN_GATHER
10371207 caught:
10381208 defb 0
1209+torch_col:
1210+ defb NO_TORCH
1211+torch_row:
1212+ defb NO_TORCH
10391213
10401214 room0_state:
10411215 defs 768
The complete program
; Shadowkeep — Unit 13: Light and Shadow
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 hangs a torch in each room and shades the floor by distance from the 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
NO_TORCH    equ     $FF
MAX_SHADE   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_torch:
            ld      a, NO_TORCH
            ld      (torch_col), a
            ld      (torch_row), a
            call    room_entry_addr
            ld      a, (hl)
            inc     hl
            ld      h, (hl)
            ld      l, a
            ld      b, 0
.ft_row:
            ld      c, 0
.ft_col:
            ld      a, (hl)
            cp      'T'
            jr      nz, .ft_skip
            ld      a, c
            ld      (torch_col), a
            ld      a, b
            ld      (torch_row), a
.ft_skip:
            inc     hl
            inc     c
            ld      a, c
            cp      32
            jr      nz, .ft_col
            inc     b
            ld      a, b
            cp      24
            jr      nz, .ft_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:
            ld      a, (torch_col)
            cp      NO_TORCH
            jr      z, .sf_dark
            ld      a, b
            ld      hl, torch_row
            sub     (hl)
            jr      nc, .sf_rpos
            neg
.sf_rpos:
            ld      d, a            ; |row - torch_row|
            ld      a, c
            ld      hl, torch_col
            sub     (hl)
            jr      nc, .sf_cpos
            neg
.sf_cpos:
            cp      d               ; A = |dc|; max(|dc|, |dr|)
            jr      nc, .sf_max
            ld      a, d
.sf_max:
            srl     a               ; distance / 2
            cp      MAX_SHADE + 1
            jr      c, .sf_done
            ld      a, MAX_SHADE
.sf_done:
            ret
.sf_dark:
            ld      a, MAX_SHADE
            ret

draw_room:
            call    find_torch
            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

; 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

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

; ----------------------------------------------------------------------------
; 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    "#..............................#"
            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    "###############T################"

; The Vault — a great altar of stone in the middle, south door (column 15)
; down to the Gallery.
room2_template:
            defb    "###############T################"
            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
torch_col:
            defb    NO_TORCH
torch_row:
            defb    NO_TORCH

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

            end     start

Stand in the lit pool under the Hall’s torch, then walk down into the dark — the floor thickens to shadow around you, and the gradient closes behind you untouched, the Warden pacing through it all the while:

From the bright pool under the torch, down into the deepening slate and back up. The light is a real space the thief moves through — and his save/restore lays each shade back as he steps off, so the gradient is never smeared. Baked light, carried for free.

Try this: move the flame

Shift the T in a room map to a side wall, or into a corner. The pool moves with it — the lit area is wherever the torch is, computed fresh. Light is data now: you place a flame by typing a letter, and the room lights itself around it. (Keep it clear of a Warden’s route, or you will light the danger and not the gold.)

Try this: a brighter, wider torch

The falloff is srl a — distance halved. Change it: srl a twice (distance / 4) makes a huge soft pool that barely reaches dark; remove the srl entirely (distance itself) and the light collapses to a tight, harsh ring. One shift instruction is the difference between a candle and a bonfire — and next unit but one, that one byte becomes a per-room dial.

Try this: light the walls too

Only floor is shaded here; the walls stay uniformly lit. Extend the idea: give # a shade ramp of its own and shade walls by distance to the torch as well. The far walls sink into the dark with the floor, and the pool becomes a true bubble of light in a black room — a big step toward Knight Lore’s gloom.

When it’s wrong, see why

  • The whole floor is darkest. No torch was found — check the room map contains a T, and that find_torch runs before the draw. NO_TORCH means everything clamps to shade 4.
  • The pool is the wrong shape or off-centre. shade_for_cell mixed up rows and columns, or isn’t taking the larger of the two gaps. Distance is max(|dr|, |dc|).
  • Floor cells are solid / the thief is trapped. A shade tile was given a bright attribute. All five shades share FLOOR_ATTR (dim); only the pattern changes, so they stay walkable.
  • A lifted coin leaves a flat pale patch. The collect repaint isn’t using shade_for_cell — it’s laying plain floor into a shaded room. Repaint the lifted cell with its distance-shade.
  • The lit area is a hard-edged block. That’s the square (Chebyshev) distance showing its corners. It’s fine, and true to the machine; for a rounder pool, sum the gaps (a diamond) or their squares (a circle, more maths).

Before and after

You started with a finished game lit like a warehouse — every cell the same flat blue — and finished with rooms that have a pool of light and a dark to step into. Nothing new drew the change: it’s the dither-is-shade idea from Unit 2, with the density now chosen by distance from a flame. A torch is a glyph you type into the map; the light bakes itself as the room paints; and both movers carry each shade beneath them without a line of extra code. The keep stopped being evenly lit and started having somewhere to hide — the first real stroke of atmosphere, over a game that was already whole.

What you’ve learnt

  • Light is dither density chosen by distance. The shading technique from Unit 2, driven by how far a cell is from a flame — no new drawing, only a new choice.
  • A light source is data. A glyph in the map; place it, move it, and the room re-lights around it.
  • Bake what doesn’t change. Static light is computed once as the room draws, costing nothing per frame — and both figures’ save/restore carry the shade for free.
  • Falloff is a feel knob. How fast light fades — one shift — sets whether the keep feels candle-lit or floodlit.

What’s next

The keep has light and dark now, but its rooms are bare — stone, pillars, and not much else. A place you believe in has things in it: an altar, a statue, banners, a scatter of rubble. In Unit 14, “Furnishings,” we add decorative objects — scenery the thief walks past, not into — that turn a lit room into a room with a story. Atmosphere is light and the things the light falls on.