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

A Place to Move

Set the thief walking the hall. It's almost all Gloaming — keyboard, cell-step movement, save/restore, wall collision — with two keep-specific twists: the save buffer now carries dithered stone, and a wall is known by its light.

19% of Shadowkeep

The hall is built and the thief is standing in it — frozen. This unit sets him walking, and here's the good news: you've written nearly all of it before. Reading the keyboard, stepping a cell at a time, saving and restoring what's under the hero, turning him back at a wall — every piece came from Gloaming. We're moving a hooded thief through a room of stone instead of a lamplighter round a square, and the engine doesn't care.

Two things are new, because the keep is not the empty square — and both are quietly satisfying.

What you'll see by the end

The hooded thief, having walked up and to the left, stopped against the top wall of lit blue stone; the dark slate floor below him is unbroken, with no trail.
Walked up and to the left, stopped hard against the lit stone — and the slate behind him is whole. He leaves the floor exactly as he found it.

Press Q A O P — up, down, left, right, the keys Spectrum fingers have known since 1982 — and the thief walks the hall a cell at a time. Push him into a wall and he stops; the lit stone won't let him through. Walk him all over the room and look at the floor behind him: the slate is unbroken. No smear, no trail, no holes punched in the dither. He passes over the stone and leaves it exactly as it was.

Reading the keys, stepping a cell

This is Gloaming, unchanged. Each frame we read the half-rows that hold Q, A, O and P, and a held key (a 0 bit — the keyboard is active-low) sets a target one cell away:

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a            ; O held? -> left
            jr      z, .left
            bit     0, a            ; P held? -> right
            jr      z, .right

.left / .right / .up / .down each nudge a targettcol, trow — by one. Nothing has moved yet; we've only proposed a move. Whether it happens depends on what's in the way.

A wall is known by its light

In Gloaming a wall cell carried a tell-tale attribute bit, and wall_at tested it. The keep is subtler: its walls and its floor are both blue PAPER — what separates them is that the walls are lit (BRIGHT) and the floor is dim. So the question "is this a wall?" becomes "is this cell BRIGHT?", and WALL_BIT is bit 6:

WALL_BIT    equ     6               ; BRIGHT marks solid stone

wall_at:
            call    attr_addr_cr
            bit     WALL_BIT, (hl)  ; Z = not bright = walkable
            ret
.move:
            ld      a, (trow)
            ld      b, a
            ld      a, (tcol)
            ld      c, a
            call    wall_at
            ret     nz              ; bright stone — stay put

This is the honest cell-based rule: solidity read straight off the look of the cell. It has a limit — it means a wall must be lit and a floor must not be, so you can't yet have a lit, walkable patch of floor under a torch. Decoupling the two — collision in its own map, the attributes free for pure mood — is a real technique, and it gets its own game later. For the keep we're building, "lit stone is solid" is exactly enough.

The floor survives him

Here's the twist that's pure Shadowkeep. When the lamplighter moved, the cell under him was blank paper; saving and restoring it was almost free. Under the thief is dithered stone — eight bytes of pattern that must come back intact, or he'd punch a hole in the floor with every step.

But the nine-byte buffer was always big enough: eight bitmap rows plus one attribute byte. So save_under records the slate before he stands on it, and restore_under lays it back as he steps off:

.move:
            ; ... not a wall, so commit:
            call    restore_under   ; put the old cell's stone back
            ld      a, (tcol)
            ld      (thief_col), a
            ld      a, (trow)
            ld      (thief_row), a
            call    save_under      ; remember the stone he's stepping onto
            call    draw_thief

Restore the old, move, save the new, draw. The same four steps Gloaming used — but now they're carrying texture, and the dither you built in Unit 2 flows back into place behind him, seamless. The engine was ready for this all along.

Milestone — set him walking

We add player_step — read QAOP, propose a target cell, refuse it if the cell is lit (a wall), otherwise restore the old stone, move, save the new stone, and redraw. Every piece is Gloaming's, pointed at the thief; the keep-specific twists are the BRIGHT-bit wall test and a save buffer now carrying dithered slate.

