Problem 02 · Complete walkthroughBeginner30 min

Tic-Tac-Toe

Design a two-player game that accepts legal moves, alternates turns, and stops with a win or draw.

Two players taking turns placing X and O marks on a three by three board
The whole game loop: one player chooses a cell, the board changes, the result is checked, and the next turn begins.

1. Requirements (~5 minutes)

The interview begins with one sentence:

“Design Tic-Tac-Toe.”

You probably know how Tic-Tac-Toe works. The danger is assuming that your version of the rules is the interviewer's version. The sentence does not tell us the board size, who starts, how invalid moves are reported, or whether features such as undo are required.

Before thinking about classes, turn the familiar game into a precise promise that our code can keep.

Use these four question groups every time

1
Actions

What must a user be able to do?

2
Rules

When does an action succeed, fail, or change state?

3
Errors

Which invalid actions must we reject?

4
Boundaries

What should we deliberately not build?

Ask questions and record what each answer changes

AskHow many players are there, and how are their marks assigned?

Why ask it?

This defines who can act and what data identifies a move.

Interviewer says

There are two local players. One uses X and the other uses O. X starts.

Write this down

Store two players, assign different marks, and make X the current player after creation or reset.

AskIs the board always 3 by 3? What exactly counts as a win?

Why ask it?

Board size and the completion rule decide the storage shape and win-check logic.

Interviewer says

Use a fixed 3 by 3 board. Three equal marks in a row, column, or diagonal wins.

Write this down

Board stores Mark[3][3] and checks 3 rows, 3 columns, and 2 diagonals.

AskWhen is the game a draw, and can moves continue after the game ends?

Why ask it?

This defines the game states and when the main action must stop.

Interviewer says

It is a draw when all nine cells are filled without a winner. Reject every move after a win or draw.

Write this down

GameStatus is IN_PROGRESS, X_WON, O_WON, or DRAW. makeMove checks status first.

AskWhich moves are invalid, and how should the caller learn why?

Why ask it?

Invalid actions should not partially change the board or switch the turn.

Interviewer says

Reject a wrong player, a cell outside the board, an occupied cell, or a move after completion. Return a clear result.

Write this down

makeMove returns MoveResult and changes state only after all checks pass.

AskDo we need a computer player, undo, score history, larger boards, or online play?

Why ask it?

Each feature creates new objects and rules. Excluding them prevents unnecessary design.

Interviewer says

No. Discuss them only if asked as extensions.

Write this down

The first version has two local human players, one match, fixed rules, no history, and no networking.

Confirm the specification

Confirmed specification

Requirements

  1. 1.Two local players use X and O; X takes the first turn.
  2. 2.Players alternate placing one mark on an empty cell of a fixed 3 by 3 board.
  3. 3.A row, column, or diagonal containing three equal marks wins.
  4. 4.A full board without a winner is a draw.
  5. 5.Reject the wrong player, out-of-range cells, occupied cells, and moves after completion.
  6. 6.Expose the board, current player, status, winner, and a reset operation.

Not building

  • Computer opponent
  • Undo or replay history
  • Score across matches
  • Variable board size
  • Online multiplayer
  • UI and storage

2. Entities and relationships (~3 minutes)

Now return to the confirmed requirements and look for candidate objects: game, player, board, cell, mark, and winning rule. We will not keep all of them as classes.

For each candidate, ask: what information changes here, and which rule needs that information? The class should be placed where the answer is clearest.

Board

Class

Where did it come from? Players place marks on a fixed 3 by 3 grid; occupied or out-of-range cells must be rejected; rows, columns, and diagonals can win.

Question to ask: Which rules can be answered by looking only at the cells?

Whether a coordinate exists, whether a cell is empty, whether three marks form a line, and whether the grid is full can all be decided from the cell array. The array changes after every accepted move.

Decision:

Create a Board class. It owns the cells and the rules that depend only on those cells. It does not decide whose turn it is.

Game

Class

Where did it come from? X starts, players alternate, moves stop after a win or draw, the winner is exposed, and the match can reset.

Question to ask: Who controls the order of a complete move?

The board cannot answer whether Alice is allowed to move now or whether the match already ended; that information belongs to the match as a whole. We need an object that checks the current player, asks the board to place a mark, then updates the result.

Decision:

Create a Game class as the public entry point. It owns turn order, status, and winner, and it coordinates one move from start to finish.

Player

Class

Where did it come from? Two local players have names and are assigned different marks for the whole match.

Question to ask: Is a player only a name, or do two pieces of information belong together?

A player's identity and mark travel together whenever we check a turn or report a winner. The values are stable during a match, and keeping them together avoids passing a loose name and mark separately.

Decision:

Create a small immutable Player class containing name and mark. Small classes are valid when they preserve a meaningful relationship between values.

