Light It
Light a lamp by editing the bytes save/restore already holds. Step onto an unlit lamp, change its saved attribute from cold cyan to bright yellow, and when you step off, restore paints it back glowing. The floor becomes state you control.
The lamps are placed and cold. The lamplighter's whole job is to warm them — and the way we do it is so economical it feels like a trick. We're already saving the nine bytes beneath him on every move (Unit 6). To light a lamp, we don't draw anything new: we edit the saved copy. Change the stored colour from cold cyan to bright yellow, and when he steps off, restore paints the lamp back — lit.
Where we start
Unit 9's level — eight cold lanterns the lamplighter can cross but not change. We give crossing a lamp a consequence.
Lighting is editing what's already saved
Look again at the move we built. When he steps onto a cell, save_under copies that cell's nine bytes into the buffer — for a lamp, that's the lantern's pixels plus its cyan attribute. He's now standing on it, covering it. The lamp is in the buffer, waiting to be restored when he leaves.
So lighting it is one comparison and one write, right after the save:
ld a, (under_lamp + 8) ; the attribute we just saved
cp LAMP_UNLIT ; is it a cold lamp?
jr nz, .not_lamp
ld a, LAMP_LIT ; then change the SAVED copy to lit
ld (under_lamp + 8), a
.not_lamp:
Nothing on screen changes yet — he's covering the cell. But the buffer now holds a lit lamp, so the instant he moves away and restore_under runs, the lamp is painted back in bright yellow. He lights it by walking on, and it glows the moment he walks off.
Two ideas worth naming
This tiny block carries two of the biggest ideas in game programming:
- Collision as a rule, not just a barrier. In Unit 7, touching a cell asked "can I go there?". Here, being on a cell does something. Collision became the game's rules — step on this kind of thing and the world changes.
- Persistent state lives in the world. The lamp doesn't stay lit because the lamplighter remembers it; it stays lit because we changed the cell. The glow lives in the floor, not the sprite. Walk to the far side of the square and back — still lit, because the change is there, in memory, where we put it.
And it's naturally idempotent: step back onto a lamp that's already lit and the save grabs yellow, the cp LAMP_UNLIT fails, nothing changes. A lit lamp stays lit. You can't accidentally un-light one by crossing it twice.
Milestone — light it
Right after save_under, we read the attribute we just stored, and if it's a cold lamp, overwrite the saved copy with the lit colour. That's the whole feature — a compare and a write, no new drawing.
| 1 | 1 | ; Gloaming — Unit 10: Light It | |
| 2 | 2 | ; Cumulative build; every step runs on its own. Narrative: the unit page. | |
| 3 | - | ; step-00 is Unit 9's cold lamps — placed, but nothing lights them yet. | |
| 3 | + | ; step-01 lights a lamp by editing its saved attribute from cyan to yellow. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| 7 | 7 | COBBLE equ %00000001 ; PAPER black, INK blue — floor | |
| 8 | 8 | WALL equ %00001111 ; PAPER blue, INK white — solid | |
| 9 | 9 | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure | |
| 10 | - | LAMP_UNLIT equ %00000101 ; PAPER black, INK cyan, no bright — a cold, unlit lamp | |
| 10 | + | LAMP_UNLIT equ %00000101 ; PAPER black, INK cyan — a cold, unlit lamp | |
| 11 | + | LAMP_LIT equ %01000110 ; BRIGHT, PAPER black, INK yellow — a lit lamp | |
| 11 | 12 | WALL_BIT equ 3 | |
| 12 | 13 | | |
| 13 | 14 | START_COL equ 15 | |
| ... | |||
| 57 | 58 | add hl, de | |
| 58 | 59 | djnz .sides | |
| 59 | 60 | | |
| 60 | - | call draw_lamps ; place the lamps from the table | |
| 61 | - | call save_under ; then the lamplighter, on top | |
| 61 | + | call draw_lamps | |
| 62 | + | call save_under | |
| 62 | 63 | call draw_lamp | |
| 63 | 64 | | |
| 64 | 65 | ; ============================================================================ | |
| 65 | - | ; THE HEARTBEAT — QAOP movement (Unit 8), unchanged. | |
| 66 | + | ; THE HEARTBEAT — move, and light any unlit lamp stepped onto. | |
| 66 | 67 | ; ============================================================================ | |
| 67 | 68 | im 1 | |
| 68 | 69 | ei | |
| ... | |||
| 120 | 121 | ld a, (trow) | |
| 121 | 122 | ld (lamp_row), a | |
| 122 | 123 | call save_under | |
| 124 | + | | |
| 125 | + | ; --- light it: if the saved cell is an unlit lamp, mark it lit --- | |
| 126 | + | ld a, (under_lamp + 8) | |
| 127 | + | cp LAMP_UNLIT | |
| 128 | + | jr nz, .not_lamp | |
| 129 | + | ld a, LAMP_LIT | |
| 130 | + | ld (under_lamp + 8), a ; restore will paint it lit when he leaves | |
| 131 | + | .not_lamp: | |
| 123 | 132 | call draw_lamp | |
| 124 | 133 | jr game_loop | |
| 125 | 134 | | |
| 126 | 135 | ; ---------------------------------------------------------------------------- | |
| 127 | - | ; draw_lamps — walk the table, drawing an unlit lantern at each (col,row). | |
| 128 | - | ; Table is column,row pairs, ended by a column of $FF. | |
| 136 | + | ; draw_lamps / draw_lantern (Unit 9). | |
| 129 | 137 | ; ---------------------------------------------------------------------------- | |
| 130 | 138 | draw_lamps: | |
| 131 | 139 | ld hl, lamp_data | |
| 132 | 140 | .next: | |
| 133 | - | ld a, (hl) ; column | |
| 141 | + | ld a, (hl) | |
| 134 | 142 | cp $FF | |
| 135 | - | ret z ; $FF column → end of table | |
| 143 | + | ret z | |
| 136 | 144 | ld c, a | |
| 137 | 145 | inc hl | |
| 138 | - | ld b, (hl) ; row | |
| 146 | + | ld b, (hl) | |
| 139 | 147 | inc hl | |
| 140 | - | push hl ; keep the table pointer | |
| 141 | - | call draw_lantern ; draw at (B=row, C=col) | |
| 148 | + | push hl | |
| 149 | + | call draw_lantern | |
| 142 | 150 | pop hl | |
| 143 | 151 | jr .next | |
| 144 | 152 | | |
| 145 | - | ; ---------------------------------------------------------------------------- | |
| 146 | - | ; draw_lantern — B=row, C=col. Stamp an unlit lantern into the cell. | |
| 147 | - | ; ---------------------------------------------------------------------------- | |
| 148 | 153 | draw_lantern: | |
| 149 | - | call attr_addr_cr ; HL = attribute, BC preserved | |
| 154 | + | call attr_addr_cr | |
| 150 | 155 | ld (hl), LAMP_UNLIT | |
| 151 | - | call scr_addr_cr ; HL = screen, BC preserved | |
| 156 | + | call scr_addr_cr | |
| 152 | 157 | ld de, lantern | |
| 153 | 158 | ld b, 8 | |
| 154 | 159 | .dlt: | |
| ... | |||
| 206 | 211 | ret | |
| 207 | 212 | | |
| 208 | 213 | ; ---------------------------------------------------------------------------- | |
| 209 | - | ; save_under / restore_under / draw_lamp (Unit 8) — protect whatever is under | |
| 210 | - | ; him, lamps included. | |
| 214 | + | ; save_under / restore_under / draw_lamp (Unit 8). | |
| 211 | 215 | ; ---------------------------------------------------------------------------- | |
| 212 | 216 | save_under: | |
| 213 | 217 | call pos_bc | |
| ... | |||
| 263 | 267 | ; Level data, state, buffer, and shapes. | |
| 264 | 268 | ; ---------------------------------------------------------------------------- | |
| 265 | 269 | lamp_data: | |
| 266 | - | defb 4, 3 ; column, row | |
| 270 | + | defb 4, 3 | |
| 267 | 271 | defb 27, 3 | |
| 268 | 272 | defb 9, 7 | |
| 269 | 273 | defb 22, 7 | |
| ... | |||
| 271 | 275 | defb 25, 15 | |
| 272 | 276 | defb 13, 20 | |
| 273 | 277 | defb 18, 20 | |
| 274 | - | defb $FF ; end of table | |
| 278 | + | defb $FF | |
| 275 | 279 | | |
| 276 | 280 | lamp_col: | |
| 277 | 281 | defb START_COL | |
| ... | |||
| 286 | 290 | defb 0, 0, 0, 0, 0, 0, 0, 0, 0 | |
| 287 | 291 | | |
| 288 | 292 | lamplighter: | |
| 289 | - | defb %00111100 ; ..XXXX.. head | |
| 290 | - | defb %00111100 ; ..XXXX.. head | |
| 291 | - | defb %00011000 ; ...XX... neck | |
| 292 | - | defb %01111110 ; .XXXXXX. arms | |
| 293 | - | defb %00011000 ; ...XX... body | |
| 294 | - | defb %00011000 ; ...XX... body | |
| 295 | - | defb %00100100 ; ..X..X.. legs | |
| 296 | - | defb %01000010 ; .X....X. feet | |
| 293 | + | defb %00111100 | |
| 294 | + | defb %00111100 | |
| 295 | + | defb %00011000 | |
| 296 | + | defb %01111110 | |
| 297 | + | defb %00011000 | |
| 298 | + | defb %00011000 | |
| 299 | + | defb %00100100 | |
| 300 | + | defb %01000010 | |
| 297 | 301 | | |
| 298 | 302 | lantern: | |
| 299 | - | defb %00011000 ; ...XX... handle top | |
| 300 | - | defb %00100100 ; ..X..X.. handle loop | |
| 301 | - | defb %01111110 ; .XXXXXX. cap | |
| 302 | - | defb %01111110 ; .XXXXXX. glass | |
| 303 | - | defb %01011010 ; .X.XX.X. glass panes | |
| 304 | - | defb %01111110 ; .XXXXXX. glass | |
| 305 | - | defb %01111110 ; .XXXXXX. base | |
| 306 | - | defb %00111100 ; ..XXXX.. foot | |
| 303 | + | defb %00011000 | |
| 304 | + | defb %00100100 | |
| 305 | + | defb %01111110 | |
| 306 | + | defb %01111110 | |
| 307 | + | defb %01011010 | |
| 308 | + | defb %01111110 | |
| 309 | + | defb %01111110 | |
| 310 | + | defb %00111100 | |
| 307 | 311 | | |
| 308 | 312 | end start | |
| 309 | 313 | |
The complete program
; Gloaming — Unit 10: Light It
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 lights a lamp by editing its saved attribute from cyan to yellow.
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
LAMP_UNLIT equ %00000101 ; PAPER black, INK cyan — a cold, unlit lamp
LAMP_LIT equ %01000110 ; BRIGHT, PAPER black, INK yellow — a lit lamp
WALL_BIT equ 3
START_COL equ 15
START_ROW equ 11
KEYS_OP equ $DFFE
KEYS_Q equ $FBFE
KEYS_A equ $FDFE
; ============================================================================
; 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 draw_lamps
call save_under
call draw_lamp
; ============================================================================
; THE HEARTBEAT — move, and light any unlit lamp stepped onto.
; ============================================================================
im 1
ei
game_loop:
halt
ld a, (lamp_col)
ld (tcol), a
ld a, (lamp_row)
ld (trow), a
ld bc, KEYS_OP
in a, (c)
bit 1, a
jr z, .left
bit 0, a
jr z, .right
ld bc, KEYS_Q
in a, (c)
bit 0, a
jr z, .up
ld bc, KEYS_A
in a, (c)
bit 0, a
jr z, .down
jr game_loop
.left:
ld hl, tcol
dec (hl)
jr .try
.right:
ld hl, tcol
inc (hl)
jr .try
.up:
ld hl, trow
dec (hl)
jr .try
.down:
ld hl, trow
inc (hl)
.try:
ld a, (trow)
ld b, a
ld a, (tcol)
ld c, a
call wall_at
jr nz, game_loop
call restore_under
ld a, (tcol)
ld (lamp_col), a
ld a, (trow)
ld (lamp_row), a
call save_under
; --- light it: if the saved cell is an unlit lamp, mark it lit ---
ld a, (under_lamp + 8)
cp LAMP_UNLIT
jr nz, .not_lamp
ld a, LAMP_LIT
ld (under_lamp + 8), a ; restore will paint it lit when he leaves
.not_lamp:
call draw_lamp
jr game_loop
; ----------------------------------------------------------------------------
; draw_lamps / draw_lantern (Unit 9).
; ----------------------------------------------------------------------------
draw_lamps:
ld hl, lamp_data
.next:
ld a, (hl)
cp $FF
ret z
ld c, a
inc hl
ld b, (hl)
inc hl
push hl
call draw_lantern
pop hl
jr .next
draw_lantern:
call attr_addr_cr
ld (hl), LAMP_UNLIT
call scr_addr_cr
ld de, lantern
ld b, 8
.dlt:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dlt
ret
; ----------------------------------------------------------------------------
; scr_addr_cr / attr_addr_cr / wall_at / pos_bc (Unit 8).
; ----------------------------------------------------------------------------
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
wall_at:
call attr_addr_cr
bit WALL_BIT, (hl)
ret
pos_bc:
ld a, (lamp_row)
ld b, a
ld a, (lamp_col)
ld c, a
ret
; ----------------------------------------------------------------------------
; save_under / restore_under / draw_lamp (Unit 8).
; ----------------------------------------------------------------------------
save_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.su:
ld a, (hl)
ld (de), a
inc de
inc h
djnz .su
call pos_bc
call attr_addr_cr
ld a, (hl)
ld (under_lamp + 8), a
ret
restore_under:
call pos_bc
call scr_addr_cr
ld de, under_lamp
ld b, 8
.ru:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .ru
call pos_bc
call attr_addr_cr
ld a, (under_lamp + 8)
ld (hl), a
ret
draw_lamp:
call pos_bc
call attr_addr_cr
ld (hl), LAMP_ATTR
call pos_bc
call scr_addr_cr
ld de, lamplighter
ld b, 8
.dl:
ld a, (de)
ld (hl), a
inc de
inc h
djnz .dl
ret
; ----------------------------------------------------------------------------
; Level data, state, buffer, and shapes.
; ----------------------------------------------------------------------------
lamp_data:
defb 4, 3
defb 27, 3
defb 9, 7
defb 22, 7
defb 6, 15
defb 25, 15
defb 13, 20
defb 18, 20
defb $FF
lamp_col:
defb START_COL
lamp_row:
defb START_ROW
tcol:
defb 0
trow:
defb 0
under_lamp:
defb 0, 0, 0, 0, 0, 0, 0, 0, 0
lamplighter:
defb %00111100
defb %00111100
defb %00011000
defb %01111110
defb %00011000
defb %00011000
defb %00100100
defb %01000010
lantern:
defb %00011000
defb %00100100
defb %01111110
defb %01111110
defb %01011010
defb %01111110
defb %01111110
defb %00111100
end start
Walk him across a cold lamp and it blooms from cyan to gold as he steps off, and stays that way behind him:
He's left one lamp glowing gold while the rest stay cold — and that glow lives in the cell, not in him:
When it's wrong, see why
Lighting fails in ways that point at where you wrote the change:
- Lamps never light. The check runs in the wrong place or tests the wrong value. Read
under_lamp + 8aftersave_under, and compare it againstLAMP_UNLIT. - A lamp flashes lit while he stands on it, then goes cold. You changed the live screen cell instead of the buffer. Light it by writing
under_lamp + 8, never the cell directly — let restore do the painting. - The cobbles light up too. Your compare is matching floor. Cobbles are
COBBLE, lamps areLAMP_UNLIT; only the exact cyan lamp value should pass thecp. - A lit lamp turns cold when you cross it again. Restore isn't writing the lit attribute back to the real cell, so the second save grabs stale data. Confirm
restore_underwritesunder_lamp + 8to the attribute.
Before and after
You started with lamps you could only cross and finished with lamps you can light — and the whole mechanic is a compare and a write slipped into a move you already had. No new drawing, because restore does the painting; no memory of which lamps are lit, because the lit state lives in the cells themselves. Crossing a lamp now means something. There's a game here: light them all.
Try this: light the lot
Walk a full circuit of the square, stepping on every lamp. By the end the whole board is warm — eight gold lanterns where eight cold ones stood. That circuit is the game; the next two units add the score and the win that make it one.
Try this: a flickering flame
A lit lamp is an attribute, so make it livelier. Set the FLASH bit in LAMP_LIT — %11000110 — and the lit lamps pulse, ink and paper swapping a few times a second, like a real flame catching. One bit, and cold becomes alive.
Try this: make them toggle
Change the rule. Instead of only lighting unlit lamps, compare against LAMP_LIT as well and swap either way — step on a lit lamp and it goes cold again. Now lamps toggle, and you have a different game (keep them all lit at once?). The point: the rule is yours. "What does stepping here do?" is a line of code you get to write.
What you've learnt
- Collision as a rule: stepping onto a kind of cell can do something, not just gate movement.
- Persistent state lives in the world: change the cell and it stays changed — the glow is in the floor, not the sprite.
- Lighting reuses save/restore: edit the saved attribute, and restore paints the lamp lit when he leaves.
- The trick is idempotent — a lit lamp stays lit, however many times he crosses it.
What's next
Lamps light, but nothing's counting. A game needs to know how you're doing and tell you. In Unit 11 we add the tally: a row of pips along the bottom, one per lamp, brightening as each is lit — a score you read at a glance. It's a coloured-cell readout, not drawn numbers, and the honest "before" of the digit HUD a later game builds.