ZX Spectrum Assembly, Tic-Tac-Toe – 0x04 Options and end of game
In this chapter of ZX Spectrum Assembly, we will implement the different options of the game and the end of the game; as we left off in the previous chapter, the game continues indefinitely. The options will be selected from a start menu.
Colours
In var.asm we will add the following constants for the colours to avoid having to use numbers and to make it easier to change the colours during the game.
INK0: equ $00 INK1: equ $01 INK2: equ $02 INK3: equ $03 INK4: equ $04 INK5: equ $05 INK6: equ $06 INK7: equ $07
As an exercise, go through the title and text declarations and where you see $10, change the number that follows it to the corresponding INK tag.
In the same way, look for INK calls in the rest of the files, and instead of loading the ink number in A, load the corresponding INK tag.
Menu
Some of the start menu texts are already defined in ar.asm: the TitleEspamatica tag and from TitleOptionStart to TitleOptionTime.
We will delete all these lines as we are going to reimplement it, adding colours and positions, and it will look like this:
Title3InStripe: db $10, INK2, $13, $01, $16, $02, $09 defm "Three in a row" TitleOptionStart: db $10, INK1, $13, $01, $16, $08, $08, "0. " db $10, INK5 defm "Start" TitleOptionPlayer: db $10, INK7, $13, $01, $16, $0a, $08, "1. " db $10, INK6 defm "Players" TitleOptionPoint: db $10, INK7, $16, $0c, $08, "2. ", $10, INK6 defm "Points" TitleOptionTime: db $10, INK7, $16, $0e, $08, "3. ", $10, INK6 defm "Time" TitleEspamatica: db $16, $14, $08 db $10, INK2, "E", $10, INK6, "s" db $10, INK4, "p", $10, INK5, "a" db $10, INK2, "m", $10, INK6, "a" db $10, INK4, "t", $10, INK5, "i" db $10, INK2, "c", $10, INK6, "a" db $10, INK7, " 2019", $ff
Now we have the definition of the start menu almost ready to paint, we just need a routine to paint the values of the options. We implement this in screen.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-$0a ; B = coord Y ld c, INI_LEFT-$15 ; C = coord X call AT ; Positions cursor ld hl, MaxPlayers ; HL = value players call PrintBCD ; Paints it ld b, INI_TOP-$0c ; B = coord Y call AT ; Positions cursor ld hl, MaxPoints ; HL = points value call PrintBCD ; Paints it ld b, INI_TOP-$0e ; B = coord Y call AT ; Positions cursor ld hl, MaxSeconds ; HL = time value jp PrintBCD ; Paints it and exits
PrintOptions paints the values of the options in green. It positions the cursor and paints each of the values.
Let’s see what the start menu looks like. We locate Main in main.asm, and under CALL OPENCHAN we add these lines:
Menu: di ; Deactivate interruptions im $01 ; Interruptions = mode 1 ei ; Activates interruptions
We have added a label for the menu part, disabled the interrupts to switch to mode one and enabled them again. We will then use the interrupts in mode two, so these lines are needed here.
At the bottom we find CALL CLS and then add the following lines:
ld hl, Title3InStripe ; HL = address three-inline call PrintString ; Paints the menu menu_op: call PrintOptions ; Paint the options jr menu_op ; Menu loop
We draw the start screen and the values of the options. We remain in an infinite loop as this is where we will implement the menu logic.
With the menu definition and a few lines we have painted it, but we still need to make it work.
Compile it, load it into the emulator and see how it looks.

Before implementing the menu logic, we will implement a routine in game.asm that uses the ROM to read the keyboard and return the ASCII code of the last key pressed. This routine will be used for the menu options and for the player name query.
; ------------------------------------------------------------------- ; Wait for a key to be pressed and return its Ascii code. ; ; Output: A -> Ascii code of the key. ; ; Alters the value of the AF and HL registers. ; ------------------------------------------------------------------- WaitKeyAlpha: ld hl, FLAGS_KEY ; HL = address flag keyboard set $03, (hl) ; Input mode L ; Loop until a key is obtained. WaitKeyAlpha_loop: bit $05, (hl) ; Key pressed? jr z, WaitKeyAlpha_loop ; Not pressed, loop res $05, (hl) ; Bit set to 0 for future inspections ; Gets the Ascii of the key pressed. ; Valid Ascii 12 ($0C), 13 ($0D) and from 32 ($20) to 127 ($7F) ; If the key pressed is Space, load ' ' in A WaitKeyAlpha_loadKey: ld hl, LAST_KEY ; HL = last key pressed address ld a, (hl) ; A = last key pressed cp $80 ; Ascii > 127? jr nc, WaitKeyAlpha ; Yes, invalid key, loop cp KEYDEL ; Pushed Delete? ret z ; Yes, exit cp KEYENT ; Press Enter? ret z ; Yes, exit cp KEYSPC ; Powered Space? jr c, WaitKeyAlpha ; Ascii < space, invalid, loop ret ; Exits
WaitKeyAlpha waits until a key is pressed that is valid for us, that is: delete, enter or an ASCII code between thirty-two and one hundred and twenty-seven. When a valid key is pressed, it returns the ASCII code for that key in A.
We implement the menu routine in main.asm, just after CALL PrintOptions, and the final aspect is this:
menu_op: call PrintOptions ; Paint the options call WaitKeyAlpha ; Wait for keypress cp KEY0 ; Pushed 0? jr z, Init ; Yes, start game menu_Players: cp KEY1 ; Pushed 1? jr nz, menu_Points ; No, skip ld a, (MaxPlayers) ; A = players xor $03 ; Alternates between 1 and 2 ld (MaxPlayers), a ; Update in memory jr menu_op ; Loop menu_Points: cp KEY2 ; Pushed 2? jr nz, menu_Time ; No, skip ld a, (MaxPoints) ; A = points inc a ; A += 1 cp $06 ; A = 6? jr nz, menu_PointsDo ; No, skip ld a, $01 ; A = 1 menu_PointsDo: ld (MaxPoints), a ; Update in memory add a, '0' ; A = value ascii ld (info_points), a ; Update in memory cp '1' ; Points 1? jr z, menu_Points1 ; Yes, skip ld a, 's' ; Plural jr menu_PointsEnd ; Jump menu_Points1: ld a, ' ' ; Singular menu_PointsEnd: ld (info_gt1), a ; Update in memory jr menu_op ; Loop menu_Time: cp KEY3 ; Pushed 3? jr nz, menu_op ; No, loop ld a, (MaxSeconds) ; A = seconds add a, $05 ; A += 5 daa ; Decimal setting cp $35 ; A = 35 BCD? jr nz, menu_TimeDo ; No, skip ld a, $05 ; A = 5 menu_TimeDo: ld (MaxSeconds), a ; Refreshes in memory jr menu_op ; Loop
After calling the routine that checks if a valid key has been pressed, it evaluates if it is any key between zero and three, and depending on which it is, it acts in one way or another. Note that it only does the decimal adjustment when incrementing seconds; it is the only option whose value is greater than nine.
We compile, load the emulator and see the results. Everything seems to go well until we press zero, start the game and the menu remains.
To fix this, we add the following line below the Init tag:
call CLS ; Clear screen

