Game Design• Patterns: Strategy, Singleton, Observer
Tic Tac Toe
Easy
Problem Summary
Design a modular, extensible Tic Tac Toe engine supporting an N x N grid, arbitrary player count, dynamic AI bot difficulty strategies, and O(1) win verification.
Functional Scope
- Customizable N x N grid dimensions (default 3x3, scalable up to 100x100).
- Arbitrary number of players (2 or more) taking alternating sequential turns.
- O(1) time-complexity victory and draw condition detection on every move.
- Pluggable bot player decision strategies (Random, Minimax, Heuristic).
- Thread-safe move execution and match state querying.
Entity-Relationship (ER) Schema
GameEngine [1] <---> [1] Board GameEngine [1] <---> [2..*] Player Board [1] <---> [N*N] Cell Player <|-- HumanPlayer Player <|-- BotPlayer BotPlayer [1] --> [1] PlayingStrategy GameEngine [1] --> [1] WinConditionStrategy
Design Approach
Apply the Strategy pattern for bot move decisions, allowing seamless toggling between random, rule-based, and Minimax AI. To avoid O(N) scanning after each move, maintain separate frequency arrays for rows, cols, and the two diagonals, verifying win condition in strict O(1) time.
Core Classes & Models
GameEngine (Coordinates game loop, turns, and win detection)Board (Maintains N x N grid cells and coordinate bounds)Cell (Holds coordinate and occupying symbol)Player (Abstract base class for participants)HumanPlayer & BotPlayer (Concrete player entities)PlayingStrategy (Strategy interface for AI decision making)MinimaxPlayingStrategy & RandomPlayingStrategyWinConditionStrategy (O(1) frequency counting win evaluator)
Code Blueprint
198 lines/**
* Low-Level Design: Tic Tac Toe
* Design Patterns Applied:
* - Strategy Pattern: Pluggable bot move strategies and O(1) win verification
* - Singleton Pattern: Central match orchestrator coordinating gameplay
* - Observer Pattern: Real-time board event broadcasting to displays and auditors
*/
import java.util.*;
enum Symbol { X, O, EMPTY }
// --- [DESIGN PATTERN: Observer] Listener for live match events ---
interface BoardObserver {
void onMoveMade(int row, int col, Symbol symbol);
void onGameOver(String outcome);
}
class ConsoleBoardObserver implements BoardObserver {
@Override
public void onMoveMade(int row, int col, Symbol symbol) {
System.out.println(">> [Observer] Player placed " + symbol + " at (" + row + ", " + col + ")");
}
@Override
public void onGameOver(String outcome) {
System.out.println(">> [Observer] Game Over: " + outcome);
}
}
class Cell {
private final int row, col;
private Symbol symbol;
public Cell(int row, int col) {
this.row = row;
this.col = col;
this.symbol = Symbol.EMPTY;
}
public boolean isEmpty() { return symbol == Symbol.EMPTY; }
public Symbol getSymbol() { return symbol; }
public void setSymbol(Symbol symbol) { this.symbol = symbol; }
}
class Board {
private final int size;
private final Cell[][] grid;
private final List<BoardObserver> observers = new ArrayList<>();
public Board(int size) {
this.size = size;
this.grid = new Cell[size][size];
for (int r = 0; r < size; r++) {
for (int c = 0; c < size; c++) {
grid[r][c] = new Cell(r, c);
}
}
}
public void addObserver(BoardObserver observer) { observers.add(observer); }
private void notifyMove(int r, int c, Symbol s) {
for (BoardObserver obs : observers) obs.onMoveMade(r, c, s);
}
public void notifyGameOver(String outcome) {
for (BoardObserver obs : observers) obs.onGameOver(outcome);
}
public boolean placeSymbol(int r, int c, Symbol s) {
if (r < 0 || r >= size || c < 0 || c >= size || !grid[r][c].isEmpty()) return false;
grid[r][c].setSymbol(s);
notifyMove(r, c, s);
return true;
}
public void printBoard() {
System.out.println("-------------");
for (int r = 0; r < size; r++) {
System.out.print("| ");
for (int c = 0; c < size; c++) {
char ch = grid[r][c].getSymbol() == Symbol.EMPTY ? ' ' : grid[r][c].getSymbol().name().charAt(0);
System.out.print(ch + " | ");
}
System.out.println("\n-------------");
}
}
public int getSize() { return size; }
}
// --- [DESIGN PATTERN: Strategy] Pluggable bot move decision strategy ---
interface PlayingStrategy {
int[] decideMove(Board board, Symbol symbol);
}
class FirstAvailablePlayingStrategy implements PlayingStrategy {
@Override
public int[] decideMove(Board board, Symbol symbol) {
int n = board.getSize();
for (int r = 0; r < n; r++) {
for (int c = 0; c < n; c++) {
return new int[]{r, c};
}
}
return new int[]{-1, -1};
}
}
class Player {
private final String name;
private final Symbol symbol;
private final PlayingStrategy strategy;
public Player(String name, Symbol symbol, PlayingStrategy strategy) {
this.name = name;
this.symbol = symbol;
this.strategy = strategy;
}
public String getName() { return name; }
public Symbol getSymbol() { return symbol; }
public PlayingStrategy getStrategy() { return strategy; }
}
// --- [DESIGN PATTERN: Singleton / Context] Game manager controlling loop ---
class TicTacToeGame {
private final Board board;
private final Queue<Player> players = new LinkedList<>();
private final int[] rowCounts, colCounts;
private int diagCount = 0, antiDiagCount = 0;
private int movesMade = 0;
private boolean isGameOver = false;
public TicTacToeGame(int size, Player p1, Player p2) {
this.board = new Board(size);
this.board.addObserver(new ConsoleBoardObserver());
this.players.add(p1);
this.players.add(p2);
this.rowCounts = new int[size];
this.colCounts = new int[size];
}
public boolean makeMove(int r, int c) {
if (isGameOver) {
System.out.println("Game is already finished!");
return false;
}
Player current = players.peek();
if (!board.placeSymbol(r, c, current.getSymbol())) {
System.out.println("Invalid move at (" + r + ", " + c + "). Cell occupied or out of bounds.");
return false;
}
movesMade++;
int val = (current.getSymbol() == Symbol.X) ? 1 : -1;
rowCounts[r] += val;
colCounts[c] += val;
if (r == c) diagCount += val;
if (r + c == board.getSize() - 1) antiDiagCount += val;
board.printBoard();
int target = board.getSize();
if (Math.abs(rowCounts[r]) == target || Math.abs(colCounts[c]) == target ||
Math.abs(diagCount) == target || Math.abs(antiDiagCount) == target) {
String winMsg = "Player " + current.getName() + " (" + current.getSymbol() + ") WINS!";
System.out.println("🎉 " + winMsg);
board.notifyGameOver(winMsg);
isGameOver = true;
return true;
}
if (movesMade == target * target) {
String drawMsg = "Game is a DRAW!";
System.out.println("🤝 " + drawMsg);
board.notifyGameOver(drawMsg);
isGameOver = true;
return true;
}
players.poll();
players.add(current);
return true;
}
}
public class Main {
public static void main(String[] args) {
Player alice = new Player("Alice", Symbol.X, new FirstAvailablePlayingStrategy());
Player bob = new Player("Bob", Symbol.O, new FirstAvailablePlayingStrategy());
TicTacToeGame game = new TicTacToeGame(3, alice, bob);
System.out.println("--- Starting Tic Tac Toe Simulation ---");
game.makeMove(0, 0); // Alice X
game.makeMove(0, 1); // Bob O
game.makeMove(1, 1); // Alice X
game.makeMove(0, 2); // Bob O
game.makeMove(2, 2); // Alice X wins diagonal
}
}