What is Tic-Tac-Toe?

Tic-Tac-Toe is a classic two-player, turn-based strategy game played on a 3×3 grid. Each player is assigned a distinct symbol, typically X or O, and players take turns placing their symbol in an unoccupied cell.

The objective is to be the first player to form a sequence of three identical symbols in one of the following ways:

  1. Horizontally — three matching symbols in the same row.
  2. Vertically — three matching symbols in the same column.
  3. Diagonally — three matching symbols along either diagonal.

A player wins as soon as they complete any valid three-symbol combination. Consequently, every move involves both creating a potential winning combination and blocking the opponent from completing one.

If all nine cells are occupied and neither player has formed a winning combination, the game ends in a draw.

From an LLD perspective, Tic-Tac-Toe is a compact but useful example for understanding fundamental object-oriented design principles. Although the game itself is simple, its implementation involves several important design concerns, including:

  • State management
  • Encapsulation
  • Composition
  • Responsibility assignment
  • Validation
  • Turn management
  • Game-flow coordination

In this chapter, we will explore the Low-Level Design (LLD) of a Tic-Tac-Toe game in a structured manner, beginning with requirements clarification and progressing toward entity identification, class design, relationships, and implementation.

Let’s begin by clarifying the requirements.

1. Clarifying Requirements

Before beginning the design, it is important to clarify the requirements and identify any assumptions that could influence the architecture.

A strong LLD discussion starts by asking targeted questions about scope, supported use cases, constraints, and expected behavior.

Interview Discussion

The following is an example of how the requirement-gathering discussion might unfold during an interview.

Candidate: Should the game support variable board sizes, such as 4×4 or 5×5?Interviewer: For the purpose of this interview, let’s stick with the standard 3×3 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 attempts an invalid move, such as selecting a cell that is already occupied? Interviewer: The game should reject the move and inform the player that they need to choose another cell.
Candidate: Should the system maintain a scoreboard across multiple games to track player wins? Interviewer: For now, let’s keep the core design focused on a single game. A scoreboard across multiple games can be discussed as a future extension.
Candidate: How should user input be handled? Should the system accept console input, or should we hardcode a sample game sequence? Interviewer: To keep the discussion focused on the object-oriented design, you can hardcode a sample sequence in a driver or demo class.
Candidate: Should we maintain move history to support features such as undo or move replay? Interviewer: That would be a useful extension, but let’s exclude it from the current scope and focus on the core gameplay logic.

These questions establish the boundaries of the system and prevent unnecessary complexity from entering the initial design.

After gathering the requirements, we can summarize them as functional and non-functional requirements.

Functional Requirements

  1. The game is played on a 3×3 grid.
  2. Two players take alternate turns and are identified by the markers X and O.
  3. The system should detect and announce the winner when a player completes a winning combination.
  4. The system should declare a draw when all cells are occupied and neither player has won.
  5. The system should reject invalid moves, such as selecting an occupied or out-of-bounds cell.
  6. Moves may be hardcoded in a driver/demo class to simulate gameplay.

Non-Functional Requirements

  1.  The design should follow object-oriented principles, with clear responsibilities and separation of concerns.
  2. The system should be modular and extensible, allowing future features such as larger boards, AI opponents, move history, undo/redo, and multiple games.
  3. The game logic should be easy to test and maintain.
  4. The system should provide clear console output representing the current state of the board.

2. Identifying Core Entities

How do we transform a set of requirements into actual entities or classes?

A useful starting point is to identify the nouns in the requirements and determine whether they represent objects with meaningful attributes or behaviors. Not every noun necessarily becomes a class, but this technique provides a systematic way to discover the initial domain model.

Let’s walk through the requirements and identify the entities that need to exist in the system.

2.1 The Game Is Played on a 3×3 Grid

The grid is central to the game, so we need an entity responsible for representing and managing it.

This gives us our first entity:

 Board

However, a board is composed of individual positions or squares. Each position can either be empty or contain a player's symbol.

This suggests another entity:

 Cell

The Board therefore contains 9 Cell objects arranged in a 3×3 structure.

Why Separate Board and Cell?

Although a Cell is part of a Board, the two entities have different responsibilities.

  • Board manages the overall grid structure.
  • Cell represents the state of a single position.
  • Board handles operations such as checking whether the board is full or placing a symbol at a specific position.
  • Cell simply maintains whether its position is currently empty or occupied.

This separation improves encapsulation and maintainability. It also gives us a natural extension point.

For example, if we later introduce features such as highlighting winning cells, tracking cell metadata, or adding visual state, those responsibilities can be associated with Cell rather than being embedded directly into Board.

2.2 Two Players Take Alternate Turns

Two players take alternate turns and are identified by markers X and O.

The system needs an entity representing each participant. Each player has a name and an assigned symbol.

This gives us the Player entity.

What About the Symbols?

We could represent symbols using strings or characters such as "X" and "O", but that approach permits invalid values.

For example, nothing would inherently prevent the creation of a player with a symbol such as "Z".

A dedicated Symbol enum provides a type-safe representation:

 enum Symbol {    X,
O,
EMPTY
}

This makes the domain model explicit and prevents invalid symbol values from being introduced through ordinary application code.

2.3 The Game Processes Moves and Determines Game Outcomes

Something must coordinate the overall gameplay.

The system needs to:

  • Accept a move.
  • Validate the move.
  • Determine whether the target cell is available.
  • Place the current player's symbol.
  • Check whether the move resulted in a win.
  • Check whether the game resulted in a draw.
  • Switch to the next player when appropriate.

These responsibilities belong to the Game entity, which acts as the orchestrator of the gameplay.

The game also needs to maintain its current state.

At any point, it may be:

  • Still in progress.
  • Won by player X.
  • Won by player O.
  • Drawn.

A simple boolean such as isGameOver, combined with a separate winner field, would require multiple pieces of state to represent the outcome.

Instead, we introduce a GameStatus enum:

 enum GameStatus {    IN_PROGRESS,
WINNER_X,
WINNER_O,
DRAW
}

This provides a single, explicit representation of the game's lifecycle.

Entity Overview

The resulting relationships can be represented as follows:

Whiteboard
Whiteboard diagram

We have identified three categories of entities.

Enums

Symbol and GameStatus define fixed sets of valid values. They improve type safety and make the domain model self-documenting.

Data Classes

Player and Cell primarily represent domain data with minimal behavior.

  • Player stores player-specific information.
  • Cell stores the current state of a board position.

Core Classes

Board and Game contain the primary application behavior.

  • Board encapsulates the grid and its operations.
  • Game orchestrates gameplay, validates moves, manages turns, and determines the outcome.
