2

Note #1 - There is a similar question but that is in Python, and I can't figure out this in C.

Note #2 - This is a human vs. AI game and I will call the AI as 'cpu'. The cpu symbol is 'O' and human symbol is 'X' always.

Note #3 - I want to design the game so that the cpu never loses (either win or draw).

I want the user to go first and choose any of the 9 squares. So basically I want the cpu to brute force calculate the result of every way the game can go, and based on that assign 'scores' to its possible moves (8 remaining choices). This way it backtracks and knows which way to go so as to not lose.

Also I want to do that using the minimax algorithm, and in C.

My problem - What I think is that I'm missing a function that actually makes use of the returned 'scores'. I do not want the code, but I would appreciate it if I could get an idea of how to implement that function. Also the nextMove() function confuses me, I want it to check if there is no unfilled box, then fill it with 'O' and then it's the player's turn. But since it will be called again in the recursion, it might not work correctly. Any suggestions?

Edit - The bounty goes to the answer that guides me on how to implement this game using minimax algorithm as described here: https://en.wikipedia.org/wiki/Minimax#Pseudocode

#include <stdio.h>
#define FALSE 0
#define TRUE 1
#define NEGINF -10000
#define POSINF +10000


struct node { // each node is like a snapshot of the game at that point in time
    char board[9]; // the 3x3 square is implemented as an array of 9 chars 
    int value; // the value of that snapshot (+1 if cpu wins, -1 if cpu loses, 0 for draw)
    int position; // 0 to 8 inclusive with 0 being [0][0] and 8 being [2][2] (row major fashion) and indicates position where the next 'X' or 'O' would be assigned
};



int max(int value, int returnedValue) {
    return value > returnedValue ?  value :  returnedValue;
}

int min(int value, int returnedValue) {
    return value < returnedValue ?  value :  returnedValue;
}

int someoneHasWon(struct node someNode) {
    if(someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'X')
        return TRUE;        
    
    if(someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'O')
        return TRUE;    
        //someNode.value = -1;
    if(someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'X')
        return TRUE;    
        //someNode.value = 1;//return 1;
    if(someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'O')
        return TRUE;    
        //someNode.value = -1;//return -1;
    if(someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'X')
        return TRUE;
            //someNode.value = 1;//return 1;
    if(someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'O')
        return TRUE;    
        //someNode.value = -1;//return -1;
    if(someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'X')
        return TRUE;    
        ////someNode.value = 1;//return 1;
    if(someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'O')
        return TRUE;    
        //someNode.value = -1;//return -1;
    if(someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'X')
        return TRUE;    
        //someNode.value = 1;//return 1;
    if(someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'O')
        return TRUE;    
        //someNode.value = -1;//return -1;
    if(someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'X')
        return TRUE;    
        //someNode.value = 1;//return 1;
    if(someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'O')
        return TRUE;    
        //someNode.value = -1;//return -1;
    if(someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'X')
        return TRUE;    
        //someNode.value = 1;//return 1;
    if(someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'O')
        return TRUE;    
        //someNode.value = -1;//return -1;
    if(someNode.board[6] == someNode.board[4] && someNode.board[4] == someNode.board[2] && someNode.board[2] == 'X')
        return TRUE;    
        //someNode.value = 1;//return 1;
    if(someNode.board[6] == someNode.board[4] && someNode.board[4] == someNode.board[2] && someNode.board[2] == 'O')
        return TRUE;

    return FALSE;
}

int isTerminal(struct node someNode) {
    for(int i = 0; i < 9; i++)
        if(someNode.board[i] == ' ') // if square left to fill then game is yet incomplete
            return FALSE;
    if(someoneHasWon(someNode) == TRUE) // if any player has won earlier with squares left
        return TRUE;
    return TRUE; // if it's a draw with no squares left to fill

}






