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

Walls

Turn the blue frame into a boundary. Read the target cell's attribute and BIT-test PAPER's bottom bit — set on walls, clear on floor — refusing the step if it's solid. Collision for free, because the map is already colour.

35% of Gloaming

The lamplighter can stand anywhere and harm nothing — so now we decide where he's allowed to stand. Until this unit, the blue frame round the square was scenery he could walk straight through. This unit makes it solid: step toward a wall and he stops.

It costs almost nothing, because of a decision we made back in Unit 1: the map is colour, and colour lives in memory. We don't need a separate table of what's solid — we read it from the attribute of the cell he's trying to enter.

Where we start

Unit 6's walker, safe over anything and stopped by nothing — he strolls clean through the blue frame and off into the dark. We give the frame teeth.

Collision is already in the colour

Look at the two cell types as bits:

COBBLE = %0000 0 001    PAPER black (0)  — floor
WALL   = %0000 1 111    PAPER blue  (1)  — solid

                  bit 3 — the bottom bit of PAPER

The walls are the only cells with a non-black PAPER. So bit 3 — the bottom bit of the PAPER field — is set on walls and clear on floor. That single bit answers the only question collision asks: can I go there?

bit  3, (hl)     ; HL = the target cell's attribute
                 ; set   → wall  → no
                 ; clear → floor → yes

No solidity table, no list of wall coordinates. The colour you already painted is the collision map. This is the Unit 1 idea — "a cell's type and its colour are the same byte" — paying for itself.

Milestone — test before you step

In Unit 5 we moved first and asked questions never. Now we ask first. Each step works out the target column (lamp_col ± 1), reads that cell's attribute and BIT 3s it, and only commits the move — restore, step, save, draw — if the bit is clear. A small helper, wall_at, takes a column and comes back with the Z flag set or clear, so the loop reads almost like English: work out the target; if wall_at, go round again; otherwise move there.

