C++ Multiplayer Tic-Tac-Toe Project
The Tic-Tac-Toe path finale: two players, two computers, one network.
What you will build
Two players on different machines play Tic-Tac-Toe over a network: a server hosts the game, validates every move, and keeps both boards in sync. This completes the Tic-Tac-Toe path — same rules as the beginner version, now distributed — and teaches client-server game architecture.
What you will learn
- Client-server design over TCP
- Designing a text protocol (MOVE 1 2, STATE ...)
- Keeping authoritative state on the server
- Handling disconnects mid-game
Feature checklist
- Server hosts; two clients connect
- Server assigns X and O, enforces turn order
- Moves validated server-side (no cheating clients)
- Both boards update after every move
- Win/draw announced to both players
- Rematch without reconnecting
Starter code
// Server-side game room (sockets abstracted as sendLine/recvLine).
#include <string>
#include <array>
using Board = std::array<std::array<char, 3>, 3>;
struct Room {
Board b{}; int turn = 0; // 0 plays X, 1 plays O
Room() { for (auto& row : b) row.fill(' '); }
bool applyMove(int player, int r, int c) {
if (player != turn) return false;
if (r < 0 || r > 2 || c < 0 || c > 2) return false;
if (b[r][c] != ' ') return false;
b[r][c] = (player == 0 ? 'X' : 'O');
turn = 1 - turn;
return true;
}
std::string state() const { // "X O X ..." for clients to draw
std::string s;
for (auto& row : b) for (char ch : row) s += ch;
return s;
}
};
// Protocol sketch:
// client -> server : "MOVE 1 2"
// server -> both : "STATE X O..." then "TURN 0" or "END X"Full walkthrough
Step 1: Reuse the board, change who is trusted
The Room struct holds the same Board type as steps 1 and 2 of this path, plus turn (0 = X, 1 = O). What changes is philosophy: in the earlier projects the program trusted the player; here clients are UNtrusted, so the server owns the only true board and every move is a request the server may refuse.
Step 2: applyMove(): the server as referee
Room::applyMove(player, r, c) returns false unless: it IS that player's turn, r and c are within 0–2, and the cell is empty. Only then place the mark, flip the turn, and report the new state. Every validation from the beginner game lives here — but now a false return means 'reject and tell the client why', not 'prompt again'.
bool Room::applyMove(int player, int r, int c) {
if (player != turn) return false;
if (r < 0 || r > 2 || c < 0 || c > 2) return false;
if (b[r][c] != ' ') return false;
b[r][c] = (player == 0 ? 'X' : 'O');
turn = 1 - turn;
return true;
}Step 3: Host: accept exactly two clients, assign marks
The server listens (the chat project's socket code), accepts two connections, and sends each an assignment line: 'YOU X' or 'YOU O'. The first client waits in a lobby state — send 'WAITING' so their screen explains the pause. A third connection is refused politely; a 2-player room with a silent third wheel is a bug farm.
Step 4: Define a tiny text protocol
Lines, both directions: client → server: 'MOVE r c', 'REMATCH', 'QUIT'. Server → clients: the board as a 9-char string after every accepted move, 'TURN X', 'WIN X' / 'DRAW', 'ERR <reason>'. Text protocols are debuggable with telnet and your own eyes — for a learning project that beats any binary format.
Step 5: Broadcast state, never deltas
After each accepted move the server sends the FULL board to both clients, not 'X played centre' — clients render whatever the server says, so a client that missed a message heals on the next move. State-sync over event-sync is the design decision that keeps multiplayer code sane at this scale.
Step 6: Win, draw, and the rematch handshake
The server runs the same wins() logic after each move and announces the result to both. For a rematch, BOTH clients must send REMATCH before the Room resets (board filled with spaces, turn back to X, optionally swap who is X). One-sided resets desync the room instantly.
Step 7: Test with three terminals and bad behaviour
Terminal 1: server. Terminals 2 and 3: clients. Then attack it: move out of turn (rejected), move onto an occupied cell (rejected), have O send coordinates 9 9 (rejected), quit mid-game from one client (the other is told the opponent left). A multiplayer program is only as good as its refusals.
Common mistakes
- Letting clients keep their own board and merely exchange moves — the first dropped or forged message forks reality into two games.
- Validating moves on the client 'to save time' — a modified client then plays anywhere, any time; the server must referee.
- Resetting the room when only one player asked for a rematch, leaving the other staring at a finished game that vanished.
Compile and run
g++ -std=c++17 -o tictactoe_mp tictactoe_mp.cpp -pthreadSockets again: POSIX on Linux/macOS as-is; Windows needs the Winsock port (see the console Chat App lecture). -pthread covers any std::thread use. Loopback (127.0.0.1) is the honest test environment — two client terminals, one server.
Sample run
$ ./tictactoe_mp server
Server listening on port 5555. Waiting for 2 players...
Player 1 connected — assigned X
Player 2 connected — assigned O. Game on!
X plays (1,1)
O plays (0,0)
O sent MOVE 1 1 — rejected (cell occupied), turn unchanged
O plays (2,2)
X plays (0,2)
O plays (2,1)
X plays (2,0)
X wins — the diagonal (0,2) (1,1) (2,0). Announced to both players.
Player 2 agrees — new game, marks swapped.
——— player 2's screen ———
$ ./tictactoe_mp client 127.0.0.1
You are O. X moves first.
Board: O . X / . X . / . . .
Your move (row col):
> 2 2
Board: O . X / . X . / . . O
X plays (2,0) — X wins. Rematch needs both players to agree.Try it in your browser
No browser demo is included for this project. Build the C++ version below — that is the real program.