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.
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.
| 1 | 1 | ; Gloaming — Unit 7: Walls | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 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 | |
| 9 | 9 | 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 | |
| 10 | 11 | | |
| 11 | 12 | ROW equ 11 | |
| 12 | 13 | ROW_THIRD equ ROW / 8 | |
| ... | |||
| 57 | 58 | add hl, de | |
| 58 | 59 | djnz .sides | |
| 59 | 60 | | |
| 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 | |
| 62 | 63 | | |
| 63 | 64 | ; ============================================================================ | |
| 64 | - | ; THE HEARTBEAT — restore, step, save, draw. | |
| 65 | + | ; THE HEARTBEAT — test the target cell, move only if it isn't a wall. | |
| 65 | 66 | ; ============================================================================ | |
| 66 | 67 | im 1 | |
| 67 | 68 | ei | |
| ... | |||
| 78 | 79 | jr game_loop | |
| 79 | 80 | | |
| 80 | 81 | .step_left: | |
| 81 | - | call restore_under ; put the old cell back as it was | |
| 82 | 82 | 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 | |
| 87 | 88 | jr game_loop | |
| 88 | 89 | | |
| 89 | 90 | .step_right: | |
| 90 | - | call restore_under | |
| 91 | 91 | ld a, (lamp_col) | |
| 92 | 92 | 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) | |
| 93 | 118 | ld (lamp_col), a | |
| 94 | 119 | call save_under | |
| 95 | 120 | call draw_lamp | |
| 96 | - | jr game_loop | |
| 121 | + | ret | |
| 97 | 122 | | |
| 98 | 123 | ; ---------------------------------------------------------------------------- | |
| 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). | |
| 102 | 125 | ; ---------------------------------------------------------------------------- | |
| 103 | 126 | scr_addr: | |
| 104 | 127 | ld a, (lamp_col) | |
| ... | |||
| 117 | 140 | ret | |
| 118 | 141 | | |
| 119 | 142 | ; ---------------------------------------------------------------------------- | |
| 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). | |
| 122 | 144 | ; ---------------------------------------------------------------------------- | |
| 123 | 145 | save_under: | |
| 124 | - | call scr_addr ; HL = screen cell | |
| 146 | + | call scr_addr | |
| 125 | 147 | ld de, under_lamp | |
| 126 | 148 | ld b, 8 | |
| 127 | 149 | .su: | |
| 128 | 150 | ld a, (hl) | |
| 129 | 151 | ld (de), a | |
| 130 | 152 | inc de | |
| 131 | - | inc h ; down one screen row | |
| 153 | + | inc h | |
| 132 | 154 | djnz .su | |
| 133 | - | call attr_addr ; HL = attribute cell | |
| 155 | + | call attr_addr | |
| 134 | 156 | ld a, (hl) | |
| 135 | 157 | ld (under_lamp + 8), a | |
| 136 | 158 | ret | |
| 137 | 159 | | |
| 138 | - | ; ---------------------------------------------------------------------------- | |
| 139 | - | ; restore_under — write the nine saved bytes back over his cell. | |
| 140 | - | ; ---------------------------------------------------------------------------- | |
| 141 | 160 | restore_under: | |
| 142 | 161 | call scr_addr | |
| 143 | 162 | ld de, under_lamp | |
| ... | |||
| 153 | 172 | ld (hl), a | |
| 154 | 173 | ret | |
| 155 | 174 | | |
| 156 | - | ; ---------------------------------------------------------------------------- | |
| 157 | - | ; draw_lamp — colour his cell and stamp his shape into it. | |
| 158 | - | ; ---------------------------------------------------------------------------- | |
| 159 | 175 | draw_lamp: | |
| 160 | 176 | call attr_addr | |
| 161 | 177 | ld (hl), LAMP_ATTR | |
| ... | |||
| 174 | 190 | ; State, buffer, and shape. | |
| 175 | 191 | ; ---------------------------------------------------------------------------- | |
| 176 | 192 | lamp_col: | |
| 177 | - | defb START_COL ; his column | |
| 193 | + | defb START_COL | |
| 194 | + | target_col: | |
| 195 | + | defb 0 | |
| 178 | 196 | | |
| 179 | 197 | 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 | |
| 181 | 199 | | |
| 182 | 200 | lamplighter: | |
| 183 | 201 | 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:
Hold the key far longer than it takes to cross, and he sits pressed against the blue, refusing to enter it:
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 3gives NZ — andjr nzmust skip the move. CheckWALL_BITis 3 and the branch isjr nz. - He won't move anywhere. You're testing his current cell, or always reading non-zero.
wall_atmust 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_atis reading the wrong address — confirm it formsROW_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
BITtest (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.