ZX Spectrum Assembly, Space Battle – 0x08 Transition between levels and scoreboard
In this chapter of ZX Spectrum Assembly, we will implement the scoreboard and a transition between levels.
We create the folder Step08 and copy from the folder Step07 the files loader.tap, const.asm, ctrl.asm, game.asm, graph.asm, int.asm, main.asm, print.asm, var.asm and make or make.bat.
Level change transition
The first thing we implement is a routine that changes the colour attributes of the screen to those in the A register. We open graph.asm.
Cla: ld hl, $5800 ld (hl), a ld de, $5801 ld bc, $02ff ldir ret
We point HL to the attribute area, LD HL, $5800, load the new attributes at that address, LD (HL), A, point DE to the next address, LD DE, $5801, load into BC the attribute area positions but one (the first one is already changed), LD BC, $02FF, change the whole attribute area, LDIR, and exit, RET.
The appearance of the routine, once commented, is as follows:
; ------------------------------------------------------------------- ; Changes the colour attributes of the display. ; ; Input: A = Colour attributes (FBPPPIII). ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- Cla: ld hl, $5800 ; HL = 1st address attributes ld (hl), a ; Load attributes ld de, $5801 ; DE = 2nd address attributes ld bc, $02ff ; BC = positions to be exchanged ldir ; Change display attributes ret
We also implement in graph.asm the routine that we will use to transition from one level to another. This routine is a variation of the FadeScreen routine found in Compiler Software’s Z80 Assembly course.
We will traverse the entire video area to make a maximum of eight moves on each byte to clean it up.
FadeScreen: ld b, $08 fadeScreen_loop1: ld hl, $4000 ld de, $1800
We load into B the iterations of the outer loop, LD B, $08, HL we set it to the start of the video area, LD HL, $4000, and we load into DE the length of the area (the pixel part), LD DE, $1800.
fadeScreen_loop2: ld a, (hl) or a jr z, fadeScreen_cont bit $00, l jr z, fadeScreen_right rla jr fadeScreen_cont fadeScreen_right: rra
We load in A the byte pointed to by HL, LD A, (HL), check if it is clean (zeroed), OR A, and jump, JR Z, fadeScreen_cont if it is clean.
If we do not jump, we check if the address pointed to by HL is even or odd, BIT $00, L, and if it is even (bit 0 is zero) we jump, JR Z, fadeScreen_right. If odd, we do not jump, we turn A left, RLA, and jump, JR fadeScreen_cont. If the memory address is even, we rotate A to the right, RRA.
Before we continue, let’s stop at three lines, OR A is the first. At this point we want to know if the byte in the video area pointed to by HL is clean (has all bits set to zero), and we can do this with CP $00, consuming two bytes and seven clock cycles. OR A only returns zero if A is zero, and uses one byte and four clock cycles; the flags are affected in much the same way, and in the carry flag the result is the same. Instead of OR A we can use AND A, the result is the same.
The next lines to look at are RLA and RRA. In both cases the A register is rotated: to the left in the case of RLA and to the right in the case of RRA.
- RLA: rotates the byte to the left, sets the value of bit 7 to the carry and passes the value of the carry to bit 0.
- RRA: rotates the byte to the right, places the value of bit 0 in the carry and passes the value of the carry to bit 7.
Carry = 1 Byte = 10000001 | |
RLA | RRA |
Carry = 1 Byte = 00000011 | Carry = 1 Byte = 11000000 |
Carry = 0 Byte = 00000111 | Carry = 0 Byte = 11100000 |
Carry = 0 Byte = 00001110 | Carry = 0 Byte = 01110000 |
As we can see in this table, if at any point in the FadeScreen routine, before we do the rotation, the carry is set to one, we may have some pixels left without cleaning, but this will not happen because another of the things that OR A does is to set the carry to zero, the same as if we used CP $00.
This brings us to the final part of the routine.
fadeScreen_cont: ld (hl), a inc hl dec de ld a, d or e jr nz, fadeScreen_loop2 ld a, b dec a push bc call Cla pop bc djnz fadeScreen_loop1 ret
We update the video position that HL points to with the rotated value, LD (HL), A, and point HL to the next position in the video area, INC HL.
Decrement DE, DEC DE, load the value of D into A, LD A, D, mix it with E, OR E, and loop until DE is zero, JR NZ, fadeScreen_loop2.
We load the value of B into A, LD A, B, remove one from A so that the value is between seven and zero, DEC A, keep the value of BC, PUSH BC, change the screen colours, CALL Cla, get the value of BC, POP BC, and continue the loop until B is zero, DJNZ fadeScreen_loop1. Finally, we exit.
Let’s pause again to explain one part of the code in more detail.
dec de ld a, d or e jr nz, fadeScreen_loop2
So far we have implemented loops using 8-bit registers, as in this case the outer loop; we load eight into B, LD B, $08, and later, with DJNZ, we decrement B and if the result is not zero, we jump and continue in the loop, thanks to the fact that doing INC or DEC on an 8-bit register affects the Z flag.
In the case of 16-bit registers, INC and DEC do not affect the Z flag. If we just decrement the register and then check if the Z flag is set, we have an infinite loop. To make a loop using a 16-bit register, after decrementing the register, we have to load one of its parts into A and then OR the other part, and if both values are zero, as we have seen above, the result is zero, the Z flag is activated and we exit the loop.
The final result of the routine is as follows.
; ------------------------------------------------------------------- ; Screen fade effect. ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- FadeScreen: ld b, $08 ; External loop repeat 8 times, 1*bit fadeScreen_loop1: ld hl, $4000 ; HL = home video area ld de, $1800 ; DE = length video area fadeScreen_loop2: ld a, (hl) ; A = byte pointed to by HL or a ; Any active pixels? jr z, fadeScreen_cont ; None, skip bit $00, l ; HL address even? jr z, fadeScreen_right ; It's even, jump rla ; Rotate A left jr fadeScreen_cont fadeScreen_right: rra ; Rotate A right fadeScreen_cont: ld (hl), a ; Update HL video position inc hl ; HL = next position dec de ld a, d or e jr nz, fadeScreen_loop2 ; Loop until BC = 0 ld a, b ; B = A dec a ; A = A-1 (A between 0 and 7) push bc ; Preserve BC call Cla ; Change screen colours pop bc ; Retrieves BC djnz fadeScreen_loop1 ; Loop until B = 0 ret
To test the implementation, open main.asm, locate the Main_restart routine and replace the CALL ChangeLevel line with the following:
call FadeScreen call ChangeLevel call PrintFrame call PrintInfoGame call PrintShip
We call the screen fade effect, CALL FadeScreen, paint the screen frame, CALL PrintFrame, the game information, CALL PrintInfoGame, and the ship, CALL PrintShip.
We compile, load the emulator, kill all the enemy ships and watch the screen fade effect.

