Espamática
ZX SpectrumRetroZ80 Assembly

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

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.

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

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

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:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

ASM
ld   a, (seconds)          ; A = seconds

Locate menu_TimeDo, delete LD, (MaxSecond), A and add the following lines instead:

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

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

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

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

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

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

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

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

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

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.

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

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

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

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

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:

VB
10 CLEAR 24200
20 LOAD ""CODE 24200
30 LOAD ""CODE 32348
40 RANDOMIZE USR 24200

The script to compile on Windows looks like this:

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

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

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

ASM
Start:

We locate the loop_reset tag and just below it we will implement the check to see if someone has won the game.

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

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

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

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.

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