EntityTypeResponsibility
SymbolEnumRepresents valid cell values: X, O, or EMPTY
GameStatusEnumRepresents the game state: IN_PROGRESS, WINNER_X, WINNER_O, or DRAW
CellData ClassRepresents a single board position and its current symbol
PlayerData ClassRepresents a player and their assigned symbol
BoardCore ClassManages the 3×3 grid and board-level operations
GameCore ClassOrchestrates gameplay, turns, validation, and win detection

With the core entities identified, the next step is to define their attributes, behaviors, and relationships.

3. Designing Classes and Relationships

Now that the entities have been identified, we can define their internal structure.

For each class, we will determine:

  • Attributes — the state maintained by the object.
  • Methods — the behavior exposed by the object.
  • Relationships — how objects interact with and depend on one another.

Note:
To keep the discussion focused on the core design, trivial getters and setters are omitted from the class definitions.

3.1 Class Definitions

We will design the classes bottom-up.

We start with the simplest domain types, then define data-holding classes, and finally introduce the classes responsible for the core game logic.

This ordering makes the design easier to understand because higher-level components depend on the lower-level abstractions.

Enums

Enums represent a fixed set of valid values. They improve type safety, eliminate invalid states, and make the domain model easier to understand.

Symbol

Symbol represents the possible values that a cell can contain.

ValueDisplay CharacterPurpose
X'X'First player's marker
O'O'Second player's marker
EMPTY'_'Unoccupied cell

Each enum value can be associated with a display character used when rendering the board.

Using an enum instead of arbitrary strings or characters ensures that only valid symbols can be assigned to a cell or player.

GameStatus

GameStatus represents the current state of the game and defines the major stages of the game lifecycle.

ValueDescriptionTerminal?
IN_PROGRESSGame is still being playedNo
WINNER_XPlayer with symbol X has wonYes
WINNER_OPlayer with symbol O has wonYes
DRAWBoard is full and neither player has wonYes

These four states cover all possible outcomes of the game.

A newly created game starts in IN_PROGRESS and eventually transitions to exactly one terminal state:

Whiteboard
Whiteboard diagram

Once the game reaches a terminal state, it cannot transition back to IN_PROGRESS.

For example:

  • DRAW → IN_PROGRESS is invalid.
  • WINNER_X → WINNER_O is invalid.
  • WINNER_O → DRAW is invalid.

This makes the game lifecycle explicit and prevents invalid state transitions.

Design Decision

We use WINNER_X and WINNER_O instead of a generic WINNER state combined with a separate winner field.

With a generic winner state, determining the outcome would require evaluating multiple pieces of state:

 WINNER + winner.symbol

With dedicated terminal states, the status itself contains the necessary information:

 WINNER_X

directly indicates that X has won.

This provides several benefits:

  • Simpler state checks.
  • Fewer pieces of state to maintain.
  • Self-contained game status.
  • Reduced possibility of inconsistent state.

The design therefore avoids maintaining both a generic winner status and a separate winner field when the same information can be represented directly by the status.

Data Classes

Data classes represent domain objects that primarily hold state and expose a small amount of behavior.

They correspond to the fundamental nouns identified during requirements analysis.

Player

Player represents a participant in the game.

Whiteboard
Whiteboard diagram

Attributes

AttributeTypeDescription
nameStringPlayer identifier, such as Alice
symbolSymbolMarker assigned to the player: X or O

Methods

MethodDescription
Player(name, symbol)Constructor that validates the supplied player information and rejects EMPTY as a player symbol

The Player class is immutable.

Once a player has been created, their name and assigned symbol should not change during the game.

This prevents accidental state mutations such as changing a player's symbol from X to O in the middle of a game.

Cell

Cell represents a single position on the board and stores its current symbol.

Whiteboard
Whiteboard diagram

Attributes

AttributeTypeDescription
symbolSymbolCurrent value of the cell: X, O, or EMPTY

Methods

MethodDescription
Cell()Initializes the cell with EMPTY
isEmpty()Returns true when the cell currently contains EMPTY

Unlike Player, Cell is mutable.

A cell starts in the EMPTY state and transitions to either X or O when a player makes a valid move.

The isEmpty method provides a small domain-specific abstraction that improves readability.

Instead of checking the underlying representation directly, callers can express their intent through:

 cell.isEmpty()

This is clearer than exposing and repeatedly comparing the internal symbol representation.

Core Classes

Core classes contain the primary application behavior. They coordinate the data classes and enforce the rules necessary to execute the game.

Board

Board encapsulates the 3×3 grid and is responsible for board-level operations.

Whiteboard
Whiteboard diagram

Its responsibilities include:

  • Maintaining the collection of cells.
  • Validating cell availability.
  • Placing symbols into cells.
  • Determining whether the board is full.
  • Rendering the current board state.

The Board does not manage players, turns, or overall game outcomes. Those responsibilities belong to Game.

Attributes

AttributeTypeDescription
gridCell[][]Two-dimensional collection representing the board
sizeintBoard dimension; 3 for the standard game

Methods

MethodDescription
Board(size)Creates a size × size board populated with empty cells
placeSymbol(row, col, symbol)Places the specified symbol at the given position
isCellEmpty(row, col)Determines whether the specified position is currently available
isFull()Determines whether all cells are occupied
printBoard()Displays the current board state

Key Design Principles

1. Single Responsibility

The Board is responsible for managing the grid and board-level state.

It does not know about:

  • Players.
  • Turns.
  • Overall game lifecycle.

This separation keeps the Board focused and allows the same abstraction to potentially be reused in other grid-based systems.

For example, a generalized board abstraction could serve as a foundation for games such as:

  • Connect Four.
  • Battleship.
  • Other grid-based games.

The exact win conditions and game rules would remain outside the Board.

2. Composition

The relationship between Board and Cell is a composition relationship.

A Board creates and owns its Cells. The Cells exist as part of the Board's internal structure rather than as independently managed entities.

Conceptually:

Whiteboard
Whiteboard diagram


For the standard 3×3 game, a Board therefore owns 9 Cell objects.

This relationship is represented in UML using a composition connector.

3. Encapsulation

The internal grid should remain private.

External components should interact with the Board through controlled operations such as:

placeSymbol()isCellEmpty()
isFull()
printBoard()

This prevents external code from directly modifying the grid and bypassing the Board's validation rules.

If a direct cell-access method such as getCell is exposed, it should also validate row and column boundaries before returning the requested cell.

Game

Game is the central orchestrator of the system.

It coordinates the Board and Players and manages the overall gameplay lifecycle.

Whiteboard
Whiteboard diagram

Its responsibilities include:

  • Tracking the current player.
  • Validating moves.
  • Placing the current player's symbol.
  • Checking whether the latest move produced a winning condition.
  • Checking whether the game has resulted in a draw.
  • Updating the game status.
  • Switching turns when the game remains in progress.

