Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Tic-Tac-Toe – 0x02 Scoreboards and a two player game

In this chapter of ZX Spectrum Assembly, will be to implement the scoreboards and the two-player game, which will form a large part of the programme.

Translation by Felipe Monge Corbalán

Table of contents

Information

First we add the name of the programme, the players, the markers and the moving piece.

In var.asm we add the necessary data.

ASM
LENNAME: equ $0c           ; Length name players

; Information
Info:              db $10, $07, $16, $00, $03, "Three in a row at "
info_points:       db "5 point"
info_gt1:          db "s"

player1_title:     db $10, $05, $16, $02, $00
player1_name:      defs LENNAME, " "
player1_figure:    db $16, $04, $00, $90, $91, $0d, $91, $90

player_tie:        db $10, $02, $16, $02, $0d, "Tables"

player2_title:     db $10, $06, $16, $02, $14
player2_name:      defs LENNAME, " "
player2_figure:    db $16, $04, $1b, $92, $93, $16, $05, $1b
                   db $94, $95, $ff

; Titles
TitleTurn:         db $16, $15, $05, $13, $01
                   db "Shift for "
TitleTurn_name:    defs LENNAME, " "
                   db $ff
TitleError:        db $10, $02, $16, $15, $0a, $12, $01, $13, $01
                   defm "Box occupied", $ff
TitleEspamatica:   defm "Espamatica 2019", $ff
TitleGameOver:     db $10, $07, $16, $15, $03, $12, $01, $13, $01
                   defm "Game terminated. Other?", $ff
TitleLostMovement: db $10, $02, $12, $01, $13, $01, $16, $15, $05
                   defm "Lose turn "
TitleLostMov_name: defs LENNAME, " "
                   db $ff
TitleOptionStart:  defm "0. Start", $ff
TitleOptionPlayer: defm "1. Players", $ff
TitleOptionPoint:  defm "2. Points", $ff
TitleOptionTime:   defm "3. Time", $ff
TitlePlayerName:   defm "Player "
TitlePlayerNumber: db " : ", $ff
TitlePointFor:     db $10, $07, $16, $15, $05, $12, $01, $13, $01
                   defm "Point for "
TitlePointName:    defs LENNAME, " "
                   db $ff
TitleTie:          db $10, $07, $16, $15, $0d, $12, $01, $13, $01
                   defm "Tables", $ff

; -------------------------------------------------------------------
; Development of the game.
; -------------------------------------------------------------------
; Board squares. One byte per square, from 1 to 9.
; Bit 0, square occupied by player 1.
; Bit 4, square occupied by player 2.
Grid:          db $00, $00, $00, $00, $00, $00, $00, $00, $00
; Positions of the pieces on the board, 2 bytes per piece X, Y
GridPos:       db POS1_LEFT, POS1_TOP ; 1
               db POS2_LEFT, POS1_TOP ; 2
               db POS3_LEFT, POS1_TOP ; 3
               db POS1_LEFT, POS2_TOP ; 4
               db POS2_LEFT, POS2_TOP ; 5
               db POS3_LEFT, POS2_TOP ; 6
               db POS1_LEFT, POS3_TOP ; 7
               db POS2_LEFT, POS3_TOP ; 8
               db POS3_LEFT, POS3_TOP ; 9

Points_p1:     db $00            ; Points player 1
Points_p2:     db $00            ; Points player 2
Points_tie:    db $00            ; Points tables
PlayerMoves:   db $00            ; Player who moves

MaxPlayers:    db $01            ; Maximum players
MaxPoints:     db $05            ; Maximum Points
MaxSeconds:    db $10            ; Maximum seconds

Name_p1:       db "Amstrad CPC " ; Player name 1
Name_p2:       db "ZX Spectrum " ; Player name 2
Name_p2Default:db "ZX Spectrum " ; Default name

In the Info tag, we define the literals that will be displayed at the top of the screen. Although it may seem strange, the division into several labels is due to the fact that there are values that change depending on the choices made by the players:

  • Points needed to win the game.
  • If it is more than one point, it is written in the plural.
  • The names of the players.

We also define the labels we will use to keep track of each player’s score and name.

We go to screen.asm and implement one routine to draw the information and another to draw the score.

ASM
; -------------------------------------------------------------------
; Paint the information of the game.
;
; Alters the value of HL.
; -------------------------------------------------------------------
PrintInfo:
ld   hl, Info              ; HL = information
jp   PrintString           ; Paints it

