Tic Tac Toe is a classic two-player game played on a 3x3 grid. Players take turns marking empty cells with their respective symbols: X or O.
The goal is to be the first to place three of your symbols in a row, either horizontally, vertically, or diagonally. At the same time, you must try to prevent your opponent from achieving the same. If all cells are filled and no player wins, the game ends in a draw.
In this chapter, we will explore the low-level design of a tic tac toe game in detail.
Lets start by clarifying the requirements:
Before starting the design, it's important to ask thoughtful questions to uncover hidden assumptions, clarify ambiguities, and define the system's scope more precisely.
Here is an example of how a discussion between the candidate and the interviewer might unfold:
Candidate: Should the game support variable board sizes, such as 4x4 or 5x5?
Interviewer: For the purpose of this interview, let’s stick with the standard 3x3 board.
Candidate: Should the game support both player-vs-player and player-vs-computer modes?
Interviewer: Let’s keep it simple and focus only on the player-vs-player mode for now.
Candidate: What should happen if a player tries to make an invalid move, like selecting an already filled cell?
Interviewer: The game should reject the move and inform the player to make another selection.
Candidate: Should the system maintain a scoreboard across multiple games to track player wins?
Interviewer: Yes, tracking the scoreboard across games would be a good addition.
Candidate: How should the user input be handled? Should we take input from the console, or just hardcode a sample game sequence?
Interviewer: To keep things focused on the design, you can hardcode a sample sequence in a demo or main method.
Candidate: Should we track the history of moves to allow features like undo or move replay?
Interviewer: That's an interesting feature to consider, but let’s leave it out for now and focus on the core gameplay logic.
After gathering the details, we can summarize the key system requirements.
After the requirements are clear, the next step is to identify the core entities that we will form the foundation of our design.
Core entities are the fundamental building blocks of our system. We identify them by analyzing the functional requirements and highlighting the key nouns and responsibilities that naturally map to object-oriented abstractions such as classes, enums, or interfaces.
Let’s walk through the functional requirements and extract the relevant entities:
This suggests the need for a Board
entity that manages the overall state of the grid. The grid itself consists of individual cells, so we also need a Cell
entity.
We need a Player
entity to represent each participant in the game. Each player is associated with a unique symbol and may optionally have an identifier such as a name or number.
The player symbols are constant and limited to X
, O
, or EMPTY
, which can be modeled using an enum Symbol
.
This behavior suggests a Game
entity that encapsulates the game loop. This class will coordinate the board and players, process moves, switch turns, and determine whether the game is still in progress, has been won, or ended in a draw.
To represent the current state of the game, such as whether it is still ongoing, has ended in a draw, or has a winner, we can use an enum GameStatus
.
To support multiple games and a persistent scoreboard, we introduce a central controller class TicTacToeSystem
. It manages game sessions and delegates gameplay to the appropriate Game
instance.
For tracking player performance across games, we need a Scoreboard
entity that maintains win/draw statistics.
Board
: Represents the 3x3 game grid. Manages a 2D matrix of cells and provides methods to update cell values, validate positions, and check for win or draw conditions.Cell
: Represents an individual square on the board. Each cell can either be empty or contain a symbol (X
or O
).Player
: Represent a player with a symbol and optionally a name or ID.Symbol
: Represents the value a cell can hold—X
, O
, or EMPTY
.Game
: Controls the overall game flow. Alternates turns, validates moves, updates the board, and checks for winning or draw conditions.GameStatus
: Represents the current state of the game. Possible values include IN_PROGRESS
, DRAW
, WINNER_X
, and WINNER_O
.Scoreboard
: Tracks cumulative scores and outcomes across multiple game sessions.TicTacToeSystem
: Orchestrates the creation of games and ties together core components like the scoreboard and game engine.These core entities define the key abstractions of the game and will guide the structure of our low-level design and class diagrams.
Once the core entities have been identified, the next step is to design the system's class structure. This involves defining the attributes and behaviors of each class, establishing relationships among them, applying relevant object-oriented design patterns, and visualizing the overall architecture using a class diagram.
We begin by defining the classes and enums, starting with simple data-holding components and progressing toward classes that encapsulate the game’s core logic.
Enums (short for enumerations) are a special data type that represents a fixed set of named constants. They are ideal when a variable should only take one of a predefined set of values. Enums enhance type safety, improve code readability, and reduce the risk of invalid values.
Here are the enums we can have in our system:
Symbol
Represents the values that a cell on the board can hold. Using an enum provides type safety and improves readability.
X
, O
, EMPTY
GameStatus
Defines the possible states of the game. This helps in managing game flow and determining the outcome.
IN_PROGRESS
, WINNER_X
, WINNER_O
, DRAW
Data classes are classes primarily used to hold data rather than encapsulate complex behavior or logic. Their main purpose is to store and transfer data in a clean and structured way. They represent entities like players and board cells in our design.
Player
Represents a player participating in the game.
name: String
– The player’s name (e.g., "Player 1")symbol: Symbol
– The marker assigned to the player (X
or O
)Player(String name, Symbol symbol)
– Constructor to initialize playergetName()
– Returns the player’s namegetSymbol()
– Returns the player’s symbolCell
Represents a single square on the board.
symbol: Symbol
– Current value of the cellCell()
– Initializes the cell with Symbol.EMPTY
getSymbol()
– Returns the current symbol in the cellsetSymbol(Symbol symbol)
– Updates the cell’s symbolCore classes are central to the system and contain the primary logic that drives the gameplay. They manage state, enforce rules, and coordinate interactions between components.
Board
Encapsulates the 3x3 grid and handles all board-related operations including its state and the rules for checking win/draw conditions.
grid: Cell[][]
– A 2D array representing the boardsize: int
– The board dimension (default is 3)Board(int size)
– Constructor to initialize the board with empty cells.placeSymbol(int row, int col, Symbol symbol)
– Places a symbol on the specified cell.isCellEmpty(int row, int col)
– Checks whether a cell is availableisFull()
– Checks if all cells are filled, a condition for a draw.checkWinner(int row, int col, Symbol symbol)
– Checks if a winning condition is met based on the last moveprintBoard()
– Displays the current state of the boardGame
The orchestrator that brings all components together and manages gameplay.
board: Board
– The game board instance.players: Player[]
– Array containing the two playerscurrentPlayer: Player
– The player whose turn it isstatus: GameStatus
– Current status of the gameGame(Player p1, Player p2, int boardSize)
– Constructor to initialize the game state.makeMove(int row, int col)
– Validates and applies a move, updates status, and switches turngetGameStatus()
– Returns the current game statusgetWinner()
– Returns the winning player, if anyprintBoard()
– Delegates to Board.printBoard()
for displayGameDriver
Serves as the entry point to simulate gameplay using a predefined sequence of moves.
Responsibility:
Game
and other componentsmakeMove()
to simulate a full game sessionThe relationships define how our classes interact. We use standard object-oriented relationships to create a well-structured system.
"has-a"
)"uses-a"
)This represents a weaker relationship where one class uses another.
The TicTacToeSystem class is implemented as a singleton to ensure a single, globally accessible entry point to the system. This is particularly useful for managing a central Scoreboard that persists across multiple games.
The TicTacToeSystem acts as a facade, providing a simplified, high-level interface (createGame, makeMove) to the client. It hides the underlying complexity of instantiating Game objects, managing the Board, wiring up observers like the Scoreboard, and handling game state transitions.
The WinningStrategy interface and its concrete implementations (RowWinningStrategy, etc.) encapsulate different win-checking algorithms.
A player can win in three ways: completing a row, column, or diagonal. Rather than hardcoding all win conditions in one place, we can create a WinningStrategy
interface and implement the conditions separately.
RowWinningStrategy
: Checks if all cells in the current row are filled with the same symbolColumnWinningStrategy
: Checks for column winsDiagonalWinningStrategy
: Checks both diagonals for a winning conditionThe GameState interface and its implementations (InProgressState, WinnerState, DrawState) allow the Game object to alter its behavior when its internal state changes.
This pattern cleanly separates the logic for handling a move when the game is in progress versus when it is already over, avoiding complex conditional statements within the Game class.
The Game (Subject) and Scoreboard (Observer) use this pattern to decouple score-keeping from the core game logic.
The Game notifies the Scoreboard only when it enters a finished state, allowing the Scoreboard to update scores without the Game needing to know about the scoreboard's existence.
GameStatus
EnumRepresents the state of the game at any point. Helps in determining if further moves are allowed or if the game is already concluded.
Symbol
EnumRepresents each player's marker as well as empty cells. This abstraction allows consistent symbol management across the game board.
Custom exception used to indicate illegal moves (e.g., out-of-bounds or occupied cells), helping to keep the game logic clean and robust.
Player
Encapsulates a player's identity and their assigned symbol (X or O).
Cell
Each cell on the board holds a symbol. Initially empty, it gets updated when a player makes a valid move.
Board
Manages the 2D grid of the game. Handles move placement, board initialization, checking if it's full, and rendering the board.
Uses the Strategy design pattern to encapsulate different ways of checking win conditions. Allows the game to remain extensible and clean.
To allow for features like a scoreboard without tightly coupling it to the game logic, we use the Observer Pattern. The Game acts as the "Subject" and notifies its "Observers" (like a Scoreboard) when its state changes.
GameObserver
and GameSubject
Decouples the core game logic from side-effects like updating scores. Scoreboard subscribes to the game lifecycle through these interfaces.
Scoreboard
Keeps a running tally of player wins. Gets notified only when a game concludes with a winner. Its update method is called whenever the Game (subject) decides to notify its observers. It inspects the Game's final state to update the scores.
GameState
and Concrete StatesApplies the State pattern to delegate move handling logic based on the current state of the game (in-progress, won, or draw).
GameState Interface: Defines the common action that can be performed in any state, in this case, handleMove.
Concrete States: InProgressState, WinnerState, and DrawState each implement the handleMove method differently.
This is the core engine of a single match. It orchestrates the interactions between the Board, Players, WinningStrategy list, and the current GameState. It delegates the complex logic to the appropriate components and notifies observers when the game ends.
TicTacToeSystem
Implements Singleton pattern to centralize system operations like creating games and processing moves. Handles I/O and delegates logic to the Game class. Ensures there is only one instance of the system, which is useful for managing a single, system-wide Scoreboard.
It also acts as facade providing a simplified, high-level interface (createGame, makeMove) to the client, hiding the complexity of creating and managing Game objects and their observers.
TicTacToeDemo
Finally, the TicTacToeDemo class serves as the entry point and demonstrates how a client would interact with our system.
This class simulates a user interacting with the TicTacToeSystem. It creates players, starts new games, and makes moves. It showcases the system's ability to handle multiple consecutive games and correctly track scores via the decoupled Scoreboard.