Skip to content
Game 2 Unit 9 of 16 1 hr learning time

Light and Shadow

The keep is flatly lit — 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.

56% of Shadowkeep

The keep is built, but it's lit like an office — every cell the same even blue, no dark corners, no sense of nightfall. Atmosphere is the whole point of Shadowkeep, and atmosphere starts with light. This unit hangs a torch in each room and lets it throw a pool of light into the gloom — and the tool that does it is one you already have.

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 away, dense and black. Nothing new but the choosing.

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 below, the pillars dark around him.
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. Same room as last unit, the same blue and black — only the dither density, chosen now by distance from the flame.

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 stands down in the shadow, the lit pool above him. Same keep 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:

            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:

shade_tiles:
            defw    shade0_tile     ; lit
            defw    shade1_tile
            defw    shade2_tile     ; the old floor
            defw    shade3_tile
            defw    shade4_tile     ; deep shadow

Lighting as the room draws

draw_room now does the choosing as it paints. A floor cell is shaded by distance; a wall, torch or chalk mark is drawn flat as before:

.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
            jr      .cell_done
.not_floor:
            call    lookup_tile      ; wall / torch / chalk: flat
            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; it's the still light a sconce casts. (A flickering, carried flame comes later.) And it costs nothing as the thief walks: his save-and-restore already preserves whatever's beneath him, lit shade and all, so he moves through the gradient and leaves 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 still drawn flat. The light is baked once, as the room draws — and the thief's save/restore carries each shade beneath him for free.

Step 1: a torch glyph, distance-to-shade, and a five-step floor ramp
+180-19
11 ; Shadowkeep — Unit 9: Light and Shadow
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 8's end: three rooms, flatly lit.
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
8-FLOOR_ATTR equ %00001000
8+FLOOR_ATTR equ %00001000 ; the shade ramp all share this (PAPER 1, INK 0); only the pattern differs
9+TORCH_ATTR equ %01001110 ; BRIGHT, PAPER 1 (blue), INK 6 (yellow) — a lit sconce (solid)
910 MARK_ATTR equ %00001111
1011 THIEF equ %01001010
1112 WALL_BIT equ 6
...
1314 START_COL equ 15
1415 START_ROW equ 11
1516 NO_EXIT equ $FF
17+NO_TORCH equ $FF
18+MAX_SHADE equ 4
1619
1720 KEYS_OP equ $DFFE
1821 KEYS_Q equ $FBFE
...
104107 add hl, hl
105108 ld de, rooms
106109 add hl, de
110+ ret
111+
112+; ----------------------------------------------------------------------------
113+; find_torch — scan the current room's map for 'T', remember where it is (or
114+; NO_TORCH if the room is unlit).
115+; ----------------------------------------------------------------------------
116+find_torch:
117+ ld a, NO_TORCH
118+ ld (torch_col), a
119+ ld (torch_row), a
120+ call room_entry_addr
121+ ld a, (hl)
122+ inc hl
123+ ld h, (hl)
124+ ld l, a
125+ ld b, 0
126+.ft_row:
127+ ld c, 0
128+.ft_col:
129+ ld a, (hl)
130+ cp 'T'
131+ jr nz, .ft_skip
132+ ld a, c
133+ ld (torch_col), a
134+ ld a, b
135+ ld (torch_row), a
136+.ft_skip:
137+ inc hl
138+ inc c
139+ ld a, c
140+ cp 32
141+ jr nz, .ft_col
142+ inc b
143+ ld a, b
144+ cp 24
145+ jr nz, .ft_row
146+ ret
147+
148+; ----------------------------------------------------------------------------
149+; shade_for_cell — row in B, column in C. Returns a shade 0..4 in A: the
150+; Chebyshev distance to the torch, halved and clamped. Near the flame = 0
151+; (lightest); far away = 4 (darkest). No torch = darkest everywhere.
152+; ----------------------------------------------------------------------------
153+shade_for_cell:
154+ ld a, (torch_col)
155+ cp NO_TORCH
156+ jr z, .sf_dark
157+ ld a, b
158+ ld hl, torch_row
159+ sub (hl)
160+ jr nc, .sf_rpos
161+ neg
162+.sf_rpos:
163+ ld d, a ; |row - torch_row|
164+ ld a, c
165+ ld hl, torch_col
166+ sub (hl)
167+ jr nc, .sf_cpos
168+ neg
169+.sf_cpos:
170+ cp d ; A = |dc|; max(|dc|, |dr|)
171+ jr nc, .sf_max
172+ ld a, d
173+.sf_max:
174+ srl a ; distance / 2
175+ cp MAX_SHADE + 1
176+ jr c, .sf_done
177+ ld a, MAX_SHADE
178+.sf_done:
179+ ret
180+.sf_dark:
181+ ld a, MAX_SHADE
107182 ret
108183
184+; ----------------------------------------------------------------------------
185+; draw_room — now lights as it draws. A floor cell ('.') is shaded by distance
186+; to the torch; everything else (wall, torch, chalk) is drawn flat.
187+; ----------------------------------------------------------------------------
109188 draw_room:
189+ call find_torch
110190 call room_entry_addr
111191 ld a, (hl)
112192 inc hl
...
119199 .room_col:
120200 ld hl, (map_ptr)
121201 ld a, (hl)
202+ cp '.'
203+ jr nz, .not_floor
204+
205+ call shade_for_cell ; A = shade 0..4
206+ add a, a ; index the pointer table
207+ ld e, a
208+ ld d, 0
209+ ld hl, shade_tiles
210+ add hl, de
211+ ld e, (hl)
212+ inc hl
213+ ld d, (hl)
214+ ld (tile_ptr), de
215+ ld a, FLOOR_ATTR
216+ ld (tile_attr), a
217+ call draw_tile
218+ jr .cell_done
219+.not_floor:
122220 call lookup_tile
123221 call draw_tile
222+.cell_done:
124223 ld hl, (map_ptr)
125224 inc hl
126225 ld (map_ptr), hl
...
381480 add hl, de
382481 ret
383482
483+; ----------------------------------------------------------------------------
484+; Palette — '.' is handled by the lighting path, so its entry here is only a
485+; fallback; '#', 'T' and '+' are looked up as normal.
486+; ----------------------------------------------------------------------------
384487 palette:
385488 defb '.'
386- defw floor_tile
489+ defw shade2_tile
387490 defb FLOOR_ATTR
388491 defb '#'
389492 defw wall_tile
390493 defb WALL_ATTR
494+ defb 'T'
495+ defw torch_tile
496+ defb TORCH_ATTR
391497 defb '+'
392498 defw mark_tile
393499 defb MARK_ATTR
394500
395-; ----------------------------------------------------------------------------
396-; The keep: three rooms. Hall -east-> Gallery -north-> Vault, and back.
397-; entry: map ptr, North, South, East, West
398-; ----------------------------------------------------------------------------
501+; The shade ramp: lightest (lit) to darkest (deep shadow), all blue/black dither.
502+shade_tiles:
503+ defw shade0_tile
504+ defw shade1_tile
505+ defw shade2_tile
506+ defw shade3_tile
507+ defw shade4_tile
508+
399509 rooms:
400510 defw room0_state
401- defb NO_EXIT, NO_EXIT, 1, NO_EXIT ; Hall: east -> Gallery
511+ defb NO_EXIT, NO_EXIT, 1, NO_EXIT
402512 defw room1_state
403- defb 2, NO_EXIT, NO_EXIT, 0 ; Gallery: north -> Vault, west -> Hall
513+ defb 2, NO_EXIT, NO_EXIT, 0
404514 defw room2_state
405- defb NO_EXIT, 1, NO_EXIT, NO_EXIT ; Vault: south -> Gallery
515+ defb NO_EXIT, 1, NO_EXIT, NO_EXIT
406516
407-; The Great Hall — four pillars, east door at row 11.
517+; The Great Hall — a torch ('T') set in the top wall, column 15.
408518 room0_template:
409- defb "################################"
519+ defb "###############T################"
410520 defb "#..............................#"
411521 defb "#..............................#"
412522 defb "#..............................#"
...
431541 defb "#..............................#"
432542 defb "################################"
433543
434-; The Gallery — a dividing wall with one gap (column 15). West door (row 11)
435-; back to the Hall; north door (column 15) up to the Vault.
544+; The Gallery — a torch low on the south wall.
436545 room1_template:
437546 defb "###############.################"
438547 defb "#..............................#"
...
457566 defb "#..............................#"
458567 defb "#..............................#"
459568 defb "#..............................#"
460- defb "################################"
569+ defb "###############T################"
461570
462-; The Vault — a great altar of stone in the middle, south door (column 15)
463-; down to the Gallery.
571+; The Vault — a torch in the top wall above the altar.
464572 room2_template:
465- defb "################################"
573+ defb "###############T################"
466574 defb "#..............................#"
467575 defb "#..............................#"
468576 defb "#..............................#"
...
487595 defb "#..............................#"
488596 defb "###############.################"
489597
490-floor_tile:
598+; ----------------------------------------------------------------------------
599+; The five shades of floor — same blue/black, denser and darker each step.
600+; ----------------------------------------------------------------------------
601+shade0_tile: ; lit — nearly all blue
602+ defb %00000000
603+ defb %00100010
604+ defb %00000000
605+ defb %00000000
606+ defb %00000000
607+ defb %10001000
608+ defb %00000000
609+ defb %00000000
610+shade1_tile:
611+ defb %00100010
612+ defb %00000000
613+ defb %10001000
614+ defb %00000000
615+ defb %00100010
616+ defb %00000000
617+ defb %10001000
618+ defb %00000000
619+shade2_tile: ; the half-and-half slate from Unit 2
620+ defb %10101010
621+ defb %01010101
622+ defb %10101010
623+ defb %01010101
491624 defb %10101010
492625 defb %01010101
493626 defb %10101010
494627 defb %01010101
628+shade3_tile:
495629 defb %10101010
630+ defb %11111111
496631 defb %01010101
632+ defb %11111111
497633 defb %10101010
634+ defb %11111111
498635 defb %01010101
636+ defb %11111111
637+shade4_tile: ; deep shadow — nearly all black
638+ defb %11111111
639+ defb %11101110
640+ defb %11111111
641+ defb %10111011
642+ defb %11111111
643+ defb %11101110
644+ defb %11111111
645+ defb %10111011
499646
500647 wall_tile:
501648 defb %00010001
...
506653 defb %00000000
507654 defb %01000100
508655 defb %00000000
656+
657+torch_tile: ; a flame in its sconce
658+ defb %00010000
659+ defb %00111000
660+ defb %00111000
661+ defb %01111100
662+ defb %01111100
663+ defb %01111100
664+ defb %00111000
665+ defb %00010000
509666
510667 mark_tile:
511668 defb %00000000
...
529686
530687 current_room:
531688 defb 0
689+torch_col:
690+ defb NO_TORCH
691+torch_row:
692+ defb NO_TORCH
532693 thief_col:
533694 defb START_COL
534695 thief_row:
The complete program
; Shadowkeep — Unit 9: 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       ; the shade ramp all share this (PAPER 1, INK 0); only the pattern differs
TORCH_ATTR  equ     %01001110       ; BRIGHT, PAPER 1 (blue), INK 6 (yellow) — a lit sconce (solid)
MARK_ATTR   equ     %00001111
THIEF       equ     %01001010
WALL_BIT    equ     6

