Skip to content
Game 1 Unit 5 of 20 1 hr learning time

One Step

The lamplighter moves. Keep his column as a byte in memory, and on a held key erase his old cell, change the column, draw the new one. Cell-by-cell movement — working, for now, only because the floor is blank.

25% of Gloaming

In Unit 4 the lamplighter felt the keys but stayed put — he just changed colour. Now he walks. Hold O and he steps left; hold P and he steps right, cell by cell along his row. This is the first half of Gloaming's second big technique — the cell sprite, a figure with a position that moves by being rubbed out where he was and drawn where he's going.

Where we start

Unit 4's figure reads O and P and recolours to show it — useful scaffolding, but he doesn't go anywhere. We throw the recolour away and spend the same key read on movement instead.

A position is state

Until now his column was fixed in the source — a constant baked in at assembly time. A thing that moves can't have a fixed position; it needs one the program can change. So his column becomes a byte in memory:

lamp_col:
    defb 15        ; his column — we'll write new values here as he walks

That one byte is the difference between a picture and a game. A picture is drawn and done; a game keeps state — where things are, what's happened — and edits it as it runs. lamp_col is Gloaming's first piece of living state. Stepping left is dec it; stepping right is inc it.

Moving is erase, then draw

You can't just draw him in the new cell — the old one would still show him, and you'd leave a trail of lamplighters down the row. Every move is two halves: erase the cell he's in (blank its eight pixel rows back to bare floor), then draw him in the new cell once the column has changed. Erase old, change column, draw new. Miss the erase and he smears; get the order wrong and he rubs himself out.

One row, one addition

He only moves left and right, so he never leaves his character row — and that keeps the arithmetic gentle. Every cell in one row shares the same screen third and row-within-third (the awkward parts from Unit 2); only the column changes. So his cell address is just ROW_SCR + col, where ROW_SCR is column 0 of his row, worked out once as an equ. No thirds to decode at run time — add the column and we're there.

Milestone 1 — walk right

We swap the whole INPUT stage. Out goes the recolour; in comes a lamp_col byte, a draw_lamp and an erase_lamp routine, and — on a held P — the erase / change-column / draw dance. (Left follows in a moment; one direction first proves the machinery.)

Step 1: position as state, and a step to the right on P
+82-52
11 ; Gloaming — Unit 5: One Step
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 4's key-reader, still only recolouring the figure.
3+; step-01 gives him a position in memory and walks him right while P is held.
44
55 org 32768
66
7-COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
8-WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
9-LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure at rest
10-RIGHT_ATTR equ %01000100 ; BRIGHT, INK green (4) — holding P
11-LEFT_ATTR equ %01000010 ; BRIGHT, INK red (2) — holding O
7+COBBLE equ %00000001 ; PAPER black, INK blue — dark ground
8+WALL equ %00001111 ; PAPER blue, INK white — pale stone
9+LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure
1210
13-LAMP_COL equ 15
14-LAMP_ROW equ 11
15-THIRD equ LAMP_ROW / 8
16-CHARROW equ LAMP_ROW - THIRD * 8
17-LAMP_SCR equ $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL
18-LAMP_ATTR_ADDR equ $5800 + LAMP_ROW * 32 + LAMP_COL
11+; --- his row is fixed; only his column changes ---
12+ROW equ 11
13+ROW_THIRD equ ROW / 8 ; which screen third (0,1,2)
14+ROW_CHARROW equ ROW - ROW_THIRD * 8 ; row within that third (0-7)
15+ROW_SCR equ $4000 + ROW_THIRD * $0800 + ROW_CHARROW * 32 ; col-0 of the row
16+ROW_ATTR equ $5800 + ROW * 32 ; col-0 attribute
17+START_COL equ 15
1918
20-; --- the keyboard half-row holding O and P ---
21-KEYS_OP equ $DFFE ; high byte $DF selects this half-row
19+KEYS_OP equ $DFFE ; half-row with P(bit0) and O(bit1)
2220
2321 start:
24- ; --- the border goes black — the night beyond the square ---
25- ld a, 0
22+ ld a, 0 ; border black
2623 out ($FE), a
2724
28- ; --- wash the whole grid in cobbles ---
29- ld hl, $5800
25+ ld hl, $5800 ; wash the grid in cobbles
3026 ld de, $5801
3127 ld (hl), COBBLE
3228 ld bc, 767
3329 ldir
3430
35- ; --- top and bottom walls ---
36- ld hl, $5800
31+ ld hl, $5800 ; wall frame — top row
3732 ld b, 32
3833 .top:
3934 ld (hl), WALL
4035 inc hl
4136 djnz .top
4237
43- ld hl, $5AE0
38+ ld hl, $5AE0 ; bottom row
4439 ld b, 32
4540 .bottom:
4641 ld (hl), WALL
4742 inc hl
4843 djnz .bottom
4944
50- ; --- left and right walls ---
51- ld hl, $5800
45+ ld hl, $5800 ; left and right columns
5246 ld b, 24
5347 .sides:
5448 ld (hl), WALL
...
6054 ld de, 32
6155 add hl, de
6256 djnz .sides
63-
64- ; --- draw the lamplighter (colour, then shape) ---
65- ld hl, LAMP_ATTR_ADDR
66- ld (hl), LAMP_ATTR
6757
68- ld hl, LAMP_SCR
69- ld de, lamplighter
70- ld b, 8
71-.draw:
72- ld a, (de)
73- ld (hl), a
74- inc de
75- inc h
76- djnz .draw
58+ call draw_lamp ; draw him once, at his starting column
7759
78- ; --- start the 50 Hz heartbeat ---
60+; --- the heartbeat: read P and, if held, step right ---
7961 im 1
8062 ei
8163
8264 game_loop:
83- halt ; wait for the next frame
65+ halt
8466
85- ; --- INPUT: read O and P and recolour to show what's held ---
86- ld bc, KEYS_OP ; BC = $DFFE — the address IS the question
87- in a, (c) ; bottom 5 bits = keys, 0 = held (active low)
88- ld d, LAMP_ATTR ; assume nothing held → white at rest
89- bit 1, a ; O (left)? Z set = bit is 0 = held
90- jr nz, .not_left
91- ld d, LEFT_ATTR ; red
92-.not_left:
67+ ld bc, KEYS_OP
68+ in a, (c) ; bottom bits, 0 = held
9369 bit 0, a ; P (right)?
94- jr nz, .not_right
95- ld d, RIGHT_ATTR ; green
96-.not_right:
97- ld a, d
98- ld (LAMP_ATTR_ADDR), a ; one attribute write — his cell recolours
70+ jr z, .step_right
71+ jr game_loop ; nothing held — hold position
9972
73+.step_right:
74+ call erase_lamp ; rub him out where he is
75+ ld a, (lamp_col)
76+ inc a ; one cell right
77+ ld (lamp_col), a
78+ call draw_lamp ; draw him where he's going
10079 jr game_loop
10180
102-; The lamplighter's shape — eight bytes, one per pixel row (from Unit 2).
81+; ----------------------------------------------------------------------------
82+; draw_lamp — colour his cell and stamp his shape into it.
83+; cell address = ROW_SCR + col, attribute = ROW_ATTR + col.
84+; ----------------------------------------------------------------------------
85+draw_lamp:
86+ ld a, (lamp_col)
87+ ld e, a
88+ ld d, 0 ; DE = column offset
89+ ld hl, ROW_ATTR
90+ add hl, de
91+ ld (hl), LAMP_ATTR ; his cell takes the figure's colour
92+
93+ ld hl, ROW_SCR
94+ add hl, de ; HL = top row of his cell
95+ ld de, lamplighter ; DE now walks the shape
96+ ld b, 8
97+.draw_row:
98+ ld a, (de)
99+ ld (hl), a
100+ inc de
101+ inc h ; down one screen row (+256)
102+ djnz .draw_row
103+ ret
104+
105+; ----------------------------------------------------------------------------
106+; erase_lamp — blank his cell back to bare cobbles.
107+; (Safe ONLY because the floor has no pixels — see the unit page.)
108+; ----------------------------------------------------------------------------
109+erase_lamp:
110+ ld a, (lamp_col)
111+ ld e, a
112+ ld d, 0
113+ ld hl, ROW_ATTR
114+ add hl, de
115+ ld (hl), COBBLE ; the vacated cell is cobbles again
116+
117+ ld hl, ROW_SCR
118+ add hl, de
119+ ld b, 8
120+ xor a ; A = 0 — a blank pixel row
121+.erase_row:
122+ ld (hl), a
123+ inc h
124+ djnz .erase_row
125+ ret
126+
127+; ----------------------------------------------------------------------------
128+; State and shape.
129+; ----------------------------------------------------------------------------
130+lamp_col:
131+ defb START_COL ; his column — changes as he walks
132+
103133 lamplighter:
104134 defb %00111100 ; ..XXXX.. head
105135 defb %00111100 ; ..XXXX.. head
The complete step 1 program
; Gloaming — Unit 5: One Step
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 gives him a position in memory and walks him right while P is held.

            org     32768