Scoreboard
The scoreboard will show you how many lives you have, how many points you have scored, the level you are at and how many enemies you have left, so we need some more information and we are going to introduce a new concept, the BCD numbers.
BCD numbers
A byte can contain numbers in the range 0 to 255. When we work with numbers in BCD format, this range is reduced from 0 to 99. BCD numbers divide the byte into two nibbles (4 bits) and store values from 0 to 9 in each of them, so the hexadecimal value 0x10, which in decimal is 16, working with BCD would be 10, we would see the number in hexadecimal notation as if it were decimal, which is useful for example when painting or when working with numbers of more than 16 bits.
To be able to work with numbers in this way, we have the DAA (Decimal Adjust Accumulator) instruction, which works as follows:
- Checks bits 0, 1, 2 and 3 if they contain a non-BCD digit greater than nine or the H flag is set, adds or subtracts $06 (0000 0110b) to the byte depending on the operation performed.
- Checks bits 4, 5, 6 and 7 if they contain a non-BCD digit greater than nine or the C flag is set, adds or subtracts $60 (0110 0000b) from the byte depending on the operation performed.
After each arithmetic instruction, increment or decrement, DAA must be performed. Let’s look at an example.
ld a, $09 ; A = $09 inc a ; A = $0a daa ; A = $10 dec a ; A = $0f daa ; A = $09 add a, $03 ; A = $0c daa ; A = $12 sub a, $03 ; A = $0f daa ; A = $09
Number of enemies and level
Now let’s open the var.asm file and locate the enemiesCounter tag, which defines twenty in hexadecimal ($14), and we’ll change it to the value in BCD, $20. We locate levelCounter and see that it defines $01; in this case we won’t change it, but we’ll add a byte with the same value, using the first byte to load the enemies of each level and make the level change, and the second to paint the number of the level we’re in.
; ------------------------------------------------------------------- ; Information about the game ; ------------------------------------------------------------------- enemiesCounter: db $20 levelCounter: db $01, $01
We use these two labels in parts of our program, but we don’t take into account that we are now working with BCD numbers, so we need to locate the places where they are used in order to modify their behaviour.
The first change is to the ChangeLevel routine in game.asm by adding seven lines. The first four lines are added at the beginning of the routine.
ld a, (levelCounter + 1) inc a daa ld b, a
We load the current level (which is already in BCD format) into A, LD A, (levelCounter + 1), increment the level, INC A, do the decimal adjustment, DAA, and load the value into B, LD B, A.
Now add the following line above the changeLevel_end tag:
ld b, a
If we go through here, the next level would be thirty-one and we only have thirty, so we load $01 into A and now we load this value into the register where we have the level in BCD format, LD B, A.
We continue with the reference to the changeLevel_end tag, after which we update the level in memory. Just below this line, LD (levelCounter), A, we add the lines that update the level in memory in BCD format.
ld a, b ld (levelCounter + 1), a
We load the current level in BCD into A, LD A, B, and update it in memory, LD (levelCounter + 1), A.
Two lines later we change LD A, $14 to LD A, $20 to get the total number of enemies in BCD.
The final aspect of the routine is as follows:
; ------------------------------------------------------------------- ; Change level. ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- ChangeLevel: ld a, (levelCounter + 1) ; A = current level BCD inc a ; Increases level daa ; Decimal adjust ld b, a ; B = A ld a, (levelCounter) ; A = current level inc a ; A = next level cp $1f ; Level 31? jr c, changeLevel_end ; Is not 31, skip ld a, $01 ; If 31, A = 1 ld b, a ; B = A changeLevel_end: ld (levelCounter), a ; Update level in memory ld a, b ; A = BCD level ld (levelCounter + 1), a ; Update in memory call LoadUdgsEnemies ; Load enemy graphics ld a, $20 ; A = total number of enemies ld (enemiesCounter), a ; Load into memory ld hl, enemiesConfigIni ; HL = initial configuration ld de, enemiesConfig ; DE = configuration ld bc, enemiesConfigEnd-enemiesConfigIni ; BC = config length ldir ; Load initial config in config ret
If we were to compile now, we would see that killing all the enemies would not result in the level change. This is because the number of enemies is now $20 (32) and we have not yet adapted all the routines to work with BCD.
We continue in game.asm, we go to checkCrashFire_endLoop, above this label is a RET, and just above it are the instructions to subtract an enemy, LD HL, enemiesCounter and DEC (HL). Let’s replace these two lines with the following:
ld a, (enemiesCounter) dec a daa ld (enemiesCounter), a
We load the number of enemies remaining into A, LD A, (enemiesCounter), subtract one, DEC A, make the adjustment, DAA, and update the value in memory, LD (enemiesCounter), A.
The final aspect of the routine is as follows:
; ------------------------------------------------------------------- ; Evaluates the collisions of the shot with enemies. ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- CheckCrashFire: ld a, (flags) ; A = flags and $02 ; Active fire? ret z ; Not active, exits ld de, (firePos) ; DE = firing position ld hl, enemiesConfig ; HL = 1st enemy definition ld b, enemiesConfigEnd-enemiesConfigIni ; B = bytes config sra b ; B = B/2, number of enemies checkCrashFire_loop: ld a, (hl) ; A = enemy Y-coordinate inc hl ; HL = enemy coord X bit $07, a ; Enemy active? jr z, checkCrashFire_endLoop ; Not active, skips and $1f ; A = coord Y enemy cp d ; Compare with shot jr nz, checkCrashFire_endLoop ; Distinct, jumps ld a, (hl) ; A = enemy X coord and $1f ; A = coord X cp e ; Compare with shot jr nz, checkCrashFire_endLoop ; Distinct, jumps dec hl ; HL = coord Y enemy res $07, (hl) ; Deactivates enemy ld b, d ; B = coord Y shot ld c, e ; C = coord X shot call DeleteChar ; Delete shot/enemy ld a, (enemiesCounter) ; A = number of enemies dec a ; Subtract one daa ; Decimal adjust ld (enemiesCounter), a ; Update in memory ret ; Exits checkCrashFire_endLoop: inc hl ; HL = coord Y enemy next djnz checkCrashFire_loop ; Loop as long as B > 0 ret
There is another routine in which we subtract an enemy, which evaluates collisions between the ship and the enemy.
We find the tag checkCrashShip_endLoop, above it we find JP PrintExplosion, and above it two lines equal to the ones we have replaced, and we have to replace them as before.
The final aspect of the routine looks like this:
; ------------------------------------------------------------------- ; Evaluates enemy collisions with the ship. ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- CheckCrashShip: ld de, (shipPos) ; DE = ship position ld hl, enemiesConfig ; HL = enemiesConfig ld b, enemiesConfigEnd-enemiesConfigIni ; B = bytes config sra b ; B = B/2, number of enemies checkCrashShip_loop: ld a, (hl) ; A = enemy Y-coordinate inc hl ; HL = enemy coord X bit $07, a ; Enemy active? jr z, checkCrashShip_endLoop ; Not active, skips and $1f ; A = coord Y enemy cp d ; Compare with ship jr nz, checkCrashShip_endLoop ; Distinct, skip ld a, (hl) ; A = enemy X coord and $1f ; A = coord X cp e ; Compare with ship jr nz, checkCrashShip_endLoop ; Distinct, skip dec hl ; HL = coord Y enemy res $07, (hl) ; Deactivates enemy ld a, (enemiesCounter) ; A = number of enemies dec a ; Subtract one daa ; Decimal adjust ld (enemiesCounter), a ; Update in memory jp PrintExplosion ; Paint explosion and it comes out checkCrashShip_endLoop: inc hl ; HL = coord Y next enemy djnz checkCrashShip_loop ; Loop until B = 0 ret
If we now compile and load in the emulator, everything works again.
Painting BCD numbers
We are going to implement a routine that draws the BCD numbers on the screen, and as you will see, it is relatively simple. To calculate the character code for each of the digits, just add the zero character.
We will open print.asm and implement the routine that draws the BCD numbers, receiving the address of the number to draw in HL.
PrintBCD: ld a, (hl) and $f0 rra rra rra rra add a, '0' rst $10
We load the number to be painted in A, LD A, (HL), we keep the tens, AND $F0, we set the value in bits zero to three, RRA, RRA, RRA, RRA, we add the code of character 0, ADD A, ‘0’, and we paint the tens, RST $10.
ld a, (hl) and $0f add a, '0' rst $10 ret
We load the number to be painted in A, LD A, (HL), we keep the units, AND $0F, we add the code of the character 0, ADD A, ‘0’, and we paint the units, RST $10. Finally, we exit, RET.
The final aspect of the routine is as follows:
; ------------------------------------------------------------------- ; Paints numbers in BCD format ; ; Input: HL -> Pointer to number to be painted ; ; Alters the value of the AF registers. ; ------------------------------------------------------------------- PrintBCD: ld a, (hl) ; A = number to be painted and $f0 ; A = tens rra rra rra rra ; Passes to bits 0 to 3 add a, '0' ; A = A + character 0 rst $10 ; Paint digit ld a, (hl) ; A = number to be painted and $0f ; A = units add a, '0' ; A = A + character 0 rst $10 ; Paint digit ret
Painting the scoreboard
We implement the routine that paints the scoreboard: lives, points, level and enemies.
The first thing to do is to define the location constants for each of the marker elements. We open const.asm and add:
COR_ENEMY: EQU $1705 ; Coord info enemies COR_LEVEL: EQU $170d ; Coord info level COR_LIVE: EQU $171e ; Coord info lives COR_POINT: EQU $1717 ; Coord info points
The values of the game information will be painted on the command line, we have two lines here. Remember that for the ROM routine that positions the cursor, the upper left corner is $1820, or Y = 24, X = 32, so the values will be painted in line 23 and in columns 5, 13, 23 and 30. If we subtract the row and column values from 24 and 32, the result is the coordinates if the top right corner were $0000.
In var.asm we will add definitions to keep track of lives and points.
livesCounter: db $05 pointsCounter: dw $0000
We open print.asm to implement the routine that will paint the marker values.
PrintInfoValue: ld a, $05 call Ink ld a, $01 call OPENCHAN
We load ink five into A, LD A, $05, and change it, CALL Ink. As the values are painted on the command line, we load into A channel one, LD A, $01, and open it, CALL OPENCHAN.
ld bc, COR_LIVE call At ld hl, livesCounter call PrintBCD
We load into BC the position where we will draw the lives, LD BC, COR_LIVE, position the cursor, CALL At, point HL to the lives counter, LD HL, livesCounter, and draw the lives, CALL PrintBCD.
ld bc, COR_POINT call At ld hl, pointsCounter + 1 call PrintBCD ld hl, pointsCounter call PrintBCD
We load into BC the position where we are going to draw the points, LD BC, COR_POINT, position the cursor, CALL At, point HL to the thousands and hundreds of points, LD HL, pointsCounter + 1, and draw it, CALL PrintBCD. We point HL to the tens and units of the points, LD HL, pointsCounter, and draw it, CALL PrintBCD.
ld bc, COR_LEVEL call At ld hl, levelCounter + 1 call PrintBCD
We load into BC the position where we want to draw the level, LD BC, COR_LEVEL, we position the cursor, CALL At, HL we point it to the level counter in BCD format, LD HL, levelCounter + 1, and we draw it, CALL PrintBCD.
ld bc, COR_ENEMY call At ld hl, enemiesCounter call PrintBCD
We load into BC the position where we are going to print the enemy counter, LD BC, COR_ENEMY, position the cursor, CALL At, HL we point it at the enemy counter, LD HL, enemiesCounter, and paints it, CALL PrintBCD.
ld a, $02 call OPENCHAN ret
Before leaving, we activate the top screen. We load the channel in A, LD A, $02, change it, CALL OPENCHAN, and exit, RET.
The last aspect of the routine is as follows:
; ------------------------------------------------------------------- ; Paints the values of the line item information. ; ; Alters the value of the AF, BC and HL registers. ; ------------------------------------------------------------------- PrintInfoValue: ld a, $05 ; A = ink 5 call Ink ; Change ink ld a, $01 ; A = channel 1 call OPENCHAN ; Activate channel, command line ld bc, COR_LIVE ; BC = position lives call At ; Position cursor ld hl, livesCounter ; HL = livesCounter call PrintBCD ; Paints it ld bc, COR_POINT ; BC = position points call At ; Position cursor ld hl, pointsCounter+1 ; HL = units thousands and hundreds call PrintBCD ; Paints it ld hl, pointsCounter ; HL = tens and units call PrintBCD ; Paints it ld bc, COR_LEVEL ; BC = position levels call At ; Position cursor ld hl, levelCounter+1 ; HL = levelCounter in BCD call PrintBCD ; Paints it ld bc, COR_ENEMY ; BC = enemy position call At ; Position cursor ld hl, enemiesCounter ; HL = enemiesCounter call PrintBCD ; Paints it ld a, $02 ; A = channel 2 call OPENCHAN ; Activates channel, top screen ret
Now it’s time to see if what we’ve implemented works, open main.asm, locate Main and the DI statement, just above it we add the following line to paint the game information:
call PrintInfoValue
Find Main_restart, and just before the last line, JR, Main_loop, add the same line as before.
Compile, load into the emulator and see the results.

