The Empty Square
Build the town square in three runnable steps — hold the border black, wipe the pixel canvas, wash in dark cobbles and frame them in blue walls — one attribute byte per cell, the screen itself the map.
The lamps are out. Night is coming to the square, and you are the lamplighter — but before a single lamp, before you can even take a step, there has to be a square.
On the ZX Spectrum you do not draw the square. You write it. The screen is memory, and colour is just bytes in a table; set a byte, a cell changes colour. So your first Z80 program is a handful of writes to memory — and a walled square appears out of the dark. We build it in three steps, and you can run the program after each one.
What you’ll build by the end

A black field — the cobbles underfoot — framed by a blue wall, sitting in the black of the coming night. No pixels are drawn yet; every block of colour is one byte of attribute memory. The texture arrives in Unit 2, the lamps later still. Today, the map is pure colour.
The screen is two memories
The Spectrum’s 256×192 screen is a grid of 32×24 character cells, each 8 pixels square — and it lives in memory twice over:
- The bitmap,
$4000–$57FF: one bit per pixel. This is the canvas — the shapes, the texture, the detail. - The attributes,
$5800–$5AFF: one byte per cell. This is the paint — each cell gets exactly one INK (foreground) and one PAPER (background) colour, and nothing more.
Attribute memory is a simple grid, one byte per cell, read left to right and top to bottom — the address of any cell is $5800 + row*32 + col:
| Row | First cell (col 0) | Last cell (col 31) |
|---|---|---|
| 0 | $5800 |
$581F |
| 1 | $5820 |
$583F |
| ⋮ | ⋮ | ⋮ |
| 23 | $5AE0 |
$5AFF |
Write a byte anywhere in that grid and the cell takes those colours immediately — no redraw, no refresh. The screen is the map: change the map by changing memory.
This unit works entirely in the paint. The canvas we simply wipe — whatever was on screen before our program ran is still sitting in the bitmap, and we want a bare stage. Unit 2 comes back for the canvas properly.
The attribute byte
One byte, packed tight — FLASH and BRIGHT up top, then three bits of PAPER and three of INK, each holding a colour number from 0 (black) to 7 (white):
This unit lays just the first two cell types. The rest — unlit lamp, lit lamp, the lamplighter, the draught — join the table as the game grows.
| Cell | Look | Attribute byte |
|---|---|---|
| Cobbles (you can walk here) | Black PAPER, blue INK | $01 = %0000_0001 |
| Wall (you cannot) | Blue PAPER, white INK | $0F = %0000_1111 |
With no pixels set, only the PAPER shows, so each cell is a solid block: cobbles black, walls blue.
Milestone 1 — wipe the canvas, wash in the cobbles
Three writes, each to a different kind of place. First the border — the edge outside the 256×192 screen, set by port $FE (bits 0–2). We hold it black: the night beyond the square. (It must not be blue — the walls are blue, and a blue border would swallow the wall frame whole.)
Then the canvas. clear_bitmap zeroes all 6,144 bytes of the bitmap by seeding the first byte and letting the Z80’s LDIR cascade copy it forward — the block-fill idiom you’ll use forever. One instruction, six thousand copies.
Then the cobbles, and it’s the same idiom again an address-range over: seed $5800 with the cobble byte, point DE one cell ahead of HL, and LDIR washes all 768 cells.
; Gloaming — Unit 1: The Empty Square
; Cumulative build; every step runs on its own. Narrative: the unit page.
; Cells: COBBLE $01 (PAPER black / INK blue), WALL $0F (PAPER blue / INK white).
org 32768
COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
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
; --- 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
; --- 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
forever:
jr forever
; ----------------------------------------------------------------------------
; 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
end start
Run it and the screen goes black:

That black screen is honest, not a mistake: cobbles are black PAPER, the border is black, so a screen full of cobbles is a black screen. You can’t see that the fills worked — which is the whole reason the next step draws something you can.
Milestone 2 — the top and bottom walls
Now something visible. Two edges of the frame are whole rows — but look where the top wall goes: row 1, from $5820, not row 0. The square keeps its top row of cells back, plain cobble, and later in the game that ledge becomes the HUD — the row where your progress and your lives will live. The bottom wall is row 23, 32 cells from $5800 + 23*32 = $5AE0.
Each wall is a small DJNZ counting loop — the Z80’s “do this B times” workhorse: load B with the count, do the work, djnz falls through only when B hits zero. They live in a subroutine, paint_walls, because the walls will be repainted more than once before this game is done.
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground | |
| 8 | + | WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone | |
| 8 | 9 | | |
| 9 | 10 | start: | |
| 10 | 11 | ; --- the border goes black — the night beyond the square --- | |
| ... | |||
| 26 | 27 | ld (hl), COBBLE | |
| 27 | 28 | ld bc, 767 | |
| 28 | 29 | ldir | |
| 30 | + | | |
| 31 | + | call paint_walls | |
| 29 | 32 | | |
| 30 | 33 | forever: | |
| 31 | 34 | jr forever | |
| 35 | + | | |
| 36 | + | ; ---------------------------------------------------------------------------- | |
| 37 | + | ; paint_walls — the square's edge, one attribute write per cell. | |
| 38 | + | ; ---------------------------------------------------------------------------- | |
| 39 | + | paint_walls: | |
| 40 | + | ld c, WALL ; the byte every wall cell gets | |
| 41 | + | | |
| 42 | + | ; the top wall: row 1 is 32 cells in a row from $5820 | |
| 43 | + | ; (row 0 is kept back — it becomes the HUD later) | |
| 44 | + | ld hl, $5820 | |
| 45 | + | ld b, 32 | |
| 46 | + | .wt: | |
| 47 | + | ld (hl), c | |
| 48 | + | inc hl | |
| 49 | + | djnz .wt | |
| 50 | + | | |
| 51 | + | ; the bottom wall: row 23, 32 cells from $5AE0 | |
| 52 | + | ld hl, $5AE0 | |
| 53 | + | ld b, 32 | |
| 54 | + | .wb: | |
| 55 | + | ld (hl), c | |
| 56 | + | inc hl | |
| 57 | + | djnz .wb | |
| 58 | + | ret | |
| 32 | 59 | | |
| 33 | 60 | ; ---------------------------------------------------------------------------- | |
| 34 | 61 | ; clear_bitmap — zero the pixel layer, $4000-$57FF, with the same |
Two blue bars appear across the black — proof the cobble fill did cover the screen, because the walls are landing on top of it:

Milestone 3 — close the frame
The frame needs its two sides. These aren’t a single run of memory — they’re column 0 and column 31 of each walled row, 1 through 23. So we walk down the rows: write the row’s first cell, hop 31 cells to write its last, then add 32 to reach the next row down, and DJNZ twenty-three times.
| 55 | 55 | ld (hl), c | |
| 56 | 56 | inc hl | |
| 57 | 57 | djnz .wb | |
| 58 | + | | |
| 59 | + | ; the side walls: column 0 and column 31 of rows 1-23. | |
| 60 | + | ; Write the row's first cell, hop 31 cells to its last, | |
| 61 | + | ; then step a full row (32) down — 23 times. | |
| 62 | + | ld hl, $5820 | |
| 63 | + | ld b, 23 | |
| 64 | + | .ws: | |
| 65 | + | ld (hl), c | |
| 66 | + | push hl | |
| 67 | + | ld de, 31 | |
| 68 | + | add hl, de | |
| 69 | + | ld (hl), c | |
| 70 | + | pop hl | |
| 71 | + | ld de, 32 | |
| 72 | + | add hl, de | |
| 73 | + | djnz .ws | |
| 58 | 74 | ret | |
| 59 | 75 | | |
| 60 | 76 | ; ---------------------------------------------------------------------------- |
The complete program
; Gloaming — Unit 1: The Empty Square
; Cumulative build; every step runs on its own. Narrative: the unit page.
; Cells: COBBLE $01 (PAPER black / INK blue), WALL $0F (PAPER blue / INK white).
org 32768
COBBLE equ %00000001 ; PAPER black (0), INK blue (1) — dark ground
WALL equ %00001111 ; PAPER blue (1), INK white (7) — pale stone
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
; --- 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
; --- 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
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
end start
The square has all four sides — a blue wall framing a black field, sitting in the black border:

It holds still: the final jr forever just keeps the frame on screen. (That idle spin becomes the real game loop — the heartbeat — in Unit 4.)
Assemble and run
Each step is a complete program, and the course’s sources are written in pasmo syntax — so two assemblers build them, to the same snapshot byte for byte. Asm198x is the 198x family’s own assembler, one binary that covers this and every other platform in the course:
asm198x --dialect pasmonext --sna steps/step-03.asm -o steps/step-03.sna
Or use pasmonext itself — the enhanced Pasmo the dialect is named for:
pasmonext --sna steps/step-03.asm steps/step-03.sna
Either way, load the .sna snapshot in your emulator. The program writes the whole square in well under a frame, then idles — so the moment it runs, the walled square is there.
When it’s wrong, see why
The square fails in ways that point straight at which write went wrong:
- The whole screen is blue, no black field. The
LDIRdidn’t cascade. The seed and the cascade have to agree: write$01to$5800, setDEto$5801(one cell ahead ofHL), and countBCas767. IfDEisn’tHL+1, or the count is off, the fill misbehaves. - A black screen with a black border and no walls. Either the wall loops didn’t run, or the border is hiding them. Confirm the border write loads black (
0), not blue (1) — a blue border the same colour as the walls swallows the frame. - Only top and bottom bars, no sides. The column walk. Each row needs two writes — col 0 and col 31 — and the pointer must advance by 32 (a full row) each time round, twenty-three times.
- The top wall sits flush with the screen edge. It’s on row 0 — the loop started at
$5800instead of$5820. The square deliberately keeps row 0 back for the HUD; the top wall belongs on row 1. - Stray dots or letters showing through the black. The old screen contents are still in the bitmap —
clear_bitmapdidn’t run, or its range is wrong. It must zero$4000–$57FF, all 6,144 bytes, before the attributes go on. - Nothing on screen at all. The program ran off the end into ROM. Every step ends with
jr foreverto hold the frame — make sure it’s there.
Before and after
You started with nothing — a cold machine — and finished with a walled square, built in three runnable steps. The first was invisible on purpose (cobbles black on black); the second proved it had worked; the third closed the frame. Three ideas carry the whole game from here: OUT sets the border, LDIR fills a block, DJNZ counts a loop. Every screen the game ever draws is some arrangement of those three.
Try this: cobbles of a different mood
Change the cobble seed in step 1 from $01 to $05 (PAPER black, INK cyan) — then to $41 (the BRIGHT bit set). The block stays black for now (no pixels), but you’ve changed what the ink will be when texture arrives in Unit 2. Predict the byte before you run it: which three bits are PAPER, which three are INK?
Try this: a thicker wall
The frame is one cell thick. In step 2, make the top and bottom walls two rows thick: after the top-row loop, repeat it for the next row down ($5840), and the same for the row above the bottom ($5AC0). Watch the square’s proportions change — and notice the interior shrink by exactly the cells you painted.
Try this: knock through a gate
Pick a cell on the bottom wall — say row 23, column 15, at $5AE0 + 15 — and write $01 (cobbles) over its $0F. A one-cell gap appears in the wall. The game never uses it — the lamplighter starts inside the square — but it’s worth doing once to feel the rule the whole game runs on: a cell’s type is one byte, and changing the byte changes the world.
What you’ve learnt
- The screen is two memories: the bitmap (
$4000–$57FF, the pixels) and the attributes ($5800–$5AFF, one colour byte per 8×8 cell, no redraw needed). - The attribute byte packs FLASH, BRIGHT, PAPER and INK into eight bits.
OUT ($FE),Asets the border;LDIRis the block-fill;DJNZis the counted loop.- Build in runnable steps — a milestone that shows nothing (the black fill) is still worth running, because the next one proves it worked.
- A cell’s type and its colour are the same byte — the idea the whole game is built on.
What’s next
The square is solid colour — blocks, not stone. In Unit 2 we lay the canvas under the paint: a sparse stipple of cobble pixels across the ground, brick courses on the walls. That’s where the black field starts looking like a place — and where the bitmap you wiped today earns its keep.