struct node nextMove(struct node someNode) {
    for(int i = 0; i < 9; i++)
        if(someNode.board[i] == ' ') {
            someNode.board[i] = 'O';
            break;
        }
        return someNode;
    }

    int minimax(struct node someNode, int depth, int maximizingPlayer) {

        if(depth == 0 || isTerminal(someNode) == TRUE) {
            if(someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'X')
                someNode.value = 1;
            if(someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'O')
                someNode.value = -1;
            if(someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'X')
                someNode.value = 1;//return 1;
            if(someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'O')
                someNode.value = -1;//return -1;
            if(someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'X')
                someNode.value = 1;//return 1;
            if(someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'O')
                someNode.value = -1;//return -1;
            if(someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'X')
                someNode.value = 1;//return 1;
            if(someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'O')
                someNode.value = -1;//return -1;
            if(someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'X')
                someNode.value = 1;//return 1;
            if(someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'O')
                someNode.value = -1;//return -1;
            if(someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'X')
                someNode.value = 1;//return 1;
            if(someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'O')
                someNode.value = -1;//return -1;
            if(someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'X')
                someNode.value = 1;//return 1;
            if(someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'O')
                someNode.value = -1;//return -1;
            if(someNode.board[6] == someNode.board[4] && someNode.board[4] == someNode.board[2] && someNode.board[2] == 'X')
                someNode.value = 1;//return 1;
            if(someNode.board[6] == someNode.board[4] && someNode.board[4] == someNode.board[2] && someNode.board[2] == 'O')
                someNode.value = -1;//return -1;
            someNode.value = 0;//return 0;
        }
        if(maximizingPlayer == TRUE) { //maximizing player is cpu i.e. 'O'
        
            someNode.value = NEGINF;
            while(isTerminal(someNode) != TRUE)
                someNode.value = max(someNode.value, minimax(nextMove(someNode), depth - 1, FALSE));
            return someNode.value;
    }
    else { //minimizing player is me i.e. 'X'
        int boxNumber = 0;
        scanf("%d", &boxNumber);
        someNode.position = boxNumber;
        someNode.board[someNode.position] = 'X';
        someNode.value = POSINF;
        while(isTerminal(someNode) != TRUE)
            someNode.value = min(someNode.value, minimax(nextMove(someNode), depth - 1, TRUE));
        return someNode.value;
    }
}

int main() {
    int boxNumber = 0;
    printf("Assume you're X and cpu is O \nInput any box number you like \n(0 to 8 both inclusive) \n...you'll be defeated anyways lol :\n");
    scanf("%d", &boxNumber);
    struct node origin;
    for(int i = 0; i < 9; i++)
        origin.board[i] = ' ';
    origin.position = boxNumber;
    origin.board[origin.position] = 'X';
    origin.value = 0;
    minimax(origin, 8, TRUE);
}

Code version #2 -

#include <stdio.h>
#define FALSE 0
#define TRUE 1
#define NEGINF -10000
#define POSINF +10000

struct node
{
    char board[9];
    int value;
    int position;
};

//char someNode.board[9] = {' ', ' ', ' ', ' ', ' ', ' ', ' ', ' ', ' '};

int max(int value, int returnedValue)
{
    return value > returnedValue ? value : returnedValue;
}

int min(int value, int returnedValue)
{
    return value < returnedValue ? value : returnedValue;
}

