Skip to content
Game 1Unit 3 of 201 hr learning time

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.

15% of Gloaming

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:

$3C$3C$18$7E$18$18$24$42
The lamplighter, top row first: the hood ($3C twice), the neck ($18), arms out with the pole ($7E), the body, and the feet stepping apart.

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:

Step 1: blit the figure into his cell — shape only, no colour
+56
77 COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
88 WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
99 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
1013
1114 start:
1215 ; --- the border goes black — the night beyond the square ---
1316 ; Port $FE bits 0-2 set the BORDER colour. A = 0 = black.
1417 ld a, 0
1518 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
1626
1727 ; --- wipe the canvas ---
1828 ; The bitmap ($4000-$57FF) is the pixel layer; whatever was on
...
4050 ; Now that the wall cells are painted, fill_walls can read the
4151 ; map back and lay brick wherever the wall bit is set.
4252 call fill_walls
53+ call draw_lamp
4354
4455 forever:
4556 jr forever
...
218229 ld e, a
219230 ld d, 0
220231 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
221258 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
222278
223279 end start
224280

Run it, and look carefully — centre of the square, row 11:

The stippled square with the lamplighter drawn at the centre in the same dark blue as the cobble stipple — the figure is barely distinguishable from the ground around him.
He's there — dead centre — and you can hardly see him. His pixels borrow the cell's INK, and the cell is still dressed as ground: cobble blue on black, the same as every stipple dot around him.

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:

Step 2: the cell's attribute becomes his — bright white on black
+7-1
77 COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
88 WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
99 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
1011
1112 START_COL equ 15 ; where the lamplighter begins
1213 START_ROW equ 11
...
244245 ret
245246
246247 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
248254 call pos_bc
249255 call scr_addr_cr
250256 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 stippled square with the lamplighter standing bright white at the centre, clearly visible against the dark ground.
The same pixels, his own colour. One attribute write separates a smudge in the stipple from the game's protagonist.

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_lamp never ran, or the position variables hold rubbish. Check the setup writes START_COL/START_ROW into lamp_col/lamp_row before call draw_lamp, and that pos_bc reads row into B, column into C — 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_lamp is aiming somewhere else. Check it feeds scr_addr_cr the position (via pos_bc), points DE at lamplighter, and walks the rows with inc h.
  • He’s there but still blue. The attribute write is missing or aimed wrong. It goes through attr_addr_cr (the $5800 table), not scr_addr_cr — a colour written into the bitmap is just four stray pixels.
  • The figure is upside down or scrambled. The defb order. 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.

Output
The lamplighter, editable. Draw, then copy the bytes into the glyph.

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 defb bytes, drawn with the machinery you already had.
  • Position is data: lamp_col/lamp_row are the only place he is; pos_bc hands 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_lamp fetches 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.