Step 1: read the target cell's attribute and refuse a wall
+47-29
11 ; Gloaming — Unit 7: Walls
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 6's walker — safe over anything, but stopped by nothing.
3+; step-01 tests the target cell's colour and refuses a step into a wall.
44
55 org 32768
66
7-COBBLE equ %00000001 ; PAPER black, INK blue — dark ground
8-WALL equ %00001111 ; PAPER blue, INK white — pale stone
7+COBBLE equ %00000001 ; PAPER black, INK blue — floor
8+WALL equ %00001111 ; PAPER blue, INK white — solid
99 LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure
10+WALL_BIT equ 3 ; PAPER bit 0: set on walls, clear on floor
1011
1112 ROW equ 11
1213 ROW_THIRD equ ROW / 8
...
5758 add hl, de
5859 djnz .sides
5960
60- call save_under ; remember what's under his start cell
61- call draw_lamp ; then draw him on top of it
61+ call save_under
62+ call draw_lamp
6263
6364 ; ============================================================================
64-; THE HEARTBEAT — restore, step, save, draw.
65+; THE HEARTBEAT — test the target cell, move only if it isn't a wall.
6566 ; ============================================================================
6667 im 1
6768 ei
...
7879 jr game_loop
7980
8081 .step_left:
81- call restore_under ; put the old cell back as it was
8282 ld a, (lamp_col)
83- dec a
84- ld (lamp_col), a
85- call save_under ; remember the new cell's contents
86- call draw_lamp ; draw him over them
83+ dec a ; A = the cell he wants to enter
84+ call wall_at ; NZ = wall, Z = clear (A preserved)
85+ jr nz, game_loop ; wall — refuse the step
86+ ld (target_col), a
87+ call commit_move
8788 jr game_loop
8889
8990 .step_right:
90- call restore_under
9191 ld a, (lamp_col)
9292 inc a
93+ call wall_at
94+ jr nz, game_loop
95+ ld (target_col), a
96+ call commit_move
97+ jr game_loop
98+
99+; ----------------------------------------------------------------------------
100+; wall_at — A = column to test. Reads that cell's attribute and BIT-tests it.
101+; Returns NZ if it's a wall, Z if walkable. Leaves A unchanged.
102+; ----------------------------------------------------------------------------
103+wall_at:
104+ ld e, a
105+ ld d, 0
106+ ld hl, ROW_ATTR
107+ add hl, de
108+ bit WALL_BIT, (hl) ; set = wall, clear = floor
109+ ret
110+
111+; ----------------------------------------------------------------------------
112+; commit_move — the move is clear: restore old cell, adopt target_col, save and
113+; draw the new one.
114+; ----------------------------------------------------------------------------
115+commit_move:
116+ call restore_under
117+ ld a, (target_col)
93118 ld (lamp_col), a
94119 call save_under
95120 call draw_lamp
96- jr game_loop
121+ ret
97122
98123 ; ----------------------------------------------------------------------------
99-; scr_addr — HL = screen address of his cell (ROW_SCR + lamp_col)
100-; attr_addr — HL = attribute address of his cell (ROW_ATTR + lamp_col)
101-; Both clobber A, DE, HL.
124+; Address helpers (Unit 6).
102125 ; ----------------------------------------------------------------------------
103126 scr_addr:
104127 ld a, (lamp_col)
...
117140 ret
118141
119142 ; ----------------------------------------------------------------------------
120-; save_under — copy the nine bytes at his cell into the buffer.
121-; 8 bitmap rows -> under_lamp[0..7], attribute -> under_lamp[8].
143+; save_under / restore_under / draw_lamp (Unit 6).
122144 ; ----------------------------------------------------------------------------
123145 save_under:
124- call scr_addr ; HL = screen cell
146+ call scr_addr
125147 ld de, under_lamp
126148 ld b, 8
127149 .su:
128150 ld a, (hl)
129151 ld (de), a
130152 inc de
131- inc h ; down one screen row
153+ inc h
132154 djnz .su
133- call attr_addr ; HL = attribute cell
155+ call attr_addr
134156 ld a, (hl)
135157 ld (under_lamp + 8), a
136158 ret
137159
138-; ----------------------------------------------------------------------------
139-; restore_under — write the nine saved bytes back over his cell.
140-; ----------------------------------------------------------------------------
141160 restore_under:
142161 call scr_addr
143162 ld de, under_lamp
...
153172 ld (hl), a
154173 ret
155174
156-; ----------------------------------------------------------------------------
157-; draw_lamp — colour his cell and stamp his shape into it.
158-; ----------------------------------------------------------------------------
159175 draw_lamp:
160176 call attr_addr
161177 ld (hl), LAMP_ATTR
...
174190 ; State, buffer, and shape.
175191 ; ----------------------------------------------------------------------------
176192 lamp_col:
177- defb START_COL ; his column
193+ defb START_COL
194+target_col:
195+ defb 0
178196
179197 under_lamp:
180- defb 0, 0, 0, 0, 0, 0, 0, 0, 0 ; 9-byte buffer: 8 pixels + 1 attribute
198+ defb 0, 0, 0, 0, 0, 0, 0, 0, 0
181199
182200 lamplighter:
183201 defb %00111100 ; ..XXXX.. head
The complete program
; Gloaming — Unit 7: Walls
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 tests the target cell's colour and refuses a step into a wall.

            org     32768

COBBLE      equ     %00000001       ; PAPER black, INK blue — floor
WALL        equ     %00001111       ; PAPER blue, INK white — solid
LAMP_ATTR   equ     %01000111       ; BRIGHT, PAPER black, INK white — the figure
WALL_BIT    equ     3               ; PAPER bit 0: set on walls, clear on floor

ROW         equ     11
ROW_THIRD   equ     ROW / 8
ROW_CHARROW equ     ROW - ROW_THIRD * 8
ROW_SCR     equ     $4000 + ROW_THIRD * $0800 + ROW_CHARROW * 32
ROW_ATTR    equ     $5800 + ROW * 32
START_COL   equ     15