PrintInfo paints the information to be displayed at the top of the screen. It does not assign ink colours or positions, as this is all defined in the Info tag.

ASM
; -------------------------------------------------------------------
; Paint the score.
;
; Alters the value of AF, BC, DE and HL.
; -------------------------------------------------------------------
PrintPoints:
ld   a, $05                ; A = cyan ink
call INK                   ; Change ink
ld   b, INI_TOP-$04        ; B = coord Y
ld   c, INI_LEFT-$03       ; C = coord X
call AT                    ; Position cursor
ld   hl, Points_p1         ; HL = points player 1
call PrintBCD              ; Paints them

ld   a, $02                ; A = red ink
call INK                   ; Change ink
ld   b, INI_TOP-$04        ; B = coord Y
ld   c, INI_LEFT-$0f       ; C = coord X
call AT                    ; Position cursor
ld   hl, Points_tie        ; HL = points tables
call PrintBCD              ; Paints them

ld   a, $06                ; A = yellow ink
call INK                   ; Change ink
ld   b, INI_TOP-$04        ; B = coord Y
ld   c, INI_LEFT-$1e       ; C = coord X
call AT                    ; Position cursor
ld   hl, Points_p2         ; HL = points player 2
jp   PrintBCD              ; Paints them in and out

PrintPoints paints the players’ scores and tables. For each score, it changes the ink, positions the cursor and paints the score.

Now we have everything ready. We go to main.asm and after the call to PrintBoard we add the code that paints the information and the score.

ASM
ld   hl, Name_p1
ld   de, player1_name
ld   bc, LENNAME
ldir
ld   hl, Name_p2
ld   de, player2_name
ld   bc, LENNAME
ldir
call PrintInfo

xor  a
ld   hl, Points_p1
ld   de, Points_p1+$01
ld   bc, $03
ld   (hl), a
ldir
call PrintPoints

The names of the players are in the Name_p1 and Name_p2 tags, so we need to pass them to the appropriate place in the information. We initialise the score and draw it.

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

Two-player game

We will implement the two-player game: controls, tile moves, checks for correctness, and tic-tac-toe performance.

In var.asm we include a series of constants with ink colours, lines on which the three dashes occur, and key codes, which we will use together with the ROM routine to check the controls.

ASM
INKWARNING: equ $C2        ; Ink Warnings
INKPLAYER1: equ $05        ; Player ink 1
INKPLAYER2: equ $06        ; Player 2 ink
INKTIE:     equ $07        ; Ink tables

KEYDEL:     equ $0c        ; Delete key
KEYENT:     equ $0d        ; Enter Key
KEYSPC:     equ $20        ; Space Key
KEY0:       equ $30        ; Key 0
KEY1:       equ $31        ; Key 1
KEY2:       equ $32        ; Key 2
KEY3:       equ $33        ; Key 3
KEY4:       equ $34        ; Key 4
KEY5:       equ $35        ; Key 5
KEY6:       equ $36        ; Key 6
KEY7:       equ $37        ; Key 7
KEY8:       equ $38        ; Key 8
KEY9:       equ $39        ; Key 9

WINNERLINE123: equ $01     ; Winning line 123
WINNERLINE456: equ $02     ; Winning line 456
WINNERLINE789: equ $03     ; Winning line 789
WINNERLINE147: equ $04     ; Winning line 147
WINNERLINE258: equ $05     ; Winning line 258
WINNERLINE369: equ $06     ; Winning line 369
WINNERLINE159: equ $07     ; Winning line 159
WINNERLINE357: equ $08     ; Winning line 357

We implement the logic in the file game.asm. We build it and add the include to main.asm.

ASM
; -------------------------------------------------------------------
; Wait for a key to be pressed on the board.
; During the game the interruptions are activated in order
; to perform the countdown. 
; With interrupts enabled, it does not update FLAGS_KEY/LAST_KEY.
;
; Output: C -> Key pressed.
;
; Alters the value of the AF and BC registers.
; -------------------------------------------------------------------
WaitKeyBoard:
ld   a, $f7                ; A = half-row 1-5
in   a, ($fe)              ; Read half-row
cpl                        ; Invert bits, keys pressed to 1
and   $1F                  ; A = bits 1 to 4
jr   z, waitKey_cont       ; No key pressed, skips

