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!