Kelick Tech Hub

Build, learn & keep up — tech, Linux & sports news included.

C++ Multiplayer Tic-Tac-Toe Project

Advanced•4–6 days•sockets, client-server, protocols

The Tic-Tac-Toe path finale: two players, two computers, one network.

Share on X

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

Feature checklist

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

Compile and run

g++ -std=c++17 -o tictactoe_mp tictactoe_mp.cpp -pthread

Sockets 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.

Related Advanced projects

← All 60 projects · Classic site (Learn & Sandbox)

Download Fake GPS