As you can see, only the level is updated, the rest of the game information is not. Also, it is not painted with the colour we have defined.
The colour part is the first thing we will fix. The system variable into which we load the screen attributes in the ink routine affects the top screen, so the command line is not affected. The command line attributes are in the same system variable as the border attributes (BORDCR), so we will make two changes.
We open print.asm, locate PrintInfoValue and delete the first two lines, LD A, $05 and CALL Ink, because as we have seen, this does not change the command line attributes.
We go back to main.asm, locate Main, and modify the part where the border colour is assigned, which currently looks like this:
xor a out ($fe), a ld a, (BORDCR) and $c7 or $07 ld (BORDCR), a
We will change lines four and five as follows:
xor a out ($fe), a ld a, (BORDCR) and $c0 or $05 ld (BORDCR), a
We compile, load the emulator and see that the values are painted in the chosen colour.
Let’s update the rest of the values. Every time the shot hits an enemy, we have to subtract one enemy and add five points. On the other hand, every time an enemy hits our ship, we have to subtract one life and one enemy.
We open game.asm, locate checkCrahsFire_endLoop and see that the lines above it already subtract one enemy from the counter.
ld a, (enemiesCounter) ; A = number of enemies dec a ; Subtract one daa ; Decimal adjust ld (enemiesCounter), a ; Update value in memory ret
It remains to add five points for killing an enemy and painting the information. We add the following lines between LD (enemiesCounter), A and RET:
ld a, (pointsCounter) add a, $05 daa ld (pointsCounter), a ld a, (pointsCounter + 1) adc a, $00 daa ld (pointsCounter + 1), a call PrintInfoValue
We load the units and tens of points, LD A, (pointsCounter), into A, add five, ADD A, $05, do the decimal adjustment, DAA, and load the value into memory, LD (pointsCounter), A.
The addition of five to the units and the decimal setting may cause a carry, for example if the value was ninety-five, which we need to add to the hundreds.
We load the hundreds and thousands, LD A, (PointsCounter + 1), into A, add zero with carry to A, ADC A, $00, do the decimal adjustment, DAA, load the value into memory, LD (PointsCounter + 1), A, and print the line item information, CALL PrintInfoValue.
The final aspect of the routine is as follows:
; ------------------------------------------------------------------- ; Evaluates the collisions of the shot with enemies. ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- CheckCrashFire: ld a, (flags) ; A = flags and $02 ; Active fire? ret z ; Not active, exits ld de, (firePos) ; DE = firing position ld hl, enemiesConfig ; HL = 1st enemy definition ld b,enemiesConfigEnd-enemiesConfigIni ; B bytes config enemies sra b ; B = B/2, number of enemies checkCrashFire_loop: ld a, (hl) ; A = enemy Y-coordinate inc hl ; HL = enemy coord X bit $07, a ; Enemy active? jr z, checkCrashFire_endLoop ; Not active, skips and $1f ; A = coord Y enemy cp d ; Compare with shot jr nz, checkCrashFire_endLoop ; Distinct, jumps ld a, (hl) ; A = enemy X coord and $1f ; A = coord X cp e ; Compare with shot jr nz, checkCrashFire_endLoop ; Distinct, jumps dec hl ; HL = coord Y enemy res $07, (hl) ; Deactivates enemy ld b, d ; B = coord Y shot ld c, e ; C = coord X shot call DeleteChar ; Delete trigger/enemy ld a, (enemiesCounter) ; A = number of enemies dec a ; Subtract one daa ; Decimal adjust ld (enemiesCounter), a ; Update in memory ld a, (pointsCounter) ; A = units and tens add a, $05 ; A = A + 5 daa ; Decimal adjust ld (pointsCounter), a ; Update in memory ld a, (pointsCounter + 1) ; A = hundreds and ud thousand adc a, $00 ; A = A + 0 with carry over daa ; Decimal adjust ld (pointsCounter + 1), a ; Update in memory call PrintInfoValue ; Paint InfoValue ret ; Exits checkCrashFire_endLoop: inc hl ; HL = coord Y next enemy djnz checkCrashFire_loop ; Loop to B = 0 ret
In this code we have used an instruction that we have not seen before, ADC (Add With Carry). This instruction adds the specified value to A, plus the value of the carry, so that if we add zero to A, and the carry is one, we would add one to A, which is commonly known as «I’ll take one».
ADC possibilities are:
Mnemonic | Cycles | Bytes | S Z H P N C |
ADC A, r | 4 | 1 | * * * V 0 * |
ADC A, N | 7 | 2 | * * * V 0 * |
ADC A, (HL) | 7 | 1 | * * * V 0 * |
ADC A, (IX+N) | 19 | 3 | * * * V 0 * |
ADC A, (IY+N) | 19 | 3 | * * * V 0 * |
ADC HL, BC | 15 | 2 | * * ? V 0 * |
ADC HL, DE | 15 | 2 | * * ? V 0 * |
ADC HL, HL | 15 | 2 | * * ? V 0 * |
ADC HL, SP | 15 | 2 | * * ? V 0 * |
ZX Spectrum Assembly, Space Battle
We only need to subtract one life if the ship crashes into an enemy. We locate checkCrashShip_endLoop, just above it we find the line JP PrintExplosion, and just above it we add the following lines:
ld a, (livesCounter) dec a daa ld (livesCounter), a call PrintInfoValue
We load the lives into A, LD A, (livesCounter), remove one, DEC A, do the decimal adjustment, DAA, load the value into memory, LD (livesCounter), A, and paint the information, CALL PrintInfoValue. As we can see, since the lives counter is a single byte, the modification is less than the one we made for the score counter.
The final aspect of the routine is as follows:
; ------------------------------------------------------------------- ; Evaluates enemy collisions with the ship. ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- CheckCrashShip: ld de, (shipPos) ; DE = ship position ld hl, enemiesConfig ; HL = enemiesConfig ld b, enemiesConfigEnd-enemiesConfigIni ; B = bytes config sra b ; B = B/2, number of enemies checkCrashShip_loop: ld a, (hl) ; A = coord Y enemy inc hl ; HL = enemy coord X bit $07, a ; Enemy active? jr z, checkCrashShip_endLoop ; Not active, skips and $1f ; A = coord Y enemy cp d ; Compare with ship jr nz, checkCrashShip_endLoop ; Distinct, skip ld a, (hl) ; A = enemy X coord and $1f ; A = coord X cp e ; Compare with ship jr nz, checkCrashShip_endLoop ; Distinct, skip dec hl ; HL = coord Y enemy res $07, (hl) ; Deactivates enemy ld a, (enemiesCounter) ; A = number of enemies dec a ; Subtract one daa ; Decimal adjust ld (enemiesCounter), a ; Update in memory ld a, (livesCounter) ; A = lives dec a ; Subtract a daa ; Decimal adjuts ld (livesCounter), a ; Update in memory call PrintInfoValue ; Paint information jp PrintExplosion ; Paint explosion and it comes out checkCrashShip_endLoop: inc hl ; HL = coord Y next enemy djnz checkCrashShip_loop ; Loop until B = 0 ret
We see if the implementation works. We compile it, load it into the emulator and see the results.

We are now ready to play our first games. We have implemented a transition between levels and the scoreboard.
ZX Spectrum Assembly, Space Battle
In the next chapter of ZX Spectrum Assembly, we will implement the start and end of the game.
Download the source code from here.
Useful links
ZX Spectrum Assembly, Space Battle by Juan Antonio Rubio García.
Translation by Felipe Monge Corbalán.
This work is licensed to Creative Commons Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0).
Any comments are always welcome.