COBBLE      equ     %00000001       ; PAPER black, INK blue — dark ground
WALL        equ     %00001111       ; PAPER blue, INK white — pale stone
LAMP_ATTR   equ     %01000111       ; BRIGHT, PAPER black, INK white — the figure

; --- his row is fixed; only his column changes ---
ROW         equ     11
ROW_THIRD   equ     ROW / 8                 ; which screen third (0,1,2)
ROW_CHARROW equ     ROW - ROW_THIRD * 8     ; row within that third (0-7)
ROW_SCR     equ     $4000 + ROW_THIRD * $0800 + ROW_CHARROW * 32   ; col-0 of the row
ROW_ATTR    equ     $5800 + ROW * 32                               ; col-0 attribute
START_COL   equ     15

KEYS_OP     equ     $DFFE           ; half-row with P(bit0) and O(bit1)

start:
            ld      a, 0            ; border black
            out     ($FE), a

            ld      hl, $5800       ; wash the grid in cobbles
            ld      de, $5801
            ld      (hl), COBBLE
            ld      bc, 767
            ldir

            ld      hl, $5800       ; wall frame — top row
            ld      b, 32
.top:
            ld      (hl), WALL
            inc     hl
            djnz    .top

            ld      hl, $5AE0       ; bottom row
            ld      b, 32
.bottom:
            ld      (hl), WALL
            inc     hl
            djnz    .bottom

            ld      hl, $5800       ; left and right columns
            ld      b, 24
.sides:
            ld      (hl), WALL
            push    hl
            ld      de, 31
            add     hl, de
            ld      (hl), WALL
            pop     hl
            ld      de, 32
            add     hl, de
            djnz    .sides

            call    draw_lamp       ; draw him once, at his starting column

; --- the heartbeat: read P and, if held, step right ---
            im      1
            ei

game_loop:
            halt

            ld      bc, KEYS_OP
            in      a, (c)          ; bottom bits, 0 = held
            bit     0, a            ; P (right)?
            jr      z, .step_right
            jr      game_loop       ; nothing held — hold position

.step_right:
            call    erase_lamp      ; rub him out where he is
            ld      a, (lamp_col)
            inc     a               ; one cell right
            ld      (lamp_col), a
            call    draw_lamp       ; draw him where he's going
            jr      game_loop

; ----------------------------------------------------------------------------
; draw_lamp — colour his cell and stamp his shape into it.
;   cell address = ROW_SCR + col, attribute = ROW_ATTR + col.
; ----------------------------------------------------------------------------
draw_lamp:
            ld      a, (lamp_col)
            ld      e, a
            ld      d, 0            ; DE = column offset
            ld      hl, ROW_ATTR
            add     hl, de
            ld      (hl), LAMP_ATTR ; his cell takes the figure's colour

            ld      hl, ROW_SCR
            add     hl, de          ; HL = top row of his cell
            ld      de, lamplighter ; DE now walks the shape
            ld      b, 8
.draw_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h               ; down one screen row (+256)
            djnz    .draw_row
            ret

; ----------------------------------------------------------------------------
; erase_lamp — blank his cell back to bare cobbles.
;   (Safe ONLY because the floor has no pixels — see the unit page.)
; ----------------------------------------------------------------------------
erase_lamp:
            ld      a, (lamp_col)
            ld      e, a
            ld      d, 0
            ld      hl, ROW_ATTR
            add     hl, de
            ld      (hl), COBBLE    ; the vacated cell is cobbles again

            ld      hl, ROW_SCR
            add     hl, de
            ld      b, 8
            xor     a               ; A = 0 — a blank pixel row
.erase_row:
            ld      (hl), a
            inc     h
            djnz    .erase_row
            ret

; ----------------------------------------------------------------------------
; State and shape.
; ----------------------------------------------------------------------------
lamp_col:
            defb    START_COL       ; his column — changes as he walks

lamplighter:
            defb    %00111100       ; ..XXXX..   head
            defb    %00111100       ; ..XXXX..   head
            defb    %00011000       ; ...XX...   neck
            defb    %01111110       ; .XXXXXX.   arms
            defb    %00011000       ; ...XX...   body
            defb    %00011000       ; ...XX...   body
            defb    %00100100       ; ..X..X..   legs
            defb    %01000010       ; .X....X.   feet

            end     start

Hold P and he walks right, one cell at a time — and the floor stays clean behind him, because each step erases the old cell before drawing the new one:

The lamplighter, having walked from the centre to the right side of the square, with clean black floor behind him — no trail.
After holding P: he's several cells right of where he started, and there's no smear behind him. The erase is doing its job.

Milestone 2 — and back the other way

Left is the mirror of right: the same erase / change-column / draw, but dec lamp_col instead of inc. We add the O test above the P test, and now he walks both ways.

Step 2: O steps left — the same move, mirrored
+14-4
11 ; Gloaming — Unit 5: One Step
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-01 gives him a position in memory and walks him right while P is held.
3+; step-02 adds the other direction — O steps left; he walks both ways.
44
55 org 32768
66
...
5757
5858 call draw_lamp ; draw him once, at his starting column
5959
60-; --- the heartbeat: read P and, if held, step right ---
60+; --- the heartbeat: read a direction and, if held, step that way ---
6161 im 1
6262 ei
6363
...
6666
6767 ld bc, KEYS_OP
6868 in a, (c) ; bottom bits, 0 = held
69+ bit 1, a ; O (left)?
70+ jr z, .step_left
6971 bit 0, a ; P (right)?
7072 jr z, .step_right
7173 jr game_loop ; nothing held — hold position
7274
73-.step_right:
75+.step_left:
7476 call erase_lamp ; rub him out where he is
77+ ld a, (lamp_col)
78+ dec a ; one cell left
79+ ld (lamp_col), a
80+ call draw_lamp
81+ jr game_loop
82+
83+.step_right:
84+ call erase_lamp
7585 ld a, (lamp_col)
7686 inc a ; one cell right
7787 ld (lamp_col), a
78- call draw_lamp ; draw him where he's going
88+ call draw_lamp
7989 jr game_loop
8090
8191 ; ----------------------------------------------------------------------------
The complete program
; Gloaming — Unit 5: One Step
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 adds the other direction — O steps left; he walks both ways.

            org     32768

COBBLE      equ     %00000001       ; PAPER black, INK blue — dark ground
WALL        equ     %00001111       ; PAPER blue, INK white — pale stone
LAMP_ATTR   equ     %01000111       ; BRIGHT, PAPER black, INK white — the figure