int someoneHasWon(struct node someNode)
{
    if (someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'x')
        return TRUE;

    if (someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'o')
        return TRUE;
    //someNode.value = -1;
    if (someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'x')
        return TRUE;
    //someNode.value = 1;//return 1;
    if (someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'o')
        return TRUE;
    //someNode.value = -1;//return -1;
    if (someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'x')
        return TRUE;
    //someNode.value = 1;//return 1;
    if (someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'o')
        return TRUE;
    //someNode.value = -1;//return -1;
    if (someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'x')
        return TRUE;
    ////someNode.value = 1;//return 1;
    if (someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'o')
        return TRUE;
    //someNode.value = -1;//return -1;
    if (someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'x')
        return TRUE;
    //someNode.value = 1;//return 1;
    if (someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'o')
        return TRUE;
    //someNode.value = -1;//return -1;
    if (someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'x')
        return TRUE;
    //someNode.value = 1;//return 1;
    if (someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'o')
        return TRUE;
    //someNode.value = -1;//return -1;
    if (someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'x')
        return TRUE;
    //someNode.value = 1;//return 1;
    if (someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'o')
        return TRUE;
    //someNode.value = -1;//return -1;
    if (someNode.board[6] == someNode.board[4] && someNode.board[4] == someNode.board[2] && someNode.board[2] == 'x')
        return TRUE;
    //someNode.value = 1;//return 1;
    if (someNode.board[6] == someNode.board[4] && someNode.board[4] 

== someNode.board[2] && someNode.board[2] == 'o')
            return TRUE;
    
        return FALSE;
    }
    
    int isTerminal(struct node someNode)
    {
        for (int i = 0; i < 9; i++)
            if (someNode.board[i] == ' ') // if square left to fill then game is yet incomplete
                return FALSE;
        if (someoneHasWon(someNode) == TRUE) // if any player has won earlier with squares left
            return TRUE;
        return TRUE; // if it's a draw with no squares left to fill
    }
    
    struct node nextMove(struct node someNode)
    {
        for (int i = 0; i < 9; i++)
            if (someNode.board[i] == ' ')
            {
                someNode.board[i] = 'o';
                break;
            }
        return someNode;
    }
    
    int minimax(struct node someNode, int depth, int maximizingPlayer)
    {
    
        if (depth == 8 || isTerminal(someNode) == TRUE)
        {
            if (someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'x')
                someNode.value = 1;
            if (someNode.board[0] == someNode.board[3] && someNode.board[3] == someNode.board[6] && someNode.board[6] == 'o')
                someNode.value = -1;
            if (someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'x')
                someNode.value = 1; //return 1;
            if (someNode.board[1] == someNode.board[4] && someNode.board[4] == someNode.board[7] && someNode.board[7] == 'o')
                someNode.value = -1; //return -1;
            if (someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'x')
                someNode.value = 1; //return 1;
            if (someNode.board[2] == someNode.board[5] && someNode.board[5] == someNode.board[8] && someNode.board[8] == 'o')
                someNode.value = -1; //return -1;
            if (someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'x')
                someNode.value = 1; //return 1;
            if (someNode.board[0] == someNode.board[1] && someNode.board[1] == someNode.board[2] && someNode.board[2] == 'o')
                someNode.value = -1; //return -1;
            if (someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'x')
                someNode.value = 1; //return 1;
            if (someNode.board[3] == someNode.board[4] && someNode.board[4] == someNode.board[5] && someNode.board[5] == 'o')
                someNode.value = -1; //return -1;
            if (someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'x')
                someNode.value = 1; //return 1;
            if (someNode.board[6] == someNode.board[7] && someNode.board[7] == someNode.board[8] && someNode.board[8] == 'o')
                someNode.value = -1; //return -1;
            if (someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'x')
                someNode.value = 1; //return 1;
            if (someNode.board[0] == someNode.board[4] && someNode.board[4] == someNode.board[8] && someNode.board[8] == 'o')
       

         someNode.value = -1; //return -1;
        if (someNode.board[6] == someNode.board[4] && someNode.board[4] == someNode.board[2] && someNode.board[2] == 'x')
            someNode.value = 1; //return 1;
        if (someNode.board[6] == someNode.board[4] && someNode.board[4] == someNode.board[2] && someNode.board[2] == 'o')
            someNode.value = -1; //return -1;
        someNode.value = 0;      //return 0;
    }
    if (maximizingPlayer == TRUE)
    { //maximizing player is cpu i.e. 'o'

        someNode.value = NEGINF;
        while (isTerminal(someNode) != TRUE)
            someNode.value = max(someNode.value, minimax(nextMove(someNode), depth + 1, FALSE));
        if (someNode.value == -1) {
            printf("o %d \n", someNode.value);
            return someNode.value;
        }
    }
    else
    { //minimizing player is me i.e. 'x'
        int boxNumber = 0;
        printf("Enter your move :\n");
        scanf("%d", &boxNumber);
        someNode.position = boxNumber;
        someNode.board[someNode.position] = 'x';
        someNode.value = POSINF;
        while (isTerminal(someNode) != TRUE)
            someNode.value = min(someNode.value, minimax(nextMove(someNode), depth + 1, TRUE));
        if (someNode.value == 1)
            return someNode.value;
    }
}

