Espamática
ZX SpectrumRetroZ80 Assembly

ZX Spectrum Assembly, Tic-Tac-Toe – 0x06 Final adjustment

In this chapter of ZX Spectrum Assembly, we will make the final adjustments.

We have reached the last chapter. We are going to make a couple of tweaks that, while not entirely necessary, I think might be of interest.

We will add an option in the menu to allow players to select the maximum number of tables they can have.

As for the tables, currently it is necessary to make all possible moves (occupy all the cells) to complete the point, although sometimes we already know that it is not possible to win and it is a table. We are going to implement the detection of tables to detect when it is no longer possible to win and end the point.

We will propose some modifications that can save some bytes and clock cycles.

We will modify the Spectrum’s moves to make it harder to beat.

Finally, we’ll add the loading screen.

Translation by Felipe Monge Corbalán

Table of contents

Tables menu

Currently, the maximum number of tables is declared in the MAXTIES constant in var.asm; we delete it.

Locate MaxPoints and add the new variable for the tables below it:

ASM
MaxTies:     db $05        ; Maximum tables

Locate TitleEspamatica and add the Tables menu option just above it:

ASM
TitleOptionTies:   db $10, INK7, $16, $10, $08, "4. ", $10, INK6
                   defm "Tables"

To make it more symmetrical, we locate Title3InStripe and replace $16, $02, $09 at the end of the line with $16, $04, $09.

In Main.asm, let’s add the handling of the new menu item. Locate menu_Time and replace JR NZ, menu_op two lines down:

ASM
jr   nz, menu_Ties         ; No, skip

Locate menu_TimeDo and three lines down, after JR menu_op, add the handling of the new menu item:

ASM
menu_Ties:  
cp   KEY4                  ; Pushed 4?
jr   nz, menu_op           ; No, loop
ld   a, (MaxTies)          ; A = tables
add  a, $02                ; A += 2
cp   $0a                   ; A < 10?
jr   c, menu_TiesDo        ; Yes, skip
ld   a, $03                ; A = 3
menu_TiesDo:
ld   (MaxTies), a          ; Update tables
jr   menu_op               ; Loop

If the four key has been pressed, the maximum number of tables is increased by two, if it is greater than nine, it is reduced to three and loaded into memory.

Both this range of values and the difference between one value and the other can be modified as required. You can also change it in points and time.

Once we have the maximum number of tables in memory, we need to use them. Find the loop_reset tag, the LD B, MAXTIES line and replace it with the following:

ASM
ld   a, (MaxTies)          ; A = maximum tables
ld   b, a                  ; B = A

All that remains is to print the selected value. In screen.asm, locate PrintOptions and add the following lines just above JP PrintBCD:

ASM
call PrintBCD              ; Paints it
ld   b, INI_TOP-$10        ; B = coord Y
call AT                    ; Position cursor
ld   hl, MaxTies           ; HL = time value

We compile, load the emulator and check that we can now define the number of tables and that the game ends when the number of tables is reached.

Table detection

Currently, in order for boards to be detected, all cells must be occupied and there must be no tic-tac-toe. In reality, it is possible to know if the point will end in a board before the board is full.

We go to game.asm and implement table detection.

To know if there is a possibility of a move, we just need to know if there is any combination left in which the cells are occupied by only one player or by no player at all.

ASM
; -------------------------------------------------------------------
; Check for tables.
; In order to check if there are tables.
;
; Output: Z  -> No tables.
;         NZ -> There are tables.
;
; Alters the value of the AF, BC, DE, HL and IX registers.
; -------------------------------------------------------------------
CheckTies:
ld   b, $f0                ; B = single-player cell mask
ld   c, $0f                ; B = cell mask another player

We load in B the mask to keep the cells occupied by one player, and in C the mask for the other player.

ASM
checkTies_check123:
ld   hl, Grid              ; HL = grid address
ld   a, (hl)               ; A = cell value 1
inc  hl                    ; HL = cell address 2
add  a, (hl)               ; A+= cell value 2
inc  hl                    ; HL = cell address 3
add  a, (hl)               ; A+= cell value 2
ld   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret  z                     ; None, exits

We place HL on the grid, add the value of the three cells in A and increment HL to move from one cell to another. We check if there are any cells occupied by a player, and if not, we leave. If so, we check if there are any cells occupied by the other player, and if not, we leave.

