Mastering Tic-Tac-Toe in C: A 2D Array Approach
Welcome back to our C Language Series! In entry #185, we're diving into a classic programming challenge that perfectly illustrates the power and utility of 2D arrays: implementing the game of Tic-Tac-Toe. This timeless game, played on a 3x3 grid, provides an excellent practical exercise for understanding array manipulation, game logic, user input handling, and function design in C.
By the end of this post, you'll have a fully functional command-line Tic-Tac-Toe game, built from the ground up, that you can play with a friend. Let's get started!
Why 2D Arrays for Tic-Tac-Toe?
Tic-Tac-Toe is inherently a grid-based game. A 2D array, which is essentially an array of arrays, provides the most intuitive and efficient way to model this grid structure in C. Each cell in our 3x3 game board can be represented by an element at a specific row and column index within the 2D array.
- Visual Representation: A
char board[3][3];directly mirrors the 3x3 visual layout. - Easy Access: Accessing a cell at (row, column) is straightforward:
board[row][column]. - Simplified Logic: Checking for win conditions (rows, columns, diagonals) becomes a series of simple loop iterations and conditional checks.
Game Logic Breakdown
To build our Tic-Tac-Toe game, we'll break it down into several manageable functions, each responsible for a specific aspect of the game:
- Board Representation: A
char2D array to store 'X', 'O', or ' ' (empty). - Initialization: A function to set all cells to empty.
- Display: A function to print the current state of the board to the console.
- Player Move: A function to get valid input from the current player and update the board.
- Win Condition Check: A function to determine if a player has won.
- Draw Condition Check: A function to determine if the game is a draw.
- Main Game Loop: Orchestrates the turns, checks for win/draw, and switches players.
Step-by-Step Implementation
Let's define the core functions we'll need.
1. Global Board and Player Variables
We'll use a global 2D array for the board and a few other global variables to manage game state, making them accessible across all functions without needing to pass them repeatedly.
#include <stdio.h>
#include <stdlib.h> // For system("cls") or system("clear")
char board[3][3]; // The 3x3 Tic-Tac-Toe board
char currentPlayer = 'X'; // Start with Player X
int totalMoves = 0; // To track for a draw condition
2. initializeBoard()
This function fills every cell of the board with a space character, signifying an empty spot.
void initializeBoard() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
board[i][j] = ' ';
}
}
totalMoves = 0; // Reset moves for a new game
}
3. displayBoard()
This function prints the current state of the game board to the console, making it easy for players to visualize the game.
void displayBoard() {
// Clear screen for better display (optional, OS-dependent)
// #ifdef _WIN32
// system("cls");
// #else
// system("clear");
// #endif
printf("\n");
printf(" %c | %c | %c\n", board[0][0], board[0][1], board[0][2]);
printf(" ---|---|---\n");
printf(" %c | %c | %c\n", board[1][0], board[1][1], board[1][2]);
printf(" ---|---|---\n");
printf(" %c | %c | %c\n", board[2][0], board[2][1], board[2][2]);
printf("\n");
}
4. playerMove()
This function prompts the current player for their move (row and column), validates the input, and updates the board array. It ensures moves are within bounds and into empty cells.
void playerMove() {
int row, col;
while (1) {
printf("Player %c, enter your move (row and column, e.g., 1 2): ", currentPlayer);
if (scanf("%d %d", &row, &col) != 2) {
printf("Invalid input. Please enter two numbers.\n");
// Clear input buffer
while (getchar() != '\n');
continue;
}
row--; // Adjust to 0-indexed
col--; // Adjust to 0-indexed
if (row < 0 || row > 2 || col < 0 || col > 2) {
printf("Invalid move. Row and column must be between 1 and 3.\n");
} else if (board[row][col] != ' ') {
printf("Invalid move. That cell is already taken. Try again.\n");
} else {
board[row][col] = currentPlayer;
totalMoves++;
break; // Valid move, exit loop
}
}
}
5. checkWin()
This crucial function checks all possible win conditions: three rows, three columns, and two diagonals. It returns 1 if the current player has won, otherwise 0.
int checkWin() {
// Check rows
for (int i = 0; i < 3; i++) {
if (board[i][0] == currentPlayer && board[i][1] == currentPlayer && board[i][2] == currentPlayer) {
return 1;
}
}
// Check columns
for (int j = 0; j < 3; j++) {
if (board[0][j] == currentPlayer && board[1][j] == currentPlayer && board[2][j] == currentPlayer) {
return 1;
}
}
// Check main diagonal
if (board[0][0] == currentPlayer && board[1][1] == currentPlayer && board[2][2] == currentPlayer) {
return 1;
}
// Check anti-diagonal
if (board[0][2] == currentPlayer && board[1][1] == currentPlayer && board[2][0] == currentPlayer) {
return 1;
}
return 0; // No win
}
6. checkDraw()
A draw occurs if all 9 cells are filled (totalMoves == 9) and no player has won. This function returns 1 for a draw, 0 otherwise.
int checkDraw() {
return (totalMoves == 9 && !checkWin()); // If all moves made and no winner
}
7. switchPlayer()
A simple function to alternate between 'X' and 'O' players.
void switchPlayer() {
currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
}
The main() Game Loop
The main function brings all these pieces together. It initializes the board, enters a loop that continues until there's a winner or a draw, handles player turns, displays the board, and switches players.
int main() {
initializeBoard();
int gameEnded = 0;
printf("Welcome to Tic-Tac-Toe!\n");
printf("Player X goes first.\n");
do {
displayBoard();
playerMove();
if (checkWin()) {
displayBoard();
printf("Congratulations! Player %c wins!\n", currentPlayer);
gameEnded = 1;
} else if (checkDraw()) {
displayBoard();
printf("It's a draw!\n");
gameEnded = 1;
} else {
switchPlayer();
}
} while (!gameEnded);
printf("Thanks for playing!\n");
return 0;
}
Complete Tic-Tac-Toe C Code
Here's the full C program combining all the functions:
#include <stdio.h>
#include <stdlib.h> // For system("cls") or system("clear")
// Global variables for the game state
char board[3][3];
char currentPlayer = 'X';
int totalMoves = 0;
// Function prototypes
void initializeBoard();
void displayBoard();
void playerMove();
int checkWin();
int checkDraw();
void switchPlayer();
int main() {
initializeBoard();
int gameEnded = 0;
printf("Welcome to Tic-Tac-Toe!\n");
printf("Player X goes first.\n");
do {
displayBoard();
playerMove(); // Current player makes a move
if (checkWin()) {
displayBoard();
printf("Congratulations! Player %c wins!\n", currentPlayer);
gameEnded = 1;
} else if (checkDraw()) {
displayBoard();
printf("It's a draw!\n");
gameEnded = 1;
} else {
switchPlayer(); // Switch to the other player
}
} while (!gameEnded); // Continue until game ends
printf("Thanks for playing!\n");
return 0;
}
// Function to initialize the board with empty spaces
void initializeBoard() {
for (int i = 0; i < 3; i++) {
for (int j = 0; j < 3; j++) {
board[i][j] = ' ';
}
}
totalMoves = 0; // Reset move count
currentPlayer = 'X'; // Reset current player to X for a new game
}
// Function to display the current state of the board
void displayBoard() {
// Optional: Clear screen for a cleaner look
// On Windows: system("cls");
// On Linux/macOS: system("clear");
// Commented out to avoid potential issues on some systems or IDEs
printf("\n");
printf(" %c | %c | %c\n", board[0][0], board[0][1], board[0][2]);
printf(" ---|---|---\n");
printf(" %c | %c | %c\n", board[1][0], board[1][1], board[1][2]);
printf(" ---|---|---\n");
printf(" %c | %c | %c\n", board[2][0], board[2][1], board[2][2]);
printf("\n");
}
// Function to handle a player's move
void playerMove() {
int row, col;
while (1) {
printf("Player %c, enter your move (row and column, e.g., 1 2): ", currentPlayer);
// Ensure that scanf successfully reads two integers
if (scanf("%d %d", &row, &col) != 2) {
printf("Invalid input. Please enter two numbers separated by a space.\n");
// Clear the input buffer to prevent infinite loops from invalid input
while (getchar() != '\n');
continue; // Ask for input again
}
row--; // Adjust to 0-indexed (0, 1, 2)
col--; // Adjust to 0-indexed (0, 1, 2)
// Validate the move
if (row < 0 || row > 2 || col < 0 || col > 2) {
printf("Invalid move. Row and column must be between 1 and 3.\n");
} else if (board[row][col] != ' ') {
printf("Invalid move. That cell is already taken. Try again.\n");
} else {
board[row][col] = currentPlayer; // Place the player's mark
totalMoves++; // Increment move count
break; // Valid move, exit the loop
}
}
}
// Function to check if the current player has won
int checkWin() {
// Check rows
for (int i = 0; i < 3; i++) {
if (board[i][0] == currentPlayer && board[i][1] == currentPlayer && board[i][2] == currentPlayer) {
return 1; // Win found in a row
}
}
// Check columns
for (int j = 0; j < 3; j++) {
if (board[0][j] == currentPlayer && board[1][j] == currentPlayer && board[2][j] == currentPlayer) {
return 1; // Win found in a column
}
}
// Check main diagonal (top-left to bottom-right)
if (board[0][0] == currentPlayer && board[1][1] == currentPlayer && board[2][2] == currentPlayer) {
return 1;
}
// Check anti-diagonal (top-right to bottom-left)
if (board[0][2] == currentPlayer && board[1][1] == currentPlayer && board[2][0] == currentPlayer) {
return 1;
}
return 0; // No win condition met
}
// Function to check if the game is a draw
int checkDraw() {
// A draw occurs if all 9 moves have been made and there's no winner
return (totalMoves == 9 && !checkWin());
}
// Function to switch the current player
void switchPlayer() {
currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
}
Compiling and Running
To compile this code, save it as tictactoe.c and use a C compiler like GCC:
gcc tictactoe.c -o tictactoe
Then, run the executable:
./tictactoe
Further Enhancements and Learning
This basic Tic-Tac-Toe game is a great starting point. Here are some ideas for how you could expand upon it:
- Player vs. AI: Implement an AI opponent (e.g., using a minimax algorithm for an unbeatable AI).
- Error Handling: Add more robust error handling for user input (e.g., handling non-numeric input more gracefully).
- Replay Option: Ask players if they want to play again after a game ends.
- Custom Board Size: Modify the code to allow for an NxN board (though winning conditions would need a more generic check).
- GUI: Port the logic to a graphical user interface (GUI) using a library like GTK+ or SDL.
Conclusion
You've just built a complete Tic-Tac-Toe game in C, leveraging the fundamental concept of 2D arrays to represent the game board. This exercise not only solidifies your understanding of array manipulation but also demonstrates good programming practices like modularity through functions, input validation, and game loop design. Congratulations on reaching another milestone in your C programming journey!
Keep practicing, keep coding, and stay tuned for more exciting topics in our C Language Series!