LLD Problem
Design Tic-Tac-Toe
Design an object-oriented Tic-Tac-Toe game with proper separation of concerns, extensibility, and clean code.
15 min readClassic LLD Problem
1Requirements
Functional
- 3x3 board (extensible to NxN)
- Two players (X and O)
- Alternate turns
- Win detection (row, column, diagonal)
- Draw detection
- Reset game
Extensions to Discuss
- Computer player (AI)
- Variable board size
- Undo move
- Multiple games, score tracking
- Save/load game state
2Class Design
Game
- board
- players[]
- currentPlayer
- status
+ play()
+ makeMove()
+ checkWin()
+ reset()
Board
- size
- cells[][]
+ placeSymbol()
+ isFull()
+ getCell()
+ print()
Player
- name
- symbol
+ makeMove()
Cell
- row
- col
- symbol
+ isEmpty()
+ setSymbol()
WinChecker
- board
+ checkRows()
+ checkCols()
+ checkDiagonals()
Symbol
- X
- O
- EMPTY
+ Enum values
3Implementation
Tic-Tac-Toe Implementation
// ========== Symbol Enum ==========
enum Symbol { X, O, EMPTY }
enum GameStatus { IN_PROGRESS, X_WINS, O_WINS, DRAW }
// ========== Cell ==========
class Cell {
private int row, col;
private Symbol symbol;
public Cell(int row, int col) {
this.row = row;
this.col = col;
this.symbol = Symbol.EMPTY;
}
public boolean isEmpty() { return symbol == Symbol.EMPTY; }
public void setSymbol(Symbol s) { this.symbol = s; }
public Symbol getSymbol() { return symbol; }
}
// ========== Board ==========
class Board {
private int size;
private Cell[][] cells;
public Board(int size) {
this.size = size;
this.cells = new Cell[size][size];
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++)
cells[i][j] = new Cell(i, j);
}
public boolean placeSymbol(int row, int col, Symbol symbol) {
if (row < 0 || row >= size || col < 0 || col >= size) return false;
if (!cells[row][col].isEmpty()) return false;
cells[row][col].setSymbol(symbol);
return true;
}
public boolean isFull() {
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++)
if (cells[i][j].isEmpty()) return false;
return true;
}
public Cell getCell(int r, int c) { return cells[r][c]; }
public int getSize() { return size; }
public void print() {
for (int i = 0; i < size; i++) {
for (int j = 0; j < size; j++) {
Symbol s = cells[i][j].getSymbol();
System.out.print(s == Symbol.EMPTY ? "." : s);
if (j < size - 1) System.out.print("|");
}
System.out.println();
if (i < size - 1) System.out.println("-+-+-");
}
}
}
// ========== Player ==========
class Player {
private String name;
private Symbol symbol;
public Player(String name, Symbol symbol) {
this.name = name;
this.symbol = symbol;
}
public Symbol getSymbol() { return symbol; }
public String getName() { return name; }
}
// ========== Win Checker ==========
class WinChecker {
public static boolean hasWon(Board board, Symbol symbol) {
int size = board.getSize();
// Check rows and columns
for (int i = 0; i < size; i++) {
if (checkLine(board, symbol, i, 0, 0, 1)) return true; // row
if (checkLine(board, symbol, 0, i, 1, 0)) return true; // col
}
// Check diagonals
if (checkLine(board, symbol, 0, 0, 1, 1)) return true;
if (checkLine(board, symbol, 0, size-1, 1, -1)) return true;
return false;
}
private static boolean checkLine(Board board, Symbol symbol,
int startR, int startC, int dR, int dC) {
int size = board.getSize();
for (int i = 0; i < size; i++) {
if (board.getCell(startR + i*dR, startC + i*dC).getSymbol() != symbol)
return false;
}
return true;
}
}
// ========== Game ==========
class Game {
private Board board;
private Player[] players;
private int currentPlayerIdx;
private GameStatus status;
public Game(Player p1, Player p2, int boardSize) {
this.board = new Board(boardSize);
this.players = new Player[]{p1, p2};
this.currentPlayerIdx = 0;
this.status = GameStatus.IN_PROGRESS;
}
public boolean makeMove(int row, int col) {
if (status != GameStatus.IN_PROGRESS) return false;
Player current = players[currentPlayerIdx];
if (!board.placeSymbol(row, col, current.getSymbol())) {
System.out.println("Invalid move!");
return false;
}
if (WinChecker.hasWon(board, current.getSymbol())) {
status = current.getSymbol() == Symbol.X ?
GameStatus.X_WINS : GameStatus.O_WINS;
System.out.println(current.getName() + " wins!");
} else if (board.isFull()) {
status = GameStatus.DRAW;
System.out.println("Draw!");
} else {
currentPlayerIdx = 1 - currentPlayerIdx;
}
return true;
}
public void printBoard() { board.print(); }
public GameStatus getStatus() { return status; }
}
// ========== Usage ==========
public class Main {
public static void main(String[] args) {
Player p1 = new Player("Alice", Symbol.X);
Player p2 = new Player("Bob", Symbol.O);
Game game = new Game(p1, p2, 3);
game.makeMove(0, 0); // X
game.makeMove(1, 1); // O
game.makeMove(0, 1); // X
game.makeMove(2, 2); // O
game.makeMove(0, 2); // X wins!
game.printBoard();
}
}4Interview Follow-up Questions
Interview Follow-up Questions
Common follow-up questions interviewers ask
5Key Takeaways
1Separate Board, Cell, Player, Game classes.
2Use Enum for Symbol and GameStatus.
3Extract win logic into WinChecker class.
4Strategy pattern for AI players.
5Design for extensibility (NxN boards, K-in-a-row).
6Optimize win checking for large boards.