Attributes

AttributeTypeDescription
boardBoardThe board on which the game is played
playersPlayer[]The two players participating in the game
currentPlayerIndexintIdentifies whose turn it is; 0 or 1
statusGameStatusRepresents the current game state

Methods

MethodDescription
Game(p1, p2, boardSize)Initializes the players, board, current turn, and initial game status
makeMove(row, col)Core gameplay operation: validates the move, places the symbol, evaluates the outcome, and switches turns when appropriate

The Game class ties the entire design together.

It owns the Board, maintains the participating Players, tracks the current turn, and controls the transition of GameStatus.

A typical successful move follows this high-level sequence:

Whiteboard
Whiteboard diagram


This keeps the responsibilities clearly separated:

  • Cell knows the state of one position.
  • Board manages the collection of cells and board-level operations.
  • Player represents a participant and their symbol.
  • Game coordinates the gameplay and controls the game lifecycle.
  • Symbol defines valid markers.
  • GameStatus defines valid game states.

3.2 Full Class Diagram

The complete object model can now be represented using the following UML class diagram:

Whiteboard
Whiteboard diagram

The final design has a clear separation of responsibilities:

ComponentPrimary Responsibility
SymbolDefines valid board markers
GameStatusDefines the game's lifecycle states
CellRepresents one board position
PlayerRepresents a participant and their marker
BoardManages the grid and board-level operations
GameOrchestrates gameplay and controls the game lifecycle

This gives us a clean object-oriented foundation with encapsulation, composition, single responsibility, type safety, and clear separation of concerns.

The domain model is now complete. The next step is to translate this design into code and implement the behavior of each class.

4. Code Implementation

Now let's translate our design into working code.

We will implement the system bottom-up: starting with foundational types, followed by data classes, and finally the classes that contain the core game logic.

This ordering is intentional because each layer builds upon the abstractions defined by the layers below it.

The implementation is organized into the following components:

  • Enums — Represent symbols and game states.
  • Custom Exception — Provides explicit error handling for invalid moves.
  • Data Classes — Represent players and individual board cells.
  • Board — Encapsulates all grid-related operations.
  • Game — Coordinates players, the board, turn management, and game-state transitions.

This structure keeps responsibilities well-defined and ensures that each class has a clear purpose.

4.1 Enums

We start with the two enums that other classes depend on.

Symbol

Each Symbol maps to a display character.

This keeps display-related logic centralized rather than scattering character mappings throughout the application.

For example, if the representation needs to change from X to x, the change can be made in a single location without modifying the rest of the game logic.

This approach improves maintainability, consistency, and separation of concerns.

GameStatus

The GameStatus enum represents the complete lifecycle of a game.

There are four possible states:

  • IN_PROGRESS — The game is currently active and players can make moves.
  • WINNER_X — Player with symbol X has won the game.
  • WINNER_O — Player with symbol O has won the game.
  • DRAW — The board is full and neither player has won.

The game always starts in IN_PROGRESS and eventually transitions to one of the three terminal states.

Using an enum instead of loosely defined boolean flags or strings provides a type-safe representation of game state and prevents invalid states from being introduced accidentally.

4.2 Custom Exception

Before implementing classes that can reject operations, we define how those failures should be represented.

A dedicated exception makes error handling more explicit and maintainable than relying on a generic RuntimeException.

InvalidMoveException

InvalidMoveException is thrown whenever a player attempts an illegal operation, including:

  • Playing on an already occupied cell.
  • Making a move after the game has ended.
  • Providing a row or column outside the valid board boundaries.

Using a domain-specific exception communicates the intent of the failure directly to the caller and keeps game-rule violations separate from unrelated runtime failures.

4.3 Data Classes

These classes primarily represent the data used by the game.

They intentionally contain minimal business logic and are responsible for maintaining valid object state.

Player

The Player class represents a participant in the game through two pieces of information:

  • Name
  • Symbol

The constructor performs validation before creating the object.

A player associated with EMPTY is invalid because EMPTY represents an unoccupied board cell rather than a playable player symbol.

Rejecting this state immediately follows the fail-fast principle.

If an invalid Player is created, the problem is detected at the point of creation rather than much later when the object participates in gameplay.

Both fields are declared final, meaning they cannot be modified after the object is constructed.

This provides immutability for the player's identity:

  • A player's name cannot unexpectedly change.
  • A player's symbol cannot be reassigned.
  • The object remains predictable throughout the lifetime of the game.
  • The possibility of state-related bugs is reduced.

Cell

The Cell class represents an individual position on the board.

Unlike Player, a Cell is intentionally mutable.

A cell starts with EMPTY and is updated when a player makes a valid move.

The isEmpty helper method provides a more expressive interface for checking whether a cell is available.

For example:

 cell.isEmpty()

is clearer and more intention-revealing than directly comparing the underlying state:

 cell.getSymbol() == Symbol.EMPTY

This small abstraction improves the readability of the code that interacts with the board.

4.4 Board Class

The Board encapsulates all operations related to the game grid.

Its responsibility is deliberately narrow: it manages the 2D array of cells and provides operations for interacting with those cells.

The Board does not know about:

  • Players.
  • Turns.
  • Winning conditions.
  • Draw conditions.
  • Overall game state.

Those responsibilities belong to the Game class.

A few important design decisions are worth highlighting.

Constructor Creates All Cells

The initializeBoard method is invoked during construction.

This guarantees that every position contains a valid Cell immediately after a Board is created.

The rest of the application therefore never has to deal with partially initialized or null cells.

Validation Is Centralized

The private validatePosition method is responsible for validating row and column coordinates.

Every public method that accepts coordinates can delegate to this method instead of duplicating boundary checks.

This reduces code duplication and ensures that all board operations apply the same validation rules.

isFull Short-Circuits

The isFull method immediately returns false when it encounters an empty cell.

There is no reason to scan the remaining cells once we already know that the board is not full.

printBoard Is Primarily for Debugging

The printBoard method is useful for demonstrating and testing the implementation, particularly in an interview environment.

In a production application, presentation logic would typically belong to a dedicated view or presentation layer rather than the Board itself.

Overall, the Board acts as a focused abstraction over the underlying grid while remaining completely independent of higher-level game rules.

4.5 Game Class

This is where everything comes together.

The Game class coordinates the players, board, turn management, and game state.

It therefore contains the majority of the application's business logic.

Although this is the most complex class, its methods are still designed around single responsibilities, so each operation performs one well-defined task.

Let's break down the key design decisions in the Game class.

Thread Safety

The makeMove method is declared synchronized.

This ensures that only one thread can execute makeMove on a particular Game instance at a time.

Without this protection, two concurrent requests could potentially observe the same board state and attempt to modify it simultaneously, resulting in an inconsistent game state.