; --- his row is fixed; only his column changes ---
ROW         equ     11
ROW_THIRD   equ     ROW / 8                 ; which screen third (0,1,2)
ROW_CHARROW equ     ROW - ROW_THIRD * 8     ; row within that third (0-7)
ROW_SCR     equ     $4000 + ROW_THIRD * $0800 + ROW_CHARROW * 32   ; col-0 of the row
ROW_ATTR    equ     $5800 + ROW * 32                               ; col-0 attribute
START_COL   equ     15

KEYS_OP     equ     $DFFE           ; half-row with P(bit0) and O(bit1)

start:
            ld      a, 0            ; border black
            out     ($FE), a

            ld      hl, $5800       ; wash the grid in cobbles
            ld      de, $5801
            ld      (hl), COBBLE
            ld      bc, 767
            ldir

            ld      hl, $5800       ; wall frame — top row
            ld      b, 32
.top:
            ld      (hl), WALL
            inc     hl
            djnz    .top

            ld      hl, $5AE0       ; bottom row
            ld      b, 32
.bottom:
            ld      (hl), WALL
            inc     hl
            djnz    .bottom

            ld      hl, $5800       ; left and right columns
            ld      b, 24
.sides:
            ld      (hl), WALL
            push    hl
            ld      de, 31
            add     hl, de
            ld      (hl), WALL
            pop     hl
            ld      de, 32
            add     hl, de
            djnz    .sides

            call    draw_lamp       ; draw him once, at his starting column

; --- the heartbeat: read a direction and, if held, step that way ---
            im      1
            ei

game_loop:
            halt

            ld      bc, KEYS_OP
            in      a, (c)          ; bottom bits, 0 = held
            bit     1, a            ; O (left)?
            jr      z, .step_left
            bit     0, a            ; P (right)?
            jr      z, .step_right
            jr      game_loop       ; nothing held — hold position

.step_left:
            call    erase_lamp      ; rub him out where he is
            ld      a, (lamp_col)
            dec     a               ; one cell left
            ld      (lamp_col), a
            call    draw_lamp
            jr      game_loop

.step_right:
            call    erase_lamp
            ld      a, (lamp_col)
            inc     a               ; one cell right
            ld      (lamp_col), a
            call    draw_lamp
            jr      game_loop

; ----------------------------------------------------------------------------
; draw_lamp — colour his cell and stamp his shape into it.
;   cell address = ROW_SCR + col, attribute = ROW_ATTR + col.
; ----------------------------------------------------------------------------
draw_lamp:
            ld      a, (lamp_col)
            ld      e, a
            ld      d, 0            ; DE = column offset
            ld      hl, ROW_ATTR
            add     hl, de
            ld      (hl), LAMP_ATTR ; his cell takes the figure's colour

            ld      hl, ROW_SCR
            add     hl, de          ; HL = top row of his cell
            ld      de, lamplighter ; DE now walks the shape
            ld      b, 8
.draw_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h               ; down one screen row (+256)
            djnz    .draw_row
            ret

; ----------------------------------------------------------------------------
; erase_lamp — blank his cell back to bare cobbles.
;   (Safe ONLY because the floor has no pixels — see the unit page.)
; ----------------------------------------------------------------------------
erase_lamp:
            ld      a, (lamp_col)
            ld      e, a
            ld      d, 0
            ld      hl, ROW_ATTR
            add     hl, de
            ld      (hl), COBBLE    ; the vacated cell is cobbles again

            ld      hl, ROW_SCR
            add     hl, de
            ld      b, 8
            xor     a               ; A = 0 — a blank pixel row
.erase_row:
            ld      (hl), a
            inc     h
            djnz    .erase_row
            ret

; ----------------------------------------------------------------------------
; State and shape.
; ----------------------------------------------------------------------------
lamp_col:
            defb    START_COL       ; his column — changes as he walks

