Espamática
ZX SpectrumRetroZ80 Assembly

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

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:

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

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.

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

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

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

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

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

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

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

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

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

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

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

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

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