C
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Functions
-
Section 4: Arrays and Strings
-
Section 5: Pointers
-
Section 6: Memory Management
-
Section 7: Structures and Unions
-
Section 8: The Preprocessor and Build Process
-
Section 9: Standard Library: stdio.h
-
Section 10: Standard Library: stdlib.h
-
Section 11: Standard Library: string.h
-
Section 12: Standard Library: ctype.h and wctype.h
-
Section 13: Standard Library: math.h, complex.h, fenv.h, tgmath.h
-
Section 14: Standard Library: Type and Limit Headers
-
Section 15: Standard Library: Error Handling and Debugging
-
Section 16: Standard Library: Localization and Encoding
-
Section 17: Standard Library: time.h
-
Section 18: Standard Library: Concurrency (C11)
-
Section 19: POSIX and System Programming (unistd.h)
-
Section 20: More Data Structures
-
Section 21: Algorithms in C
-
Section 22: Bitwise Operations
-
Section 23: Command-Line Programs
-
Section 24: Debugging and Best Practices
-
Section 25: Compiler and Language Internals
-
Section 26: Embedded and Cross-Platform Considerations
-
Section 27: Networking Basics
-
Section 28: Practical Projects
-
Section 29: Interview Practice
-
Section 30: C23 Modern Features
-
Section 31: More Practice and Review
225: Building a Tic-Tac-Toe Game
Up until now, we've been dealing with isolated concepts—loops here, arrays there. It's time to smash them all together. We're going to build a command-line Tic-Tac-Toe game. It's a classic for a reason: it forces you to think about state management and how to translate a visual grid into data the computer understands.
Mapping the Board to a 2D Array
First, we need a way to represent the 3x3 grid. While you could use a single array of 9 elements, a 2D array is much more intuitive here because it maps directly to the rows and columns of the actual game. I'll use a char array since we only care about 'X', 'O', or an empty space.
char board[3][3] = {
{' ', ' ', ' '},
{' ', ' ', ' '},
{' ', ' ', ' '}
};
I'm initializing these with spaces rather than null terminators or zeros. It makes printing the board to the console much cleaner, as we can just print the character and a pipe symbol | to create a visual grid.
Handling Player Turns and the "Overwriting" Bug
The core of the game is a loop that keeps running until someone wins. I'll use a variable to track whose turn it is, swapping between 'X' and 'O' after every valid move. Here is how I initially approached the input logic:
int row, col;
printf("Enter row and col (0-2): ");
scanf("%d %d", &row, &col);
board[row][col] = currentPlayer;
I ran this version for a second and immediately realized I'd made a rookie mistake: I wasn't checking if the spot was already taken. My 'X' player could just move into 'O's square and overwrite it. That's not much of a game. I need to wrap that assignment in a conditional check to ensure the square is still a space before allowing the move.
if (board[row][col] == ' ') {
board[row][col] = currentPlayer;
// Now we can switch players
currentPlayer = (currentPlayer == 'X') ? 'O' : 'X';
} else {
printf("That spot is already taken! Try again.\n");
}
Logic for Detecting a Win
This is the part where most people get bogged down in nested loops. To keep things readable, I'm going to move the win-check logic into its own function. I need to check three things: every row, every column, and the two diagonals.
For the rows and columns, I can use a simple loop. For the diagonals, it's easier to just hard-code the indices since there are only two of them. Here is the logic I settled on:
int checkWin() {
// Check rows and columns
for (int i = 0; i < 3; i++) {
if (board[i][0] == board[i][1] && board[i][1] == board[i][2] && board[i][0] != ' ') return 1;
if (board[0][i] == board[1][i] && board[1][i] == board[2][i] && board[0][i] != ' ') return 1;
}
// Check diagonals
if (board[0][0] == board[1][1] && board[1][1] == board[2][2] && board[0][0] != ' ') return 1;
if (board[0][2] == board[1][1] && board[1][1] == board[2][0] && board[0][2] != ' ') return 1;
return 0;
}
Note that I'm checking board[i][0] != ' '. If I don't do that, the function will return a win as soon as the game starts because three empty spaces technically "match."
Polishing the Game Loop
Now we just tie it all together. We need a function to print the board so the players can actually see where they are moving. I'll use a nested loop to iterate through the 2D array and print the characters with some basic formatting.
void printBoard() {
for (int i = 0; i < 3; i++) {
printf(" %c | %c | %c \n", board[i][0], board[i][1], board[i][2]);
if (i < 2) printf("---|---|---\n");
}
}
By combining the input loop, the overwrite check, the win condition, and the print function, we have a fully functional game. It's not flashy, but it's logically sound. You've now managed state, handled user input errors, and implemented a search algorithm for the win condition—all in one project.
📋 Practical Task
Implementing a Draw Condition for Tic-Tac-Toe
Currently, if no one wins and the board fills up, the game continues indefinitely or crashes depending on how the loop is structured. Your task is to modify the game logic to detect a "Draw".
Implement a mechanism (either a counter that tracks total moves or a function that scans the board for any remaining spaces) that triggers a "Draw!" message and terminates the game if all 9 squares are filled and checkWin() still returns 0.
There are no comments for now.