Step 1: QAOP movement, wall-by-light, and save/restore the stone
+182-70
11 ; Shadowkeep — Unit 3: A Place to Move
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 2's end: the dithered hall, the thief standing still.
3+; step-01 sets him walking — QAOP, wall-by-light, and save/restore the stone.
44
55 org 32768
6-
7-; Two shades of stone, both PAPER 1 (blue) over INK 0 (black). The shade comes
8-; from the bitmap dither density; BRIGHT lifts the walls into the light.
9-WALL_ATTR equ %01001000 ; BRIGHT — lit stone (drawn with a sparse dither)
10-FLOOR_ATTR equ %00001000 ; dim — dark slate (drawn with a 50% dither)
11-THIEF equ %01001010 ; BRIGHT, PAPER 1 (blue), INK 2 (red) — the thief
126
13-HERO_COL equ 15
14-HERO_ROW equ 11
7+WALL_ATTR equ %01001000 ; BRIGHT — lit stone (solid)
8+FLOOR_ATTR equ %00001000 ; dim — dark slate (walkable)
9+THIEF equ %01001010 ; BRIGHT, PAPER 1 (blue), INK 2 (red)
10+WALL_BIT equ 6 ; BRIGHT marks solid stone
1511
12+START_COL equ 15
13+START_ROW equ 11
1614 LAST_ROW equ 23
1715 LAST_COL equ 31
16+
17+KEYS_OP equ $DFFE ; O = bit 1 (left), P = bit 0 (right)
18+KEYS_Q equ $FBFE ; Q = bit 0 (up)
19+KEYS_A equ $FDFE ; A = bit 0 (down)
1820
1921 ; ----------------------------------------------------------------------------
20-; SETUP — build the hall, then stand the thief in it.
22+; SETUP — build the hall, save the floor under the start cell, draw the thief,
23+; then run the frame-locked loop, stepping him once per frame.
2124 ; ----------------------------------------------------------------------------
2225 start:
2326 ld a, 0
24- out ($FE), a ; black border
27+ out ($FE), a
28+
29+ ld a, START_COL
30+ ld (thief_col), a
31+ ld a, START_ROW
32+ ld (thief_row), a
2533
2634 call draw_hall
35+ call save_under
2736 call draw_thief
2837
2938 im 1
3039 ei
3140 .loop:
3241 halt
42+ call player_step
3343 jr .loop
3444
3545 ; ----------------------------------------------------------------------------
36-; draw_hall — every cell of the screen: a wall tile around the edge, a floor
37-; tile within. Two nested loops, row in B and column in C, exactly the shape of
38-; the fill you wrote in Gloaming — but laying eight-byte stone tiles, not one
39-; flat attribute byte.
46+; player_step — read a direction, test the target cell, and move only if it's
47+; not a wall. Pure Gloaming, pointed at the thief.
48+; ----------------------------------------------------------------------------
49+player_step:
50+ ld a, (thief_col)
51+ ld (tcol), a
52+ ld a, (thief_row)
53+ ld (trow), a
54+
55+ ld bc, KEYS_OP
56+ in a, (c)
57+ bit 1, a
58+ jr z, .left
59+ bit 0, a
60+ jr z, .right
61+ ld bc, KEYS_Q
62+ in a, (c)
63+ bit 0, a
64+ jr z, .up
65+ ld bc, KEYS_A
66+ in a, (c)
67+ bit 0, a
68+ jr z, .down
69+ ret
70+
71+.left:
72+ ld hl, tcol
73+ dec (hl)
74+ jr .move
75+.right:
76+ ld hl, tcol
77+ inc (hl)
78+ jr .move
79+.up:
80+ ld hl, trow
81+ dec (hl)
82+ jr .move
83+.down:
84+ ld hl, trow
85+ inc (hl)
86+.move:
87+ ld a, (trow)
88+ ld b, a
89+ ld a, (tcol)
90+ ld c, a
91+ call wall_at
92+ ret nz ; a wall — stay put
93+
94+ call restore_under ; put the floor back where he was
95+ ld a, (tcol)
96+ ld (thief_col), a
97+ ld a, (trow)
98+ ld (thief_row), a
99+ call save_under ; remember the floor he's stepping onto
100+ call draw_thief
101+ ret
102+
103+; wall_at — row in B, column in C. Z set (walkable) if the cell isn't BRIGHT.
104+wall_at:
105+ call attr_addr_cr
106+ bit WALL_BIT, (hl)
107+ ret
108+
109+; ----------------------------------------------------------------------------
110+; The thief's save / restore / draw — Gloaming's, renamed. The nine-byte buffer
111+; holds the eight bitmap rows of dithered stone plus the floor's attribute.
112+; ----------------------------------------------------------------------------
113+pos_bc:
114+ ld a, (thief_row)
115+ ld b, a
116+ ld a, (thief_col)
117+ ld c, a
118+ ret
119+
120+save_under:
121+ call pos_bc
122+ call scr_addr_cr
123+ ld de, under_thief
124+ ld b, 8
125+.save_row:
126+ ld a, (hl)
127+ ld (de), a
128+ inc de
129+ inc h
130+ djnz .save_row
131+ call pos_bc
132+ call attr_addr_cr
133+ ld a, (hl)
134+ ld (under_thief + 8), a
135+ ret
136+
137+restore_under:
138+ call pos_bc
139+ call scr_addr_cr
140+ ld de, under_thief
141+ ld b, 8
142+.restore_row:
143+ ld a, (de)
144+ ld (hl), a
145+ inc de
146+ inc h
147+ djnz .restore_row
148+ call pos_bc
149+ call attr_addr_cr
150+ ld a, (under_thief + 8)
151+ ld (hl), a
152+ ret
153+
154+draw_thief:
155+ call pos_bc
156+ call attr_addr_cr
157+ ld (hl), THIEF
158+ call pos_bc
159+ call scr_addr_cr
160+ ld de, thief
161+ ld b, 8
162+.thief_row:
163+ ld a, (de)
164+ ld (hl), a
165+ inc de
166+ inc h
167+ djnz .thief_row
168+ ret
169+
170+; ----------------------------------------------------------------------------
171+; draw_hall / pick_tile / draw_tile — unchanged from Unit 2.
40172 ; ----------------------------------------------------------------------------
41173 draw_hall:
42174 ld b, 0
43-.row:
175+.hall_row:
44176 ld c, 0
45-.col:
46- call pick_tile ; sets tile_ptr + tile_attr for this cell
47- call draw_tile ; draws it at (B, C)
48-
177+.hall_col:
178+ call pick_tile
179+ call draw_tile
49180 inc c
50181 ld a, c
51182 cp LAST_COL + 1
52- jr nz, .col
183+ jr nz, .hall_col
53184 inc b
54185 ld a, b
55186 cp LAST_ROW + 1
56- jr nz, .row
187+ jr nz, .hall_row
57188 ret
58189
59-; pick_tile — wall if this cell is on the edge, floor otherwise. Leaves the
60-; choice in tile_ptr / tile_attr. Preserves B (row) and C (column).
61190 pick_tile:
62191 ld a, b
63- or a ; row 0?
192+ or a
64193 jr z, .wall
65- cp LAST_ROW ; row 23?
194+ cp LAST_ROW
66195 jr z, .wall
67196 ld a, c
68- or a ; column 0?
197+ or a
69198 jr z, .wall
70- cp LAST_COL ; column 31?
199+ cp LAST_COL
71200 jr z, .wall
72201 .floor:
73202 ld hl, floor_tile
...
82211 ld (tile_attr), a
83212 ret
84213
85-; draw_tile — colour cell (B,C), then lay its eight bitmap rows. The address
86-; helpers clobber A and DE, so the tile pointer is fetched *after* them.
87214 draw_tile:
88215 push bc
89216 call attr_addr_cr
90217 ld a, (tile_attr)
91218 ld (hl), a
92- call scr_addr_cr ; B,C still hold row, column
219+ call scr_addr_cr
93220 ld de, (tile_ptr)
94221 ld b, 8
95-.draw_tile_row:
222+.tile_row:
96223 ld a, (de)
97224 ld (hl), a
98225 inc de
99226 inc h
100- djnz .draw_tile_row
227+ djnz .tile_row
101228 pop bc
102- ret
103-
104-; ----------------------------------------------------------------------------
105-; draw_thief — unchanged from Unit 1: colour his cell, lay his eight rows.
106-; He stands on the floor; his solid sprite simply replaces the floor in his one
107-; cell (cell-based drawing — no see-through. Masking comes much later, in its
108-; own game). Save/restore in Unit 3 will protect the floor as he moves.
109-; ----------------------------------------------------------------------------
110-draw_thief:
111- ld b, HERO_ROW
112- ld c, HERO_COL
113- call attr_addr_cr
114- ld (hl), THIEF
115- call scr_addr_cr
116- ld de, thief
117- ld b, 8
118-.draw_thief_row:
119- ld a, (de)
120- ld (hl), a
121- inc de
122- inc h
123- djnz .draw_thief_row
124229 ret
125230
126231 ; ----------------------------------------------------------------------------
127-; scr_addr_cr / attr_addr_cr — carried from Gloaming. Row in B, column in C.
232+; scr_addr_cr / attr_addr_cr — carried from Gloaming.
128233 ; ----------------------------------------------------------------------------
129234 scr_addr_cr:
130235 ld a, b
...
158263 ret
159264
160265 ; ----------------------------------------------------------------------------
161-; The stone tiles. Read the bits: a 1 is a black INK pixel, a 0 is blue PAPER
162-; showing through. The floor mixes them half-and-half (dark slate); the wall
163-; scatters a few black specks across mostly-blue, BRIGHT stone (lit, light).
164-; Both patterns tile seamlessly cell to cell.
266+; Data.
165267 ; ----------------------------------------------------------------------------
166-floor_tile: ; 50% checker — dark slate
268+floor_tile:
167269 defb %10101010
168270 defb %01010101
169271 defb %10101010
...
173275 defb %10101010
174276 defb %01010101
175277
176-wall_tile: ; sparse specks — light, lit stone
278+wall_tile:
177279 defb %00010001
178280 defb %00000000
179281 defb %01000100
...
184286 defb %00000000
185287
186288 thief:
187- defb %00011000 ; ...XX... the hood's peak
188- defb %00111100 ; ..XXXX.. the hood
189- defb %01111110 ; .XXXXXX. hood meets shoulders
190- defb %01111110 ; .XXXXXX. the cloak
191- defb %01111110 ; .XXXXXX. the cloak
192- defb %01111110 ; .XXXXXX. the cloak
193- defb %00111100 ; ..XXXX.. the cloak narrows
194- defb %00100100 ; ..X..X.. two feet at the hem
289+ defb %00011000
290+ defb %00111100
291+ defb %01111110
292+ defb %01111110
293+ defb %01111110
294+ defb %01111110
295+ defb %00111100
296+ defb %00100100
195297
298+thief_col:
299+ defb START_COL
300+thief_row:
301+ defb START_ROW
302+tcol:
303+ defb 0
304+trow:
305+ defb 0
196306 tile_ptr:
197307 defw 0
198308 tile_attr:
199309 defb 0
310+under_thief:
311+ defb 0, 0, 0, 0, 0, 0, 0, 0, 0
200312
201313 end start
202314
The complete program
; Shadowkeep — Unit 3: A Place to Move
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 sets him walking — QAOP, wall-by-light, and save/restore the stone.

            org     32768

