1

I'd appreciate some help please folks with a (what should be) simple game in GDScript 2.0. I am making a tic-tac-toe game and Player 2 is an AI using the minimax algorith. However the AI is really dumb sometimes and just lets me win when it would have been so easy to block a winning line. I've been trying to debug this all day long and I am pulling my hair out. The complete project can be found at:

https://github.com/thearduinoguy/KitKatGo

Thanks.

var INFINITY = 10000000
var gridSize = 3
var game_depth = 50
var win_length = 3

#=================================================================================================
func minimax(grid: Array, size: int, length: int, depth: int, alpha: int, beta: int, is_maximizing: bool) -> int:
    var winner = check_winner(grid, size, length)
    if winner != EMPTY:
        return (winner * (size - depth)) * (1 if depth % 2 == 0 else -1)

    if depth >= game_depth:
        return 0

    if is_maximizing:
        var best_value = -INFINITY
        for r in range(size):
            for c in range(size):
                if grid[r * size + c] == EMPTY:
                    grid[r * size + c] = PLAYER_O
                    var value = minimax(grid, size, length, depth + 1, alpha, beta, false)
                    grid[r * size + c] = EMPTY
                    best_value = max(value, best_value)
                    alpha = max(alpha, best_value)
                    if beta <= alpha:
                        break
        return best_value
    else:
        var best_value = INFINITY
        for r in range(size):
            for c in range(size):
                if grid[r * size + c] == EMPTY:
                    grid[r * size + c] = PLAYER_X
                    var value = minimax(grid, size, length, depth + 1, alpha, beta, true)
                    grid[r * size + c] = EMPTY
                    best_value = min(value, best_value)
                    beta = min(beta, best_value)
                    if beta <= alpha:
                        break
        return best_value

#=================================================================================================
func find_best_move(grid: Array, size: int, length: int) -> Vector2:
    var best_value = -INFINITY
    var best_move = Vector2(-1, -1)

    for r in range(size):
        for c in range(size):
            if grid[r * size + c] == EMPTY:
                grid[r * size + c] = PLAYER_O
                var move_value = minimax(grid, size, length, 0, -INFINITY, INFINITY, true)
                grid[r * size + c] = EMPTY

                if move_value > best_value or (move_value == best_value and randf() > 0.5):
                    best_value = move_value
                    best_move = Vector2(c, r)

            if best_value == INFINITY:  # Early exit when we found the best possible move
                break
        if best_value == INFINITY:  # This break is for the outer loop
            break

    return best_move
unlut
  • 3,525
  • 2
  • 14
  • 23
  • 2
    Your code should be inside the question. – trincot Mar 18 '23 at 19:22
  • I assume you know how to use godot's debugger. Thus, it may help to create a graphical visualization that you can overlay on top of the game. It will give you another perspective into how the AI is consuming data and deciding its moves. I have not looked at the project. – hola Mar 19 '23 at 20:08

1 Answers1

1

Your code is mostly correct, but main problem is at the start of the minimax search you are making two consecutive moves for PLAYER_O. One move in the find_best_move function and then one move in the minimax function when you call it with true arg. Also you should first decide which player is the maximizing player and which player is the minimizing player then write your code consistently. I made two small chances:

1- Your AI player is the minimizer, so your find_best_move function now searchs for move with least value. You can change it the other way around, but the main thing is find_best_move and minimax functions must be consistent.
2- minimax function returns a constant value for the base case, depending on the winner side, no need to perform depth calculations.

#=================================================================================================
func minimax(grid: Array, size: int, length: int, depth: int, alpha: int, beta: int, is_maximizing: bool) -> int:
    var winner = check_winner(grid, size, length)
    if winner != EMPTY:
        #print("Winner:",winner, " for grid:", grid)
        return winner*100
        #return (winner * (size - depth)) * (1 if depth % 2 == 0 else -1)

    if depth >= game_depth:
        return 0

    if is_maximizing:
        var best_value = -INFINITY
        for r in range(size):
            for c in range(size):
                if grid[r * size + c] == EMPTY:
                    grid[r * size + c] = PLAYER_O
                    var value = minimax(grid, size, length, depth + 1, alpha, beta, false)
                    grid[r * size + c] = EMPTY
                    best_value = max(value, best_value)
                    alpha = max(alpha, best_value)
                    if beta <= alpha:
                        break
        return best_value
    else:
        var best_value = INFINITY
        for r in range(size):
            for c in range(size):
                if grid[r * size + c] == EMPTY:
                    grid[r * size + c] = PLAYER_X
                    var value = minimax(grid, size, length, depth + 1, alpha, beta, true)
                    grid[r * size + c] = EMPTY
                    best_value = min(value, best_value)
                    beta = min(beta, best_value)
                    if beta <= alpha:
                        break
        return best_value

#=================================================================================================
func find_best_move(grid: Array, size: int, length: int) -> Vector2:
    var best_value = INFINITY
    var best_move = Vector2(-1, -1)

    for r in range(size):
        for c in range(size):
            if grid[r * size + c] == EMPTY:
                grid[r * size + c] = PLAYER_O
                var move_value = minimax(grid, size, length, 1, -INFINITY, INFINITY, false)
                grid[r * size + c] = EMPTY

                if move_value < best_value or (move_value == best_value and randf() > 0.5):
                    best_value = move_value
                    best_move = Vector2(c, r)
                
                print("Move value for (", r, ",", c, "):", move_value)

            if best_value == -INFINITY:  # Early exit when we found the best possible move
                break
        if best_value == -INFINITY:  # This break is for the outer loop
            break

    return best_move
unlut
  • 3,525
  • 2
  • 14
  • 23