Start of game
When the game starts, the first thing we will do is ask for the names of the players. First, we add another constant to rom.asm; this address contains the coordinates of the cursor.
; Cursor position on screen 2. ; If loaded at BC -> B = Y, C = X. CURSOR: equ $5c88
We implement the routine responsible for retrieving the names of the players in game.asm. Although we won’t go through it instruction by instruction, due to the size of the routine we will implement it in blocks.
GetPlayersName: ld hl, Name_p1 ld de, Name_p1+$01 ld bc, LENNAME*2-$01 ld (hl), " " ldir
We have cleared the names of the players.
ld e, $01 getPlayersName_loop: ld a, INK4 call INK ld b, INI_TOP-$0f ld c, INI_LEFT-$01 call CLL call AT ld hl, TitlePlayerNumber ld (hl), "1" ld a, $01 cp e jr z, getPlayersName_cont ld (hl), "2" getPlayersName_cont: ld hl, TitlePlayerName call PrintString ld hl, Name_p1 ld a, $01 cp e jr z, getPlayersName_cont2 ld hl, Name_p2
We make a loop of at most two iterations, one per player, change the colour, delete the line where the names are requested, position the cursor, prepare the title depending on the player, and point HL to the player’s name.
getPlayersName_cont2: ld d, $00 ld a, INK3 call INK call getPlayersName_getName ld a, (MaxPlayers) cp $02 jr nz, getPlayersName_onlyOne inc e cp e jr z, getPlayersName_loop ret
We use D to control the length of the name, change the ink and ask for the player’s name. We get the players, see if there are two players, and if not, default the name to two. If there are two players, we check if the name entered is the name of player two, and if not, we loop to ask for it.
getPlayersName_onlyOne: ld hl, Name_p2Default ld de, Name_p2 ld bc, LENNAME ldir ret
If it is a single player, give the second player the default name.
getPlayersName_getName: push hl call WaitKeyAlpha pop hl cp KEYDEL jr z, getPlayersName_delete cp KEYENT jr z, getPlayersName_enter push de ld e, a ld a, LENNAME cp d ld a, e pop de jr z, getPlayersName_getName ld (hl), a inc hl rst $10 inc d jr getPlayersName_getName
We wait for a valid key to be pressed, check if it is Delete and if so, we jump to its operation. We check if it is enter and if it is, we jump to its operation.
If it is neither delete nor enter, we check if we have reached the maximum length and if not, we add the character to the name and draw it.
getPlayersName_delete: ld a, $00 cp d jr z, getPlayersName_getName dec d dec hl ld a, ' ' ld (hl), a ld bc, (CURSOR) inc c call AT rst $10 call AT jr getPlayersName_getName
If the key pressed is delete and the length of the name is not zero, we delete the previous character of the name and the display.
getPlayersName_enter: ld a, 0 cp d jr z, getPlayersName_getName ret
If the key pressed is enter and the length of the name is not zero, the name request is terminated.
The final aspect of the routine is as follows:
; ------------------------------------------------------------------- ; Ask for the names of the players. ; ; Alters the value of the AF, BC, DE and HL registers. ; ------------------------------------------------------------------- GetPlayersName: ld hl, Name_p1 ; HL = address player name 1 ld de, Name_p1+$01 ; DE = HL+1 ld bc, LENNAME*2-$01 ; BC = length names - 1 ld (hl), " " ; Clears first position ldir ; Clean up the rest ld e, $01 ; E = 1 getPlayersName_loop: ld a, INK4 ; A = ink 4 call INK ; Change ink ld b, INI_TOP-$0f ; B = coord Y ld c, INI_LEFT-$01 ; X = coord X call CLL ; Delete the line call AT ; Position cursor ld hl, TitlePlayerNumber ; HL = player number ld (hl), "1" ; Player 1 ld a, $01 ; A = 1 cp e ; Player 1? jr z, getPlayersName_cont; Yes, skip ld (hl), "2" ; Player 2 getPlayersName_cont: ld hl, TitlePlayerName ; HL = title player name call PrintString ; Paints it ld hl, Name_p1 ; HL = address player name 1 ld a, $01 ; A = 1 cp e ; Player 1? jr z, getPlayersName_cont2 ; Yes, skip ld hl, Name_p2 ; HL = address player name 2 getPlayersName_cont2: ld d, $00 ; D = counter length name ld a, INK3 ; A = ink 3 call INK ; Change ink call getPlayersName_getName; Request player name ld a, (MaxPlayers) ; A = players cp $02 ; Two players? jr nz, getPlayersName_onlyOne ; One player, default name inc e ; E+=1 cp e ; Compare with players jr z, getPlayersName_loop; Equals, jumps ret ; Single player ; Copies the default name of player 2 getPlayersName_onlyOne: ld hl, Name_p2Default ; HL = default player name 2 ld de, Name_p2 ; DE = name player 2 ld bc, LENNAME ; Length name ldir ; Copy default name ret ; Sale ; Requests the player's name getPlayersName_getName: push hl ; Preserve HL call WaitKeyAlpha ; Wait for valid key pop hl ; Retrieve HL cp KEYDEL ; Delete? jr z, getPlayersName_delete ; Yes, skip cp KEYENT ; Enter? jr z, getPlayersName_enter ; Yes, skip push de ; Preserves DE ld e, a ; E = code Ascii ld a, LENNAME ; A = maximum length name cp d ; D = maximum length? ld a, e ; A = code Ascii pop de ; Retrieve DE jr z, getPlayersName_getName ; D = maximum length ; other character ; Enter or Delete ld (hl), a ; Append character to name inc hl ; HL = next position rst $10 ; Print character inc d ; D+=1 jr getPlayersName_getName; Request another character getPlayersName_delete: ld a, $00 ; A = 0 cp d ; Length 0? jr z, getPlayersName_getName ; Yes, another character dec d ; D-=1 dec hl ; HL-=1, previous character ld a, ' ' ; A = space ld (hl), a ; Clear previous character ld bc, (CURSOR) ; BC = cursor position inc c ; BC = previous column for AT call AT ; Position cursor rst $10 ; Delete the display character call AT ; Position cursor jr getPlayersName_getName; Other character getPlayersName_enter: ld a, 0 ; A = 0 cp d ; Length 0? jr z, getPlayersName_getName ; Yes, another character ret ; End name
Let’s see if it works. In main.asm we find Init, and under CALL CLS we add the call to the name request, and another call to CLS.
call GetPlayersName ; Request player names call CLS ; Clear screen
We compile, load into the emulator and see the results. We can now enter the names of the players and they will appear in the game information. If it is a single player game, we are playing against ZX Spectrum.