; Evaluates the key pressed from 1 to 5.
ld   c, KEY1               ; C = key 1
ld   b, $05                ; B = keys to check
waitKey_1_5:
rra                        ; Pulsed key?
ret  c                     ; Pressed, jump
inc  c                     ; C = code next key
djnz waitKey_1_5           ; Loop for 5 keys

waitKey_cont:
ld   a, $ef                ; A = half-row 0-6
in   a, ($fe)              ; Read half-row
cpl                        ; Invert bits, keys pressed to 1
and  $1F                   ; A = bits 1 to 4
jr   z, waitKey_end        ; No key pressed, skip

; Evaluates the key pressed 9 to 6
rra                        ; Skip zero key
ld   c, KEY9               ; C = key 9
ld   b, $04                ; B = keys to check
waitKey_9_6:
rra                        ; Pulsed key?
ret  c                     ; Pressed, jump
dec  c                     ; C = next key code
djnz waitKey_9_6           ; Loop for 4 keys

; No key has been pressed
waitKey_end:
ld   c, KEY0
ret

With WaitKeyBoard we will check if any of the keys on the board, from one to nine, have been pressed. In this case we will not use the ROM routine, because later we will have the mode two interrupts active, and neither FLAGS_KEY nor LAST_KEY will be updated, and the ROM will not be able to tell us the last key pressed.

The key check is very similar to the one we did in Space Battle, with the following variations:

  • CPL: we invert the bits and evaluate if any keys were pressed with AND $1F; we skip if none were pressed.
  • What we return, in this case in C, is the ASCII code of the key pressed; of the zero key if none was pressed.

We can also see that, before checking the keys 9 to 6, we make a rotation to ignore the 0 key.

We now continue with the logic of the token movement.

ASM
; -------------------------------------------------------------------
; Check and carry out the movement if it is correct.
;
; Input:  C  -> Key pressed
; Output: Z  -> Correct movement
;         NZ -> Incorrect movement
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
ToMove:
push bc                    ; Preserves BC
ld   hl, Grid              ; HL = grid address
ld   a, c                  ; A = C (key code)
sub  $30                   ; A = numeric value key
dec  a                     ; A-=1, so that it does not add up 
                           ; to one more
ld   b, $00                ; B = 0
ld   c, a                  ; C = A, BC = offset
add  hl, bc                ; HL = key box address
pop  bc                    ; Retrieves BC

ld   a, (hl)               ; A = box value
or   a                     ; Are you free?
ret  nz                    ; Busy, exits

ld   a, (PlayerMoves)      ; A = player moving
or   a                     ; Player 1?
jr   nz, toMove_p2         ; Player 2, jump
set  $00, (hl)             ; Activate box player 1
jr   toMove_end            ; Skip

toMove_p2:
set  $04, (hl)             ; Activate player 2 box

toMove_end:
xor  a                     ; Puts flag Z
ret                        ; Exits

ToMove receives the code of the key pressed in C. We calculate the value by subtracting the ASCII code from zero, subtracting one so as not to add too much to the offset, and point to the cell corresponding to the key pressed. We check that the cell is not occupied, if it is not, we activate bits one or four of the cell according to the player who is moving. If the cell is occupied, it goes out with NZ, if not, it goes out with Z (XOR A).

We need a routine that paints the tile in the right place; we implement this in screen.asm.

ASM
; -------------------------------------------------------------------
; Paint the card.
;
; Input: C -> Key pressed.
;
; Alters the value of the AF, BC and HL registers.
; -------------------------------------------------------------------
PrintOXO:
; Calculation of the card position
ld   a, c                  ; A = key
sub  $30                   ; A = key value
dec  a                     ; A-=1, so as not to over-add in offset
add  a, a                  ; A+=A, offset, two bytes per position
ld   b, $00                ; B = 0
ld   c, a                  ; C = A
ld   hl, GridPos           ; HL = address grid positions
add  hl, bc                ; HL+=BC, address coord X cell
ld   c, (hl)               ; C = coord X cell
inc  hl                    ; HL = address coord Y
ld   b, (hl)               ; B = coord Y cell
call AT                    ; Position cursor

; Calculation sheet
ld   a, (PlayerMoves)      ; A = player who moves
or   a                     ; Check player
jr   nz, printOXO_Y        ; Non-zero, skip

