The five storms have different courses, but they play alike: the same speeds, the same spacing, the same gentle drift and the same colours. Reaching the fifth storm should feel like getting somewhere. In this lesson each storm gets rules of its own, and the rules are data: five bytes per storm in a table, read when the storm starts.
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.
phase- —
storm- —
storm_speed- —
storm_cut- —
drift_mask- —
score- —
elapsed- —
Space launches; O/P steer; hold Space for 2X. One hit ends the voyage. At the result R retries from the first storm and Q returns to the title.
The inspector shows the three rule bytes of the current storm. Cross a storm and watch them change.
What you should see


The first storm is the one we have always flown. From the second on, meteors fall faster and arrive closer together, and from the third they wander sideways more. Each storm also has its own colours, getting hotter: green, yellow, magenta and red in the second, until the fifth is red all the way down.
The rules as data
All of that comes from one table:
; Each storm's rules: extra speed for every spawn, updates cut from every gap
; between spawns, the mask that sets how often meteors drift (3: every fourth
; update, 1: every second, 0: every update) and its colour table.
storm_rules:
defb 0,0,3
defw row_colours
defb 1,1,3
defw storm_2_colours
defb 1,2,1
defw storm_3_colours
defb 2,2,1
defw storm_4_colours
defb 2,3,0
defw storm_5_colours
; The table clear and paint_bands read; the title sets it to row_colours.
band_colours: defw row_coloursEach storm’s record is five bytes: three defb values and a defw address.
| Storm | Extra speed | Gap cut | Drift mask | Colours |
|---|---|---|---|---|
| 1 | 0 | 0 | 3 | row_colours |
| 2 | 1 | 1 | 3 | storm_2_colours |
| 3 | 1 | 2 | 1 | storm_3_colours |
| 4 | 2 | 2 | 1 | storm_4_colours |
| 5 | 2 | 3 | 0 | storm_5_colours |
The extra speed is added to every spawn’s speed. The gap cut is taken off every delay between spawns. The drift mask sets how often meteors drift, which we come to below. The last two bytes are the address of a 24-byte colour table, one byte per character row, like unit 28’s row_colours. The first storm’s record keeps everything as it was, so the first storm plays exactly as it did in unit 31.
Nothing else about a storm needs to know these numbers. Changing how hard the fifth storm is means changing five bytes, not finding every place the program uses a speed.
Reading a storm’s record
start_storm already finds the storm’s course. Now it also finds its rules:
; Five bytes per storm: speed, gap cut, drift mask, colour table.
ld a,(storm)
ld e,a
ld d,0
ld hl,storm_rules
add hl,de
add hl,de
add hl,de
add hl,de
add hl,de
ld a,(hl)
ld (storm_speed),a
inc hl
ld a,(hl)
ld (storm_cut),a
inc hl
ld a,(hl)
ld (drift_mask),a
inc hl
ld e,(hl)
inc hl
ld d,(hl)
ld (band_colours),de
retThe record for storm number storm is at storm_rules plus five times storm. Five add hl,de instructions make that, as unit 14 found an event, and then the record is copied out a byte at a time: into storm_speed, storm_cut and drift_mask, which are in the run’s block of memory, and the address into band_colours, which is not. Copying the rules once, when the storm starts, means the code that runs every update reads a plain variable instead of finding the record again.
Faster and closer
waves reads each event’s speed and delay. Here is how it uses the rules:
waves:
ld a,(wave)
cp WAVES
ret z
ld a,(wave_timer)
dec a
ld (wave_timer),a
ret nz
ld a,(wave)
ld e,a
ld d,0
ld hl,(course)
add hl,de
add hl,de
add hl,de
add hl,de
add hl,de
ld a,(hl)
ld (spawn_x),a
inc hl
ld a,(storm_speed)
add a,(hl)
ld (spawn_speed),a
inc hl
ld a,(storm_cut)
ld c,a
ld a,(hl)
sub c
ld (wave_timer),a
inc hl
ld a,(hl)
ld (spawn_drift),a
inc hl
ld a,(hl)
ld (spawn_kind),a
ld a,(wave)
inc a
ld (wave),a
jp spawnThe speed is loaded into A as storm_speed, and add a,(hl) adds the event’s own speed from the table, with HL pointing at it. The delay goes the other way: the cut goes into C, the event’s delay into A, and sub c takes the cut away.
The events’ delays are 6 to 10 updates and the largest cut is 3, so the shortest gap in the fifth storm is 3 updates. The speeds are 2 to 5, so the fifth storm’s meteors fall at 4 to 7 pixels per update.
What would happen if a storm's gap cut were 6, and an event's delay was 6?
Show the explanation
sub c gives 0, and wave_timer becomes 0. waves takes 1 from the timer before it tests it, and 0 minus 1 is 255: the next spawn waits 256 updates, more than ten seconds. The storm would stall. A cut has to stay below the smallest delay.
How often to drift
Since unit 15, meteors have drifted sideways on every fourth update. The test was ld a,(ticks) and and 3: the two low bits of the course step are both zero on every fourth step. Now the 3 comes from drift_mask:
; Stars fall vertically; meteors drift one pixel every fourth update.
ld a,(ix+2)
cp 2
jr z,drift_done
ld a,(drift_mask)
ld c,a
ld a,(ticks)
and c
jr nz,drift_done
ld a,(ix+0)
add a,(ix+4)
cp 8
jr c,drift_left_edge
cp 225
jr nc,drift_right_edge
jr drift_storeand c keeps only the bits of ticks that are set in the mask, and the meteor drifts when what is left is zero.
| Mask | Bits tested | Zero on | Drift |
|---|---|---|---|
| 3 (%11) | bits 0 and 1 | steps 0, 4, 8, … | every fourth update |
| 1 (%01) | bit 0 | steps 0, 2, 4, … | every second update |
| 0 | none | every step | every update |
The mask is not a count. It works for every fourth and every second step because 4 and 2 are powers of two: a mask of 2 would make meteors drift on two steps out of every four, in pairs.
Each storm’s colours
Unit 28’s clear wrote row_colours into the attribute map. Its second half is now a routine of its own, paint_bands, and reads whichever table band_colours points at:
; Colour by place: each character row takes one byte from the table at
; band_colours, written to all 32 of its cells. Objects take the colour
; of the row they pass through, so no two objects in one cell can disagree.
paint_bands:
ld hl,$5800
ld de,(band_colours)
ld c,24
clear_row:
ld a,(de)
ld b,32
clear_cell:
ld (hl),a
inc hl
djnz clear_cell
inc de
dec c
jr nz,clear_row
retclear runs straight on into paint_bands, so every screen still gets its colours the same way. Three places choose the table first:
- The title points
band_coloursatrow_colours, so the title has the first storm’s colours whichever storm the last run ended in. new_gamenow callsstart_stormbeforeclear, so a retry paints the first storm’s colours.- The interlude calls
paint_bandsstraight afterstart_stormhas picked the next storm’s table. It changes all 768 attributes between storms, when nothing is moving, and never during flight.
Each later table keeps the cyan HUD, the white score and the white rows for the ship, and changes the storm in between.
Still survivable
Faster meteors that arrive closer together could make a storm that cannot be crossed at all, whoever is playing. The checks rule that out. The route model that steers through the courses now reads each storm’s rules from the program’s memory, applies the same speeds, gaps and drift, and finds a keyboard route through every storm before the checks fly it. They also record every object’s speed in flight: 2 to 5 pixels per update in the first storm, 3 to 6 in the second and third, 4 to 7 in the fourth and fifth.
A route existing does not make a storm comfortable. These are first values, chosen so the model always finds a way through. Play the voyage and tune the table.
There is a side effect. Faster meteors that arrive closer together finish their course sooner, so the storms get shorter:
| Storm | Time | Bonus | × | Added |
|---|---|---|---|---|
| 1 | 20.92 s | 80 | 1 | 80 |
| 2 | 18.28 s | 82 | 2 | 164 |
| 3 | 15.56 s | 85 | 3 | 255 |
| 4 | 16.08 s | 84 | 4 | 336 |
| 5 | 12.08 s | 88 | 5 | 440 |
A harder storm is a quicker one, and unit 31’s bonus pays for speed, so the voyage on the measured route now ends at 13730, up from 13150.
Experiments
Keep a working copy before each change, then press Revert when you have finished.
Change storm 5's record to `defb 2,3,3`. What changes in the fifth storm, and what stays the same?
Show the explanation
Only the drift: the mask is 3 again, so its meteors drift on every fourth update instead of every update. They still fall 2 pixels per update faster and arrive 3 updates sooner, and the storm is still red. Each byte of the record controls one thing.
Make the first storm's record the same as the fifth's: `defb 2,3,0` and `defw storm_5_colours`. What do you see on launch, and on the title?
Show the explanation
A red storm from the start, falling fast and drifting on every update. The title is unchanged: it always points band_colours at row_colours, not at the first storm’s table.
Then try a palette of your own for one storm, keeping the first three and last four bytes so the HUD and the ship rows stay as they are.
The game now gets harder as it goes on. Its record still only keeps the best score, though, which says little about how far a voyage got. The next lesson remembers the furthest storm instead.
Sources
Zilog, Z80 CPU User Manual, UM008011-0816: ADD A,(HL) adds the byte HL points at to A; SUB r subtracts a register from A; AND r keeps the bits set in both. The maintained Meteor Storm source linked through this page supplies the rules table and the routines; its verification reads the rules from the running program, routes and flies every storm, checks every cell’s colour in each storm and records every object’s speed. The screenshots are native Emu198x captures of this program.
Run it here
This unit's program, built for you.
- Program
- meteor-storm.tap
- Machine
- ZX Spectrum 48K (PAL)