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

Save and Restore

Retire the blank-erase. Keep a nine-byte buffer of whatever's beneath the lamplighter — eight bitmap rows plus the attribute — restore it when he leaves and save the next cell as he arrives. He can cross anything unharmed.

30% of Gloaming

Unit 5 moved the lamplighter by blanking the cell he left — and we named the flaw out loud: it only works while the floor is empty. The first thing with real pixels he steps onto, the blank-erase will scrub away.

This unit fixes it for good, and it completes the cell-sprite technique. Instead of blanking, we remember: before he stands in a cell, save the nine bytes already there; when he leaves, put them back. From now on he can walk over anything without harming it.

Where we start

Unit 5's walker, still erasing by blanking — clean on bare floor, destructive the moment there's a pixel beneath him. We swap that erase for something that preserves what it covers.

What's under him

A cell is nine bytes: the eight bitmap rows that hold its pixels, plus the one attribute that holds its colour. To preserve a cell while the lamplighter stands on it, we copy all nine into a small buffer:

under_lamp:
    defb 0,0,0,0,0,0,0,0,0   ; 8 pixel rows + 1 attribute

under_lamp always holds "whatever is underneath him right now". Save fills it as he arrives; restore empties it back as he leaves.

The dance: restore, step, save, draw

Every move is now four steps in a strict order:

  1. Restore — write the buffer back over his current cell. The background reappears, exactly as it was.
  2. Step — change lamp_col.
  3. Save — copy the new cell's nine bytes into the buffer, before we draw over them.
  4. Draw — stamp him into the new cell.

Order is everything. Restore before you step (or you'll restore the wrong cell); save before you draw (or you'll save his own figure as the "background" and carry it around like a stain).

Milestone — remember, don't blank

We retire erase_lamp and build the buffer machinery: a nine-byte under_lamp, a save_under and a restore_under that copy a cell to and from it, and two tiny address helpers (scr_addr, attr_addr) that hand back his cell's address so save, restore, and draw all share the same ROW_SCR + col sum. Setup now saves the start cell before drawing him; each move runs the restore / step / save / draw dance.