For a simple console-based implementation, concurrency may not be necessary. However, explicitly protecting the state demonstrates an important production-oriented design consideration if the game were later exposed through a concurrent server or API.

The makeMove Flow

The makeMove method follows a strict sequence of validations and state transitions.

1. Check Whether the Game Is Already Over

If the game has reached a terminal state, the move is rejected immediately.

2. Validate That the Target Cell Is Empty

A player cannot overwrite an existing move.

3. Place the Current Player's Symbol

Once validation succeeds, the selected cell is updated.

4. Check for a Win

The board is evaluated using the position of the newly placed symbol.

5. Check for a Draw

If there is no winner and the board is now full, the game transitions to DRAW.

6. Switch to the Next Player

If neither a win nor a draw has occurred, control passes to the other player.

This ordering is important.

The win check must happen immediately after placing the move, followed by the draw check, before switching the active player.

Win Check

The checkWin method determines whether the most recent move resulted in a winning configuration.

Because only the most recently modified cell can create a new winning line, there is no need to scan every possible combination on the board.

The method checks:

  • The row containing the last move.
  • The column containing the last move.
  • The main diagonal, if the position lies on it.
  • The anti-diagonal, if the position lies on it.

The moment one of these lines is completely occupied by the current player's Symbol, the player is declared the winner.

This approach keeps the win-detection logic both simple and efficient, while avoiding unnecessary examination of unrelated board positions.

Move Sequence Diagram

The following sequence diagram illustrates what happens when a player makes a move:

5. Run And Test

public class Main {
    public static void main(String[] args) {
        Player alice = new Player("Alice", Symbol.X);
        Player bob = new Player("Bob", Symbol.O);

        Game game = new Game(alice, bob, 3);

        System.out.println("========== TIC TAC TOE ==========");

        // Alice (X) completes the top row and wins
        game.makeMove(0, 0);  // X at (0,0)
        game.makeMove(1, 0);  // O at (1,0)
        game.makeMove(0, 1);  // X at (0,1)
        game.makeMove(1, 1);  // O at (1,1)
        game.makeMove(0, 2);  // X at (0,2) - Alice wins!

        game.printBoard();

        System.out.println("Result: " + game.getStatus());
        Player winner = game.getWinner();
        if (winner != null) {
            System.out.println("Winner: " + winner.getName());
        }
    }
}
class Board {
    private final Cell[][] grid;
    private final int size;

    public Board(int size) {
        this.size = size;
        this.grid = new Cell[size][size];
        initializeBoard();
    }

    private void initializeBoard() {
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                grid[i][j] = new Cell();
            }
        }
    }

    public void placeSymbol(int row, int col, Symbol symbol) {
        validatePosition(row, col);
        grid[row][col].setSymbol(symbol);
    }

    public boolean isCellEmpty(int row, int col) {
        validatePosition(row, col);
        return grid[row][col].isEmpty();
    }

    public boolean isFull() {
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                if (grid[i][j].isEmpty()) {
                    return false;
                }
            }
        }
        return true;
    }

    public Cell getCell(int row, int col) {
        validatePosition(row, col);
        return grid[row][col];
    }

    public int getSize() {
        return size;
    }

    private void validatePosition(int row, int col) {
        if (row < 0 || row >= size || col < 0 || col >= size) {
            throw new InvalidMoveException(
                "Position (" + row + ", " + col + ") is out of bounds"
            );
        }
    }

    public void printBoard() {
        System.out.println();
        for (int i = 0; i < size; i++) {
            for (int j = 0; j < size; j++) {
                System.out.print(" " + grid[i][j].getSymbol().getDisplayChar() + " ");
                if (j < size - 1) System.out.print("|");
            }
            System.out.println();
            if (i < size - 1) {
                System.out.println("-".repeat(size * 4 - 1));
            }
        }
        System.out.println();
    }
}
class Cell {
    private Symbol symbol;

    public Cell() {
        this.symbol = Symbol.EMPTY;
    }

    public Symbol getSymbol() {
        return symbol;
    }

    public void setSymbol(Symbol symbol) {
        this.symbol = symbol;
    }

    public boolean isEmpty() {
        return symbol == Symbol.EMPTY;
    }
}
class Game {
    private final Board board;
    private final Player[] players;
    private int currentPlayerIndex;
    private GameStatus status;

    public Game(Player player1, Player player2, int boardSize) {
        this.board = new Board(boardSize);
        this.players = new Player[]{player1, player2};
        this.currentPlayerIndex = 0;
        this.status = GameStatus.IN_PROGRESS;
    }

    public synchronized void makeMove(int row, int col) {
        // Check if game is already over
        if (status != GameStatus.IN_PROGRESS) {
            throw new InvalidMoveException("Game is already over!");
        }

        // Validate the move
        if (!board.isCellEmpty(row, col)) {
            throw new InvalidMoveException(
                "Cell (" + row + ", " + col + ") is already occupied"
            );
        }

        // Place the symbol
        Player currentPlayer = players[currentPlayerIndex];
        board.placeSymbol(row, col, currentPlayer.getSymbol());

        // Check for win
        if (checkWin(row, col, currentPlayer.getSymbol())) {
            status = (currentPlayer.getSymbol() == Symbol.X)
                ? GameStatus.WINNER_X
                : GameStatus.WINNER_O;
            return;
        }

        // Check for draw
        if (board.isFull()) {
            status = GameStatus.DRAW;
            return;
        }

        // Switch to next player
        currentPlayerIndex = (currentPlayerIndex + 1) % 2;
    }

    private boolean checkWin(int row, int col, Symbol symbol) {
        int size = board.getSize();

        // Check the row of the last move
        boolean win = true;
        for (int c = 0; c < size; c++) {
            if (board.getCell(row, c).getSymbol() != symbol) { win = false; break; }
        }
        if (win) return true;

        // Check the column of the last move
        win = true;
        for (int r = 0; r < size; r++) {
            if (board.getCell(r, col).getSymbol() != symbol) { win = false; break; }
        }
        if (win) return true;

        // Check the main diagonal (only if the move is on it)
        if (row == col) {
            win = true;
            for (int i = 0; i < size; i++) {
                if (board.getCell(i, i).getSymbol() != symbol) { win = false; break; }
            }
            if (win) return true;
        }

        // Check the anti-diagonal (only if the move is on it)
        if (row + col == size - 1) {
            win = true;
            for (int i = 0; i < size; i++) {
                if (board.getCell(i, size - 1 - i).getSymbol() != symbol) { win = false; break; }
            }
            if (win) return true;
        }

        return false;
    }

    public Board getBoard() { return board; }
    public Player getCurrentPlayer() { return players[currentPlayerIndex]; }
    public GameStatus getStatus() { return status; }

