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.
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:
- Restore — write the buffer back over his current cell. The background reappears, exactly as it was.
- Step — change
lamp_col. - Save — copy the new cell's nine bytes into the buffer, before we draw over them.
- 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.
| 1 | 1 | ; Gloaming — Unit 6: Save and Restore | |
| 2 | 2 | ; 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. | |
| 4 | 4 | | |
| 5 | 5 | org 32768 | |
| 6 | 6 | | |
| ... | |||
| 8 | 8 | WALL equ %00001111 ; PAPER blue, INK white — pale stone | |
| 9 | 9 | LAMP_ATTR equ %01000111 ; BRIGHT, PAPER black, INK white — the figure | |
| 10 | 10 | | |
| 11 | - | ; --- his row is fixed; only his column changes --- | |
| 12 | 11 | 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 | |
| 17 | 16 | START_COL equ 15 | |
| 18 | 17 | | |
| 19 | - | KEYS_OP equ $DFFE ; half-row with P(bit0) and O(bit1) | |
| 18 | + | KEYS_OP equ $DFFE | |
| 20 | 19 | | |
| 20 | + | ; ============================================================================ | |
| 21 | + | ; SETUP — runs once. | |
| 22 | + | ; ============================================================================ | |
| 21 | 23 | start: | |
| 22 | 24 | ld a, 0 ; border black | |
| 23 | 25 | out ($FE), a | |
| ... | |||
| 55 | 57 | add hl, de | |
| 56 | 58 | djnz .sides | |
| 57 | 59 | | |
| 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 | |
| 59 | 62 | | |
| 60 | - | ; --- the heartbeat: read a direction and, if held, step that way --- | |
| 63 | + | ; ============================================================================ | |
| 64 | + | ; THE HEARTBEAT — restore, step, save, draw. | |
| 65 | + | ; ============================================================================ | |
| 61 | 66 | im 1 | |
| 62 | 67 | ei | |
| 63 | 68 | | |
| ... | |||
| 65 | 70 | halt | |
| 66 | 71 | | |
| 67 | 72 | ld bc, KEYS_OP | |
| 68 | - | in a, (c) ; bottom bits, 0 = held | |
| 73 | + | in a, (c) | |
| 69 | 74 | bit 1, a ; O (left)? | |
| 70 | 75 | jr z, .step_left | |
| 71 | 76 | bit 0, a ; P (right)? | |
| 72 | 77 | jr z, .step_right | |
| 73 | - | jr game_loop ; nothing held — hold position | |
| 78 | + | jr game_loop | |
| 74 | 79 | | |
| 75 | 80 | .step_left: | |
| 76 | - | call erase_lamp ; rub him out where he is | |
| 81 | + | call restore_under ; put the old cell back as it was | |
| 77 | 82 | ld a, (lamp_col) | |
| 78 | - | dec a ; one cell left | |
| 83 | + | dec a | |
| 79 | 84 | 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 | |
| 81 | 87 | jr game_loop | |
| 82 | 88 | | |
| 83 | 89 | .step_right: | |
| 84 | - | call erase_lamp | |
| 90 | + | call restore_under | |
| 85 | 91 | ld a, (lamp_col) | |
| 86 | - | inc a ; one cell right | |
| 92 | + | inc a | |
| 87 | 93 | ld (lamp_col), a | |
| 94 | + | call save_under | |
| 88 | 95 | call draw_lamp | |
| 89 | 96 | jr game_loop | |
| 90 | 97 | | |
| 91 | 98 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 94 | 102 | ; ---------------------------------------------------------------------------- | |
| 95 | - | draw_lamp: | |
| 103 | + | scr_addr: | |
| 96 | 104 | ld a, (lamp_col) | |
| 97 | 105 | 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 | |
| 99 | 115 | ld hl, ROW_ATTR | |
| 100 | 116 | add hl, de | |
| 101 | - | ld (hl), LAMP_ATTR ; his cell takes the figure's colour | |
| 117 | + | ret | |
| 102 | 118 | | |
| 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 | |
| 106 | 126 | 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: | |
| 108 | 146 | ld a, (de) | |
| 109 | 147 | ld (hl), a | |
| 110 | 148 | 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 | |
| 113 | 154 | ret | |
| 114 | 155 | | |
| 115 | 156 | ; ---------------------------------------------------------------------------- | |
| 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. | |
| 118 | 158 | ; ---------------------------------------------------------------------------- | |
| 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 | |
| 129 | 164 | ld b, 8 | |
| 130 | - | xor a ; A = 0 — a blank pixel row | |
| 131 | - | .erase_row: | |
| 165 | + | .dl: | |
| 166 | + | ld a, (de) | |
| 132 | 167 | ld (hl), a | |
| 168 | + | inc de | |
| 133 | 169 | inc h | |
| 134 | - | djnz .erase_row | |
| 170 | + | djnz .dl | |
| 135 | 171 | ret | |
| 136 | 172 | | |
| 137 | 173 | ; ---------------------------------------------------------------------------- | |
| 138 | - | ; State and shape. | |
| 174 | + | ; State, buffer, and shape. | |
| 139 | 175 | ; ---------------------------------------------------------------------------- | |
| 140 | 176 | 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 | |
| 142 | 181 | | |
| 143 | 182 | lamplighter: | |
| 144 | 183 | 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 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:
And here he is after walking straight through that cell and out the other side — the marker is still there:
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_underisn't being called, or runs after the step. Restore the old cell first, then changelamp_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_underbefore the firstdraw_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.