Skip to content
Game 2 Unit 2 of 16 1 hr learning time

The First Hall

The thief stood on flat blue. A keep is made of stone, and stone has shade. Meet dithering — mixing two colours in the bitmap to get the shades between them — and build the keep's first room: a dark slate floor inside lit stone walls.

13% of Shadowkeep

In Unit 1 the thief stood on a flat wash of blue — one attribute byte, the same colour in every cell. It did the job, but it was never going to be a keep. A keep is built of stone, and stone has texture and shade: dark in the depths, lit where the light falls. This unit gives the thief his first room, and the single trick that makes it look like stone.

That trick is dithering, and it's the most useful idea on the whole machine for getting around the Spectrum's two-colours-per-cell limit.

What you'll see by the end

A dark, finely-textured slate floor filling a room, framed by a bright blue stone wall border, with the red hooded thief standing in the centre.
The keep's first hall: dark slate floor, lit stone walls — two shades of stone, both made from the same blue and black, separated only by dither density.

A hall. A floor of dark, mottled slate, walled in by lit blue stone, the thief standing in the middle of it. Two shades of stone — and yet, look closely, both are made from the same two colours: blue and black. No new colours, no attribute trickery. Just a pattern of pixels, and an eye that does the mixing for us.

Two colours, many shades

A cell only ever holds two colours — its INK and its PAPER. We can't put a third in. But the bitmap decides, for each of the cell's 64 pixels, whether it shows INK or PAPER. So if we scatter black INK pixels across blue PAPER, the eye doesn't see individual pixels at arm's length — it blends them into a single perceived shade, somewhere between blue and black. And the density of that scatter sets where between:

PAPER blue, INK black — perceived shade by dither density:

  none     ████  flat blue            (Unit 1's wash — too bright)
  sparse   ▓▓▓▓  light, lit stone     (our walls)
  half     ▒▒▒▒  dark slate           (our floor)
  most     ░░░░  near-black shadow

This is dithering. One pair of colours, a ramp of shades — and, for free, texture: the floor stops being a flat panel and starts looking like worked stone. Everything Shadowkeep does with light and shadow later is built on this one idea.

The two stone tiles

A tile is just eight bytes — eight rows of eight pixels — drawn into a cell exactly the way Gloaming drew its lantern glyph. Here are our two, and you can read the density straight off the bits:

floor_tile:                 wall_tile:
    defb %10101010              defb %00010001
    defb %01010101              defb %00000000
    defb %10101010              defb %01000100
    defb %01010101              defb %00000000
    defb %10101010              defb %00010001
    defb %01010101              defb %00000000
    defb %10101010              defb %01000100
    defb %01010101              defb %00000000

The floor is a 50% checker — every other pixel black — which blends to dark slate. The wall scatters just a few black specks across mostly-blue, so it stays light; and we give the wall the BRIGHT bit, lifting it into a lit, pale stone. Same blue, same black; the density does the rest. Both patterns tile seamlessly — each cell is identical, and a checker continues unbroken across the joins.

WALL_ATTR   equ     %01001000       ; BRIGHT — lit stone
FLOOR_ATTR  equ     %00001000       ; dim — dark slate

Drawing a cell, then a room

draw_tile colours a cell and lays its eight bitmap rows. It's Gloaming's glyph-draw, made general — give it a row, a column, a tile and an attribute, and it draws there:

draw_tile:
            push    bc              ; keep row/column for the loop below
            call    attr_addr_cr
            ld      a, (tile_attr)
            ld      (hl), a
            call    scr_addr_cr
            ld      de, (tile_ptr)  ; fetched after the helpers — they use DE
            ld      b, 8
.draw_tile_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .draw_tile_row
            pop     bc
            ret

draw_hall then walks every cell of the screen with two nested loops — the same shape as the fill you wrote in Gloaming, row in B, column in C — and pick_tile chooses a wall tile on the edges, floor within:

pick_tile:
            ld      a, b
            or      a               ; top row?
            jr      z, .wall
            cp      LAST_ROW        ; bottom row?
            jr      z, .wall
            ld      a, c
            or      a               ; left column?
            jr      z, .wall
            cp      LAST_COL        ; right column?
            jr      z, .wall
            ; ... otherwise floor ...

A bordered rectangle of stone: walls around, slate within. The keep's first room.

The thief on the floor

The thief is drawn last, on top, exactly as in Unit 1. One honest detail: his solid sprite replaces the floor in his single cell — there's no seeing the slate through him, because cell-based drawing writes the whole cell. That's the honest version; a later game teaches masking, where a sprite keeps the background showing through its gaps. For now, he stands on the stone, and that's enough.

Milestone — build the hall

Unit 1's flat blue wash becomes a room of stone: draw_hall walks every cell, pick_tile lays a lit wall tile on the edges and a dark slate tile within, and the thief stands on the floor as before. The diff is large because a whole flat wash is replaced by a tiled room — but every new piece is the eight-row glyph-draw you already know, repeated over a grid.

Step 1: the flat blue wash becomes a dithered hall of stone
+120-21
11 ; Shadowkeep — Unit 2: The First Hall
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 = Unit 1's end: the dark keep with the thief, on a flat blue wash.
3+; step-01 builds the dithered hall — lit stone walls, dark slate floor — around him.
44
55 org 32768
66
7-STONE equ %00001000 ; PAPER 1 (blue), INK 0 — cold blue stone
7+; Two shades of stone, both PAPER 1 (blue) over INK 0 (black). The shade comes
8+; from the bitmap dither density; BRIGHT lifts the walls into the light.
9+WALL_ATTR equ %01001000 ; BRIGHT — lit stone (drawn with a sparse dither)
10+FLOOR_ATTR equ %00001000 ; dim — dark slate (drawn with a 50% dither)
811 THIEF equ %01001010 ; BRIGHT, PAPER 1 (blue), INK 2 (red) — the thief
9-HERO_COL equ 15 ; the middle of the keep, as in Gloaming
12+
13+HERO_COL equ 15
1014 HERO_ROW equ 11
15+
16+LAST_ROW equ 23
17+LAST_COL equ 31
1118
1219 ; ----------------------------------------------------------------------------
13-; SETUP — the keep is dark, and a figure stands in it.
20+; SETUP — build the hall, then stand the thief in it.
1421 ; ----------------------------------------------------------------------------
1522 start:
1623 ld a, 0
17- out ($FE), a ; black border — the keep wants the dark
18-
19- ld hl, $5800 ; wash all 768 attribute cells in stone
20- ld de, $5801
21- ld (hl), STONE
22- ld bc, 767
23- ldir
24+ out ($FE), a ; black border
2425
26+ call draw_hall
2527 call draw_thief
2628
2729 im 1
...
3133 jr .loop
3234
3335 ; ----------------------------------------------------------------------------
34-; draw_thief — colour his cell, then lay his eight rows into the bitmap.
35-; Exactly Gloaming's draw_lamp, pointed at a different eight bytes.
36+; draw_hall — every cell of the screen: a wall tile around the edge, a floor
37+; tile within. Two nested loops, row in B and column in C, exactly the shape of
38+; the fill you wrote in Gloaming — but laying eight-byte stone tiles, not one
39+; flat attribute byte.
40+; ----------------------------------------------------------------------------
41+draw_hall:
42+ ld b, 0
43+.row:
44+ ld c, 0
45+.col:
46+ call pick_tile ; sets tile_ptr + tile_attr for this cell
47+ call draw_tile ; draws it at (B, C)
48+
49+ inc c
50+ ld a, c
51+ cp LAST_COL + 1
52+ jr nz, .col
53+ inc b
54+ ld a, b
55+ cp LAST_ROW + 1
56+ jr nz, .row
57+ ret
58+
59+; pick_tile — wall if this cell is on the edge, floor otherwise. Leaves the
60+; choice in tile_ptr / tile_attr. Preserves B (row) and C (column).
61+pick_tile:
62+ ld a, b
63+ or a ; row 0?
64+ jr z, .wall
65+ cp LAST_ROW ; row 23?
66+ jr z, .wall
67+ ld a, c
68+ or a ; column 0?
69+ jr z, .wall
70+ cp LAST_COL ; column 31?
71+ jr z, .wall
72+.floor:
73+ ld hl, floor_tile
74+ ld (tile_ptr), hl
75+ ld a, FLOOR_ATTR
76+ ld (tile_attr), a
77+ ret
78+.wall:
79+ ld hl, wall_tile
80+ ld (tile_ptr), hl
81+ ld a, WALL_ATTR
82+ ld (tile_attr), a
83+ ret
84+
85+; draw_tile — colour cell (B,C), then lay its eight bitmap rows. The address
86+; helpers clobber A and DE, so the tile pointer is fetched *after* them.
87+draw_tile:
88+ push bc
89+ call attr_addr_cr
90+ ld a, (tile_attr)
91+ ld (hl), a
92+ call scr_addr_cr ; B,C still hold row, column
93+ ld de, (tile_ptr)
94+ ld b, 8
95+.draw_tile_row:
96+ ld a, (de)
97+ ld (hl), a
98+ inc de
99+ inc h
100+ djnz .draw_tile_row
101+ pop bc
102+ ret
103+
104+; ----------------------------------------------------------------------------
105+; draw_thief — unchanged from Unit 1: colour his cell, lay his eight rows.
106+; He stands on the floor; his solid sprite simply replaces the floor in his one
107+; cell (cell-based drawing — no see-through. Masking comes much later, in its
108+; own game). Save/restore in Unit 3 will protect the floor as he moves.
36109 ; ----------------------------------------------------------------------------
37110 draw_thief:
38111 ld b, HERO_ROW
39112 ld c, HERO_COL
40113 call attr_addr_cr
41114 ld (hl), THIEF
42- call scr_addr_cr ; B,C still hold the row and column
115+ call scr_addr_cr
43116 ld de, thief
44117 ld b, 8
45-.dt:
118+.draw_thief_row:
46119 ld a, (de)
47120 ld (hl), a
48121 inc de
49- inc h ; next pixel row is one cell-third down: +256
50- djnz .dt
122+ inc h
123+ djnz .draw_thief_row
51124 ret
52125
53126 ; ----------------------------------------------------------------------------
54-; scr_addr_cr / attr_addr_cr — row in B, column in C, address out in HL.
55-; Carried verbatim from Gloaming.
127+; scr_addr_cr / attr_addr_cr — carried from Gloaming. Row in B, column in C.
56128 ; ----------------------------------------------------------------------------
57129 scr_addr_cr:
58130 ld a, b
...
86158 ret
87159
88160 ; ----------------------------------------------------------------------------
89-; The hooded thief — eight bytes, drawn here so you can read the figure in the
90-; ones and zeros: a pointed hood, a cloaked body, two feet at the hem.
161+; The stone tiles. Read the bits: a 1 is a black INK pixel, a 0 is blue PAPER
162+; showing through. The floor mixes them half-and-half (dark slate); the wall
163+; scatters a few black specks across mostly-blue, BRIGHT stone (lit, light).
164+; Both patterns tile seamlessly cell to cell.
91165 ; ----------------------------------------------------------------------------
166+floor_tile: ; 50% checker — dark slate
167+ defb %10101010
168+ defb %01010101
169+ defb %10101010
170+ defb %01010101
171+ defb %10101010
172+ defb %01010101
173+ defb %10101010
174+ defb %01010101
175+
176+wall_tile: ; sparse specks — light, lit stone
177+ defb %00010001
178+ defb %00000000
179+ defb %01000100
180+ defb %00000000
181+ defb %00010001
182+ defb %00000000
183+ defb %01000100
184+ defb %00000000
185+
92186 thief:
93187 defb %00011000 ; ...XX... the hood's peak
94188 defb %00111100 ; ..XXXX.. the hood
...
98192 defb %01111110 ; .XXXXXX. the cloak
99193 defb %00111100 ; ..XXXX.. the cloak narrows
100194 defb %00100100 ; ..X..X.. two feet at the hem
195+
196+tile_ptr:
197+ defw 0
198+tile_attr:
199+ defb 0
101200
102201 end start
103202
The complete program
; Shadowkeep — Unit 2: The First Hall
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 builds the dithered hall — lit stone walls, dark slate floor — around him.

            org     32768

; Two shades of stone, both PAPER 1 (blue) over INK 0 (black). The shade comes
; from the bitmap dither density; BRIGHT lifts the walls into the light.
WALL_ATTR   equ     %01001000       ; BRIGHT — lit stone (drawn with a sparse dither)
FLOOR_ATTR  equ     %00001000       ; dim    — dark slate (drawn with a 50% dither)
THIEF       equ     %01001010       ; BRIGHT, PAPER 1 (blue), INK 2 (red) — the thief

HERO_COL    equ     15
HERO_ROW    equ     11

LAST_ROW    equ     23
LAST_COL    equ     31

; ----------------------------------------------------------------------------
; SETUP — build the hall, then stand the thief in it.
; ----------------------------------------------------------------------------
start:
            ld      a, 0
            out     ($FE), a        ; black border

            call    draw_hall
            call    draw_thief

            im      1
            ei
.loop:
            halt
            jr      .loop

; ----------------------------------------------------------------------------
; draw_hall — every cell of the screen: a wall tile around the edge, a floor
; tile within. Two nested loops, row in B and column in C, exactly the shape of
; the fill you wrote in Gloaming — but laying eight-byte stone tiles, not one
; flat attribute byte.
; ----------------------------------------------------------------------------
draw_hall:
            ld      b, 0
.row:
            ld      c, 0
.col:
            call    pick_tile       ; sets tile_ptr + tile_attr for this cell
            call    draw_tile       ; draws it at (B, C)

            inc     c
            ld      a, c
            cp      LAST_COL + 1
            jr      nz, .col
            inc     b
            ld      a, b
            cp      LAST_ROW + 1
            jr      nz, .row
            ret

; pick_tile — wall if this cell is on the edge, floor otherwise. Leaves the
; choice in tile_ptr / tile_attr. Preserves B (row) and C (column).
pick_tile:
            ld      a, b
            or      a               ; row 0?
            jr      z, .wall
            cp      LAST_ROW        ; row 23?
            jr      z, .wall
            ld      a, c
            or      a               ; column 0?
            jr      z, .wall
            cp      LAST_COL        ; column 31?
            jr      z, .wall
.floor:
            ld      hl, floor_tile
            ld      (tile_ptr), hl
            ld      a, FLOOR_ATTR
            ld      (tile_attr), a
            ret
.wall:
            ld      hl, wall_tile
            ld      (tile_ptr), hl
            ld      a, WALL_ATTR
            ld      (tile_attr), a
            ret

; draw_tile — colour cell (B,C), then lay its eight bitmap rows. The address
; helpers clobber A and DE, so the tile pointer is fetched *after* them.
draw_tile:
            push    bc
            call    attr_addr_cr
            ld      a, (tile_attr)
            ld      (hl), a
            call    scr_addr_cr     ; B,C still hold row, column
            ld      de, (tile_ptr)
            ld      b, 8
.draw_tile_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .draw_tile_row
            pop     bc
            ret

; ----------------------------------------------------------------------------
; draw_thief — unchanged from Unit 1: colour his cell, lay his eight rows.
; He stands on the floor; his solid sprite simply replaces the floor in his one
; cell (cell-based drawing — no see-through. Masking comes much later, in its
; own game). Save/restore in Unit 3 will protect the floor as he moves.
; ----------------------------------------------------------------------------
draw_thief:
            ld      b, HERO_ROW
            ld      c, HERO_COL
            call    attr_addr_cr
            ld      (hl), THIEF
            call    scr_addr_cr
            ld      de, thief
            ld      b, 8
.draw_thief_row:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .draw_thief_row
            ret

; ----------------------------------------------------------------------------
; scr_addr_cr / attr_addr_cr — carried from Gloaming. Row in B, column in C.
; ----------------------------------------------------------------------------
scr_addr_cr:
            ld      a, b
            and     %00011000
            or      %01000000
            ld      h, a
            ld      a, b
            and     %00000111
            rrca
            rrca
            rrca
            or      c
            ld      l, a
            ret

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 stone tiles. Read the bits: a 1 is a black INK pixel, a 0 is blue PAPER
; showing through. The floor mixes them half-and-half (dark slate); the wall
; scatters a few black specks across mostly-blue, BRIGHT stone (lit, light).
; Both patterns tile seamlessly cell to cell.
; ----------------------------------------------------------------------------
floor_tile:                         ; 50% checker — dark slate
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101
            defb    %10101010
            defb    %01010101

wall_tile:                          ; sparse specks — light, lit stone
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000
            defb    %00010001
            defb    %00000000
            defb    %01000100
            defb    %00000000

thief:
            defb    %00011000       ; ...XX...   the hood's peak
            defb    %00111100       ; ..XXXX..   the hood
            defb    %01111110       ; .XXXXXX.   hood meets shoulders
            defb    %01111110       ; .XXXXXX.   the cloak
            defb    %01111110       ; .XXXXXX.   the cloak
            defb    %01111110       ; .XXXXXX.   the cloak
            defb    %00111100       ; ..XXXX..   the cloak narrows
            defb    %00100100       ; ..X..X..   two feet at the hem

tile_ptr:
            defw    0
tile_attr:
            defb    0

            end     start

A dark slate hall walled in lit stone, the thief at its centre. Hold the picture from Unit 1 beside it: same blue, same black, an entirely different place.

Try this: re-shade the floor

floor_tile is a 50% checker. Make it darker — set more bits, say %11101110 / %10111011 alternating — and the floor sinks toward black, a deeper dungeon. Make it sparser — %10001000 / %00100010 — and it lifts toward the wall's shade. You're sliding the floor up and down the shade ramp by changing nothing but pixel density. Find the slate you like.

Try this: a wall that reads as blocks

Our wall is a flat sparse dither. Real keep walls have courses — stone blocks with mortar between. Design a wall_tile with a solid run along the bottom row (%11111111) and a vertical line every few pixels, so repeated cells read as a brick course. Texture and shade are the same tool used two ways: pattern for what the stone is, density for how lit it is.

Try this: a third shade

Add a RUBBLE_ATTR / rubble_tile at a density between wall and floor, and have pick_tile lay a band of it along the bottom row inside the walls — a drift of broken stone. You'll have three readable shades on screen, all from blue and black. That's the whole vocabulary the atmosphere of this keep is built from.

When it's wrong, see why

  • The floor is solid blue, no texture. draw_tile is laying the same byte eight times instead of eight different ones, or tile_ptr points at the wrong data. Each inc de must walk through the tile's eight bytes.
  • The pattern smears diagonally or streaks. The inc h after each row is what steps down the cell. With inc l you'd write along one pixel row — a streak, not a tile.
  • The whole screen is one tile (all wall, or all floor). pick_tile's edge tests are wrong. Wall when row is 0 or LAST_ROW, or column is 0 or LAST_COL; floor otherwise.
  • The border colours flicker or the room is the wrong size. LAST_ROW/LAST_COL are 23 and 31 — the last valid cell indices, not 24 and 32.
  • The checker shimmers unpleasantly on a real TV. A fine 50% dither can buzz on composite video; coarsen it to 2×2 blocks (%11001100 / %00110011) for a calmer texture. On the Next or an emulator it's rock-steady.

Before and after

You started with the thief on a flat blue wash and finished with him standing in a hall of stone — dark slate underfoot, lit walls around — and not one new colour was spent. Two shades came from one pair of colours, the eye doing the blending; texture came free in the bargain; and the whole room is the glyph-draw from Gloaming, made general and walked over a grid. The keep stopped being a colour and started being a place — on the strength of pixel density alone.

What you've learnt

  • Dithering beats the two-colour limit. Mixing INK and PAPER pixels in the bitmap makes the eye perceive shades between a cell's two colours.
  • Density is shade. Sparse INK reads light, half-and-half reads mid, dense reads dark — a whole ramp from one pair of colours.
  • A tile is eight bytes, drawn like a glyph. draw_tile is Gloaming's glyph-draw made general; a room is that draw, repeated over a grid.
  • Texture and shade are the same tool. The pattern decides what the stone is; the density decides how lit it is — the foundation for this keep's light and shadow.
  • Cell-based sprites replace their cell. The thief covers the floor under him; masking, which keeps the background, is a deliberately later lesson.

What's next

The hall is built, but the thief is rooted to the spot. In Unit 3, "A Place to Move," we bring back Gloaming's keyboard reading, cell-step movement, and wall collision — and set the thief walking the hall. He'll save and restore the dithered stone beneath him as he goes, so the floor survives his passing, and the lit walls will turn him back. The engine you finished in Gloaming, now moving a thief through a room of stone.