Cell

Field

Where did it come from? A move chooses one row and column, and that position is either empty, X, or O.

Question to ask: What behavior would a Cell object add in this fixed game?

A cell does not have rules beyond holding one mark. Coordinate validation belongs to Board because Board knows its size. Occupancy is simply whether the stored mark is null. A Cell class would add another wrapper without removing complexity.

Decision:

Store each cell directly as Mark or null inside Mark[][]. Introduce a Cell class only if cells later gain their own state or behavior.

Mark, GameStatus, and MoveResult

Enum

Where did it come from? Only X and O may be placed; a match has a small set of end states; every rejected move needs a clear reason.

Question to ask: Are these objects with changing identity, or names from fixed lists?

They describe allowed values rather than independent things. Using strings would allow spelling mistakes and booleans would allow contradictory states. Each concept has a small closed set of valid choices.

Decision:

Use three enums. Mark describes a piece, GameStatus describes the whole match, and MoveResult explains the result of one attempted action.

WinningRule

Leave out

Where did it come from? This version has one fixed rule: three equal marks in a row, column, or diagonal.

Question to ask: Do we currently have several interchangeable ways to decide a winner?

No. An interface with one implementation would make the reader jump between files without giving us flexibility we need today. The rule only reads Board data, so Board can contain it directly.

Decision:

Do not create a WinningRule interface yet. If the interviewer later asks for different board sizes or rule sets, that new requirement will justify extracting it.

See one move flow through the objects

Swipe to follow the flow →

Players send makeMove to Game, Game asks Board to place a mark, and Board returns whether the placement was accepted or rejected
Game protects the match flow; Board protects the cells. The return result lets Game update the turn only after a mark was accepted.

Read the arrows in order. A player sends makeMove to Game. Game first checks the current player and match status. It then asks Board to place the mark. Board checks the coordinate and cell, stores the mark only when valid, and returns accepted or rejected. Game changes the turn only after an accepted placement. That is why the UI talks to Game instead of editing Board directly.

3. Class design (10–15 minutes)

The entity step told us which objects we need. The class-design step decides exactly what each object must remember and expose.

Start from the action a user performs: “player X places a mark at row 1, column 2.” That action enters Game.makeMove. Walk through what Game must know, then move down to the Board operations it needs.

Derive Game state and methods

Take the requirements one sentence at a time. “Players alternate” means Game must remember currentPlayer. “Moves stop after completion” means it must remember status. “Expose the winner” means a winner field is needed. We are not guessing fields; each one pays for a stated behavior.

Two players use X and O

GamePlayer playerX, playerO

Players alternate turns

GamePlayer currentPlayer

The game can finish

GameGameStatus status

Expose the winner

GamePlayer winner

Accept one move

GamemakeMove(player, row, col)

Start again

Gamereset()

Derive Board state and methods

Game should not inspect array indexes itself. It asks Board questions in the language of the problem: is this coordinate inside, can this mark be placed, is there a winning line, is the board full? That keeps all cell knowledge in one place.

Store a fixed 3 by 3 grid

BoardMark[][] cells

Reject bad coordinates

BoardisInside(row, col)

Reject an occupied cell

Boardplace(row, col, mark)

Detect a winner

BoardhasWinningLine(mark)

Detect a draw

BoardisFull()

Start again

Boardclear()

Decide whether a pattern helps

Before choosing a pattern, ask what rule has multiple versions right now. The answer is none: there is one fixed board and two human players. A direct design is easier to explain and test.

Do not force: Strategy for the basic game

The requirements contain one fixed winning rule and two human players. A WinningStrategy or MoveStrategy interface would create extra code without any second implementation. Write the direct rule first.

Do not force: Singleton for Game

We may want several games in tests or in a future tournament. A normal object supports that naturally. Global singleton state would make this harder.

Use: Strategy when a computer player is added

If an extension requires easy, medium, and hard computer players, those are several ways to choose a move. At that point a MoveStrategy interface has a real purpose. Do not add it before that requirement arrives.

4. Implementation (~10 minutes)

This is the complete Java version. The main method changes state only after validation succeeds.

What the code does

It creates two players, stores marks on a three-by-three board, accepts or rejects moves, alternates turns, and ends the match with a win or draw.

Why it is structured this way

Board owns cell and winning-line rules. Game owns the match workflow: whose turn it is, whether play may continue, and who won. Keeping these jobs separate makes invalid-state changes easier to prevent.

Follow one request through the code

  1. 1A caller sends Game.makeMove with a player, row, and column.
  2. 2Game checks match status and turn order before touching the board.
  3. 3Board validates the cell and places the mark only when it is empty.
  4. 4Game checks win, then draw, otherwise changes the current player.

Code concepts to understand