The next two checks have the same structure.

ASM
checkTies_check456:
inc  hl                    ; A = cell address 4
ld   a, (hl)               ; A = cell value 4
inc  hl                    ; HL = cell address 5
add  a, (hl)               ; A+= cell value 5
inc  hl                    ; HL = cell address 6
add  a, (hl)               ; A+= cell value 6
ld   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret  z                     ; None, exits

checkTies_check789:
inc  hl                    ; A = cell address 7
ld   a, (hl)               ; A = cell value 7
inc  hl                    ; HL = cell address 8
add  a, (hl)               ; A+= cell value 8
inc  hl                    ; HL = cell address 9
add  a, (hl)               ; A+= cell value 9
ld   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret  z                     ; None, exits

Look closely at how we go through the cells with HL, this helps us to save a few bytes in an implementation we did earlier.

The following checks cannot be done by changing cells with HL, as they are not numerically contiguous; we use IX.

ASM
checkTies_check147:
ld   ix, Grid-$01          ; IX = address Grid - 1
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   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret  z                     ; None, exits

The rest of the checks follow the same structure.

ASM
checkTies_check258:
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   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret  z                     ; None, exits

checkTies_check369:
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   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret  z                     ; None, exits

checkTies_check159:
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   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret  z                     ; None, exits

checkTies_check357:
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   d, a                  ; Preserve A in D
and  b                     ; A = cells occupied by a player
ret  z                     ; None, exits
ld   a, d                  ; Retrieves A from D
and  c                     ; A = cells occupied by another player
ret                        ; Exits with Z in correct state

With this we now have the prediction of whether there will be tables, all that remains is to go to main.asm and use it.

Find loop_tie and three lines down substitute:

ASM
ld   a, $09                ; A = 9
cp   (hl)                  ; Counter = 9?
jr   nz, loop_cont         ; No, skip

By:

ASM
call CheckTies             ; Any possible movement?
jr   z, loop_cont          ; No, skip

We no longer have to wait until the board is full to know if there are checkers or not. We call CheckTies and if there are no boards we continue with the point.

We compile, load the emulator and see the results.

You may see some behaviour that makes you think something is wrong. You see rows one, two, three free, but you know that the Spectrum will occupy one cell, then you another, and so it should mark tables.

The system is predicting, not guessing. Imagine that there are two free cells in a row and it is the turn of the player whose cell is not occupied to move. Well, he will occupy one of the cells, so we already know it is a draw, but imagine that the player rests on his laurels and loses his turn, the other player occupies another cell and there is only one free cell, and it is still not possible to predict draws, if the other player falls asleep again, he loses his turn again and the first player gets three in a row.

We save bytes and clock cycles

We will implement several modifications to save bytes and clock cycles.

The first of these will be in game.asm, on the set of CheckWinner and ZxMove routines, applying the way we have implemented CheckTies to the horizontal lines, the numerically contiguous ones.

Go to the CheckWinner_check tag and find the following lines:

ASM
ld   a, (ix+1)             ; A = cell 1
add  a, (ix+2)             ; A+= cell 2
add  a, (ix+3)             ; A+= cell 3

And we replace them with:

ASM
ld   hl, Grid              ; HL = cell address 1
ld   a, (hl)               ; A = cell value 1
inc  hl                    ; HL = cell address 2
add  a, (hl)               ; A+= cell value 2
inc  hl                    ; HL = cell address 3
add  a, (hl)               ; A+= cell value 3

We locate the lines:

ASM
ld   a, (ix+4)             ; A = cell 4
add  a, (ix+5)             ; A+= cell 5
add  a, (ix+6)             ; A+= cell 6

And we replace them with:

ASM
inc  hl                    ; HL = cell address 4
ld   a, (hl)               ; A = cell value 4
inc  hl                    ; HL = cell address 5
add  a, (hl)               ; A+= cell value 5
inc  hl                    ; HL = cell address 6
add  a, (hl)               ; A+= cell value 6

We locate the lines:

ASM
ld   a, (ix+7)             ; A = cell 7
add  a, (ix+8)             ; A+= cell 8
add  a, (ix+9)             ; A+= cell 9

And we replace them with:

ASM
inc  hl                    ; HL = cell address 7
ld   a, (hl)               ; A = cell value 7
inc  hl                    ; HL = cell address 8
add  a, (hl)               ; A+= cell value 8
inc  hl                    ; HL = cell address 9
add  a, (hl)               ; A+= cell value 9

This part is ready, add the comments of the routine to HL as the affected register.

This is the comparison between the two versions:

VersionCyclesBytes
1688/641118
2638/591111
ZX Spectrum Assembly, Tic-Tac-Toe

As you can see, we save fifty clock cycles and seven bytes. We compile, load into the emulator and see that it still works.

We continue to modify ZxMove, specifically two parts of this routine set. We locate zxMoveToWin_123 and replace it:

ASM
ld   a, (ix+$01)           ; A = cell value 1
add  a, (ix+$02)           ; A+= cell value 2
add  a, (ix+$03)           ; A+= cell value 3

By:

ASM
ld   hl, Grid              ; HL = cell address 1
ld   a, (hl)               ; A = cell value 1
inc  hl                    ; HL = cell address 2
add  a, (hl)               ; A+= cell value 2
inc  hl                    ; HL = cell address 3
add  a, (hl)               ; A+= cell value 3

Locate zxMoveToWin_456 and replace it:

ASM
ld   a, (ix+$04)           ; A = cell value 4
add  a, (ix+$05)           ; A+= cell value 5
add  a, (ix+$06)           ; A+= cell value 6

By:

ASM
ld   hl, Grid+$03          ; HL = cell address 4
ld   a, (hl)               ; A = cell value 4
inc  hl                    ; HL = cell address 5
add  a, (hl)               ; A+= cell value 5
inc  hl                    ; HL = cell address 6
add  a, (hl)               ; A+= cell value 6

Locate zxMoveToWin_789 and replace it:

ASM
ld   a, (ix+$07)           ; A = cell value 7
add  a, (ix+$08)           ; A+= cell value 8
add  a, (ix+$09)           ; A+= cell value 9

By:

ASM
ld   hl, Grid+$06          ; HL = cell address 7
ld   a, (hl)               ; A = cell value 7
inc  hl                    ; HL = cell address 8
add  a, (hl)               ; A+= cell value 8
inc  hl                    ; HL = cell address 9
add  a, (hl)               ; A+= cell value 9

We have now completed the first part. The main difference with CheckTies and CheckWinner is that although they are numerically contiguous cells, the move from cell three to cell four and from cell six to cell seven is not with INC HL, as ToMove changes the value of HL.

Locate zxMoveAttack_123 and replace it:

ASM
ld   a, (ix+$01)           ; A = cell value 1
add  a, (ix+$02)           ; A+= cell value 2
add  a, (ix+$03)           ; A+= cell value 3

By:

ASM
ld   hl, Grid              ; HL = cell address 1
ld   a, (hl)               ; A = cell value 1
inc  hl                    ; HL = cell address 2
add  a, (hl)               ; A+= cell value 2
inc  hl                    ; HL = cell address 3
add  a, (hl)               ; A+= cell value 3

Locate zxMoveAttack_456 and replace it:

ASM
ld   a, (ix+$04)           ; A = cell value 4
add  a, (ix+$05)           ; A+= cell value 5
add  a, (ix+$06)           ; A+= cell value 6

By:

ASM
ld   hl, Grid+$03          ; HL = cell address 4
ld   a, (hl)               ; A = cell value 4
inc  hl                    ; HL = cell address 5
add  a, (hl)               ; A+= cell value 5
inc  hl                    ; HL = cell address 6
add  a, (hl)               ; A+= cell value 6

Locate zxMoveAttack_789 and replace it:

ASM
ld   a, (ix+$07)           ; A = cell value 7
add  a, (ix+$08)           ; A+= cell value 8
add  a, (ix+$09)           ; A+= cell value 9

By:

ASM
ld   hl, Grid+$06          ; HL = cell address 7
ld   a, (hl)               ; A = cell value 7
inc  hl                    ; HL = cell address 8
add  a, (hl)               ; A+= cell value 8
inc  hl                    ; HL = cell address 9
add  a, (hl)               ; A+= cell value 9

We make another modification in zxMoveDefence_cornerBlock34 and zxMoveDefence_cornerBlock67 that saves clock cycles but no bytes.

Replacement in zxMoveDefence_cornerBlock34:

ASM
ld   a, (ix+$03)           ; A = cell value 3
add  a, (ix+$04)           ; A+= cell value 4

By:

ASM
ld   hl, Grid+$02          ; A = cell address 3
ld   a, (hl)               ; A = cell value 3
inc  hl                    ; HL = cell address 4
add  a, (hl)               ; A+= cell value 4

In zxMoveDefence_cornerBlock67 we replace:

ASM
ld   a, (ix+$06)           ; A = cell value 6
add  a, (ix+$07)           ; A+= cell value 7

By:

ASM
ld   hl, Grid+$05          ; A = cell address 6
ld   a, (hl)               ; A = cell value 6
inc  hl                    ; HL = cell address 7
add  a, (hl)               ; A+= cell value 7

The changes in game.asm are done. As I said, add HL as affected registry in the comments of the routine, we forgot it before because the HL registry was already affected by the zxMoveGeneric routine.

This is the comparison between the two versions of ZxMove:

VersionCyclesBytes
14632/4079808
24532/3979802
ZX Spectrum Assembly, Tic-Tac-Toe

Version two occupies six bytes less and takes a hundred cycles less than version one, which seems small when we compare it with the bytes saved by CheckWinner with fewer modifications. Remember that there the transition from cell three to cell four and from cell six to cell seven is done with INC HL, which we cannot do here.

The difference between INC HL and LD HL, Grid is four clock cycles and two bytes, and that limits our savings.

Same tile for both players

If we have capacity problems, which we don’t, we can save another handful of bytes by painting the same tile in a different colour for both players.

We go to sprite.asm, comment out the Sprite_P2 definition and put the Sprite_CROSS, Sprite_SLASH and Sprite_MINUS definitions at the top of the file, like this:

ASM
; -------------------------------------------------------------------
; File: sprite.asm
;
; Definition of graphs.
; -------------------------------------------------------------------
; Crosshead Sprite
Sprite_CROSS:
db $18, $18, $18, $ff, $ff, $18, $18, $18 ; $90

; Sprite of the vertical line
Sprite_SLASH:
db $18, $18, $18, $18, $18, $18, $18, $18 ; $91

; Sprite of the horizontal line
Sprite_MINUS:
db $00, $00, $00, $ff, $ff, $00, $00, $00 ; $92

; Player 1 Sprite
Sprite_P1:
db $c0, $e0, $70, $38, $1c, $0e, $07, $03 ; $93
db $03, $07, $0e, $1c, $38, $70, $e0, $c0 ; $94

; ; Player Sprite 2
; Sprite_P2:
; db $03, $0f, $1c, $30, $60, $60, $c0, $c0 ; $95 Up/Left
; db $c0, $f0, $38, $0c, $06, $06, $03, $03 ; $96 Top/Right
; db $c0, $c0, $60, $60, $30, $1c, $0f, $03 ; $97 Down/Left
; db $03, $03, $06, $06, $0c, $38, $f0, $c0 ; $98 Down/Right

Commenting on player two’s sprite not only forces us to change the routine that paints the tiles, it also forces us to change the routine that paints the board as the UDGs change. We have uploaded the board sprites above so that we can only change the board painting routine once, and so that if we decide to repaint the two tiles later, it won’t be affected.

By commenting out Sprite_P2, we save thirty-two bytes. It doesn’t sound like much, but in certain situations it can save us.

We go to var.asm and change the definition of Board_1 and _2 and leave them as they are:

ASM
; Vertical lines of the board.
Board_1:
db $12, $00, $13, $00        
db $20, $20, $20, $20, $91, $20, $20, $20, $20, $91, $20, $20, $20
db $20, $ff

; Horizontal lines of the board.
Board_2:        
db $92, $92, $92, $92, $90, $92, $92, $92, $92, $90, $92, $92, $92
db $92, $ff

We compile, load into the emulator and see the result, which is the mess we made.

That player two’s tile is painted wrong is to be expected, but what about the board? We have changed the position of the sprite definition, but UDG is still pointing to Sprite_P1.

We go to main.asm and under the Main tag we replace:

ASM
ld   hl, Sprite_P1         ; HL = address Sprite_P1

By:

ASM
ld   hl, Sprite_CROSS      ; HL = address Sprite_CROSS

Also change the comment in the line below.