Step 1: a nine-byte buffer replaces the blank-erase
+83-44
11 ; Gloaming — Unit 6: Save and Restore
22 ; Cumulative build; every step runs on its own. Narrative: the unit page.
3-; step-00 is Unit 5's walker, still blanking the cell he leaves.
3+; step-01 remembers what's under him — save on arrival, restore on leaving.
44
55 org 32768
66
...
88 WALL equ %00001111 ; PAPER blue, INK white — pale stone
99 LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure
1010
11-; --- his row is fixed; only his column changes ---
1211 ROW equ 11
13-ROW_THIRD equ ROW / 8 ; which screen third (0,1,2)
14-ROW_CHARROW equ ROW - ROW_THIRD * 8 ; row within that third (0-7)
15-ROW_SCR equ $4000 + ROW_THIRD * $0800 + ROW_CHARROW * 32 ; col-0 of the row
16-ROW_ATTR equ $5800 + ROW * 32 ; col-0 attribute
12+ROW_THIRD equ ROW / 8
13+ROW_CHARROW equ ROW - ROW_THIRD * 8
14+ROW_SCR equ $4000 + ROW_THIRD * $0800 + ROW_CHARROW * 32
15+ROW_ATTR equ $5800 + ROW * 32
1716 START_COL equ 15
1817
19-KEYS_OP equ $DFFE ; half-row with P(bit0) and O(bit1)
18+KEYS_OP equ $DFFE
2019
20+; ============================================================================
21+; SETUP — runs once.
22+; ============================================================================
2123 start:
2224 ld a, 0 ; border black
2325 out ($FE), a
...
5557 add hl, de
5658 djnz .sides
5759
58- call draw_lamp ; draw him once, at his starting column
60+ call save_under ; remember what's under his start cell
61+ call draw_lamp ; then draw him on top of it
5962
60-; --- the heartbeat: read a direction and, if held, step that way ---
63+; ============================================================================
64+; THE HEARTBEAT — restore, step, save, draw.
65+; ============================================================================
6166 im 1
6267 ei
6368
...
6570 halt
6671
6772 ld bc, KEYS_OP
68- in a, (c) ; bottom bits, 0 = held
73+ in a, (c)
6974 bit 1, a ; O (left)?
7075 jr z, .step_left
7176 bit 0, a ; P (right)?
7277 jr z, .step_right
73- jr game_loop ; nothing held — hold position
78+ jr game_loop
7479
7580 .step_left:
76- call erase_lamp ; rub him out where he is
81+ call restore_under ; put the old cell back as it was
7782 ld a, (lamp_col)
78- dec a ; one cell left
83+ dec a
7984 ld (lamp_col), a
80- call draw_lamp
85+ call save_under ; remember the new cell's contents
86+ call draw_lamp ; draw him over them
8187 jr game_loop
8288
8389 .step_right:
84- call erase_lamp
90+ call restore_under
8591 ld a, (lamp_col)
86- inc a ; one cell right
92+ inc a
8793 ld (lamp_col), a
94+ call save_under
8895 call draw_lamp
8996 jr game_loop
9097
9198 ; ----------------------------------------------------------------------------
92-; draw_lamp — colour his cell and stamp his shape into it.
93-; cell address = ROW_SCR + col, attribute = ROW_ATTR + col.
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.
94102 ; ----------------------------------------------------------------------------
95-draw_lamp:
103+scr_addr:
96104 ld a, (lamp_col)
97105 ld e, a
98- ld d, 0 ; DE = column offset
106+ ld d, 0
107+ ld hl, ROW_SCR
108+ add hl, de
109+ ret
110+
111+attr_addr:
112+ ld a, (lamp_col)
113+ ld e, a
114+ ld d, 0
99115 ld hl, ROW_ATTR
100116 add hl, de
101- ld (hl), LAMP_ATTR ; his cell takes the figure's colour
117+ ret
102118
103- ld hl, ROW_SCR
104- add hl, de ; HL = top row of his cell
105- ld de, lamplighter ; DE now walks the shape
119+; ----------------------------------------------------------------------------
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].
122+; ----------------------------------------------------------------------------
123+save_under:
124+ call scr_addr ; HL = screen cell
125+ ld de, under_lamp
106126 ld b, 8
107-.draw_row:
127+.su:
128+ ld a, (hl)
129+ ld (de), a
130+ inc de
131+ inc h ; down one screen row
132+ djnz .su
133+ call attr_addr ; HL = attribute cell
134+ ld a, (hl)
135+ ld (under_lamp + 8), a
136+ ret
137+
138+; ----------------------------------------------------------------------------
139+; restore_under — write the nine saved bytes back over his cell.
140+; ----------------------------------------------------------------------------
141+restore_under:
142+ call scr_addr
143+ ld de, under_lamp
144+ ld b, 8
145+.ru:
108146 ld a, (de)
109147 ld (hl), a
110148 inc de
111- inc h ; down one screen row (+256)
112- djnz .draw_row
149+ inc h
150+ djnz .ru
151+ call attr_addr
152+ ld a, (under_lamp + 8)
153+ ld (hl), a
113154 ret
114155
115156 ; ----------------------------------------------------------------------------
116-; erase_lamp — blank his cell back to bare cobbles.
117-; (Safe ONLY because the floor has no pixels — see the unit page.)
157+; draw_lamp — colour his cell and stamp his shape into it.
118158 ; ----------------------------------------------------------------------------
119-erase_lamp:
120- ld a, (lamp_col)
121- ld e, a
122- ld d, 0
123- ld hl, ROW_ATTR
124- add hl, de
125- ld (hl), COBBLE ; the vacated cell is cobbles again
126-
127- ld hl, ROW_SCR
128- add hl, de
159+draw_lamp:
160+ call attr_addr
161+ ld (hl), LAMP_ATTR
162+ call scr_addr
163+ ld de, lamplighter
129164 ld b, 8
130- xor a ; A = 0 — a blank pixel row
131-.erase_row:
165+.dl:
166+ ld a, (de)
132167 ld (hl), a
168+ inc de
133169 inc h
134- djnz .erase_row
170+ djnz .dl
135171 ret
136172
137173 ; ----------------------------------------------------------------------------
138-; State and shape.
174+; State, buffer, and shape.
139175 ; ----------------------------------------------------------------------------
140176 lamp_col:
141- defb START_COL ; his column — changes as he walks
177+ defb START_COL ; his column
178+
179+under_lamp:
180+ defb 0, 0, 0, 0, 0, 0, 0, 0, 0 ; 9-byte buffer: 8 pixels + 1 attribute
142181
143182 lamplighter:
144183 defb %00111100 ; ..XXXX.. head
The complete program
; Gloaming — Unit 6: Save and Restore
; Cumulative build; every step runs on its own. Narrative: the unit page.
; step-01 remembers what's under him — save on arrival, restore on leaving.

            org     32768

COBBLE      equ     %00000001       ; PAPER black, INK blue — dark ground
WALL        equ     %00001111       ; PAPER blue, INK white — pale stone
LAMP_ATTR   equ     %01000111       ; BRIGHT, PAPER black, INK white — the figure

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      ; remember what's under his start cell
            call    draw_lamp       ; then draw him on top of it

; ============================================================================
; THE HEARTBEAT — restore, step, save, draw.
; ============================================================================
            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:
            call    restore_under   ; put the old cell back as it was
            ld      a, (lamp_col)
            dec     a
            ld      (lamp_col), a
            call    save_under      ; remember the new cell's contents
            call    draw_lamp       ; draw him over them
            jr      game_loop

.step_right:
            call    restore_under
            ld      a, (lamp_col)
            inc     a
            ld      (lamp_col), a
            call    save_under
            call    draw_lamp
            jr      game_loop

