Tic-Tac-Toe in C
I’ve been trying to get back into the roll of working with C programming, I haven’t really done much of it since I wrote about the N64 SDK several years back. So the thought came to me: why not try and make a game of Tic-Tac-Toe in C as a little project?
Update 30/10/2023: There is a Nintendo 64 version of this game available. Find out more about it here: Tic-Tac-Toe 64
What I find so fascinating about working in C it is that it is very bare-bones. Something as basic as appending to an array or searching a string isn’t done with a function out of the box. It really makes you peel back the onion of abstraction.
About Tic Tac Toe
It’s a pretty well-known game, but it’s a good idea to go through the fundamentals to be able to break the game down to its core components. Tic-Tac-Toe (aka Noughts and Crosses) is a 2-player game where each player takes turns placing a symbol (typically “X” and “O”) on a 3×3 grid. The first player to complete a row, column or diagonal wins the game.
Tic-Tac-Toe is a fairly simple game. That is, there isn’t much creativity or strategy since there are only 9! (362,880) possible games, most of which are nonsense and post-win. Every cell can be part of a row or column, but only the corners can be part of diagonals and the centre can be part of both diagonals. This means that the main strategy is to control the key points like the centre and corners since it gives you more ways to complete 3 in a row.
Board size
The standard board size for Tic-Tac-Toe is 3×3, and after working on this project, I realise why.
The 1×1 and 2×2 boards are trivial. In the 1×1, the first player wins on the first turn. In a 2×2, the first player wins on their second turn, no matter what.
For 4×4 and beyond, the game is impossible to win unless you have a deliberately stupid opponent. You have to look at it this way: in order to complete a row in a 4×4 board as player X, you need 4 Xs in that row. However, to block that row from completion, your opponent just needs one O. This makes it way too easy to block compared to winning, making it moot.
Some other versions of the game make it so that you don’t need a full row to win, but that might make it too easy to win in certain circumstances.
Strategy to win
Unlike a game of chess, two expert players will always end up in a draw. Every strategy has a counter so the only way to win is if one player makes an avoidable mistake.
That said, the optimal way to win (or at least, no lose) works like this. I know it sounds kind of obvious, but when your turn comes you need to place your symbol in the following order of priorities:
- If you have 2 in a row, complete the row to win
- If the opponent has 2 in a row, block their win
- Place it in the centre cell
- Place it in a corner cell (in a row/col where you already have one)
- Place anywhere else
Now, there are a few exceptions to this but it works as a general rule of thumb.
Tic-Tac-Toe in C
You can find the source code for this project here. The code is all in one file (just over 300 lines) split into a few functions for which I’ll give a brief overview of each.
The code starts out with the declarations:
#include <stdio.h> #include <stdlib.h> #define X 88 #define O 79 #define SPACE 32
These are pretty straightforward. Include a few libraries and use #define to simplify some common magic numbers. 88, 79 and 32 are just the ASCII values for capital X, capital O and space.
main()
This is the function that is summoned upon starting the program, as is standard in most C applications.
int main() {
// Declare vars
int players;
int i, j;
int *pboard;
int size;
int winner = 0;
int playerArray[] = {X, O};
int p;
// Intro
printf("Welcome to Tic Tac Toe in C!\n");
// Get settings
// Players
printf("Choose players (1/2): ");
players = getchar();
if (players == 49 || players == 50) {
players -= 48;
getchar();
printf("%i-player game.\n", players);
} else if (players == 10){
players = 1;
printf("Defaulted to %i player.\n", players);
} else {
printf("Please select only 1 or 2 players (%s).\n", players);
return 0;
}
// Board size
printf("Choose board size (1-9): ");
size = getchar();
if (size-48 >= 1 && size-48 <= 9) {
// Good!
size -= 48;
} else if (size == 10){
size = 3;
printf("Defaulted to a %ix%i board.\n", size, size);
} else {
printf("Please select a number between 1 and 9 (%s).\n", size);
return 0;
}
printf("Playing a %i-player game on a %ix%i board.\n", players, size, size);
// Generate the board
pboard = malloc(size * size * sizeof(char));
for (i = 0; i < size; i++) {
for (j = 0; j < size; j++) {
*(pboard + (sizeof(char)*i*size) + (sizeof(char)*j)) = SPACE;
}
}
// Play the game!
while (winner == 0) {
for (p = 0; p < sizeof(playerArray)/sizeof(playerArray[0]); p++){
if (winner) {
break;
}
if (players == 2 || (players == 1 && playerArray[p] == X)) {
// If it is a human turn
printBoard(pboard, size);
turn(pboard, playerArray[p], size);
} else {
// If it's a bot turn
botTurn(pboard, playerArray[p], size);
}
winner = checkWin(pboard, size);
}
}
// Print final board
printBoard(pboard, size);
free(pboard);
// Declare winner!
if (winner == 1) {
printf("Tie!");
} else {
printf("%c wins the game!", winner);
}
printf(" Press any key to finish.");
getchar();getchar();
return 0;
}
After the initial declarations, this can be split into a few smaller sections:
- Get settings:
- User inputs the number of players.
- User inputs the board size.
- Generate the board:
- Create an array of chars which is n2 in length.
- Fill the board up with SPACE (ASCII 32).
- Play the game:
- While no winner is declared:
- Alternate between player 1 and player 2.
- If current player is P1 or P2 is a human, play a human turn.
- Otherwise, play a bot turn.
- Finally check for a winner. If there is one, end the game.
- Close the game
Simple enough? The complicated part of the game comes in when you look at the ‘play the game’ section, which summons come more complicated functions.
printBoard()
void printBoard(int *p, int s) {
// Input the pointer to the board array and the size
// Function prints out the current board status
int i, j;
for (i=0;i<s;i++) {
printf("\n");
if (i > 0) {
// Print horizontal dividers
for (j=0;j<s;j++) {
if (j > 0) {
printf("----");
} else {
printf("\t---");
}
}
} else {
// Print headers
printf("\t");
for (j=0;j<s;j++) {
if (j > 0) {
printf(" %c ", j+65);
} else {
printf(" %c ", j+65);
}
}
printf("\n");
}
printf("\n %i", i+1);
// Print row values
for (j=0;j<s;j++) {
if (j > 0) {
printf("|");
} else {
printf("\t");
}
printf(" %c ",*(p + (sizeof(char)*i*s) + (sizeof(char)*j)));
}
}
printf("\n\n");
}
This function simply takes the board array pointer and board size as input and prints out the current status of the board. It’s longer than it should be because there’s a lot of exceptions that take place. For example when printing column dividers for 3×3 board, you have two dividers and three cells so the first cell needs to be printed without the divider while the others do need one.
turn()
int turn(int *p, char player, int size) {
// Get action command
char action[3];
int x, y;
while (1){
printf("Player %c, place your mark:",player);
scanf("%s",action);
x = action[0];
y = action[1];
// Interrupt game by pressing q
if (action[0] == 113 || action[0] == 81) {
printf("Exiting the game...");
exit(0);
}
// Validate input
if (x >= 97) {
x -= 32;
}
if (x > (64 + size) || x < 65){
printf("%c is an invalid value for x.\n", x);
continue;
} else if (y > 48 + size || y < 48) {
printf("%c is an invalid value for y.\n", y);
continue;
} else {
// Set to index
x -= 65;
y -= 49;
// printf("x:%d, y:%d\n",x, y);
}
// Check if cell has already been set
if(*(p + (sizeof(char)*y*size) + (sizeof(char)*x)) == SPACE) {
break;
} else {
printf("Cell has already been set.\n");
}
}
// Set cell
*(p + (sizeof(char)*y*size) + (sizeof(char)*x)) = player;
return 0;
}
The turn function deals with player turns. It takes the board and player as an input, and asks the player to input a cell like “a1” or “b3”. It checks to see if that input is valid, and then updates that cell with the player’s symbol.
botTurn
int botTurn(int *p, char player, int size) {
int i,j; // Loop iterators
int c,d; // Secondary loop iterator
int cv,l; // Secondary loop counter
int mult; // Score multiplier
int value; // Cell's current value in loop
int score; // Cell's priority
int highscore=0;// Cell with the highest score.
// Byte 1 is blank, 2 is x value, 3 is y value, 4 is score
int selfScore=0;// Count of the bot's own filled cells
// Loop through rows
for (i = 0; i < size; i++) {
// Loop through cells
for (j = 0; j < size; j++) {
value = *(p + (sizeof(char)*i*size) + (sizeof(char)*j));
if (value == SPACE) {
// If cell has not been chosen yet
// Get coordinate score
score = 1 + 1; // Add one for each row/column
if (i == j) { score++; } // Add one for diagonal 1
if (size - 1 - i == j) { score++; } // Add one for diagonal 2
mult = 1 + (1<<8) + (1<<16) + (1<<24);
selfScore = 0;
for (c = 0; c < size; c++) {
cv = (*(p + (sizeof(char)*i*size) + (sizeof(char)*c)) << 24) + // Horizontal
(*(p + (sizeof(char)*c*size) + (sizeof(char)*j)) << 16); // Vertical
if (i == j) {
cv += *(p + (sizeof(char)*c*size) + (sizeof(char)*c)) << 8; // Diagonal 1
}
if (size - 1 - i == j) {
cv += *(p + (sizeof(char)*(size - 1-c)*size) + (sizeof(char)*c)); // Diagonal 2
}
// Loop through the 4 parameters
for (l=0;l<4;l++) {
// Only add score if it it isn't blank or belonging to a the bot
if ((((cv>>(l*8)) & 0xFF) != 0x20) && (((cv>>(l*8)) & 0xFF) != player) && (((cv>>(l*8)) & 0xFF) != 0)) {
score += (mult>>(l*8)) & 0xFF;
mult += 2<<(l*8);
}
// Check if there are many of our own cells filled
if (((cv>>(l*8)) & 0xFF) == player) {
selfScore += 1<<(l*8);
}
// If row/col/diag is missing just one cell, fill it.
if (selfScore >> (l*8) == (size - 1)) {
score += 5;
}
}
}
// Check for a high score
if (score > (highscore & 0xFF)) {
highscore = (score & 0xFF) | (i << 16 & 0xFF0000) | (j << 8 & 0xFF00);
// printf("%08x!", highscore);
} else {
// printf("%08x ", score & 0xFF);
}
} else {
// If cell has been set, just print who filled it
//printf("%c ", value);
}
}
}
// Set cell
*(p + (sizeof(char)*(highscore >> 16 & 0xFF)*size) + (sizeof(char)*(highscore >> 8 & 0xFF))) = player;
}
This is, in my opinion, the most complex function in the game. Basically, it goes through each cell in the board and assigns it a priority score according to a few different factors:
- One point for each row, column and diagonal the cell is in
- In a 3×3, the centre cell gets 4 points, the corners get 3 and the rest get 2.
- If we’re just missing 1 cell for a win, add 5 points to bump it to the top
- Add 1 point for each of the opponent’s symbols in that row/col/diag to block them from winning
Basically, it goes through the whole of the board’s empty cells and for each one of those cells, it scans the whole board to determine its score.
Finally, the function searches for the cell with the highest score and fills it up. If there’s a tie, it picks the first one.
checkWin()
int checkWin(int *p, int size) {
int i,j; // Loop iterators
int hx,ho; // Horizontal counters
int vx,vo; // Vertical counters
int hval, vval; // Temp storage for stuff
int dx1, dx2, do1, do2;
int total = 0;
dx1 = dx2 = do1 = do2 = 0;
// Loop through rows and columns
for (i = 0; i < size; i++) {
// Reset
hx = ho = vx = vo = 0;
// Loop through cells
for (j = 0; j < size; j++) {
// Get values for rows and columns
hval = *(p + (sizeof(char)*i*size) + (sizeof(char)*j));
vval = *(p + (sizeof(char)*j*size) + (sizeof(char)*i));
// Check for matches
if (hval == X) {
hx ++;
}
if (hval == O) {
ho ++;
}
if (vval == X) {
vx ++;
}
if (vval == O) {
vo ++;
}
// Check diagonals
// Downwards
if (i == j) {
if (hval == X) {
dx1 ++;
}
if (hval == O) {
do1 ++;
}
}
// Upwards
if (size - 1 - i == j) {
if (hval == X) {// It's an X
dx2 ++;
}
if (hval == O) {// It's an O
do2 ++;
}
}
}
// printf("Straights; Row: %i, hx: %i, ho: %i, vx: %i, vo: %i.\n", i, hx, ho, vx, vo);
// printf("Diagonals; Row: %i, dx1: %i, do1: %i, dx2: %i, do2: %i.\n", i, dx1, do1, dx2, do2);
if (hx == size || vx == size || dx1 == size || dx2 == size) {
return X;
} else if (ho == size || vo == size || do1 == size || do2 == size) {
return O;
}
total += hx + ho;
}
// Check if board is full
// printf("Total: %i",total);
if (total == size * size) {
return 1;
}
return 0;
}
As the name suggests, the checkWin function checks the board to see if any rows, columns or diagonals are filled with Xs or Os. If it is, it returns the winner, otherwise a full board returns one and a continuing game returns zero.
Conclusion
Making this Tic-Tac-Toe game in C was quite a lot of fun and a bit of a learning experience, particularly dealing with a 2D array and pointers. It is very basic and doesn’t require a lot of libraries so it’s fairly lightweight too.
There is a way to beat the game’s AI in 3×3. I don’t want to give too much away, but it involves making it choose a corner when it should have picked an edge cell.
There are a few idea I have to extend the game such as having two AIs duke it out against each other, have more than 2 players or not require full rows to win. This I’m not planning an update soon, but it might be a fun thing to include in the future.