int main()
{
    int boxNumber = 0;
    printf("Assume you're x and cpu is O \nInput any box number you like \n(0 to 8 both inclusive) \n...you'll be defeated anyways lol :\n");
    scanf("%d", &boxNumber);
    struct node origin;
    origin.position = boxNumber;
    origin.board[origin.position] = 'x';
    origin.value = 0;
    printf("%c %d \n", origin.board[origin.position], origin.position);
    int val = minimax(origin, 1, TRUE);
    if(val > 0)
        printf("You lose");
    else if(val < 0)
        printf("You win");
    else
        printf("It's a draw");
    return 0;
}
Divyanshu Varma
  • 122
  • 3
  • 17
  • Do you want to implement in a specific way (you do describe a lot about what you think and what you want) or do you want the program so that AI never loses? I ask because I suspect you do not get answers ( in spite of the bounty) because of the possible conflict. I.e. what makes you think that making a never-losing AI the way you describe is actually possible? Consider writing something like "I will award the bounty to the answer which achieves a never-losing AI; the one closest to my approach to break ties." Or "Bounty to the answer which gets the best (losing) AI according to my approach." – Yunnosch Dec 11 '20 at 11:06
  • Yes, I want the AI to never lose (either win or draw). Also, I'm doing it to teach myself Minimax, as it is described here: https://en.wikipedia.org/wiki/Minimax#Pseudocode – Divyanshu Varma Dec 11 '20 at 12:49
  • https://www.geeksforgeeks.org/minimax-algorithm-in-game-theory-set-3-tic-tac-toe-ai-finding-optimal-move/ – ralf htp Dec 11 '20 at 17:17

2 Answers2

2

What I think is that I'm missing a function that actually makes use of the returned 'scores'.

Well, no, the the minimax() function itself is the only one that needs the score it (recursively) computes. The issue is that the pseudocode you're looking at on Wikipedia is very "pseudo". It illustrates the idea of the algorithm reasonably well, but it doesn't provide a very good model for actually implementing it. In particular, it omits a key component that any practical implementation requires: in addition to computing a score, minimax() needs to provide at least one move that leads to that score. That's the move the top-level caller chooses.

Also the nextMove() function confuses me, I want it to check if there is no unfilled box, then fill it with 'O' and then it's the player's turn. But since it will be called again in the recursion, it might not work correctly. Any suggestions?

There are at least two possible general approaches:

  1. make temporary copies of the board as needed, so that you can modify these without changing the original, or
  2. keep track of each position tested, and clear it after the test.

Either way, it's helpful to separate the move analysis process from the actual choice of move.

Note well that you are already making copies by passing struct node objects by value. By itself, that's not necessarily a full or correct implementation of option (1), but it's definitely something to be aware of, especially with respect to my main point, above.

Also, I don't particularly like the pseudocode's approach of using a boolean input to minimax() to indicate whether to maximize or minimize. I prefer instead to pass an argument to indicate which player's move it is. In a symmetric game such as Tic Tac Toe, the minimax() function then makes use of the fact that the better a result is for one player, the worse it is for the other. Among other things, this tends to simplify the code because the code then doesn't need separate cases around the value of that parameter.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • I realised that I was not using the value returned by the `minimax` function to the `main` method. I updated that. Also, I noticed that the pseudo code on wikipedia uses the depth of the tree as if it was its height (basically the opposite of my definition of height). So I changed `depth - 1` to `depth + 1` in the recursive call. – Divyanshu Varma Dec 14 '20 at 11:26
  • The code is still not working though, and any comments/hints on my code would be appreciated. – Divyanshu Varma Dec 14 '20 at 11:27
1

Tic-tac-toe is a very simple game, so thus doesn't have too many game-states. So you do not need to limit the AI to a depth.

function bestMove(board, maximisingPlayer) is
  if tie then
    return {value: 0}
  else if maximising player won then
    return {value: ∞}
  else if minimising player won then
    return {value: -∞}

  if maximisingPlayer then
    value := -∞
    move := 0

    for each child of node do
      tempValue := minimax(child, FALSE).value

      if tempValue > value then
        value := tempValue
        move := child
    
    return {value: value, move: move}
  else 
    value := ∞
    move := 0

    for each child of node do
      tempValue := minimax(child, TRUE).value

      if tempValue < value then
        value := tempValue
        move := child
    
    return {value: value, move: move}

/* Returns best child node after move is made. */
currentBestMove := bestMove(currentBoard, currentPlayer).move
    

Also as a remark on your code, do not put the win logic in the same procedure as the minimax, try to split your code into smaller functions as much as possible. Also, you may store the win combinations in an array.

Crupeng
  • 317
  • 2
  • 14