Unit 29 ended with a score that could not keep up. On a full voyage it climbed to 1940, fell to 0500, climbed again and finished on 0020, although the run had earned 5,140 points. The score was one byte, and a byte stops at 255.
In this lesson the score gets a second byte. That means adding, comparing and printing numbers that do not fit in A, using the 16-bit instructions the Z80 has and one it does not.
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- —
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 now shows score as a word. Collect stars and watch it pass 255 without starting again.
What you should see

The score line has an extra digit: SCORE 00000. On the route the checks fly, the score at the start of each storm now reads 0, 960, 1940, 3060 and 4050, and the voyage ends at 05140. Those are unit 29’s numbers with the lost 2560s put back.
Two bytes, low first
score changes from defb 0 to defw 0, and so do best_score and hud_last_score, the HUD’s copy of the score it last printed. A word is two bytes, low byte first, the order unit 18’s elapsed and unit 29’s course use. Together they count to 65,535, and since the score stores tens of points, that is 655,350 points. A five-storm voyage comes nowhere near.
The low byte does what the one-byte score did. The high byte counts how many times the low byte has gone round: a score of 5,140 points is 514 tens, which is 2 × 256 + 2, so the high byte holds 2 and the low byte 2. Unit 29’s program kept only the low byte, which is why it showed 0020.
Adding
A star used to add with add a,c. Now:
advance_meteors_absolute:
cp 16
jp nc,advance_meteors_skip
ld a,(ix+2)
cp 2
jr nz,meteor_contact
ld a,(boost_time)
inc a
ld e,a
ld d,0
ld hl,(score)
add hl,de
ld (score),hl
ld hl,star_sound
call start_sound
jr advance_meteors_removeThe points, 1 at 1X or 2 at 2X, go into E with D set to 0, so DE holds the same number as a word. ld hl,(score) loads both bytes of the score into HL, add hl,de adds the two words, and ld (score),hl stores both bytes back.
add hl,de does the carry for us. When the low bytes add up to more than 255, the extra goes into the high byte, exactly as a carry in column addition goes into the next column. 255 tens plus 2 is 257: low byte 1, high byte 1.
The finish bonus is added the same way:
finish_bonus:
ld (finish_points),a
ld e,a
ld d,0
ld hl,(score)
add hl,de
ld (score),hl
ld hl,(elapsed)finish_points stays one byte. One storm’s bonus is never more than 100, and adding it to the score goes through DE like a star’s points.
The score is 255 tens and you collect a star at 2X. What does the HUD show next?
Show the explanation
SCORE 02570. DE is 2, so HL goes from 255 to 257. In bytes, the low byte goes from 255 to 1 with a carry, and the high byte from 0 to 1. decimal4 prints 257 as 0257, and the score line’s fixed final 0 makes it 2570.
Comparing
A one-byte comparison was cp. The Z80 has no 16-bit compare, so the program subtracts and reads the flags. The HUD only reprints the score when it has changed:
hud_choose_score:
ld a,(hud_drawn)
or a
jr z,hud_score_changed
ld hl,(score)
ld de,(hud_last_score)
or a
sbc hl,de
ret z
hud_score_changed:
ld hl,(score)
ld (hud_last_score),hl
call score_line
ld a,1
ld (hud_drawn),a
retsbc hl,de subtracts DE from HL, and the carry flag as well. That is what the C in the name means, and it is why or a comes first: OR-ing A with itself leaves A unchanged and clears the carry flag, so the subtraction is HL minus DE and nothing more. If the score equals the last one printed, the result is zero, the zero flag is set and ret z returns.
The subtraction destroys HL, so the score is loaded again before it is copied into hud_last_score.
save_score uses the other flag:
; Keep the score if it is at least the best: score - best_score sets carry
; only when the score is lower. Changes AF/DE/HL.
save_score:
ld hl,(score)
ld de,(best_score)
or a
sbc hl,de
ret c
ld hl,(score)
ld (best_score),hl
retWhen the score is lower than the best, the subtraction has to borrow, and the carry flag is set. ret c keeps the best. Otherwise the score is at least the best, and it is stored as the new best score.
Why does save_score need `or a` before `sbc hl,de`, when it has just loaded HL and DE and done nothing else?
Show the explanation
The carry flag is whatever the last instruction that changed it left behind. ld instructions do not change the flags, so after the two loads the carry is still whatever it was before save_score was called. If it happened to be set, sbc would take away one more than DE, and a score equal to the best would look lower than it and not be saved. or a makes the carry 0 every time.
Printing four digits
decimal3 turned a byte into three digits: it subtracted 100 as many times as it could, then 10, and the units were what was left. The new routine, decimal4, does the same with 1,000, 100 and 10 for a word in HL:
; HL (0-9999) to four decimal digits at DE. Each digit counts how many times
; its power of ten can be taken away: adding a negative BC carries while HL
; is still at least that power; the add that does not carry has gone one
; too far, so subtracting BC with the carry clear puts it back.
; Changes AF/BC/DE/HL.
decimal4:
ld bc,-1000
call decimal_digit
ld bc,-100
call decimal_digit
ld bc,-10
call decimal_digit
ld a,l
add a,'0'
ld (de),a
ret
decimal_digit:
ld a,'0'-1
decimal_digit_count:
inc a
add hl,bc
jr c,decimal_digit_count
sbc hl,bc
ld (de),a
inc de
retEach digit comes from decimal_digit. It is handed a negative power of ten in BC, and adds it to HL again and again. Adding a negative number is subtracting, but it gives a useful flag. In 16 bits, -100 is 65,436, and adding 65,436 to HL carries out of the top bit exactly when HL was at least 100. So the carry flag means “there was another hundred to take away”.
A counts the adds. It starts at one below the character '0', and each pass adds 1 to it before adding BC. Here is the hundreds digit of 514:
| Pass | A | HL after add hl,bc |
Carry |
|---|---|---|---|
| 1 | ‘0’ | 414 | yes |
| 2 | ‘1’ | 314 | yes |
| 3 | ‘2’ | 214 | yes |
| 4 | ‘3’ | 114 | yes |
| 5 | ‘4’ | 14 | yes |
| 6 | ‘5’ | 14 - 100, wrapped | no |
The sixth add found no hundred left: HL has gone below zero, wrapping round to 65,450. The carry flag is clear, so sbc hl,bc subtracts BC with nothing extra, and subtracting -100 adds the 100 back: HL is 14 again. A holds '5', and ld (de),a stores it. The tens digit then comes out as '1', and the 4 left over is the units, made into a character with add a,'0'.
Four digits cover 0 to 9,999, or 99,990 points. score_line and records call decimal4, and both digit buffers have a fifth character, the fixed 0.
Experiments
Keep a working copy before each change, then press Revert when you have finished.
In decimal4, change the third `ld bc,-10` to `ld bc,-100`. What does the score line show for a score of 514 tens?
Show the explanation
SCORE 050>0. The first two steps take out no thousands and five hundreds, leaving 14. The third step now tries to take out 100, finds none and stores '0'. The units step is handed 14, and '0' plus 14 is character 62, >. Each step must take out ten times less than the one before it, or the last step is left with more than one digit’s worth.
Put it back. Then, in the star code, replace `ld d,0` with `ld d,1`. What does each star add?
Show the explanation
256 more than before. DE is now 256 plus the star’s points, so a star adds 257 or 258 tens, over 2,500 points. add hl,de adds all 16 bits of DE, so the high byte has to be set deliberately, which is why the original sets D to 0.
The score can now count the whole voyage. Every storm still pays the same bonus, though, and the fifth is the hardest to reach. The next lesson makes it worth more.
Sources
Zilog, Z80 CPU User Manual, UM008011-0816: ADD HL,ss adds a register pair to HL and sets the carry flag on a carry out of bit 15; SBC HL,ss subtracts the register pair and the carry flag from HL, setting the zero flag when the result is zero and the carry flag on a borrow; LD HL,(nn) and LD (nn),HL load and store a pair low byte first; OR r resets the carry flag. The maintained Meteor Storm source linked through this page supplies decimal4 and the score code, and its verification flies the measured route and reads the score lines back from the screen. The screenshot is a native Emu198x capture of this program.
Run it here
This unit's program, built for you.
- Program
- meteor-storm.tap
- Machine
- ZX Spectrum 48K (PAL)