Local Multiplayer might be fun if you really have someone to play with, but let’s face it: paper is still a better medium for this game.
Let’s build a script that can match the ability to play of human players and sometimes make a suboptimal move to not force a draw each game. To make the script easier to handle, I chose to hard-code the human player as the one playing Xs while the script will playing Os and will be described here as Player O or the NPC (non-player character).
Just be warned: the logic is not perfect, it’s not optimal, but it does a decent job at giving the player a challenge.
Notation
This page contains board samples that also represent potential moves done by the NPC – in several cases there are more than one good moves that can be done and it’s good to have a basic understanding on how to read the board in this case.

Making the game easier by rolling a D10 dice
Tic-Tac-Toe is a solved game that can always end with a draw when both players play perfectly, but let’s face it, it’s not fun playing knowing you will never win.
To push the luck a bit in favor of the player, we generate a random number that will be used to sometimes skip the most optimal moves.
set @chance = Random(1,10)
NPC Logic
Let’s dive into the two objectives a player can have:
#Winning
The main priority of the NPC is simple: win the game.
Let’s consider the following situation, where the NPC (Player O) has to select the next move:
![]() | ⟶ | ![]() |
Selecting the 3rd cell in the top row will make this player victorious, but we need to make the script be able to identify it.
Let’s use code to identify the opportunities for moves:
set @opportunities = ReplaceList(@verification, "E", "1", "2", "3", "4", "5", "6", "7", "8", "9")
set @opportunitiesForO = ReplaceList(@opportunities, "WIN", "OOE", "OEO", "EOO")
set @opportunitiesForX = ReplaceList(@opportunities, "WIN", "XXE", "XEX", "EXX")
What’s happening here?
- The board in this state is represented as the following string:
@fields = 'OO34X6X89'
- The
@verification
string for this looks like this:OO3^4X6^X89^O4X^OX8^369^OX9^XX3^
- We replace each number contained in this string with the letter
E
(symbolizing empty cells) using theReplaceList
function:OOE^EXE^XEE^OEX^OXE^EEE^OXE^XXE^
- Player O is close to winning if this string contains any of the following substrings:
OOE
,OEO
,EOO
) and if this is the case, we replace this with the substringWIN
(this is stored in the@opportunitiesForO
variable for Player O) - The variable string
@opportunitiesForX
is used in the same way to see if Player X is about to win the game
Let’s try to seize the victory:
/* NPC Logic: START */
/* Grab the win opportuniny*/
IF
IndexOf(@opportunitiesForO, "WIN") > 0
THEN
set @winPosition = IndexOf(@opportunitiesForO, "WIN")
set @emptyPositionInWin = IndexOf(Substring(@opportunities, @winPosition, 3), "E")
set @fieldPosition = Add(@winPosition, Add(@emptyPositionInWin, -1))
set @fieldIndex = Substring(@verification, @fieldPosition, 1)
set @fields = Replace(@fields, @fieldIndex, "O")
If we learn that the NPC can win in their next move, we must only determine where exactly the winning O
should be placed and learn that we we do the following:
@winPosition
→ we determine at which place in the@opportunitiesForO
theWIN
string occurs@emptyPositionInWin
→ using the win position from above, we extract the a 3 letter substring from the@opportunities
string (it’s a step before replacing the winning line withWIN
) and find which of the three characters contains theE
character representing the empty cell@fieldPosition
→ here we add the numbers from the above steps to get the index from the @verification string in the next step@fieldIndex
→ using the number from above, we retrieve the number hidden at the@fieldPosition
in the@verification
string
set @state = "Os Win!"
set @stateClass = "o"
set @gameOver = true
set @firstPlayer = "X"
Blocking Xs from winning
If you can’t win immediately, you need to concentrate on blocking the other player from winning in their next move. In similar way as above, we detect if Player X has an option to win in their next move.
/* Block Xs from winning */
ELSEIF
IndexOf(@opportunitiesForX, "WIN") > 0
THEN
set @winPosition = IndexOf(@opportunitiesForX, "WIN")
set @emptyPositionInWin = IndexOf(Substring(@opportunities, @winPosition, 3), "E")
set @fieldPosition = Add(@winPosition, Add(@emptyPositionInWin, -1))
set @fieldIndex = Substring(@verification, @fieldPosition, 1)
set @fields = Replace(@fields, @fieldIndex, "O")
Making the (sometimes sub-optimal) first move
Player X isn’t always the one that’s starting the game. Knowing this we need to have a separate case for the NPC making the first move.
There are three distinct opening that we can make:
- Take any of the corner cells
- Take the middle cell
- Take any of the edge cells
/* First Move Logic */
ELSEIF
@fields == "123456789"
AND
@firstPlayer == "O"
THEN
/* Don't be a perfect opponent */
IF @chance <= 6
THEN
set @fields = Replace(@fields, Add(Multiply(Random(1,4),2), -1), "O") /* Select a corner */
ELSEIF
@chance >= 7 AND @chance <= 9
THEN
set @fields = Replace(@fields, "5", "O") /* Select the middle*/
ELSEIF
@chance == 10
THEN
set @fields = Replace(@fields, Multiply(Random(1,4),2), "O") /* Select an edge*/
ENDIF
As you can see, ff the NPC starts with the first move, it considers the @chance
variable to make the decision on how to start.
- values from 1 to 6 (60% chance) – select a corner
- values from 7 to 9 (30% chance) – play the middle
- value equal to 10 (10% chance) – grab an edge cell
- Corner cells have the following values:
1
,3
,7
,9
- Let’s concatenate this into one string
1379
- We can now generate a random number (
Random(1,4)
) to get a random number from a range including numbers from the first to the last (4th) position in the string - Now we can combine this into
Substring('1379', Random(1,4), 1)
which will represent a random value of the four possible corner cells
Select a corner cell

