0

I implemented minimax with alpha-beta pruning for a tic-tac-toe game. This is the code:

This function returns if there are any more moves left.

bool isMovesLeft()
{
    for (int x = 0; x < ROWS; x++)
        for (int y = 0; y < COLS; y++)
            if (game.Board[x][y] == EMPTY)
                return true;

    return false;
}

Evaluates the board and returns the score. Possible outcomes: -10, 0, +10.

int evaluateBoard()
{
    for (int x = 0; x < ROWS; x++)
    {
        if (game.Board[x][0] == game.Board[x][1] &&
            game.Board[x][1] == game.Board[x][2])
        {
            if (game.Board[x][0] == PLAYER_X)
                return 10;
            else if (game.Board[x][0] == PLAYER_O)
                return -10;
        }
    }

    for (int y = 0; y < COLS; y++)
    {
        if (game.Board[0][y] == game.Board[1][y] &&
            game.Board[1][y] == game.Board[2][y])
        {
            if (game.Board[0][y] == PLAYER_X)
                return 10;
            else if (game.Board[0][y] == PLAYER_O)
                return -10;
        }
    }

    if (game.Board[0][0] == game.Board[1][1] &&
        game.Board[1][1] == game.Board[2][2])
    {
        if (game.Board[0][0] == PLAYER_X)
            return 10;
        else if (game.Board[0][0] == PLAYER_O)
            return -10;
    }

    if (game.Board[0][2] == game.Board[1][1] &&
        game.Board[1][1] == game.Board[2][0])
    {
        if (game.Board[0][2] == PLAYER_X)
            return 10;
        else if (game.Board[0][2] == PLAYER_O)
            return -10;
    }

    return 0;
}

The actual minimax algorithm with alpha-beta pruning:

int minimax(int depth, int alpha, int beta, bool isMax)
{
    int score = evaluateBoard();

    if (score == 10)
        return 10 - depth;

    if (score == -10)
        return -10 + depth;

    if (!isMovesLeft())
        return 0;

    if (isMax)
    {
        int best = -1000;

        for (int x = 0; x < ROWS; x++)
        {
            for (int y = 0; y < COLS; y++)
            {
                if (game.Board[x][y] == EMPTY)
                {
                    game.Board[x][y] = PLAYER_X;

                    int max = minimax(depth + 1, alpha, beta, !isMax);
                    if (max > best)
                        best = max;

                    if (best > alpha)
                        alpha = best;

                    game.Board[x][y] = EMPTY;

                    if (beta <= alpha)
                        break;
                }
            }
        }

        return best;
    }
    else if (!isMax)
    {
        int best = 1000;

        for (int x = 0; x < ROWS; x++)
        {
            for (int y = 0; y < COLS; y++)
            {
                if (game.Board[x][y] == EMPTY)
                {
                    game.Board[x][y] = PLAYER_O;

                    int min = minimax(depth + 1,alpha, beta, isMax);
                    if (min < best)
                        best = min;

                    if (best < beta)
                        beta = best;

                    game.Board[x][y] = EMPTY;

                    if (beta <= alpha)
                        break;
                }
            }
        }

        return best;
    }
}

Returns the best move. Called when it is the opponent's turn.

BestMove findBestMove()
{
    int bestScore = -1000;

    BestMove bestMove;
    bestMove.Row = -1;
    bestMove.Col = -1;

    if (game.Board[1][1] == EMPTY)
    {
        bestMove.Row = 1;
        bestMove.Col = 1;
        return bestMove;
    }

    for (int x = 0; x < ROWS; x++)
    {
        for (int y = 0; y < COLS; y++)
        {
            if (game.Board[x][y] == EMPTY)
            {
                game.Board[x][y] = PLAYER_X;

                int score = minimax(0, -10000000000, 10000000000, false);

                game.Board[x][y] = EMPTY;

                if (score > bestScore)
                {
                    bestScore = score;
                    bestMove.Row = x;
                    bestMove.Col = y;
                }
            }
        }
    }

    return bestMove;
}

I've also added the depth when calculating the score (in the minimax function) to make the AI slightly more intelligent.

However, the AI still plays silly moves and is quite easily beatable. Some wins are repeated over and over again.

Is the above code correct or am I missing something?

trincot
  • 317,000
  • 35
  • 244
  • 286
Ivan-Mark Debono
  • 15,500
  • 29
  • 132
  • 263
  • 1
    *Is the above code correct or am I missing something?* -- I am assuming you wrote the code. If so, you're the best person to answer the question. If the code does something that goes against the plan you have on paper, you should debug it to see where it goes against your plan. – PaulMcKenzie Sep 27 '22 at 15:55
  • Yes I wrote the code and taking the pseudocode from wikipedia. The AI does play but some moves are a little silly and it loses. – Ivan-Mark Debono Sep 27 '22 at 15:57
  • 2
    How big are the ints? 10000000000 = 0x2 540B E400 - more than 32 bits – Den-Jason Sep 27 '22 at 15:59
  • *Is the above code correct or am I missing something?* -- Well, if the program doesn't work the way you want it to work, then of course the code isn't "correct", and the next step is to debug the code. What debugging have you done on this code? – PaulMcKenzie Sep 27 '22 at 16:03
  • 4
    you could for example set up a test where you know the current state, you know what should be the next move, but your algorithm plays a different one. Then you can attach a debugger to see where and why the code does what it does – 463035818_is_not_an_ai Sep 27 '22 at 16:05
  • 2
    Exactly -- you should have set up a simple test run, knowing *exactly* what the results should be, and then run the program under the debugger, single-stepping through the code to see where the program diverges from your expectations. That's how any programmer who has written code like this would have proceeded. If you are running random examples, get rid of the randomness and use known data. Once you have the program working using known data, then introduce the randomness back into the picture. – PaulMcKenzie Sep 27 '22 at 16:07
  • _"taking the pseudocode from wikipedia"_ - That's all fine but it may also have prevented you to fully understand the algorithm which makes it hard to "see" implementation errors. I think the start of your alpha/beta function looks fishy. Why does it start by doing an evaluation of the board and terminate based on the result even though it's not a terminal node? In pseudo langage: _if depth = 0 or node is a terminal node then return the heuristic value of node_. Otherwise, it should go ahead and recurse down. – Ted Lyngmo Sep 27 '22 at 16:25

1 Answers1

0

The problem is caused by a wrong value for isMax. In the main else block of the minimise function, the recursive call is made by passing isMax, but this means the recursive call will get the same value for isMax as the current call. This is wrong. It should toggle that value. Either pass !isMax, or just pass a hard-coded true, since here isMax is false.

trincot
  • 317,000
  • 35
  • 244
  • 286