We compile, load it into the emulator and see that the board looks good, but the parts don’t. Don’t worry, this is to be expected.

We are going to modify the routine that paints the tiles so that it does it well, or not, you can leave it as it is; three in a bloody line.

We locate printOXO_X in screen.asm and the lines where the UDG is loaded in A. We add three to the value it loads:

ld a, $90 becomes ld a, $93
ld a, $91 becomes ld a, $94

We locate printOXO_Y and the lines on which the UDG is loaded in A. We add three to the value it loads:

ld a, $92 becomes ld a, $95
ld a, $93 becomes ld a, $96
ld a, $94 becomes ld a, $97
ld a, $95 becomes ld a, $98

So if we decided to paint two different tiles, the circle would be painted.

As we are now in the situation of painting a single tile, we comment on the lines:

ASM
ld   a, $95                ; A = 1st sprite
ld   a, $98                ; A = 4th sprite

And below that we add the line:

ASM
ld   a, $93                ; A = 1st sprite

We comment on the lines:

ASM
ld   a, $96                ; A = 2nd sprite
ld   a, $97                ; A = 3rd sprite

            And below that we add the line:

ASM
ld   a, $94                ; A = 2nd sprite

A bit of a mess, isn’t it? Here’s what it looks like:

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

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

So, if we want to redraw the two pieces, in sprite.asm we uncomment Sprite_P2, and in printOXO_Y we alternate the comments on the LD A, … lines.

This will paint the pieces on the board, but not in the information part of the game.

We go back to var.asm and modify player1_figure to look like this:

ASM
player1_figure:   db $16, $04, $00, $90, $91, $0d, $91, $90

And we leave it at that:

ASM
player1_figure:   db $16, $04, $00, $93, $94, $0d, $94, $93

We modify player2_figure so that it now looks like this:

ASM
player2_figure:   db $16, $04, $1b, $92, $93, $16, $05, $1b
                  db $94, $95, $ff

And we leave it at that:

ASM
player2_figure:   db $16, $04, $1b, $93, $94, $16, $05, $1b
                  db $94, $93, $ff

Finally, we add the annotated definition for painting different tiles.

ASM
; player2_figure:   db $16, $04, $1b, $95, $96, $16, $05, $1b
;                   db $97, $98, $ff

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

You can paint the same tile in a different colour for both players, or you can paint a different colour for each player. If we were using a black and white TV, there would be no doubt.

If you keep all the changes, we have saved forty-five bytes and one hundred and fifty clock cycles. If you decide to paint different tiles for each player, the byte saving is thirteen bytes.

Spectrum movement

If you have played several games against the Spectrum, you will have worked out how to beat it every time you start the game, because there is at least one move that the Spectrum does not know how to defend, we have not programmed it.

The specific move is to occupy two corner cells: two and six or six and eight. If we occupy cell two, the Spectrum occupies cell five, we move to cell six and the Spectrum moves to cell seven, we move to cell three and we have two tic-tac-toe moves: cells one, two and three and cells three, six and nine.

In game.asm we locate zxMoveAttack_123 and above it we implement the lines that make it impossible to beat the Spectrum with this move.

ASM
; -------------------------------------------------------------------
; Defensive corner movement.
; -------------------------------------------------------------------
zxMoveDefence_corner24:
ld   a, (ix+$02)           ; A = cell value 2
add  a, (ix+$04)           ; A+= cell value 4
cp   b                     ; A = B?
jr   nz, zxMoveDefence_corner26 ; No, skip
ld   c, KEY1               ; C = key 1
call ToMove                ; Move to cell 1
ret  z                     ; If correct, exits

We check if player one has occupied squares two and four, in which case we move to square one if possible.

The rest of the checks follow the same structure.

ASM
zxMoveDefence_corner26:
ld   a, (ix+$02)           ; A = cell value 2
add  a, (ix+$06)           ; A+= cell value 6
cp   b                     ; A = B?
jr   nz, zxMoveDefence_corner84 ; No, skip
ld   c, KEY3               ; C = key 3
call ToMove                ; Move to cell 3
ret  z                     ; If correct, exits
zxMoveDefence_corner84:
ld   a, (ix+$08)           ; A = cell value 8
add  a, (ix+$04)           ; A+= cell value 4
cp   b                     ; A = B?
jr   nz, zxMoveDefence_corner86 ; No, skip
ld   c, KEY7               ; C = key 7
call ToMove                ; Move to cell 3
ret  z                     ; If correct, exits
zxMoveDefence_corner86:
ld   a, (ix+$08)           ; A = cell value 8
add  a, (ix+$06)           ; A+= cell value 6
cp   b                     ; A = B?
jr   nz, zxMoveAttack_123  ; No, skip
ld   c, KEY9               ; C = key 9
call ToMove                ; Move to cell nine
ret  z                     ; If correct, exits