; ----------------------------------------------------------------------------
; scr_addr  — HL = screen address of his cell  (ROW_SCR + lamp_col)
; attr_addr — HL = attribute address of his cell (ROW_ATTR + lamp_col)
;   Both clobber A, DE, HL.
; ----------------------------------------------------------------------------
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 — copy the nine bytes at his cell into the buffer.
;   8 bitmap rows -> under_lamp[0..7], attribute -> under_lamp[8].
; ----------------------------------------------------------------------------
save_under:
            call    scr_addr        ; HL = screen cell
            ld      de, under_lamp
            ld      b, 8
.su:
            ld      a, (hl)
            ld      (de), a
            inc     de
            inc     h               ; down one screen row
            djnz    .su
            call    attr_addr       ; HL = attribute cell
            ld      a, (hl)
            ld      (under_lamp + 8), a
            ret

; ----------------------------------------------------------------------------
; restore_under — write the nine saved bytes back over his cell.
; ----------------------------------------------------------------------------
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 — colour his cell and stamp his shape into it.
; ----------------------------------------------------------------------------
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       ; his column

under_lamp:
            defb    0, 0, 0, 0, 0, 0, 0, 0, 0   ; 9-byte buffer: 8 pixels + 1 attribute

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

On the bare floor he walks exactly as he did in Unit 5 — one figure, no trail, no damage:

The lamplighter, having walked left of centre across an empty square, one clean figure with no trail.
Walked left across empty floor. On bare cobbles, save-and-restore and blank-erase look identical — which is exactly why the difference needs a marker to show.

The difference, made visible

The whole point of this unit is invisible on an empty floor, so let's put something on the floor. This is the mirror of Unit 5's "watch the assumption bite": we poke a bright marker into a cell in his path and walk him over it.

Here he is approaching the marker — a yellow block at the column to his left:

The square with a solid yellow block left of centre and the lamplighter standing a few cells to its right.
Before: a marker on the floor, the lamplighter a few cells to its right, about to walk left through it.

And here he is after walking straight through that cell and out the other side — the marker is still there:

The lamplighter now to the left of the yellow block, which is intact.
After: he covered the marker as he passed, then restored it on the way out. In Unit 5 the blank-erase would have wiped it away; here it survives. Same scene, opposite outcome — that's the whole unit.

He saved the block's nine bytes as he stepped on, and restored them as he stepped off. Nothing destroyed.

When it's wrong, see why

Save-and-restore bugs are about the order of the four steps:

  • A trail of figures again. restore_under isn't being called, or runs after the step. Restore the old cell first, then change lamp_col.
  • He drags a smear of background around with him. You saved after drawing — so the buffer holds his own figure, and you stamp it into the next cell. Save the new cell before draw_lamp.
  • The cell he started on is wrong after he first moves. Setup must save_under before the first draw_lamp, so the buffer holds real floor, not his figure.
  • It looks exactly like Unit 5. Correct, on a blank floor. The marker above is how you see the difference.

Before and after

You started with a walker that scrubbed the floor clean behind him and finished with one that leaves it untouched — by remembering nine bytes instead of blanking them. On empty cobbles the two are indistinguishable; over a marker, one destroys and one preserves. This solid save-and-restore is the honest groundwork. Drawing only his lit pixels, so scenery shows through the gaps in his shape — masking — is the upgrade a later game makes to this exact technique.

Try this: watch it survive

Poke your own mark into a floor cell in his path, in setup:

ld   hl, ROW_SCR + 9
ld   (hl), %11111111

In Unit 5, walking over column 9 wiped that bar away. Now walk over it and it's still there when he leaves — saved on the way in, restored on the way out.

Try this: forget to restore

Comment out one call restore_under. The trail of lamplighters comes straight back — the old cell never gets rebuilt, so his figure stays stamped in it. A neat way to see that restore is doing real work every step, even when the floor looks empty.

Try this: the square hole (a look ahead)

Fill a cell in his path with a stipple instead of a solid bar (%10101010), and walk onto it. While he's standing there, his cell is a solid block — his white figure on black — and the stipple is hidden under him, not showing through the gaps. Save-and-restore protects the background, but he still covers it with an opaque square. Drawing only his lit pixels, so scenery shows through the gaps, is masking — the upgrade a later game makes.

What you've learnt

  • Preserve a sprite's background by saving the cell's bytes before drawing and restoring them when it leaves.
  • A cell is nine bytes: eight bitmap rows plus one attribute.
  • The order is restore (old) → step → save (new) → draw — and save must come before draw.
  • This solid save/restore is the honest "before"; drawing only lit pixels — masking — is the upgrade a later game makes.

What's next

The lamplighter can now stand anywhere safely — so it's time to decide where he's allowed to go. In Unit 7 we add collision: before each step, read the attribute of the target cell and BIT-test it. If it's a wall, the step is refused. The square's blue frame stops being decoration and becomes a boundary he can't cross.