Room from Loops
Use DJNZ to draw rooms with loops instead of individual writes — the same instruction, fewer lines, bigger rooms.
The Unit 1 room was five cells wide and took 25 individual writes. What about a room nine cells wide? Or twenty? Writing each cell by hand doesn’t scale. You need a loop.
The Z80 has an instruction designed exactly for this: DJNZ — Decrement B and Jump if Not Zero. Load a count into B, write a cell, increment the address, and DJNZ loops back until B hits zero. One loop replaces an entire row of writes.
Filling a Row
The simplest loop fills an entire row with one colour:
; Shadowkeep — DJNZ: Fill a row
org 32768
start:
ld a, 0
out ($fe), a ; Black border
; Clear screen
ld hl, $4000
ld de, $4001
ld bc, 6911
ld (hl), 0
ldir
; Fill row 12 with blue (all 32 columns)
ld hl, $5980 ; $5800 + (12 x 32)
ld b, 32 ; 32 cells
ld a, $09 ; Solid blue
.fill: ld (hl), a ; Write attribute
inc hl ; Next cell
djnz .fill ; B = B - 1, loop if not zero
.loop: halt
jr .loop
end start
Three instructions do the work:
.fill: ld (hl), a ; Write attribute
inc hl ; Next cell
djnz .fill ; B = B - 1, loop if not zero
ld (hl), a writes the colour value in A to the address held in HL. inc hl advances HL to the next cell. djnz .fill subtracts 1 from B and jumps back to .fill if B isn’t zero yet.
Before the loop, B holds 32 — one for every column. After 32 iterations, B reaches zero, DJNZ falls through, and the entire row is filled.
Assemble and run this snippet. Row 12 turns solid blue — 32 cells, 3 instructions.
Why HL?
In Units 1 and 2, you wrote to fixed addresses: ld ($594e), a. That works for individual cells but won’t work inside a loop — you need the address to advance each time.
The Z80 can use register pairs as pointers. HL is the most common one. ld (hl), a means “store A at the address held in HL.” inc hl advances the pointer by one byte — which, in attribute memory, means the next cell to the right.
Other register pairs (BC, DE) can also hold addresses, but HL has the most instructions built around it. You’ll use HL as your primary pointer throughout Shadowkeep.
Drawing a Middle Row
A wall row is simple — every cell is the same colour. A middle row has structure: wall on the left, floor in the middle, wall on the right.
; Draw one middle row: wall, floor, floor..., wall
;
; Row 11, cols 12-20 (9 cells wide)
ld hl, $596c ; Row 11, col 12
ld a, WALL
ld (hl), a ; Left wall
inc hl
ld a, FLOOR
ld b, 7 ; 7 floor cells (cols 13-19)
.floor: ld (hl), a
inc hl
djnz .floor
ld a, WALL
ld (hl), a ; Right wall (col 20)
The pattern is straightforward:
- Write one wall cell (left edge)
- Advance HL past it
- Load FLOOR into A, loop 7 times for the floor
- Write one wall cell (right edge)
The DJNZ loop handles the floor — 7 cells wide. The two walls are single writes before and after the loop. This scales: change the 7 to 20 and you get a room 22 cells wide. The structure of the code doesn’t change, only the count.
Named Constants
Hard-coded numbers make code difficult to modify. Named constants fix this:
ROOM_WIDTH equ 9 ; Total width including walls
ROOM_INNER equ 7 ; Floor width (width - 2 walls)
Now the loop uses ld b, ROOM_INNER instead of ld b, 7. Change ROOM_INNER once and every floor loop updates. This matters when the room dimensions change — and they will.
The equ directive doesn’t generate any code. It tells the assembler “whenever you see ROOM_INNER, substitute 7.” The assembled output is identical.
The Complete Room
The program draws a 9×5 room — top wall, three middle rows, bottom wall:
; ============================================================================
; SHADOWKEEP — Unit 3: Room from Loops
; ============================================================================
; Draws a 9x5 room using DJNZ loops instead of individual writes.
;
; DJNZ = Decrement B and Jump if Not Zero.
; One loop fills an entire row. The room that took 25 writes in Unit 1
; now takes far fewer instructions.
; ============================================================================
org 32768
; Attribute values
WALL equ $09 ; PAPER 1 (blue) + INK 1
FLOOR equ $38 ; PAPER 7 (white) + INK 0
TREASURE equ $70 ; BRIGHT + PAPER 6 (yellow)
HAZARD equ $90 ; FLASH + PAPER 2 (red)
; Room dimensions
ROOM_TOP equ 10 ; Top wall row
ROOM_LEFT equ 12 ; Left wall column
ROOM_WIDTH equ 9 ; Total width including walls
ROOM_INNER equ 7 ; Floor width (width - 2 walls)
; ----------------------------------------------------------------------------
; Entry point
; ----------------------------------------------------------------------------
start:
; Black border
ld a, 0
out ($fe), a
; Clear screen
ld hl, $4000
ld de, $4001
ld bc, 6911
ld (hl), 0
ldir
; ==================================================================
; Draw the room
; ==================================================================
; --- Top wall (row 10, cols 12-20) ---
ld hl, $594c ; $5800 + (10 x 32) + 12
ld b, ROOM_WIDTH
ld a, WALL
.top: ld (hl), a
inc hl
djnz .top
; --- Row 11: wall, floor, wall ---
ld hl, $596c ; Row 11, col 12
ld a, WALL
ld (hl), a ; Left wall
inc hl
ld a, FLOOR
ld b, ROOM_INNER
.r11: ld (hl), a
inc hl
djnz .r11
ld a, WALL
ld (hl), a ; Right wall
; --- Row 12: wall, floor, wall ---
ld hl, $598c ; Row 12, col 12
ld a, WALL
ld (hl), a
inc hl
ld a, FLOOR
ld b, ROOM_INNER
.r12: ld (hl), a
inc hl
djnz .r12
ld a, WALL
ld (hl), a
; --- Row 13: wall, floor, wall ---
ld hl, $59ac ; Row 13, col 12
ld a, WALL
ld (hl), a
inc hl
ld a, FLOOR
ld b, ROOM_INNER
.r13: ld (hl), a
inc hl
djnz .r13
ld a, WALL
ld (hl), a
; --- Bottom wall (row 14, cols 12-20) ---
ld hl, $59cc ; Row 14, col 12
ld b, ROOM_WIDTH
ld a, WALL
.bot: ld (hl), a
inc hl
djnz .bot
; --- Place treasure and hazard ---
ld a, TREASURE
ld ($5990), a ; Row 12, col 16 (centre of room)
ld a, HAZARD
ld ($59af), a ; Row 13, col 15
; --- Done ---
.loop: halt
jr .loop
end start