Validate first, change state secondA rejected move must leave the board and current player exactly as they were.

Game.makeMove checks completion and turn order first. Board.place then checks the coordinates and cell. Only after placement succeeds does Game calculate the result or switch players.

This order is important. If the turn changed before the board accepted the move, clicking an occupied cell would incorrectly give the other player a turn.

Board rules versus game workflowBoard answers questions about cells; Game controls the match.

Board knows whether a coordinate is valid, whether a cell is empty, whether a mark has a winning line, and whether the board is full. It does not know player names or whose turn it is.

Game knows the current player, status, and winner. It calls Board instead of reading or changing the cell array directly. This gives both classes one clear reason to change.

Enums instead of strings and booleansMark, GameStatus, and MoveResult give every meaningful state a clear name.

An enum prevents invalid values such as "FINSHED" and avoids combinations of booleans such as isWon=true and isDraw=true. MoveResult also tells the caller why a move failed without throwing an exception for an expected user action.

Why no Strategy pattern in the basic gameThere is only one fixed human move flow and one fixed winning rule.

A pattern should solve a current source of change. The basic requirements do not include bots, several board sizes, or selectable winning rules, so extra interfaces would make the first design harder to read.

If the interviewer adds computer players, introduce MoveStrategy. If they add different winning rules, introduce WinningRule. The extension creates the need for the pattern.

Value types

These enums define the complete vocabulary used by the rest of the program. They remove magic strings and make method results readable at the call site.

Mark.java
The two marks a player can own
public enum Mark {
    X,
    O
}
GameStatus.java
The complete state of a match
public enum GameStatus {
    IN_PROGRESS,
    X_WON,
    O_WON,
    DRAW
}
MoveResult.java
Why a requested move was accepted or rejected
public enum MoveResult {
    ACCEPTED,
    NOT_YOUR_TURN,
    OUT_OF_BOUNDS,
    CELL_OCCUPIED,
    GAME_ALREADY_OVER
}

Player and board

Player is stable identity data: a name and one fixed mark. Board owns the mutable cell array and all rules that can be answered by looking only at that array. Notice that callers cannot receive and modify the array directly.

Player.java
A player's identity and fixed mark
import java.util.Objects;

public final class Player {
    private final String name;
    private final Mark mark;

    public Player(String name, Mark mark) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException("name is required");
        }
        this.name = name;
        this.mark = Objects.requireNonNull(mark);
    }

    public String getName() {
        return name;
    }

    public Mark getMark() {
        return mark;
    }

    @Override
    public String toString() {
        return name + " (" + mark + ")";
    }
}
Board.java
Owns cells, placement checks, and fixed winning rules
import java.util.Arrays;

public final class Board {
    public static final int SIZE = 3;
    private final Mark[][] cells = new Mark[SIZE][SIZE];

    public boolean place(int row, int column, Mark mark) {
        if (!isInside(row, column) || mark == null) return false;
        if (cells[row][column] != null) return false;
        cells[row][column] = mark;
        return true;
    }

    public boolean isInside(int row, int column) {
        return row >= 0 && row < SIZE && column >= 0 && column < SIZE;
    }

    public boolean hasWinningLine(Mark mark) {
        for (int index = 0; index < SIZE; index++) {
            if (cells[index][0] == mark
                    && cells[index][1] == mark
                    && cells[index][2] == mark) {
                return true;
            }
            if (cells[0][index] == mark
                    && cells[1][index] == mark
                    && cells[2][index] == mark) {
                return true;
            }
        }

        boolean mainDiagonal = cells[0][0] == mark
                && cells[1][1] == mark
                && cells[2][2] == mark;
        boolean otherDiagonal = cells[0][2] == mark
                && cells[1][1] == mark
                && cells[2][0] == mark;

        return mainDiagonal || otherDiagonal;
    }

    public boolean isFull() {
        for (Mark[] row : cells) {
            for (Mark cell : row) {
                if (cell == null) return false;
            }
        }
        return true;
    }

    public Mark getMark(int row, int column) {
        if (!isInside(row, column)) {
            throw new IndexOutOfBoundsException("Cell is outside the board");
        }
        return cells[row][column];
    }

    public void clear() {
        for (Mark[] row : cells) {
            Arrays.fill(row, null);
        }
    }

    @Override
    public String toString() {
        StringBuilder output = new StringBuilder();
        for (int row = 0; row < SIZE; row++) {
            for (int column = 0; column < SIZE; column++) {
                Mark mark = cells[row][column];
                output.append(mark == null ? "." : mark);
                if (column < SIZE - 1) output.append(" ");
            }
            output.append(System.lineSeparator());
        }
        return output.toString();
    }
}

Game controller

