Skip to content
Game 1 Unit 1 of 20 1 hr learning time

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.

5% of Gloaming

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 square framed by a blue rectangular wall, on a black background — the town square at dusk, drawn purely in attribute colour.
The finished square: a black field framed in blue, sitting in the black of the coming night. No pixels drawn — every block of colour is a single byte of attribute memory.

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.

CellLookAttribute 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:

An entirely black Spectrum screen with a black border.
The cobbles, washed in. Black PAPER on a black border — so you can't yet tell the fill ran at all. That's exactly why the walls come next: they prove it.

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.

Step 2: fill the top and bottom rows with wall
+17-3
99
1010 start:
1111 ; --- the border goes black — the night beyond the square ---
12- ; Port $FE bits 0-2 set the BORDER colour. A = 0 = black.
1312 ld a, 0
1413 out ($FE), a
1514
1615 ; --- 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.
1916 ld hl, $5800 ; first attribute cell
2017 ld de, $5801 ; one cell forward
2118 ld (hl), COBBLE ; seed the cascade
2219 ld bc, 767 ; the remaining cells
2320 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
2438
2539 .loop:
2640 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:

A black Spectrum screen with a solid blue horizontal bar near the top and another near the bottom.
Top and bottom walls, one DJNZ loop each. The black between them is the cobble fill from step 1 — now you can see it was there all along.

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.

Step 3: walk the rows, writing the left and right columns
+16
3535 ld (hl), WALL
3636 inc hl
3737 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
3854
3955 .loop:
4056 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:

A black square framed by a complete blue rectangular wall, on a black border.
All four walls closed. The town square, walled, waiting for dusk.

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 LDIR didn't cascade. The seed and the cascade have to agree: write $01 to $5800, set DE to $5801 (one cell ahead of HL), and count BC as 767. If DE isn't HL+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 .loop to 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),A sets the border; LDIR is the block-fill; DJNZ is 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.