C Project: Tic-Tac-Toe Game
Updated 13 min read Aimerscrew
Building tic tac toe in C is a great way to put 2D arrays, nested loops and functions to work in a real game. Two players share the keyboard, take turns typing a cell number from 1 to 9, and the program draws the board, rejects bad moves, spots a winner or a draw, and keeps score across games. You'll build it in four small steps, then put it all together.
🌍 Real-life example
Tic-tac-toe (called "noughts and crosses" in the UK) is played on napkins, whiteboards and fogged-up car windows all over the world. The rules fit in one sentence: get three in a row before your opponent. That makes it ideal for a first game, because all your effort goes into the code, not the rules.
What you'll build
Your game will:
- draw a 3 × 3 board with numbered cells,
- alternate turns between X and O,
- reject anything that isn't a number from 1 to 9, and cells already taken,
- detect a win on any row, column or diagonal, and a draw when the board is full,
- offer to play again and print the final score.
Here's how the screen looks at the start of a game. The player typed 5 after the prompt:
=== Tic-Tac-Toe ===
Type a cell number to place your mark.
1 | 2 | 3
---+---+---
4 | 5 | 6
---+---+---
7 | 8 | 9
Player X, choose a cell (1-9): 5
Concepts used in this project
| Concept | Where it's used | Lesson |
|---|---|---|
| 2D arrays | The board[3][3] | multidimensional arrays |
| Nested loops | Filling, printing and checking the board | nested loops |
| Passing arrays to functions | Every function receives the board | arrays and functions |
/ and % | Turning cell 1-9 into a row and column | arithmetic operators |
char arithmetic | '1' + 1 is '2' | data types |
| Logical operators | Three-in-a-row checks | logical operators |
| Ternary operator | Switching between X and O | ternary operator |
Step 1: Create and print the board
The board is a char board[3][3]: 3 rows, 3 columns. Instead of filling it with spaces, we fill it with the characters '1' to '9'. That way the empty board shows players exactly which number to type. When someone moves, their 'X' or 'O' replaces the number.
#include <stdio.h>
/* Fill the board with the cell numbers '1' to '9'. */
void init_board(char board[3][3]) {
char cell = '1';
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
board[row][col] = cell;
cell++;
}
}
}
void print_board(char board[3][3]) {
printf("\n");
for (int row = 0; row < 3; row++) {
printf(" %c | %c | %c\n", board[row][0], board[row][1], board[row][2]);
if (row < 2) {
printf("---+---+---\n");
}
}
}
int main(void) {
char board[3][3];
init_board(board);
board[1][1] = 'X';
board[0][2] = 'O';
printf("Board after two moves:");
print_board(board);
return 0;
}
Board after two moves:
1 | 2 | O
---+---+---
4 | X | 6
---+---+---
7 | 8 | 9
Characters are stored as small numbers, so cell++ turns '1' into '2', and so on up to '9'. The divider line only appears between rows, thanks to if (row < 2). print_board() prints "\n" first, which ends the line above it. In the full game, that newline leaves an empty line between each board and the text before it.
Step 2: How do you turn cell 1-9 into a row and column?
Players think in cells 1 to 9, but the array needs a row and a column from 0 to 2. Subtract 1 to get 0 to 8, then use division and remainder:
row = (cell - 1) / 3(integer division drops the fraction),col = (cell - 1) % 3(the remainder).
For cell 6: (6 - 1) / 3 is 1 and (6 - 1) % 3 is 2, so cell 6 is board[1][2].
The function that reads a move uses this mapping to check whether the cell is free. It loops until the player gives a valid answer:
/* Asks the player for a free cell. Returns 1-9, or 0 if the input has ended. */
int read_move(char board[3][3], char player) {
int cell;
int status;
while (1) {
printf("Player %c, choose a cell (1-9): ", player);
status = scanf("%d", &cell);
if (status == EOF) {
return 0;
}
clear_input();
if (status != 1 || cell < 1 || cell > 9) {
printf("Please type a number from 1 to 9.\n");
continue;
}
int row = (cell - 1) / 3;
int col = (cell - 1) % 3;
if (board[row][col] == 'X' || board[row][col] == 'O') {
printf("Cell %d is already taken. Pick another.\n", cell);
continue;
}
return cell;
}
}
The order of the checks matters. Because || stops at the first true part, cell < 1 is never tested when scanf() failed, and the array is only indexed after we know cell is between 1 and 9. Indexing first could read outside the array. clear_input() is the same helper used in the earlier projects: it discards the rest of the line.
Step 3: How do you check for a win or a draw?
A player wins with three of their marks in a row, a column or a diagonal. That's eight lines in total. One loop checks row i and column i together, and two extra checks cover the diagonals. A draw happens when the board is full and nobody has won, so a second function looks for any cell that still holds a number. This test program runs both functions on two finished boards:
#include <stdio.h>
/* Returns 1 if player has three in a row anywhere on the board. */
int is_winner(char board[3][3], char player) {
for (int i = 0; i < 3; i++) {
if (board[i][0] == player && board[i][1] == player && board[i][2] == player) {
return 1; /* row i */
}
if (board[0][i] == player && board[1][i] == player && board[2][i] == player) {
return 1; /* column i */
}
}
if (board[0][0] == player && board[1][1] == player && board[2][2] == player) {
return 1; /* diagonal from top-left */
}
if (board[0][2] == player && board[1][1] == player && board[2][0] == player) {
return 1; /* diagonal from top-right */
}
return 0;
}
/* Returns 1 if every cell holds an X or an O. */
int is_board_full(char board[3][3]) {
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
if (board[row][col] != 'X' && board[row][col] != 'O') {
return 0;
}
}
}
return 1;
}
int main(void) {
char won[3][3] = {
{'X', 'O', 'O'},
{'O', 'X', '6'},
{'X', '8', 'X'}
};
char drawn[3][3] = {
{'X', 'O', 'X'},
{'X', 'O', 'O'},
{'O', 'X', 'X'}
};
printf("Board 1: X wins? %d, O wins? %d, full? %d\n",
is_winner(won, 'X'), is_winner(won, 'O'), is_board_full(won));
printf("Board 2: X wins? %d, O wins? %d, full? %d\n",
is_winner(drawn, 'X'), is_winner(drawn, 'O'), is_board_full(drawn));
return 0;
}
Board 1: X wins? 1, O wins? 0, full? 0
Board 2: X wins? 0, O wins? 0, full? 1
On board 1, X holds [0][0], [1][1] and [2][2], the top-left diagonal. Board 2 is full with no line for either player, which is a draw. Always check for a win before checking for a full board: the ninth move can complete a line and fill the board at the same time, and that's a win, not a draw.
Step 4: The game loop and play again
play_game() runs one game. It draws the board, reads a move, places the mark, checks for a win or a draw, and then switches players with the ternary operator. It returns 'X' or 'O' for the winner, 'D' for a draw, or 'Q' if the input ended:
char play_game(void) {
char board[3][3];
char player = 'X';
int cell;
init_board(board);
while (1) {
print_board(board);
cell = read_move(board, player);
if (cell == 0) {
return 'Q';
}
board[(cell - 1) / 3][(cell - 1) % 3] = player;
if (is_winner(board, player)) {
print_board(board);
printf("Player %c wins!\n", player);
return player;
}
if (is_board_full(board)) {
print_board(board);
printf("It's a draw!\n");
return 'D';
}
player = (player == 'X') ? 'O' : 'X';
}
}
Notice that board is a local variable of play_game(). Every new game gets a fresh board, so there's nothing to reset. main() then calls play_game() in a loop, adds the result to the score and asks whether to play again.
The complete tic tac toe game in C
Save it as tictactoe.c, compile with gcc -std=c17 -Wall -Wextra tictactoe.c -o tictactoe and run ./tictactoe.
#include <stdio.h>
/* Throw away the rest of the current input line. */
void clear_input(void) {
int ch;
while ((ch = getchar()) != '\n' && ch != EOF) {
}
}
/* Fill the board with the cell numbers '1' to '9'. */
void init_board(char board[3][3]) {
char cell = '1';
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
board[row][col] = cell;
cell++;
}
}
}
void print_board(char board[3][3]) {
printf("\n");
for (int row = 0; row < 3; row++) {
printf(" %c | %c | %c\n", board[row][0], board[row][1], board[row][2]);
if (row < 2) {
printf("---+---+---\n");
}
}
}
/* Returns 1 if player has three in a row anywhere on the board. */
int is_winner(char board[3][3], char player) {
for (int i = 0; i < 3; i++) {
if (board[i][0] == player && board[i][1] == player && board[i][2] == player) {
return 1; /* row i */
}
if (board[0][i] == player && board[1][i] == player && board[2][i] == player) {
return 1; /* column i */
}
}
if (board[0][0] == player && board[1][1] == player && board[2][2] == player) {
return 1; /* diagonal from top-left */
}
if (board[0][2] == player && board[1][1] == player && board[2][0] == player) {
return 1; /* diagonal from top-right */
}
return 0;
}
/* Returns 1 if every cell holds an X or an O. */
int is_board_full(char board[3][3]) {
for (int row = 0; row < 3; row++) {
for (int col = 0; col < 3; col++) {
if (board[row][col] != 'X' && board[row][col] != 'O') {
return 0;
}
}
}
return 1;
}
/* Asks the player for a free cell. Returns 1-9, or 0 if the input has ended. */
int read_move(char board[3][3], char player) {
int cell;
int status;
while (1) {
printf("Player %c, choose a cell (1-9): ", player);
status = scanf("%d", &cell);
if (status == EOF) {
return 0;
}
clear_input();
if (status != 1 || cell < 1 || cell > 9) {
printf("Please type a number from 1 to 9.\n");
continue;
}
int row = (cell - 1) / 3;
int col = (cell - 1) % 3;
if (board[row][col] == 'X' || board[row][col] == 'O') {
printf("Cell %d is already taken. Pick another.\n", cell);
continue;
}
return cell;
}
}
/* Plays one game. Returns 'X' or 'O' for the winner, 'D' for a draw,
or 'Q' if the input ended. */
char play_game(void) {
char board[3][3];
char player = 'X';
int cell;
init_board(board);
while (1) {
print_board(board);
cell = read_move(board, player);
if (cell == 0) {
return 'Q';
}
board[(cell - 1) / 3][(cell - 1) % 3] = player;
if (is_winner(board, player)) {
print_board(board);
printf("Player %c wins!\n", player);
return player;
}
if (is_board_full(board)) {
print_board(board);
printf("It's a draw!\n");
return 'D';
}
player = (player == 'X') ? 'O' : 'X';
}
}
int main(void) {
int x_wins = 0;
int o_wins = 0;
int draws = 0;
char result;
char answer;
printf("=== Tic-Tac-Toe ===\n");
printf("Type a cell number to place your mark.\n");
while (1) {
result = play_game();
if (result == 'Q') {
break;
}
if (result == 'X') {
x_wins++;
} else if (result == 'O') {
o_wins++;
} else {
draws++;
}
printf("\nPlay again? (y/n): ");
if (scanf(" %c", &answer) != 1) {
break;
}
clear_input();
if (answer != 'y' && answer != 'Y') {
break;
}
}
printf("Final score: X %d, O %d, draws %d\n", x_wins, o_wins, draws);
return 0;
}
Sample run
Two players share the keyboard. Every number after choose a cell (1-9): and the final n were typed by the players. Along the way, O tries the taken cell 5 and X types 10, which is out of range. X wins with the diagonal 1-5-9 after setting up two threats at once.
=== Tic-Tac-Toe ===
Type a cell number to place your mark.
1 | 2 | 3
---+---+---
4 | 5 | 6
---+---+---
7 | 8 | 9
Player X, choose a cell (1-9): 5
1 | 2 | 3
---+---+---
4 | X | 6
---+---+---
7 | 8 | 9
Player O, choose a cell (1-9): 5
Cell 5 is already taken. Pick another.
Player O, choose a cell (1-9): 2
1 | O | 3
---+---+---
4 | X | 6
---+---+---
7 | 8 | 9
Player X, choose a cell (1-9): 10
Please type a number from 1 to 9.
Player X, choose a cell (1-9): 7
1 | O | 3
---+---+---
4 | X | 6
---+---+---
X | 8 | 9
Player O, choose a cell (1-9): 3
1 | O | O
---+---+---
4 | X | 6
---+---+---
X | 8 | 9
Player X, choose a cell (1-9): 1
X | O | O
---+---+---
4 | X | 6
---+---+---
X | 8 | 9
Player O, choose a cell (1-9): 4
X | O | O
---+---+---
O | X | 6
---+---+---
X | 8 | 9
Player X, choose a cell (1-9): 9
X | O | O
---+---+---
O | X | 6
---+---+---
X | 8 | X
Player X wins!
Play again? (y/n): n
Final score: X 1, O 0, draws 0
If all nine cells fill up without a line, the last board is printed followed by It's a draw!, and the draw is added to the score.
Common mistakes in this project
1. Using the cell number directly as an index
Writing board[cell / 3][cell % 3] without subtracting 1 shifts every move one cell to the right, and cell 9 becomes board[3][0], which is outside the array. That's undefined behavior: it may corrupt other variables or crash.
2. Indexing before validating
Checking board[row][col] == 'X' before making sure cell is between 1 and 9 reads outside the array when a player types 0 or 42. Validate the range first.
3. Checking for a draw before a win
If is_board_full() runs first, a game won on the ninth move is reported as a draw. Check is_winner() first.
4. Using = instead of == in the checks
if (board[0][0] = player) assigns the player's mark to the cell and is always true. GCC with -Wall warns "suggest parentheses around assignment used as truth value"; treat that warning as an error.
5. Forgetting to switch players
Without player = (player == 'X') ? 'O' : 'X';, X moves every turn. Place the switch at the end of the loop, after the win and draw checks, so the winner's name is still correct when you print it.
Ideas to extend it
🏋️ Challenges
- Take turns starting. Let the loser of the previous game, or O on every second game, move first. Pass the starting player to
play_game(). - Player names. Ask for both names at the start and print "Maya wins!" instead of "Player X wins!".
- Simple computer player. Let O pick a random free cell with
rand(), as in the number guessing game. - Smarter computer. Before moving randomly, the computer checks each free cell: if placing O there wins, take it; if placing X there would let X win, block it.
- Any size board. Use a
#define SIZE 4constant and rewriteis_winner()with loops so it works for any square board.
Show a hint for challenge 3
/* Returns a random free cell from 1 to 9 (call srand() once in main). */
int computer_move(char board[3][3]) {
int cell;
do {
cell = rand() % 9 + 1;
} while (board[(cell - 1) / 3][(cell - 1) % 3] == 'X' ||
board[(cell - 1) / 3][(cell - 1) % 3] == 'O');
return cell;
}
Only call it when the board isn't full; otherwise the loop never finds a free cell. Remember #include <stdlib.h> for rand().
What's next?
You've finished all five projects: a calculator, a guessing game, a records manager, a bank system and now a game with a 2D board. Time to sharpen your skills with focused practice problems in C exercises.
Key takeaways
- Tic tac toe in C uses a char board[3][3]; filling it with '1' to '9' shows players which number to type.
- Convert a cell number to array indexes with row = (cell - 1) / 3 and col = (cell - 1) % 3.
- Validate the cell range before indexing the array, then check that the cell is free.
- There are exactly eight winning lines: three rows, three columns and two diagonals.
- Check for a win before checking for a full board, so a win on the ninth move is not called a draw.
- Keeping the board local to play_game() gives every new game a fresh board automatically.
Frequently asked questions
How do you make tic tac toe in C?
Store the board in a char board[3][3], print it with nested loops, read each move as a cell number from 1 to 9, convert it to a row and column, place X or O, then check all eight winning lines and whether the board is full. Repeat until someone wins or the game is a draw.
How do you check for a winner in tic tac toe in C?
Loop i from 0 to 2 and check row i (board[i][0], board[i][1], board[i][2]) and column i (board[0][i], board[1][i], board[2][i]). Then check the two diagonals, board[0][0], board[1][1], board[2][2] and board[0][2], board[1][1], board[2][0].
How do you detect a draw in tic tac toe?
After checking that the last move did not win, look at every cell. If none still holds a number (every cell is X or O), the board is full and the game is a draw.
Why use a 2D array for the tic tac toe board?
A 2D array matches the shape of the board: board[row][col] reads naturally, and rows, columns and diagonals become simple index patterns. A 1D array of 9 cells also works, but the winning-line checks are harder to read.
How do I add a computer opponent to my C tic tac toe game?
Start with a function that picks a random free cell using rand(). Then improve it: first look for a move that wins, then a move that blocks the opponent, and only then choose randomly. A perfect player can be built with the minimax algorithm, which uses recursion.