    public Player getWinner() {
        if (status == GameStatus.WINNER_X) {
            return players[0].getSymbol() == Symbol.X ? players[0] : players[1];
        } else if (status == GameStatus.WINNER_O) {
            return players[0].getSymbol() == Symbol.O ? players[0] : players[1];
        }
        return null;
    }

    public void printBoard() {
        board.printBoard();
    }
}
class Player {
    private final String name;
    private final Symbol symbol;

    public Player(String name, Symbol symbol) {
        if (symbol == Symbol.EMPTY) {
            throw new IllegalArgumentException("Player cannot have EMPTY symbol");
        }
        this.name = name;
        this.symbol = symbol;
    }

    public String getName() {
        return name;
    }

    public Symbol getSymbol() {
        return symbol;
    }

    @Override
    public String toString() {
        return name + " (" + symbol.getDisplayChar() + ")";
    }
}
enum GameStatus {
    IN_PROGRESS,
    WINNER_X,
    WINNER_O,
    DRAW
}
enum Symbol {
    X('X'),
    O('O'),
    EMPTY('_');

    private final char displayChar;

    Symbol(char displayChar) {
        this.displayChar = displayChar;
    }

    public char getDisplayChar() {
        return displayChar;
    }
}
class InvalidMoveException extends RuntimeException {
    public InvalidMoveException(String message) {
        super(message);
    }
}
from entities.Player import Player
from entities.Game import Game
from enums.Symbol import Symbol


def main():

    alice = Player("Alice", Symbol.X)
    bob = Player("Bob", Symbol.O)

    game = Game(alice, bob, 3)

    print("========== TIC TAC TOE ==========")

    # Alice (X) completes the top row and wins
    game.make_move(0, 0)  # X at (0,0)
    game.make_move(1, 0)  # O at (1,0)
    game.make_move(0, 1)  # X at (0,1)
    game.make_move(1, 1)  # O at (1,1)
    game.make_move(0, 2)  # X at (0,2) - Alice wins!

    game.print_board()

    print("Result:", game.get_status())

    winner = game.get_winner()

    if winner is not None:
        print("Winner:", winner.get_name())


if __name__ == "__main__":
    main()
from entities.Cell import Cell
from enums.Symbol import Symbol
from exceptions.InvalidMoveException import InvalidMoveException


class Board:

    def __init__(self, size):
        self.size = size
        self.grid = [
            [Cell() for _ in range(size)]
            for _ in range(size)
        ]

    def place_symbol(self, row, col, symbol):
        self.validate_position(row, col)
        self.grid[row][col].set_symbol(symbol)

    def is_cell_empty(self, row, col):
        self.validate_position(row, col)
        return self.grid[row][col].is_empty()

    def is_full(self):
        for i in range(self.size):
            for j in range(self.size):
                if self.grid[i][j].is_empty():
                    return False

        return True

    def get_cell(self, row, col):
        self.validate_position(row, col)
        return self.grid[row][col]

    def get_size(self):
        return self.size

    def validate_position(self, row, col):
        if (
            row < 0
            or row >= self.size
            or col < 0
            or col >= self.size
        ):
            raise InvalidMoveException(
                f"Position ({row}, {col}) is out of bounds"
            )

    def print_board(self):
        print()

        for i in range(self.size):

            for j in range(self.size):
                print(
                    f" {self.grid[i][j].get_symbol().get_display_char()} ",
                    end=""
                )

                if j < self.size - 1:
                    print("|", end="")

            print()

            if i < self.size - 1:
                print("-" * (self.size * 4 - 1))

        print()
from enums.Symbol import Symbol


class Cell:

    def __init__(self):
        self.symbol = Symbol.EMPTY

    def get_symbol(self):
        return self.symbol

    def set_symbol(self, symbol):
        self.symbol = symbol

    def is_empty(self):
        return self.symbol == Symbol.EMPTY
from enums.GameStatus import GameStatus
from enums.Symbol import Symbol
from exceptions.InvalidMoveException import InvalidMoveException
from entities.Board import Board


class Game:

    def __init__(self, player1, player2, board_size):
        self.board = Board(board_size)
        self.players = [player1, player2]
        self.current_player_index = 0
        self.status = GameStatus.IN_PROGRESS

    def make_move(self, row, col):

        # Check if game is already over
        if self.status != GameStatus.IN_PROGRESS:
            raise InvalidMoveException("Game is already over!")

        # Validate the move
        if not self.board.is_cell_empty(row, col):
            raise InvalidMoveException(
                f"Cell ({row}, {col}) is already occupied"
            )

        # Place the symbol
        current_player = self.players[self.current_player_index]

        self.board.place_symbol(
            row,
            col,
            current_player.get_symbol()
        )

        # Check for win
        if self.check_win(
            row,
            col,
            current_player.get_symbol()
        ):
            if current_player.get_symbol() == Symbol.X:
                self.status = GameStatus.WINNER_X
            else:
                self.status = GameStatus.WINNER_O

            return

        # Check for draw
        if self.board.is_full():
            self.status = GameStatus.DRAW
            return

        # Switch player
        self.current_player_index = (
            self.current_player_index + 1
        ) % 2

    def check_win(self, row, col, symbol):

        size = self.board.get_size()

        # Check row
        win = True

        for c in range(size):
            if self.board.get_cell(row, c).get_symbol() != symbol:
                win = False
                break

        if win:
            return True

        # Check column
        win = True

        for r in range(size):
            if self.board.get_cell(r, col).get_symbol() != symbol:
                win = False
                break

        if win:
            return True

        # Check main diagonal
        if row == col:
            win = True

            for i in range(size):
                if self.board.get_cell(i, i).get_symbol() != symbol:
                    win = False
                    break

            if win:
                return True

        # Check anti-diagonal
        if row + col == size - 1:
            win = True

            for i in range(size):
                if (
                    self.board
                    .get_cell(i, size - 1 - i)
                    .get_symbol() != symbol
                ):
                    win = False
                    break

            if win:
                return True

        return False

    def get_board(self):
        return self.board

    def get_current_player(self):
        return self.players[self.current_player_index]

    def get_status(self):
        return self.status

    def get_winner(self):

        if self.status == GameStatus.WINNER_X:

            if self.players[0].get_symbol() == Symbol.X:
                return self.players[0]

            return self.players[1]

        elif self.status == GameStatus.WINNER_O:

            if self.players[0].get_symbol() == Symbol.O:
                return self.players[0]

            return self.players[1]

        return None

    def print_board(self):
        self.board.print_board()
from enums.Symbol import Symbol


class Player:

    def __init__(self, name, symbol):
        if symbol == Symbol.EMPTY:
            raise ValueError("Player cannot have EMPTY symbol")

        self.name = name
        self.symbol = symbol

    def get_name(self):
        return self.name

    def get_symbol(self):
        return self.symbol

    def __str__(self):
        return f"{self.name} ({self.symbol.get_display_char()})"
