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.
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
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 target — tcol, 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.
| 1 | 1 | ; Shadowkeep — Unit 3: A Place to Move | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | 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 | |
| 12 | 6 | | |
| 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 | |
| 15 | 11 | | |
| 12 | + | START_COL equ 15 | |
| 13 | + | START_ROW equ 11 | |
| 16 | 14 | LAST_ROW equ 23 | |
| 17 | 15 | 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) | |
| 18 | 20 | | |
| 19 | 21 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 21 | 24 | ; ---------------------------------------------------------------------------- | |
| 22 | 25 | start: | |
| 23 | 26 | 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 | |
| 25 | 33 | | |
| 26 | 34 | call draw_hall | |
| 35 | + | call save_under | |
| 27 | 36 | call draw_thief | |
| 28 | 37 | | |
| 29 | 38 | im 1 | |
| 30 | 39 | ei | |
| 31 | 40 | .loop: | |
| 32 | 41 | halt | |
| 42 | + | call player_step | |
| 33 | 43 | jr .loop | |
| 34 | 44 | | |
| 35 | 45 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 40 | 172 | ; ---------------------------------------------------------------------------- | |
| 41 | 173 | draw_hall: | |
| 42 | 174 | ld b, 0 | |
| 43 | - | .row: | |
| 175 | + | .hall_row: | |
| 44 | 176 | 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 | |
| 49 | 180 | inc c | |
| 50 | 181 | ld a, c | |
| 51 | 182 | cp LAST_COL + 1 | |
| 52 | - | jr nz, .col | |
| 183 | + | jr nz, .hall_col | |
| 53 | 184 | inc b | |
| 54 | 185 | ld a, b | |
| 55 | 186 | cp LAST_ROW + 1 | |
| 56 | - | jr nz, .row | |
| 187 | + | jr nz, .hall_row | |
| 57 | 188 | ret | |
| 58 | 189 | | |
| 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). | |
| 61 | 190 | pick_tile: | |
| 62 | 191 | ld a, b | |
| 63 | - | or a ; row 0? | |
| 192 | + | or a | |
| 64 | 193 | jr z, .wall | |
| 65 | - | cp LAST_ROW ; row 23? | |
| 194 | + | cp LAST_ROW | |
| 66 | 195 | jr z, .wall | |
| 67 | 196 | ld a, c | |
| 68 | - | or a ; column 0? | |
| 197 | + | or a | |
| 69 | 198 | jr z, .wall | |
| 70 | - | cp LAST_COL ; column 31? | |
| 199 | + | cp LAST_COL | |
| 71 | 200 | jr z, .wall | |
| 72 | 201 | .floor: | |
| 73 | 202 | ld hl, floor_tile | |
| ... | |||
| 82 | 211 | ld (tile_attr), a | |
| 83 | 212 | ret | |
| 84 | 213 | | |
| 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. | |
| 87 | 214 | draw_tile: | |
| 88 | 215 | push bc | |
| 89 | 216 | call attr_addr_cr | |
| 90 | 217 | ld a, (tile_attr) | |
| 91 | 218 | ld (hl), a | |
| 92 | - | call scr_addr_cr ; B,C still hold row, column | |
| 219 | + | call scr_addr_cr | |
| 93 | 220 | ld de, (tile_ptr) | |
| 94 | 221 | ld b, 8 | |
| 95 | - | .draw_tile_row: | |
| 222 | + | .tile_row: | |
| 96 | 223 | ld a, (de) | |
| 97 | 224 | ld (hl), a | |
| 98 | 225 | inc de | |
| 99 | 226 | inc h | |
| 100 | - | djnz .draw_tile_row | |
| 227 | + | djnz .tile_row | |
| 101 | 228 | 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 | |
| 124 | 229 | ret | |
| 125 | 230 | | |
| 126 | 231 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 128 | 233 | ; ---------------------------------------------------------------------------- | |
| 129 | 234 | scr_addr_cr: | |
| 130 | 235 | ld a, b | |
| ... | |||
| 158 | 263 | ret | |
| 159 | 264 | | |
| 160 | 265 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 165 | 267 | ; ---------------------------------------------------------------------------- | |
| 166 | - | floor_tile: ; 50% checker — dark slate | |
| 268 | + | floor_tile: | |
| 167 | 269 | defb %10101010 | |
| 168 | 270 | defb %01010101 | |
| 169 | 271 | defb %10101010 | |
| ... | |||
| 173 | 275 | defb %10101010 | |
| 174 | 276 | defb %01010101 | |
| 175 | 277 | | |
| 176 | - | wall_tile: ; sparse specks — light, lit stone | |
| 278 | + | wall_tile: | |
| 177 | 279 | defb %00010001 | |
| 178 | 280 | defb %00000000 | |
| 179 | 281 | defb %01000100 | |
| ... | |||
| 184 | 286 | defb %00000000 | |
| 185 | 287 | | |
| 186 | 288 | 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 | |
| 195 | 297 | | |
| 298 | + | thief_col: | |
| 299 | + | defb START_COL | |
| 300 | + | thief_row: | |
| 301 | + | defb START_ROW | |
| 302 | + | tcol: | |
| 303 | + | defb 0 | |
| 304 | + | trow: | |
| 305 | + | defb 0 | |
| 196 | 306 | tile_ptr: | |
| 197 | 307 | defw 0 | |
| 198 | 308 | tile_attr: | |
| 199 | 309 | defb 0 | |
| 310 | + | under_thief: | |
| 311 | + | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 200 | 312 | | |
| 201 | 313 | end start | |
| 202 | 314 | |
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:
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_OPis$DFFE,KEYS_Qis$FBFE,KEYS_Ais$FDFE— and the keyboard is active-low, so a held key reads as a0bit (test withBIT, branch onZ). - He walks through walls.
wall_atisn't reading the right bit, or your wall cells aren't BRIGHT.WALL_BITis 6; the wall tile's attribute (%01001000) has it set, the floor's (%00001000) does not. - He leaves a trail of thieves.
restore_underisn't being called before the move, so the old cell never gets its stone back. - The floor gets holes / wrong stone behind him.
save_undermust run after the position updates (so it saves the new cell), and beforedraw_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 zchain 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.