Espamática
RetroZ80 AssemblyZX Spectrum

ZX Spectrum Assembly, Space Battle – 0x09 Start the game

In this chapter of ZX Spectrum Assembly, we will implement the start and end of the game.

As in the previous chapters, we will create the folder Step09 and copy the files loader.tap, const.asm, ctrl.asm, game.asm, graph.asm, int.asm, main.asm, make or make.bat, print.asm and var.asm from the folder Step08.

Translation by Felipe Monge Corbalán

PrintString routine

Before we start with the aim of this chapter, we will review the PrintString routine to look at three variations.

The PrintString variations we will see are implemented in a new file called testprint.asm, then we will decide which routine is the definitive one.

Open testprint.asm and add the following code.

ASM
org 	$5dad

TestPrint:
ld   hl, string
ld   b, stringEOF - string
call PrintString

ld   hl, stringNull
call PrintStringNull

ld   hl, stringFF
call PrintStringFF

ret

; -------------------------------------------------------------------
; Paints chains.
;
; Input: HL = first position of the string
;        B  = length of the chain.
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
PrintString:
ld   a, (hl)               ; A = character to be painted
rst  $10                   ; Paint the character
inc  hl                    ; HL = next character
djnz PrintString           ; Loop until B = 0

ret

; -------------------------------------------------------------------
; Paints chains.
;
; Input: HL = first position of the string
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
PrintStringNull:
ld   a, (hl)               ; A = character to be painted
or   a                     ; Is it 0?
ret  z                     ; Is 0, exits
rst  $10                   ; Paint the character
inc  hl                    ; HL = next character
jr   PrintStringNull       ; Loop

; -------------------------------------------------------------------
; Paints chains.
;
; Input: HL = first position of the string
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
PrintStringFF:
ld   a, (hl)               ; A = character to be painted
cp   $ff                   ; Is it $FF?
ret  z                     ; Is $FF, exits
rst  $10                   ; Paint the character
inc  hl                    ; HL = next character
jr   PrintStringFF         ; Loop

string:
db $10, $05, $11, $03, $16, $05, $0a, "Hello Assembly"
stringEOF:

stringNull:
db $10, $07, $11, $01, $16, $07, $0a, "Hello Assembly", $00

stringFF:
db $10, $02, $11, $07, $16, $09, $0a, "Hello Assembly", $ff

end  TestPrint

In this code we can see three PrintString routines:

  • PrintString: the routine as we have it now.
  • PrintStringNull: prints strings and uses the $00 character as the end.
  • PrintStringFF: prints strings and uses the $FF character as the end.

The first of these routines we already know, so we will explain the PrintStringNull routine; PrintStringFF only differs in one line.

ASM
PrintStringNull:
ld 	a, (hl)
or 	a
ret 	z
rst 	$10
inc 	hl
jr 	PrintStringNull

PrintStringNull and PrintStringFF, get in HL the position of the string (same as PrintString), but do not need the length.

We load in A the character pointed to by HL, LD A, (HL), see if it is zero, OR A, and if it is, RET Z.

The line that changes in PrintStringFF is OR A, which is CP $FF, since this is the character used as the end of the string in this case. Remember that the result of OR A is zero only if A is zero. With CP it is zero if A and the comparator are equal.

If the character loaded at A is not the end of the string, we print the character, RST $10, point HL to the next character, INC HL, and loop until the whole string is printed, JR PrintStringNull.

Using one or the other routine has its advantages and disadvantages. Let’s do the first comparison on bytes and clock cycles.

 BytesCycles
PrintString647/42
PrintStringNull751/45
PrintStringFF854/48
ZX Spectrum Assembly, Space Battle

Looking at this table, the most optimal routine is the first one, as it uses fewer bytes and is faster. In reality, it is faster, but it does not occupy fewer bytes, because each time we call it, we have to add two bytes of load in B to the length of the string. If we use it a lot, we quickly see that the byte saving does not happen.

The logical choice is the second routine, which is faster and uses fewer bytes than the third, but as we will see below, it has its disadvantages.

TestPrint is a single program, to compile it we call PASMO from the command line:

ShellScript
pasmo --name TestPrint --tapbas testprint.asm testprint.tap

We compile TestPrint, load it and see the results.

As you can see, everything went well. We have three strings and we have painted each one with a different routine.

Let’s now look at the disadvantages of the second routine, for which we will modify the second string, which is now:

ASM
stringNull:
db $10, $07, $11, $01, $16, $07, $0a, "Hello Assembly", $00

And let’s change the second byte, $07, to $00.

