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

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.

30% of Gloaming

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.

Step 1: propose, clamp, commit, draw — and never erase
+56-17
88 WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
99 WALL_BIT equ 3 ; the attribute bit that says "this is wall"
1010 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)
1211
1312 START_COL equ 15 ; where the lamplighter begins
1413 START_ROW equ 11
...
128127 ret
129128
130129 ; ----------------------------------------------------------------------------
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.
135134 ; ----------------------------------------------------------------------------
136135 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+
137142 ld bc, KEYS_OP
138143 in a, (c)
139144 bit 1, a ; O — a zero bit is a pressed key
140- jr z, .held
145+ jr z, .pleft
141146 bit 0, a ; P, same half-row
142- jr z, .held
147+ jr z, .pright
143148 ld bc, KEYS_Q
144149 in a, (c)
145150 bit 0, a ; Q
146- jr z, .held
151+ jr z, .pup
147152 ld bc, KEYS_A
148153 in a, (c)
149154 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
159194 ret
160195
161196 ; ----------------------------------------------------------------------------
...
321356 defb START_COL
322357 lamp_row:
323358 defb START_ROW
359+tcol:
360+ defb 0
361+trow:
362+ defb 0
324363
325364 lamplighter:
326365 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:

An L-shaped trail of white lamplighter figures across the square — one copy of him left in every cell he passed through.
He moved, all right. He's also still in every cell he moved through — the screen keeps what you write until something overwrites it.
Two seconds of the smear painting itself: each step draws him at the new cell, and nothing ever unmakes the old one.

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.

Step 2: erase where he was, then move and draw
+20-1
185185 ret c
186186 cp 31
187187 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
189190 ld a, (tcol)
190191 ld (lamp_col), a
191192 ld a, (trow)
...
327328 ld b, a
328329 ld a, (lamp_col)
329330 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
330349 ret
331350
332351 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:

The lamplighter at the end of an L-shaped trail of smooth black cells gouged through the stippled ground.
The same walk with the erase in: one figure — and a gouge. Every cell he crossed has been blanked to bare black; the stipple that was there is simply gone.
The gouge cutting itself: the erase blanks each departed cell to nothing, and the nothing shows, because the floor was never nothing.

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_row still 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 bit test 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 c rejects row 1 and below, cp 23 / ret nc rejects 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.