The Lamplighter
Draw the game's first character — eight bytes of figure blitted like any texture, his position held as data, and his own attribute colour to lift him out of the dark.
The square is built and the dusk is settling into it. Someone has to light the lamps. This unit draws him — and on this machine, the surprise is how little new machinery that takes. You already own almost everything a character needs.
A character is a texture with a name
Unit 2 taught the whole trick: eight defb bytes, stamped into a cell’s bitmap by blit_tex’s inc h walk. A character’s shape is exactly the same kind of data. Here is the lamplighter — hood, then neck, arms spread with the pole, body, and feet planted apart on the cobbles:
What makes him a character rather than scenery isn’t the drawing. It’s two things this unit adds around the drawing: his position lives in data, and his cell gets a colour that is his.
Position is data
The textures went everywhere; the lamplighter is somewhere. His cell lives in two variables:
lamp_col: defb START_COL ; 15
lamp_row: defb START_ROW ; 11
Nothing in the drawing code knows the numbers. pos_bc reads the variables into the (C, B) registers that scr_addr_cr and attr_addr_cr already expect, and draw_lamp asks it — twice, once per address — rather than remembering anything itself. That indirection looks like ceremony today, drawing one figure in one place. It’s the whole future: when he moves, in Unit 6, changing the data will be changing where he is. The setup writes START_COL and START_ROW into the variables before anything draws, and everything downstream just asks.
Milestone 1 — the figure, in borrowed clothes
Draw his shape and nothing else — the same eight-byte stamp blit_tex does, aimed at his cell:
| 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 | 9 | WALL_BIT equ 3 ; the attribute bit that says "this is wall" | |
| 10 | + | | |
| 11 | + | START_COL equ 15 ; where the lamplighter begins | |
| 12 | + | START_ROW equ 11 | |
| 10 | 13 | | |
| 11 | 14 | start: | |
| 12 | 15 | ; --- the border goes black — the night beyond the square --- | |
| 13 | 16 | ; Port $FE bits 0-2 set the BORDER colour. A = 0 = black. | |
| 14 | 17 | ld a, 0 | |
| 15 | 18 | out ($FE), a | |
| 19 | + | | |
| 20 | + | ; --- place the lamplighter --- | |
| 21 | + | ; His position is data. Everything that draws him reads it. | |
| 22 | + | ld a, START_COL | |
| 23 | + | ld (lamp_col), a | |
| 24 | + | ld a, START_ROW | |
| 25 | + | ld (lamp_row), a | |
| 16 | 26 | | |
| 17 | 27 | ; --- wipe the canvas --- | |
| 18 | 28 | ; The bitmap ($4000-$57FF) is the pixel layer; whatever was on | |
| ... | |||
| 40 | 50 | ; Now that the wall cells are painted, fill_walls can read the | |
| 41 | 51 | ; map back and lay brick wherever the wall bit is set. | |
| 42 | 52 | call fill_walls | |
| 53 | + | call draw_lamp | |
| 43 | 54 | | |
| 44 | 55 | forever: | |
| 45 | 56 | jr forever | |
| ... | |||
| 218 | 229 | ld e, a | |
| 219 | 230 | ld d, 0 | |
| 220 | 231 | add hl, de | |
| 232 | + | ret | |
| 233 | + | | |
| 234 | + | ; ---------------------------------------------------------------------------- | |
| 235 | + | ; The lamplighter's draw. | |
| 236 | + | ; ---------------------------------------------------------------------------- | |
| 237 | + | | |
| 238 | + | ; pos_bc — the lamplighter's cell into (C, B), read fresh from the data. | |
| 239 | + | pos_bc: | |
| 240 | + | ld a, (lamp_row) | |
| 241 | + | ld b, a | |
| 242 | + | ld a, (lamp_col) | |
| 243 | + | ld c, a | |
| 244 | + | ret | |
| 245 | + | | |
| 246 | + | draw_lamp: | |
| 247 | + | ; his shape, eight bytes down the cell like any texture | |
| 248 | + | call pos_bc | |
| 249 | + | call scr_addr_cr | |
| 250 | + | ld de, lamplighter | |
| 251 | + | ld b, 8 | |
| 252 | + | .dl: | |
| 253 | + | ld a, (de) | |
| 254 | + | ld (hl), a | |
| 255 | + | inc de | |
| 256 | + | inc h | |
| 257 | + | djnz .dl | |
| 221 | 258 | ret | |
| 259 | + | | |
| 260 | + | ; ---------------------------------------------------------------------------- | |
| 261 | + | ; Data. | |
| 262 | + | ; ---------------------------------------------------------------------------- | |
| 263 | + | | |
| 264 | + | lamp_col: | |
| 265 | + | defb START_COL | |
| 266 | + | lamp_row: | |
| 267 | + | defb START_ROW | |
| 268 | + | | |
| 269 | + | lamplighter: | |
| 270 | + | defb %00111100 | |
| 271 | + | defb %00111100 | |
| 272 | + | defb %00011000 | |
| 273 | + | defb %01111110 | |
| 274 | + | defb %00011000 | |
| 275 | + | defb %00011000 | |
| 276 | + | defb %00100100 | |
| 277 | + | defb %01000010 | |
| 222 | 278 | | |
| 223 | 279 | end start | |
| 224 | 280 | |
Run it, and look carefully — centre of the square, row 11:

He is almost invisible, and the almost is the lesson. Pixels have no colour of their own — they are lit or unlit, and lit pixels show whatever INK their cell’s attribute holds. His cell still wears the cobble attribute from Unit 1, so his figure is drawn in the ground’s dark blue, camouflaged in a field of dots the same colour. The shape arrived; the identity didn’t.
Milestone 2 — his own light
Three lines: compute his cell’s attribute address, and write him a colour of his own before the shape goes down. LAMP_ATTR is bright white INK on black PAPER — of everyone in this dusk, only he carries light:
| 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 | 9 | WALL_BIT equ 3 ; the attribute bit that says "this is wall" | |
| 10 | + | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — his own light | |
| 10 | 11 | | |
| 11 | 12 | START_COL equ 15 ; where the lamplighter begins | |
| 12 | 13 | START_ROW equ 11 | |
| ... | |||
| 244 | 245 | ret | |
| 245 | 246 | | |
| 246 | 247 | draw_lamp: | |
| 247 | - | ; his shape, eight bytes down the cell like any texture | |
| 248 | + | ; his colour first: the cell's attribute becomes his own — | |
| 249 | + | ; bright white on the black, his own light about him | |
| 250 | + | call pos_bc | |
| 251 | + | call attr_addr_cr | |
| 252 | + | ld (hl), LAMP_ATTR | |
| 253 | + | ; then his shape, eight bytes down the cell like any texture | |
| 248 | 254 | call pos_bc | |
| 249 | 255 | call scr_addr_cr | |
| 250 | 256 | ld de, lamplighter |
The complete program
; Gloaming — Unit 3: The Lamplighter
; Cumulative build; every step runs on its own. Narrative: the unit page.
; The figure: 8 defb bytes blitted like any texture — then given his colour.
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
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
forever:
jr forever
; ----------------------------------------------------------------------------
; 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
; ----------------------------------------------------------------------------
; 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
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
end start

The same shape, transformed by one byte. Notice what kind of byte it is: an attribute. Unit 1 said a cell’s type and its colour are the same thing, and Unit 2 used that to find the walls. Now it cuts the other way — giving a cell a colour is giving it a type. The lamplighter’s white, the walls’ blue paper, and soon the lamps’ own attributes: the game will tell everything apart by the byte its cell wears.
Assemble and run
As ever, either assembler takes the pasmo-syntax source to the same snapshot:
asm198x --dialect pasmonext --sna steps/step-02.asm -o steps/step-02.sna
pasmonext --sna steps/step-02.asm steps/step-02.sna
When it’s wrong, see why
- No figure at all, anywhere.
draw_lampnever ran, or the position variables hold rubbish. Check the setup writesSTART_COL/START_ROWintolamp_col/lamp_rowbeforecall draw_lamp, and thatpos_bcreads row intoB, column intoC— swapped registers put him at (11, 15) instead of (15, 11), or off the map entirely. - A white square, no shape. The attribute landed, the pixels didn’t — the shape half of
draw_lampis aiming somewhere else. Check it feedsscr_addr_crthe position (viapos_bc), pointsDEatlamplighter, and walks the rows withinc h. - He’s there but still blue. The attribute write is missing or aimed wrong. It goes through
attr_addr_cr(the$5800table), notscr_addr_cr— a colour written into the bitmap is just four stray pixels. - The figure is upside down or scrambled. The
defborder. The first byte is the top pixel row; check the eight lines against the pattern above. - He has a stipple dot for a hat. Harmless, and worth understanding: his glyph replaced the eight bitmap bytes of his cell, so any stipple in his cell is gone — but the cells around him still carry theirs. If a dot seems to touch him, it belongs to a neighbouring cell.
Before and after
The square was empty; now its keeper stands in it. The drawing cost almost nothing new — the glyph is Unit 2’s texture idea wearing a name, and the address routines were already on the page. What’s genuinely new is smaller and more important: position as data (two bytes the whole game will steer him by) and identity as an attribute (one byte that makes a cell his). Both are promises the coming units collect on.
Try this: redraw him
Design your own eight bytes — taller hood, a longer pole, a broader stance. Click pixels below (it starts as the shipped lamplighter); copy the defb rows over lamplighter and rebuild. Keep the silhouette readable at 8×8: single pixels vanish into the stipple, so build him from runs of two or more. If your figure reads badly, step back from the screen — if it doesn’t read there, it won’t read on a television either.
Try this: start him somewhere else
Change START_COL and START_ROW — the corner by a wall (2, 2), the bottom of the square (15, 22). One constant each, and every part of the drawing follows, because nothing but the data knew where he was. (Keep him off row 0 — that ledge belongs to the HUD — and inside the walls, or he’ll stand in the brick until Unit 9 teaches the game to mind.)
Try this: a lamplighter less bright
Swap LAMP_ATTR to %00000111 — white, but without the BRIGHT bit. Run it and compare. The Spectrum’s BRIGHT gives each colour two intensities, and the difference between the two whites is the difference between “a figure in the dusk” and “the one carrying the light”. We spent the bit deliberately.
What you’ve learnt
- A character’s shape is texture data — eight
defbbytes, drawn with the machinery you already had. - Position is data:
lamp_col/lamp_roware the only place he is;pos_bchands them to the address routines, and nothing else knows. - Pixels borrow their cell’s INK — a shape without its own attribute wears whatever the ground was wearing.
- An attribute write is an identity: one byte marks the cell as his, the way blue paper marks a wall.
draw_lampfetches the position fresh before each address calculation — cheap insurance that stays correct however the code around it grows.
What’s next
He stands in a square that holds perfectly still — because the program ends in a spin, doing nothing forever. In Unit 4 that idle spin becomes the game’s heartbeat: a loop locked to the television’s 50 Hz frame, beating once per frame whether there’s work to do or not. It looks like nothing — the screen won’t change — and you’ll build a little probe to prove the pulse is real before taking the probe out again.