Generalizing a board game into Board, Player, GameState, and a pluggable WinningStrategy — the design that lets the same skeleton support Connect-4 or an N x N board with minimal change.
Published September 23, 2026
A prompt worth over-generalizing deliberately — the value isn't a working tic-tac-toe, it's a design that survives "now make it Connect-4" as a follow-up.
class Board {
private final char[][] grid;
private final int size;
Board(int size) { this.size = size; this.grid = new char[size][size]; }
boolean placeMark(int row, int col, char symbol) {
if (grid[row][col] != 0) return false; // cell occupied
grid[row][col] = symbol;
return true;
}
char get(int row, int col) { return grid[row][col]; }
int getSize() { return size; }
}
class Player {
private final String name;
private final char symbol;
}
enum GameState { IN_PROGRESS, WON, DRAW }
class Game {
private final Board board;
private final List<Player> players;
private final WinningStrategy winningStrategy; // pluggable — the key generalization point
private int currentPlayerIndex = 0;
private GameState state = GameState.IN_PROGRESS;
boolean makeMove(int row, int col) {
Player current = players.get(currentPlayerIndex);
if (!board.placeMark(row, col, current.getSymbol())) return false;
if (winningStrategy.checkWin(board, current.getSymbol())) state = GameState.WON;
else if (isBoardFull()) state = GameState.DRAW;
else currentPlayerIndex = (currentPlayerIndex + 1) % players.size();
return true;
}
}
interface WinningStrategy { boolean checkWin(Board board, char symbol); }
class ThreeInARowStrategy implements WinningStrategy {
public boolean checkWin(Board board, char symbol) {
int n = board.getSize();
for (int i = 0; i < n; i++) {
if (checkRow(board, i, symbol) || checkColumn(board, i, symbol)) return true;
}
return checkDiagonals(board, symbol);
}
// checkRow/checkColumn/checkDiagonals: scan for `n` consecutive matching symbols
}
class ConnectKStrategy implements WinningStrategy {
private final int k; // 4 for Connect-4
ConnectKStrategy(int k) { this.k = k; }
public boolean checkWin(Board board, char symbol) {
// scan every direction (horizontal, vertical, both diagonals) for k consecutive matches
return hasKConsecutive(board, symbol, k);
}
}
This is Strategy Pattern applied to "what counts as a win" — Game, Board, and Player never need to know or care whether winning means three-in-a-row or four-in-a-row; they only ever call winningStrategy.checkWin(board, symbol). Supporting Connect-4 means writing one new WinningStrategy implementation and configuring Game with it — zero changes to Board, Player, or Game's own turn-management logic.
The design already supports this — Board's constructor already takes a size parameter, and ThreeInARowStrategy's row/column/diagonal scans are already written generically in terms of board.getSize(), not a hard-coded 3. The only assumption worth calling out explicitly: for tic-tac-toe specifically, a win condition of "N in a row on an N x N board" is a design choice, not a law of nature — a 5x5 board might reasonably still only require 3 (or 4) in a row to win, which would mean decoupling the win-length from the board size as two separate configuration values rather than assuming they're always equal.
Q: Why does checkWin() take the board and symbol as parameters instead of the strategy holding a reference to the board itself? A: Keeping WinningStrategy stateless (no board reference stored) means a single strategy instance can be reused across multiple concurrent games safely, and it makes the strategy trivially unit-testable in isolation by passing in any board configuration directly, without needing to construct a full Game first.
Q: How would you support a 3-player variant? A: The design already generalizes reasonably well — Game already holds a List<Player> and cycles through it via modulo, not a hard-coded two-player assumption. The larger design question a 3-player variant actually raises is what a 'win' even means with three competing symbols, which is a WinningStrategy concern, not a Game/Board concern.
Q: Is Board responsible for checking whether a move is legal beyond 'is this cell empty'? A: For tic-tac-toe, cell-occupancy is the only legality rule — but naming this explicitly matters, because a different board game (chess, say) would need move legality to depend on piece type and game rules far beyond simple occupancy, which is exactly the kind of complexity Chess Engine Design tackles separately as its own concern, not bolted onto a generic Board class.