START_COL   equ     15
START_ROW   equ     11
NO_EXIT     equ     $FF
NO_TORCH    equ     $FF
MAX_SHADE   equ     4

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

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

            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      a, START_COL
            ld      (thief_col), a
            ld      a, START_ROW
            ld      (thief_row), a

            call    draw_room
            call    save_under
            call    draw_thief

            im      1
            ei
.loop:
            halt
            call    player_step
            call    mark_step
            jr      .loop

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 — now lights as it draws. A floor cell ('.') is shaded by distance
; to the torch; everything else (wall, torch, chalk) is drawn flat.
; ----------------------------------------------------------------------------
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      ; A = shade 0..4
            add     a, a                ; index the pointer table
            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    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
            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

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 — '.' is handled by the lighting path, so its entry here is only a
; fallback; '#', 'T' and '+' are looked up as normal.
; ----------------------------------------------------------------------------
palette:
            defb    '.'
            defw    shade2_tile
            defb    FLOOR_ATTR
            defb    '#'
            defw    wall_tile
            defb    WALL_ATTR
            defb    'T'
            defw    torch_tile
            defb    TORCH_ATTR
            defb    '+'
            defw    mark_tile
            defb    MARK_ATTR

; 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

rooms:
            defw    room0_state
            defb    NO_EXIT, NO_EXIT, 1, NO_EXIT
            defw    room1_state
            defb    2, NO_EXIT, NO_EXIT, 0
            defw    room2_state
            defb    NO_EXIT, 1, NO_EXIT, NO_EXIT

; The Great Hall — a torch ('T') set in the top wall, column 15.
room0_template:
            defb    "###############T################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................."
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#........##..........##........#"
            defb    "#........##..........##........#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "################################"

; The Gallery — a torch low on the south wall.
room1_template:
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############.################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "...............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############T################"

; The Vault — a torch in the top wall above the altar.
room2_template:
            defb    "###############T################"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#.............####.............#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "#..............................#"
            defb    "###############.################"

; ----------------------------------------------------------------------------
; The five shades of floor — same blue/black, denser and darker each step.
; ----------------------------------------------------------------------------
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

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

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

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
torch_col:
            defb    NO_TORCH
torch_row:
            defb    NO_TORCH
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

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 straight down into the dark — the floor thickens to shadow around you, and the gradient closes behind you untouched:

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.

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.

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.
  • The torch is walkable / draws as floor. T must be in the palette with a bright attribute, and the floor branch only catches . — anything else falls through to the palette.
  • 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, add the gaps instead of taking the larger (a diamond) or sum their squares (a circle, more maths).

Before and after

You started with a keep lit like an office — 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 instead of fixed. A torch is a glyph you type into the map; the light bakes itself as the room paints; and the thief carries each shade beneath him without a line of extra code. The keep stopped being evenly lit and started having somewhere to hide — the first real stroke of atmosphere.

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 the hero's save/restore carries 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 sconce, a scatter of rubble, a well. In Unit 10, "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.