lamplighter:
            defb    %00111100       ; ..XXXX..   head
            defb    %00111100       ; ..XXXX..   head
            defb    %00011000       ; ...XX...   neck
            defb    %01111110       ; .XXXXXX.   arms
            defb    %00011000       ; ...XX...   body
            defb    %00011000       ; ...XX...   body
            defb    %00100100       ; ..X..X..   legs
            defb    %01000010       ; .X....X.   feet

            end     start

Hold a key and he glides — right on P, left on O — one figure, no trail, the floor wiped clean at every step:

The lamplighter walks right while P is held, then back left on O — one cell per frame, so he's brisk. Each step erases his old cell before drawing the new one, so there's never a trail. (Nothing stops him at the walls yet — that's Unit 7.)

He moves once per frame while held — brisk, since the loop runs fifty times a second. Taming that into a steadier walk is the last "Try this" below.

The assumption, named out loud

Here's the catch, and it matters: erasing means writing zeros over his cell, and that only looks right because the floor is blank. Cobbles are pure attribute colour — no pixels — so blanking a floor cell destroys nothing.

The moment he steps onto something with pixels — a lamp, which arrives in Unit 9 — this same blank-erase would wipe the lamp out as he passed. The naive erase isn't wrong yet; it's wrong later, and we'll feel exactly when. Unit 6 fixes it by saving what's underneath him before he stands there. For now: blank floor, blank erase, and it genuinely works.

When it's wrong, see why

Movement bugs are almost always the erase and the order:

  • He leaves a trail of figures. The erase isn't running, or runs in the wrong place. The order must be: erase the old cell, then change lamp_col, then draw the new cell.
  • He vanishes the moment you press. You changed lamp_col before erasing, so you blanked the cell he's moving to and never cleaned up the one he left. Erase first.
  • He runs off the right edge and the screen garbles. Nothing stops him yet — no wall collision (Unit 7), no edge bounds (Unit 8). Tap instead of holding, for now.
  • Nothing moves at all. The key read isn't reaching the step branches. Check the IN A,(C) from Unit 4, and that jr z (held) routes into .step_left / .step_right.

Before and after

You started with a figure that could only change colour and finished with one that walks — built by spending the same key read on movement instead of decoration. The two ideas under it carry the rest of the game: a position is state in memory you edit, and moving is erase then draw, in that order. The figure glides cleanly today only because the floor beneath him is empty — the honest limitation Unit 6 exists to remove.

Try this: take the erase away

Comment out the call erase_lamp in one of the step branches and run it. Now he leaves a trail of lamplighters across the row — every cell he passes keeps his shape, because nothing rubbed it out. Ugly, but it shows you exactly what the erase is for. Put it back.

Try this: watch the assumption bite

Prove the blank-erase flaw to yourself before Unit 6 fixes it. In setup, poke a stray pixel into a floor cell in his path — a solid bar at column 22:

ld   hl, ROW_SCR + 22
ld   (hl), %11111111

Walk him over column 22. As he leaves, the erase wipes your bar away — he damaged the floor just by passing. That's the bug Unit 6 exists to kill.

Try this: slow him to a walk

Moving every frame is fast. Add a one-byte counter that only lets him step every few frames: load it from, say, 4 after a step, dec it each frame, and only move when it reaches zero. Holding a key now repeats at a steady, controllable pace — the start of how real games handle held keys.

What you've learnt

  • A moving thing keeps its position as state in memory (lamp_col); moving means editing that state.
  • A move is erase the old cell, then draw the new one — order matters.
  • Within a single row, a cell's address is just ROW_SCR + col — no thirds to decode.
  • The naive blank-erase only works over blank floor; pixels underneath would be destroyed — the problem Unit 6 solves.

What's next

The lamplighter walks, but he can only ever cross empty floor without harm. In Unit 6 we make him safe over anything: before he stands in a cell, save the eight bytes (and the colour) already there; when he leaves, restore them. He'll be able to walk over lamps, walls, anything — without scrubbing it away. It's the honest "before" of a technique a later game upgrades into true masking.