printOXO_X:
ld   a, INKPLAYER1         ; A = ink player 1
call INK                   ; Change ink
ld   a, $90                ; A = 1st sprite
rst  $10                   ; Paints it
ld   a, $91                ; A = 2nd sprite
rst  $10                   ; Paints it
dec  b                     ; B = bottom line
call AT                    ; Position cursor
ld   a, $91                ; A = 2nd sprite
rst  $10                   ; Paints it
ld   a, $90                ; A = 1st sprite
rst  $10                   ; Paints it
ret                        ; Exists

printOXO_Y:
ld   a, INKPLAYER2         ; A = ink player 2
call INK                   ; Change ink
ld   a, $92                ; A = 1st sprite
rst  $10                   ; Paints it
ld   a, $93                ; A = 2nd sprite
rst  $10                   ; Paints it
dec  b                     ; B = bottom line
call AT                    ; Position cursor
ld   a, $94                ; A = 3rd sprite
rst  $10                   ; Paints it
ld   a, $95                ; A = 4th sprite
rst  $10                   ; Paints it
ret 

In the first part of PrintOXO, the offset is calculated to obtain the coordinates where to paint the piece, the cursor is obtained and positioned. Then you get the moving player.

The way to paint one tile or the other is almost identical: change the colour, paint the top part, position the cursor on the bottom line and paint the bottom part before leaving.

Now we can start testing how everything we have implemented looks in the program. We go to main.asm and under the Loop tag, before JR Loop, we add the calls to the implemented routines.

ASM
call WaitKeyBoard
ld   a, c
cp   KEY0
jr   z, loop_cont
call ToMove
jr   nz, loop_cont
call PrintOXO
ld   a, (PlayerMoves)
xor  $01
ld   (PlayerMoves), a
loop_cont:

We wait for the player to press a key, and when he does, we check that the move is correct, and if it is, we paint the piece and change players.

We compile, load the emulator and see the results.

We see how the tiles are painted and once the whole board is occupied we can only restart and reload, so we have to implement the following:

  • When the board is full, reset it.
  • Display information messages.
  • Check if there is a tic-tac-toe or a board.
  • Refresh the scoreboard.

Resetting the board

In var.asm, before PlayersMoves, we add a tag to keep track of the number of moves. The board is full when the value reaches nine.

We put it first because PlayersMoves is only restarted at the start of a game. If we reset it with every point, player one would always move first.

ASM
MoveCounter: db $00        ; Move counter

Also in var.asm, we add a constant at the beginning where we specify the length of data to initialise in each line item.

ASM
LENDATA: equ $04           ; Length of data to be initialised

As we need to add more data to initialise, we change the value of this constant.

In main.asm we locate the Loop tag and four lines before the line LD BC, $03. Replace this line with the following:

ASM
ld   bc, LENDATA

Compile, load into the emulator and check that everything still works.

As we have mentioned, we will not always initialise all the data, so it is a good idea to implement a routine that can be passed the length of the data to be initialised, so that it can be called from different places with different values.

The values we want to initialise are:

  • Points_p1
  • Points_p2
  • Points_tie
  • MoveCounter
  • PlayerMoves (only at the start of the game)

There are more values to initialise, but they are higher up, so either we initialise them in two parts, or we change the label. The label is Grid (each player’s moves) and we need to set all the values to zero at the start of each point. Let’s change the position of the label, just above Points_p1, like this:

ASM
; -------------------------------------------------------------------
; Development of the game.
; -------------------------------------------------------------------
; Positions of the pieces on the board, 2 bytes per piece Y, X
GridPos:     db POS1_LEFT, POS1_TOP ; 1
             db POS2_LEFT, POS1_TOP ; 2
             db POS3_LEFT, POS1_TOP ; 3
             db POS1_LEFT, POS2_TOP ; 4
             db POS2_LEFT, POS2_TOP ; 5
             db POS3_LEFT, POS2_TOP ; 6
             db POS1_LEFT, POS3_TOP ; 7
             db POS2_LEFT, POS3_TOP ; 8
             db POS3_LEFT, POS3_TOP ; 9

