Skip to content

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.

  • Sinclair ZX Spectrum
  • Assembly

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:

Code
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 added−17 removed
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
Removed: LAMP_GLOW   equ     %01000110       ; his warmth while a key is down (this unit only)

START_COL   equ     15              ; where the lamplighter begins
START_ROW   equ     11
…            ret

; ----------------------------------------------------------------------------
Removed: ; player_step — scan the keyboard. Reading port $FE with a half-row
Removed: ; address in B selects five keys; a key held pulls its bit LOW. While
Removed: ; any direction key is down, the lamplighter glows — proof the
Removed: ; machine can feel you.
Added: ; player_step — the keys become movement. Each direction key edits a
Added: ; TARGET position (tcol, trow) — a proposal, not yet a move — so it
Added: ; can be vetoed before it becomes real. Then the move commits: leave
Added: ; the old cell, take the new one, draw.
; ----------------------------------------------------------------------------
player_step:
Added:             ; --- propose: the target starts where he stands ---
Added:             ld      a, (lamp_col)
Added:             ld      (tcol), a
Added:             ld      a, (lamp_row)
Added:             ld      (trow), a
Added: 
            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a            ; O — a zero bit is a pressed key
Removed:             jr      z, .held
Added:             jr      z, .pleft
            bit     0, a            ; P, same half-row
Removed:             jr      z, .held
Added:             jr      z, .pright
            ld      bc, KEYS_Q
            in      a, (c)
            bit     0, a            ; Q
Removed:             jr      z, .held
Added:             jr      z, .pup
            ld      bc, KEYS_A
            in      a, (c)
            bit     0, a            ; A
Removed:             jr      z, .held
Removed:             call    pos_bc
Removed:             call    attr_addr_cr
Removed:             ld      (hl), LAMP_ATTR
Removed:             ret
Removed: .held:
Removed:             call    pos_bc
Removed:             call    attr_addr_cr
Removed:             ld      (hl), LAMP_GLOW
Added:             jr      z, .pdown
Added:             ret                     ; nothing held — nothing to do
Added: 
Added: .pleft:
Added:             ld      hl, tcol
Added:             dec     (hl)
Added:             jr      .pmove
Added: .pright:
Added:             ld      hl, tcol
Added:             inc     (hl)
Added:             jr      .pmove
Added: .pup:
Added:             ld      hl, trow
Added:             dec     (hl)
Added:             jr      .pmove
Added: .pdown:
Added:             ld      hl, trow
Added:             inc     (hl)
Added: .pmove:
Added:             ; Scaffold (route skeleton): a numeric edge clamp so the detour
Added:             ; cannot walk the lamplighter off the map — past the map's edge
Added:             ; the address sums leave screen memory for the system's own. The
Added:             ; walls take this job in unit 9; unit 10 retires the numbers.
Added:             ld      a, (trow)
Added:             cp      2
Added:             ret     c
Added:             cp      23
Added:             ret     nc
Added:             ld      a, (tcol)
Added:             cp      1
Added:             ret     c
Added:             cp      31
Added:             ret     nc
Added:             ; --- commit: the target becomes his position, and he's drawn ---
Added:             ld      a, (tcol)
Added:             ld      (lamp_col), a
Added:             ld      a, (trow)
Added:             ld      (lamp_row), a
Added:             call    draw_lamp
            ret

; ----------------------------------------------------------------------------
…            defb    START_COL
lamp_row:
            defb    START_ROW
Added: tcol:
Added:             defb    0
Added: trow:
Added:             defb    0

lamplighter:
            defb    %00111100
The complete step 1 program
step-01.asm
; 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 added−1 removed
            ret     c
            cp      31
            ret     nc
Removed:             ; --- commit: the target becomes his position, and he's drawn ---
Added:             ; --- commit: erase where he was, move, draw where he is ---
Added:             call    erase_lamp
            ld      a, (tcol)
            ld      (lamp_col), a
            ld      a, (trow)
…            ld      b, a
            ld      a, (lamp_col)
            ld      c, a
Added:             ret
Added: 
Added: ; erase_lamp — the naive erase: blank the cell the lamplighter leaves.
Added: ; Zero its eight bitmap bytes, repaint it ground colour. The cell is
Added: ; clean — and whatever the floor had there is gone with him.
Added: ; (Detour: watch what it does to the cobbles.)
Added: erase_lamp:
Added:             call    pos_bc
Added:             call    scr_addr_cr
Added:             ld      b, 8
Added:             xor     a
Added: .el:
Added:             ld      (hl), a
Added:             inc     h
Added:             djnz    .el
Added:             call    pos_bc
Added:             call    attr_addr_cr
Added:             ld      (hl), COBBLE
            ret

draw_lamp:
The complete program
step-02.asm
; 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.

Run it here

This unit's program, built for you.

Program
step-02.tap
Machine
ZX Spectrum 48K (PAL)

Download step-02.tap