Walls
The map gets opinions: two brick buildings arrive as rectangle data, walk on as paint you can ghost through — then wall_at bit-tests the target cell's attribute, and everything brick turns solid at once.
Movement is honest now — paced, and the ground survives it. But the map still has no say in where the lamplighter goes. The walls round the square are paint: he’d stroll clean through them if the scaffold clamp weren’t standing guard, and Unit 6 said as much when the clamp went in. This unit is where the map starts pushing back — and the square gets something worth pushing back with.
A town needs buildings
An empty walled square is an arena; Gloaming is a town square at dusk, and towns have buildings. We could paint one the way we painted the frame — a run of attribute writes at hand-picked addresses — but there are two buildings, and later levels might want a different skyline. So the buildings follow the pattern the whole game will keep returning to: shape as code, placement as data. Each building is four bytes — column, row, width, height — in a little table with an $FF end-marker:
bldg_data:
defb 5, 5, 4, 3 ; a 4×3 building, north-west
defb 23, 5, 4, 3 ; its twin, north-east
defb $FF ; end of the town
One routine, paint_buildings, walks the table and stamps each rectangle’s cells with the WALL attribute — and that’s all it does. No pixels. The brick texture arrives free, because Unit 2’s fill_walls textures every cell whose attribute says wall, wherever it finds one. Paint the colour, and the brickwork follows.
Milestone 1 — buildings you can walk through
Build exactly that much — the table, the painter, one call in the setup — and nothing else. In particular, no collision. The point of stopping here is to feel the gap between looking solid and being solid.
| 53 | 53 | ldir | |
| 54 | 54 | | |
| 55 | 55 | call paint_walls | |
| 56 | + | call paint_buildings | |
| 56 | 57 | | |
| 57 | 58 | ; --- brick the walls --- | |
| 58 | 59 | ; Now that the wall cells are painted, fill_walls can read the | |
| ... | |||
| 318 | 319 | defb %10000000 | |
| 319 | 320 | defb %10000000 | |
| 320 | 321 | defb %11111111 | |
| 322 | + | | |
| 323 | + | ; paint_buildings — walk the rectangle table: each entry is col, row, | |
| 324 | + | ; width, height; $FF ends the list. Every cell inside a rectangle gets | |
| 325 | + | ; the WALL attribute — and because fill_walls textures by the wall bit, | |
| 326 | + | ; the brickwork arrives without another line of drawing code. | |
| 327 | + | paint_buildings: | |
| 328 | + | ld hl, bldg_data | |
| 329 | + | .pb: | |
| 330 | + | ld a, (hl) | |
| 331 | + | cp $FF | |
| 332 | + | ret z | |
| 333 | + | ld c, a ; col | |
| 334 | + | inc hl | |
| 335 | + | ld b, (hl) ; row | |
| 336 | + | inc hl | |
| 337 | + | ld d, (hl) ; width | |
| 338 | + | inc hl | |
| 339 | + | ld e, (hl) ; height | |
| 340 | + | inc hl | |
| 341 | + | push hl | |
| 342 | + | .pbrow: | |
| 343 | + | push bc | |
| 344 | + | push de | |
| 345 | + | .pbcol: | |
| 346 | + | push bc | |
| 347 | + | push de | |
| 348 | + | call attr_addr_cr | |
| 349 | + | ld (hl), WALL | |
| 350 | + | pop de | |
| 351 | + | pop bc | |
| 352 | + | inc c | |
| 353 | + | dec d | |
| 354 | + | jr nz, .pbcol | |
| 355 | + | pop de | |
| 356 | + | pop bc | |
| 357 | + | inc b | |
| 358 | + | dec e | |
| 359 | + | jr nz, .pbrow | |
| 360 | + | pop hl | |
| 361 | + | jr .pb | |
| 362 | + | | |
| 363 | + | bldg_data: | |
| 364 | + | defb 5, 5, 4, 3 | |
| 365 | + | defb 23, 5, 4, 3 | |
| 366 | + | defb $FF | |
| 321 | 367 | | |
| 322 | 368 | ; ---------------------------------------------------------------------------- | |
| 323 | 369 | ; scr_addr_cr — HL = bitmap address of cell (C, B)'s first pixel row. |
The complete step 1 program
; Gloaming — Unit 9: Walls
; Cumulative build; every step runs on its own. Narrative: the unit page.
; Collision is a bit-test on the attribute; buildings are rectangle data.
org 32768
COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
WALL_BIT equ 3 ; the attribute bit that says "this is wall"
LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — his own light
START_COL equ 15 ; where the lamplighter begins
START_ROW equ 11
PLAYER_REPEAT equ 6 ; frames between steps while a key is held
KEYS_OP equ $DFFE ; half-row P O I U Y — bits 1 and 0
KEYS_Q equ $FBFE ; half-row Q W E R T — bit 0 is Q
KEYS_A equ $FDFE ; half-row A S D F G — bit 0 is A
start:
; --- the border goes black — the night beyond the square ---
; Port $FE bits 0-2 set the BORDER colour. A = 0 = black.
ld a, 0
out ($FE), a
; --- place the lamplighter ---
; His position is data. Everything that draws him reads it.
ld a, START_COL
ld (lamp_col), a
ld a, START_ROW
ld (lamp_row), a
xor a
ld (player_timer), a
; --- wipe the canvas ---
; The bitmap ($4000-$57FF) is the pixel layer; whatever was on
; screen before us still lives there. Zero it so only our
; attribute colours show.
call clear_bitmap
; --- texture the ground ---
; Blit the cobble stipple into every cell's bitmap, rows 1-23.
; The attributes will colour these pixels in a moment.
call fill_ground
; --- wash in the cobbles ---
; Seed the first attribute cell, point DE one cell ahead, and
; let LDIR cascade the byte through all 768 cells.
ld hl, $5800
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
call paint_walls
call paint_buildings
; --- brick the walls ---
; Now that the wall cells are painted, fill_walls can read the
; map back and lay brick wherever the wall bit is set.
call fill_walls
; save what he is about to stand on, BEFORE the first draw
call save_under
call draw_lamp
; --- start the heartbeat ---
; IM 1: every 50 Hz frame interrupt calls the ROM's handler.
; EI: let it. HALT then sleeps until the next frame arrives,
; so the loop below beats exactly once per frame.
im 1
ei
main_loop:
halt
call play_step
jr main_loop
; play_step — one beat of the game: ask the keyboard.
play_step:
call player_step
ret
; ----------------------------------------------------------------------------
; paint_walls — the square's edge, one attribute write per cell.
; ----------------------------------------------------------------------------
paint_walls:
ld c, WALL ; the byte every wall cell gets
; the top wall: row 1 is 32 cells in a row from $5820
; (row 0 is kept back — it becomes the HUD later)
ld hl, $5820
ld b, 32
.wt:
ld (hl), c
inc hl
djnz .wt
; the bottom wall: row 23, 32 cells from $5AE0
ld hl, $5AE0
ld b, 32
.wb:
ld (hl), c
inc hl
djnz .wb
; the side walls: column 0 and column 31 of rows 1-23.
; Write the row's first cell, hop 31 cells to its last,
; then step a full row (32) down — 23 times.
ld hl, $5820
ld b, 23
.ws:
ld (hl), c
push hl
ld de, 31
add hl, de
ld (hl), c
pop hl
ld de, 32
add hl, de
djnz .ws
ret
; ----------------------------------------------------------------------------
; clear_bitmap — zero the pixel layer, $4000-$57FF, with the same
; seed-and-cascade LDIR idiom the cobble wash uses.
; ----------------------------------------------------------------------------
clear_bitmap:
ld hl, $4000
ld de, $4001
ld (hl), 0
ld bc, 6143
ldir
ret
; ----------------------------------------------------------------------------
; player_step — the keys become movement. Each direction key edits a
; TARGET position (tcol, trow) — a proposal, not yet a move — so it
; can be vetoed before it becomes real. Then the move commits: leave
; the old cell, take the new one, draw.
; ----------------------------------------------------------------------------
player_step:
; --- propose: the target starts where he stands ---
ld a, (lamp_col)
ld (tcol), a
ld a, (lamp_row)
ld (trow), a
; The held-key gate: the first press steps at once, then one
; step every PLAYER_REPEAT frames. Releasing every direction
; key re-arms the instant first step, so taps stay crisp.
ld bc, KEYS_OP
in a, (c)
cpl
and %00000011
ld e, a
ld bc, KEYS_Q
in a, (c)
cpl
and %00000001
or e
ld e, a
ld bc, KEYS_A
in a, (c)
cpl
and %00000001
or e
jr nz, .held
xor a
ld (player_timer), a
ret
.held:
ld a, (player_timer)
or a
jr z, .stepnow
dec a
ld (player_timer), a
ret
.stepnow:
ld a, PLAYER_REPEAT
ld (player_timer), a
ld bc, KEYS_OP
in a, (c)
bit 1, a ; O — a zero bit is a pressed key
jr z, .pleft
bit 0, a ; P, same half-row
jr z, .pright
ld bc, KEYS_Q
in a, (c)
bit 0, a ; Q
jr z, .pup
ld bc, KEYS_A
in a, (c)
bit 0, a ; A
jr z, .pdown
ret ; nothing held — nothing to do
.pleft:
ld hl, tcol
dec (hl)
jr .pmove
.pright:
ld hl, tcol
inc (hl)
jr .pmove
.pup:
ld hl, trow
dec (hl)
jr .pmove
.pdown:
ld hl, trow
inc (hl)
.pmove:
; Scaffold (route skeleton): a numeric edge clamp so the detour
; cannot walk the lamplighter off the map — past the map's edge
; the address sums leave screen memory for the system's own. The
; walls take this job in unit 9; unit 10 retires the numbers.
ld a, (trow)
cp 2
ret c
cp 23
ret nc
ld a, (tcol)
cp 1
ret c
cp 31
ret nc
; --- commit: restore, step, save, draw — in that order ---
call restore_under
ld a, (tcol)
ld (lamp_col), a
ld a, (trow)
ld (lamp_row), a
call save_under
call draw_lamp
ret
; ----------------------------------------------------------------------------
; fill_ground — the cobble stipple. Not decoration: the stipple is what
; makes ground-state changes visible later, when the game starts
; recolouring these pixels. Rows 1-23 (row 0 is the HUD).
; ----------------------------------------------------------------------------
fill_ground:
ld b, 1 ; rows 1-23 (row 0 is the HUD)
.fgr:
ld c, 0
.fgc:
ld de, cobble_tex
call blit_tex
inc c
ld a, c
cp 32
jr c, .fgc
inc b
ld a, b
cp 24
jr c, .fgr
ret
; fill_walls — brickwork. Driven by the wall attribute bit, so anything
; painted as wall — now or later in the game — gets its brick for free:
; the map itself decides where the brick goes.
fill_walls:
ld b, 1
.fwr:
ld c, 0
.fwc:
push bc
call attr_addr_cr
bit WALL_BIT, (hl)
pop bc
jr z, .fwn
ld de, brick_tex
call blit_tex
.fwn:
inc c
ld a, c
cp 32
jr c, .fwc
inc b
ld a, b
cp 24
jr c, .fwr
ret
; blit_tex — write the 8-byte texture at DE into cell (C, B)'s bitmap.
; scr_addr_cr finds the cell's first pixel row; INC H steps down the
; other seven, 256 bytes apart.
blit_tex:
push bc
call scr_addr_cr
ld b, 8
.bt:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .bt
pop bc
ret
cobble_tex:
defb %10000010
defb %00000000
defb %00001000
defb %00000000
defb %00100001
defb %00000000
defb %00010000
defb %00000000
brick_tex:
; mortar courses with staggered verticals — dusk-lit stone
defb %00001000
defb %00001000
defb %00001000
defb %11111111
defb %10000000
defb %10000000
defb %10000000
defb %11111111
; paint_buildings — walk the rectangle table: each entry is col, row,
; width, height; $FF ends the list. Every cell inside a rectangle gets
; the WALL attribute — and because fill_walls textures by the wall bit,
; the brickwork arrives without another line of drawing code.
paint_buildings:
ld hl, bldg_data
.pb:
ld a, (hl)
cp $FF
ret z
ld c, a ; col
inc hl
ld b, (hl) ; row
inc hl
ld d, (hl) ; width
inc hl
ld e, (hl) ; height
inc hl
push hl
.pbrow:
push bc
push de
.pbcol:
push bc
push de
call attr_addr_cr
ld (hl), WALL
pop de
pop bc
inc c
dec d
jr nz, .pbcol
pop de
pop bc
inc b
dec e
jr nz, .pbrow
pop hl
jr .pb
bldg_data:
defb 5, 5, 4, 3
defb 23, 5, 4, 3
defb $FF
; ----------------------------------------------------------------------------
; scr_addr_cr — HL = bitmap address of cell (C, B)'s first pixel row.
; The row's top two bits pick the third of the screen (H), its bottom
; three become L's top bits, and the column fills L's low five.
; ----------------------------------------------------------------------------
scr_addr_cr:
ld a, b
and %00011000 ; the third (row bits 4-3) ...
or %01000000 ; ... under the screen base $40xx
ld h, a
ld a, b
and %00000111 ; the char row within the third ...
rrca ; ... rotated into bits 7-5
rrca
rrca
or c ; the column in bits 4-0
ld l, a
ret
; attr_addr_cr — HL = attribute address of cell (C, B):
; $5800 + row*32 + col, the row shifted up five times.
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
; ----------------------------------------------------------------------------
; The lamplighter's save / restore / draw.
; ----------------------------------------------------------------------------
; pos_bc — the lamplighter's cell into (C, B), read fresh from the data.
pos_bc:
ld a, (lamp_row)
ld b, a
ld a, (lamp_col)
ld c, a
ret
; save_under — copy the nine bytes of his cell into the buffer: eight
; bitmap rows, then the attribute. Runs as he ARRIVES, before the
; draw — so the buffer always holds true ground, never him.
save_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.su:
ld a, (hl)
ld (de), a
inc de
inc h
djnz .su
call pos_bc
call attr_addr_cr
ld a, (hl)
ld (under_lamp + 8), a
ret
; restore_under — the same nine bytes back the other way: the ground
; returns exactly as it was. Runs as he LEAVES, while the position
; still points at the old cell.
restore_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.ru:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .ru
call pos_bc
call attr_addr_cr
ld a, (under_lamp + 8)
ld (hl), a
ret
draw_lamp:
; his colour first: the cell's attribute becomes his own —
; bright white on the black, his own light about him
call pos_bc
call attr_addr_cr
ld (hl), LAMP_ATTR
; then his shape, eight bytes down the cell like any texture
call pos_bc
call scr_addr_cr
ld de, lamplighter
ld b, 8
.dl:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dl
ret
; ----------------------------------------------------------------------------
; Data.
; ----------------------------------------------------------------------------
lamp_col:
defb START_COL
lamp_row:
defb START_ROW
tcol:
defb 0
trow:
defb 0
player_timer:
defb 0
under_lamp:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
end start