Game.makeMove is the central workflow and should be read from top to bottom: reject illegal game state, reject the wrong player, validate the cell, place the mark, calculate the result, and switch the turn only if play continues. TicTacToeDemo proves how the public API is meant to be used.

Game.java
Controls turns, status, and the move workflow
import java.util.Objects;

public final class Game {
    private final Board board;
    private final Player playerX;
    private final Player playerO;
    private Player currentPlayer;
    private Player winner;
    private GameStatus status;

    public Game(Player first, Player second) {
        Objects.requireNonNull(first);
        Objects.requireNonNull(second);
        if (first.getMark() == second.getMark()) {
            throw new IllegalArgumentException("Players need different marks");
        }

        this.playerX = first.getMark() == Mark.X ? first : second;
        this.playerO = first.getMark() == Mark.O ? first : second;
        this.board = new Board();
        reset();
    }

    public MoveResult makeMove(Player player, int row, int column) {
        if (status != GameStatus.IN_PROGRESS) {
            return MoveResult.GAME_ALREADY_OVER;
        }
        if (player != currentPlayer) {
            return MoveResult.NOT_YOUR_TURN;
        }
        if (!board.isInside(row, column)) {
            return MoveResult.OUT_OF_BOUNDS;
        }
        if (!board.place(row, column, player.getMark())) {
            return MoveResult.CELL_OCCUPIED;
        }

        if (board.hasWinningLine(player.getMark())) {
            winner = player;
            status = player.getMark() == Mark.X
                    ? GameStatus.X_WON
                    : GameStatus.O_WON;
        } else if (board.isFull()) {
            status = GameStatus.DRAW;
        } else {
            currentPlayer = currentPlayer == playerX ? playerO : playerX;
        }

        return MoveResult.ACCEPTED;
    }

    public void reset() {
        board.clear();
        currentPlayer = playerX;
        winner = null;
        status = GameStatus.IN_PROGRESS;
    }

    public Board getBoard() {
        return board;
    }

    public Player getCurrentPlayer() {
        return currentPlayer;
    }

    public Player getWinner() {
        return winner;
    }

    public GameStatus getStatus() {
        return status;
    }
}
TicTacToeDemo.java
Runs a complete winning game
public final class TicTacToeDemo {
    public static void main(String[] args) {
        Player alice = new Player("Alice", Mark.X);
        Player bob = new Player("Bob", Mark.O);
        Game game = new Game(alice, bob);

        play(game, alice, 0, 0);
        play(game, bob,   1, 0);
        play(game, alice, 0, 1);
        play(game, bob,   1, 1);
        play(game, alice, 0, 2);

        System.out.println(game.getBoard());
        System.out.println("Status: " + game.getStatus());
        System.out.println("Winner: " + game.getWinner());
    }

    private static void play(Game game, Player player, int row, int column) {
        MoveResult result = game.makeMove(player, row, column);
        System.out.printf("%s -> (%d,%d): %s%n",
                player.getName(), row, column, result);
    }
}

Verify the design with a real scenario

  1. 0Initial: the board is empty, status is IN_PROGRESS, and Alice (X) is current.
  2. 1Alice plays (0,0). Board accepts X and Game changes current player to Bob.
  3. 2Alice tries another move immediately. Game returns NOT_YOUR_TURN and the board does not change.
  4. 3Bob plays (1,0), then turns continue until Alice completes row 0.
  5. 4After Alice plays (0,2), Board finds X X X, Game sets X_WON, stores Alice as winner, and does not switch turns.
  6. 5Any later move returns GAME_ALREADY_OVER and the winning board stays unchanged.

Try the flow below. Click the same cell twice to confirm that a rejected move does not switch the player.

Try it yourself

Play and inspect each move

Move 0X's turn

Choose an empty cell. Try clicking an occupied cell too.

Live object state

Game.status

IN_PROGRESS

Game.current

Player X

Board.moves

0 / 9

What happened

  1. Game created. Player X owns the first turn.
1UI sends intent
2Game checks status
3Board validates cell
4Rule evaluates result

5. Extensions (~5 minutes)

“Add computer players with several difficulty levels”

Now introduce a MoveStrategy interface with chooseMove(Board). Easy, medium, and hard bots implement different selection rules. Human players still receive moves from input, so do not put minimax logic inside Board or Game.

“Support an N by N board and K marks in a row”

Pass size into Board and move win checking behind a WinningRule interface. The fixed eight-line implementation can then be replaced without changing the turn workflow in Game.

“Add undo”

Store each accepted move in a stack. undo() removes the most recent mark, restores the previous player, and changes status back to IN_PROGRESS. Rejected moves never enter the history.

“Play over a network”

Keep this domain model. Add a service around Game that identifies the remote player, serializes commands, and ensures that two moves cannot update one game at the same time.