Features Required:
Initialization: Ability to initialize a new game.
Move Validation: Check if a move is valid.
Board Update: Update the board with the player's move.
Win Condition: Check for a win condition.
Draw Condition: Check for a draw condition.
Player Switching: Switch between players after each move.
Design Patterns Involved:
Singleton Pattern: To ensure there is only one instance of the game controller.
Factory Pattern: To create player instances.
Observer Pattern: To notify players of the game state changes.
Strategy Pattern: To encapsulate the algorithm for move validation and win/draw condition checking.
Diagram
Detailed Implementation:
Singleton Pattern:
We will use the Singleton pattern to ensure that there is only one instance of the game controller.
public class GameController {
private static GameController instance;
private GameController() {
// private constructor to prevent instantiation
}
public static GameController getInstance() {
if (instance == null) {
instance = new GameController();
}
return instance;
}
}
Factory Pattern:
We will use the Factory pattern to create player instances.
public abstract class Player {
protected char symbol;
public char getSymbol() {
return symbol;
}
public abstract void makeMove(Board board);
}
public class PlayerFactory {
public static Player createPlayer(char symbol) {
return new HumanPlayer(symbol);
}
}
public class HumanPlayer extends Player {
public HumanPlayer(char symbol) {
this.symbol = symbol;
}
@Override
public void makeMove(Board board) {
// Implementation for making a move
}
public void update(Board board) {
// Implementation to update player with board state
}
}
Observer Pattern:
We will use the Observer pattern to notify players of the game state changes.
import java.util.ArrayList;
import java.util.List;
public class Board {
private char[][] board;
private List<Player> observers = new ArrayList<>();
public Board(int size) {
board = new char[size][size];
}
public void addObserver(Player player) {
observers.add(player);
}
public void notifyObservers() {
for (Player player : observers) {
player.update(this);
}
}
public void updateBoard(int x, int y, char symbol) {
board[x][y] = symbol;
notifyObservers();
}
public char getCell(int x, int y) {
return board[x][y];
}
public int getSize() {
return board.length;
}
}
Strategy Pattern:
We will use the Strategy pattern to encapsulate the algorithms for move validation and win/draw condition checking.
public interface MoveStrategy {
boolean isValidMove(Board board, int x, int y);
}
public interface WinStrategy {
boolean checkWin(Board board, char symbol);
}
public class DefaultMoveStrategy implements MoveStrategy {
@Override
public boolean isValidMove(Board board, int x, int y) {
return board.getCell(x, y) == '\0';
}
}
public class DefaultWinStrategy implements WinStrategy {
@Override
public boolean checkWin(Board board, char symbol) {
int size = board.getSize();
// Check rows and columns
for (int i = 0; i < size; i++) {
if (checkRow(board, symbol, i) || checkColumn(board, symbol, i)) {
return true;
}
}
// Check diagonals
return checkDiagonal(board, symbol) || checkAntiDiagonal(board, symbol);
}
private boolean checkRow(Board board, char symbol, int row) {
for (int i = 0; i < board.getSize(); i++) {
if (board.getCell(row, i) != symbol) {
return false;
}
}
return true;
}
private boolean checkColumn(Board board, char symbol, int col) {
for (int i = 0; i < board.getSize(); i++) {
if (board.getCell(i, col) != symbol) {
return false;
}
}
return true;
}
private boolean checkDiagonal(Board board, char symbol) {
for (int i = 0; i < board.getSize(); i++) {
if (board.getCell(i, i) != symbol) {
return false;
}
}
return true;
}
private boolean checkAntiDiagonal(Board board, char symbol) {
int size = board.getSize();
for (int i = 0; i < size; i++) {
if (board.getCell(i, size - 1 - i) != symbol) {
return false;
}
}
return true;
}
}
Complete Implementation:
import java.util.Scanner;
public class TicTacToe {
private Board board;
private Player currentPlayer;
private Player player1;
private Player player2;
private MoveStrategy moveStrategy;
private WinStrategy winStrategy;
public TicTacToe() {
GameController gameController = GameController.getInstance();
board = new Board(3);
player1 = PlayerFactory.createPlayer('X');
player2 = PlayerFactory.createPlayer('O');
currentPlayer = player1;
moveStrategy = new DefaultMoveStrategy();
winStrategy = new DefaultWinStrategy();
board.addObserver(player1);
board.addObserver(player2);
}
public void playGame() {
Scanner scanner = new Scanner(System.in);
while (true) {
System.out.println("Player " + currentPlayer.getSymbol() + "'s turn");
System.out.println("Enter row (0, 1, or 2): ");
int x = scanner.nextInt();
System.out.println("Enter column (0, 1, or 2): ");
int y = scanner.nextInt();
// Validate move
if (moveStrategy.isValidMove(board, x, y)) {
// Update board
board.updateBoard(x, y, currentPlayer.getSymbol());
// Check for win
if (winStrategy.checkWin(board, currentPlayer.getSymbol())) {
System.out.println("Player " + currentPlayer.getSymbol() + " wins!");
break;
}
// Check for draw
if (isDraw()) {
System.out.println("Game is a draw!");
break;
}
// Switch player
switchPlayer();
} else {
System.out.println("Invalid move! Try again.");
}
}
scanner.close();
}
private void switchPlayer() {
currentPlayer = (currentPlayer == player1) ? player2 : player1;
}
private boolean isDraw() {
for (int i = 0; i < board.getSize(); i++) {
for (int j = 0; j < board.getSize(); j++) {
if (board.getCell(i, j) == '\0') {
return false;
}
}
}
return true;
}
public static void main(String[] args) {
TicTacToe game = new TicTacToe();
game.playGame();
}
}
Explanation:
GameController: Ensures a single instance of the game controller using the Singleton pattern.
Player and PlayerFactory: Creates player instances using the Factory pattern.
HumanPlayer
extendsPlayer
and implements themakeMove
method.Board: Manages the game board and uses the Observer pattern to notify players of state changes.
MoveStrategy and WinStrategy: Encapsulate move validation and win condition checking using the Strategy pattern.
DefaultMoveStrategy
checks if a move is valid, andDefaultWinStrategy
checks for win conditions.TicTacToe: The main game class initializes the board, players, and strategies, and contains the game loop to handle player moves, check for wins/draws, and switch players.
Issues in the Above Design (Covered in our premium course)
Limited to Human Players:
- The current design only supports human players. It lacks the flexibility to easily incorporate AI players or networked multiplayer functionality.
Hardcoded Board Size:
- The board size is hardcoded to 3x3. This restricts the game to traditional Tic-Tac-Toe and does not support variations like 4x4 or larger grids.
Game Modes:
- Above design does not support multiple game modes like human vs AI or AI vs AI etc.
Multiple Game:
- Above design does not support multiple Tic-Tac-Toe games simultaneously.
Lack of Clear Separation of Concerns:
- The
TicTacToe
class handles multiple responsibilities, including game control, input handling, and player switching, which can make the code harder to maintain. Similarly Some Other classes also.
- The
Soon will add YouTube video on this channel - https://www.youtube.com/channel/UCgdIgkU_hq0VtGPhVPA0CPQ