System Design
Design With Sid
system design

Designing and Implementing Tic-Tac-Toe: A Practical Guide to System Design

Designing and Implementing Tic-Tac-Toe: A Practical Guide to System Design

Introduction

We all remember playing Tic-Tac-Toe as kids, a simple yet incredibly strategic game that often resulted in many draws. But have you ever thought about how to design and implement this game programmatically? In this post, we’ll walk through the system design for a Tic-Tac-Toe game. Much like our​⬤

Step 1: Player Entity

The first part of our Tic-Tac-Toe game design begins with the Player entity. A player is essential in any game design as they represent the individual participants who take turns to play. Each player needs a name and a unique symbol to mark their moves on the board (either 'X' or 'O').

Here’s the code for the Player class:

package entities;

public class Player {
    private String name;
    private char symbol;

    public Player(String name, char symbol) {
        this.name = name;
        this.symbol = symbol;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public char getSymbol() {
        return symbol;
    }

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

Step 2: Board Entity

In this step, we design the Board class, which represents the 2D grid where players make their moves. The Board class also contains logic for checking if a player wins after each move by evaluating the rows, columns, and diagonals.

Here’s the updated Board class:

package entities;

public class Board {
    private Player[][] board; // A 2D array holding Player references
    private int n;            // Size of the board, e.g., 3 for a 3x3 grid

    public Board(int n) {
        this.n = n;
        board = new Player[n][n]; // Initialize board with nulls
    }

    public Player[][] getBoard() {
        return board;
    }

    public void setBoard(Player[][] board) {
        this.board = board;
    }

    public int getSize() {
        return n;
    }

    public void setN(int n) {
        this.n = n;
    }

    // Function to make a move on the board
    public boolean makeMove(int row, int col, Player player) {
        if (row < 0 || row >= n || col < 0 || col >= n || board[row][col] != null) {
            return false; // Invalid move
        }

        board[row][col] = player;  // Place the player's move
        return checkWinner(row, col, player); // Check if the move wins the game
    }

    // Check if the player wins with this move
    private boolean checkWinner(int row, int col, Player player) {
        // Check the row of the last move
        boolean winRow = true;
        for (int i = 0; i < n; i++) {
            if (board[row][i] != player) {
                winRow = false;
                break;
            }
        }

        // Check the column of the last move
        boolean winCol = true;
        for (int i = 0; i < n; i++) {
            if (board[i][col] != player) {
                winCol = false;
                break;
            }
        }

        // Check the main diagonal (if applicable)
        boolean winDiag1 = true;
        if (row == col) { // The move is on the main diagonal
            for (int i = 0; i < n; i++) {
                if (board[i][i] != player) {
                    winDiag1 = false;
                    break;
                }
            }
        } else {
            winDiag1 = false;
        }

        // Check the anti-diagonal (if applicable)
        boolean winDiag2 = true;
        if (row + col == n - 1) { // The move is on the anti-diagonal
            for (int i = 0; i < n; i++) {
                if (board[i][n - 1 - i] != player) {
                    winDiag2 = false;
                    break;
                }
            }
        } else {
            winDiag2 = false;
        }

        // Return true if the player has won on any row, column, or diagonal
        return winRow || winCol || winDiag1 || winDiag2;
    }

    // Optional: Print the board for debugging
    public void printBoard() {
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < n; j++) {
                if (board[i][j] != null) {
                    System.out.print(board[i][j].getSymbol() + " "); // Display player's initial
                } else {
                    System.out.print("- "); // Empty cell
                }
            }
            System.out.println();
        }
        System.out.println();
    }
}

Step 3: Game Entity

Here's the Game entity, which integrates the Board and Player classes to manage the overall flow of Tic-Tac-Toe, including alternating player turns, checking for wins, and handling the end of the game.

package entities;

import java.util.List;

public class Game {
    private Board board;
    private List<Player> players;
    private int currentPlayerIndex;
    private boolean isGameOver;
    
    // Constructor to initialize the game
    public Game(int boardSize, List<Player> players) {
        this.board = new Board(boardSize);  // Create the board with given size
        this.players = players;             // Set the list of players
        this.currentPlayerIndex = 0;        // Set the first player to start
        this.isGameOver = false;            // Game starts as not over
    }
    
    // Method to make a move and play the game
    public void playGame(int row, int col) {
        if (isGameOver) {
            System.out.println("Game over! No more moves can be made.");
            return;  // If the game is already over, no further moves are allowed
        }
        
        Player currentPlayer = players.get(currentPlayerIndex);  // Get current player
        boolean hasWon = board.makeMove(row, col, currentPlayer); // Make the move

        // If the player wins, print the board and declare the winner
        if (hasWon) {
            board.printBoard();  // Print the final board state
            System.out.println(currentPlayer.getName() + " wins the game!");
            isGameOver = true;
            return;  // End the game
        }
        
        // Switch to the next player
        currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
        board.printBoard();  // Print the board after each move
    }
    
    // Method to check if the game is a draw
    public boolean isDraw() {
        // If there is any empty spot left, it's not a draw
        for (int i = 0; i < board.getSize(); i++) {
            for (int j = 0; j < board.getSize(); j++) {
                if (board.getBoard()[i][j] == null) {
                    return false; // Still moves possible
                }
            }
        }
        return !isGameOver;  // Return true if all spots are filled and no winner
    }
    
    // Method to check if the game is over
    public boolean isGameOver() {
        return isGameOver;
    }
}

Step 4: PlayGame Class

The PlayGame class serves as the main driver to initiate and play the Tic-Tac-Toe game. It creates players, sets up the game, and demonstrates gameplay through a series of moves.

package entities;

import java.util.List;

public class PlayGame {

    public static void main(String[] args) {
        // Create players
        Player player1 = new Player("Alice", 'X'); // Alice uses 'X'
        Player player2 = new Player("Bob", 'O');    // Bob uses 'O'

        // Create a list of players
        List<Player> players = List.of(player1, player2); // Store players in a list
        Game game = new Game(3, players); // Create a new game with a 3x3 board
        
        // Sample game play demonstrating a sequence of moves
        game.playGame(0, 0);  // Alice's turn (places 'X' in the top-left corner)
        game.playGame(1, 1);  // Bob's turn (places 'O' in the center)
        game.playGame(0, 1);  // Alice's turn (places 'X' in the top-center)
        game.playGame(2, 2);  // Bob's turn (places 'O' in the bottom-right corner)
        game.playGame(0, 2);  // Alice's turn (places 'X' in the top-right, Alice wins)
    }
}

Conclusion

In this blog post, we've successfully implemented a Tic-Tac-Toe game in Java, following a structured approach similar to our previous Snake and Ladder game.

Key Highlights:

  • Player Entity: We created a Player class that represents each player, encapsulating their name and symbol (X or O).
  • Board Structure: The Board class is designed to maintain the state of the game, check for winners, and manage player moves on the grid.
  • Game Logic: The Game class orchestrates the gameplay, ensuring turns are taken in sequence, checking for wins or draws after each move.
  • Gameplay Simulation: Through the PlayGame class, we demonstrated a sample gameplay session, showing how moves are made and how the game progresses to a conclusion.

Final Thoughts

This implementation provides a solid foundation for the Tic-Tac-Toe game, allowing for potential enhancements such as AI opponents, graphical interfaces, or multiplayer capabilities over networks.

Feel free to experiment with the code, customize it, and share your experiences! Happy coding!