; Board squares. One byte per square, from 1 to 9.
; Bit 0 to 1, square occupied by player 1.
; Bit 4 to 1, square occupied by player 2.
Grid:        db $00, $00, $00, $00, $00, $00, $00, $00, $00
Points_p1:   db $00        ; Points player 1
Points_p2:   db $00        ; Points player 2
Points_tie:  db $00        ; Points tables
MoveCounter: db $00        ; Move counter
PlayerMoves: db $00        ; Player moving

The value of LENDATA has to be changed to $0D.

We implement the initialisation routine in game.asm.

ASM
; -------------------------------------------------------------------
; Initialises the values of the item/item.
;
; Input: BC -> Length of the values to be initialised.
;
; Alters the value of the BC, DE and HL registers.
; -------------------------------------------------------------------
ResetValues:
ld   hl, Grid              ; HL = Grid address
ld   de, Grid+$01          ; DE = address of Grid+1
ld   (hl), $00             ; Resets first position to zero
ldir                       ; Resets the remainder (BC) to zero

ret

ResetValues gets the number of bytes to clear in BC (must be one less than the total length, that’s why LENDATA is $0D instead of $0E), points HL and DE to the first and second grid positions, clears the first and then the rest.

If you look closely, this routine should look very familiar, take a look at the CLS routine in screen.asm. They are almost identical!

We could implement a routine with the lines LD (HL), $00, LDIR and RET, bearing in mind that before calling it we would have to load the necessary values in HL and BC, and remove CLS and ResetValues. I’ll leave it up to you, I’m going to leave it as it is now.

We go back to main.asm, find Loop and a few lines above XOR A. From XOR A to CALL PrintPoint, we keep only the lines LD BC, LENDATA and CALL PrintPoints, the rest we delete.

Between LD BC, LENDATA and CALL PrintPoints, we add the call to initialise the values, as follows:

ASM
call PrintInfo

ld   bc, LENDATA
call ResetValues
call PrintPoints

Loop:

After loading the length of data to be cleaned into BC, we call the initialisation of the values and draw the points.

We compile, load into the emulator and see that everything still works.

To know if the board is full, we need to update MoveCounter with every move and check if it has reached nine.

Locate loop_cont and add the following lines just above it, after LD (PlayerMoves), A:

ASM
ld   hl, MoveCounter
inc  (hl)
ld   a, $09
cp   (hl)
jr   nz, loop_cont
ld   bc, LENDATA-$01
call ResetValues
call PrintBoard

We increment the move counter, check if it has reached nine, in which case we load into BC the length of the data to be erased minus one (so as not to erase the moving player), initialise the data and draw the board empty.

Compile and load into the emulator. If we press the keys from one to nine until the board is full, the player whose turn it is to move has not been cleared and will move the player whose turn it was in the next turn.

Information messages

During the course of the game there are several messages that need to be displayed to the players: if the move is wrong, which player is moving, who gets the point, if there is a draw and when the game is over.

The messages have been defined at the beginning of the chapter, and in each of them the ink, brightness, flicker and location, so all we need to do is call PrintString with the address of the message in HL.

There are two reasons why this cannot be done:

  • The messages Turn to, Missed turn and Point to are not complete, they are completed with the name of the player who has the turn.
  • Before writing a message, the line must be cleared.

We will implement two routines, one in game.asm and the other in screen.asm; these routines will act as a bridge when we call PrintString.We will start with the routine we have implemented in screen.asm.

ASM
; -------------------------------------------------------------------
; Paint the messages
;
; Input: HL-> message address
; -------------------------------------------------------------------
PrintMsg:
ld   b, INI_TOP-$15        ; B = coord Y line to be deleted (inverted)
call CLL                   ; Deletes the line
jp   PrintString           ; Paint the message

PrintMsg receives the address of the message in HL, clears the message line and jumps to paint the message, where it exits. Clearing the line before painting a new message is the only use of this routine.

We implement the game.asm routine, which completes the three messages that paint the player’s name.

ASM
; -------------------------------------------------------------------
; Completes the messages in which the player's name is displayed.
;
; Input: HL -> message address
;        DE -> address where the name should go
;
; Alters the value of the AF, BC and DE registers.
; -------------------------------------------------------------------
DoMsg:
push hl                    ; Preserves HL
ld   hl, player1_name      ; HL = name player 1
ld   a, (PlayerMoves)      ; A = player
or   a                     ; Player 1?
ld   a, INKPLAYER1         ; A = ink player 1
jr   z, doMsg_cont         ; Player 1, skip
ld   hl, player2_name      ; HL = name player 2
ld   a, INKPLAYER2         ; A = ink player 2
doMsg_cont:
call INK                   ; Change ink
ld   bc, LENNAME           ; BC = length name
ldir                       ; Pass player name to message
pop  hl                    ; Retrieve HL
jp   PrintMsg              ; Paint message