Shift time
One of the options is the number of seconds each player has to make their move. If the move has not been made within these seconds, the player loses the turn and it passes to the other player.
We will use interrupts to control the time. We create the file int.asm.
At the top of main.asm, just below ORG $5E88, we add the following lines (we already did this in Space Battle):
; ------------------------------------------------------------------- ; Flags ; bit 0 -> Reset countdown ; bit 1 -> Lose turn ; bit 2 -> Paint countdown ; bit 3 -> Warning sound, countdown ends ; ------------------------------------------------------------------- flags: db $00 ; Value countdown countdown: db $00 ; Seconds per shift seconds: db $00
Seconds is used to tell the interrupts how many seconds per turn the players have chosen. With seconds, the MaxSeconds variable is no longer needed, so we will remove it.
Locate Main and after the CALL OPENCHAN line, initialise seconds:
ld a, $10 ; A = $10 BCD ld (seconds), a ; Update seconds ld (countdown), a ; Update countdown
Locate menu_Time and after JR NZ, menu_op modify the line LD A, (MaxSeconds), as follows:
ld a, (seconds) ; A = seconds
Locate menu_TimeDo, delete LD, (MaxSecond), A and add the following lines instead:
ld (seconds), a ; Update seconds ld (countdown), a ; Update countdown
In screen.asm, at the end of the PrintOptions routine, we replace LD HL, MaxSeconds with:
ld hl, seconds ; HL = time value
Finally, we delete the MaxSeconds definition in var.asm.
The interrupt routine will change the value of the flags and the countdown, we need to take this into account in the main loop.
For the interrupt routine to run fifty times per second (in PAL, sixty in NTSC), mode two of the interrupts must be enabled. Locate Loop in main.asm and add over it:
di ; Disables interruptions ld a, $28 ; A = $28 ld i, a ; I = A (interruptions in $7e5c) im $02 ; Interruptions = mode 2 ei ; Activates interruptions
We disable the interrupts, load $28 into the interrupt register, set it to mode two and enable it.
We look for loop_key and just below it we implement the flag handling logic.
ld a, (flags) ; A = Flags bit $02, a ; Bit 2 active? res $02, a ; Disables bit 2 jr z, loop_warning ; Not active, skips push af ; Preserve AF call PrintCountDown ; Paint countdown pop af ; Retrieve AF
We load the flags in A and evaluate if bit two is active and deactivate it. If it is not active, we jump, if it is, we draw the countdown.
loop_warning: bit $03, a ; Bit 3 active? res $03, a ; Disable bit 3 jr z, loop_lostMov ; Not active, skip ld bc, SoundCountDown ; BC = sound direction call PlayMusic ; Outputs sound
We evaluate whether bit three is active and deactivate it. If it is active, we jump, if it is not, we give the warning sound.
loop_lostMov: bit $01, a ; Bit 1 active? res $01, a ; Disables bit 1 ld (flags), a ; Update in memory halt ; Synchronise with interrupts jr z, loop_keyCont ; Not active, skip ld hl, TitleLostMovement ; HL = address mov lost ld de, TitleLostMov_name ; DE = address name call DoMsg ; Paint message ld bc, SoundLostMovement ; BC = sound direction call PlayMusic ; Outputs sound jr loop_cont ; Jump loop_keyCont:
We check if bit one is active, disable it, update the flags and synchronise with the interrupts. If it is, we draw the lost motion message and play the sound. The loop_keyCont tag is above the CALL WaitKeyBoard.
Finally, locate loop_cont, delete JR Loop and add the following lines in its place:
ld a, $01 ; A = restart countdown ld (flags), a ; Update flags jp Loop ; Main loop
We have updated the flags so that the interrupt routine restarts the countdown. We also replaced JR with JP because the lines we added make JR out of range.
In screen.asm we will implement the routine that draws the countdown.
; ------------------------------------------------------------------- ; Paint the countdown. ; ; Alters the value of the AF, BC and HL registers. ; ------------------------------------------------------------------- PrintCountDown: ld a, INK3 ; A = ink 3 call INK ; Change ink ld b, INI_TOP-$0c ; B = coord Y ld c, INI_LEFT ; C = coord X call AT ; Position cursor ld hl, countdown ; HL = direction countdown call PrintBCD ; Paint countdown left ld c, INI_LEFT-$1e ; C = coord X call AT ; Position cursor jp PrintBCD ; Paints countdown right and exits
We paint the countdown in two positions, on the left and right of the board. We set the ink to magenta, move the cursor to the left, draw the countdown, move the cursor to the right and draw the countdown.
The interrupt handling routine is implemented in int.asm (do not include this file in main.asm). We will also look at this implementation in blocks.
; ------------------------------------------------------------------- ; int.asm ; ; Mode 2 Interrupt Handling ; ------------------------------------------------------------------- org $7e5c ; ------------------------------------------------------------------- ; Indicators ; bit 0 -> Reset countdown ; bit 1 -> Lose turn ; bit 2 -> Paint countdown ; bit 3 -> Warning sound, countdown ends ; ------------------------------------------------------------------- FLAGS: equ $5e88 COUNTDOWN: equ FLAGS+$01 ; Countdown Value SECONDS: equ FLAGS+$02 ; Seconds per turn
The interrupt routine is loaded at address $7E5C. Then we add the constants with the memory addresses we will use for the information exchange between main.asm and int.asm.
CountDownISR: push af push bc push de push hl push ix ; Preserves records
The first step of the routine is to preserve the value of the registers.
countDown_flags: ld a, (FLAGS) ; A = flags and $01 ; Reset countdown? jr z, countDown_cont ; No, skip ld a, (SECONDS) ; A = SECONDS ld (COUNTDOWN), a ; Refreshes in memory ld a, $04 ; A = paint countdown ld (FLAGS), a ; Update in memory jr countDownISR_end ; End
We check if main.asm indicates that the countdown should be restarted (shift change) and jump if it does not. If there is a shift, it restarts the countdown at the value specified in the menu, indicates in flags that the countdown should be painted, and jumps to exit the routine.
countDown_cont: ld hl, countDownTicks ; HL = ticks counter inc (hl) ; Counter ticks+=1 ld a, $32 ; A = 50 cp (hl) ; Counter ticks = 50? jr nz, countDownISR_end ; No, skip. xor a ; A = 0, Z = 1, Carry = 0 ld (hl), a ; Counter ticks = 0
In countDownTicks we add one at each pause, and when we reach fifty it is a sign that a second has passed, which we check in this block. If it has not reached fifty, it jumps to exit the routine, if it has, we set the ticks counter to zero and continue with the routine.
; It's reached 50, it's been a second. ld a, (COUNTDOWN) ; A = countdown value dec a ; A-=1 daa ; Decimal setting ld (COUNTDOWN), a ; Update in memory ld b, $04 ; B = paint countdown / 4 sec cp b ; Less than 4 seconds? jr nc, countDownISR_reset; A >= 4, skip set $03, b ; Warning sound or a ; A = 0? jr nz, countDownISR_reset; No, skip set $01, b ; Misses turn
If one second has passed, we calculate what kind of information to pass to main.asm. We subtract one second from the countdown, see if it is below four (it also communicates to main.asm that the countdown has to be painted), and jump if it is not. If it is, we set the bit for the warning sound, evaluate whether the countdown has reached zero, and jump if it has not. Turn on the shift loss bit if it has reached zero.
countDownISR_reset: ld a, b ; A = B ld (FLAGS), a ; Updates in memory
Update the value of flags with the information you want to pass to main.asm.
countDownISR_end: pop ix pop hl pop de pop bc pop af ; Retrieves records ei ; Activates interruptions reti ; Exits countDownTicks: db $00 ; Ticks (50*sec)
We get the value of the registers, enable interrupts and exit. Finally we declare the tick counter.
Remember that we now need to compile two separate .tap’s and make our own loader.
The basic loader looks like this:
10 CLEAR 24200 20 LOAD ""CODE 24200 30 LOAD ""CODE 32348 40 RANDOMIZE USR 24200
The script to compile on Windows looks like this:
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 Generating Tic-tac-toe copy /y /b loader.tap+oxo.tap+int.tap TicTacToe.tap echo Process completed
The Linux version looks like this:
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 Generating Tic-tac-toe cat loader.tap oxo.tap int.tap > TicTacToe.tap echo Process completed
We compile it, load it into the emulator and check that the countdown is painted, that an alarm goes off if it goes under four seconds and that it loses the turn if it goes to zero. We also see the missed turn message.

