Home > Blockchain >  Minimax algorithm behaving strangely
Minimax algorithm behaving strangely

Time:12-18

I am working on assignment. I want to create a game that is simulated and played by the computer. It is a logical game and the goal is to collect the most tokens with the highest value. Program starts by asking for input in this format('END' signals its the end of input, working on removing it):

N:1,2 
W:3,5
E:9,1,1,1
S:1,7
END

Letters stand for direction like East, West, North, South. Numbers after the directions are tokens with value. You can only pick tokens from end of an direction. Player that collects most valuable tokens wins.

I need to make the game to play itself the most optimal way, found minimax algorithm. But I am completly clueless how to implement it correctly.

I am asking some kind soul, to help me make it work correctly. Give some tips at least :)

This is what I tried. Its working somehow but not the most optimal way. For example in the input I provided the most optimal result is:

A/B: 15/16

But I am getting:

A/B: 14/17

My code here.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <math.h>
#define MAX_TOKENS 100

struct Game
{
    //lists with the values ​​of the tokens on the individual arms of the cross
    int *north;
    int *west;
    int *east;
    int *south;
    //lengths of token value lists
    int north_size;
    int west_size;
    int east_size;
    int south_size;
    //scores of players A and B
    int score_a;
    int score_b;
};
void remove_token(struct Game *game, int player, char direction)
{
    //variable for the token taken
    int token;

    //according to the direction, remove the token and add its value to the player's score
    switch (direction)
    {
    case 'N':
        //remove the token from the N arm
        token = game->north[game->north_size - 1];
        //reduce the length of the token value list by 1
        game->north_size--;
        //add the chip value to the player's score
        if (player == 0)
            game->score_a  = token;
        else
            game->score_b  = token;
        break;
    case 'W':
        //remove the token from the W arm
        token = game->west[game->west_size - 1];
        //reduce the length of the token value list by 1
        game->west_size--;
        //add the chip value to the player's score
        if (player == 0)
            game->score_a  = token;
        else
            game->score_b  = token;
        break;
    case 'E':
        //remove the token from the E arm
        token = game->east[game->east_size - 1];
        //reduce the length of the token value list by 1
        game->east_size--;
        //add the chip value to the player's score
        if (player == 0)
            game->score_a  = token;
        else
            game->score_b  = token;
        break;
    case 'S':
        //remove the token from the S arm
        token = game->south[game->south_size - 1];
        //reduce the length of the token value list by 1
        game->south_size--;
        //add the chip value to the player's score
        if (player == 0)
            game->score_a  = token;
        else
            game->score_b  = token;
        break;
    default:
        //throw an error if the player entered an invalid direction
        printf("Neplatný směr pro odebrání žetonu!\n");
    }
}
char minimax(struct Game *game, int depth, int player, int alpha, int beta)
{
    //if all arms are empty or we have reached the maximum depth, return the best direction
    if (game->north_size == 0 && game->west_size == 0 && game->east_size == 0 && game->south_size == 0 || depth == 0)
        return 'X';

    //the best move value for player A
    int best_score_a = INT_MIN;
    //the best move value for player B
    int best_score_b = INT_MAX;
    //direction for best move
    char best_direction;

    //go through all arms to find the best move
    if (game->north_size > 0)
    {
        //copy the game state
        struct Game game_copy = *game;
        //remove the token from the N arm
        remove_token(&game_copy, player, 'N');
        //find out the best move for your opponent
        int score = minimax(&game_copy, depth - 1, player == 0 ? 1 : 0, alpha, beta);
        //update the best move for player A
        if (player == 0 && score > best_score_a)
        {
            best_score_a = score;
            best_direction = 'N';
        }
        //update the best move for player B
        if (player == 1 && score < best_score_b)
        {
            best_score_b = score;
            best_direction = 'N';
        }
        //update alpha and beta
        if (player == 0)
            alpha = fmax(alpha, score);
        else
            beta = fmin(beta, score);
        //if beta is less than alpha, we end traversing the tree
        if (beta <= alpha)
            return best_direction;
    }
      if (game->west_size > 0)
    {
        //copy the game state
        struct Game game_copy = *game;
        //remove the token from the W arm
        remove_token(&game_copy, player, 'W');
        //find out the best move for your opponent
        int score = minimax(&game_copy, depth - 1, player == 0 ? 1 : 0, alpha, beta);
        //update the best move for player A
                if (player == 0 && score > best_score_a)
        {
            best_score_a = score;
            best_direction = 'W';
        }
        //update the best move for player B
        if (player == 1 && score < best_score_b)
        {
            best_score_b = score;
            best_direction = 'W';
        }
        //update alpha and beta
        if (player == 0)
            alpha = fmax(alpha, score);
        else
            beta = fmin(beta, score);
        //if beta is less than alpha, we end traversing the tree
        if (beta <= alpha)
            return best_direction;
    }
    if (game->east_size > 0)
    {
        //copy the game state
        struct Game game_copy = *game;
        //remove the token from the E arm
        remove_token(&game_copy, player, 'E');
        //find out the best move for your opponent
        int score = minimax(&game_copy, depth - 1, player == 0 ? 1 : 0, alpha, beta);
        //update the best move for player A
        if (player == 0 && score > best_score_a)
        {
            best_score_a = score;
            best_direction = 'E';
        }
        //update the best move for player B
        if (player == 1 && score < best_score_b)
        {
            best_score_b = score;
            best_direction = 'E';
        }
        //update alpha and beta
        if (player == 0)
            alpha = fmax(alpha, score);
        else
            beta = fmin(beta, score);
        //if beta is less than alpha, we end traversing the tree
        if (beta <= alpha)
            return best_direction;
    }
    if (game->south_size > 0)
    {
        //copy the game state
        struct Game game_copy = *game;
        //remove the token from the S arm
        remove_token(&game_copy, player, 'S');
        //find out the best move for your opponent
        int score = minimax(&game_copy, depth - 1, player == 0 ? 1 : 0, alpha, beta);
        //update the best move for player A
        if (player == 0 && score > best_score_a)
        {
            best_score_a = score;
            best_direction = 'S';
        }
        //update as soon as possible
                if (player == 1 && score < best_score_b)
        {
            best_score_b = score;
            best_direction = 'S';
        }
        //update alpha and beta
        if (player == 0)
            alpha = fmax(alpha, score);
        else
            beta = fmin(beta, score);
        //if beta is less than alpha, we end traversing the tree
        if (beta <= alpha)
            return best_direction;
    }

    //return the best move
    return player == 0 ? best_direction : best_direction;
}
void read_tokens(int *north, int *west, int *east, int *south, int *north_size, int *west_size, int *east_size, int *south_size)
{
    //buffer for reading in input
    char buffer[MAX_TOKENS];

    //read in the input line by line
    while (fgets(buffer, MAX_TOKENS, stdin) != NULL)
    {
        //remove the newline character from the end of the line
        buffer[strcspn(buffer, "\n")] = 0;
        //check for the "END" string to end the input
        if (strcmp(buffer, "END") == 0)
            break;
        //split the line at the colon character
        char *direction = strtok(buffer, ":");
        char *tokens = strtok(NULL, ":");

        //split the tokens at each comma
        char *token = strtok(tokens, ",");

        //determine the direction and store the tokens in the appropriate array
        if (strcmp(direction, "N") == 0)
        {
            while (token != NULL)
            {
                north[*north_size] = atoi(token);
                (*north_size)  ;
                token = strtok(NULL, ",");
            }
        }
        else if (strcmp(direction, "W") == 0)
        {
            while (token != NULL)
            {
                west[*west_size] = atoi(token);
                (*west_size)  ;
                token = strtok(NULL, ",");
            }
        }
        else if (strcmp(direction, "E") == 0)
        {
            while (token != NULL)
            {
                east[*east_size] = atoi(token);
                (*east_size)  ;
                token = strtok(NULL, ",");
            }
        }
        else if (strcmp(direction, "S") == 0)
        {
            while (token != NULL)
            {
                south[*south_size] = atoi(token);
                (*south_size)  ;
                token = strtok(NULL, ",");
            }
        }
        else
        {
            //invalid direction = error
            printf("Nespravny vstup.\n");
        }
    }
}