ASM
stringNull:
db $10, $00, $11, $01, $16, $16, $07, $0a, "Hello Assembly", $00

We compile, load and see the results.

Something is wrong. Let’s review the definition of chains.

ASM
string:
db $10, $05, $11, $03, $16, $05, $0a, "Hello Assembly"
stringEOF:

stringNull:
db $10, $00, $11, $01, $16, $07, $0a, "Hello Assembly", $00

stringFF:
db $10, $02, $11, $07, $16, $09, $0a, "Hello Assembly", $ff

The second string, stringNull, is terminated with $00 because the routine paints until it reaches this value. The strings start with $10, which is the INK code, so the next byte must be a colour code, from $00 to $07.

When we pass stringNull to the PrintStringNull routine, it reads the first character, $10 (INK), reads the next character, $00, and exits.

Next we load the string stringFF into HL and call the PrintStringFF routine. This routine reads the first character, $10 (INK), and prints it, but because the previous character was also an INK, it now expects a colour code, and what we give it is $10 (16), an invalid colour, hence the message K Invalid Colour, 40:1.

We modify the second byte of the string stringNull again, setting it to $05, and also the second byte of the string stringFF, now worth $02, setting it to $00.

We compile, load into the emulator and see the results.

As we can see, it is working again and is printing the third string in black, $00, so we opt for PrintStringFF.

We copy the code from the routine, open print.asm, locate PrintString and replace the code from that routine with the code we just copied. The final appearance of the routine should be as follows:

ASM
; -------------------------------------------------------------------
; Paints strings ending in $FF.
;
; Input: HL = first position of the string
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
PrintString:
ld   a, (hl)               ; A = character to be painted
cp   $ff                   ; Is it $FF?
ret  z                     ; Is $FF, exits
rst  $10                   ; Paint the character
inc  hl                    ; HL = next character
jr   PrintString           ; Loop

We need to change the definition of strings and PrintString calls.

We go to print.asm, find the PrintFrame tag, delete the second one and add two after CALL PrintString.

ASM
ld   hl, frameTopGraph     ; HL = top address
; ld   b, frameEnd-framTopGraph ; Loads the length in B
call PrintString           ; Paints the string

ld   hl, frameBottomGraph  ; HL = bottom direction
call PrintString           ; Paints the string

Find PrintInfoGame and delete the fourth line.

ASM
ld   a, $01                ; A = channel 1
call OPENCHAN              ; Activate channel, command line
ld   hl, infoGame          ; HL = address string titles
; ld   b, infoGame_end-infoGame ; Load length into B

Open the var.asm file, find the infoGame tag and add $FF after the enemies. Delete the infoGame_end tag as it is no longer useful.

ASM
infoGame:
db $10, $03, $16, $00, $00
db 'Lives   Points   Level   Enemies', $ff
; infoGame_end:

We locate the frameTopGraph tag and add the byte $FF to the end of the definition. In the frameBottomGraph tag we do the same and delete the frameEnd tag, which is no longer useful.

ASM
frameTopGraph:
db $16, $00, $00, $10, $01
db $96, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97
db $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97, $97
db $97, $97, $97, $97, $97, $98, $ff
frameBottomGraph:
db $16, $14, $00
db $9b, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c
db $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c, $9c
db $9c, $9c, $9c, $9c, $9c, $9d, $ff

We compile (we do this again by running make or make.bat), load it into the emulator and see that it works. We can also see that the program now takes up three bytes less; for each line we removed where we loaded the length of the string into B, we saved two bytes (six in total), but by putting $ff at the end of the strings we have added three bytes again.

Start and end of the game

We will implement a start screen as a menu and two endings, one for when we are killed without finishing the game, the other for when we manage to finish it.

Start of the game

On the start screen we show an introductory text, the control buttons and the selection of the different control types.

We open var.asm and add the definition of the splash screen at the top.

ASM
title:
db $10, $02, $16, $00, $0a, "SPACE BATTLE", $0d, $0d, $0d, $ff

firstScreen:
db $10, $06, "  Alien ships attack the Earth,", $0d
db "the future depends on you.", $0d, $0d
db "  Destroy all enemies that you", $0d
db "can, and protect the planet.", $0d, $0d, $0d
db $10, $03, "Z - Left", $16, $0a, $17, "X - Right"
db $16, $0c, $0c, "V - Shot", $0d, $0d
db $10, $04, "1 - Keyboard      3 - Sinclair 1", $0d, $0d
db "2 - Kempston      4 - Sinclair 2", $0d, $0d, $0d
db $10, $05, "  Aim, shoot and dodge the enemy", $0d
db "ships, defeat and release to the", $0d
db "planet of the threat."
db $ff

