Skip to content

Instantly share code, notes, and snippets.

@MurphyMc
Created December 22, 2023 02:42
Show Gist options
  • Select an option

  • Save MurphyMc/f92271b800c2fea509023be3959fb5b6 to your computer and use it in GitHub Desktop.

Select an option

Save MurphyMc/f92271b800c2fea509023be3959fb5b6 to your computer and use it in GitHub Desktop.
// 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