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.
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.
| 1 | 1 | ; Gloaming — Unit 2: The Lamplighter | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground | |
| 8 | 8 | 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 | |
| 9 | 23 | | |
| 10 | 24 | start: | |
| 11 | 25 | ; --- the border goes black — the night beyond the square --- | |
| ... | |||
| 47 | 61 | ld de, 32 | |
| 48 | 62 | add hl, de | |
| 49 | 63 | 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 | |
| 50 | 81 | | |
| 51 | 82 | .loop: | |
| 52 | 83 | halt | |
| 53 | 84 | 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 | |
| 54 | 97 | | |
| 55 | 98 | end start | |
| 56 | 99 | |
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:
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.
| 1 | 1 | ; Gloaming — Unit 2: The Lamplighter | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| ... | |||
| 67 | 67 | ld (hl), LAMP_ATTR | |
| 68 | 68 | | |
| 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 --- | |
| 70 | 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). | |
| 71 | + | ; DE walks the sprite bytes (INC DE = next row of the shape). | |
| 72 | 72 | 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 | |
| 74 | 74 | ld b, 8 ; eight rows | |
| 75 | 75 | .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 | |
| 79 | 79 | inc h ; next screen row down (+256) | |
| 80 | 80 | djnz .draw | |
| ... | |||
| 84 | 84 | jr .loop | |
| 85 | 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. | |
| 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. | |
| 88 | 89 | 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 | |
| 97 | 98 | | |
| 98 | 99 | 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:
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 hwalks them.inc hloradd hl, 32steps 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 Hsteps down a cell — the layout's awkwardness turned into one instruction. - A sprite is eight bytes, one per row;
1bits draw in INK,0bits 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 anequ— 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.