We set the colour to red, $10, $02, position the cursor on line 0, column 8, $16, $00, $08, paint the name of the game, SPACE BATTLE, and add three line breaks, $0d, $0d, $0d. We continue defining the rest of the lines until we reach the string delimiter, $FF, the value the PrintString routine expects to know the end of the string.

We will now open the print.asm file and at the end of it we will implement the routine that draws the splash screen and will soon save the choice of controls.

ASM
PrintFirstScreen:
call CLS
ld   hl, title
call PrintString
ld   hl, firstScreen
call PrintString

We clear the screen, CALL CLS, load into HL the memory address where the title definition starts, LD HL, title, paint it, CALL PrintString, load into HL the start address of the screen definition, LD HL, firstScreen, and call the routine that paints the strings, CALL PrintString.

As we are going to allow a choice of different control types later on, we are preparing them.

ASM
printFirstScreen_op:
ld   a, $f7
in   a, ($fe)
bit  $00, a
jr   nz, printFirstScreen_op
call FadeScreen

ret

We load the half stack 1-5 in A, LD A, $F7, read the keyboard, IN A, ($FE), check if the one (Keyboard) is pressed, BIT $00, A, and if not, continue until it is pressed, JR NZ, printFirstScreen_op. We perform the fade effect, CALL FadeScreen, and exit, RET.

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; Display screen and selection of controls.
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
PrintFirstScreen:
Call CLS                   ; Clear screen
Ld   hl, title             ; HL = title definition
call PrintString           ; Print title
ld   hl, firstScreen       ; HL = screen definition
call PrintString           ; Paint screen

printFirstScreen_op:
ld   a, $f7                ; A = half-row 1-5
in   a, ($fe)              ; Read keyboard
bit  $00, a                ; 1 pressed?
jr   nz, printFirstScreen_op ; Not pressed, loop
call FadeScreen            ; Fade screen

ret

It’s time to see if the implementation works. We open the main.asm file, locate the Main tag and within it the call to print the frame, CALL PrintFrame. Just above this call, we will include the call to the routine that paints the splash screen.

ASM
call   PrintFirstScreen

We compile, load the emulator and see the results.

As you can see, we’re now in the start screen, and we won’t leave it until we press one. There is still more to do, but for now we will leave it at that and move on to the end of the game.

End of the game

The end of the game can happen in two different ways: we run out of five lives and lose, or we pass level thirty and win.

Based on the previous paragraph, we will define two different end screens. We go back to var.asm and after the definition of firstScreen we add the definition of the two end screens.

ASM
gameOverScreen:
db $10, $06, "  You have lost all your ships,"
db "you have not been able to save the Earth.", $0d, $0d
db "  The planet has been invaded by aliens.", $0d, $0d
db "  You can try again, it's up to you to save the Earth.", $ff

winScreen:
db $10, $06, "Congratulations, you have destroyed the aliens, you saved the Earth.", $0d, $0d
db "The inhabitants of the planet will be eternally grateful to you.", $ff

pressEnter:
db $10, $04, $16, $10, $03, "Press enter to continue", $ff

As with the start screen, we will implement the routines that print the end screens and wait for the Enter key to continue. Let’s go to print.asm and place ourselves at the end of it.

The routine we are going to implement will have A set to zero if it is the end of the game because we have lost, and non-zero if it is the end of the game because we have won.

ASM
PrintEndScreen:
push af
call FadeScreen
ld   hl, title
call PrintString
pop  af
or   a
jr   nz, printEndScreen_Win

We preserve the value of A, PUSH AF, fade the screen, CALL FadeScreen, point HL to the start of the title, LD HL, title, and paint it, CALL PrintString. Get the value of AF, POP AF, evaluate if A is zero, OR A, and skip if not, JR NZ, printEndScreen_Win.

ASM
printEndScreen_GameOver:
ld   hl, gameOverScreen
call PrintString
jr   printEndScreen_WaitKey

If the value of A is zero, we point HL to the beginning of the end-of-game screen definition, because we have lost the end-of-game screen, LD HL, gameOverScreen, draw it, CALL PrintString, and jump to wait for the Enter key to be pressed, JR printEndScreen_WaitKey.

ASM
printEndScreen_Win:
ld   hl, winScreen
call PrintString

If the value of A is not zero, we point HL to the beginning of the end-of-game screen definition, because we won, LD HL, winScreen, and print it, CALL PrintString.

We prepare the rest to wait for the player to press Enter.

ASM
printEndScreen_WaitKey:
ld   hl, pressEnter
call PrintString
call PrintInfoGame
call PrintInfoValue