DoMsg receives the address of the message to be painted in HL and the address where the player’s name is to be placed in DE. He checks which player’s turn it is, places one or the other name and the player’s ink. He paints the message and leaves.

We go to main.asm and paint all the messages we can, starting with who has the turn.

Locate Loop and add the following lines just below it:

ASM
ld   b, $19
loop_wait:
halt
djnz loop_wait
ld   hl, TitleTurn
ld   de, TitleTurn_name
call DoMsg
loop_key:

Each time we go through the loop, we paint the player whose turn it is to move. Before we do this, we pause for about half a second to allow time to read the messages.

Now find the line CP KEY0 and just below it in the line JR Z, loop_cont, change loop_cont to loop_key.

Compile, load it into the emulator and you will see that we can see which player has the turn to move.

Let’s also draw the busy box error message. Find the CALL ToMove line, delete the following line, JR NZ, loop_cont, and add the following lines:

ASM
jr   z, loop_print
ld   hl, TitleError
call PrintMsg
jr   Loop
loop_print:

If it returns from the ToMove routine with the Z flag active, this means that the movement is correct and we jump to loop_print, the label we added before CALL PrintOXO. If not, the move is incorrect and we print the error message and go back to the start of the loop.

Compile, load into the emulator and try to move to an occupied square. The error message should be painted, even if it’s only visible for a short time; don’t worry, we’ll remove the pause at the beginning of the loop later and use the sound effects for timing.

Table check

The draw check is simple; if there are nine moves and no three in a row, there is a draw.

Find the loop_cont tag and four lines above it, between JR NZ, loop_cont and LD BC, LENDATA-$01, add the following:

ASM
ld   hl, Points_tie
inc  (hl)
ld   hl, TitleTie
call PrintMsg

With these lines, if nine moves are reached without three in a row, one move is added to the draw score.

Go to the loop_key tag and add the following line above it:

ASM
call PrintPoints

Now, in each iteration of the loop, we draw the notes. The CALL PrintPoints just above the Loop tag can be removed.

Compile, load in the emulator and you will see that not everything works as it should, the table marker is not updated.

Actually everything works as it should, the problem is that ResetValues is resetting the scores, which is easy to fix, but we will fix it later.

We also notice that when repainting the board the flashing is active because the message Tables leaves it that way.

To solve this we could implement a routine to remove the flickering, but we decided to solve it in the board definition. We go to var.asm and just below the Board_1 tag we add the following line to disable the flickering and glowing when the board is painted.

ASM
db $12, $00, $13, $00

Compile, load into the emulator, move until the board is full and check that there is no flickering when repainting.

Tic-tac-toe check

The three in a row check should not be made until at least three moves have been made. In principle, there should be at least five moves before there are three in a row, but since we are going to implement the possibility of the player losing the turn if it takes longer than the set time, it is possible that there will be three in a row with three moves. However, we are going to perform the operation each time a piece is moved, so it will always take the same (approximate) time for each iteration of the loop.

In game.asm we add the routine that checks for tic-tac-toe.

ASM
; -------------------------------------------------------------------
; Check if there are three in a row.
;
; Return: A -> tic-tac-toe line
;         Z if there are three in a row, NZ otherwise.   
; 
; Alters the value of the AF, B and IX registers.
; -------------------------------------------------------------------
CheckWinner:
ld   ix, Grid-$01          ; IX = grid address - 1
ld   b, $03                ; B = sum cells player 1
ld   a, (PlayerMoves)      ; A = player
or   a                     ; Player 1?
jr   z, CheckWinner_check  ; Player 1, skip
ld   b, $30                ; B = sum cells player 2

CheckWinner_check:
ld   a, (ix+1)             ; A = cell 1
add  a, (ix+2)             ; A+= cell 2
add  a, (ix+3)             ; A+= cell 3
cp   b                     ; Three in a row?
ld   a, WINNERLINE123      ; A = flag line 123
ret  z                     ; Tic-tac-toe, exits

