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

The Lamplighter

Leave attribute colour behind and write the bitmap. Meet the Spectrum's famous screen layout — the eight rows of a cell sit 256 bytes apart — then fill one cell, and shape it into an 8×8 figure with eight INC H steps.

10% of Gloaming

The square is built — black cobbles, blue walls, every cell of it a byte of colour written straight to attribute memory in Unit 1. But colour alone draws only blocks. For a figure — something with a shape — we need pixels. This unit draws the lamplighter himself, an 8×8 figure poked into a single cell, in two steps: first prove we can mark the cell at all, then give the mark a shape.

Where we start

Unit 1's finished square — border, cobbles, walls — and nothing else. Everything below draws one figure into the middle of it.

Pixels live somewhere else

In Unit 1 we only ever wrote attribute memory at $5800. That sets a cell's two colours, but it can't draw a shape — a cell is one INK and one PAPER, nothing finer. The actual pixels — which of the 64 dots in a cell are lit — live in a separate, larger block: the bitmap, $4000 to $57FF, 6144 bytes.

So a cell is described by two places at once: its pixels in the bitmap, its two colours in attribute memory. A lit pixel is drawn in the cell's INK; an unlit one shows its PAPER. To draw the lamplighter we write his pixels to the bitmap — and give his cell a colour so they show.

The Spectrum's awkward, generous layout

Here is the surprise. You might expect the eight rows of pixels in a cell to be eight bytes in a row — address, address+1, address+2. They are not. The eight rows of a single cell sit 256 bytes apart.

The reason is how the screen is wired. The 192 pixel rows are split into three thirds of 64 rows each (top, middle, bottom). Inside a third, the address climbs by 32 for each character row down — but by 256 for each pixel row inside a character:

The eight pixel rows of ONE cell:

  row 0  →  A          (top of the cell)
  row 1  →  A + 256
  row 2  →  A + 512
  ...
  row 7  →  A + 1792    (bottom of the cell)

Awkward to picture — but it hands us a gift. Adding 256 to an address adds 1 to its high byte and leaves the low byte untouched. And the Z80 has an instruction that does exactly that: INC H. So to walk down the eight rows of a cell, we just INC H eight times. The hard-looking layout collapses into one tidy instruction.

Finding the cell's address

We still need the address of the cell's top row to start from. For a cell at column col, row row (0–23), it's:

address = $4000 + third*$0800 + (row within third)*32 + col

where third is row / 8 and the row within the third is the remainder. You don't have to work that out in your head — let the assembler do the arithmetic. We name the lamplighter's column and row, and write the formula as an equ; the assembler computes the final address at build time. Change the column or row and the address recomputes itself.

Milestone 1 — fill the cell solid

Before a shape, prove you can hit the cell at all. We set the cell's colour (one attribute write, the idea from Unit 1), then walk its eight rows with INC H — writing a solid %11111111 into each, so every pixel lights. If a white block lands exactly where the lamplighter should stand, the address and the row-walk are right; the shape is then just a matter of which bits we write.