We point HL to the beginning of the string that asks for the Enter key to be pressed, LD HL, pressEnter, paint it, CALL PrintString, then the titles of the game information, CALL PrintInfoGame, and finally we print the game information to show the player the level he has reached and the points he has scored, CALL PrintInfoValue.

ASM
printEndScreen_WaitKeyLoop:
ld   a, $bf 
in   a, ($fe)
rra
jr   c, printEndScreen_WaitKeyLoop
call FadeScreen

ret 

We load the Enter-H half stack into A, LD A, $BF, read it, IN A, ($FE), rotate A to the right, RRA, and loop until carry is disabled, JR C, printEndScreen_WaitKeyLoop. If Enter is pressed (carry is disabled), we fade the screen, CALL FadeScreen, and exit, RET.

To evaluate whether Enter was pressed, we read the Enter-H half stack; the zero bit indicates whether Enter was pressed or not, with a value of one if it was not pressed and zero if it was. Turning A clockwise sets the value of the zero bit in the carry, so that if it is on, Enter has not been pressed, and if it is off, it has.

The final aspect of the routine is as follows:

ASM
; -------------------------------------------------------------------
; End of game screen.
;
; Input: A -> End type, 0 = Game Over, !0 = Win.
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
PrintEndScreen:
push af                    ; Preserve AF
call FadeScreen            ; Fade screen
ld   hl, title             ; HL = title
call PrintString           ; Print title
pop  af                    ; Retrieves AF
or   a                     ; ¿A = 0?
jr   nz, printEndScreen_Win ; 0 != 0, skip

printEndScreen_GameOver:
ld   hl, gameOverScreen    ; HL = Game Over screen
call PrintString           ; Paint it
jr   printEndScreen_WaitKey ; Skip waiting for Enter keystroke

printEndScreen_Win:
ld   hl, winScreen         ; HL = WinScreen
call PrintString           ; Paint it

printEndScreen_WaitKey:
ld   hl, pressEnter        ; HL = string 'Press Enter
call PrintString           ; Paint it
call PrintInfoGame         ; Paint game info. titles
call PrintInfoValue        ; Paint game data

printEndScreen_WaitKeyLoop:
ld   a, $bf                ; A = half-stack Enter-H
in   a, ($fe)              ; Reads the keyboard
rra                        ; Rota A right, status Enter
jr   c, printEndScreen_WaitKeyLoop ; Carry, not clicked, loop
call FadeScreen            ; Fade screen

ret

Now we need to test that our end of game screens are displayed correctly, so we go to main.asm, find the CALL PrintFirstScreen line that we added earlier, and just above it we add the following lines:

ASM
xor  a
call PrintEndScreen
ld   a, $01
call PrintEndScreen

Set A to zero, XOR A, print the end of game screen, CALL PrintEndScreen, set A to one, LD A, $01, and paint the end of game.

We compile, load into the emulator and see the result.

The first call we make to the routine that draws the end, we make it with A at zero, and so it draws the screen that corresponds to when we lost all our lives. When we press Enter, we set A to one and call the routine again.

This time it will paint the screen corresponding to the fact that we have passed all thirty levels.

Press Enter and you should see the start screen.

Now we need to put everything together so that everything is in its place. First we’ll remove the last four lines we used to test the PrintEndScreen routine and replace them with the following lines to initialise the game data:

ASM
Main_start:
xor  a
ld   hl, enemiesCounter 
ld   (hl), $20
inc  hl
ld   (hl), a ; $1d
inc  hl
ld   (hl), a ; $29
inc  hl
ld   (hl), $05
inc  hl
ld   (hl), a
inc  hl
ld   (hl), a

call ChangeLevel

Set A to zero, XOR A. We point HL to the enemy counter, LD HL, enemiesCounter, and set it to twenty in BCD, LD (HL), $20. We point HL to the level counter, INC HL, and set it to zero, LD (HL), A. We point HL to the BCD level counter, INC HL, and set it to zero, LD (HL), A. We point HL to the life counter, INC HL, and set it to five, LD (HL), $05. We point HL to the first byte of the BCD point marker, INC HL, and set it to zero, LD (HL), A. We point HL to the second byte, INC HL, and set it to zero, LD (HL), A. Finally, we call level change to reset the enemies and load level one.

In the lines where we load the level, we have commented the values $1D and $29. Later, we will use these values to test the end of the game by playing only the last level.

We look for the load lines of the interrupt vector, from DI to EI, cut them and paste them on top of Main_start.

We find the Main_loop tag and right at the end, above JR Main_loop, we add the check if we have lives.

