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:
- Horizontally — three matching symbols in the same row.
- Vertically — three matching symbols in the same column.
- 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
- The game is played on a 3×3 grid.
- Two players take alternate turns and are identified by the markers X and O.
- The system should detect and announce the winner when a player completes a winning combination.
- The system should declare a draw when all cells are occupied and neither player has won.
- The system should reject invalid moves, such as selecting an occupied or out-of-bounds cell.
- Moves may be hardcoded in a driver/demo class to simulate gameplay.
Non-Functional Requirements
- The design should follow object-oriented principles, with clear responsibilities and separation of concerns.
- The system should be modular and extensible, allowing future features such as larger boards, AI opponents, move history, undo/redo, and multiple games.
- The game logic should be easy to test and maintain.
- 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:
BoardHowever, 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:
CellThe 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:
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.
| Entity | Type | Responsibility |
|---|---|---|
| Symbol | Enum | Represents valid cell values: X, O, or EMPTY |
| GameStatus | Enum | Represents the game state: IN_PROGRESS, WINNER_X, WINNER_O, or DRAW |
| Cell | Data Class | Represents a single board position and its current symbol |
| Player | Data Class | Represents a player and their assigned symbol |
| Board | Core Class | Manages the 3×3 grid and board-level operations |
| Game | Core Class | Orchestrates 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.
| Value | Display Character | Purpose |
|---|---|---|
| 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.
| Value | Description | Terminal? |
|---|---|---|
| IN_PROGRESS | Game is still being played | No |
| WINNER_X | Player with symbol X has won | Yes |
| WINNER_O | Player with symbol O has won | Yes |
| DRAW | Board is full and neither player has won | Yes |
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:
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.symbolWith dedicated terminal states, the status itself contains the necessary information:
WINNER_Xdirectly 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.
Attributes
| Attribute | Type | Description |
|---|---|---|
| name | String | Player identifier, such as Alice |
| symbol | Symbol | Marker assigned to the player: X or O |
Methods
| Method | Description |
|---|---|
| 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.
Attributes
| Attribute | Type | Description |
|---|---|---|
| symbol | Symbol | Current value of the cell: X, O, or EMPTY |
Methods
| Method | Description |
|---|---|
| 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.
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
| Attribute | Type | Description |
|---|---|---|
| grid | Cell[][] | Two-dimensional collection representing the board |
| size | int | Board dimension; 3 for the standard game |
Methods
| Method | Description |
|---|---|
| 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:
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.
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
| Attribute | Type | Description |
|---|---|---|
| board | Board | The board on which the game is played |
| players | Player[] | The two players participating in the game |
| currentPlayerIndex | int | Identifies whose turn it is; 0 or 1 |
| status | GameStatus | Represents the current game state |
Methods
| Method | Description |
|---|---|
| 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:
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:
The final design has a clear separation of responsibilities:
| Component | Primary Responsibility |
|---|---|
| Symbol | Defines valid board markers |
| GameStatus | Defines the game's lifecycle states |
| Cell | Represents one board position |
| Player | Represents a participant and their marker |
| Board | Manages the grid and board-level operations |
| Game | Orchestrates 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.EMPTYThis 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
)