void print_progress(struct Game *game, int player, char direction)
{
    char letter_player = ' ';
    if (player == 0)
    {
        letter_player = 'A';
    }
    else
        letter_player = 'B';
    //printing of individual steps
    switch (direction)
    {
    case 'N':
        printf("%c: %c[%d] (%d)\n", letter_player, direction, game->north_size, game->north[game->north_size - 1]);
        break;
    case 'W':
        printf("%c: %c[%d] (%d)\n", letter_player, direction, game->west_size, game->west[game->west_size - 1]);
        break;
    case 'E':
        printf("%c: %c[%d] (%d)\n", letter_player, direction, game->east_size, game->east[game->east_size - 1]);
        break;
    case 'S':
        printf("%c: %c[%d] (%d)\n", letter_player, direction, game->south_size, game->south[game->south_size - 1]);
        break;
    default:
        break;
    }
}

void play(struct Game *game, int depth)
{
    //variable for current player (A or B)
    int player = 0;

    //until all chips are taken, alternate players taking chips
    while (game->north_size > 0 || game->west_size > 0 || game->east_size > 0 || game->south_size > 0)
    {
        //player A
        if (player == 0)
        {
            //function on the selection of a token
            char direction = minimax(game, depth, 0, INT_MIN, INT_MAX);
            print_progress(game, player, direction);
            //remove the token from the game
            remove_token(game, player, direction);
        }
        //player B
        else
        {
            //function on the selection of a token
            char direction = minimax(game, depth, 1, INT_MIN, INT_MAX);
            print_progress(game, player, direction);
            //remove the token from the game
            remove_token(game, player, direction);
        }

        //switch players
        player = (player   1) % 2;
    }
}
int main(void)
{
    //field for token values
    int north[MAX_TOKENS], west[MAX_TOKENS], east[MAX_TOKENS], south[MAX_TOKENS];
    //sizes of token value fields
    int north_size = 0, west_size = 0, east_size = 0, south_size = 0;
    printf("Input:\n");
    //fetch token values ​​from input
    read_tokens(north, west, east, south, &north_size, &west_size, &east_size, &south_size);

    //creating a game
    struct Game game;
    game.north = north;
    game.west = west;
    game.east = east;
    game.south = south;
    game.north_size = north_size;
    game.west_size = west_size;
    game.east_size = east_size;
    game.south_size = south_size;
    game.score_a = 0;
    game.score_b = 0;
    //set the maximum depth of the minimax search tree
    int depth = 1;
    //start the game using the play() function
    play(&game, depth);

    //evaluation of the result of the game

    printf("Celkem A/B: %d/%d\n", game.score_a, game.score_b);

    return 0;
}