Randomly selecting a corner
Select the middle

- Edge cells have the following values:
2
,4
,6
,8
- We can see those are even number which are multipliers of 2
- This means that if we got a random number from the range of 1 to 4 (
Random(1,4)
) and multiply it by 2 (Multiply(Random(1,4),2)
), we will always generate one of our even which represent edge cells
Select an edge cell
Technically, the middle of an edge cell (not the entire edge):

Randomly selecting an edge
Second move logic
Sometimes the NPC, will start second and needs to be reactive to what the human player is doing.
After a corner was taken
The NPC will take the middle position to block possible forks.

/* Second move */
ELSEIF
/* if X has played a corner field first */
@fields == "X23456789"
OR
@fields == "12X456789"
OR
@fields == "123456X89"
OR
@fields == "12345678X"
THEN
set @fields = Replace(@fields, "5", "O")
After the middle was taken
The NPC will take a random corner:

ELSEIF
/* if X has played the center field first */
@fields == "1234X6789"
THEN
set @fields = Replace(@fields, Add(Multiply(Random(1,4),2), -1), "O")
/* Fourth Move: Counter play for corner forks :D */
This sums up the coded reactions to the first move done by the player.
Blocking Forking
One other situation we need to account for is preventing players from building forks – the occur when a player has two or more next move that would allow them to win.
The most common type of this is the corner fork where one player controls 3 corners which would give them 3 opportunities to win:

We need to prevent this situation from happening, so if Player X has to two corners taken and Player O took the middle, in the next move it needs to take an edge. Why? Because it creates a line of two O’s and Player X needs to respond to this by blocking Os from winning.
There are two possible boards arrangements for this type of forking and here you can see the responses:
Corner forks
The middle edge cells will be selected at random:

The code accounting for both forks:
ELSEIF
(@fields == "X234O678X"
OR
@fields == "12X4O6X89")
AND
@chance >= 9
THEN
set @fields = Replace(@fields, Multiply(Random(1,4),2), "O")
There’s no code fighting a different type of fork, so consider this a hint on how to beat the NPC 😉
Fallback move
If the situation we’re dealing with is not like any of the cases we processed above, we just simply have NPC select a random free field.
ELSE
set @tempFields = ReplaceList(@fields, "", "X", "O")
set @newField = Substring(@tempFields, Random(1, Length(@tempFields)), 1)
set @fields = Replace(@fields, @newField, "O")
ENDIF
With this out of the way, we have a NPC player and we can see the full code next.