ld   a, (ix+4)             ; A = cell 4
add  a, (ix+5)             ; A+= cell 5
add  a, (ix+6)             ; A+= cell 6
cp   b                     ; Three in a row?
ld   a, WINNERLINE456      ; A = indicator line 456
ret  z                     ; Tic-tac-toe, exits

ld   a, (ix+7)             ; A = cell 7
add  a, (ix+8)             ; A+= cell 8
add  a, (ix+9)             ; A+= cell 9
cp   b                     ; Three in a row?
ld   a, WINNERLINE789      ; A = line indicator line 789
ret  z                     ; Tic-tac-toe, exits

ld   a, (ix+1)             ; A = cell 1
add  a, (ix+4)             ; A+= cell 4
add  a, (ix+7)             ; A+= cell 7
cp   b                     ; Three in a row?
ld   a, WINNERLINE147      ; A = indicator line 147
ret  z                     ; Tic-tac-toe, exits

ld   a, (ix+2)             ; A = cell 2
add  a, (ix+5)             ; A+= cell 5
add  a, (ix+8)             ; A+= cell 8
cp   b                     ; Three in a row?
ld   a, WINNERLINE258      ; A = indicator line 258
ret  z                     ; Tic-tac-toe, exits

ld   a, (ix+3)             ; A = cell 3
add  a, (ix+6)             ; A+= cell 6
add  a, (ix+9)             ; A+= cell 9
cp   b                     ; Three in a row?
ld   a, WINNERLINE369      ; A = indicator line 369
ret  z                     ; Tic-tac-toe, exits

ld   a, (ix+1)             ; A = cell 1
add  a, (ix+5)             ; A+= cell 5
add  a, (ix+9)             ; A+= cell 9
cp   b                     ; Three in a row?
ld   a, WINNERLINE159      ; A = indicator line 159
ret  z                     ; Tic-tac-toe, exits

ld   a, (ix+3)             ; A = cell 3
add  a, (ix+5)             ; A+= cell 5
add  a, (ix+7)             ; A+= cell 7
cp   b                     ; Three in a row?
ld   a, WINNERLINE357      ; A = line indicator 357
ret                        ; Last condition, always comes out

In CheckWinner we point IX to the address above the grid, in B we load the value that the cells should add if one or the other player wins: three for player one and thirty for player two.

Then we check the possible combinations of three in a row that exist and leave if there were any, with the winning combination in A and the Z flag activated. To check if there were three in a row, we add up the values of the cells in A and check with B.

In the final check, if there are no three in a row, the Z flag is deactivated (NZ).

In main.asm we will add the lines to do the three in a row check and to act on whether it is successful or not. We add the following lines just below CALL PrintOXO.

ASM
loop_checkWinner:
call CheckWinner
jr   nz, loop_tie
ld   hl, TitlePointFor
ld   de, TitlePointName
call DoMsg
ld   hl, Points_p1
ld   a, (PlayerMoves)
or   a
jr   z, loop_win
inc  hl
loop_win:
inc  (hl)
jr   loop_reset 
loop_tie:

We call the tic-tac-toe check and skip if there are no tic-tac-toes. If there are three in a row, we draw the point-to message, find out which player made it and increase their score.

Just below loop_tie are these lines:

ASM
ld   a, (PlayerMoves)
xor  $01
ld   (PlayerMoves), a

We remove it and put it under the loop_cont tag, otherwise the next point will be started by the same player who won the previous one.

Finally, we add the loop_reset tag before LD BC, LENDATA-$01.

We compile, load into the emulator and everything seems to work, except for the bookmarks, which still don’t update.

We will visually mark where the three in a row have occurred by drawing a diagonal across the three tiles. We implement this routine in screen.asm.

ASM
; -------------------------------------------------------------------
; Print the winning line.
;
; Entry: A -> Winning line
;
; Alters the value of the AF, BC, DE and HL registers.
; -------------------------------------------------------------------
PrintWinnerLine:
ld   hl, COORDX            ; HL = coord X
ld   bc, $6c6c             ; BC = displacement
ld   de, $01ff             ; DE = orientation
cp   WINNERLINE159         ; Does line 159 win?
jr   z, printWinnerLine_159; Yes, skip
ld   e, $01                ; DE = orientation
cp   WINNERLINE357         ; Does line 357 win?
jr   z, printWinnerLine_357; Yes, skip

