Save and Restore
Repay the gouge: a nine-byte buffer remembers what the lamplighter stands on — eight bitmap rows and an attribute — and the move becomes a dance with a strict order: restore, step, save, draw.
One debt left, and it’s the older one. Since Unit 6, every step has blanked the cell behind the lamplighter — the gouge is slower now that he’s paced, but it’s still there, cutting bare black through the stipple wherever he walks. The erase can’t be fixed by adjusting it, because its failure isn’t a detail — it’s the whole idea. erase_lamp repaints an approximation of the floor: right attribute, blank canvas. The screen held the only copy of those stippled pixels, he drew over them, and they’re gone.
So stop approximating. Remember.
Nine bytes of memory
A character cell is eight bitmap bytes and one attribute — nine bytes describe everything the screen knows about it. Set aside nine bytes of ordinary memory:
under_lamp:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
The name is the contract: what is under the lamplighter. Before he’s drawn anywhere, copy the cell’s nine bytes into the buffer; when he leaves, copy them back. The floor doesn’t survive because we redrew it — it survives because it was never lost. Two routines do it, and they’re mirror images: save_under reads the screen into the buffer, restore_under writes the buffer onto the screen. Same nine bytes, opposite directions.
The dance
Where those calls go matters more than what they do. Both routines find the cell through pos_bc — they operate on wherever lamp_col/lamp_row currently point — so the commit becomes a strict four-beat sequence:
- Restore — put the ground back, while the position still names the old cell.
- Step — update the position; only now does he officially move.
- Save — the position names the new cell; capture its ground before anything touches it.
- Draw — paint him over the cell whose contents are now safe.
Each beat leans on the one before it. Restore any later and you’d restore the wrong cell; save any later and the buffer would capture him instead of the floor. And there’s a fifth beat hiding in the setup: at the very start, before the first draw_lamp ever runs, the start cell must be saved too — otherwise the buffer’s opening zeroes are the “ground” his first step puts back.
Milestone 1 — the buffer, and a witness
Build all of it — the buffer, both routines, the reordered commit — and add one temporary thing: a marker, a single loud yellow cell painted on the floor in his path, three cells to his left. It’s scaffolding, one attribute write in the setup, and its job is to testify. Anything the old erase crossed was destroyed; if the marker survives being walked over, the floor genuinely comes back.
| 11 | 11 | | |
| 12 | 12 | START_COL equ 15 ; where the lamplighter begins | |
| 13 | 13 | START_ROW equ 11 | |
| 14 | + | MARKER equ %00110110 ; a bright yellow-on-yellow cell (this step only) | |
| 14 | 15 | PLAYER_REPEAT equ 6 ; frames between steps while a key is held | |
| 15 | 16 | | |
| 16 | 17 | KEYS_OP equ $DFFE ; half-row P O I U Y — bits 1 and 0 | |
| ... | |||
| 58 | 59 | ; Now that the wall cells are painted, fill_walls can read the | |
| 59 | 60 | ; map back and lay brick wherever the wall bit is set. | |
| 60 | 61 | call fill_walls | |
| 62 | + | ; a marker on the floor (this step only): one loud cell in his | |
| 63 | + | ; path, to prove that passage now leaves things standing | |
| 64 | + | ld hl, $5800 + 11*32 + 12 | |
| 65 | + | ld (hl), MARKER | |
| 66 | + | ; save what he is about to stand on, BEFORE the first draw | |
| 67 | + | call save_under | |
| 61 | 68 | call draw_lamp | |
| 62 | 69 | | |
| 63 | 70 | ; --- start the heartbeat --- | |
| ... | |||
| 222 | 229 | ret c | |
| 223 | 230 | cp 31 | |
| 224 | 231 | ret nc | |
| 225 | - | ; --- commit: erase where he was, move, draw where he is --- | |
| 226 | - | call erase_lamp | |
| 232 | + | ; --- commit: restore, step, save, draw — in that order --- | |
| 233 | + | call restore_under | |
| 227 | 234 | ld a, (tcol) | |
| 228 | 235 | ld (lamp_col), a | |
| 229 | 236 | ld a, (trow) | |
| 230 | 237 | ld (lamp_row), a | |
| 238 | + | call save_under | |
| 231 | 239 | call draw_lamp | |
| 232 | 240 | ret | |
| 233 | 241 | | |
| ... | |||
| 356 | 364 | ret | |
| 357 | 365 | | |
| 358 | 366 | ; ---------------------------------------------------------------------------- | |
| 359 | - | ; The lamplighter's draw. | |
| 367 | + | ; The lamplighter's save / restore / draw. | |
| 360 | 368 | ; ---------------------------------------------------------------------------- | |
| 361 | 369 | | |
| 362 | 370 | ; pos_bc — the lamplighter's cell into (C, B), read fresh from the data. | |
| ... | |||
| 367 | 375 | ld c, a | |
| 368 | 376 | ret | |
| 369 | 377 | | |
| 370 | - | ; erase_lamp — the naive erase: blank the cell the lamplighter leaves. | |
| 371 | - | ; Zero its eight bitmap bytes, repaint it ground colour. The cell is | |
| 372 | - | ; clean — and whatever the floor had there is gone with him. | |
| 373 | - | ; (Detour: watch what it does to the cobbles.) | |
| 374 | - | erase_lamp: | |
| 378 | + | ; save_under — copy the nine bytes of his cell into the buffer: eight | |
| 379 | + | ; bitmap rows, then the attribute. Runs as he ARRIVES, before the | |
| 380 | + | ; draw — so the buffer always holds true ground, never him. | |
| 381 | + | save_under: | |
| 375 | 382 | call pos_bc | |
| 376 | 383 | call scr_addr_cr | |
| 384 | + | ld de, under_lamp | |
| 377 | 385 | ld b, 8 | |
| 378 | - | xor a | |
| 379 | - | .el: | |
| 386 | + | .su: | |
| 387 | + | ld a, (hl) | |
| 388 | + | ld (de), a | |
| 389 | + | inc de | |
| 390 | + | inc h | |
| 391 | + | djnz .su | |
| 392 | + | call pos_bc | |
| 393 | + | call attr_addr_cr | |
| 394 | + | ld a, (hl) | |
| 395 | + | ld (under_lamp + 8), a | |
| 396 | + | ret | |
| 397 | + | | |
| 398 | + | ; restore_under — the same nine bytes back the other way: the ground | |
| 399 | + | ; returns exactly as it was. Runs as he LEAVES, while the position | |
| 400 | + | ; still points at the old cell. | |
| 401 | + | restore_under: | |
| 402 | + | call pos_bc | |
| 403 | + | call scr_addr_cr | |
| 404 | + | ld de, under_lamp | |
| 405 | + | ld b, 8 | |
| 406 | + | .ru: | |
| 407 | + | ld a, (de) | |
| 380 | 408 | ld (hl), a | |
| 409 | + | inc de | |
| 381 | 410 | inc h | |
| 382 | - | djnz .el | |
| 411 | + | djnz .ru | |
| 383 | 412 | call pos_bc | |
| 384 | 413 | call attr_addr_cr | |
| 385 | - | ld (hl), COBBLE | |
| 414 | + | ld a, (under_lamp + 8) | |
| 415 | + | ld (hl), a | |
| 386 | 416 | ret | |
| 387 | 417 | | |
| 388 | 418 | draw_lamp: | |
| ... | |||
| 418 | 448 | defb 0 | |
| 419 | 449 | player_timer: | |
| 420 | 450 | defb 0 | |
| 451 | + | | |
| 452 | + | under_lamp: | |
| 453 | + | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 421 | 454 | | |
| 422 | 455 | lamplighter: | |
| 423 | 456 | defb %00111100 |
The complete step 1 program
; Gloaming — Unit 8: Save and Restore
; Cumulative build; every step runs on its own. Narrative: the unit page.
; The under-buffer: remember nine bytes, and the ground survives his passage.
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
MARKER equ %00110110 ; a bright yellow-on-yellow cell (this step only)
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
; --- 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
; a marker on the floor (this step only): one loud cell in his
; path, to prove that passage now leaves things standing
ld hl, $5800 + 11*32 + 12
ld (hl), MARKER
; 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
; ----------------------------------------------------------------------------
; 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 hold O and walk him straight through it:

Watch the crossing closely and you can see the mechanism’s honesty: while he stands on the marker, it’s gone — the cell holds him, and the yellow lives only in the buffer. The screen shows one thing per cell; memory is where the other thing waits.
Milestone 2 — strike the scaffolding
The marker has testified; take it down. Removing it changes nothing about the machinery — which is the point of scaffolding.
| 11 | 11 | | |
| 12 | 12 | START_COL equ 15 ; where the lamplighter begins | |
| 13 | 13 | START_ROW equ 11 | |
| 14 | - | MARKER equ %00110110 ; a bright yellow-on-yellow cell (this step only) | |
| 15 | 14 | PLAYER_REPEAT equ 6 ; frames between steps while a key is held | |
| 16 | 15 | | |
| 17 | 16 | KEYS_OP equ $DFFE ; half-row P O I U Y — bits 1 and 0 | |
| ... | |||
| 59 | 58 | ; Now that the wall cells are painted, fill_walls can read the | |
| 60 | 59 | ; map back and lay brick wherever the wall bit is set. | |
| 61 | 60 | call fill_walls | |
| 62 | - | ; a marker on the floor (this step only): one loud cell in his | |
| 63 | - | ; path, to prove that passage now leaves things standing | |
| 64 | - | ld hl, $5800 + 11*32 + 12 | |
| 65 | - | ld (hl), MARKER | |
| 66 | 61 | ; save what he is about to stand on, BEFORE the first draw | |
| 67 | 62 | call save_under | |
| 68 | 63 | call draw_lamp |
The complete program
; Gloaming — Unit 8: Save and Restore
; Cumulative build; every step runs on its own. Narrative: the unit page.
; The under-buffer: remember nine bytes, and the ground survives his passage.
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
; --- 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
; ----------------------------------------------------------------------------
; 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
And now the debt is formally repaid. Here is the same walk that carved Unit 6’s gouge — right, then down — at the gated pace:

This is how the machines of this era move anything over a background that matters — there’s no hardware undo, no layers, no compositor. A sprite that respects the world is a sprite that carries a piece of memory around underneath itself. (The Spectrum has no hardware sprites at all, so this is the whole story here; even machines that have them, like the C64, fall back to exactly this technique the moment they run out.)
When it’s wrong, see why
Nearly every failure in this unit is an order failure — and the commonest ones share a single signature:
- He leaves copies of himself, one per step. The dance is out of order, and there are two ways to get here. If the save runs after the draw, the buffer photographs him, and every restore faithfully puts him back. If the restore runs after the position update, it heals the wrong cell and the old one keeps his image. Same trail either way — so don’t diagnose from the screen; recheck all four beats against the listing. Order bugs are why the order is written down.
- His starting cell becomes a black hole. The setup’s
save_underis missing. The buffer still holds its nine zeroes — black pixels, black-on-black attribute — and his first step “restores” that nothing onto the start cell. - The top of the cell heals but the bottom stays smeared. One of the copy loops isn’t running all eight rows — check both
ld b, 8s. Nine bytes is eight plus one; the attribute travels separately after the loop. - The gouge is back, exactly as before. The commit still calls
erase_lampsomewhere, orrestore_underisn’t being reached. The old routine is gone from this build for a reason — approximation and memory can’t share the job.
Before and after
Unit 6 built movement wrong twice and named both debts; Unit 7 paid the speed, and this unit pays the vandalism. The mechanism is small — nine bytes, two mirror-image copy loops, and a commit rewritten as a strict four-beat dance — but the consequence is a threshold: the world now outlives the player’s passage through it. That matters more than tidiness. Lamps are coming, and a lamp the lamplighter can destroy by walking past it isn’t a game object — it’s set dressing. From here on, things on the floor are real.
The detour that began at Unit 5 is over, and it ends exactly on the main road: keys, a gated step, and honest movement over ground that keeps. What’s left in this phase is geometry — walls that argue, and edges that hold without scaffolding.
Try this: swap the save and the draw
In the commit, move call save_under after call draw_lamp, rebuild, and walk. He duplicates himself across the square, one copy per step — the buffer is photographing him instead of the floor. This is the classic form of the bug, worth seeing once on purpose: when a moving thing leaves images of itself behind, the save/draw order is the first place to look.
Try this: half a cell
Change save_under’s ld b, 8 to ld b, 4 and walk around. The top half of every departed cell restores; the bottom half keeps the old smear. The buffer doesn’t know what a cell is — it copies exactly as many bytes as you ask, and the screen shows precisely where you stopped asking.
Try this: a field of witnesses
Put the marker back — then add four more, at different spots around the square (any attribute address, any loud colour). Walk over all of them. Each survives, one at a time, because only one cell is ever under him — a single nine-byte buffer is enough for any number of things on the floor, so long as there’s only one of him.
What you’ve learnt
- The screen holds one copy of everything — draw over it unremembered, and it’s gone.
- A cell is nine bytes: eight bitmap rows plus an attribute, and a buffer that size preserves it exactly.
save_underandrestore_underare mirror loops — screen→buffer and buffer→screen, addressed through the current position.- The commit is a strict dance: restore, step, save, draw — and every reordering has a visible signature.
- One mover, one buffer — and the technique scales by adding buffers, not size.
What’s next
Movement is honest; now the map gets opinions. In Unit 9 the walls stop being paint: before the commit, the game reads the attribute of the cell he’s headed for, recognises brick when it sees it, and vetoes the move. The propose-veto-commit shape from Unit 6 has been waiting for this — its first real veto.