End of game
At the end of this chapter we will implement the end of the game, which occurs when one of the players reaches the points defined in the menu, or when the maximum number of tables is reached.
At the end of the game we are asked if we want another game; we add the keys of the answer as constants in var.asm. We also add the maximum number of tables.
KEYN: equ $4e ; N key KEYn: equ $6e ; Key n KEYS: equ $53 ; S key KEYs: equ $73 ; Key s MAXTIES: equ $05 ; Maximum number of tables
Depending on whether you want to play another game or not, you will jump to one place or another in main.asm. We locate the Init tag, and after CALL GetPlayersName we add the following tag:
Start:
We locate the loop_reset tag and just below it we will implement the check to see if someone has won the game.
call PrintPoints ; Paint the dots ld a, (MaxPoints) ; A = maximum points ld b, a ; B = A ld a, (Points_p1) ; A = points player 1 cp b ; Player 1 wins? jr z, EndPlay ; Yes, skip ld a, (Points_p2) ; A = points player 2 cp b ; Player 2 wins? jr z, EndPlay ; Yes, skip ld b, MAXTIES ; B = maximum tables ld a, (Points_tie) ; A = points tables cp b ; A = B? jr z, EndPlay ; Yes, skip
We draw the points and compare the players’ scores with the set maximum score. If one of the players has the required number of points, they win the game.
At the end of main.asm, before the includes, we add the split end routine.
EndPlay: di ; Disables interrupts im $01 ; Mode 1 interrupts ei ; Activates interruptions ld hl, TitleGameOver ; HL = title game over call PrintMsg ; Paints the message endPlay_waitKey: call WaitKeyAlpha ; Wait key cp KEYN ; Pressed N? jp z, Menu ; Yes, menu cp KEYn ; Pushed n? jp z, Menu ; Yes, menu cp KEYS ; Pulsed S? jp z, Start ; Yes, start cp KEYs ; Pushed s? jp z, Start ; Yes, start jr endPlay_waitKey ; Not pressed, loop
We deactivate interruptions, switch to mode one, activate interruptions, wait for the question to be answered and, depending on the answer, we jump to one place or another.
- Menu: Return to the main menu. We can select the different options and enter new player names.
- Start: is the label we have added. It clears the screen, paints the screen, resets the game data and switches interrupts to mode two.
If none of the expected keys have been pressed, it will loop until one is.
If you go to the Menu tag, you will see that the first three lines are for switching to interrupt mode one, which is what we do in EndPlay. Delete the three lines below the Menu tag.
We compile, load into the emulator and see the results. Everything seems to work fine, but at the end of the game the score is flashing. This is something we inherited from the beginning, although we didn’t notice it until now.
To change the behaviour, locate PrintPoints in screen.asm and add the lines needed to disable the flickering just below it.
ld a,(ATTR_T) ; A = temporary attributes and $7f ; Removes flicker ld (ATTR_T), a ; Update in memory

At this point, we can play the first full two-player games.
ZX Spectrum Assembly, Tic-Tac-Toe
Want to play against the Spectrum and beat it? We will implement this in the next chapter of ZX Spectrum Assembly,
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.