KEYS_OP     equ     $DFFE

; ============================================================================
; SETUP — runs once.
; ============================================================================
start:
            ld      a, 0            ; border black
            out     ($FE), a

            ld      hl, $5800       ; wash the grid in cobbles
            ld      de, $5801
            ld      (hl), COBBLE
            ld      bc, 767
            ldir

            ld      hl, $5800       ; wall frame — top row
            ld      b, 32
.top:
            ld      (hl), WALL
            inc     hl
            djnz    .top

            ld      hl, $5AE0       ; bottom row
            ld      b, 32
.bottom:
            ld      (hl), WALL
            inc     hl
            djnz    .bottom

            ld      hl, $5800       ; left and right columns
            ld      b, 24
.sides:
            ld      (hl), WALL
            push    hl
            ld      de, 31
            add     hl, de
            ld      (hl), WALL
            pop     hl
            ld      de, 32
            add     hl, de
            djnz    .sides

            call    save_under
            call    draw_lamp

; ============================================================================
; THE HEARTBEAT — test the target cell, move only if it isn't a wall.
; ============================================================================
            im      1
            ei

game_loop:
            halt

            ld      bc, KEYS_OP
            in      a, (c)
            bit     1, a            ; O (left)?
            jr      z, .step_left
            bit     0, a            ; P (right)?
            jr      z, .step_right
            jr      game_loop

.step_left:
            ld      a, (lamp_col)
            dec     a               ; A = the cell he wants to enter
            call    wall_at         ; NZ = wall, Z = clear  (A preserved)
            jr      nz, game_loop   ; wall — refuse the step
            ld      (target_col), a
            call    commit_move
            jr      game_loop

.step_right:
            ld      a, (lamp_col)
            inc     a
            call    wall_at
            jr      nz, game_loop
            ld      (target_col), a
            call    commit_move
            jr      game_loop

; ----------------------------------------------------------------------------
; wall_at — A = column to test. Reads that cell's attribute and BIT-tests it.
;   Returns NZ if it's a wall, Z if walkable. Leaves A unchanged.
; ----------------------------------------------------------------------------
wall_at:
            ld      e, a
            ld      d, 0
            ld      hl, ROW_ATTR
            add     hl, de
            bit     WALL_BIT, (hl)  ; set = wall, clear = floor
            ret

; ----------------------------------------------------------------------------
; commit_move — the move is clear: restore old cell, adopt target_col, save and
;   draw the new one.
; ----------------------------------------------------------------------------
commit_move:
            call    restore_under
            ld      a, (target_col)
            ld      (lamp_col), a
            call    save_under
            call    draw_lamp
            ret

; ----------------------------------------------------------------------------
; Address helpers (Unit 6).
; ----------------------------------------------------------------------------
scr_addr:
            ld      a, (lamp_col)
            ld      e, a
            ld      d, 0
            ld      hl, ROW_SCR
            add     hl, de
            ret

attr_addr:
            ld      a, (lamp_col)
            ld      e, a
            ld      d, 0
            ld      hl, ROW_ATTR
            add     hl, de
            ret

; ----------------------------------------------------------------------------
; save_under / restore_under / draw_lamp (Unit 6).
; ----------------------------------------------------------------------------
save_under:
            call    scr_addr
            ld      de, under_lamp
            ld      b, 8
.su:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h
            djnz    .su
            call    attr_addr
            ld      a, (hl)
            ld      (under_lamp + 8), a
            ret

restore_under:
            call    scr_addr
            ld      de, under_lamp
            ld      b, 8
.ru:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .ru
            call    attr_addr
            ld      a, (under_lamp + 8)
            ld      (hl), a
            ret

draw_lamp:
            call    attr_addr
            ld      (hl), LAMP_ATTR
            call    scr_addr
            ld      de, lamplighter
            ld      b, 8
