A pool needs two operations: find somewhere to put a new object, and visit the objects that already exist. Both are short walks through the same memory layout.
Unit 12 named every record in the source: three ld ix instructions in new_game and three more in advance_meteors. A storm of a hundred meteors written that way would need a hundred of each. This lesson’s program keeps the four-byte record and changes how records are chosen and visited. SLOTS equ 20 reserves twenty records, spawn finds a free one itself, and advance_meteors visits every record in one loop.
Our three meteors are deliberately few enough to follow: X=80 at speed 2, X=116 at speed 3 and X=170 at speed 4, all started together by new_game. It no longer sets IX; it stores spawn_x and spawn_speed and calls spawn for each.
Source · view and edit
Companion file: assets.inc
The include line reads this editable data when you assemble. Keep it beside your source for a local build.
Edit the source, then assemble.
Edits are saved in this browser. Keep a downloaded copy of work you want to keep.
Focus the Spectrum screen to use the keyboard, or hold these buttons. Assemble again for a fresh machine.
Run the program to inspect it. Editing source does not change the running machine.
active_count- —
pool_overflow- —
hull- —
phase- —
Read the active object records
These rows read the twenty-slot pool. Empty slots are omitted. The display samples RAM while the program runs, so values can change between observations.
| Slot | X | Y | Kind / active | Speed |
|---|
Find a free record
spawn sets IX to the first record and B to the twenty-slot capacity. At spawn_find it reads offset 2. Zero means free, so the search branches to spawn_found. Otherwise ld de,4 and add ix,de advance one record; DJNZ decrements the remaining count.
At a free slot, the routine writes X, Y=24, active=1 and speed, then draws the new meteor. Setting an active flag without initialising position and speed would let old values become a new object.
spawn:
ld ix,objects
ld b,SLOTS
spawn_find:
ld a,(ix+2)
or a
jr z,spawn_found
ld de,4
add ix,de
djnz spawn_find
; A full pool is a verification failure.
ld a,1
ld (pool_overflow),a
ret
spawn_found:
ld a,(spawn_x)
ld (ix+0),a
ld (ix+1),24
ld a,1
ld (ix+2),a
ld a,(spawn_speed)
ld (ix+3),a
ld e,(ix+0)
ld d,24
jp draw_meteorIf every slot is occupied, the routine sets pool_overflow and returns. It does not write beyond the reserved pool. Three meteors cannot fill twenty slots; the flag makes a broken capacity assumption observable.
Visit occupied records only
advance_meteors replaces unit 12’s three explicit calls with one loop. It starts with IX at the first slot and B at SLOTS, and tests active before doing any work. The body is unit 12’s advance_meteor: an active object erases its image, advances by its own speed, checks retirement and contact, and draws its surviving image. Then IX advances by four and B counts down. Empty slots take the skip path but still advance the pointer and counter.
advance_meteors:
ld ix,objects
ld b,SLOTS
advance_meteors_next:
ld a,(ix+2)
or a
jp z,advance_meteors_skip
push bc
ld e,(ix+0)
ld d,(ix+1)
call draw_meteor
pop bc
ld a,(ix+1)
add a,(ix+3)
ld (ix+1),a
cp 174
jr nc,advance_meteors_remove
; Inset rectangles: meteor y+2..9, ship y+3..13.
cp 154
jp c,advance_meteors_skip
cp 172
jp nc,advance_meteors_skip
ld a,(ship_x)
ld c,a
ld a,(ix+0)
sub c
jr nc,advance_meteors_absolute
neg
advance_meteors_absolute:
cp 16
jp nc,advance_meteors_skip
; One contact ends this attempt; no recovery state is needed.
xor a
ld (hull),a
push bc
call impact_sound
pop bc
advance_meteors_remove:
ld (ix+2),0
advance_meteors_skip:
ld a,(ix+2)
or a
jr z,advance_meteors_next_slot
push bc
ld e,(ix+0)
ld d,(ix+1)
call draw_meteor
pop bc
advance_meteors_next_slot:
ld de,4
add ix,de
dec b
jp nz,advance_meteors_next
retUnit 12’s routine left with ret or a jump to draw_meteor when it had finished with a record. Inside the loop, leaving would abandon the remaining records, so every path now reaches advance_meteors_skip instead. That label draws a surviving meteor; a freed record reads zero there and is not drawn. Either way the loop reaches advance_meteors_next_slot.
Contact works as it did in unit 12: it clears hull and frees the record. The walk carries on through the remaining records, and the main loop checks hull once after advance_meteors returns, exactly as unit 11 arranged.
Drawing changes B, so the caller uses push bc and pop bc around draw_meteor, and around impact_sound too. Without that preservation, the drawing row counter would replace the pool’s remaining-slot counter. A routine’s clobber list matters most when it is called inside another loop.
After the hull check, a new routine, count_objects, walks the pool once more and counts the active records into active_count. Zero means every meteor has left, and the main loop jumps to won. This replaces unit 12’s OR of three named active bytes, which could not name twenty. new_game also calls count_objects after its spawns, so active_count reads 3 when the flight begins.
Why does count_objects visit all twenty slots instead of assuming the occupied ones come first?
Show the explanation
Objects leave at different times. Slot 1 can become free while slot 2 is still active, and the next spawn may reuse slot 1. The occupied records need not form one unbroken prefix. Testing each active byte handles those holes.
Make the capacity problem visible
Change SLOTS equ 20 to SLOTS equ 1. Assemble and launch. The first spawn in new_game takes the only record; the second and third find no free record, so pool_overflow is already 1 when the flight begins and only the X=80 meteor falls. This is a deliberate capacity experiment, not a new difficulty mode. Restore twenty slots before continuing.
The full pool refuses a spawn safely. Does that preserve the same game?
Show the explanation
No. We stayed inside the reserved memory, but two meteors never appeared. A missing meteor removes a hazard, so the challenge changes. Memory safety and preserved game rules are different checks.
For this experiment, restore the capacity and confirm that the overflow flag stays clear. A future game could deliberately drop a decorative effect when busy, but it would need to identify that effect as optional. We should not silently apply the same policy to collision checks, scoring or required hazards.
A pool makes storage predictable, but it does not prove the chosen capacity is enough. Any longer storm has to be checked against that bound. We have made one explicit allocation rule, first free slot, not built a general-purpose object framework.
Sources
Zilog, Z80 CPU User Manual, UM008011-0816, instruction descriptions for the operations shown in this lesson. The running experiment supplies the program-specific observations.
Run it here
This unit's program, built for you.
- Program
- meteor-storm.tap
- Machine
- ZX Spectrum 48K (PAL)