CodePudding user response:

This is not an answer. The OP did ask for 'tips', though. Here are a few:

  1. Don't drag-in the floating point library (math.h) for a single function ( fmax() ) that you could/should implement, especially as you are dealing with integer values and results.

Here are two 'branchless' statements returning the min or max of two integers: i & j.

int minVal = j ^ ((i^j) & -(i<j));
int maxVal = j ^ ((i^j) & -(i>j));
  1. You can safely drop END from your data file. fgets() will find 4 lines and then return NULL, finishing the loading of the initial values.

  2. As it stands, the initial values are single ASCII digits. Rather than calling atoi(), merely subtracting the ASCII character '0' from each ASCII digit will yield the (binary) integer value intended. (You might consider using 'A'-'Z' in the initial data, instead of '0' - '9', to both increase the range of points and obscure the best path to winning the game.

3a) Instead of "N:1,5,6,4", the data file could contain "N1564" obviating the need for using strtok()... Simply use each character in sequence.

  1. It's quite understandable that you, as a beginner, have coded to the "literal" game involving 4 points of a compass. You've written code that would be very difficult to change to having 5 or 6 (or more) 'stack's of values to choose from.

If, instead of discrete variables named 'north...', 'west...', etc., had you used an array of 4 elements, most of the copy/paste/adapt that has bloated the source code's size would melt down to processing each of the 4 (or 5 or 6) 'stacks' in a loop. imo, the art of coding is balancing the drive to "make something work" with the necessity of code re-use wherever possible. Look for redundancy and strive to eliminate that.

  1. Your code eliminates the trailing '\n' after fgets() has filled the input buffer. You then go on to translate an array of characters into arrays of discrete integer values. Then, the minimax function recusively trims copies of those arrays as it plays the game.

Consider keeping and working with the original array of characters (the string) instead. When "the user" picks the last 'character' (top of the stack) as their 'move', simply shorten the string of that stack. (Use C's string.h string handling capabilities instead of doing the work yourself with your own arrays of ints.) (The string "" has no more available tokens. Your code doesn't need to maintain a counter for each 'direction'.)

  1. (This might be easiest.) When you've moved to using 4 arrays - one for each of 'N', 'W', 'E' and 'S' - you will see that the fixation of compass points has blinded you to using 'A', 'B', 'C' and 'D' instead... Given the "non-special" nature of "ABCD", it becomes a goal to write code that would work for "ABCDE" or "ABCDEF".

Summary: By replacing replicated code with re-used code, your program will shrink down to something easily manageable. When functionality is re-used and made 'general purpose', you will likely be able to improve the minimax processing. Time spent finding these 'improvements' will be re-paid in abundance.

Putting my money where my mouth is, below is the 28-line version of one of the OP's posted 59-line function. This still uses the discrete variables for each "compass direction". Were those bundled into an array, this, too, could be written in 1/2 again as many lines.

void remove_token( struct Game *game, int player, char direction ) {
    int *arr, arr_sz;

    //according to the direction, remove the token and add its value to the player's score
    switch( direction ) {
        case 'N':
            arr = game->north;
            arr_sz = --game->north_size;
            break;
        case 'W':
            arr = game->west;
            arr_sz = --game->west_size;
            break;
        case 'E':
            arr = game->east;
            arr_sz = --game->east_size;
            break;
        case 'S':
            arr = game->south;
            arr_sz = --game->south_size;
            break;
    }

    //add the chip value to the player's score
    if (player == 0)
        game->score_a  = arr[ arr_sz ];
    else
        game->score_b  = arr[ arr_sz ];
}
  • Related