WALL_ATTR   equ     %01001000       ; BRIGHT — lit stone (solid)
FLOOR_ATTR  equ     %00001000       ; dim — dark slate (walkable)
THIEF       equ     %01001010       ; BRIGHT, PAPER 1 (blue), INK 2 (red)
WALL_BIT    equ     6               ; BRIGHT marks solid stone

START_COL   equ     15
START_ROW   equ     11
LAST_ROW    equ     23
LAST_COL    equ     31

KEYS_OP     equ     $DFFE           ; O = bit 1 (left), P = bit 0 (right)
KEYS_Q      equ     $FBFE           ; Q = bit 0 (up)
KEYS_A      equ     $FDFE           ; A = bit 0 (down)

; ----------------------------------------------------------------------------
; SETUP — build the hall, save the floor under the start cell, draw the thief,
; then run the frame-locked loop, stepping him once per frame.
; ----------------------------------------------------------------------------
start:
            ld      a, 0
            out     ($FE), a

            ld      a, START_COL
            ld      (thief_col), a
            ld      a, START_ROW
            ld      (thief_row), a

            call    draw_hall
            call    save_under
            call    draw_thief

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

; ----------------------------------------------------------------------------
; player_step — read a direction, test the target cell, and move only if it's
; not a wall. Pure Gloaming, pointed at the thief.
; ----------------------------------------------------------------------------
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              ; a wall — stay put

            call    restore_under   ; put the floor back where he was
            ld      a, (tcol)
            ld      (thief_col), a
            ld      a, (trow)
            ld      (thief_row), a
            call    save_under      ; remember the floor he's stepping onto
            call    draw_thief
            ret

