ZX Spectrum Assembly, Tic-Tac-Toe – 0x01 Game board
In this chapter of ZX Spectrum Assembly, we begin the development of another game, in this case Tic-Tac-Toe.
Translation by Felipe Monge Corbalán
Table of contents
- Introduction
- Game board
- Sprites
- ROM routines and variables
- Variables
- Screen
- Main
- ZX Spectrum Assembly, Tic-Tac-Toe
- Useful links
Introduction
Tic-tac-toe was born from reading the book «ZX Spectrum Game Programming Club» by Gary Plowman. When we finish typing the Basic list in the book, he suggests adding the single player mode as an exercise.
Having concluded that Basic was not my thing, I decided to develop the game from scratch in Assembly, with the following options:
- One and two player modes.
- Possibility to specify the names of the players.
- Points to be earned to finish the game.
- Maximum time per move.
In Space Battle we saw the use of the ROM routine for keyboard handling, but it is in Tic Tac Toe where it is used most intensively.
We will also use interrupts to control the maximum time per move and give the user a countdown of the seconds left to move; he will lose his turn if he uses them up without having made the move.
At this point you should already have a certain level, so we will not explain the instructions one by one; we will explain what the routine does as a whole, which together with the comments should be sufficient for your understanding.
Game board
The first thing we are going to do is to design and paint the game board, which consists of a grid of nine cells.
We create the folder TicTacToe and the files:
- Main.asm
- Rom.asm
- Screen.asm
- Sprite.asm
- Var.asm
This time I won’t create folders for each step, but if you prefer you can continue this way.
Sprites
We define the graphics in sprite.asm: the X for player one, the O for player two, the board crosshairs and the vertical and horizontal lines. As in Space Battle, we will use the UDGs.
The appearance of the graphics in sprite.asm is as follows:
; -------------------------------------------------------------------
; File: sprite.asm
;
; Definition of graphs.
; -------------------------------------------------------------------
; Sprite player 1
Sprite_P1:
db $c0, $e0, $70, $38, $1c, $0e, $07, $03 ; $90
db $03, $07, $0e, $1c, $38, $70, $e0, $c0 ; $91
; Sprite player 2
Sprite_P2:
db $03, $0f, $1c, $30, $60, $60, $c0, $c0 ; $92 Top/Left
db $c0, $f0, $38, $0c, $06, $06, $03, $03 ; $93 Up/right
db $c0, $c0, $60, $60, $30, $1c, $0f, $03 ; $94 Down/Left
db $03, $03, $06, $06, $0c, $38, $f0, $c0 ; $95 Down/Right
; Sprite crosshead
Sprite_CROSS:
db $18, $18, $18, $ff, $ff, $18, $18, $18 ; $96
; Sprite vertical line
Sprite_SLASH:
db $18, $18, $18, $18, $18, $18, $18, $18 ; $97
; Sprite horizontal line
Sprite_MINUS:
db $00, $00, $00, $ff, $ff, $00, $00, $00 ; $98
With the practice you got in Space Battle, you should be able to draw the sprites on paper to see how they look.
The next step is to draw the board. Constants are needed to reference the ROM, define positions, data to be painted and routines to paint it.
ROM routines and variables
The ROM constants are defined in rom.asm.
; -------------------------------------------------------------------
; File: rom.asm
;
; ROM routines and system variables.
; -------------------------------------------------------------------
; Permanent attributes of screen 2, main screen.
ATTR_S: equ $5c8d
; Current display attributes 2.
ATTR_T: equ $5c8f
; Edge attribute and screen 1. Used by BEEPER.
BORDCR: equ $5c48
; Address of starting coordinates X and Y (PLOT)
COORDX: equ $5c7d
COORDY: equ $5c7e
;--------------------------------------------------------------------
; Address where the keypad status flags are located when
; the interruptions are not active.
;
; Bit 3 = 1 input in L mode, 0 input in K mode.
; Bit 5 = 1 a key has been pressed, 0 has not been pressed.
; Bit 6 = 1 numeric character, 0 alphanumeric.
;--------------------------------------------------------------------
FLAGS_KEY: equ $5c3b
;--------------------------------------------------------------------
; Address where the last key was pressed
; when interrupts are not active.
;--------------------------------------------------------------------
LAST_KEY: equ $5c08
; Direction of user-defined graphics.
UDG: equ $5c7b
; Start address of the graphics area of the VideoRAM.
VIDEORAM: equ $4000
; Length of the graphics area of the VideoRAM.
VIDEORAM_L: equ $1800
; Start address of the VideoRAM attributes area.
VIDEOATT: equ $5800
; Length of the VideoRAM attribute area.
VIDEOATT_L: equ $0300
;--------------------------------------------------------------------
; ROM beeper routine.
;
; Input: HL -> Note.
; DE -> Duration.
;
; Alters the value of the AF, BC, DE, HL and IX registers.
;--------------------------------------------------------------------
BEEPER: equ $03b5
;--------------------------------------------------------------------
; Draw a line from the COORDS coordinates.
;
; Input: B -> Vertical displacement of the line.
; C -> Horizontal displacement of the line.
; D -> Vertical orientation of the line:
; $01 = Up, $FF = Down.
; E -> Horizontal orientation of the line:
; $01 = Left, $FF = Right.
;
; Alters the value of the AF, BC and HL registers.
;--------------------------------------------------------------------
DRAW: equ $24ba
;--------------------------------------------------------------------
; ROM AT routine. Position the cursor.
;
; Input: B -> Y-coordinate.
; C -> X-coordinate.
;
; The top left corner of the screen is 24, 33.
; (0-21) (0-31)
;--------------------------------------------------------------------
LOCATE: equ $0dd9
;--------------------------------------------------------------------
; ROM routine that opens the output channel.
;
; Input: A -> Channel (1 = lower display, 2 = upper display).
;--------------------------------------------------------------------
OPENCHAN: equ $1601
As you will see, we have added variables and routines that we have not seen before. Don’t worry, we’ll see them as we go along.
Variables
In var.asm we add the necessary data to paint the board.
; -------------------------------------------------------------------
; File: var.asm
;
; Variable and constant declarations.
; -------------------------------------------------------------------
; Board ink
INKBOARD: equ $04
; Position Y = 0 for LOCATE.
INI_TOP: equ $18
; Position X = 0 for LOCATE.
INI_LEFT: equ $21
; Positions for painting OXO elements.
POS1_TOP: equ INI_TOP - $07
POS2_TOP: equ POS1_TOP - $05
POS3_TOP: equ POS2_TOP - $05
POS1_LEFT: equ INI_LEFT - $0a
POS2_LEFT: equ POS1_LEFT - $05
POS3_LEFT: equ POS2_LEFT - $05
; -------------------------------------------------------------------
; Graphics.
; -------------------------------------------------------------------
; Vertical lines of the board.
Board_1:
db $20, $20, $20, $20, $97, $20, $20, $20, $20, $97, $20, $20, $20
db $20, $ff
; Horizontal lines of the board.
Board_2:
db $98, $98, $98, $98, $96, $98, $98, $98, $98, $96, $98, $98, $98
db $98, $ff
; -------------------------------------------------------------------
; Parts of the screen.
; -------------------------------------------------------------------
; Guide numbers so that the player knows which key to press
; to place the tokens (O X O)
Board_Helper:
; Magenta ink
db $10, $03
; AT,Y,X,number
db $16, $08, $0a, "1", $16, $08, $10, "2", $16, $08, $15, "3"
db $16, $0d, $0a, "4", $16, $0d, $10, "5", $16, $0d, $15, "6"
db $16, $12, $0a, "7", $16, $12, $10, "8", $16, $12, $15, "9"
db $ff
Screen
In screen.asm we will implement the necessary routines to draw the elements on the screen.
; -------------------------------------------------------------------
; Position the cursor. The upper corner is at 24.33.
;
; Input: B -> Y.
; C -> X.
; -------------------------------------------------------------------
AT:
push af
push bc
push de
push hl ; Preserve records
call LOCATE ; Position cursor
pop hl
pop de
pop bc
pop af ; Retrieves records
ret
The AT routine receives the Y and X coordinates simultaneously in the B and C registers (they are inverted coordinates). It calls the ROM routine that positions the cursor.
We will manage the different colour attributes in a differentiated way, with one routine for each attribute.
; -------------------------------------------------------------------
; Change the border colour.
;
; Input: A -> Colour for the border.
;
; Alters the value of the AF registers.
; -------------------------------------------------------------------
BORDER:
push bc ; Preserves BC
and $07 ; A = colour
out ($fe), a ; Change border colour
rlca
rlca
rlca ; Rotate three bits to the left to set
; colour in paper/border bits
ld b, a ; B = A
ld a, (BORDCR) ; A = system variable BORDCR
and $c7 ; Remove the paper/border bits
or b ; Adds the paper/border colour
ld (BORDCR), a ; BORDCR = A, so that BEEPER does not change
pop bc ; Retrieves BC
ret
The BORDER routine receives in A the colour to be assigned to the border. To make sure it is the right colour, we keep bits 0, 1 and 2. We change the border colour and then set the value in the system variable BORDCR so that BEEPER does not change it.
; -------------------------------------------------------------------
; Assigns the colour of the ink.
;
; Input: A -> Ink colour.
; FBPPPIII
;
; Alters the value of the AF registers.
; -------------------------------------------------------------------
INK:
push bc ; Preserves BC
and $07 ; A = INK
ld b, a ; B = A
ld a, (ATTR_T) ; A = current attribute
and $f8 ; A = FLASH, BRIGHT and PAPER
or b ; Add INK
ld (ATTR_T), a ; Update current attribute
ld (ATTR_S), a ; Update permanent attribute
pop bc ; Retrieves BC
ret
The INK routine gets the ink colour in A. We make sure to keep only the ink colour, get the current attributes, leave the flicker, brightness and background and add the ink. We update the system variables queried by RST $10 and by changing the channel to apply the colour attributes.
; -------------------------------------------------------------------
; Assigns the background colour.
;
; Input: A -> Background colour.
;
; Alters the value of the AF registers.
; -------------------------------------------------------------------
PAPER:
push bc ; Preserves BC
and $07 ; A = colour
rlca
rlca
rlca ; Rotate three bits to the left to set
; colour in paper/border bits
ld b, a ; B = A
ld a, (ATTR_T) ; A = current attribute
and $c7 ; Remove background
or b ; Add background
ld (ATTR_T), a ; Update current attribute
ld (ATTR_S), a ; Update permanent attribute
pop bc ; Retrieves BC
ret
The PAPER routine gets the colour in A. We keep the colour, rotate it three bits to the left to put it in the background bits, and store it in B. We get the current attributes, remove the background, and add the one that came in A.
; -------------------------------------------------------------------
; Paints strings ending in $FF.
;
; Input: HL -> Address of the string.
;
; Alters the value of the AF and HL registers.
; -------------------------------------------------------------------
PrintString:
ld a, (hl) ; A = character to paint
cp $ff ; Is it $FF?
ret z ; Yes, exit
rst $10 ; Paints character
inc hl ; HL = next character
jr PrintString ; Loop to end of string
The PrintString routine has already been seen in Space Battle, so it needs no explanation.
We will implement routines to clean up the whole screen, a line and the attributes.
; -------------------------------------------------------------------
; Changes the attributes of the VideoRAM with the specified attribute.
;
; Input: A -> Specified attribute (FBPPPIII)
; Bits 0-2 Ink colour (0-7).
; Bits 3-5 Paper colour (0-7).
; Bit 6 Brightness (0/1).
; Bit 7 Blink (0/1).
;
; Alters the value of the BC, DE and HL registers.
; -------------------------------------------------------------------
CLA:
ld hl, VIDEOATT ; HL = start attributes
ld (hl), a ; Change attribute
ld de, VIDEOATT+1 ; DE = 2nd address attributes VideoRAM
ld bc, VIDEOATT_L-1 ; BC = length attributes - 1
ldir ; Changes the attributes
ld (ATTR_T), a ; Updates system variable
ld (ATTR_S), a ; current attribute, permanent
ld (BORDCR), a ; and border
ret
The CLA routine receives in A the attributes to be assigned to the screen, in the format already seen above. It modifies them and updates the current, permanent and border attribute system variables.
; -------------------------------------------------------------------
; Deletes the specified screen line.
;
; Input: B -> Screen line to be deleted.
; Inverted coordinates.
;
; Alters the value of the AF registers.
; -------------------------------------------------------------------
CLL:
ld a, (ATTR_T) ; A = current attributes
and $3f ; A = PAPER + INK
ld (ATTR_T), a ; Update in memory
push bc ; Preserves BC
ld c, INI_LEFT ; C = 1st column
call AT ; Position cursor
ld b, $20 ; B = 32 columns*line
CLL_loop:
ld a, $20 ; A = space
rst $10 ; Prints it out
djnz CLL_loop ; Loop while B > 0
pop bc ; Recover BC
ret
The CLL routine receives the line to be deleted in A (with inverted coordinates) and deletes it. Before it does this, it removes the brightness and blinking of the current attributes.
; -------------------------------------------------------------------
; Deletes the graphics from the VideoRAM.
;
; Alters the value of the BC, DE and HL registers.
; -------------------------------------------------------------------
CLS:
ld hl, VIDEORAM ; HL = VideoRAM address
ld de, VIDEORAM+1 ; DE = 2nd address VideoRAM
ld bc, VIDEORAM_L-1 ; BC = length VideoRAM - 1
ld (hl), $00 ; Clean 1st position
ldir ; Clean up the rest
ret
The CLS routine erases the graphic area of the screen, the pixels.
As in Space Battle, we will draw some numerical data in BDC format, but this time, because the range of numbers will be between zero and ten, we will check if the ten is zero to draw a space instead.
; -------------------------------------------------------------------
; Paints the value of BCD numbers.
;
; It only paints numbers from 0 to 99.
;
; Input: HL -> Pointer to the number to be painted.
;
; Alters the value of the AF register.
; -------------------------------------------------------------------
PrintBCD:
ld a, (hl) ; A = number to paint
and $f0 ; A = tens
rrca
rrca
rrca
rrca ; Tens to bits from 0 to 3
or a ; Tens = 0?
jr nz, PrintBCD_ascii ; No, skip
ld a, ' ' ; Tens = 0
jr PrintBCD_continue ; Paint space
PrintBCD_ascii:
add a, '0' ; A = A + Ascii code of 0
PrintBCD_continue:
rst $10 ; Paint 1st digit
ld a, (hl) ; A = number to paint
and $0f ; Keeps the units
add a, '0' ; A = A + Ascii code of 0
rst $10 ; Print 2nd digit
ret
PrintBCD gets the HL pointer to the number to paint, loads it into A, takes the tens and if they are zero, paints a space. The rest of the routine is the same as in Space Battle.
We have almost everything ready to paint the board, we just need the routine to do it.
; -------------------------------------------------------------------
; Paint the board.
;
; Alters the value of the AF, BC, D and HL registers.
; -------------------------------------------------------------------
PrintBoard:
ld a, INKBOARD ; A = ink
call INK ; Change ink
; Initial coordinates of the board
ld b, INI_TOP-$06 ; B = coord Y
ld c, INI_LEFT-$09 ; C = coord X
ld d, $0e ; D = number of rows board
printBoard_1:
call AT ; Position cursor
ld hl, Board_1 ; HL = vertical line
call PrintString ; Paints vertical line characters
dec b ; B-=1, next line
dec d ; D-=1
jr nz, printBoard_1 ; Loop while D > 0
printBoard_2:
; Coordinates of the first horizontal line
ld b, INI_TOP-$0a ; B = coord Y
ld c, INI_LEFT-$09 ; C = coord X
call AT ; Position cursor
ld hl, Board_2 ; HL = horizontal line
call PrintString ; Paint horizontal line
; Coordinates of the second horizontal line,
; X-coordinate does not change.
ld b, INI_TOP-$0f ; B = coord Y
call AT ; Position cursor
ld hl, Board_2
call PrintString ; Paint horizontal line
printBoard_3:
; Paints the guide numbers so that the user knows which key to press
; To place the tokens (O X O)
ld hl, Board_Helper ; HL = helper
jp PrintString ; Paints helper and exits
PrintBoard paints the board, changes the ink and prepares everything for painting the vertical line, which is painted on printBoard_1. The horizontal line is painted on printBoard_2 and the guide for the player to know which keys to press is painted on printBoard_3.
When the vertical and horizontal lines are drawn, the vertical line is erased where it crosses. Look closely at the definition of board2 and you will see that the same character is not painted over the whole board.
Main
To see if everything works, we implement the first version of the main.asm file.
org $5e88
Main:
ld hl, Sprite_P1
ld (UDG), hl
ld a, $02
call OPENCHAN
xor a
call BORDER
call CLA
call CLS
Init:
call PrintBoard
Loop:
jr Loop
include "rom.asm".
include "screen.asm"
include "sprite.asm"
include "var.asm"
end Main
We set the start address, bearing in mind that we will be making a custom loader. We show where the UDGs are, open channel two, change the border, attributes, clear the screen and paint the board. We stay in an infinite loop, which will be the main loop.
We compile, load into the emulator and see the results.
ZX Spectrum Assembly, Tic-Tac-Toe
We already have the board on which the game will be played.
In the next 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.
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.