Although the play is in two corners, we cover all four.

Just above zxMoveDefence_cornerBlock2938Cont is the JR NZ line, zxMoveAttack_123, we modify it and leave it as it is:

ASM
jr   nz, zxMoveDefence_corner24 ; No, no mov cross, skip

If you compile and play some games against Spectrum, you will see that it is no longer possible to beat him with this move, but it is still possible to beat him, there are still moves that he does not know how to defend.

Difficulty

Now it is more difficult to beat the Spectrum, we have to wait for him to start the game and depending on the move he makes, we can beat him.

We will add another option to the menu to be able to choose the level of difficulty: one to not cover the play in the corner, two to cover the play in the corner.

In var.asm, after MaxTies, we add the variable for the level of difficulty:

ASM
Level:       db $02        ; Difficulty level

When a new menu item is added, it is appended to the Spamatica line. We move all the menu items up one line, like this:

ASM
TitleOptionStart:  db $10, INK1, $13, $01, $16, $07, $08, "0. "
                   db $10, INK5
                   defm "Start"
TitleOptionPlayer: db $10, INK7, $13, $01, $16, $09, $08, "1. "
                   db $10, INK6
                   defm "Players"
TitleOptionPoint:  db $10, INK7, $16, $0b, $08, "2. ", $10, INK6
                   defm "Points"
TitleOptionTime:   db $10, INK7, $16, $0d, $08, "3. ", $10, INK6
                   defm "Time"
TitleOptionTies:   db $10, INK7, $16, $0f, $08, "4. ", $10, INK6
                   defm "Tables"

After the definition of TitleOptionTies, we add the definition of Difficulty:

ASM
TitleOptionLevel:  db $10, INK7, $16, $11, $08, "5. ", $10, INK6
                   defm "Difficulty"

In screen.asm we find PrintOptions and subtract one from all Y coordinate assignments:

LD B, INI_TOP-$0A go to LD B,INI_TOP-$09
LD B, INI_TOP-$0C go to LD B,INI_TOP-$0B
LD B, INI_TOP-$0E go to LD B,INI_TOP-$0D
LD B, INI_TOP-$10 go to LD B,INI_TOP-$0F

Above JP PrintBCD we add the following lines:

ASM
call PrintBCD              ; Paints it
ld   b, INI_TOP-$11        ; B = coord Y
call AT                    ; Position cursor
ld   hl, Level             ; HL = difficulty value

The final aspect of the routine is as follows:

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-$09        ; B = coord Y
ld   c, INI_LEFT-$15       ; C = coord X
call AT                    ; Position cursor
ld   hl, MaxPlayers        ; HL = value players
call PrintBCD              ; Paints it
ld   b, INI_TOP-$0b        ; B = coord Y
call AT                    ; Position cursor
ld   hl, MaxPoints         ; HL = points value
call PrintBCD              ; Paints it
ld   b, INI_TOP-$0d        ; B = coord Y
call AT                    ; Position cursor
ld   hl, seconds           ; HL = time value
call PrintBCD              ; Paints it
ld   b, INI_TOP-$0f        ; B = coord Y
call AT                    ; Position cursor
ld   hl, MaxTies           ; HL = time value
call PrintBCD              ; Paints it
ld   b, INI_TOP-$11        ; B = coord Y
call AT                    ; Position cursor
ld   hl, Level             ; HL = difficulty value
jp   PrintBCD              ; Paints it in and exits

In main.asm find menu_Ties and then the line CP KEY4. After this line, replace JR NZ, menu_op with:

ASM
jr   nz, menu_Level        ; No, skip

After menu_TiesDo, after the JR line menu_op, we add the processing lines of the new menu item:

ASM
menu_Level:
cp   KEY5                  ; Pushed 5?
jr   nz, menu_op           ; No, loop
ld   a, (Level)            ; A = difficulty
xor  $03                   ; Alternates between 1 and 2
ld   (Level), a            ; Refresh in memory
jr   menu_op               ; Loop

Finally, in game.asm we locate zxMoveDefence_corner24, and just below it we add:

ASM
ld   a, (Level)            ; A = difficulty
cp   $01                   ; Difficulty = 1?
jr   z, zxMoveAttack_123   ; Yes, jumps

These lines do not check for corner play when the difficulty level is one. Compile and test.

Loading screen

Finally, we’re going to add the loading screen, but what’s the difference when we’ve already seen how it’s done in ZX-Pong and Space Battle? Well, this time we’re going to do it differently, because we’re not going to load the screen into VideoRAM, we’re going to load it into a different memory address and then make it appear all at once.

This is the loading screen you can download from here.

Please be kind, art has never been my thing. I will be happy if you design your own loading screen, which I am sure will be better than this one; it shouldn’t be difficult.

In order to load the loading screen into a memory area and then dump it all at once into VideoRAM, we need to implement the routine to do this in a separate file and compile it separately.

The loading process will do the following:

  • Load the loader.
  • Load the routine that dumps the loading screen into memory address 24200.
  • Load the loading screen at address 24250.
  • Runs the routine that dumps the loading screen into VideoRAM.
  • Load the tic-tac-toe routine at address 24200.
  • Load the interrupt routine at address 32348.
  • Run the tic-tac-toe program.

The first thing we are going to look at is the loader we developed in Basic to do all of the above.

VB
 10 CLEAR 24200
 20 INK 0: PAPER 4: BORDER 4: CLS
 30 POKE 23610,255: POKE 23739,111
 40 LOAD ""CODE 24200
 50 LOAD ""CODE 24250
 60 RANDOMIZE USR 24200
 70 LOAD ""CODE 24200
 80 LOAD ""CODE 32348
 90 CLS
100 RANDOMIZE USR 24200

Don’t forget to save it with SAVE «OXO» LINE 10.

The next step is to implement the routine that dumps the loading screen into the VideoRAM. We create the file loadScr.asm and add the following lines:

ASM
; -------------------------------------------------------------------
; LoadScr.asm
;
; The loading screen will be loaded in $5eba, and this routine will 
; be passed by the VideoRAM to appear all at once and will then 
; clear the area.
; Where it was initially loaded.
; The cleaning of this memory area is not necessary, but we do it.
; in case we need to debug, not to find code that actually are 
; remnants of the loading screen.
; -------------------------------------------------------------------
org  $5e88                 ; Loading address

ld   hl, $5eba             ; HL = address where the screen is located
ld   de, $4000             ; DE = VideoRAM address
ld   bc, $1b00             ; Screen length
ldir                       ; Flip the screen

ld   hl, $5eba             ; HL = address where the screen is located
ld   de, $5ebb             ; Next address
ld   bc, $1aff             ; Screen length - 1
ld   (hl), $00             ; Clear the first position
ldir                       ; Clean up the rest

ret

The loading screen is loaded at $5eba. This routine copies $1B00 positions (6912 bytes), the entire area of pixels and attributes, from this address into VideoRAM.

Once copied, we clean up the memory area where the screen was loaded; it is not necessary, but if we have to debug, we will avoid residual code that could confuse us.

Now we just need to modify the script we need to compile and generate the final .tap. The Windows version 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 Compiling loadscr
pasmo --name LoadScr --tap loadScr.asm loadscr.tap loadscr.log
echo Generating Tic-Tac-Toe
copy /b /y loader.tap+loadscr.tap+TresEnRayaScr.tap+oxo.tap+int.tap TicTacToe.tap

The Linux version would look 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 Compiling loadscr
pasmo --name LoadScr --tap loadScr.asm loadscr.tap loadscr.log
echo Generating Tic-Tac-Toe
cat loader.tap loadscr.tap TresEnRayaScr.Tap oxo.tap int.tap > TicTacToe.tap

And that is it.

ZX Spectrum Assembly, Tic-Tac-Toe

Thank you very much for joining me on this journey, I hope this is just the beginning, that it has helped you to learn and become passionate about assembly programming for the ZX Spectrum. I will continue to study and learn, and if I can gather more material in the future, I may write another tutorial.

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