ZX Spectrum Assembly, Tic-Tac-Toe – 0x04 Options and end of game
In this chapter of ZX Spectrum Assembly, we will implement the different options of the game and the end of the game; as we left off in the previous chapter, the game continues indefinitely. The options will be selected from a start menu.
Translation by Felipe Monge Corbalán
Table of contents
Colours
In var.asm we will add the following constants for the colours to avoid having to use numbers and to make it easier to change the colours during the game.
INK0: equ $00
INK1: equ $01
INK2: equ $02
INK3: equ $03
INK4: equ $04
INK5: equ $05
INK6: equ $06
INK7: equ $07
INK0: equ $00
INK1: equ $01
INK2: equ $02
INK3: equ $03
INK4: equ $04
INK5: equ $05
INK6: equ $06
INK7: equ $07
As an exercise, go through the title and text declarations and where you see $10, change the number that follows it to the corresponding INK tag.
In the same way, look for INK calls in the rest of the files, and instead of loading the ink number in A, load the corresponding INK tag.
Menu
Some of the start menu texts are already defined in ar.asm: the TitleEspamatica tag and from TitleOptionStart to TitleOptionTime.
We will delete all these lines as we are going to reimplement it, adding colours and positions, and it will look like this:
Title3InStripe: db $10, INK2, $13, $01, $16, $02, $09
defm "Three in a row"
TitleOptionStart: db $10, INK1, $13, $01, $16, $08, $08, "0. "
db $10, INK5
defm "Start"
TitleOptionPlayer: db $10, INK7, $13, $01, $16, $0a, $08, "1. "
db $10, INK6
defm "Players"
TitleOptionPoint: db $10, INK7, $16, $0c, $08, "2. ", $10, INK6
defm "Points"
TitleOptionTime: db $10, INK7, $16, $0e, $08, "3. ", $10, INK6
defm "Time"
TitleEspamatica: db $16, $14, $08
db $10, INK2, "E", $10, INK6, "s"
db $10, INK4, "p", $10, INK5, "a"
db $10, INK2, "m", $10, INK6, "a"
db $10, INK4, "t", $10, INK5, "i"
db $10, INK2, "c", $10, INK6, "a"
db $10, INK7, " 2019", $ff
Now we have the definition of the start menu almost ready to paint, we just need a routine to paint the values of the options. We implement this in screen.asm.
; -------------------------------------------------------------------
; Paint the values of the options.
;
; Alters the value of the AF, BC and HL registers.
; -------------------------------------------------------------------
PrintOptions:
ld a, INK4 ; A = green ink
call INK ; Change ink
ld b, INI_TOP-$0a ; B = coord Y
ld c, INI_LEFT-$15 ; C = coord X
call AT ; Positions cursor
ld hl, MaxPlayers ; HL = value players
call PrintBCD ; Paints it
ld b, INI_TOP-$0c ; B = coord Y
call AT ; Positions cursor
ld hl, MaxPoints ; HL = points value
call PrintBCD ; Paints it
ld b, INI_TOP-$0e ; B = coord Y
call AT ; Positions cursor
ld hl, MaxSeconds ; HL = time value
jp PrintBCD ; Paints it and exits
PrintOptions paints the values of the options in green. It positions the cursor and paints each of the values.
Let’s see what the start menu looks like. We locate Main in main.asm, and under CALL OPENCHAN we add these lines:
Menu:
di ; Deactivate interruptions
im $01 ; Interruptions = mode 1
ei ; Activates interruptions
We have added a label for the menu part, disabled the interrupts to switch to mode one and enabled them again. We will then use the interrupts in mode two, so these lines are needed here.
At the bottom we find CALL CLS and then add the following lines:
ld hl, Title3InStripe ; HL = address three-inline
call PrintString ; Paints the menu
menu_op:
call PrintOptions ; Paint the options
jr menu_op ; Menu loop
We draw the start screen and the values of the options. We remain in an infinite loop as this is where we will implement the menu logic.
With the menu definition and a few lines we have painted it, but we still need to make it work.
Compile it, load it into the emulator and see how it looks.
Before implementing the menu logic, we will implement a routine in game.asm that uses the ROM to read the keyboard and return the ASCII code of the last key pressed. This routine will be used for the menu options and for the player name query.
; -------------------------------------------------------------------
; Wait for a key to be pressed and return its Ascii code.
;
; Output: A -> Ascii code of the key.
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
WaitKeyAlpha:
ld hl, FLAGS_KEY ; HL = address flag keyboard
set $03, (hl) ; Input mode L
; Loop until a key is obtained.
WaitKeyAlpha_loop:
bit $05, (hl) ; Key pressed?
jr z, WaitKeyAlpha_loop ; Not pressed, loop
res $05, (hl) ; Bit set to 0 for future inspections
; Gets the Ascii of the key pressed.
; Valid Ascii 12 ($0C), 13 ($0D) and from 32 ($20) to 127 ($7F)
; If the key pressed is Space, load ' ' in A
WaitKeyAlpha_loadKey:
ld hl, LAST_KEY ; HL = last key pressed address
ld a, (hl) ; A = last key pressed
cp $80 ; Ascii > 127?
jr nc, WaitKeyAlpha ; Yes, invalid key, loop
cp KEYDEL ; Pushed Delete?
ret z ; Yes, exit
cp KEYENT ; Press Enter?
ret z ; Yes, exit
cp KEYSPC ; Powered Space?
jr c, WaitKeyAlpha ; Ascii < space, invalid, loop
ret ; Exits
WaitKeyAlpha waits until a key is pressed that is valid for us, that is: delete, enter or an ASCII code between thirty-two and one hundred and twenty-seven. When a valid key is pressed, it returns the ASCII code for that key in A.
We implement the menu routine in main.asm, just after CALL PrintOptions, and the final aspect is this:
menu_op:
call PrintOptions ; Paint the options
call WaitKeyAlpha ; Wait for keypress
cp KEY0 ; Pushed 0?
jr z, Init ; Yes, start game
menu_Players:
cp KEY1 ; Pushed 1?
jr nz, menu_Points ; No, skip
ld a, (MaxPlayers) ; A = players
xor $03 ; Alternates between 1 and 2
ld (MaxPlayers), a ; Update in memory
jr menu_op ; Loop
menu_Points:
cp KEY2 ; Pushed 2?
jr nz, menu_Time ; No, skip
ld a, (MaxPoints) ; A = points
inc a ; A += 1
cp $06 ; A = 6?
jr nz, menu_PointsDo ; No, skip
ld a, $01 ; A = 1
menu_PointsDo:
ld (MaxPoints), a ; Update in memory
add a, '0' ; A = value ascii
ld (info_points), a ; Update in memory
cp '1' ; Points 1?
jr z, menu_Points1 ; Yes, skip
ld a, 's' ; Plural
jr menu_PointsEnd ; Jump
menu_Points1:
ld a, ' ' ; Singular
menu_PointsEnd:
ld (info_gt1), a ; Update in memory
jr menu_op ; Loop
menu_Time:
cp KEY3 ; Pushed 3?
jr nz, menu_op ; No, loop
ld a, (MaxSeconds) ; A = seconds
add a, $05 ; A += 5
daa ; Decimal setting
cp $35 ; A = 35 BCD?
jr nz, menu_TimeDo ; No, skip
ld a, $05 ; A = 5
menu_TimeDo:
ld (MaxSeconds), a ; Refreshes in memory
jr menu_op ; Loop
After calling the routine that checks if a valid key has been pressed, it evaluates if it is any key between zero and three, and depending on which it is, it acts in one way or another. Note that it only does the decimal adjustment when incrementing seconds; it is the only option whose value is greater than nine.
We compile, load the emulator and see the results. Everything seems to go well until we press zero, start the game and the menu remains.
To fix this, we add the following line below the Init tag:
call CLS ; Clear screen
Start of game
When the game starts, the first thing we will do is ask for the names of the players. First, we add another constant to rom.asm; this address contains the coordinates of the cursor.
; Cursor position on screen 2.
; If loaded at BC -> B = Y, C = X.
CURSOR: equ $5c88
We implement the routine responsible for retrieving the names of the players in game.asm. Although we won’t go through it instruction by instruction, due to the size of the routine we will implement it in blocks.
GetPlayersName:
ld hl, Name_p1
ld de, Name_p1+$01
ld bc, LENNAME*2-$01
ld (hl), " "
ldir
We have cleared the names of the players.
ld e, $01
getPlayersName_loop:
ld a, INK4
call INK
ld b, INI_TOP-$0f
ld c, INI_LEFT-$01
call CLL
call AT
ld hl, TitlePlayerNumber
ld (hl), "1"
ld a, $01
cp e
jr z, getPlayersName_cont
ld (hl), "2"
getPlayersName_cont:
ld hl, TitlePlayerName
call PrintString
ld hl, Name_p1
ld a, $01
cp e
jr z, getPlayersName_cont2
ld hl, Name_p2
We make a loop of at most two iterations, one per player, change the colour, delete the line where the names are requested, position the cursor, prepare the title depending on the player, and point HL to the player’s name.
getPlayersName_cont2:
ld d, $00
ld a, INK3
call INK
call getPlayersName_getName
ld a, (MaxPlayers)
cp $02
jr nz, getPlayersName_onlyOne
inc e
cp e
jr z, getPlayersName_loop
ret
We use D to control the length of the name, change the ink and ask for the player’s name. We get the players, see if there are two players, and if not, default the name to two. If there are two players, we check if the name entered is the name of player two, and if not, we loop to ask for it.
getPlayersName_onlyOne:
ld hl, Name_p2Default
ld de, Name_p2
ld bc, LENNAME
ldir
ret
If it is a single player, give the second player the default name.
getPlayersName_getName:
push hl
call WaitKeyAlpha
pop hl
cp KEYDEL
jr z, getPlayersName_delete
cp KEYENT
jr z, getPlayersName_enter
push de
ld e, a
ld a, LENNAME
cp d
ld a, e
pop de
jr z, getPlayersName_getName
ld (hl), a
inc hl
rst $10
inc d
jr getPlayersName_getName
We wait for a valid key to be pressed, check if it is Delete and if so, we jump to its operation. We check if it is enter and if it is, we jump to its operation.
If it is neither delete nor enter, we check if we have reached the maximum length and if not, we add the character to the name and draw it.
getPlayersName_delete:
ld a, $00
cp d
jr z, getPlayersName_getName
dec d
dec hl
ld a, ' '
ld (hl), a
ld bc, (CURSOR)
inc c
call AT
rst $10
call AT
jr getPlayersName_getName
If the key pressed is delete and the length of the name is not zero, we delete the previous character of the name and the display.
getPlayersName_enter:
ld a, 0
cp d
jr z, getPlayersName_getName
ret
If the key pressed is enter and the length of the name is not zero, the name request is terminated.
The final aspect of the routine is as follows:
; -------------------------------------------------------------------
; Ask for the names of the players.
;
; Alters the value of the AF, BC, DE and HL registers.
; -------------------------------------------------------------------
GetPlayersName:
ld hl, Name_p1 ; HL = address player name 1
ld de, Name_p1+$01 ; DE = HL+1
ld bc, LENNAME*2-$01 ; BC = length names - 1
ld (hl), " " ; Clears first position
ldir ; Clean up the rest
ld e, $01 ; E = 1
getPlayersName_loop:
ld a, INK4 ; A = ink 4
call INK ; Change ink
ld b, INI_TOP-$0f ; B = coord Y
ld c, INI_LEFT-$01 ; X = coord X
call CLL ; Delete the line
call AT ; Position cursor
ld hl, TitlePlayerNumber ; HL = player number
ld (hl), "1" ; Player 1
ld a, $01 ; A = 1
cp e ; Player 1?
jr z, getPlayersName_cont; Yes, skip
ld (hl), "2" ; Player 2
getPlayersName_cont:
ld hl, TitlePlayerName ; HL = title player name
call PrintString ; Paints it
ld hl, Name_p1 ; HL = address player name 1
ld a, $01 ; A = 1
cp e ; Player 1?
jr z, getPlayersName_cont2 ; Yes, skip
ld hl, Name_p2 ; HL = address player name 2
getPlayersName_cont2:
ld d, $00 ; D = counter length name
ld a, INK3 ; A = ink 3
call INK ; Change ink
call getPlayersName_getName; Request player name
ld a, (MaxPlayers) ; A = players
cp $02 ; Two players?
jr nz, getPlayersName_onlyOne ; One player, default name
inc e ; E+=1
cp e ; Compare with players
jr z, getPlayersName_loop; Equals, jumps
ret
; Single player
; Copies the default name of player 2
getPlayersName_onlyOne:
ld hl, Name_p2Default ; HL = default player name 2
ld de, Name_p2 ; DE = name player 2
ld bc, LENNAME ; Length name
ldir ; Copy default name
ret ; Sale
; Requests the player's name
getPlayersName_getName:
push hl ; Preserve HL
call WaitKeyAlpha ; Wait for valid key
pop hl ; Retrieve HL
cp KEYDEL ; Delete?
jr z, getPlayersName_delete ; Yes, skip
cp KEYENT ; Enter?
jr z, getPlayersName_enter ; Yes, skip
push de ; Preserves DE
ld e, a ; E = code Ascii
ld a, LENNAME ; A = maximum length name
cp d ; D = maximum length?
ld a, e ; A = code Ascii
pop de ; Retrieve DE
jr z, getPlayersName_getName ; D = maximum length
; other character
; Enter or Delete
ld (hl), a ; Append character to name
inc hl ; HL = next position
rst $10 ; Print character
inc d ; D+=1
jr getPlayersName_getName; Request another character
getPlayersName_delete:
ld a, $00 ; A = 0
cp d ; Length 0?
jr z, getPlayersName_getName ; Yes, another character
dec d ; D-=1
dec hl ; HL-=1, previous character
ld a, ' ' ; A = space
ld (hl), a ; Clear previous character
ld bc, (CURSOR) ; BC = cursor position
inc c ; BC = previous column for AT
call AT ; Position cursor
rst $10 ; Delete the display character
call AT ; Position cursor
jr getPlayersName_getName; Other character
getPlayersName_enter:
ld a, 0 ; A = 0
cp d ; Length 0?
jr z, getPlayersName_getName ; Yes, another character
ret ; End name
Let’s see if it works. In main.asm we find Init, and under CALL CLS we add the call to the name request, and another call to CLS.
call GetPlayersName ; Request player names
call CLS ; Clear screen
We compile, load into the emulator and see the results. We can now enter the names of the players and they will appear in the game information. If it is a single player game, we are playing against ZX Spectrum.
Shift time
One of the options is the number of seconds each player has to make their move. If the move has not been made within these seconds, the player loses the turn and it passes to the other player.
We will use interrupts to control the time. We create the file int.asm.
At the top of main.asm, just below ORG $5E88, we add the following lines (we already did this in Space Battle):
; -------------------------------------------------------------------
; Flags
; bit 0 -> Reset countdown
; bit 1 -> Lose turn
; bit 2 -> Paint countdown
; bit 3 -> Warning sound, countdown ends
; -------------------------------------------------------------------
flags: db $00
; Value countdown
countdown: db $00
; Seconds per shift
seconds: db $00
Seconds is used to tell the interrupts how many seconds per turn the players have chosen. With seconds, the MaxSeconds variable is no longer needed, so we will remove it.
Locate Main and after the CALL OPENCHAN line, initialise seconds:
ld a, $10 ; A = $10 BCD
ld (seconds), a ; Update seconds
ld (countdown), a ; Update countdown
Locate menu_Time and after JR NZ, menu_op modify the line LD A, (MaxSeconds), as follows:
ld a, (seconds) ; A = seconds
Locate menu_TimeDo, delete LD, (MaxSecond), A and add the following lines instead:
ld (seconds), a ; Update seconds
ld (countdown), a ; Update countdown
In screen.asm, at the end of the PrintOptions routine, we replace LD HL, MaxSeconds with:
ld hl, seconds ; HL = time value
Finally, we delete the MaxSeconds definition in var.asm.
The interrupt routine will change the value of the flags and the countdown, we need to take this into account in the main loop.
For the interrupt routine to run fifty times per second (in PAL, sixty in NTSC), mode two of the interrupts must be enabled. Locate Loop in main.asm and add over it:
di ; Disables interruptions
ld a, $28 ; A = $28
ld i, a ; I = A (interruptions in $7e5c)
im $02 ; Interruptions = mode 2
ei ; Activates interruptions
We disable the interrupts, load $28 into the interrupt register, set it to mode two and enable it.
We look for loop_key and just below it we implement the flag handling logic.
ld a, (flags) ; A = Flags
bit $02, a ; Bit 2 active?
res $02, a ; Disables bit 2
jr z, loop_warning ; Not active, skips
push af ; Preserve AF
call PrintCountDown ; Paint countdown
pop af ; Retrieve AF
We load the flags in A and evaluate if bit two is active and deactivate it. If it is not active, we jump, if it is, we draw the countdown.
loop_warning:
bit $03, a ; Bit 3 active?
res $03, a ; Disable bit 3
jr z, loop_lostMov ; Not active, skip
ld bc, SoundCountDown ; BC = sound direction
call PlayMusic ; Outputs sound
We evaluate whether bit three is active and deactivate it. If it is active, we jump, if it is not, we give the warning sound.
loop_lostMov:
bit $01, a ; Bit 1 active?
res $01, a ; Disables bit 1
ld (flags), a ; Update in memory
halt ; Synchronise with interrupts
jr z, loop_keyCont ; Not active, skip
ld hl, TitleLostMovement ; HL = address mov lost
ld de, TitleLostMov_name ; DE = address name
call DoMsg ; Paint message
ld bc, SoundLostMovement ; BC = sound direction
call PlayMusic ; Outputs sound
jr loop_cont ; Jump
loop_keyCont:
We check if bit one is active, disable it, update the flags and synchronise with the interrupts. If it is, we draw the lost motion message and play the sound. The loop_keyCont tag is above the CALL WaitKeyBoard.
Finally, locate loop_cont, delete JR Loop and add the following lines in its place:
ld a, $01 ; A = restart countdown
ld (flags), a ; Update flags
jp Loop ; Main loop
We have updated the flags so that the interrupt routine restarts the countdown. We also replaced JR with JP because the lines we added make JR out of range.
In screen.asm we will implement the routine that draws the countdown.
; -------------------------------------------------------------------
; Paint the countdown.
;
; Alters the value of the AF, BC and HL registers.
; -------------------------------------------------------------------
PrintCountDown:
ld a, INK3 ; A = ink 3
call INK ; Change ink
ld b, INI_TOP-$0c ; B = coord Y
ld c, INI_LEFT ; C = coord X
call AT ; Position cursor
ld hl, countdown ; HL = direction countdown
call PrintBCD ; Paint countdown left
ld c, INI_LEFT-$1e ; C = coord X
call AT ; Position cursor
jp PrintBCD ; Paints countdown right and exits
We paint the countdown in two positions, on the left and right of the board. We set the ink to magenta, move the cursor to the left, draw the countdown, move the cursor to the right and draw the countdown.
The interrupt handling routine is implemented in int.asm (do not include this file in main.asm). We will also look at this implementation in blocks.
; -------------------------------------------------------------------
; int.asm
;
; Mode 2 Interrupt Handling
; -------------------------------------------------------------------
org $7e5c
; -------------------------------------------------------------------
; Indicators
; bit 0 -> Reset countdown
; bit 1 -> Lose turn
; bit 2 -> Paint countdown
; bit 3 -> Warning sound, countdown ends
; -------------------------------------------------------------------
FLAGS: equ $5e88
COUNTDOWN: equ FLAGS+$01 ; Countdown Value
SECONDS: equ FLAGS+$02 ; Seconds per turn
The interrupt routine is loaded at address $7E5C. Then we add the constants with the memory addresses we will use for the information exchange between main.asm and int.asm.
CountDownISR:
push af
push bc
push de
push hl
push ix ; Preserves records
The first step of the routine is to preserve the value of the registers.
countDown_flags:
ld a, (FLAGS) ; A = flags
and $01 ; Reset countdown?
jr z, countDown_cont ; No, skip
ld a, (SECONDS) ; A = SECONDS
ld (COUNTDOWN), a ; Refreshes in memory
ld a, $04 ; A = paint countdown
ld (FLAGS), a ; Update in memory
jr countDownISR_end ; End
We check if main.asm indicates that the countdown should be restarted (shift change) and jump if it does not. If there is a shift, it restarts the countdown at the value specified in the menu, indicates in flags that the countdown should be painted, and jumps to exit the routine.
countDown_cont:
ld hl, countDownTicks ; HL = ticks counter
inc (hl) ; Counter ticks+=1
ld a, $32 ; A = 50
cp (hl) ; Counter ticks = 50?
jr nz, countDownISR_end ; No, skip.
xor a ; A = 0, Z = 1, Carry = 0
ld (hl), a ; Counter ticks = 0
In countDownTicks we add one at each pause, and when we reach fifty it is a sign that a second has passed, which we check in this block. If it has not reached fifty, it jumps to exit the routine, if it has, we set the ticks counter to zero and continue with the routine.
; It's reached 50, it's been a second.
ld a, (COUNTDOWN) ; A = countdown value
dec a ; A-=1
daa ; Decimal setting
ld (COUNTDOWN), a ; Update in memory
ld b, $04 ; B = paint countdown / 4 sec
cp b ; Less than 4 seconds?
jr nc, countDownISR_reset; A >= 4, skip
set $03, b ; Warning sound
or a ; A = 0?
jr nz, countDownISR_reset; No, skip
set $01, b ; Misses turn
If one second has passed, we calculate what kind of information to pass to main.asm. We subtract one second from the countdown, see if it is below four (it also communicates to main.asm that the countdown has to be painted), and jump if it is not. If it is, we set the bit for the warning sound, evaluate whether the countdown has reached zero, and jump if it has not. Turn on the shift loss bit if it has reached zero.
countDownISR_reset:
ld a, b ; A = B
ld (FLAGS), a ; Updates in memory
Update the value of flags with the information you want to pass to main.asm.
countDownISR_end:
pop ix
pop hl
pop de
pop bc
pop af ; Retrieves records
ei ; Activates interruptions
reti ; Exits
countDownTicks: db $00 ; Ticks (50*sec)
We get the value of the registers, enable interrupts and exit. Finally we declare the tick counter.
Remember that we now need to compile two separate .tap’s and make our own loader.
The basic loader looks like this:
10 CLEAR 24200
20 LOAD ""CODE 24200
30 LOAD ""CODE 32348
40 RANDOMIZE USR 24200
The script to compile on Windows looks like this:
echo off
cls
echo Compiling oxo
pasmo --name OXO --tap main.asm oxo.tap oxo.log
echo Compiling int
pasmo --name INT --tap int.asm int.tap int.log
echo Generating Tic-tac-toe
copy /y /b loader.tap+oxo.tap+int.tap TicTacToe.tap
echo Process completed
The Linux version looks like this:
clear
echo Compiling oxo
pasmo --name OXO --tap main.asm oxo.tap oxo.log
echo Compiling int
pasmo --name INT --tap int.asm int.tap int.log
echo Generating Tic-tac-toe
cat loader.tap oxo.tap int.tap > TicTacToe.tap
echo Process completed
We compile it, load it into the emulator and check that the countdown is painted, that an alarm goes off if it goes under four seconds and that it loses the turn if it goes to zero. We also see the missed turn message.
End of game
At the end of this chapter we will implement the end of the game, which occurs when one of the players reaches the points defined in the menu, or when the maximum number of tables is reached.
At the end of the game we are asked if we want another game; we add the keys of the answer as constants in var.asm. We also add the maximum number of tables.
KEYN: equ $4e ; N key
KEYn: equ $6e ; Key n
KEYS: equ $53 ; S key
KEYs: equ $73 ; Key s
MAXTIES: equ $05 ; Maximum number of tables
Depending on whether you want to play another game or not, you will jump to one place or another in main.asm. We locate the Init tag, and after CALL GetPlayersName we add the following tag:
Start:
We locate the loop_reset tag and just below it we will implement the check to see if someone has won the game.
call PrintPoints ; Paint the dots
ld a, (MaxPoints) ; A = maximum points
ld b, a ; B = A
ld a, (Points_p1) ; A = points player 1
cp b ; Player 1 wins?
jr z, EndPlay ; Yes, skip
ld a, (Points_p2) ; A = points player 2
cp b ; Player 2 wins?
jr z, EndPlay ; Yes, skip
ld b, MAXTIES ; B = maximum tables
ld a, (Points_tie) ; A = points tables
cp b ; A = B?
jr z, EndPlay ; Yes, skip
We draw the points and compare the players’ scores with the set maximum score. If one of the players has the required number of points, they win the game.
At the end of main.asm, before the includes, we add the split end routine.
EndPlay:
di ; Disables interrupts
im $01 ; Mode 1 interrupts
ei ; Activates interruptions
ld hl, TitleGameOver ; HL = title game over
call PrintMsg ; Paints the message
endPlay_waitKey:
call WaitKeyAlpha ; Wait key
cp KEYN ; Pressed N?
jp z, Menu ; Yes, menu
cp KEYn ; Pushed n?
jp z, Menu ; Yes, menu
cp KEYS ; Pulsed S?
jp z, Start ; Yes, start
cp KEYs ; Pushed s?
jp z, Start ; Yes, start
jr endPlay_waitKey ; Not pressed, loop
We deactivate interruptions, switch to mode one, activate interruptions, wait for the question to be answered and, depending on the answer, we jump to one place or another.
- Menu: Return to the main menu. We can select the different options and enter new player names.
- Start: is the label we have added. It clears the screen, paints the screen, resets the game data and switches interrupts to mode two.
If none of the expected keys have been pressed, it will loop until one is.
If you go to the Menu tag, you will see that the first three lines are for switching to interrupt mode one, which is what we do in EndPlay. Delete the three lines below the Menu tag.
We compile, load into the emulator and see the results. Everything seems to work fine, but at the end of the game the score is flashing. This is something we inherited from the beginning, although we didn’t notice it until now.
To change the behaviour, locate PrintPoints in screen.asm and add the lines needed to disable the flickering just below it.
ld a,(ATTR_T) ; A = temporary attributes
and $7f ; Removes flicker
ld (ATTR_T), a ; Update in memory
At this point, we can play the first full two-player games.
ZX Spectrum Assembly, Tic-Tac-Toe
Want to play against the Spectrum and beat it? We will implement this in the next chapter of ZX Spectrum Assembly,
Download the source code from here.
Useful links
ZX Spectrum Assembly, Tic-Tac-Toe 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.