Now walk up and hold O, straight at the nearer building:

Two things are true in that clip, and they’re both worth saying. The building isn’t solid, because nothing in the move ever asks about it — walls are exactly as solid as the code that checks for them, which is currently no code at all. And his passage didn’t scar it, because Unit 8’s buffer is honestly indifferent to what it preserves. You built that machinery over stipple; it just restored brickwork without a single change.
The bit that was always waiting
So how does the move ask? The answer was planted in Unit 1. Look at the two attribute bytes this map is made of:
Every solid cell on this screen — frame and buildings alike — has bit 3 set; every walkable cell has it clear. There’s no need for a table of what’s solid, no list of wall coordinates: the colour you painted is the collision map. One helper reads it:
wall_at:
call attr_addr_cr ; HL = attribute of cell (C, B)
bit WALL_BIT, (hl) ; bit 3: set on brick, clear on floor
ret ; NZ = wall, Z = floor
Three instructions, and the second one is the entire collision system.
Milestone 2 — the veto
Unit 6 built the move as propose-veto-commit and admitted the veto slot was empty ceremony. Four units later, here’s the tenant. At the top of .pmove, before anything commits: load the target cell into (C, B), call wall_at, and if the answer is brick — ret nz — the proposal dies. He doesn’t bounce, doesn’t flash; the step simply never happens.
| 211 | 211 | ld hl, trow | |
| 212 | 212 | inc (hl) | |
| 213 | 213 | .pmove: | |
| 214 | + | ; The veto: ask the target cell's attribute whether it's wall. | |
| 215 | + | ; NZ means brick — the proposal dies here and he stays put. | |
| 216 | + | ld a, (trow) | |
| 217 | + | ld b, a | |
| 218 | + | ld a, (tcol) | |
| 219 | + | ld c, a | |
| 220 | + | call wall_at | |
| 221 | + | ret nz | |
| 222 | + | | |
| 214 | 223 | ; Scaffold (route skeleton): a numeric edge clamp so the detour | |
| 215 | 224 | ; cannot walk the lamplighter off the map — past the map's edge | |
| 216 | 225 | ; the address sums leave screen memory for the system's own. The | |
| ... | |||
| 402 | 411 | ld e, a | |
| 403 | 412 | ld d, 0 | |
| 404 | 413 | add hl, de | |
| 414 | + | ret | |
| 415 | + | | |
| 416 | + | ; wall_at — is cell (C, B) wall? The answer is already on the screen: | |
| 417 | + | ; every wall cell's attribute has WALL_BIT set, so one bit-test of | |
| 418 | + | ; attribute memory is the whole collision system. NZ = wall. | |
| 419 | + | wall_at: | |
| 420 | + | call attr_addr_cr | |
| 421 | + | bit WALL_BIT, (hl) | |
| 405 | 422 | ret | |
| 406 | 423 | | |
| 407 | 424 | ; ---------------------------------------------------------------------------- |
The complete program
; Gloaming — Unit 9: Walls
; Cumulative build; every step runs on its own. Narrative: the unit page.
; Collision is a bit-test on the attribute; buildings are rectangle data.
org 32768
COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
WALL_BIT equ 3 ; the attribute bit that says "this is wall"
LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — his own light
START_COL equ 15 ; where the lamplighter begins
START_ROW equ 11
PLAYER_REPEAT equ 6 ; frames between steps while a key is held
KEYS_OP equ $DFFE ; half-row P O I U Y — bits 1 and 0
KEYS_Q equ $FBFE ; half-row Q W E R T — bit 0 is Q
KEYS_A equ $FDFE ; half-row A S D F G — bit 0 is A
start:
; --- the border goes black — the night beyond the square ---
; Port $FE bits 0-2 set the BORDER colour. A = 0 = black.
ld a, 0
out ($FE), a
; --- place the lamplighter ---
; His position is data. Everything that draws him reads it.
ld a, START_COL
ld (lamp_col), a
ld a, START_ROW
ld (lamp_row), a
xor a
ld (player_timer), a
; --- wipe the canvas ---
; The bitmap ($4000-$57FF) is the pixel layer; whatever was on
; screen before us still lives there. Zero it so only our
; attribute colours show.
call clear_bitmap
; --- texture the ground ---
; Blit the cobble stipple into every cell's bitmap, rows 1-23.
; The attributes will colour these pixels in a moment.
call fill_ground
; --- wash in the cobbles ---
; Seed the first attribute cell, point DE one cell ahead, and
; let LDIR cascade the byte through all 768 cells.
ld hl, $5800
ld de, $5801
ld (hl), COBBLE
ld bc, 767
ldir
call paint_walls
call paint_buildings
; --- brick the walls ---
; Now that the wall cells are painted, fill_walls can read the
; map back and lay brick wherever the wall bit is set.
call fill_walls
; save what he is about to stand on, BEFORE the first draw
call save_under
call draw_lamp
; --- start the heartbeat ---
; IM 1: every 50 Hz frame interrupt calls the ROM's handler.
; EI: let it. HALT then sleeps until the next frame arrives,
; so the loop below beats exactly once per frame.
im 1
ei
main_loop:
halt
call play_step
jr main_loop
; play_step — one beat of the game: ask the keyboard.
play_step:
call player_step
ret
; ----------------------------------------------------------------------------
; paint_walls — the square's edge, one attribute write per cell.
; ----------------------------------------------------------------------------
paint_walls:
ld c, WALL ; the byte every wall cell gets
; the top wall: row 1 is 32 cells in a row from $5820
; (row 0 is kept back — it becomes the HUD later)
ld hl, $5820
ld b, 32
.wt:
ld (hl), c
inc hl
djnz .wt
; the bottom wall: row 23, 32 cells from $5AE0
ld hl, $5AE0
ld b, 32
.wb:
ld (hl), c
inc hl
djnz .wb
; the side walls: column 0 and column 31 of rows 1-23.
; Write the row's first cell, hop 31 cells to its last,
; then step a full row (32) down — 23 times.
ld hl, $5820
ld b, 23
.ws:
ld (hl), c
push hl
ld de, 31
add hl, de
ld (hl), c
pop hl
ld de, 32
add hl, de
djnz .ws
ret
; ----------------------------------------------------------------------------
; clear_bitmap — zero the pixel layer, $4000-$57FF, with the same
; seed-and-cascade LDIR idiom the cobble wash uses.
; ----------------------------------------------------------------------------
clear_bitmap:
ld hl, $4000
ld de, $4001
ld (hl), 0
ld bc, 6143
ldir
ret
; ----------------------------------------------------------------------------
; player_step — the keys become movement. Each direction key edits a
; TARGET position (tcol, trow) — a proposal, not yet a move — so it
; can be vetoed before it becomes real. Then the move commits: leave
; the old cell, take the new one, draw.
; ----------------------------------------------------------------------------
player_step:
; --- propose: the target starts where he stands ---
ld a, (lamp_col)
ld (tcol), a
ld a, (lamp_row)
ld (trow), a
; The held-key gate: the first press steps at once, then one
; step every PLAYER_REPEAT frames. Releasing every direction
; key re-arms the instant first step, so taps stay crisp.
ld bc, KEYS_OP
in a, (c)
cpl
and %00000011
ld e, a
ld bc, KEYS_Q
in a, (c)
cpl
and %00000001
or e
ld e, a
ld bc, KEYS_A
in a, (c)
cpl
and %00000001
or e
jr nz, .held
xor a
ld (player_timer), a
ret
.held:
ld a, (player_timer)
or a
jr z, .stepnow
dec a
ld (player_timer), a
ret
.stepnow:
ld a, PLAYER_REPEAT
ld (player_timer), a
ld bc, KEYS_OP
in a, (c)
bit 1, a ; O — a zero bit is a pressed key
jr z, .pleft
bit 0, a ; P, same half-row
jr z, .pright
ld bc, KEYS_Q
in a, (c)
bit 0, a ; Q
jr z, .pup
ld bc, KEYS_A
in a, (c)
bit 0, a ; A
jr z, .pdown
ret ; nothing held — nothing to do
.pleft:
ld hl, tcol
dec (hl)
jr .pmove
.pright:
ld hl, tcol
inc (hl)
jr .pmove
.pup:
ld hl, trow
dec (hl)
jr .pmove
.pdown:
ld hl, trow
inc (hl)
.pmove:
; The veto: ask the target cell's attribute whether it's wall.
; NZ means brick — the proposal dies here and he stays put.
ld a, (trow)
ld b, a
ld a, (tcol)
ld c, a
call wall_at
ret nz
; Scaffold (route skeleton): a numeric edge clamp so the detour
; cannot walk the lamplighter off the map — past the map's edge
; the address sums leave screen memory for the system's own. The
; walls take this job in unit 9; unit 10 retires the numbers.
ld a, (trow)
cp 2
ret c
cp 23
ret nc
ld a, (tcol)
cp 1
ret c
cp 31
ret nc
; --- commit: restore, step, save, draw — in that order ---
call restore_under
ld a, (tcol)
ld (lamp_col), a
ld a, (trow)
ld (lamp_row), a
call save_under
call draw_lamp
ret
; ----------------------------------------------------------------------------
; fill_ground — the cobble stipple. Not decoration: the stipple is what
; makes ground-state changes visible later, when the game starts
; recolouring these pixels. Rows 1-23 (row 0 is the HUD).
; ----------------------------------------------------------------------------
fill_ground:
ld b, 1 ; rows 1-23 (row 0 is the HUD)
.fgr:
ld c, 0
.fgc:
ld de, cobble_tex
call blit_tex
inc c
ld a, c
cp 32
jr c, .fgc
inc b
ld a, b
cp 24
jr c, .fgr
ret
; fill_walls — brickwork. Driven by the wall attribute bit, so anything
; painted as wall — now or later in the game — gets its brick for free:
; the map itself decides where the brick goes.
fill_walls:
ld b, 1
.fwr:
ld c, 0
.fwc:
push bc
call attr_addr_cr
bit WALL_BIT, (hl)
pop bc
jr z, .fwn
ld de, brick_tex
call blit_tex
.fwn:
inc c
ld a, c
cp 32
jr c, .fwc
inc b
ld a, b
cp 24
jr c, .fwr
ret
; blit_tex — write the 8-byte texture at DE into cell (C, B)'s bitmap.
; scr_addr_cr finds the cell's first pixel row; INC H steps down the
; other seven, 256 bytes apart.
blit_tex:
push bc
call scr_addr_cr
ld b, 8
.bt:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .bt
pop bc
ret
cobble_tex:
defb %10000010
defb %00000000
defb %00001000
defb %00000000
defb %00100001
defb %00000000
defb %00010000
defb %00000000
brick_tex:
; mortar courses with staggered verticals — dusk-lit stone
defb %00001000
defb %00001000
defb %00001000
defb %11111111
defb %10000000
defb %10000000
defb %10000000
defb %11111111
; paint_buildings — walk the rectangle table: each entry is col, row,
; width, height; $FF ends the list. Every cell inside a rectangle gets
; the WALL attribute — and because fill_walls textures by the wall bit,
; the brickwork arrives without another line of drawing code.
paint_buildings:
ld hl, bldg_data
.pb:
ld a, (hl)
cp $FF
ret z
ld c, a ; col
inc hl
ld b, (hl) ; row
inc hl
ld d, (hl) ; width
inc hl
ld e, (hl) ; height
inc hl
push hl
.pbrow:
push bc
push de
.pbcol:
push bc
push de
call attr_addr_cr
ld (hl), WALL
pop de
pop bc
inc c
dec d
jr nz, .pbcol
pop de
pop bc
inc b
dec e
jr nz, .pbrow
pop hl
jr .pb
bldg_data:
defb 5, 5, 4, 3
defb 23, 5, 4, 3
defb $FF
; ----------------------------------------------------------------------------
; scr_addr_cr — HL = bitmap address of cell (C, B)'s first pixel row.
; The row's top two bits pick the third of the screen (H), its bottom
; three become L's top bits, and the column fills L's low five.
; ----------------------------------------------------------------------------
scr_addr_cr:
ld a, b
and %00011000 ; the third (row bits 4-3) ...
or %01000000 ; ... under the screen base $40xx
ld h, a
ld a, b
and %00000111 ; the char row within the third ...
rrca ; ... rotated into bits 7-5
rrca
rrca
or c ; the column in bits 4-0
ld l, a
ret
; attr_addr_cr — HL = attribute address of cell (C, B):
; $5800 + row*32 + col, the row shifted up five times.
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
; wall_at — is cell (C, B) wall? The answer is already on the screen:
; every wall cell's attribute has WALL_BIT set, so one bit-test of
; attribute memory is the whole collision system. NZ = wall.
wall_at:
call attr_addr_cr
bit WALL_BIT, (hl)
ret
; ----------------------------------------------------------------------------
; The lamplighter's save / restore / draw.
; ----------------------------------------------------------------------------
; pos_bc — the lamplighter's cell into (C, B), read fresh from the data.
pos_bc:
ld a, (lamp_row)
ld b, a
ld a, (lamp_col)
ld c, a
ret
; save_under — copy the nine bytes of his cell into the buffer: eight
; bitmap rows, then the attribute. Runs as he ARRIVES, before the
; draw — so the buffer always holds true ground, never him.
save_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.su:
ld a, (hl)
ld (de), a
inc de
inc h
djnz .su
call pos_bc
call attr_addr_cr
ld a, (hl)
ld (under_lamp + 8), a
ret
; restore_under — the same nine bytes back the other way: the ground
; returns exactly as it was. Runs as he LEAVES, while the position
; still points at the old cell.
restore_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.ru:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .ru
call pos_bc
call attr_addr_cr
ld a, (under_lamp + 8)
ld (hl), a
ret
draw_lamp:
; his colour first: the cell's attribute becomes his own —
; bright white on the black, his own light about him
call pos_bc
call attr_addr_cr
ld (hl), LAMP_ATTR
; then his shape, eight bytes down the cell like any texture
call pos_bc
call scr_addr_cr
ld de, lamplighter
ld b, 8
.dl:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dl
ret
; ----------------------------------------------------------------------------
; Data.
; ----------------------------------------------------------------------------
lamp_col:
defb START_COL
lamp_row:
defb START_ROW
tcol:
defb 0
trow:
defb 0
player_timer:
defb 0
under_lamp:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
end start
Same walk as the ghost — up five, then hold O at the building:

Notice what the video is really showing: the veto never traps him, because it judges one proposed cell at a time and a new direction is a new proposal. That’s the difference between collision and a cage — and it’s why the propose-veto-commit shape was worth building before anything needed it.
One honest note about the frame. The perimeter walls are now solid too — the same bit, the same test — but you can’t see it yet, because the scaffold clamp still stops him one cell earlier, exactly where the walls stand. The clamp has just become redundant; it hasn’t yet become gone. Proving the walls hold on their own is the next unit’s whole job.
When it’s wrong, see why
Collision bugs are about which cell you test and which way the bit reads:
- He still walks through everything. The branch is inverted. A wall’s bit 3 is set, so
wall_atcomes back NZ — andret nzmust abandon the move.ret zgives you a world where only walls are walkable, which is memorable but wrong. - He won’t move at all. You’re testing his current cell instead of the target — his own cell is never a wall, but if the
(C, B)load readslamp_row/lamp_colrather thantrow/tcol, other mistakes (a stale attribute under him, say) surface here. The veto asks about where he’s going. - The veto works east-west but not north-south (or the reverse). The load order into
wall_atis swapped — row belongs in B, column in C, the same convention every address routine has used since Unit 2. Swapped, you’re testing a mirrored cell somewhere else on the map. - He stops one cell early, everywhere. That’s not the veto — that’s the clamp, still doing its scaffold job at the map’s edge. At the buildings, one-cell-early means the target calculation nudged before the test; check the veto reads the freshly-nudged
tcol/trow.
Before and after
The square began this unit as scenery and ended it as geography. Two buildings arrived as eight bytes of rectangle data; a three-instruction helper turned their paint into stone; and Unit 6’s veto slot, empty since the day it was built, finally has its tenant. Nothing needed a new table or a new concept — the collision map has been sitting in attribute memory since Unit 1, waiting for someone to read it. And the step-1 ghost proved something quietly important about last unit, too: machinery built honestly doesn’t care what you point it at.
Try this: knock a doorway
In setup, after call paint_buildings, repaint one cell of the first building’s east face as floor:
ld hl, $5800 + 6*32 + 8
ld (hl), COBBLE
Walk up five and hold O, the pinning walk — and this time he steps into the building through the gap, then pins against the interior wall one cell in. A doorway, and you didn’t touch the collision code: wall_at reads the live attribute, and that cell now says floor. The map and the collision were never separate things.
Try this: grow the town
Add a third building to bldg_data — say defb 12, 15, 8, 2, a long low terrace south of centre — and rebuild. It’s painted, textured, and solid, and you wrote no new code at all. That’s the data-table contract: the routine is finished, the town isn’t.
Try this: take the veto out
Comment out the call wall_at / ret nz pair and walk at a building. You’re back to the ghost — those two lines are the entire difference between a picture of a town and a town. Put them back, and as you do, notice how the ghost never once corrupted the screen: solidity and safety turned out to be different jobs, done by different units.
What you’ve learnt
- Level geometry as data: rectangles in a table, one painter routine,
$FFto finish — the code is done, the map is editable. - Looking solid and being solid are unrelated — solidity exists only where code checks for it.
- The attribute byte doubles as the collision map: one
BITtest of the target cell answers “can I go there?”. - The veto completes propose-veto-commit: test the target before the commit, and refuse by simply not moving.
- A veto is per-proposal, not per-player — blocked in one direction, he’s free in the other three.
What’s next
The walls are solid; the scaffold that impersonated them is now dead weight. In Unit 10 the numeric clamp comes out, the walls take the boundary job for real — and the movement core converges, byte for byte, with the game this module is building toward. The detour’s last trace disappears.