Created
December 22, 2023 02:42
-
-
Save MurphyMc/f92271b800c2fea509023be3959fb5b6 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // Simple single player tic-tac-toe in C | |
| // Computer player uses minimax algorithm and has adjustable skill | |
| // by introducing randomness. | |
| // MurphyMc @ github, 2023 | |
| #include <stdio.h> | |
| #include <time.h> | |
| #include <stdlib.h> | |
| #include <stdbool.h> | |
| // Once in "skill" turns, I'll make a move that isn't necessarily the | |
| // best move; 1 means I play randomly. 100 would mean I play pretty | |
| // darn well. 0 is a special case where I'll always play my best. | |
| int skill = 6; | |
| char * marks = "X O"; | |
| int8_t board[9]; | |
| // X is -1, O is 1. Which is you and which is me? | |
| int you = -1; // X (but might get flipped) | |
| int me; // -you | |
| // These encode all winning positions | |
| int starts[] = {0,3,6,0,1,2,0,2}; | |
| int steps[] = {1,1,1,3,3,3,4,2}; | |
| // In minimax(), we iterate through each possible move. If we just did that | |
| // in order, we'd always play the same way. So instead, we iterate in a | |
| // random order. We put the random order in "mutation". | |
| int mutation[] = {0,1,2,3,4,5,6,7,8}; | |
| void mutate () | |
| { | |
| // Scramble mutation array using Fisher-Yates algorithm | |
| for (int done = 0; done < 9 - 1; ++done) | |
| { | |
| int pick = done + (rand() % (9 - done)); | |
| int tmp = mutation[done]; | |
| mutation[done] = mutation[pick]; | |
| mutation[pick] = tmp; | |
| } | |
| } | |
| char mark (int m) | |
| { | |
| return marks[m+1]; | |
| } | |
| int check_win () | |
| { | |
| for (int i = 0; i < 8; ++i) | |
| { | |
| int pos = starts[i], step = steps[i]; | |
| int first = board[pos]; | |
| if (!first) continue; | |
| if ((first == board[pos+step]) && (first == board[pos+step*2])) | |
| return first; | |
| } | |
| return 0; | |
| } | |
| bool moves_remain () | |
| { | |
| // Returns true if there are still valid moves | |
| for (int i = 0; i < 9; ++i) | |
| { | |
| if (board[i] == 0) return true; | |
| } | |
| return false; | |
| } | |
| void draw () | |
| { | |
| for (int i = 0; i < 9; ++i) | |
| { | |
| printf(" "); | |
| if (board[i] == 0) | |
| printf("%i", i+1); | |
| else | |
| printf("%c", mark(board[i])); | |
| if (i % 3 < 2) | |
| printf(" |"); | |
| else if (i < 8) | |
| printf("\n ---------\n"); | |
| } | |
| printf("\n"); | |
| } | |
| void your_move () | |
| { | |
| while (true) | |
| { | |
| printf("What's your move, %c? ", mark(you)); | |
| fflush(stdout); | |
| char buf[16]; | |
| if (!fgets(buf, sizeof(buf), stdin) || buf[0] == 'q' || buf[0] == 'Q') | |
| { | |
| printf("Bye!\n"); | |
| exit(0); | |
| } | |
| if (buf[0] == '?') | |
| { | |
| draw(); | |
| continue; | |
| } | |
| int move = buf[0] - '0' - 1; | |
| if (move >= 0 && move <= 8) | |
| { | |
| if (!board[move]) | |
| { | |
| board[move] = you; | |
| return; | |
| } | |
| printf("There is already an %c there!\n", mark(board[move])); | |
| } | |
| printf("Enter a number from 1 to 9, or Q to quit.\n"); | |
| } | |
| } | |
| int minimax (int turn, bool recursing) | |
| { | |
| // minimax algorithm -- minimize the maximum | |
| // The algorithm works by essentially simulating each player; turn is 1 or -1 | |
| // depending on the "current" player. For a given player, it tries a move, | |
| // and then calls minimax() recursively with the opposite "turn" in order to | |
| // simulate the other player. minimax() returns the "score" for the player | |
| // it's being called as, so the caller -- after trying each possible move it | |
| // can make -- picks a move which results in a minimum opponent score. That | |
| // is, it tries to MINImize the opponent's MAXimum score. Our best move goes | |
| // with their worst score. | |
| // What's a score? Well, currently it's pretty simple: it's 0 if it's not a | |
| // win, and it's 1 if it's a win. :) | |
| // The return value is usually (when recursing is true) the score. But for | |
| // the topmost call (recursing is false), it returns the best move. | |
| int win = check_win(); | |
| if (win) return win*turn; | |
| int best_move = -1; // Invalid | |
| int worst_opponent_score = 0; | |
| for (int j = 0; j < 9; j++) | |
| { | |
| int i = mutation[j]; | |
| if (board[i]) continue; | |
| board[i] = turn; // try this move | |
| int opponent_score = minimax(-turn, true); | |
| if (opponent_score < worst_opponent_score || best_move == -1) | |
| { | |
| worst_opponent_score = opponent_score; | |
| best_move = i; | |
| } | |
| board[i] = 0; // undo the move we were trying | |
| } | |
| if (best_move == -1) return 0; // no moves; tie | |
| if (!recursing) return best_move; | |
| return -worst_opponent_score; // my score is opposite of theirs! | |
| } | |
| void my_turn () | |
| { | |
| printf("My turn...\n"); | |
| if (skill && ((rand() % skill) == 0)) | |
| { | |
| // Make a random move... | |
| int o = rand(); | |
| for (int j = 0; j < 9; j++) | |
| { | |
| int m = (j+o) % 9; | |
| if (board[m]) continue; | |
| board[m] = me; | |
| return; | |
| } | |
| } | |
| board[minimax(me, false)] = me; | |
| } | |
| int main (int argc, char * argv[]) | |
| { | |
| // Initialize randomization... | |
| srand(time(NULL)); | |
| mutate(); | |
| if (rand() & 1) | |
| you *= -1; | |
| me = -you; | |
| int turn = -1; | |
| printf("You're playing as %c.\n\n", mark(you)); | |
| if (turn == you) | |
| { | |
| printf("You go first, since you're X.\n\n"); | |
| draw(); | |
| printf("\n"); | |
| } | |
| else | |
| printf("X goes first -- that's me.\n\n"); | |
| while (true) | |
| { | |
| if (!moves_remain()) | |
| { | |
| printf("It's a tie!\n"); | |
| break; | |
| } | |
| if (check_win()) | |
| { | |
| if (check_win() == you) | |
| printf("You won!\n"); | |
| else | |
| printf("I won!\n"); | |
| break; | |
| } | |
| if (turn == you) | |
| your_move(); | |
| else | |
| my_turn(); | |
| turn *= -1; | |
| printf("\n"); | |
| draw(); | |
| printf("\n"); | |
| } | |
| return 0; | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment