ZX Spectrum Assembly, Tic-Tac-Toe – 0x05 Me versus the Spectrum
In this chapter of ZX Spectrum Assembly, we will implement the possibility to play against the Spectrum.
Translation by Felipe Monge Corbalán
Table of contents
Me versus the Spectrum
Once we can play against friends and family, the possibility remains that we may not have anyone to play with, hence the need to be able to play against the Spectrum.
We wrote the bulk of this implementation in game.asm, starting with a routine that generates semi-random numbers between one and nine, so that when the Spectrum starts the game, it does so in a different way.
; -------------------------------------------------------------------
; Gets a semi-random number between 1 and 9.
;
; Return: A -> Number obtained.
; -------------------------------------------------------------------
GetRandomN:
ld a, r ; A = R
and $0f ; Leave bits 0 to 3
inc a ; A+=1
cp $0a ; A > 9?
ret c ; No, exit
rra ; A/=2, because Carry = 0, otherwise SRL A
ret ; Exits
GetRandomN gets a semi-random number between one and nine, using register R. In the RRA line, as seen in the comments, we divide A by two because the carry is zero and because we only do it once, otherwise we would have to do it with SRL A. RRA is one byte and takes four clock cycles, SRL A is only twice as long. It is only divided by two if the number obtained is greater than 9.
The part that carries out the movements of the spectrum is very long, as there are several combinations that are evaluated, so we will see it in blocks. However, as there are many similarities between the different blocks, we will only explain the most important ones.
; -------------------------------------------------------------------
; Moves ZX Spectrum.
;
; Alters the value of the AF, BC and IX registers.
; -------------------------------------------------------------------
ZxMove:
ld a, (MoveCounter) ; A = moves
or a ; Movements = 0?
jr nz, zxMove_center ; No, skip
call GetRandomN ; A = number between 1 and 9
add a, '0' ; A = ascii code
ld c, a ; C = A
call ToMove ; Move to cell
ret ; Exits
zxMove_center:
cp $01 ; Moves > 1?
jr nz, zxMove_cont ; Yes, skip
ld c, KEY5 ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
In this first part we decide if it is the first move of the game, and if so we take a random number between one and nine and move to that cell. If it is the second move, we try to move to cell five (middle). If these conditions are not met, we continue with the checks to see if the spectrum can win or lose.
zxMove_cont:
ld ix, Grid-$01 ; IX = dir Grid-1
ld b, $20 ; B = Spectrum value can earn
call zxMoveToWin_123 ; Move to gain Spectrum
ret z ; If valid, exits
ld b, $02 ; B = value player 1 can win
call zxMoveToWin_123 ; Move to avoid it
ret z ; If valid, exits
jp zxMoveDefence_diagonally ; Defensive movements
Remember that the moves of player two, in this case the Spectrum, are signalled in bit four of the cells, so we load $20 into B, which would be the value if there were two cells occupied by the Spectrum in a row.
We call zxMoveToWin_123 so that if Spectrum wins, it makes the move.
If the Spectrum has not won, we load two into B to check if player one has the move to win. We call zxMoveToWin_123 a second time to prevent it from winning.
If none of the above has happened, we go on the defensive.
; -------------------------------------------------------------------
; Evaluates whether the Spectrum has the movement to win.
; -------------------------------------------------------------------
zxMoveToWin_123:
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$02) ; A+= cell value 2
add a, (ix+$03) ; A+= cell value 3
cp b ; A = B?
jp nz, zxMoveToWin_456 ; No, skip
; Spectrum can win
ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
inc c ; C = key 2
call ToMove ; Move to cell 2
ret z ; If correct, exits
inc c ; C = key 3
call ToMove ; Move to cell 3
ret ; Exits
If the Spectrum occupies two squares, we try to move to square one, if not, to square two and if not, to square three. To go from one square to the next, we increment C, numerically they are contiguous.
The checks for the combinations four, five, six and seven, eight, nine are the same as above.
zxMoveToWin_456:
ld a, (ix+$04) ; A = cell value 4
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$06) ; A+= cell value 6
cp b ; A = B?
jr nz, zxMoveToWin_789 ; No, skip
; Spectrum can win
ld c, KEY4 ; C = key 4
call ToMove ; Move to cell 4
ret z ; If correct, exits
inc c ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
inc c ; C = key 6
call ToMove ; Move to cell 6
ret ; Sale
zxMoveToWin_789:
ld a, (ix+$07) ; A = cell value 7
add a, (ix+$08) ; A+= cell value 8
add a, (ix+$09) ; A+= cell value 9
cp b ; A = B?
jr nz, zxMoveToWin_147 ; No, skip
; Spectrum can win
Ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret z ; If correct, exits
inc c ; C = key 8
call ToMove ; Move to cell 8
ret z ; If correct, exits
inc c ; C = key 9
call ToMove ; Move to cell 9
ret ; Exits
The rest of the tests change slightly.
zxMoveToWin_147:
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$04) ; A+= cell value 4
add a, (ix+$07) ; A+= cell value 7
cp b ; A=B?
jr nz, zxMoveToWin_258 ; No, skip
ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
ld c, KEY4 ; C = key 4
call ToMove ; Move to cell 4
ret z ; If correct, exits
ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret ; Exits
As the squares are not numerically contiguous, at the end we load each key instead of incrementing C. From here on, all the checks to see if the Spectrum can win or lose are the same.
zxMoveToWin_258:
ld a, (ix+$02) ; A = cell value 2
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$08) ; A+= cell value 8
cp b ; A=B?
jr nz, zxMoveToWin_369 ; No, skip
ld c, KEY2 ; C = key 2
call ToMove ; Move to cell 2
ret z ; If correct, exits
ld c, KEY5 ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
ld c, KEY8 ; C = key 8
call ToMove ; Move to cell 8
ret ; Exits
zxMoveToWin_369:
ld a, (ix+$03) ; A = cell value 3
add a, (ix+$06) ; A+= cell value 6
add a, (ix+$09) ; A+= cell value 9
cp b ; A=B?
jr nz, zxMoveToWin_159 ; No, skip
ld c, KEY3 ; C = key 3
call ToMove ; Move to cell 3
ret z ; If correct, exits
ld c, KEY6 ; C = key 6
call ToMove ; Move to cell 6
ret z ; If correct, exits
ld c, KEY9 ; C = key 9
call ToMove ; Move to cell 9
ret ; Exits
zxMoveToWin_159:
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$09) ; A+= cell value 9
cp b ; A=B?
jr nz, zxMoveToWin_357 ; No, skip
ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
ld c, KEY5 ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
ld c, KEY9 ; C = key 9
call ToMove ; Move to cell 9
ret ; Exits
zxMoveToWin_357:
ld a, (ix+$03) ; A = cell value 3
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$07) ; A+= cell value 7
cp b ; A = B?
ret nz
ld c, KEY3 ; C = key 3
call ToMove ; Move to cell 3
ret z ; If correct, exits
ld c, KEY5 ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret ; Exits
The set of routines zxMoveToWin is used both to know if Spectrum can win and make the move to do so, and to know if Spectrum can lose and make the move to avoid it.
If both player one and Spectrum have no chance of winning, we try to anticipate player one’s move. First we check if the player makes a diagonal move that occupies both vertices, and if so we make a cross block.
; -------------------------------------------------------------------
; Diagonal defensive movement.
; -------------------------------------------------------------------
zxMoveDefence_diagonally:
; Checks if player 1 has diagonal checkers
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$09) ; A+= cell value 9
cp b ; A = B?
jr z, zxMoveDefence_crossBlock ; Yes, mov diagonal, skip
ld a, (ix+$03) ; A = cell value 3
add a, (ix+$07) ; A+= cell value 7
cp b ; A = B?
jr nz, zxMoveDefence_crossBlock1 ; No, skip
zxMoveDefence_crossBlock:
; Cross locking
ld c, KEY4 ; C = key 4
call ToMove ; Move to cell 4
ret z ; If correct, exits
ld c, KEY6 ; C = key 6
call ToMove ; Move to cell 6
ret z ; If correct, exits
ld c, KEY2 ; C = key 2
call ToMove ; Move to cell 2
ret z ; If correct, exits
ld c, KEY8 ; C = key 8
call ToMove ; Move to cell 8
ret z ; If correct, exits
Then we check if he has tried a diagonal move with the centre occupied, and if so, depending on which vertex is occupied, we block the vertical.
; -------------------------------------------------------------------
; Defensive diagonal movement with the centre occupied.
; -------------------------------------------------------------------
zxMoveDefence_crossBlock1:
ld a, (ix+$05) ; A = cell value 5
and $0f ; A = player value 1
jr z, zxMoveDefence_cornerBlock16 ; Z = not occupied, skip
ld a, (ix+$01) ; A = cell value 1
and $0f ; A = player value 1
jr z, zxMoveDefence_crossBlock3 ; Z = not occupied, skip
ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret z ; If correct, exits
zxMoveDefence_crossBlock3:
ld a, (ix+$03) ; A = cell value 3
and $0f ; A = player value 1
jr z, zxMoveDefence_crossBlock7 ; Z = not occupied, skip
ld c, KEY9 ; C = key 9
call ToMove ; Move to cell 9
ret z ; If correct, exits
zxMoveDefence_crossBlock7:
ld a, (ix+$07) ; A = cell value 7
and $0f ; A = player value 1
jr z, zxMoveDefence_crossBlock9 ; Z = not occupied, skip
ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
zxMoveDefence_crossBlock9:
ld a, (ix+$09) ; A = cell value 9
and $0f ; A = player value 1
jr z, zxMoveDefence_cornerBlock16 ; Z = not occupied, skip
ld c, KEY3 ; C = key 3
call ToMove ; Move to cell 3
ret z ; If correct, exits
If player one has not attempted a diagonal move, we will see if he has attempted a cross move and try to block it.
; -------------------------------------------------------------------
; Defensive cross movement.
; -------------------------------------------------------------------
zxMoveDefence_cornerBlock16:
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$06) ; A+= cell value 6
cp b ; A = B?
jr nz, zxMoveDefence_cornerBlock34 ; No, no mov cross, skip
ld c, KEY3 ; C = key 3
call ToMove ; Move to cell 3
ret z ; If correct, exits
zxMoveDefence_cornerBlock34:
ld a, (ix+$03) ; A = cell value 3
add a, (ix+$04) ; A+= cell value 4
cp b ; A = B?
jr nz, zxMoveDefence_cornerBlock67 ; No, no mov cross, skip
ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
zxMoveDefence_cornerBlock67:
ld a, (ix+$06) ; A = cell value 6
add a, (ix+$07) ; A+= cell value 7
cp b ; A = B?
jr nz, zxMoveDefence_cornerBlock49 ; No, no mov cross, skip
ld c, KEY9 ; C = key 9
call ToMove ; Move to cell 9
ret z ; If correct, exits
zxMoveDefence_cornerBlock49:
ld a, (ix+$04) ; A = cell value 4
add a, (ix+$09) ; A+= cell value 9
cp b ; A = B?
jr nz, zxMoveDefence_cornerBlock1827 ; No, no mov cross, skip
ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret z ; If correct, exits
zxMoveDefence_cornerBlock1827:
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$08) ; A+= cell value 8
cp b ; A = B?
jr z, zxMoveDefence_cornerBlock1827Cont ; Yes, mov cross, block
ld a, (ix+$02) ; A = cell value 2
add a, (ix+$07) ; A+= Value cell 7
cp b ; A = B?
jr nz, zxMoveDefence_cornerBlock2938 ; No, no mov cross, skip
zxMoveDefence_cornerBlock1827Cont:
ld c, KEY4 ; C = key 4
call ToMove ; Move to cell 4
ret z ; If correct, exits
zxMoveDefence_cornerBlock2938:
ld a, (ix+$02) ; A = cell value 2
add a, (ix+$09) ; A+= cell value 9
cp b ; A = B?
jr z, zxMoveDefence_cornerBlock2938Cont ; Yes, mov cross, block
ld a, (ix+$03) ; A = cell value 3
add a, (ix+$08) ; A+= cell value 8
cp b ; A = B?
jr nz, zxMoveAttack_123 ; No, no mov cross, skip
zxMoveDefence_cornerBlock2938Cont:
ld c, KEY6 ; C = key 6
call ToMove ; Move to cell 6
ret z ; If correct, exits
If we’ve got this far, we’ve established that Spectrum has no move to win, that player one has no move to win, that player one is in no danger from player one’s diagonal or cross moves; it’s time for Spectrum to go on the attack.
This last part will only be done in the first few moves, as the board fills up we will be more concerned with defending than attacking.
; -------------------------------------------------------------------
; Horizontal, vertical and diagonal offensive movement.
; -------------------------------------------------------------------
zxMoveAttack_123:
ld b, $20 ; B = Spectrum value with two boxes
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$02) ; A+= cell value 2
add a, (ix+$03) ; A+= cell value 3
ld c, a ; Preserve A in C
and $03 ; A = player 1 cells
jr nz, zxMoveAttack_456 ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
Jr z, zxMoveAttack_456 ; Yes, jumps
ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
inc c ; C = key 2
call ToMove ; Move to cell 2
ret z ; If correct, exits
inc c ; C = key 3
call ToMove ; Move to cell 3
ret z ; If correct, exits
For the attack, we look for combinations where player one has no cells occupied and the Spectrum has only one.
The rest of the checks are very similar to the previous one.
zxMoveAttack_456:
ld a, (ix+$04) ; A = cell value 4
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$06) ; A+= cell value 6
ld c, a ; Preserve A in C
and $03 ; A = player 1 cells
jr nz, zxMoveAttack_789 ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
jr z, zxMoveAttack_789 ; Yes, jumps
ld c, KEY4 ; C = key 4
call ToMove ; Move to cell 4
ret z ; If correct, exits
inc c ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
inc c ; C = key 6
call ToMove ; Move to cell 6
ret z ; If correct, exits
zxMoveAttack_789:
ld a, (ix+$07) ; A = cell value 7
add a, (ix+$08) ; A+= cell value 8
add a, (ix+$09) ; A+= cell value 9
ld c, a ; Preserve A in C
and $03 ; A = player 1 cells
jr nz, zxMoveAttack_147 ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
jr z, zxMoveAttack_147 ; Yes, jumps
ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret z ; If correct, exits
inc c ; C = key 8
call ToMove ; Move to cell 8
ret z ; If correct, exits
inc c ; C = key 9
call ToMove ; Move to cell 9
ret z ; If correct, exits
zxMoveAttack_147:
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$04) ; A+= cell value 4
add a, (ix+$07) ; A+= cell value 7
ld c, a ; Preserve A in C
and $03 ; A = player 1 boxes
jr nz, zxMoveAttack_258 ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
jr z, zxMoveAttack_258 ; Yes, jumps
ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
ld c, KEY4 ; C = key 4
call ToMove ; Move to cell 4
ret z ; If correct, exits
ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret z ; If correct, exits
zxMoveAttack_258:
ld a, (ix+$02) ; A = cell value 2
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$08) ; A+= cell value 8
ld c, a ; Preserve A in C
and $03 ; A = player 1 cells
jr nz, zxMoveAttack_369 ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
jr z, zxMoveAttack_369 ; Yes, jumps
ld c, KEY2 ; C = key 2
call ToMove ; Move to cell 2
ret z ; If correct, exits
ld c, KEY5 ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
ld c, KEY8 ; C = key 8
call ToMove ; Move to cell 8
ret z ; If correct, exits
zxMoveAttack_369:
ld a, (ix+$03) ; A = cell value 3
add a, (ix+$06) ; A+= cell value 6
add a, (ix+$09) ; A+= cell value 9
ld c, a ; Preserve A in C
and $03 ; A = player 1 cells
jr nz, zxMoveAttack_159 ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
Jr z, zxMoveAttack_159 ; Yes, jumps
ld c, KEY3 ; C = key 3
call ToMove ; Move to cell 3
ret z ; If correct, exits
ld c, KEY6 ; C = key 6
call ToMove ; Move to cell 6
ret z ; If correct, exits
ld c, KEY9 ; C = key 9
call ToMove ; Move to cell 9
ret z ; If correct, exits
zxMoveAttack_159:
ld a, (ix+$01) ; A = cell value 1
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$09) ; A+= cell value 9
ld c, a ; Preserve A in C
and $03 ; A = player 1 cells
jr nz, zxMoveAttack_357 ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
Jr z, zxMoveAttack_357 ; Yes, jumps
Ld c, KEY1 ; C = key 1
call ToMove ; Move to cell 1
ret z ; If correct, exits
ld c, KEY5 ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
ld c, KEY9 ; C = key 9
call ToMove ; Move to cell 9
ret z ; If correct, exits
zxMoveAttack_357:
ld a, (ix+$03) ; A = cell value 3
add a, (ix+$05) ; A+= cell value 5
add a, (ix+$07) ; A+= cell value 7
ld c, a ; Preserve A in C
and $03 ; A = player 1 cells
jr nz, zxMoveGeneric ; Any? Yes, jump
ld a, c ; Retrieves A
and $30 ; A = Spectrum boxes
cp b ; Two busy?
jr z, zxMoveGeneric ; Yes, skip
ld c, KEY3 ; C = key 3
call ToMove ; Move to cell 3
ret z ; If correct, exits
ld c, KEY5 ; C = key 5
call ToMove ; Move to cell 5
ret z ; If correct, exits
ld c, KEY7 ; C = key 7
call ToMove ; Move to cell 7
ret z ; If correct, exits
When we arrive here, the Spectrum has not found a place to go and will move to the first free cell.
; -------------------------------------------------------------------
; Generic movement.
; If with all of the above, you have not made any movement
; moves to the first free cell.
; -------------------------------------------------------------------
zxMoveGeneric:
ld c, '1' ; C = ascii 1 (cell 1)
ld b, $09 ; B = total cells
ld a, $00 ; A = empty cell
ld hl, Grid ; HL = address 1st cell
zxMoveGeneric_loop:
cp (hl) ; Free cell?
jr z, zxMoveGeneric_end ; Yes, skip
inc c ; C = ascii next cell
inc hl ; HL = next cell
djnz zxMoveGeneric_loop ; Loop until B = 0
zxMoveGeneric_end:
call ToMove ; Move to free cell
ret ; Exits
We have already implemented the way the Spectrum behaves in single player games. It’s not perfect, it has some loopholes and sometimes the Spectrum makes some erratic moves. That’s fine, if we made it perfect we could never beat the Spectrum and nobody likes that.
Now we need to integrate what we have implemented inside the main loop into main.asm.
Find the loop_lostMov tag, and five lines down replace JR Z, loop_keyCont with:
jr z, loop_players ; Not active, skip
Locate the loop_keyCont tag and replace the line above it, JR loop_cont, with:
jp loop_cont ; Jump
Between this line and loop_keyCont we add the Spectrum movement for single player games, this causes JR to give an out of range error, so we have replaced it with JP.
Finally, between this line and loop_keyCont, we add the following lines:
loop_players:
ld a, (PlayerMoves) ; A = player who moves
or a ; Player 1?
jr z, loop_keyCont ; Yes, skip
ld a, (MaxPlayers) ; A = players
cp $02 ; 2 players?
jr z, loop_keyCont ; Yes, skip
call ZxMove ; Move Spectrum
jr nz, loop_players ; NZ = incorrect, loop
push bc ; Preserve BC
ld bc, SoundSpectrum ; BC = sound address
call PlayMusic ; Outputs sound
pop bc ; Retrieves BC
jr loop_print ; Paints movement
We get which player is moving, if it is two we get how many players there are and if it is one it moves the spectrum.
Compile, load into the emulator and see the result. If all went well, we can now play against the Spectrum. If you think it is too easy, you can tweak the Spectrum’s movements, which we will do in the next chapter.
ZX Spectrum Assembly, Tic-Tac-Toe
In the next chapter of ZX Spectrum Assembly, we will make the final adjustments.
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.