; wall_at — row in B, column in C. Z set (walkable) if the cell isn't BRIGHT.
wall_at:
            call    attr_addr_cr
            bit     WALL_BIT, (hl)
            ret

; ----------------------------------------------------------------------------
; The thief's save / restore / draw — Gloaming's, renamed. The nine-byte buffer
; holds the eight bitmap rows of dithered stone plus the floor's attribute.
; ----------------------------------------------------------------------------
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

; ----------------------------------------------------------------------------
; draw_hall / pick_tile / draw_tile — unchanged from Unit 2.
; ----------------------------------------------------------------------------
draw_hall:
            ld      b, 0
.hall_row:
            ld      c, 0
.hall_col:
            call    pick_tile
            call    draw_tile
            inc     c
            ld      a, c
            cp      LAST_COL + 1
            jr      nz, .hall_col
            inc     b
            ld      a, b
            cp      LAST_ROW + 1
            jr      nz, .hall_row
            ret

pick_tile:
            ld      a, b
            or      a
            jr      z, .wall
            cp      LAST_ROW
            jr      z, .wall
            ld      a, c
            or      a
            jr      z, .wall
            cp      LAST_COL
            jr      z, .wall
.floor:
            ld      hl, floor_tile
            ld      (tile_ptr), hl
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            ret
.wall:
            ld      hl, wall_tile
            ld      (tile_ptr), hl
            ld      a, WALL_ATTR
            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