from enum import Enum


class GameStatus(Enum):
    IN_PROGRESS = 1
    WINNER_X = 2
    WINNER_O = 3
    DRAW = 4
from enum import Enum


class Symbol(Enum):
    X = 'X'
    O = 'O'
    EMPTY = '_'

    def get_display_char(self):
        return self.value
class InvalidMoveException(Exception):
    pass
#include <iostream>

#include "entities/Player.h"
#include "entities/Game.h"
#include "enums/Symbol.h"

int main() {

    Player alice("Alice", Symbol::X);

    Player bob("Bob", Symbol::O);

    Game game(&alice, &bob, 3);

    std::cout
        << "========== TIC TAC TOE =========="
        << std::endl;

    // Alice (X) completes the top row and wins

    game.makeMove(0, 0);  // X at (0,0)
    game.makeMove(1, 0);  // O at (1,0)
    game.makeMove(0, 1);  // X at (0,1)
    game.makeMove(1, 1);  // O at (1,1)
    game.makeMove(0, 2);  // X at (0,2) - Alice wins!

    game.printBoard();

    Player* winner = game.getWinner();

    if (winner != nullptr) {
        std::cout
            << "Winner: "
            << winner->getName()
            << std::endl;
    }

    return 0;
}
#include "Board.h"
#include "../exceptions/InvalidMoveException.h"

#include <iostream>
#include <string>

Board::Board(int size) {

    if (size <= 0) {
        throw InvalidMoveException(
            "Board size must be greater than 0"
        );
    }

    this->size = size;

    grid = new Cell*[size];

    for (int i = 0; i < size; i++) {
        grid[i] = new Cell[size];
    }

    initializeBoard();
}

Board::~Board() {

    for (int i = 0; i < size; i++) {
        delete[] grid[i];
    }

    delete[] grid;
}

void Board::initializeBoard() {

    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {
            grid[i][j] = Cell();
        }
    }
}

void Board::placeSymbol(
    int row,
    int col,
    Symbol symbol
) {
    validatePosition(row, col);

    grid[row][col].setSymbol(symbol);
}

bool Board::isCellEmpty(
    int row,
    int col
) const {

    validatePosition(row, col);

    return grid[row][col].isEmpty();
}

bool Board::isFull() const {

    for (int i = 0; i < size; i++) {
        for (int j = 0; j < size; j++) {

            if (grid[i][j].isEmpty()) {
                return false;
            }
        }
    }

    return true;
}

Cell& Board::getCell(
    int row,
    int col
) {
    validatePosition(row, col);

    return grid[row][col];
}

const Cell& Board::getCell(
    int row,
    int col
) const {
    validatePosition(row, col);

    return grid[row][col];
}

int Board::getSize() const {
    return size;
}

void Board::validatePosition(
    int row,
    int col
) const {

    if (
        row < 0 ||
        row >= size ||
        col < 0 ||
        col >= size
    ) {
        throw InvalidMoveException(
            "Position (" +
            std::to_string(row) +
            ", " +
            std::to_string(col) +
            ") is out of bounds"
        );
    }
}

void Board::printBoard() const {

    std::cout << std::endl;

    for (int i = 0; i < size; i++) {

        for (int j = 0; j < size; j++) {

            std::cout
                << " "
                << getDisplayChar(
                    grid[i][j].getSymbol()
                )
                << " ";

            if (j < size - 1) {
                std::cout << "|";
            }
        }

        std::cout << std::endl;

        if (i < size - 1) {

            for (int k = 0; k < size * 4 - 1; k++) {
                std::cout << "-";
            }

            std::cout << std::endl;
        }
    }

    std::cout << std::endl;
}
#ifndef GAME_STATUS_H
#define GAME_STATUS_H

enum class GameStatus {
    IN_PROGRESS,
    WINNER_X,
    WINNER_O,
    DRAW
};

#endif
#ifndef INVALID_MOVE_EXCEPTION_H
#define INVALID_MOVE_EXCEPTION_H

#include <stdexcept>
#include <string>

class InvalidMoveException : public std::runtime_error {

public:

    explicit InvalidMoveException(const std::string& message)
        : std::runtime_error(message) {}
};

#endif
#ifndef SYMBOL_H
#define SYMBOL_H

enum class Symbol {
    X,
    O,
    EMPTY
};

inline char getDisplayChar(Symbol symbol) {
    switch (symbol) {
        case Symbol::X:
            return 'X';

        case Symbol::O:
            return 'O';

        case Symbol::EMPTY:
            return '_';
    }

    return '_';
}

#endif
#ifndef BOARD_H
#define BOARD_H

#include "Cell.h"
#include "../enums/Symbol.h"

class Board {

private:
    Cell** grid;
    int size;

    void initializeBoard();

    void validatePosition(int row, int col) const;

public:

    explicit Board(int size);

    ~Board();

    void placeSymbol(
        int row,
        int col,
        Symbol symbol
    );

    bool isCellEmpty(
        int row,
        int col
    ) const;

    bool isFull() const;

    Cell& getCell(
        int row,
        int col
    );

    const Cell& getCell(
        int row,
        int col
    ) const;

    int getSize() const;

    void printBoard() const;
};

#endif
#ifndef CELL_H
#define CELL_H

#include "../enums/Symbol.h"

class Cell {

private:
    Symbol symbol;

public:

    Cell();

    Symbol getSymbol() const;

    void setSymbol(Symbol symbol);

    bool isEmpty() const;
};

#endif
#include "Cell.h"

Cell::Cell() {
    symbol = Symbol::EMPTY;
}

Symbol Cell::getSymbol() const {
    return symbol;
}

void Cell::setSymbol(Symbol symbol) {
    this->symbol = symbol;
}

bool Cell::isEmpty() const {
    return symbol == Symbol::EMPTY;
}
#ifndef GAME_H
#define GAME_H

#include "Board.h"
#include "Player.h"
#include "../enums/GameStatus.h"

#include <mutex>

class Game {

private:
    Board board;

    Player* players[2];

    int currentPlayerIndex;

    GameStatus status;

    std::mutex gameMutex;

    bool checkWin(
        int row,
        int col,
        Symbol symbol
    );

public:

    Game(
        Player* player1,
        Player* player2,
        int boardSize
    );

    void makeMove(
        int row,
        int col
    );

    Board& getBoard();

    Player* getCurrentPlayer();

    GameStatus getStatus() const;

    Player* getWinner();

    void printBoard();
};

#endif
#ifndef PLAYER_H
#define PLAYER_H

#include <string>
#include "../enums/Symbol.h"

class Player {

private:
    std::string name;
    Symbol symbol;

public:

    Player(const std::string& name, Symbol symbol);

    std::string getName() const;

    Symbol getSymbol() const;
};

#endif
#include "Game.h"
#include "../exceptions/InvalidMoveException.h"

Game::Game(
    Player* player1,
    Player* player2,
    int boardSize
)
    : board(boardSize) {

    players[0] = player1;
    players[1] = player2;

    currentPlayerIndex = 0;

    status = GameStatus::IN_PROGRESS;
}

void Game::makeMove(
    int row,
    int col
) {

    std::lock_guard<std::mutex> lock(gameMutex);

    // Check if game is already over
    if (status != GameStatus::IN_PROGRESS) {
        throw InvalidMoveException(
            "Game is already over!"
        );
    }

    // Validate move
    if (!board.isCellEmpty(row, col)) {
        throw InvalidMoveException(
            "Cell (" +
            std::to_string(row) +
            ", " +
            std::to_string(col) +
            ") is already occupied"
        );
    }

    // Place symbol
    Player* currentPlayer =
        players[currentPlayerIndex];

    board.placeSymbol(
        row,
        col,
        currentPlayer->getSymbol()
    );

    // Check win
    if (
        checkWin(
            row,
            col,
            currentPlayer->getSymbol()
        )
    ) {

        if (currentPlayer->getSymbol() == Symbol::X) {
            status = GameStatus::WINNER_X;
        } else {
            status = GameStatus::WINNER_O;
        }

        return;
    }

    // Check draw
    if (board.isFull()) {
        status = GameStatus::DRAW;
        return;
    }

    // Switch player
    currentPlayerIndex =
        (currentPlayerIndex + 1) % 2;
}

bool Game::checkWin(
    int row,
    int col,
    Symbol symbol
) {

    int size = board.getSize();

    // Check row
    bool win = true;

    for (int c = 0; c < size; c++) {

        if (
            board.getCell(row, c).getSymbol()
            != symbol
        ) {
            win = false;
            break;
        }
    }

    if (win) {
        return true;
    }

    // Check column
    win = true;

    for (int r = 0; r < size; r++) {

        if (
            board.getCell(r, col).getSymbol()
            != symbol
        ) {
            win = false;
            break;
        }
    }

    if (win) {
        return true;
    }

    // Main diagonal
    if (row == col) {

        win = true;

        for (int i = 0; i < size; i++) {

            if (
                board.getCell(i, i).getSymbol()
                != symbol
            ) {
                win = false;
                break;
            }
        }

        if (win) {
            return true;
        }
    }

    // Anti-diagonal
    if (row + col == size - 1) {

        win = true;

        for (int i = 0; i < size; i++) {

            if (
                board
                    .getCell(i, size - 1 - i)
                    .getSymbol()
                != symbol
            ) {
                win = false;
                break;
            }
        }

        if (win) {
            return true;
        }
    }

    return false;
}

Board& Game::getBoard() {
    return board;
}

Player* Game::getCurrentPlayer() {
    return players[currentPlayerIndex];
}

GameStatus Game::getStatus() const {
    return status;
}

Player* Game::getWinner() {

    if (status == GameStatus::WINNER_X) {

        if (players[0]->getSymbol() == Symbol::X) {
            return players[0];
        }

        return players[1];
    }

    if (status == GameStatus::WINNER_O) {

        if (players[0]->getSymbol() == Symbol::O) {
            return players[0];
        }

        return players[1];
    }

    return nullptr;
}

void Game::printBoard() {
    board.printBoard();
}
#include "Player.h"
#include "../exceptions/InvalidMoveException.h"

Player::Player(
    const std::string& name,
    Symbol symbol
) {
    if (symbol == Symbol::EMPTY) {
        throw InvalidMoveException(
            "Player cannot have EMPTY symbol"
        );
    }

    this->name = name;
    this->symbol = symbol;
}

std::string Player::getName() const {
    return name;
}

Symbol Player::getSymbol() const {
    return symbol;
}
package main

import (
	"fmt"

	"tic-tac-toe/entities"
	"tic-tac-toe/enums"
)

func main() {

	alice, err := entities.NewPlayer(
		"Alice",
		enums.X,
	)

	if err != nil {
		fmt.Println(err)
		return
	}

	bob, err := entities.NewPlayer(
		"Bob",
		enums.O,
	)

	if err != nil {
		fmt.Println(err)
		return
	}

	game := entities.NewGame(
		alice,
		bob,
		3,
	)

	fmt.Println(
		"========== TIC TAC TOE ==========",
	)

	// Alice (X) completes the top row and wins

	err = game.MakeMove(0, 0) // X at (0,0)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = game.MakeMove(1, 0) // O at (1,0)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = game.MakeMove(0, 1) // X at (0,1)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = game.MakeMove(1, 1) // O at (1,1)
	if err != nil {
		fmt.Println(err)
		return
	}

	err = game.MakeMove(0, 2) // X at (0,2) - Alice wins!
	if err != nil {
		fmt.Println(err)
		return
	}

	game.PrintBoard()

	winner := game.GetWinner()

	if winner != nil {
		fmt.Println(
			"Winner:",
			winner.GetName(),
		)
	}
}
package entities

import (
	"fmt"
	"strings"

	"tic-tac-toe/enums"
	"tic-tac-toe/exceptions"
)

type Board struct {
	grid [][]*Cell
	size int
}

func NewBoard(size int) *Board {

	board := &Board{
		size: size,
		grid: make([][]*Cell, size),
	}

	board.initializeBoard()

	return board
}

func (b *Board) initializeBoard() {

	for i := 0; i < b.size; i++ {

		b.grid[i] = make([]*Cell, b.size)

		for j := 0; j < b.size; j++ {
			b.grid[i][j] = NewCell()
		}
	}
}

func (b *Board) PlaceSymbol(
	row int,
	col int,
	symbol enums.Symbol,
) error {

	if err := b.validatePosition(row, col); err != nil {
		return err
	}

	b.grid[row][col].SetSymbol(symbol)

	return nil
}

func (b *Board) IsCellEmpty(
	row int,
	col int,
) (bool, error) {

	if err := b.validatePosition(row, col); err != nil {
		return false, err
	}

	return b.grid[row][col].IsEmpty(), nil
}

func (b *Board) IsFull() bool {

	for i := 0; i < b.size; i++ {

		for j := 0; j < b.size; j++ {

			if b.grid[i][j].IsEmpty() {
				return false
			}
		}
	}

	return true
}

func (b *Board) GetCell(
	row int,
	col int,
) (*Cell, error) {

	if err := b.validatePosition(row, col); err != nil {
		return nil, err
	}

	return b.grid[row][col], nil
}

func (b *Board) GetSize() int {
	return b.size
}