Step 1: colour the cell, then fill its eight rows solid with INC H
+44-1
11 ; Gloaming — Unit 2: The Lamplighter
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 1's finished square — the ground this unit draws on.
3+; step-01 fills the figure's cell solid — proof the INC H row-walk lands right.
44
55 org 32768
66
77 COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
88 WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
9+LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black (0), INK white (7) — the figure
10+
11+; --- where the lamplighter stands: a cell named by (column 0-31, row 0-23) ---
12+LAMP_COL equ 15
13+LAMP_ROW equ 11
14+
15+; The screen splits top/middle/bottom into THIRDS of 8 character rows. The top
16+; pixel-row of a cell lives at $4000 + third*$0800 + (row-within-third)*32 + col.
17+THIRD equ LAMP_ROW / 8
18+CHARROW equ LAMP_ROW - THIRD * 8
19+LAMP_SCR equ $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL
20+
21+; The attribute cell for the same (col,row) is the simpler linear address.
22+LAMP_ATTR_ADDR equ $5800 + LAMP_ROW * 32 + LAMP_COL
923
1024 start:
1125 ; --- the border goes black — the night beyond the square ---
...
4761 ld de, 32
4862 add hl, de
4963 djnz .sides
64+
65+ ; --- give the figure's cell a warm colour so its pixels read ---
66+ ld hl, LAMP_ATTR_ADDR
67+ ld (hl), LAMP_ATTR
68+
69+ ; --- fill the cell solid: walk its eight rows with INC H ---
70+ ; HL walks the screen rows (INC H = down one row, +256).
71+ ; DE walks the eight bytes (INC DE = next row of the block).
72+ ld hl, LAMP_SCR ; top pixel-row of his cell
73+ ld de, lamplighter ; the eight bytes
74+ ld b, 8 ; eight rows
75+.draw:
76+ ld a, (de)
77+ ld (hl), a
78+ inc de
79+ inc h ; next screen row down (+256)
80+ djnz .draw
5081
5182 .loop:
5283 halt
5384 jr .loop
85+
86+; Eight bytes, one per pixel row. Solid for now — every pixel lit — so the
87+; whole cell fills white. Step 2 replaces these with the figure's shape.
88+lamplighter:
89+ defb %11111111
90+ defb %11111111
91+ defb %11111111
92+ defb %11111111
93+ defb %11111111
94+ defb %11111111
95+ defb %11111111
96+ defb %11111111
5497
5598 end start
5699
The complete step 1 program
; Gloaming — Unit 2: The Lamplighter
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 fills the figure's cell solid — proof the INC H row-walk lands right.

            org     32768

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

; --- where the lamplighter stands: a cell named by (column 0-31, row 0-23) ---
LAMP_COL    equ     15
LAMP_ROW    equ     11

; The screen splits top/middle/bottom into THIRDS of 8 character rows. The top
; pixel-row of a cell lives at $4000 + third*$0800 + (row-within-third)*32 + col.
THIRD       equ     LAMP_ROW / 8
CHARROW     equ     LAMP_ROW - THIRD * 8
LAMP_SCR    equ     $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL

; The attribute cell for the same (col,row) is the simpler linear address.
LAMP_ATTR_ADDR equ  $5800 + LAMP_ROW * 32 + LAMP_COL

start:
            ; --- the border goes black — the night beyond the square ---
            ld      a, 0
            out     ($FE), a

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

            ; --- top and bottom walls ---
            ld      hl, $5800
            ld      b, 32
.top:
            ld      (hl), WALL
            inc     hl
            djnz    .top

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

            ; --- left and right walls ---
            ld      hl, $5800
            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

            ; --- give the figure's cell a warm colour so its pixels read ---
            ld      hl, LAMP_ATTR_ADDR
            ld      (hl), LAMP_ATTR

            ; --- fill the cell solid: walk its eight rows with INC H ---
            ; HL walks the screen rows (INC H = down one row, +256).
            ; DE walks the eight bytes (INC DE = next row of the block).
            ld      hl, LAMP_SCR    ; top pixel-row of his cell
            ld      de, lamplighter ; the eight bytes
            ld      b, 8            ; eight rows
.draw:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h               ; next screen row down (+256)
            djnz    .draw

.loop:
            halt
            jr      .loop

; Eight bytes, one per pixel row. Solid for now — every pixel lit — so the
; whole cell fills white. Step 2 replaces these with the figure's shape.
lamplighter:
            defb    %11111111
            defb    %11111111
            defb    %11111111
            defb    %11111111
            defb    %11111111
            defb    %11111111
            defb    %11111111
            defb    %11111111

            end     start

A solid white block sits in the middle of the square — one cell, all 64 pixels lit:

The walled square with a small solid white block in the centre cell.
The cell, filled solid. The block proves the address maths and the eight INC H steps landed in the right place — now there's somewhere to draw a shape.

Milestone 2 — give it a shape

A sprite is just eight bytes — one per pixel row. A 1 bit is a lit pixel; a 0 shows the PAPER behind. Lay the bits out and you can see the figure stand up:

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

Nothing about the draw changes — same attribute write, same eight INC H steps. We only change the eight bytes the loop copies: solid %11111111 becomes the figure's shape.