ASM
ld   a, (livesCounter)
or   a
jr   z, GameOver

We load A in lives, LD A, (livesCounter), evaluate if it is zero, OR A, and jump if it is, JR Z, GameOver.

Look for Main_restart, and under it add the check if we have passed the last level.

ASM
ld   a, (levelCounter)
cp   $1e
jr   z, Win

We load the level in A, LD A, (levelCounter), check if it is the last one, CP $1E, and jump if it is, JR Z, Win.

Find the line CALL ChangeLevel, which is almost at the end of Main_restart, cut and paste it under CALL FadeScreen, so it is the fifth line of Main_restart.

Let’s go to the end of Main_restart, and below that we will implement the end of game.

ASM
GameOver:
xor  a
call PrintEndScreen
jp   Main_start

We set A to zero, XOR A, paint the end screen, CALL PrintEndScreen, and return to the start, JP MainStart.

ASM
Win:
ld   a, $01
call PrintEndScreen
jp   Main_start

We set A to one, LD A, $01, print the end screen, CALL PrintEndScreen, and return to the start, JP MainStart.

As you can see we have used JP instead of JR because if we put JR Main_start in Win we get an out of range jump error.

We are going to make a new change, we are going to make the ship paint at the start position when we change levels. Go into the game.asm file, find the changeLevel_end tag and add the following before the RET:

ASM
ld   hl, shipPos           ; HL = position of the ship
ld   (hl), SHIP_INI        ; Updates it with the initial

We point HL to the ship’s position, LD HL, shipPos, and update it with the initial position, LD (HL), SHIP_INI. Since the Z80 is little-endian, HL points to the ship’s X-coordinate; loading SHIP_INI into (HL) loads the second byte defined in SHIP_INI at the ship’s X-coordinate, LD (HL), $11.

And then we come to the moment of truth, we compile, load the game into the emulator and if all goes well, when we lose the five lives, the game ends, Game Over.

Back to Main.asm and the lines:

ASM
ld   (hl), a               ; $1d
inc  hl
ld   (hl), a               ; $29

We leave them as they are:

ASM
ld   (hl), $1d
inc  hl
ld   (hl), $29

We compile, load and start the game at level thirty. We beat it and game over, win.

Since we have changed a few things in main.asm, we can see how it should look now:

ASM
org  $5dad

; -------------------------------------------------------------------
; Indicators
;
; Bit 0 -> ship must be moved 0 = No, 1 = Yes
; Bit 1 -> Trigger is active 0 = No, 1 = Yes
; Bit 2 -> Enemies must be moved 0 = No, 1 = Yes
; -------------------------------------------------------------------
flags:
db $00

Main:
ld   a, $02
call OPENCHAN

ld   hl, udgsCommon
ld   (UDG), hl

ld   hl, ATTR_P
ld   (hl), $07
call CLS

xor  a
out  ($fe), a
ld   a, (BORDCR)
and  $c0
or   $05
ld   (BORDCR), a

di
ld   a, $28
ld   i, a
im   2
ei

Main_start:
xor  a
ld   hl, enemiesCounter 
ld   (hl), $20
inc  hl
ld   (hl), a ; $1d
inc  hl
ld   (hl), a ; $29
inc  hl
ld   (hl), $05
inc  hl
ld   (hl), a
inc  hl
ld   (hl), a

call ChangeLevel
call PrintFirstScreen
call PrintFrame
call PrintInfoGame
call PrintShip
call PrintInfoValue

call LoadUdgsEnemies
call PrintEnemies

Main_loop:
call CheckCtrl
call MoveFire

push de
call CheckCrashFire
pop  de

ld   a, (enemiesCounter)
cp   $00
jr   z, Main_restart

call MoveShip
call MoveEnemies
call CheckCrashShip

ld   a, (livesCounter)
or   a
jr   z, GameOver

jr   Main_loop

Main_restart:
ld   a, (levelCounter)
cp   $1e
jr   z, Win

call FadeScreen
call ChangeLevel
call PrintFrame
call PrintInfoGame
call PrintShip
call PrintInfoValue
jr   Main_loop

GameOver:
Xor  a
call PrintEndScreen
jp   Main_start

Win:
ld   a, $01
call PrintEndScreen
jp   Main_start

include "ctrl.asm"
include "const.asm"
include "game.asm"
include "graph.asm"
include "print.asm"
include "var.asm"

end  Main

At this point we can play, but there is still work to be done.

ZX Spectrum Assembly, Space Battle

In the next chapter of ZX Spectrum Assembly, we will implement joystick control and get an extra life every five hundred points.

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