Espamática
ZX SpectrumRetroZ80 Assembly

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.

Translation by Felipe Monge Corbalán

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.

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:

ASM
; -------------------------------------------------------------------
; 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.

ASM
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.

ASM
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
RLARRA
Carry = 1 Byte = 00000011Carry = 1 Byte = 11000000
Carry = 0 Byte = 00000111Carry = 0 Byte = 11100000
Carry = 0 Byte = 00001110Carry = 0 Byte = 01110000
ZX Spectrum Assembly, Space Battle

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.

ASM
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.

ASM
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.

ASM
; -------------------------------------------------------------------
; 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:

ASM
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.

ASM
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.

ASM
; -------------------------------------------------------------------
; 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.

ASM
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:

ASM
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.

ASM
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:

ASM
; -------------------------------------------------------------------
; 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:

ASM
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:

ASM
; -------------------------------------------------------------------
; 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:

ASM
; -------------------------------------------------------------------
; 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.

ASM
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.

ASM
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:

ASM
; -------------------------------------------------------------------
; 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:

ASM
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.

ASM
livesCounter:
db $05
pointsCounter:
dw $0000

We open print.asm to implement the routine that will paint the marker values.

ASM
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.

ASM
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.

ASM
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.

ASM
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.

ASM
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.

ASM
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:

ASM
; -------------------------------------------------------------------
; 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:

ASM
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:

ASM
xor  a
out  ($fe), a
ld   a, (BORDCR)
and  $c7
or   $07
ld   (BORDCR), a

We will change lines four and five as follows:

ASM
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.

ASM
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:

ASM
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:

ASM
; -------------------------------------------------------------------
; 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:

MnemonicCyclesBytesS Z H P N C
ADC A, r41* * * V 0 *
ADC A, N72* * * V 0 *
ADC A, (HL)71* * * V 0 *
ADC A, (IX+N)193* * * V 0 *
ADC A, (IY+N)193* * * V 0 *
ADC HL, BC152* * ? V 0 *
ADC HL, DE152* * ? V 0 *
ADC HL, HL152* * ? V 0 *
ADC HL, SP152* * ? V 0 *
* Affects flag, V = overflow, 0 = sets flag to 0, ? = unknown value
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:

ASM
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:

ASM
; -------------------------------------------------------------------
; 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.

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.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

ACEPTAR
Aviso de cookies

Descubre más desde Espamática

Suscríbete ahora para seguir leyendo y obtener acceso al archivo completo.

Seguir leyendo