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

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.

50% of Gloaming

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.

Step 1: edit the saved attribute — cold lamp becomes lit
+41-37
11 ; Gloaming — Unit 10: Light It
22 ; 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.
44
55 org 32768
66
77 COBBLE equ %00000001 ; PAPER black, INK blue — floor
88 WALL equ %00001111 ; PAPER blue, INK white — solid
99 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
1112 WALL_BIT equ 3
1213
1314 START_COL equ 15
...
5758 add hl, de
5859 djnz .sides
5960
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
6263 call draw_lamp
6364
6465 ; ============================================================================
65-; THE HEARTBEAT — QAOP movement (Unit 8), unchanged.
66+; THE HEARTBEAT — move, and light any unlit lamp stepped onto.
6667 ; ============================================================================
6768 im 1
6869 ei
...
120121 ld a, (trow)
121122 ld (lamp_row), a
122123 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:
123132 call draw_lamp
124133 jr game_loop
125134
126135 ; ----------------------------------------------------------------------------
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).
129137 ; ----------------------------------------------------------------------------
130138 draw_lamps:
131139 ld hl, lamp_data
132140 .next:
133- ld a, (hl) ; column
141+ ld a, (hl)
134142 cp $FF
135- ret z ; $FF column → end of table
143+ ret z
136144 ld c, a
137145 inc hl
138- ld b, (hl) ; row
146+ ld b, (hl)
139147 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
142150 pop hl
143151 jr .next
144152
145-; ----------------------------------------------------------------------------
146-; draw_lantern — B=row, C=col. Stamp an unlit lantern into the cell.
147-; ----------------------------------------------------------------------------
148153 draw_lantern:
149- call attr_addr_cr ; HL = attribute, BC preserved
154+ call attr_addr_cr
150155 ld (hl), LAMP_UNLIT
151- call scr_addr_cr ; HL = screen, BC preserved
156+ call scr_addr_cr
152157 ld de, lantern
153158 ld b, 8
154159 .dlt:
...
206211 ret
207212
208213 ; ----------------------------------------------------------------------------
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).
211215 ; ----------------------------------------------------------------------------
212216 save_under:
213217 call pos_bc
...
263267 ; Level data, state, buffer, and shapes.
264268 ; ----------------------------------------------------------------------------
265269 lamp_data:
266- defb 4, 3 ; column, row
270+ defb 4, 3
267271 defb 27, 3
268272 defb 9, 7
269273 defb 22, 7
...
271275 defb 25, 15
272276 defb 13, 20
273277 defb 18, 20
274- defb $FF ; end of table
278+ defb $FF
275279
276280 lamp_col:
277281 defb START_COL
...
286290 defb 0, 0, 0, 0, 0, 0, 0, 0, 0
287291
288292 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
297301
298302 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
307311
308312 end start
309313
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:

The lamplighter walks down and then across a cold lamp. He lights it by standing on it — nothing shows while he covers it — and the instant he steps off, restore paints it back bright yellow. The glow stays put in the floor behind him.

He's left one lamp glowing gold while the rest stay cold — and that glow lives in the cell, not in him:

The square of lamps with one lamp near the bottom now glowing yellow, the lamplighter just beside it; the other lamps still cyan.
One lit, seven cold. Walk a full circuit and you'd leave a trail of warm lamps — the glow stays because we changed the cell, not the sprite.

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 + 8 after save_under, and compare it against LAMP_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 are LAMP_UNLIT; only the exact cyan lamp value should pass the cp.
  • 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_under writes under_lamp + 8 to 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.