; ----------------------------------------------------------------------------
; scr_addr_cr / attr_addr_cr — carried from Gloaming.
; ----------------------------------------------------------------------------
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

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

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

thief_col:
            defb    START_COL
thief_row:
            defb    START_ROW
tcol:
            defb    0
trow:
            defb    0
tile_ptr:
            defw    0
tile_attr:
            defb    0
under_thief:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0

            end     start

Walk him round the hall and the two new things show at once — the walls turn him back on every side, and the slate stays whole behind him, no trail:

QAOP walks the thief cell by cell. He stops dead against the lit stone on all four sides, and the dithered floor closes seamlessly behind every step — save the stone, move, restore it. The engine Gloaming built, now carrying texture.

Try this: slow his stride

He moves once per frame a key is held — fifty cells a second, a blur. Add a step_timer byte: count it down each frame, and only read keys and move when it hits zero, then reset it (to, say, 4). A slower, more deliberate thief. You've just separated game speed from frame rate — the knob behind every character's sense of weight.

Try this: footprints (then take them back)

Comment out the call restore_under in .move. Walk around. Now he doesn't tidy up — every cell he leaves keeps his sprite, and he smears a trail of thieves across the floor. Ugly, but instructive: that single call is the whole of "leave the world as you found it." Put it back.

Try this: rebind to a joystick feel

The cursor keys (5/6/7/8 on a 48K) or QAOP are both period-correct. Change the half-rows and bits in player_step to read 5/6/7/8 instead, and feel how a different control scheme changes the game before you've touched a single graphic.

When it's wrong, see why

  • He won't move. The half-row ports must be right — KEYS_OP is $DFFE, KEYS_Q is $FBFE, KEYS_A is $FDFE — and the keyboard is active-low, so a held key reads as a 0 bit (test with BIT, branch on Z).
  • He walks through walls. wall_at isn't reading the right bit, or your wall cells aren't BRIGHT. WALL_BIT is 6; the wall tile's attribute (%01001000) has it set, the floor's (%00001000) does not.
  • He leaves a trail of thieves. restore_under isn't being called before the move, so the old cell never gets its stone back.
  • The floor gets holes / wrong stone behind him. save_under must run after the position updates (so it saves the new cell), and before draw_thief (so it saves stone, not the sprite). Restore old, move, save new, draw — in that order.
  • He drifts diagonally or jumps two cells. Only one direction should win per frame — the jr z chain returns after the first key found. Make sure each branch falls through to .move, not into the next test.

Before and after

You started with a thief rooted to one cell and finished with him walking the hall, turned back by its walls, leaving the floor whole behind him — and almost none of it was new. Movement is the propose-then-test from Gloaming; the wall test just reads the cell's light instead of a marker bit; and the save/restore buffer was always nine bytes, big enough to carry dithered stone as readily as blank paper. Two small keep-specific twists on a finished engine, and the keep has someone walking through it.

What you've learnt

  • Movement is a proposal, then a test. Read a direction into a target cell; commit only if it's clear. The same shape as Gloaming.
  • Collision can read the look. With walls lit and floor dim, "is it solid?" is "is it BRIGHT?" — honest and cheap, with a limit a later game lifts.
  • Save/restore carries whatever's under the hero. The nine-byte buffer holds dithered stone as happily as blank paper; the floor survives his passing, no trail.
  • Order is the whole trick. Restore old, move, save new, draw — get the sequence right and the world stays whole.

What's next

The thief walks a single hand-placed room. Before the keep can become many rooms, it needs a way to describe a room as data — not cells nailed into the code, but a compact map the same drawing routine can lay out, whatever its shape. In Unit 4, "The Keep's Hand," we build that: a clean tile-and-attribute palette and the room-data format every room of Shadowkeep will share. It's the groundwork that turns one hall into a keep.