C++ Tic-Tac-Toe Project
Two players, one 3×3 grid — the first project on the Tic-Tac-Toe path.
What you will build
A two-player Tic-Tac-Toe game on a 3×3 board drawn in the console. The win-checking logic — rows, columns, diagonals — is a perfect workout in array indexing, and this humble game is step one of a three-project path that ends with networked multiplayer.
What you will learn
- Representing a board as a 2D array
- Writing a win-detection function
- Validating moves (occupied cell, out of range)
- Structuring a turn-based game loop
Feature checklist
- Draws the board after every move
- Two players alternate X and O
- Detects wins and draws correctly
- Rejects invalid moves without losing the turn
- Offers a rematch at the end
Starter code
#include <iostream>
#include <array>
using Board = std::array<std::array<char, 3>, 3>;
bool wins(const Board& b, char p) {
for (int i = 0; i < 3; ++i) {
if (b[i][0] == p && b[i][1] == p && b[i][2] == p) return true; // row
if (b[0][i] == p && b[1][i] == p && b[2][i] == p) return true; // col
}
if (b[0][0] == p && b[1][1] == p && b[2][2] == p) return true;
if (b[0][2] == p && b[1][1] == p && b[2][0] == p) return true;
return false;
}
void draw(const Board& b) {
for (auto& row : b) {
std::cout << " " << row[0] << " | " << row[1] << " | " << row[2] << "\n";
std::cout << "---+---+---\n";
}
}
// main(): fill board with ' ', alternate players,
// read row/col, place mark, check wins() after each move.Full walkthrough
Step 1: Represent the board as data first
The starter defines using Board = std::array<std::array<char, 3>, 3> — a 3×3 grid of chars where ' ' means empty, 'X' and 'O' are the marks. Naming the type Board makes every function signature readable, and std::array gives you .fill() and safe copying with no manual memory work.
Step 2: Write draw() before any game logic
draw(const Board& b) walks the rows and prints ' X | O | ' style lines with a '---+---+---' separator between them. Build this first and call it on an empty board from main() — seeing the grid on screen proves your data layout before a single rule exists, which is exactly how you keep debugging sessions short.
void draw(const Board& b) {
for (auto& row : b) {
std::cout << " " << row[0] << " | " << row[1]
<< " | " << row[2] << "\n";
std::cout << "---+---+---\n";
}
}Step 3: Write wins() — the rule engine
wins(const Board& b, char p) returns true if player p owns any full line. One loop checks row i and column i together (that symmetry halves the code), then the two diagonals are checked explicitly. Writing it as a pure function of (board, player) matters: the AI and multiplayer versions later in this catalogue reuse this exact shape.
Step 4: Detect a draw separately from a win
A draw is not 'nobody won yet' — it is 'the board is full AND wins() is false for both players'. Count the non-space cells after each move; at 9 with no winner, declare the draw. Checking wins() first is essential, because the 9th move can also be the winning move.
Step 5: Build the turn loop in main()
Fill the board with spaces, set char turn = 'X', and loop: draw the board, prompt 'Player X, row and column (0-2): ', read two ints, and validate before placing. After a legal move, check wins() for the player who just moved, then swap turn with turn = (turn == 'X' ? 'O' : 'X').
Step 6: Validate moves without stealing the turn
A move is legal only if row and column are both within 0–2 AND the target cell is ' '. If it is illegal, print why ('That cell is taken.') and loop again WITHOUT swapping the turn — losing your move because of a typo feels unfair, and the checklist explicitly demands the retry behaviour.
if (r < 0 || r > 2 || c < 0 || c > 2 || b[r][c] != ' ') {
std::cout << "Invalid move — try again.\n";
continue; // same player moves again
}Step 7: End the game properly and offer a rematch
When wins() fires, draw the final board once more and announce 'Player X wins!' — players want to SEE the winning line, not just hear about it. Then ask 'Rematch? (y/n)': on 'y', refill the board with spaces and reset the turn to 'X'; on anything else, exit the loop and say goodbye.
Common mistakes
- Checking only rows and columns and forgetting the two diagonals — the bug every first version of this game ships with.
- Swapping the turn before validating the move, so an invalid entry hands the turn to the opponent.
- Using 1–3 input but indexing the array directly with it, walking off the board (subtract 1, or prompt for 0–2 like the starter).
Compile and run
g++ -std=c++17 -o tic_tac_toe tic_tac_toe.cppSample run
$ ./tic_tac_toe
| |
---+---+---
| X |
---+---+---
| |
Player X opened at 1,1 (centre). Player O, row and column (0-2):
> 0 0
Player X, row and column (0-2):
> 0 2
Player O, row and column (0-2):
> 2 2
Player X, row and column (0-2):
> 2 0
O | | X
---+---+---
| X |
---+---+---
X | | O
Player X wins! (the diagonal 0,2 - 1,1 - 2,0)
Rematch? (y/n):
> nTry it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.