Loading content...
Design patterns are not abstract academic exercises—they are proven solutions to recurring problems. A chess game system naturally presents three problems that patterns solve elegantly:
These problems map perfectly to the Strategy, Command, and Memento patterns. In this page, we'll see how each pattern transforms what could be a tangled mess of conditionals into clean, extensible, testable code.
Master the application of three classic Gang of Four patterns to chess: Strategy for piece movement behaviors, Command for move execution and undo, and Memento for game state preservation. Understand not just the patterns, but why they're the right choice for these specific problems.
The Problem:
Six piece types, each with completely different movement rules. The naive approach uses conditionals:
if (piece.type === 'KING') {
// King movement logic
} else if (piece.type === 'QUEEN') {
// Queen movement logic
} else if (piece.type === 'ROOK') {
// ... and so on
}
This violates the Open/Closed Principle—adding a new piece type (for chess variants) requires modifying this switch statement. It also lumps all logic into one place, making testing difficult.
The Strategy Pattern Solution:
Define a family of algorithms (movement behaviors), encapsulate each in its own class, and make them interchangeable. The piece holds a reference to its movement strategy.
Pattern Structure:
MovementStrategy — defines getPossibleMoves() and canAttack()KingMovement, QueenMovement, RookMovement, etc.Piece — holds a MovementStrategy and delegates movement queries to it123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206
/** * MovementStrategy - The Strategy interface * * Defines the contract for all piece movement behaviors. * Each piece type implements this differently. */interface MovementStrategy { /** * Get all squares this piece can potentially move to * (pseudo-legal moves, not considering check) */ getPossibleMoves(piece: Piece, board: Board): Position[]; /** * Can this piece attack the target square? * Used for check detection. */ canAttack(piece: Piece, target: Position, board: Board): boolean;} /** * KingMovementStrategy - One square in any direction */class KingMovementStrategy implements MovementStrategy { private static readonly DIRECTIONS: [number, number][] = [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1] ]; getPossibleMoves(piece: Piece, board: Board): Position[] { const moves: Position[] = []; const pos = piece.getPosition(); for (const [df, dr] of KingMovementStrategy.DIRECTIONS) { const target = pos.offset(df, dr); if (target && !board.isOccupiedByColor(target, piece.color)) { moves.push(target); } } return moves; } canAttack(piece: Piece, target: Position, board: Board): boolean { const pos = piece.getPosition(); const fileDiff = Math.abs(target.getFileIndex() - pos.getFileIndex()); const rankDiff = Math.abs(target.rank - pos.rank); return fileDiff <= 1 && rankDiff <= 1 && (fileDiff + rankDiff > 0); }} /** * SlidingMovementStrategy - Base class for Queen, Rook, Bishop * Uses Template Method pattern within Strategy pattern */abstract class SlidingMovementStrategy implements MovementStrategy { protected abstract getDirections(): [number, number][]; getPossibleMoves(piece: Piece, board: Board): Position[] { const moves: Position[] = []; const pos = piece.getPosition(); for (const [df, dr] of this.getDirections()) { let distance = 1; while (true) { const target = pos.offset(df * distance, dr * distance); if (!target) break; if (board.isEmpty(target)) { moves.push(target); distance++; } else if (!board.isOccupiedByColor(target, piece.color)) { moves.push(target); // Can capture break; } else { break; // Blocked by own piece } } } return moves; } canAttack(piece: Piece, target: Position, board: Board): boolean { const pos = piece.getPosition(); for (const [df, dr] of this.getDirections()) { let distance = 1; while (true) { const checkPos = pos.offset(df * distance, dr * distance); if (!checkPos) break; if (checkPos.equals(target)) return true; if (board.isOccupied(checkPos)) break; distance++; } } return false; }} class RookMovementStrategy extends SlidingMovementStrategy { protected getDirections(): [number, number][] { return [[0, 1], [0, -1], [1, 0], [-1, 0]]; }} class BishopMovementStrategy extends SlidingMovementStrategy { protected getDirections(): [number, number][] { return [[1, 1], [1, -1], [-1, 1], [-1, -1]]; }} class QueenMovementStrategy extends SlidingMovementStrategy { protected getDirections(): [number, number][] { return [ [0, 1], [0, -1], [1, 0], [-1, 0], // Rook directions [1, 1], [1, -1], [-1, 1], [-1, -1] // Bishop directions ]; }} /** * KnightMovementStrategy - L-shaped jumps */class KnightMovementStrategy implements MovementStrategy { private static readonly JUMPS: [number, number][] = [ [-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1] ]; getPossibleMoves(piece: Piece, board: Board): Position[] { const moves: Position[] = []; const pos = piece.getPosition(); for (const [df, dr] of KnightMovementStrategy.JUMPS) { const target = pos.offset(df, dr); if (target && !board.isOccupiedByColor(target, piece.color)) { moves.push(target); } } return moves; } canAttack(piece: Piece, target: Position, board: Board): boolean { const pos = piece.getPosition(); const fileDiff = Math.abs(target.getFileIndex() - pos.getFileIndex()); const rankDiff = Math.abs(target.rank - pos.rank); return (fileDiff === 2 && rankDiff === 1) || (fileDiff === 1 && rankDiff === 2); }} /** * PawnMovementStrategy - The most complex movement rules */class PawnMovementStrategy implements MovementStrategy { getPossibleMoves(piece: Piece, board: Board): Position[] { const moves: Position[] = []; const pos = piece.getPosition(); const direction = piece.color === Color.WHITE ? 1 : -1; const startRank = piece.color === Color.WHITE ? 2 : 7; // Forward one const oneForward = pos.offset(0, direction); if (oneForward && board.isEmpty(oneForward)) { moves.push(oneForward); // Forward two from start if (pos.rank === startRank) { const twoForward = pos.offset(0, direction * 2); if (twoForward && board.isEmpty(twoForward)) { moves.push(twoForward); } } } // Diagonal captures for (const fileDir of [-1, 1]) { const captureSquare = pos.offset(fileDir, direction); if (captureSquare && board.isOccupiedByOpponent(captureSquare, piece.color)) { moves.push(captureSquare); } } return moves; } canAttack(piece: Piece, target: Position, board: Board): boolean { const pos = piece.getPosition(); const direction = piece.color === Color.WHITE ? 1 : -1; const fileDiff = Math.abs(target.getFileIndex() - pos.getFileIndex()); const expectedRank = pos.rank + direction; return fileDiff === 1 && target.rank === expectedRank; }}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455
/** * Piece with Strategy Pattern * * Now Piece is simpler—it delegates movement logic to its strategy. * Adding a new piece type only requires a new strategy class. */class Piece { public readonly type: PieceType; public readonly color: Color; private position: Position; private movementStrategy: MovementStrategy; private hasMoved: boolean = false; constructor(type: PieceType, color: Color, position: Position) { this.type = type; this.color = color; this.position = position; this.movementStrategy = MovementStrategyFactory.create(type); } /** * Delegate to strategy */ public getPossibleMoves(board: Board): Position[] { return this.movementStrategy.getPossibleMoves(this, board); } public canAttack(target: Position, board: Board): boolean { return this.movementStrategy.canAttack(this, target, board); } // ... other methods unchanged} /** * Factory for creating appropriate strategy */class MovementStrategyFactory { private static strategies: Map<PieceType, MovementStrategy> = new Map([ [PieceType.KING, new KingMovementStrategy()], [PieceType.QUEEN, new QueenMovementStrategy()], [PieceType.ROOK, new RookMovementStrategy()], [PieceType.BISHOP, new BishopMovementStrategy()], [PieceType.KNIGHT, new KnightMovementStrategy()], [PieceType.PAWN, new PawnMovementStrategy()], ]); public static create(type: PieceType): MovementStrategy { const strategy = this.strategies.get(type); if (!strategy) { throw new Error(`Unknown piece type: ${type}`); } return strategy; }}We could subclass Piece (King extends Piece, Queen extends Piece). But Strategy is more flexible: strategies can be shared across pieces (fairy chess pieces might share movement with standard ones), swapped at runtime (unlikely for chess but possible), and tested in complete isolation. In practice, either approach works; Strategy shines when behaviors need to vary independently of the object hierarchy.
The Problem:
Moves in chess must:
The naive approach scatters this logic throughout the Game class. Undo becomes especially messy—you need to know exactly what each move type did to reverse it.
The Command Pattern Solution:
Encapsulate each move as an object that contains all information needed to:
Pattern Structure:
MoveCommand — defines execute() and undo()NormalMoveCommand, CaptureCommand, CastlingCommand, etc.Game — executes commands and maintains history stackBoard — the object modified by commands123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
/** * MoveCommand - The Command interface * * Each move type implements this interface with its own * execution and undo logic. */interface MoveCommand { /** * Execute the move on the board */ execute(board: Board): void; /** * Reverse the move, restoring previous state */ undo(board: Board): void; /** * Get algebraic notation for this move */ getNotation(): string; /** * Get move metadata for history */ getMetadata(): MoveMetadata;} interface MoveMetadata { piece: PieceType; color: Color; from: Position; to: Position; isCapture: boolean; isCheck: boolean; isCheckmate: boolean; timestamp: Date;} /** * NormalMoveCommand - Simple piece movement without capture */class NormalMoveCommand implements MoveCommand { private piece: Piece; private from: Position; private to: Position; private wasFirstMove: boolean; constructor(piece: Piece, from: Position, to: Position) { this.piece = piece; this.from = from; this.to = to; this.wasFirstMove = !piece.hasBeenMoved(); } execute(board: Board): void { board.removePiece(this.from); board.placePiece(this.piece, this.to); this.piece.setPosition(this.to); } undo(board: Board): void { board.removePiece(this.to); board.placePiece(this.piece, this.from); this.piece.setPosition(this.from); // Restore hasMoved state if this was first move if (this.wasFirstMove) { this.piece.resetMoved(); } } getNotation(): string { return `${this.piece.getSymbol()}${this.to.toAlgebraic()}`; } getMetadata(): MoveMetadata { return { piece: this.piece.type, color: this.piece.color, from: this.from, to: this.to, isCapture: false, isCheck: false, isCheckmate: false, timestamp: new Date(), }; }} /** * CaptureCommand - Move that captures an opponent piece */class CaptureCommand implements MoveCommand { private piece: Piece; private from: Position; private to: Position; private capturedPiece: Piece; private wasFirstMove: boolean; constructor( piece: Piece, from: Position, to: Position, capturedPiece: Piece ) { this.piece = piece; this.from = from; this.to = to; this.capturedPiece = capturedPiece; this.wasFirstMove = !piece.hasBeenMoved(); } execute(board: Board): void { // Remove captured piece first board.removePiece(this.to); // Move attacking piece board.removePiece(this.from); board.placePiece(this.piece, this.to); this.piece.setPosition(this.to); } undo(board: Board): void { // Move piece back board.removePiece(this.to); board.placePiece(this.piece, this.from); this.piece.setPosition(this.from); // Restore captured piece board.placePiece(this.capturedPiece, this.to); if (this.wasFirstMove) { this.piece.resetMoved(); } } getNotation(): string { const pieceSymbol = this.piece.type === PieceType.PAWN ? this.from.file : this.piece.getSymbol(); return `${pieceSymbol}x${this.to.toAlgebraic()}`; } getMetadata(): MoveMetadata { return { piece: this.piece.type, color: this.piece.color, from: this.from, to: this.to, isCapture: true, isCheck: false, isCheckmate: false, timestamp: new Date(), }; }} /** * CastlingCommand - Complex move involving King and Rook */class CastlingCommand implements MoveCommand { private king: Piece; private rook: Piece; private kingFrom: Position; private kingTo: Position; private rookFrom: Position; private rookTo: Position; private isKingside: boolean; constructor( king: Piece, rook: Piece, kingFrom: Position, kingTo: Position, rookFrom: Position, rookTo: Position, isKingside: boolean ) { this.king = king; this.rook = rook; this.kingFrom = kingFrom; this.kingTo = kingTo; this.rookFrom = rookFrom; this.rookTo = rookTo; this.isKingside = isKingside; } execute(board: Board): void { // Move King board.removePiece(this.kingFrom); board.placePiece(this.king, this.kingTo); this.king.setPosition(this.kingTo); // Move Rook board.removePiece(this.rookFrom); board.placePiece(this.rook, this.rookTo); this.rook.setPosition(this.rookTo); } undo(board: Board): void { // Move King back board.removePiece(this.kingTo); board.placePiece(this.king, this.kingFrom); this.king.setPosition(this.kingFrom); this.king.resetMoved(); // Move Rook back board.removePiece(this.rookTo); board.placePiece(this.rook, this.rookFrom); this.rook.setPosition(this.rookFrom); this.rook.resetMoved(); } getNotation(): string { return this.isKingside ? 'O-O' : 'O-O-O'; } getMetadata(): MoveMetadata { return { piece: PieceType.KING, color: this.king.color, from: this.kingFrom, to: this.kingTo, isCapture: false, isCheck: false, isCheckmate: false, timestamp: new Date(), }; }} /** * EnPassantCommand - Pawn captures pawn in passing */class EnPassantCommand implements MoveCommand { private pawn: Piece; private from: Position; private to: Position; private capturedPawn: Piece; private capturedPawnPos: Position; constructor( pawn: Piece, from: Position, to: Position, capturedPawn: Piece ) { this.pawn = pawn; this.from = from; this.to = to; this.capturedPawn = capturedPawn; // Captured pawn is on same rank as attacking pawn, target file this.capturedPawnPos = Position.at(to.file, from.rank); } execute(board: Board): void { // Remove captured pawn (not at destination!) board.removePiece(this.capturedPawnPos); // Move attacking pawn board.removePiece(this.from); board.placePiece(this.pawn, this.to); this.pawn.setPosition(this.to); } undo(board: Board): void { // Move pawn back board.removePiece(this.to); board.placePiece(this.pawn, this.from); this.pawn.setPosition(this.from); // Restore captured pawn board.placePiece(this.capturedPawn, this.capturedPawnPos); } getNotation(): string { return `${this.from.file}x${this.to.toAlgebraic()} e.p.`; } getMetadata(): MoveMetadata { return { piece: PieceType.PAWN, color: this.pawn.color, from: this.from, to: this.to, isCapture: true, isCheck: false, isCheckmate: false, timestamp: new Date(), }; }} /** * PromotionCommand - Pawn transforms to another piece */class PromotionCommand implements MoveCommand { private pawn: Piece; private promotedPiece: Piece; private from: Position; private to: Position; private capturedPiece: Piece | null; constructor( pawn: Piece, promoteTo: PieceType, from: Position, to: Position, capturedPiece: Piece | null ) { this.pawn = pawn; this.from = from; this.to = to; this.capturedPiece = capturedPiece; // Create the promoted piece this.promotedPiece = new Piece(promoteTo, pawn.color, to); } execute(board: Board): void { // Remove captured piece if any if (this.capturedPiece) { board.removePiece(this.to); } // Remove pawn board.removePiece(this.from); // Place promoted piece board.placePiece(this.promotedPiece, this.to); } undo(board: Board): void { // Remove promoted piece board.removePiece(this.to); // Restore pawn board.placePiece(this.pawn, this.from); // Restore captured piece if any if (this.capturedPiece) { board.placePiece(this.capturedPiece, this.to); } } getNotation(): string { const captureNotation = this.capturedPiece ? `${this.from.file}x` : ''; const promotionSymbol = this.promotedPiece.getSymbol(); return `${captureNotation}${this.to.toAlgebraic()}=${promotionSymbol}`; } getMetadata(): MoveMetadata { return { piece: PieceType.PAWN, color: this.pawn.color, from: this.from, to: this.to, isCapture: this.capturedPiece !== null, isCheck: false, isCheckmate: false, timestamp: new Date(), }; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
/** * Game as Command Invoker * * The Game maintains a stack of executed commands * and a stack of undone commands (for redo). */class Game { private commandHistory: MoveCommand[] = []; private redoStack: MoveCommand[] = []; /** * Execute a move command */ private executeCommand(command: MoveCommand): void { command.execute(this.board); this.commandHistory.push(command); // Clear redo stack (new branch) this.redoStack = []; } /** * Undo the last move */ public undo(): boolean { if (this.commandHistory.length === 0) { return false; } const command = this.commandHistory.pop()!; command.undo(this.board); this.redoStack.push(command); this.revertTurn(); this.updateGameStatus(); return true; } /** * Redo the last undone move */ public redo(): boolean { if (this.redoStack.length === 0) { return false; } const command = this.redoStack.pop()!; command.execute(this.board); this.commandHistory.push(command); this.switchTurn(); this.updateGameStatus(); return true; } /** * Get move history as notations */ public getMoveHistory(): string[] { return this.commandHistory.map(cmd => cmd.getNotation()); } /** * Export game as PGN (Portable Game Notation) */ public toPGN(): string { const moves = this.commandHistory.map((cmd, idx) => { const moveNum = Math.floor(idx / 2) + 1; const notation = cmd.getNotation(); if (idx % 2 === 0) { // White's move return `${moveNum}. ${notation}`; } else { // Black's move return notation; } }); return moves.join(' '); }}Each command is self-contained with its own execute/undo logic. Adding a new move type (perhaps for a chess variant) requires only a new command class—no changes to Game. Commands can also be serialized for saving games, replayed for analysis, or sent over a network for multiplayer.
The Problem:
While Command pattern handles undo for individual moves, sometimes we need to capture the complete game state—for example:
This requires capturing ALL state: board positions, turn, castling rights, en passant, move counters, and history.
The Memento Pattern Solution:
Create a snapshot object (Memento) that captures the internal state of the Game without exposing it. The Game can later restore itself from this snapshot.
Pattern Structure:
Game — creates and restores from mementosGameSnapshot — stores captured state123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103
/** * GameSnapshot - The Memento * * Captures complete game state at a point in time. * Immutable once created. */class GameSnapshot { // Private state—only Game can access contents private readonly boardState: BoardState; private readonly currentPlayerIndex: number; private readonly status: GameStatus; private readonly castlingRights: CastlingRights; private readonly enPassantTarget: Position | null; private readonly halfMoveClock: number; private readonly fullMoveNumber: number; private readonly moveHistory: MoveRecord[]; private readonly timestamp: Date; constructor( boardState: BoardState, currentPlayerIndex: number, status: GameStatus, castlingRights: CastlingRights, enPassantTarget: Position | null, halfMoveClock: number, fullMoveNumber: number, moveHistory: MoveRecord[] ) { // Deep copy everything to ensure immutability this.boardState = JSON.parse(JSON.stringify(boardState)); this.currentPlayerIndex = currentPlayerIndex; this.status = status; this.castlingRights = { ...castlingRights }; this.enPassantTarget = enPassantTarget; this.halfMoveClock = halfMoveClock; this.fullMoveNumber = fullMoveNumber; this.moveHistory = [...moveHistory]; this.timestamp = new Date(); } // Getters for restoration (accessed by Game) public getBoardState(): BoardState { return this.boardState; } public getCurrentPlayerIndex(): number { return this.currentPlayerIndex; } public getStatus(): GameStatus { return this.status; } public getCastlingRights(): CastlingRights { return this.castlingRights; } public getEnPassantTarget(): Position | null { return this.enPassantTarget; } public getHalfMoveClock(): number { return this.halfMoveClock; } public getFullMoveNumber(): number { return this.fullMoveNumber; } public getMoveHistory(): MoveRecord[] { return this.moveHistory; } public getTimestamp(): Date { return this.timestamp; } /** * Serialize to FEN (Forsyth-Edwards Notation) for portability * FEN is a standard string format for chess positions */ public toFEN(): string { // [position] [active] [castling] [en passant] [halfmove] [fullmove] const position = this.boardStateToFENPosition(); const active = this.currentPlayerIndex === 0 ? 'w' : 'b'; const castling = this.castlingToFEN(); const enPassant = this.enPassantTarget?.toAlgebraic() ?? '-'; return `${position} ${active} ${castling} ${enPassant} ${this.halfMoveClock} ${this.fullMoveNumber}`; } private boardStateToFENPosition(): string { // Implementation of FEN position encoding // Returns format like: rnbqkbnr/pppppppp/8/8/4P3/8/PPPP1PPP/RNBQKBNR // ... } private castlingToFEN(): string { let result = ''; if (this.castlingRights.whiteKingside) result += 'K'; if (this.castlingRights.whiteQueenside) result += 'Q'; if (this.castlingRights.blackKingside) result += 'k'; if (this.castlingRights.blackQueenside) result += 'q'; return result || '-'; }} interface BoardState { pieces: Array<{ type: PieceType; color: Color; position: string; // Algebraic notation hasMoved: boolean; }>;} interface CastlingRights { whiteKingside: boolean; whiteQueenside: boolean; blackKingside: boolean; blackQueenside: boolean;} interface MoveRecord { notation: string; piece: PieceType; from: string; to: string;}12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697
/** * Game with Memento support */class Game { /** * Create a snapshot of current state */ public createSnapshot(): GameSnapshot { return new GameSnapshot( this.getBoardState(), this.currentPlayerIndex, this.status, this.getCastlingRights(), this.enPassantTarget, this.halfMoveClock, this.moveNumber, this.getMoveRecords() ); } /** * Restore state from a snapshot */ public restoreFromSnapshot(snapshot: GameSnapshot): void { // Restore board this.board = new Board(); for (const pieceData of snapshot.getBoardState().pieces) { const piece = new Piece( pieceData.type, pieceData.color, Position.fromAlgebraic(pieceData.position) ); if (pieceData.hasMoved) piece.markAsMoved(); this.board.placePiece(piece, piece.getPosition()); } // Restore game state this.currentPlayerIndex = snapshot.getCurrentPlayerIndex(); this.status = snapshot.getStatus(); this.castlingRights = snapshot.getCastlingRights(); this.enPassantTarget = snapshot.getEnPassantTarget(); this.halfMoveClock = snapshot.getHalfMoveClock(); this.moveNumber = snapshot.getFullMoveNumber(); // Clear command history (we're at a new state) this.commandHistory = []; this.redoStack = []; this.eventEmitter.emit({ type: 'GAME_RESTORED', timestamp: new Date(), gameId: this.id, fromFEN: snapshot.toFEN(), }); } /** * Load from FEN string */ public static fromFEN(fen: string, players: [Player, Player]): Game { const game = new Game(generateId(), players[0], players[1]); const snapshot = GameSnapshot.fromFEN(fen); game.restoreFromSnapshot(snapshot); return game; } /** * Get current position as FEN */ public toFEN(): string { return this.createSnapshot().toFEN(); } private getBoardState(): BoardState { const pieces: BoardState['pieces'] = []; for (const piece of this.board.getAllPieces()) { pieces.push({ type: piece.type, color: piece.color, position: piece.getPosition().toAlgebraic(), hasMoved: piece.hasBeenMoved(), }); } return { pieces }; } private getCastlingRights(): CastlingRights { return { whiteKingside: this.canCastleKingside(Color.WHITE), whiteQueenside: this.canCastleQueenside(Color.WHITE), blackKingside: this.canCastleKingside(Color.BLACK), blackQueenside: this.canCastleQueenside(Color.BLACK), }; }}Command pattern handles fine-grained undo (individual moves). Memento handles coarse-grained undo (complete state restoration). In chess, we use Command for move-by-move undo and Memento for loading positions. Both patterns complement each other.
The true power of patterns emerges when they work together. In our chess system:
The Flow:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990
/** * Example: Making a move with all patterns */class Game { public makeMove(from: Position, to: Position, promoteTo?: PieceType): MoveResult { // 1. Get piece and validate ownership const piece = this.board.getPieceAt(from); if (!piece || piece.color !== this.getCurrentColor()) { return MoveResult.error('Invalid piece'); } // 2. Use STRATEGY to get possible moves const possibleMoves = piece.getPossibleMoves(this.board); // 3. Validate legality (not in check after move) const legalMoves = possibleMoves.filter(pos => !this.wouldLeaveKingInCheck(piece, from, pos) ); if (!legalMoves.some(pos => pos.equals(to))) { return MoveResult.error('Illegal move'); } // 4. Create appropriate COMMAND const command = this.createMoveCommand(piece, from, to, promoteTo); // 5. Execute command this.executeCommand(command); // 6. Update turn and state this.switchTurn(); this.updateGameStatus(); return MoveResult.success(command.getMetadata()); } private createMoveCommand( piece: Piece, from: Position, to: Position, promoteTo?: PieceType ): MoveCommand { const targetPiece = this.board.getPieceAt(to); // Determine move type and create appropriate command // Castling if (piece.type === PieceType.KING && Math.abs(to.getFileIndex() - from.getFileIndex()) === 2) { return this.createCastlingCommand(piece, from, to); } // En passant if (piece.type === PieceType.PAWN && from.file !== to.file && !targetPiece) { return this.createEnPassantCommand(piece as Pawn, from, to); } // Promotion if (piece.type === PieceType.PAWN && this.isPromotionRank(to, piece.color)) { return new PromotionCommand( piece, promoteTo ?? PieceType.QUEEN, from, to, targetPiece ); } // Capture if (targetPiece) { return new CaptureCommand(piece, from, to, targetPiece); } // Normal move return new NormalMoveCommand(piece, from, to); } /** * Save game state using MEMENTO */ public save(): GameSnapshot { return this.createSnapshot(); } /** * Load game state using MEMENTO */ public load(snapshot: GameSnapshot): void { this.restoreFromSnapshot(snapshot); }}| Pattern | Responsibility | Key Classes | When Applied |
|---|---|---|---|
| Strategy | Piece movement behavior | MovementStrategy, *MovementStrategy | Move validation, attack detection |
| Command | Move execution and undo | MoveCommand, *Command classes | Making moves, undo/redo |
| Memento | State snapshot and restore | GameSnapshot, Game | Save/load, position setup |
While Strategy, Command, and Memento are the primary patterns, our chess design employs several others:
Observer Pattern: Game emits events (MoveMade, GameEnded); UI and analytics subscribe.
Factory Method: MovementStrategyFactory creates appropriate strategies; MoveCommandFactory creates commands.
Template Method: SlidingMovementStrategy defines the sliding algorithm; subclasses provide directions.
Flyweight: Position caches all 64 instances; shared rather than creating new objects.
Null Object: Empty square representation avoids null checks throughout the board operations.
Result Pattern: MoveResult encapsulates success/failure without exceptions for expected failures.
We identified these patterns because the problems called for them, not to demonstrate pattern knowledge. Always ask: 'Does this pattern solve a real problem here?' A simpler solution that works is better than a pattern that adds unnecessary complexity.
Design patterns transform our chess implementation from a tangled web of conditionals into a clean, extensible architecture:
What's Next:
With all design elements in place, the final page brings everything together in a complete design walkthrough—class diagrams, sequence diagrams, and the full picture of how entities, patterns, and behaviors integrate into a production-ready chess engine.
You've mastered the application of Strategy, Command, and Memento patterns to a real system. These patterns appear repeatedly in software—game engines, text editors, transaction systems, workflow automation. Recognition and application are now part of your toolkit.