Step 2: replace the solid block with the figure's eight bytes
+18-17
11 ; Gloaming — Unit 2: The Lamplighter
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-01 fills the figure's cell solid — proof the INC H row-walk lands right.
3+; step-02 gives the cell a shape — the eight bytes become the figure.
44
55 org 32768
...
6767 ld (hl), LAMP_ATTR
6868
69- ; --- fill the cell solid: walk its eight rows with INC H ---
69+ ; --- draw his eight-byte shape down the eight rows of the cell ---
7070 ; HL walks the screen rows (INC H = down one row, +256).
71- ; DE walks the eight bytes (INC DE = next row of the block).
71+ ; DE walks the sprite bytes (INC DE = next row of the shape).
7272 ld hl, LAMP_SCR ; top pixel-row of his cell
73- ld de, lamplighter ; the eight bytes
73+ ld de, lamplighter ; his shape, eight bytes
7474 ld b, 8 ; eight rows
7575 .draw:
76- ld a, (de)
77- ld (hl), a
78- inc de
76+ ld a, (de) ; one row of the shape
77+ ld (hl), a ; into the screen
78+ inc de ; next shape byte
7979 inc h ; next screen row down (+256)
8080 djnz .draw
...
8484 jr .loop
8585
86-; Eight bytes, one per pixel row. Solid for now — every pixel lit — so the
87-; whole cell fills white. Step 2 replaces these with the figure's shape.
86+; The lamplighter's shape — eight bytes, one per pixel row. A 1 bit is a lit
87+; pixel (drawn in the cell's INK); a 0 bit shows the PAPER behind. Read the
88+; bytes top-down and the little figure stands up.
8889 lamplighter:
89- defb %11111111
90- defb %11111111
91- defb %11111111
92- defb %11111111
93- defb %11111111
94- defb %11111111
95- defb %11111111
96- defb %11111111
90+ defb %00111100 ; ..XXXX.. head
91+ defb %00111100 ; ..XXXX.. head
92+ defb %00011000 ; ...XX... neck
93+ defb %01111110 ; .XXXXXX. arms
94+ defb %00011000 ; ...XX... body
95+ defb %00011000 ; ...XX... body
96+ defb %00100100 ; ..X..X.. legs
97+ defb %01000010 ; .X....X. feet
9798
9899 end start
The complete program
; Gloaming — Unit 2: The Lamplighter
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-02 gives the cell a shape — the eight bytes become the figure.

            org     32768

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

; --- where the lamplighter stands: a cell named by (column 0-31, row 0-23) ---
LAMP_COL    equ     15
LAMP_ROW    equ     11

; The screen splits top/middle/bottom into THIRDS of 8 character rows. The top
; pixel-row of a cell lives at $4000 + third*$0800 + (row-within-third)*32 + col.
THIRD       equ     LAMP_ROW / 8
CHARROW     equ     LAMP_ROW - THIRD * 8
LAMP_SCR    equ     $4000 + THIRD * $0800 + CHARROW * 32 + LAMP_COL

; The attribute cell for the same (col,row) is the simpler linear address.
LAMP_ATTR_ADDR equ  $5800 + LAMP_ROW * 32 + LAMP_COL

start:
            ; --- the border goes black — the night beyond the square ---
            ld      a, 0
            out     ($FE), a

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

            ; --- top and bottom walls ---
            ld      hl, $5800
            ld      b, 32
.top:
            ld      (hl), WALL
            inc     hl
            djnz    .top

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

            ; --- left and right walls ---
            ld      hl, $5800
            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

            ; --- give the figure's cell a warm colour so its pixels read ---
            ld      hl, LAMP_ATTR_ADDR
            ld      (hl), LAMP_ATTR

            ; --- draw his eight-byte shape down the eight rows of the cell ---
            ; HL walks the screen rows (INC H = down one row, +256).
            ; DE walks the sprite bytes (INC DE = next row of the shape).
            ld      hl, LAMP_SCR    ; top pixel-row of his cell
            ld      de, lamplighter ; his shape, eight bytes
            ld      b, 8            ; eight rows
.draw:
            ld      a, (de)         ; one row of the shape
            ld      (hl), a         ; into the screen
            inc     de              ; next shape byte
            inc     h               ; next screen row down (+256)
            djnz    .draw

.loop:
            halt
            jr      .loop

; The lamplighter's shape — eight bytes, one per pixel row. A 1 bit is a lit
; pixel (drawn in the cell's INK); a 0 bit shows the PAPER behind. Read the
; bytes top-down and the little figure stands up.
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

The block becomes a figure — a head, arms, a body, two legs — standing in the centre of the square:

The walled square with a small white human figure standing in the centre cell.
The lamplighter, drawn from eight bytes. He doesn't move yet — that's Unit 3 — but he is there, every pixel one you chose.

He doesn't move yet (that's Unit 3), but he is there, drawn from eight bytes you wrote by hand. The final halt/jr loop holds the frame, exactly as before.

When it's wrong, see why

A figure drawn wrong points straight at which part of the draw slipped:

  • A vertical smear, or a scrambled mess instead of a block. The row-walk used the wrong step. Within a cell the rows are 256 apart — only inc h walks them. inc hl or add hl, 32 steps by bytes or character-rows and scatters the shape.
  • Nothing in the cell at all. Either the bytes are all zero, or the cell's INK equals its PAPER (both black) so lit pixels are invisible. Check the attribute has a non-black INK in bits 0–2 — that's why Milestone 1 fills solid first, so a blank cell means the colour, not the shape.
  • The block (or figure) is off by whole rows. The cell address. The thirds formula is sensitive to which third the row falls in; re-check the row number, and have the assembler's listing print the computed address if unsure.
  • The walls or cobbles vanished. Pixels went into attribute memory ($5800+) by mistake. Pixels go to the bitmap ($4000+); colour goes to attributes. Different addresses entirely.

Before and after

You started with an empty square and finished with a figure standing in it — built the honest way round. Milestone 1 filled the cell solid to prove the address and the INC H walk; Milestone 2 kept every line of that draw and changed only the eight bytes it copies. That split is the whole lesson: placement and shape are separate problems, and solving placement first makes shape just data.

Try this: move him

Change LAMP_COL and LAMP_ROW — try LAMP_ROW equ 4 (the top third) or LAMP_ROW equ 20 (the bottom). The figure jumps to the new cell, and you never touched the address: the equ formula recomputed it. Watch LAMP_ROW equ 7 then 8 — that's the boundary between the top and middle thirds, and the formula steps across it for you.

Try this: change his shape

Edit the eight defb bytes in step 2. Give him a hat (%01111110 on the top row), or a wider stance (%10000001 for the feet). Sketch the eight rows on paper first, write down the byte for each, then predict what you'll see before you run it. Each 1 is a pixel of INK; each 0 is PAPER.

Try this: a warmer figure

Change LAMP_ATTR from %01000111 (bright white INK) to %01000110 (bright yellow) — a lamplighter who already carries a little warmth. The pixels don't change; only the INK colour they're drawn in does. The bitmap holds the shape; the attribute holds the colour. Two separate things, as ever.

What you've learnt

  • Pixels live in the bitmap ($4000$57FF), separate from the attribute colour at $5800+. A cell is described by both at once.
  • The Spectrum's screen is non-linear: the eight rows of one cell are 256 bytes apart, so INC H steps down a cell — the layout's awkwardness turned into one instruction.
  • A sprite is eight bytes, one per row; 1 bits draw in INK, 0 bits show PAPER.
  • Placement before shape — fill the cell solid to prove the address and the walk, then the shape is only the eight bytes you copy.
  • Let the assembler compute a cell's address from (col, row) with an equ — you write the formula once, it does the sum.

What's next

The lamplighter stands still because nothing tells him to do otherwise — the program draws him once and idles. In Unit 3 we replace that idle loop with the real thing: the frame-locked game loop, a 50 Hz heartbeat (IM 1 + HALT) that every later unit beats to. It's the first of Gloaming's two big techniques, and the moment the square starts to feel alive.