The room is wider than Unit 1’s version, but the code is shorter. Five DJNZ loops replace what would have been 45 individual writes. The top and bottom walls each take one loop. Each middle row takes a loop for the floor plus two single writes for the side walls.
Notice the three middle rows are nearly identical — same pattern, different starting addresses. That repetition is a hint. In a later unit, you’ll use an outer loop to avoid writing each row separately. For now, seeing the repetition helps you understand what the outer loop will replace.
Try This: Wider Room
Change the constants to make a wider room:
ROOM_LEFT equ 8 ; Move left edge to column 8
ROOM_WIDTH equ 17 ; 17 cells wide
ROOM_INNER equ 15 ; 15 floor cells
You’ll also need to recalculate the starting addresses. Row 10, column 8:
$5800 + (10 × 32) + 8 = $5948
Update all five ld hl instructions to use the new column offset. The DJNZ loops handle the wider rows automatically — only the counts and addresses change.
Try This: Taller Room
Add rows 15 and 16 as middle rows, and move the bottom wall to row 17. You’ll need two more middle-row blocks (copy the pattern from rows 11–13) and a new bottom wall address:
Row 15: $5800 + (15 × 32) + 12 = $59EC
Row 16: $5800 + (16 × 32) + 12 = $5A0C
Row 17: $5800 + (17 × 32) + 12 = $5A2C (bottom wall)
The room grows taller but the code pattern stays the same. Each new row is the same wall-floor-wall structure.
Try This: Row of Treasure
Replace one middle row’s floor loop with treasure cells:
ld a, TREASURE
ld b, ROOM_INNER
.treasure: ld (hl), a
inc hl
djnz .treasure
An entire row of bright yellow. When the game has movement and collision detection, you’ll collect these.
If It Doesn’t Work
- Row appears in the wrong place? Check the starting address. Each row is 32 bytes apart. Row 10 starts at $5800 + 320 = $5940, plus the column offset.
- Loop runs too many or too few times? B must be set before the loop label. If
ld b, ROOM_WIDTHis inside the loop, B resets every iteration and the loop never ends. - Only part of the row fills? Make sure
inc hlis inside the loop. Without it, every iteration writes to the same cell. - Floor overwrites the wall? The left wall must be written before the floor loop starts, and HL must be incremented past it before the loop.
- DJNZ jumps to the wrong label? Each loop needs its own label.
.top,.r11,.r12— reusing a label causes the assembler to error or the wrong loop to repeat.
What You’ve Learnt
- DJNZ label — decrement B and jump if not zero. The Z80’s built-in counted loop. B is always the counter.
- LD (HL), A — store A at the address held in HL. HL acts as a pointer into memory.
- INC HL — advance the pointer by one byte. In attribute memory, this moves one cell to the right.
- Loop structure — set HL to the start address, B to the count, A to the value, then loop: write, advance, decrement.
- EQU constants — named values that make code readable and easy to change. No code is generated.
- Pattern recognition — the three identical middle rows hint at a future optimisation. Repetition in code is a sign that a loop could replace it.
What’s Next
The room is drawn but nothing moves. In Unit 4, you’ll read the keyboard with IN A,($FE) and move a marker around the screen — the first step toward a player character exploring the maze.