One Step
The four keys become four directions: propose a target cell, clamp it to the map, then move — first without erasing (he smears), then with the naive erase (the floor pays). Honest movement, with its costs on show.
The machine feels you; now the touch has to move him. Hold P and the lamplighter steps right, O left, Q up, A down — cell by cell across the square. The reads are last unit’s, unchanged. What’s new is what a step is — and this unit builds it wrong twice, on purpose, because both failures teach something the fix alone never would.
A move is a proposal
Unit 3 made his position data: lamp_col and lamp_row are the only place he is. So moving him is editing two bytes — but not directly. The keys edit a target first:
tcol: defb 0 ; where he's trying to go
trow: defb 0
Each frame, the target starts as a copy of where he stands; a held key nudges it one cell in its direction; and only then, after anything that wants to object has had its say, does the target become his real position. That shape — propose, veto, commit — looks like ceremony today, when the only objector is a boundary check. It is the load-bearing frame of the whole game: walls will veto moves in Unit 9, and the draught will make its own proposals in Unit 16, all through these two bytes.
The scaffold at the edge
One objector exists from the very first step, and honesty demands we name it. Nothing yet stops the lamplighter at the walls — they’re paint, not physics — and past the map’s edge lies real trouble: Unit 2 taught you the address arithmetic, so you can see it — a row past 23 walks the attribute sum out of $5800’s table and into memory the system owns. A dozen held frames would crash the machine.
So the move commits only inside a numeric fence: rows 2–22, columns 1–30. This clamp is scaffolding, and it’s marked as such in the source — the walls take over the job in Unit 9, and Unit 10 takes the numbers down. Until then, the fence keeps the detour safe.
Milestone 1 — move without leaving
Build the move the naive way: decode the four keys into four nudges, clamp, commit, draw at the new cell. Nothing else. In particular — don’t clean up.
| 8 | 8 | WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone | |
| 9 | 9 | WALL_BIT equ 3 ; the attribute bit that says "this is wall" | |
| 10 | 10 | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — his own light | |
| 11 | - | LAMP_GLOW equ %01000110 ; his warmth while a key is down (this unit only) | |
| 12 | 11 | | |
| 13 | 12 | START_COL equ 15 ; where the lamplighter begins | |
| 14 | 13 | START_ROW equ 11 | |
| ... | |||
| 128 | 127 | ret | |
| 129 | 128 | | |
| 130 | 129 | ; ---------------------------------------------------------------------------- | |
| 131 | - | ; player_step — scan the keyboard. Reading port $FE with a half-row | |
| 132 | - | ; address in B selects five keys; a key held pulls its bit LOW. While | |
| 133 | - | ; any direction key is down, the lamplighter glows — proof the | |
| 134 | - | ; machine can feel you. | |
| 130 | + | ; player_step — the keys become movement. Each direction key edits a | |
| 131 | + | ; TARGET position (tcol, trow) — a proposal, not yet a move — so it | |
| 132 | + | ; can be vetoed before it becomes real. Then the move commits: leave | |
| 133 | + | ; the old cell, take the new one, draw. | |
| 135 | 134 | ; ---------------------------------------------------------------------------- | |
| 136 | 135 | player_step: | |
| 136 | + | ; --- propose: the target starts where he stands --- | |
| 137 | + | ld a, (lamp_col) | |
| 138 | + | ld (tcol), a | |
| 139 | + | ld a, (lamp_row) | |
| 140 | + | ld (trow), a | |
| 141 | + | | |
| 137 | 142 | ld bc, KEYS_OP | |
| 138 | 143 | in a, (c) | |
| 139 | 144 | bit 1, a ; O — a zero bit is a pressed key | |
| 140 | - | jr z, .held | |
| 145 | + | jr z, .pleft | |
| 141 | 146 | bit 0, a ; P, same half-row | |
| 142 | - | jr z, .held | |
| 147 | + | jr z, .pright | |
| 143 | 148 | ld bc, KEYS_Q | |
| 144 | 149 | in a, (c) | |
| 145 | 150 | bit 0, a ; Q | |
| 146 | - | jr z, .held | |
| 151 | + | jr z, .pup | |
| 147 | 152 | ld bc, KEYS_A | |
| 148 | 153 | in a, (c) | |
| 149 | 154 | bit 0, a ; A | |
| 150 | - | jr z, .held | |
| 151 | - | call pos_bc | |
| 152 | - | call attr_addr_cr | |
| 153 | - | ld (hl), LAMP_ATTR | |
| 154 | - | ret | |
| 155 | - | .held: | |
| 156 | - | call pos_bc | |
| 157 | - | call attr_addr_cr | |
| 158 | - | ld (hl), LAMP_GLOW | |
| 155 | + | jr z, .pdown | |
| 156 | + | ret ; nothing held — nothing to do | |
| 157 | + | | |
| 158 | + | .pleft: | |
| 159 | + | ld hl, tcol | |
| 160 | + | dec (hl) | |
| 161 | + | jr .pmove | |
| 162 | + | .pright: | |
| 163 | + | ld hl, tcol | |
| 164 | + | inc (hl) | |
| 165 | + | jr .pmove | |
| 166 | + | .pup: | |
| 167 | + | ld hl, trow | |
| 168 | + | dec (hl) | |
| 169 | + | jr .pmove | |
| 170 | + | .pdown: | |
| 171 | + | ld hl, trow | |
| 172 | + | inc (hl) | |
| 173 | + | .pmove: | |
| 174 | + | ; Scaffold (route skeleton): a numeric edge clamp so the detour | |
| 175 | + | ; cannot walk the lamplighter off the map — past the map's edge | |
| 176 | + | ; the address sums leave screen memory for the system's own. The | |
| 177 | + | ; walls take this job in unit 9; unit 10 retires the numbers. | |
| 178 | + | ld a, (trow) | |
| 179 | + | cp 2 | |
| 180 | + | ret c | |
| 181 | + | cp 23 | |
| 182 | + | ret nc | |
| 183 | + | ld a, (tcol) | |
| 184 | + | cp 1 | |
| 185 | + | ret c | |
| 186 | + | cp 31 | |
| 187 | + | ret nc | |
| 188 | + | ; --- commit: the target becomes his position, and he's drawn --- | |
| 189 | + | ld a, (tcol) | |
| 190 | + | ld (lamp_col), a | |
| 191 | + | ld a, (trow) | |
| 192 | + | ld (lamp_row), a | |
| 193 | + | call draw_lamp | |
| 159 | 194 | ret | |
| 160 | 195 | | |
| 161 | 196 | ; ---------------------------------------------------------------------------- | |
| ... | |||
| 321 | 356 | defb START_COL | |
| 322 | 357 | lamp_row: | |
| 323 | 358 | defb START_ROW | |
| 359 | + | tcol: | |
| 360 | + | defb 0 | |
| 361 | + | trow: | |
| 362 | + | defb 0 | |
| 324 | 363 | | |
| 325 | 364 | lamplighter: | |
| 326 | 365 | defb %00111100 |
The complete step 1 program
; Gloaming — Unit 6: One Step
; Cumulative build; every step runs on its own. Narrative: the unit page.
; The keys become movement: propose a target, clamp it, then move and draw.
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
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
; --- 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
; --- 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
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
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: the target becomes his position, and he's drawn ---
ld a, (tcol)
ld (lamp_col), a
ld a, (trow)
ld (lamp_row), a
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
; ----------------------------------------------------------------------------
; 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 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
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
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
end start
Hold P for a moment, then A:

Of course. The screen is memory, and memory holds what it’s given — drawing him somewhere new never undrew him. A picture just accumulates; a moving figure has to leave. That’s not a bug in the drawing, it’s a missing half of the move.
Milestone 2 — the naive erase
So: before the commit, blank the cell he’s standing in. erase_lamp zeroes the cell’s eight bitmap bytes and paints its attribute back to ground colour — then the move proceeds as before.
| 185 | 185 | ret c | |
| 186 | 186 | cp 31 | |
| 187 | 187 | ret nc | |
| 188 | - | ; --- commit: the target becomes his position, and he's drawn --- | |
| 188 | + | ; --- commit: erase where he was, move, draw where he is --- | |
| 189 | + | call erase_lamp | |
| 189 | 190 | ld a, (tcol) | |
| 190 | 191 | ld (lamp_col), a | |
| 191 | 192 | ld a, (trow) | |
| ... | |||
| 327 | 328 | ld b, a | |
| 328 | 329 | ld a, (lamp_col) | |
| 329 | 330 | ld c, a | |
| 331 | + | ret | |
| 332 | + | | |
| 333 | + | ; erase_lamp — the naive erase: blank the cell the lamplighter leaves. | |
| 334 | + | ; Zero its eight bitmap bytes, repaint it ground colour. The cell is | |
| 335 | + | ; clean — and whatever the floor had there is gone with him. | |
| 336 | + | ; (Detour: watch what it does to the cobbles.) | |
| 337 | + | erase_lamp: | |
| 338 | + | call pos_bc | |
| 339 | + | call scr_addr_cr | |
| 340 | + | ld b, 8 | |
| 341 | + | xor a | |
| 342 | + | .el: | |
| 343 | + | ld (hl), a | |
| 344 | + | inc h | |
| 345 | + | djnz .el | |
| 346 | + | call pos_bc | |
| 347 | + | call attr_addr_cr | |
| 348 | + | ld (hl), COBBLE | |
| 330 | 349 | ret | |
| 331 | 350 | | |
| 332 | 351 | draw_lamp: |
The complete program
; Gloaming — Unit 6: One Step
; Cumulative build; every step runs on its own. Narrative: the unit page.
; The keys become movement: propose a target, clamp it, then move and draw.
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
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
; --- 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
; --- 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
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
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: erase where he was, move, draw where he is ---
call erase_lamp
ld a, (tcol)
ld (lamp_col), a
ld a, (trow)
ld (lamp_row), a
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
; ----------------------------------------------------------------------------
; 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 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
; erase_lamp — the naive erase: blank the cell the lamplighter leaves.
; Zero its eight bitmap bytes, repaint it ground colour. The cell is
; clean — and whatever the floor had there is gone with him.
; (Detour: watch what it does to the cobbles.)
erase_lamp:
call pos_bc
call scr_addr_cr
ld b, 8
xor a
.el:
ld (hl), a
inc h
djnz .el
call pos_bc
call attr_addr_cr
ld (hl), COBBLE
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
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
end start
One lamplighter now, no copies. And look at the floor:

This is the second honest failure, and the subtler one. The erase doesn’t restore the cell — it destroys it and repaints an approximation: right attribute, blank canvas. On a plain floor you’d never see the difference; the course you’re in once shipped exactly this code over a plain floor, and the flaw was invisible. Your floor has texture, so the flaw has a shape: a gouged trail everywhere he’s been. The move “works” only because the ground is uniform — the moment the floor holds anything worth keeping, blanking it is vandalism. Hold that thought for two units; Unit 8 repays it properly.
One more thing you can’t have missed: he’s fast. One cell per frame is fifty cells a second — a held key crosses the whole square in just over half a second. When this game was first playtested, the verdict came back in capitals: “WAY TOO FAST.” That’s Unit 7’s problem.
When it’s wrong, see why
- He smears even with the erase in. Order. The erase must run while
lamp_col/lamp_rowstill point at the old cell — erase, then update the position, then draw. Erase after the update and he blanks the cell he just arrived in. - He vanishes entirely. Same family: if the erase runs after the position update and the draw, the last thing written is a blank — he’s rubbed out every frame.
- One direction works, another doesn’t. The decode. Each key’s
bittest jumps to its own nudge; check each key tests the right bit of the right half-row (P and O share$DFFE; Q and A have their own rows). - He sticks at an invisible fence one cell short of the wall. Correct! That’s the clamp — rows 2–22, columns 1–30, chosen to match where the walls will stand. If he sticks somewhere else, check the four compares:
cp 2 / ret crejects row 1 and below,cp 23 / ret ncrejects row 23 and beyond. - Garbage appears at the screen edge, or the machine dies. The clamp is missing or its bounds are wrong — the address sums have left the screen. Put the fence back; this is precisely what it’s for.
Before and after
The keys stopped being a glow and became a walk: propose a target, clamp it, commit it, draw. You built the move wrong twice and watched each failure announce itself — the smear taught that a mover must leave; the gouge taught that how it leaves matters, and that “blank it” is an answer that only survives on an empty floor. Both lessons came free because the canvas came first. And underneath the failures, the propose-veto-commit shape is in place — the frame every mover in this game will use from now on.
Try this: trace the fence
Walk him slowly around the whole edge of the square — as far up, down, left and right as he’ll go. He stops one cell short of the wall on every side. That’s the clamp’s geometry, deliberately matching where the walls will take over in Unit 9 — when they do, nothing about the feel will change, and that’s the point.
Try this: walk off the world
In step 2, change the clamp’s cp 23 to cp 25 and hold A. He walks into the bottom wall and one row beyond — and the screen’s bottom edge erupts in wrong colours, because at row 24 the bitmap address arithmetic wraps his glyph bytes into the attribute table: his pixels are being written as paint. Now delete the two row checks entirely and hold A again. He marches on through the printer buffer and, around row 32, into the system variables — and the machine dies. Reset the emulator, put the fence back, and remember what it guards: past the map, every address still lands somewhere.
Try this: feel the speed
Tap P as lightly as you can and count the cells. Two, three, four? A “tap” is several frames, and every frame is a step. Now hold it and watch him hit the fence almost instantly. Fifty steps a second is not a walking pace — it’s why the next unit exists.
What you’ve learnt
- A move is propose → veto → commit: keys edit a target, checks veto it, and only then does state change.
- The clamp is declared scaffolding — a numeric fence standing in for the walls, keeping the address sums inside the screen.
- A moving figure must leave its old cell — drawing doesn’t undraw.
- The naive erase destroys what it can’t restore — visible on any floor that holds something, invisible only on emptiness.
- One step per frame is fifty per second — input needs pacing, not just reading.
What’s next
Two debts stand: he’s uncontrollably fast, and he vandalises the floor. Unit 7 pays the first — a repeat gate that turns “held” into a stride: one press, one step, then a measured pace. The timer idiom it introduces will be reused by the night itself, later, for stalking you.