Tic-Tac-Toe AI takes two squares in one move

What’s up, G-fam! I’ve been working on a hybrid tic-tac-toe game, and so far, the game works good…if you play against a human. Virtually all the events are using a grid-based system, where the squares are named as such:

A1 A2 A3
B1 B2 B3
C1 C2 C3

I’m working on an AI that plans its moves in reaction to what’s already on the board, and I’ve done so by breaking down each type of move in order of priority: as such, winning is the most important, then blocking a row that the opponent almost covered, then picking the center if it’s not occupied, then picking a random corner, then picking a random edge.

Here is an example of this priorities list. The variable playerTurn dictates if it’s P1 or P2’s turn, Player2Is dictates if the match is against a human or an AI, and the variable MoveMade dictates whether it should make a move or not. It’s turned to false each time Player1 finishes their move.

Here is an example of how a winning move is done, based on horizontal, vertical or diagonal placement:

And here’s an example of a blocking move:

Thing is, the AI has this weird tendency of doing two moves when it has the chance. If the board is perfectly set up so it can block and win, it doesn’t prioritize just winning, so it blocks and wins at the same time. What gives? I’d like it to just do one move, not two.

If y’all need more details, feel free to tell me. I’ll upload more screenshots.

The blocking events don’t check whether MoveMade is already true. So a winning move is played, and then a blocking move is played.

The blocking logic is missing “if MoveMade = false” conditions.

Trigger once seems redundant here, since you are already setting MoveMade every time (thus preventing other events from triggering anyway)

Side note, now that you have done everything the painful way, by copying events for each possibility, I very much encourage you to try recreating these events using arrays and iteration instead.

I was going to mention that too. But if @Rrad is up to it, there’s the option of going one further, and using alphabeta pruning to work out the next best move for the AI.

1 Like

Okay, DAMN. You guys are making me dig really deep. I appreciate it. To answer MrMen’s first reply, nah, it didn’t work even with that. Suppose putting a check for MoveMade above the event groups and in the conditions themselves does nothing. I’ll go ahead and try the array and iteration method instead, but if it doesn’t work, I’ll try to document myself on alphabeta pruning. I was hoping I didn’t have to turn to minimaxing, but if I have to, then that AI is gonna be a hell of an opponent.

I’ll keep you posted on my progress. Thanks very much for the replies!

It should work. Can you post the events when you added the MoveMade condition to the blocking events?


That’ll depend on how many nodes deep you go. Which also means you can have a difficulty selection for the game :slight_smile:

@MrMen Alright, here they are:

This applies to the horizontal 1 combinations tho. For some reason, it isn’t skipped over if a winning move is done. Also removed the Trigger Once conditions, and you guys were right. Works just as good without them.

That means either:

  1. the MoveMade is either not set in the winning move event,
  2. MoveMade is set to false before the block is checked, or
  3. the block and/or win is not done in Horiz1 event group.

I’d suggest checking your other events to see if (2) or (3) is true.

Nah, it still won’t play ball. I even put the code as the very first one in the scene, thinking something else might interfere with it. I’m guessing I have to redo it from scratch using the array and iterations. I’ll need to brush up on using them first, but I’ll keep you posted.

Alright, everyone. I’ve pretty much recreated the whole code to include arrays. I’ve pretty much modelled it after this example right here: Tic tac toe - a game example from the GDevelop game making app | GDevelop. I love how much more intuitive it is, especially when it comes to the win conditions, and just how little code is in there.

But here’s another issue: I’m pretty much stumped. I haven’t worked much with arrays before, and I’ve only just understood how to represent a grid using them. I would love some help regarding the AI now. @magicsofa you’ve mentioned recreating the prior events using arrays and iteration. Apparently it’s much less painful than what I have in mind. Guys, I would really love (and most likely need) some help or ideas with how to implement it.

In this event you’re changing the “MoveMade” variable after waiting 1 second, instead it should happen before the wait action.

Well, you should have a 3x3 array, so each square can be accessed by coordinates. The middle square would be Array[1][1]. The corners are [0][0], [0][2], [2][0], and [2][2].

(You could also accomplish this using objects. The Tilemap object already gives you a grid of tiles that works just like an array, so you could just use a Tilemap instead of having a separate variable for the array data. In this case you would use the relevant actions to read tiles at specific grid locations)

The next step is to read the board. To do this you’ll need to store information about which grid you are currently reading, as well as the states you have found so far (X, O, or empty). The current grid could be represented by some variables like curX and curY, or an invisible object that you move around. The found moves could just be an array with 3 elements since you only need to think about one row/column/diagonal at a time.

So, instead of directly testing A1Tile, A2Tile, and so on, you can test any row by setting up the cursor and moving it appropriately. To test the top row you start at grid 0, 0, then add 1 to the X coordinate to get the top middle tile, then add 1 again to get the top left corner. Each time you put the result in your 3-element array (also frequently referred to as a list).

Finally, check your list to see whether you should win or block. I might do this by having a variable to count the number of X or O found, as well as a variable to remember the last empty space. If two same letters are found in a row, then you know you can win or block by placing in the empty space.

There are of course different levels of sophistication that you can shoot for, but the basic idea here is to think about your events and consider whether you can represent them with numbers and arithmetic instead of having to copy and paste the same logic over and over. So for example, instead of having separate events for rows and columns, you could have variables to remember the direction that you are going in:

curX += dx
curY += dy

To do a row you’d have dx = 1, dy = 0. For a column, dx = 0, dy = 1. And for diagonal, dx and dy both = 1.

It is definitely more complex initially, but the power of it is that you can use one block of code/events to check any row or column, starting from any position, going in any direction you like. With your initial method, imagine changing the grid to 4x4. Suddenly you need to add a whole bunch more actions and events, whereas if you’re doing things numerically, you just have to change a few parameters (i.e. do 4 steps instead of 3)