The Empty Square
Build the town square in three runnable steps — hold the border black, wash the screen in dark cobbles, frame it in blue walls — drawn entirely in colour, one 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 and the lamps come later. Today, the map is pure colour.
The screen is the map
The Spectrum's 256×192 screen is a grid of 32×24 character cells, each 8 pixels square. Every cell has exactly one INK (foreground) and one PAPER (background) colour — two colours, no more. Those colours live in their own block of memory, attribute memory, from $5800 to $5AFF: one byte per cell, 768 bytes in all.
Attribute memory: $5800 → $5AFF (768 bytes, one per cell)
col: 0 1 2 ... 31
row 0 $5800 $5801 $5802 ... $581F
row 1 $5820 $5821 ... $583F
...
row 23 $5AE0 ... $5AFF ← last row
The address of any cell is $5800 + row*32 + col. Write a byte there and that cell takes those colours immediately — no redraw, no refresh. The screen is the map: change the map by changing memory.
The attribute byte
One byte, packed tight:
bit: 7 6 5 4 3 2 1 0
FLASH BRIGHT P P P I I I
└ PAPER (0-7) ┘ └ INK (0-7) ┘
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 drawn, only the PAPER shows, so each cell is a solid block: cobbles black, walls blue.
Milestone 1 — wash in the cobbles
Two writes. 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 cobbles. Instead of writing 768 cells by hand, we seed the first cell with $01, point DE one cell ahead of HL, and let the Z80's LDIR cascade copy that single byte through all 768 cells — the block-fill idiom you'll use forever. One instruction, 767 copies.
; 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
; --- wash the whole grid in cobbles ---
; Seed $5800 with COBBLE, point DE one cell on, and let LDIR
; cascade that single byte through all 768 attribute cells.
ld hl, $5800 ; first attribute cell
ld de, $5801 ; one cell forward
ld (hl), COBBLE ; seed the cascade
ld bc, 767 ; the remaining cells
ldir
.loop:
halt
jr .loop
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 fill 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: the top row (32 cells from $5800) and the bottom row (32 cells from $5800 + 23*32 = $5AE0). Each 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 to the next instruction only when B hits zero.
| 9 | 9 | | |
| 10 | 10 | start: | |
| 11 | 11 | ; --- the border goes black — the night beyond the square --- | |
| 12 | - | ; Port $FE bits 0-2 set the BORDER colour. A = 0 = black. | |
| 13 | 12 | ld a, 0 | |
| 14 | 13 | out ($FE), a | |
| 15 | 14 | | |
| 16 | 15 | ; --- wash the whole grid in cobbles --- | |
| 17 | - | ; Seed $5800 with COBBLE, point DE one cell on, and let LDIR | |
| 18 | - | ; cascade that single byte through all 768 attribute cells. | |
| 19 | 16 | ld hl, $5800 ; first attribute cell | |
| 20 | 17 | ld de, $5801 ; one cell forward | |
| 21 | 18 | ld (hl), COBBLE ; seed the cascade | |
| 22 | 19 | ld bc, 767 ; the remaining cells | |
| 23 | 20 | ldir | |
| 21 | + | | |
| 22 | + | ; --- top and bottom walls --- | |
| 23 | + | ; Top row: 32 cells from $5800. | |
| 24 | + | ld hl, $5800 | |
| 25 | + | ld b, 32 | |
| 26 | + | .top: | |
| 27 | + | ld (hl), WALL | |
| 28 | + | inc hl | |
| 29 | + | djnz .top | |
| 30 | + | | |
| 31 | + | ; Bottom row: 32 cells from $5800 + 23*32 = $5AE0. | |
| 32 | + | ld hl, $5AE0 | |
| 33 | + | ld b, 32 | |
| 34 | + | .bottom: | |
| 35 | + | ld (hl), WALL | |
| 36 | + | inc hl | |
| 37 | + | djnz .bottom | |
| 24 | 38 | | |
| 25 | 39 | .loop: | |
| 26 | 40 | halt |
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 every one of the 24 rows. So we walk down the rows: write the first cell, jump 31 cells to write the last, then add 32 to reach the next row down, and DJNZ twenty-four times.
| 35 | 35 | ld (hl), WALL | |
| 36 | 36 | inc hl | |
| 37 | 37 | djnz .bottom | |
| 38 | + | | |
| 39 | + | ; --- left and right walls --- | |
| 40 | + | ; Walk all 24 rows, writing the first cell (col 0) and the | |
| 41 | + | ; last cell (col 31) of each. | |
| 42 | + | ld hl, $5800 | |
| 43 | + | ld b, 24 | |
| 44 | + | .sides: | |
| 45 | + | ld (hl), WALL ; col 0 of this row | |
| 46 | + | push hl | |
| 47 | + | ld de, 31 | |
| 48 | + | add hl, de | |
| 49 | + | ld (hl), WALL ; col 31 of this row | |
| 50 | + | pop hl | |
| 51 | + | ld de, 32 | |
| 52 | + | add hl, de ; advance to the next row | |
| 53 | + | djnz .sides | |
| 38 | 54 | | |
| 39 | 55 | .loop: | |
| 40 | 56 | halt |
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 ---
ld a, 0
out ($FE), a
; --- wash the whole grid in cobbles ---
ld hl, $5800 ; first attribute cell
ld de, $5801 ; one cell forward
ld (hl), COBBLE ; seed the cascade
ld bc, 767 ; the remaining cells
ldir
; --- top and bottom walls ---
; Top row: 32 cells from $5800.
ld hl, $5800
ld b, 32
.top:
ld (hl), WALL
inc hl
djnz .top
; Bottom row: 32 cells from $5800 + 23*32 = $5AE0.
ld hl, $5AE0
ld b, 32
.bottom:
ld (hl), WALL
inc hl
djnz .bottom
; --- left and right walls ---
; Walk all 24 rows, writing the first cell (col 0) and the
; last cell (col 31) of each.
ld hl, $5800
ld b, 24
.sides:
ld (hl), WALL ; col 0 of this row
push hl
ld de, 31
add hl, de
ld (hl), WALL ; col 31 of this row
pop hl
ld de, 32
add hl, de ; advance to the next row
djnz .sides
.loop:
halt
jr .loop
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 halt/jr loop just keeps the frame on screen. (That idle loop becomes the real game loop in Unit 3.)
Assemble and run
Each step is a complete program. Assemble any one to a .sna snapshot and load it in your emulator — for the finished square:
pasmonext --sna steps/step-03.asm steps/step-03.sna
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-four times.
- Garbled pixels inside the cells. Something wrote to the bitmap (
$4000–$57FF) instead of attributes ($5800+). This unit only ever touches attribute memory and the border — check every address starts$58,$59, or$5A. - Nothing on screen at all. The program ran off the end into ROM. Every step ends with
halt/jr .loopto 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. 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 ($5820), 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 the gate
The lamplighter has to get in. After step 3, 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 gate. (We'll start the lamplighter there in Unit 5.)
What you've learnt
- The screen is memory: attribute bytes at
$5800–$5AFF, one per 8×8 cell, decide colour with no redraw. - 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 empty. In Unit 2 we draw the first thing that isn't just colour — the lamplighter, an 8×8 figure poked pixel-by-pixel into a single cell. That's where the screen stops being a grid of blocks and starts holding a character.