ld   c, $00                ; Displacement
cp   WINNERLINE147         ; Do you win line 147?
jr   z, printWinnerLine_147; Yes, skip
cp   WINNERLINE258         ; Does line 258 win?
jr   z, printWinnerLine_258; Yes, skip
cp   WINNERLINE369         ; Does line 369 win?
jr   z, printWinnerLine_369; Yes, skip

ld   bc, $006c             ; Displacement
ld   (hl), $48             ; Coord X
inc  hl                    ; Aim HL at coord Y
cp   WINNERLINE123         ; Do you win line 123?
jr   z, printWinnerLine_123; Yes, skip
cp   WINNERLINE456         ; Do you win line 456?
jr   z, printWinnerLine_456; Yes, skip
cp   WINNERLINE789         ; Does line 789 win?
jr   z, printWinnerLine_789; If so, jump to painting it.

printWinnerLine_159:
ld   (hl), $b7             ; Coord X
jr   printWinnerLine_Y

printWinnerLine_357:
ld   (hl), $48             ; Coord X
jr   printWinnerLine_Y

printWinnerLine_147:
ld   (hl), $58             ; Coord X
jr   printWinnerLine_Y

printWinnerLine_258:
ld   (hl), $80             ; Coord X
jr   printWinnerLine_Y

printWinnerLine_369:
ld  (hl), $a8              ; Coord X

printWinnerLine_Y:
inc  hl                    ; Aim HL at coord Y
ld   (hl), $10             ; Coord Y
jr   printWinnerLine_end   ; Paint the line

printWinnerLine_123:
ld   (hl), $70             ; Coord Y
jr   printWinnerLine_end   ; Paint the line

printWinnerLine_456:
ld   (hl), $47             ; Coord Y
jr   printWinnerLine_end   ; Paint the line

printWinnerLine_789:
ld   (hl), $20             ; Coord Y

printWinnerLine_end:
jp   DRAW                  ; Paint the line

PrintWinnerLine gets the winning line in A. First we evaluate which is the line to jump to one part of the routine or another. As the different lines to be drawn share common data, we only change the data that differs and call DRAW to draw the line and exit (see the DRAW comments in ROM.asm for more information).

In main.asm, locate the loop_checkWinner tag and add the following line under JR NZ, loop_tie:

ASM
call PrintWinnerLine

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

As we can see, a line is drawn by marking the three in a row, although it is so fast that we can hardly see it. In the next chapter we will use sound for timing.

However, if you want to check that the line is drawn, you can put these two lines below CALL PrintWinnerLine:

ASM
tmp:
jr   tmp

Don’t forget to remove them afterwards.

Scoreboard update

We are going to fix the long-standing bug that causes the scoreboard not to be updated.

In var.asm we locate the Grid tag and see that below it are the Points_p1, Points_p2 and Point_tie tags. We move these three tags to just below MoveCounter.

ASM
; -------------------------------------------------------------------
; Development of the game.
; -------------------------------------------------------------------
; Positions of the pieces on the board, 2 bytes per piece Y, X
GridPos:     db POS1_LEFT, POS1_TOP ; 1
             db POS2_LEFT, POS1_TOP ; 2
             db POS3_LEFT, POS1_TOP ; 3
             db POS1_LEFT, POS2_TOP ; 4
             db POS2_LEFT, POS2_TOP ; 5
             db POS3_LEFT, POS2_TOP ; 6
             db POS1_LEFT, POS3_TOP ; 7
             db POS2_LEFT, POS3_TOP ; 8
             db POS3_LEFT, POS3_TOP ; 9

; Board squares. One byte per square, from 1 to 9.
; Bit 0 to 1, square occupied by player 1.
; Bit 4 to 1, square occupied by player 2.
Grid:        db $00, $00, $00, $00, $00, $00, $00, $00, $00
MoveCounter: db $00        ; Move counter
Points_p1:   db $00        ; Points player 1
Points_p2:   db $00        ; Points player 2
Points_tie:  db $00        ; Points tables
PlayerMoves: db $00        ; Player moving

In main.asm, find the loop_reset tag, and on the bottom line, LD BC, LENDATA-$01, replace $01 with $04.

Compile, load into the emulator, and now the bookmarks are updated.

ZX Spectrum Assembly, Tic-Tac-Toe

In the next chapter of ZX Spectrum Assembly, we will implement different sounds and use them for timing by pausing between events.

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