.dl:
            ld      a, (de)
            ld      (hl), a
            inc     de
            inc     h
            djnz    .dl
            ret

; ----------------------------------------------------------------------------
; State, buffer, and shape.
; ----------------------------------------------------------------------------
lamp_col:
            defb    START_COL
target_col:
            defb    0

under_lamp:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0

lamplighter:
            defb    %00111100       ; ..XXXX..   head
            defb    %00111100       ; ..XXXX..   head
            defb    %00011000       ; ...XX...   neck
            defb    %01111110       ; .XXXXXX.   arms
            defb    %00011000       ; ...XX...   body
            defb    %00011000       ; ...XX...   body
            defb    %00100100       ; ..X..X..   legs
            defb    %01000010       ; .X....X.   feet

            end     start

Walk him into the wall and he stops at the last floor cell — and stays there, however long the key is held:

Holding P, the lamplighter walks right across the square and halts at the last floor cell — the wall refuses the next step. We wrote no 'stay on screen' code; the side walls bound him purely because they're solid.

Hold the key far longer than it takes to cross, and he sits pressed against the blue, refusing to enter it:

The lamplighter stopped at the right-hand wall, at the last floor cell just inside the blue frame.
Stopped at the last floor cell — the cell beyond him is a wall, so wall_at refuses the step. The frame is a boundary now, not decoration.

Notice there's no "stay on screen" code anywhere. The side walls bound him because they're solid; the map does the work.

When it's wrong, see why

Collision bugs are about which cell you test, and which way the bit reads:

  • He walks through walls regardless. The test is inverted. A wall has bit 3 set, so BIT 3 gives NZ — and jr nz must skip the move. Check WALL_BIT is 3 and the branch is jr nz.
  • He won't move anywhere. You're testing his current cell, or always reading non-zero. wall_at must test the target (lamp_col ± 1), worked out before the move.
  • He stops one cell short of, or one cell inside, the wall. Off-by-one in the target. The cell to test is the one he's entering, not the one he's on.
  • He slips past the side wall and the screen garbles. wall_at is reading the wrong address — confirm it forms ROW_ATTR + col.

Before and after

You started with a frame he ignored and finished with one he can't cross — and the whole boundary is three instructions: work out the target, BIT-test its colour, refuse if solid. No coordinate list, no edge checks; the collision map is the attribute memory you painted in Unit 1. Make a wall cell floor, or a floor cell wall, and his world changes with it — because the map and the collision are the same bytes.

Try this: knock a gate

Open a hole in the wall. In setup, after the frame is drawn, paint one wall cell as floor — the right wall of his row:

ld   hl, ROW_ATTR + 31
ld   (hl), COBBLE

Walk right: he passes straight through the gap, because wall_at reads the live attribute and that cell is floor now. Change the map, change the collision.

Try this: plant a pillar

The opposite — make a floor cell solid. Set an interior cell in his path to the wall colour:

ld   hl, ROW_ATTR + 20
ld   (hl), WALL

A blue block appears mid-floor, and he stops at it just as he stops at the frame. Collision isn't about the edge of the square — it's about whatever cell he's stepping into, wherever it is.

Try this: take the wall test out

Comment out the call wall_at and its jr nz. He walks straight through the wall and off into the dark, exactly like Unit 6. Those lines are the whole difference between a figure and a figure that respects the world. Put them back.

What you've learnt

  • Collision can read the map you already have — a cell's attribute byte — with no separate solidity table.
  • A single BIT test (PAPER's bottom bit here) classifies a cell as solid or floor.
  • Test the target cell before committing the move, and refuse if it's solid.
  • The walls bound him with no dedicated edge code — being solid is enough.

What's next

He's penned left and right, but he can only move along one row. In Unit 8 we complete the movement engine: the other two directions, up and down. That brings back the full screen-address arithmetic from Unit 2 — moving between rows crosses those awkward thirds — and the top and bottom walls catch him just as the sides do. By the end he'll roam the whole square, and be properly shut inside it.