func (b *Board) validatePosition(
	row int,
	col int,
) error {

	if (
		row < 0 ||
		row >= b.size ||
		col < 0 ||
		col >= b.size
	) {
		return exceptions.NewInvalidMoveException(
			fmt.Sprintf(
				"Position (%d, %d) is out of bounds",
				row,
				col,
			),
		)
	}

	return nil
}

func (b *Board) PrintBoard() {

	fmt.Println()

	for i := 0; i < b.size; i++ {

		for j := 0; j < b.size; j++ {

			fmt.Printf(
				" %c ",
				b.grid[i][j].GetSymbol().GetDisplayChar(),
			)

			if j < b.size-1 {
				fmt.Print("|")
			}
		}

		fmt.Println()

		if i < b.size-1 {
			fmt.Println(
				strings.Repeat(
					"-",
					b.size*4-1,
				),
			)
		}
	}

	fmt.Println()
}
package enums

type Symbol int

const (
	X Symbol = iota
	O
	EMPTY
)

func (s Symbol) GetDisplayChar() rune {
	switch s {
	case X:
		return 'X'
	case O:
		return 'O'
	case EMPTY:
		return '_'
	default:
		return '_'
	}
}
package exceptions

type InvalidMoveException struct {
	Message string
}

func (e *InvalidMoveException) Error() string {
	return e.Message
}

func NewInvalidMoveException(message string) error {
	return &InvalidMoveException{
		Message: message,
	}
}
package entities

import "tic-tac-toe/enums"

type Cell struct {
	symbol enums.Symbol
}

func NewCell() *Cell {
	return &Cell{
		symbol: enums.EMPTY,
	}
}

func (c *Cell) GetSymbol() enums.Symbol {
	return c.symbol
}

func (c *Cell) SetSymbol(symbol enums.Symbol) {
	c.symbol = symbol
}

func (c *Cell) IsEmpty() bool {
	return c.symbol == enums.EMPTY
}
package entities

import (
	"fmt"
	"sync"

	"tic-tac-toe/enums"
	"tic-tac-toe/exceptions"
)

type Game struct {
	board              *Board
	players            [2]*Player
	currentPlayerIndex int
	status             enums.GameStatus

	mutex sync.Mutex
}

func NewGame(
	player1 *Player,
	player2 *Player,
	boardSize int,
) *Game {

	return &Game{
		board: NewBoard(boardSize),

		players: [2]*Player{
			player1,
			player2,
		},

		currentPlayerIndex: 0,

		status: enums.IN_PROGRESS,
	}
}

func (g *Game) MakeMove(
	row int,
	col int,
) error {

	g.mutex.Lock()
	defer g.mutex.Unlock()

	// Check if game is already over
	if g.status != enums.IN_PROGRESS {
		return exceptions.NewInvalidMoveException(
			"Game is already over!",
		)
	}

	// Validate the move
	isEmpty, err := g.board.IsCellEmpty(row, col)

	if err != nil {
		return err
	}

	if !isEmpty {
		return exceptions.NewInvalidMoveException(
			fmt.Sprintf(
				"Cell (%d, %d) is already occupied",
				row,
				col,
			),
		)
	}

	// Place the symbol
	currentPlayer :=
		g.players[g.currentPlayerIndex]

	err = g.board.PlaceSymbol(
		row,
		col,
		currentPlayer.GetSymbol(),
	)

	if err != nil {
		return err
	}

	// Check for win
	if g.checkWin(
		row,
		col,
		currentPlayer.GetSymbol(),
	) {

		if currentPlayer.GetSymbol() == enums.X {
			g.status = enums.WINNER_X
		} else {
			g.status = enums.WINNER_O
		}

		return nil
	}

	// Check for draw
	if g.board.IsFull() {
		g.status = enums.DRAW
		return nil
	}

	// Switch to next player
	g.currentPlayerIndex =
		(g.currentPlayerIndex + 1) % 2

	return nil
}

func (g *Game) checkWin(
	row int,
	col int,
	symbol enums.Symbol,
) bool {

	size := g.board.GetSize()

	// Check the row of the last move
	win := true

	for c := 0; c < size; c++ {

		cell, _ := g.board.GetCell(row, c)

		if cell.GetSymbol() != symbol {
			win = false
			break
		}
	}

	if win {
		return true
	}

	// Check the column of the last move
	win = true

	for r := 0; r < size; r++ {

		cell, _ := g.board.GetCell(r, col)

		if cell.GetSymbol() != symbol {
			win = false
			break
		}
	}

	if win {
		return true
	}

	// Check the main diagonal
	if row == col {

		win = true

		for i := 0; i < size; i++ {

			cell, _ := g.board.GetCell(i, i)

			if cell.GetSymbol() != symbol {
				win = false
				break
			}
		}

		if win {
			return true
		}
	}

	// Check the anti-diagonal
	if row+col == size-1 {

		win = true

		for i := 0; i < size; i++ {

			cell, _ := g.board.GetCell(
				i,
				size-1-i,
			)

			if cell.GetSymbol() != symbol {
				win = false
				break
			}
		}

		if win {
			return true
		}
	}

	return false
}

func (g *Game) GetBoard() *Board {
	return g.board
}

func (g *Game) GetCurrentPlayer() *Player {
	return g.players[g.currentPlayerIndex]
}

func (g *Game) GetStatus() enums.GameStatus {
	return g.status
}

func (g *Game) GetWinner() *Player {

	if g.status == enums.WINNER_X {

		if g.players[0].GetSymbol() == enums.X {
			return g.players[0]
		}

		return g.players[1]
	}

	if g.status == enums.WINNER_O {

		if g.players[0].GetSymbol() == enums.O {
			return g.players[0]
		}

		return g.players[1]
	}

	return nil
}

func (g *Game) PrintBoard() {
	g.board.PrintBoard()
}
package entities

import (
	"fmt"

	"tic-tac-toe/enums"
	"tic-tac-toe/exceptions"
)

type Player struct {
	name   string
	symbol enums.Symbol
}

func NewPlayer(
	name string,
	symbol enums.Symbol,
) (*Player, error) {

	if symbol == enums.EMPTY {
		return nil, exceptions.NewInvalidMoveException(
			"Player cannot have EMPTY symbol",
		)
	}

	return &Player{
		name:   name,
		symbol: symbol,
	}, nil
}

func (p *Player) GetName() string {
	return p.name
}

func (p *Player) GetSymbol() enums.Symbol {
	return p.symbol
}

func (p *Player) String() string {
	return fmt.Sprintf(
		"%s (%c)",
		p.name,
		p.symbol.GetDisplayChar(),
	)
}
package enums

type GameStatus int

const (
	IN_PROGRESS GameStatus = iota
	WINNER_X
	WINNER_O
	DRAW
)