Skip to content
Game 1Unit 8 of 201 hr learning time

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.

40% of Gloaming

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:

  1. Restore — put the ground back, while the position still names the old cell.
  2. Step — update the position; only now does he officially move.
  3. Save — the position names the new cell; capture its ground before anything touches it.
  4. 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.

Step 1: save_under and restore_under replace the erase — plus a marker to prove it
+45-12
1111
1212 START_COL equ 15 ; where the lamplighter begins
1313 START_ROW equ 11
14+MARKER equ %00110110 ; a bright yellow-on-yellow cell (this step only)
1415 PLAYER_REPEAT equ 6 ; frames between steps while a key is held
1516
1617 KEYS_OP equ $DFFE ; half-row P O I U Y — bits 1 and 0
...
5859 ; Now that the wall cells are painted, fill_walls can read the
5960 ; map back and lay brick wherever the wall bit is set.
6061 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
6168 call draw_lamp
6269
6370 ; --- start the heartbeat ---
...
222229 ret c
223230 cp 31
224231 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
227234 ld a, (tcol)
228235 ld (lamp_col), a
229236 ld a, (trow)
230237 ld (lamp_row), a
238+ call save_under
231239 call draw_lamp
232240 ret
233241
...
356364 ret
357365
358366 ; ----------------------------------------------------------------------------
359-; The lamplighter's draw.
367+; The lamplighter's save / restore / draw.
360368 ; ----------------------------------------------------------------------------
361369
362370 ; pos_bc — the lamplighter's cell into (C, B), read fresh from the data.
...
367375 ld c, a
368376 ret
369377
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:
375382 call pos_bc
376383 call scr_addr_cr
384+ ld de, under_lamp
377385 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)
380408 ld (hl), a
409+ inc de
381410 inc h
382- djnz .el
411+ djnz .ru
383412 call pos_bc
384413 call attr_addr_cr
385- ld (hl), COBBLE
414+ ld a, (under_lamp + 8)
415+ ld (hl), a
386416 ret
387417
388418 draw_lamp:
...
418448 defb 0
419449 player_timer:
420450 defb 0
451+
452+under_lamp:
453+ defb 0, 0, 0, 0, 0, 0, 0, 0, 0
421454
422455 lamplighter:
423456 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
The lamplighter at the centre of the square with a solid yellow cell on the floor three cells to his left.
The witness in place: one yellow cell in his path. Under the old erase, walking over anything meant destroying it.

Now hold O and walk him straight through it:

The crossing: he steps onto the marker — for that beat he covers it entirely — and steps off, and it's simply there again. Saved on arrival, restored on departure, exact to the pixel.
The lamplighter left of the yellow marker cell, which is intact; the stippled floor shows no trail.
After the crossing. The marker stands, the stipple behind him is whole, and the gouge is history.

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.

Step 2: the marker comes down; the buffer stays
-5
1111
1212 START_COL equ 15 ; where the lamplighter begins
1313 START_ROW equ 11
14-MARKER equ %00110110 ; a bright yellow-on-yellow cell (this step only)
1514 PLAYER_REPEAT equ 6 ; frames between steps while a key is held
1615
1716 KEYS_OP equ $DFFE ; half-row P O I U Y — bits 1 and 0
...
5958 ; Now that the wall cells are painted, fill_walls can read the
6059 ; map back and lay brick wherever the wall bit is set.
6160 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
6661 ; save what he is about to stand on, BEFORE the first draw
6762 call save_under
6863 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:

The lamplighter after an L-shaped walk right and down; the stippled floor behind him is completely intact.
Unit 6's gouge walk, replayed: the floor doesn't remember he was there. Compare the trail this exact walk used to leave.
A full lap of the square — right, down, left, up, back to the start — and the ground is untouched everywhere. Movement finally costs the world nothing.

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_under is 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_lamp somewhere, or restore_under isn’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_under and restore_under are 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.