AlgoMaster Logo

Design Tic Tac Toe Game

Ashish

Ashish Pratap Singh

easy
10 min read

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.

Tic Tac Toe

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:

1. Clarifying 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:

After gathering the details, we can summarize the key system requirements.

1.1 Functional Requirements

  • The game is played on a 3x3 grid.
  • Two players take alternate turns, identified by markers ‘X’ and ‘O’.
  • The game should detect and announce the winner.
  • The game should declare a draw if all cells are filled and no player has won.
  • The game should reject invalid moves and inform the player.
  • The system should maintain a scoreboard across multiple games.
  • Moves can be hardcoded in a driver/demo class to simulate gameplay.

1.2 Non-Functional Requirements

  • The design should follow object-oriented principles with clear responsibilities and separation of concerns.
  • The system should be modular and extensible to support future features like larger boards, AI opponent, move history, etc.
  • The game logic should be testable and easy to maintain.
  • The system should provide clear console output that reflects the current state of the game board.

After the requirements are clear, the next step is to identify the core entities that we will form the foundation of our design.

2. Identifying Core Entities

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:

1. The game is played on a 3x3 grid.

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.

2. Two players take alternate turns, identified by markers ‘X’ and ‘O’.

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.

3. The game processes moves and determines game outcomes.

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.

4. The system should maintain a scoreboard across multiple games.

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.

These core entities define the key abstractions of the game and will guide the structure of our low-level design and class diagrams.

3. Designing Classes and Relationships

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.

3.1 Class Definitions

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

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:

Tic Tac Toe Enums

Symbol

Represents the values that a cell on the board can hold. Using an enum provides type safety and improves readability.

  • Values: XOEMPTY

GameStatus

Defines the possible states of the game. This helps in managing game flow and determining the outcome.

  • Values: IN_PROGRESSWINNER_XWINNER_ODRAW

Data Classes

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.

Player
Attributes
  • name: String – The player’s name (e.g., "Player 1")
  • symbol: Symbol – The marker assigned to the player (X or O)
Methods
  • Player(String name, Symbol symbol) – Constructor to initialize player
  • getName() – Returns the player’s name
  • getSymbol() – Returns the player’s symbol

Cell

Represents a single square on the board.

Cell
Attributes
  • symbol: Symbol – Current value of the cell
Methods
  • Cell() – Initializes the cell with Symbol.EMPTY
  • getSymbol() – Returns the current symbol in the cell
  • setSymbol(Symbol symbol) – Updates the cell’s symbol

Core Classes

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

Board

Attributes

  • grid: Cell[][] – A 2D array representing the board
  • size: int – The board dimension (default is 3)

Methods

  • 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 available
  • isFull() – 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 move
  • printBoard() – Displays the current state of the board

Game

The orchestrator that brings all components together and manages gameplay.

Game

Attributes

  • board: Board – The game board instance.
  • players: Player[] – Array containing the two players
  • currentPlayer: Player – The player whose turn it is
  • status: GameStatus – Current status of the game

Methods

  • Game(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 turn
  • getGameStatus() – Returns the current game status
  • getWinner() – Returns the winning player, if any
  • printBoard() – Delegates to Board.printBoard() for display

GameDriver

Serves as the entry point to simulate gameplay using a predefined sequence of moves.

Responsibility:

  • Instantiates the Game and other components
  • Calls makeMove() to simulate a full game session
  • Displays the final result

3.2 Class Relationships

The relationships define how our classes interact. We use standard object-oriented relationships to create a well-structured system.

Composition ("has-a")

  • Board --* Cell: A Board is composed of multiple Cell objects. The Cells' lifecycle is managed by the Board; they are created when the Board is initialized and do not exist independently.
  • Game --* Board: Each Game instance has one Board. The Board is created and owned by the Game.

Association ("uses-a")

This represents a weaker relationship where one class uses another.

  • Game --> WinningStrategy: A Game uses a list of WinningStrategy objects to check for a win condition.
  • Game --> GameState: A Game holds a reference to a GameState object and delegates move handling to it. The GameState object can be changed during the game's lifecycle.
  • WinningStrategy --> Board: The checkWinner method in a WinningStrategy implementation takes a Board object as an argument to perform its check.
  • Scoreboard --> Game: The update method in Scoreboard takes a Game object as an argument to inspect its final state and identify the winner.

Implementation / Inheritance ("is-a"):

  • Game --|> GameSubject: Game is a subject that can be observed.
  • Scoreboard --|> GameObserver: Scoreboard is an observer that listens to Game events.
  • RowWinningStrategy, ColumnWinningStrategy, DiagonalWinningStrategy --|> WinningStrategy: These are concrete implementations of the winning strategy contract.
  • InProgressState, WinnerState, DrawState --|> GameState: These are concrete implementations of the game state contract.

3.3 Key Design Patterns

Singleton

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.

Facade (Implicit)

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.

Strategy Pattern

Winning Strategy (for rule modularity)

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 symbol
  • ColumnWinningStrategy: Checks for column wins
  • DiagonalWinningStrategy: Checks both diagonals for a winning condition

State

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

Observer

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.

3.4 Full Class Diagram

4. Code Implementation

4.1 Game Status and Symbols

GameStatus Enum

Represents the state of the game at any point. Helps in determining if further moves are allowed or if the game is already concluded.

Symbol Enum

Represents each player's marker as well as empty cells. This abstraction allows consistent symbol management across the game board.

4.2 Custom Exception

Custom exception used to indicate illegal moves (e.g., out-of-bounds or occupied cells), helping to keep the game logic clean and robust.

4.3 Core Entities

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.

4.4 Winning Strategy Pattern

Interface and Implementations

Uses the Strategy design pattern to encapsulate different ways of checking win conditions. Allows the game to remain extensible and clean.

  • WinningStrategy Interface: This defines a common contract for any algorithm that checks for a win.
  • Concrete Strategies: RowWinningStrategy, ColumnWinningStrategy, and DiagonalWinningStrategy are concrete implementations. The Game class will hold a list of these strategies and iterate through them after each move.

4.5 Observer Pattern for Score Tracking

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.

4.6 Game State Pattern

GameState and Concrete States

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

  • InProgressState contains all the logic for a standard move.
  • WinnerState and DrawState prevent any further moves by throwing an exception.

4.7 Core Game Engine

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.

4.8 System Layer and Demo

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.

5. Run and Test

Languages
Loading...
Loading editor...

6. Quiz

Test Your Understanding

Q1.
Question 1 of 0