Loading content...
A chess game is a finite state machine with well-defined states and transitions. From the moment players sit down until someone wins, loses, or draws, the game progresses through a series of states governed by precise rules. Understanding and implementing this state machine correctly is essential for a robust chess engine.
Game state management encompasses far more than just tracking whose turn it is. It includes detecting when the game has reached a terminal condition (checkmate, stalemate), handling the various draw scenarios, maintaining state for undo/redo functionality, and ensuring that every transition is valid according to chess rules.
Master the complete game state lifecycle: turn alternation, check/checkmate/stalemate detection algorithms, draw conditions (agreement, insufficient material, threefold repetition, fifty-move rule), resignation, and timeout handling. Learn to model game state as a finite state machine with clean transitions.
Chess games transition through distinct states. Modeling these as an explicit state machine makes the code clearer and prevents invalid transitions.
State Categories:
Once a game reaches a terminal state, no further moves are possible. The transition is irreversible.
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162
/** * GameStatus - All possible states of a chess game * * Design: Explicit states make status checks and transitions clear. * Terminal states are distinguished from active states. */enum GameStatus { // === Pre-Game === WAITING_FOR_PLAYERS = 'WAITING_FOR_PLAYERS', READY_TO_START = 'READY_TO_START', // === Active States === ACTIVE = 'ACTIVE', // Normal play, no check CHECK = 'CHECK', // King is threatened but can escape // === Terminal States: Decisive === CHECKMATE_WHITE_WINS = 'CHECKMATE_WHITE_WINS', CHECKMATE_BLACK_WINS = 'CHECKMATE_BLACK_WINS', RESIGNATION_WHITE_WINS = 'RESIGNATION_WHITE_WINS', RESIGNATION_BLACK_WINS = 'RESIGNATION_BLACK_WINS', TIMEOUT_WHITE_WINS = 'TIMEOUT_WHITE_WINS', TIMEOUT_BLACK_WINS = 'TIMEOUT_BLACK_WINS', // === Terminal States: Draw === DRAW_STALEMATE = 'DRAW_STALEMATE', DRAW_AGREEMENT = 'DRAW_AGREEMENT', DRAW_INSUFFICIENT_MATERIAL = 'DRAW_INSUFFICIENT_MATERIAL', DRAW_THREEFOLD_REPETITION = 'DRAW_THREEFOLD_REPETITION', DRAW_FIFTY_MOVE_RULE = 'DRAW_FIFTY_MOVE_RULE', DRAW_TIMEOUT_VS_INSUFFICIENT = 'DRAW_TIMEOUT_VS_INSUFFICIENT',} /** * Helper methods for status classification */class GameStatusHelper { static isActive(status: GameStatus): boolean { return status === GameStatus.ACTIVE || status === GameStatus.CHECK; } static isTerminal(status: GameStatus): boolean { return !this.isActive(status) && status !== GameStatus.WAITING_FOR_PLAYERS && status !== GameStatus.READY_TO_START; } static isDecisive(status: GameStatus): boolean { return status.includes('CHECKMATE') || status.includes('RESIGNATION') || (status.includes('TIMEOUT') && !status.includes('INSUFFICIENT')); } static isDraw(status: GameStatus): boolean { return status.startsWith('DRAW_'); } static getWinner(status: GameStatus): Color | null { if (status.includes('WHITE_WINS')) return Color.WHITE; if (status.includes('BLACK_WINS')) return Color.BLACK; return null; }}Turn management is deceptively simple on the surface—White moves, then Black, alternating. But proper implementation must handle edge cases and integrate with the undo/redo system.
Core Rules:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384
/** * Turn management within the Game class */class Game { private players: [Player, Player]; // [White, Black] private currentPlayerIndex: number = 0; // 0 = White's turn private moveNumber: number = 1; // Full moves (increments after Black) private halfMoveCount: number = 0; // Half-moves (ply) since game start /** * Get the player whose turn it is */ public getCurrentPlayer(): Player { return this.players[this.currentPlayerIndex]; } /** * Get the color of the current player */ public getCurrentColor(): Color { return this.currentPlayerIndex === 0 ? Color.WHITE : Color.BLACK; } /** * Get the opponent of the current player */ public getOpponent(): Player { return this.players[this.currentPlayerIndex === 0 ? 1 : 0]; } /** * Switch to the next player's turn * Called after a successful move */ private switchTurn(): void { this.currentPlayerIndex = this.currentPlayerIndex === 0 ? 1 : 0; this.halfMoveCount++; // Move number increments after Black's move if (this.currentPlayerIndex === 0) { this.moveNumber++; } } /** * Revert turn (for undo) */ private revertTurn(): void { // If we're reverting to White's move, decrement move number if (this.currentPlayerIndex === 0) { this.moveNumber--; } this.currentPlayerIndex = this.currentPlayerIndex === 0 ? 1 : 0; this.halfMoveCount--; } /** * Validate that a piece can be moved by the current player */ private validateTurn(piece: Piece): boolean { return piece.color === this.getCurrentColor(); } /** * Get current move number in standard notation * Format: "1." for White's move, "1..." for Black's move */ public getMoveNotation(): string { if (this.currentPlayerIndex === 0) { return `${this.moveNumber}.`; } else { return `${this.moveNumber}...`; } } /** * Get total half-moves (ply) since game start * Useful for timing, analysis, and draw detection */ public getPlyCount(): number { return this.halfMoveCount; }}In chess, a 'move' typically refers to a pair of moves (White + Black). A 'ply' or 'half-move' is a single player's move. When we say 'move 10', we mean after White's 10th and Black's 10th moves. Ply count is essential for the fifty-move rule (100 ply without capture or pawn move).
Checkmate is the ultimate goal of chess—the opponent's King is in check with no legal escape. Detection requires checking two conditions:
If both are true, the game ends with the opponent winning.
Algorithm:
function isCheckmate(color):
if not isInCheck(color):
return false // Not even in check
// Check if ANY legal move exists
for each piece of color:
if getLegalMoves(piece.position).length > 0:
return false // Found an escape
return true // No escape found
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
/** * Checkmate and Stalemate Detection * * Key insight: Both checkmate and stalemate involve having no legal moves. * The difference is whether the King is in check. */class GameStateDetector { private game: Game; constructor(game: Game) { this.game = game; } /** * Detect if the given color is checkmated * Checkmate = In check + No legal moves */ public isCheckmate(color: Color): boolean { // Must be in check if (!this.isInCheck(color)) { return false; } // Must have no legal moves return !this.hasLegalMove(color); } /** * Detect if the given color is stalemated * Stalemate = Not in check + No legal moves */ public isStalemate(color: Color): boolean { // Must NOT be in check if (this.isInCheck(color)) { return false; } // Must have no legal moves return !this.hasLegalMove(color); } /** * Check if the given color has any legal move * Early termination for efficiency—we only need to find ONE legal move */ private hasLegalMove(color: Color): boolean { const pieces = this.game.getBoard().getAllPieces(color); for (const piece of pieces) { const legalMoves = this.game.getLegalMoves(piece.getPosition()); if (legalMoves.length > 0) { return true; // Early termination } } return false; } /** * Is the King of the given color in check? */ private isInCheck(color: Color): boolean { return CheckDetector.isInCheck(this.game.getBoard(), color); } /** * Comprehensive game state check after each move * Returns the new game status */ public detectGameState(): GameStatus { const currentColor = this.game.getCurrentColor(); // Check for checkmate if (this.isCheckmate(currentColor)) { return currentColor === Color.WHITE ? GameStatus.CHECKMATE_BLACK_WINS : GameStatus.CHECKMATE_WHITE_WINS; } // Check for stalemate if (this.isStalemate(currentColor)) { return GameStatus.DRAW_STALEMATE; } // Check for other draw conditions if (this.isInsufficientMaterial()) { return GameStatus.DRAW_INSUFFICIENT_MATERIAL; } if (this.isThreefoldRepetition()) { return GameStatus.DRAW_THREEFOLD_REPETITION; } if (this.isFiftyMoveRule()) { return GameStatus.DRAW_FIFTY_MOVE_RULE; } // Game continues—is the current player in check? if (this.isInCheck(currentColor)) { return GameStatus.CHECK; } return GameStatus.ACTIVE; }} /** * Integration in Game class */class Game { private stateDetector: GameStateDetector; /** * Called after each move to update game status */ private updateGameStatus(): void { this.status = this.stateDetector.detectGameState(); } /** * Main move execution flow */ public makeMove(from: Position, to: Position, promoteTo?: PieceType): MoveResult { // ... validation logic ... // Execute the move this.executeMove(move); // Switch to opponent's turn this.switchTurn(); // Detect new game state (check/checkmate/stalemate) this.updateGameStatus(); // If game ended, record the outcome if (GameStatusHelper.isTerminal(this.status)) { this.recordGameEnd(); } return MoveResult.success(move); }}hasLegalMove() uses early termination—we stop as soon as ONE legal move is found. For stalemate/checkmate detection, we don't need the full list of legal moves, just whether any exist. This can save significant computation.
Chess has multiple draw conditions, each with different detection logic:
1. Stalemate — No legal moves but not in check (covered above)
2. Draw by Agreement — Both players agree to draw
3. Insufficient Material — Neither side can theoretically checkmate
4. Threefold Repetition — Same position occurs three times (same player to move, same castling rights, same en passant possibility)
5. Fifty-Move Rule — 50 consecutive moves without a pawn move or capture
6. Dead Position — Position where no sequence of moves can lead to checkmate (automatic)
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980
/** * Insufficient Material Detection * * These material configurations cannot force checkmate: * - King vs King * - King + Bishop vs King * - King + Knight vs King * - King + Bishop vs King + Bishop (same color bishops) */class InsufficientMaterialDetector { public static isInsufficientMaterial(board: Board): boolean { const whitePieces = board.getAllPieces(Color.WHITE); const blackPieces = board.getAllPieces(Color.BLACK); const whiteMaterial = this.categorizeMaterial(whitePieces); const blackMaterial = this.categorizeMaterial(blackPieces); // King vs King if (whiteMaterial.total === 1 && blackMaterial.total === 1) { return true; } // King + minor piece vs King if (whiteMaterial.total === 2 && blackMaterial.total === 1) { if (whiteMaterial.bishops === 1 || whiteMaterial.knights === 1) { return true; } } if (blackMaterial.total === 2 && whiteMaterial.total === 1) { if (blackMaterial.bishops === 1 || blackMaterial.knights === 1) { return true; } } // King + Bishop vs King + Bishop (same color squares) if (whiteMaterial.total === 2 && blackMaterial.total === 2) { if (whiteMaterial.bishops === 1 && blackMaterial.bishops === 1) { const whiteBishop = whitePieces.find(p => p.type === PieceType.BISHOP)!; const blackBishop = blackPieces.find(p => p.type === PieceType.BISHOP)!; if (this.isSameColorSquare(whiteBishop, blackBishop)) { return true; } } } return false; } private static categorizeMaterial(pieces: Piece[]): MaterialCount { return { total: pieces.length, queens: pieces.filter(p => p.type === PieceType.QUEEN).length, rooks: pieces.filter(p => p.type === PieceType.ROOK).length, bishops: pieces.filter(p => p.type === PieceType.BISHOP).length, knights: pieces.filter(p => p.type === PieceType.KNIGHT).length, pawns: pieces.filter(p => p.type === PieceType.PAWN).length, }; } private static isSameColorSquare(piece1: Piece, piece2: Piece): boolean { const pos1 = piece1.getPosition(); const pos2 = piece2.getPosition(); // Square color is determined by (file + rank) parity const color1 = (pos1.getFileIndex() + pos1.rank) % 2; const color2 = (pos2.getFileIndex() + pos2.rank) % 2; return color1 === color2; }} interface MaterialCount { total: number; queens: number; rooks: number; bishops: number; knights: number; pawns: number;}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111
/** * Threefold Repetition Detection * * A position is defined by: * - Piece placement * - Active color (whose turn) * - Castling availability (KQkq) * - En passant target square * * We use a hash of these elements to track position occurrences. */class RepetitionDetector { private positionHistory: Map<string, number> = new Map(); /** * Record the current position after a move */ public recordPosition(game: Game): void { const hash = this.computePositionHash(game); const count = this.positionHistory.get(hash) ?? 0; this.positionHistory.set(hash, count + 1); } /** * Check if current position has occurred 3+ times */ public isThreefoldRepetition(game: Game): boolean { const hash = this.computePositionHash(game); const count = this.positionHistory.get(hash) ?? 0; return count >= 3; } /** * Undo position recording (for move undo) */ public unrecordPosition(game: Game): void { const hash = this.computePositionHash(game); const count = this.positionHistory.get(hash) ?? 0; if (count > 1) { this.positionHistory.set(hash, count - 1); } else { this.positionHistory.delete(hash); } } /** * Compute a unique hash for the current position * Uses FEN-like encoding for consistency */ private computePositionHash(game: Game): string { const board = game.getBoard(); const parts: string[] = []; // Piece placement for (let rank = 8; rank >= 1; rank--) { let emptyCount = 0; let rankStr = ''; for (const file of Board.FILES) { const piece = board.getPieceAt(Position.at(file, rank)); if (piece) { if (emptyCount > 0) { rankStr += emptyCount; emptyCount = 0; } rankStr += this.pieceToChar(piece); } else { emptyCount++; } } if (emptyCount > 0) rankStr += emptyCount; parts.push(rankStr); } const placement = parts.join('/'); // Active color const activeColor = game.getCurrentColor() === Color.WHITE ? 'w' : 'b'; // Castling availability const castling = this.getCastlingString(game); // En passant target const enPassant = game.getEnPassantTarget()?.toAlgebraic() ?? '-'; return `${placement} ${activeColor} ${castling} ${enPassant}`; } private pieceToChar(piece: Piece): string { const chars: Record<PieceType, string> = { [PieceType.KING]: 'k', [PieceType.QUEEN]: 'q', [PieceType.ROOK]: 'r', [PieceType.BISHOP]: 'b', [PieceType.KNIGHT]: 'n', [PieceType.PAWN]: 'p', }; const char = chars[piece.type]; return piece.color === Color.WHITE ? char.toUpperCase() : char; } private getCastlingString(game: Game): string { let result = ''; if (game.canCastleKingside(Color.WHITE)) result += 'K'; if (game.canCastleQueenside(Color.WHITE)) result += 'Q'; if (game.canCastleKingside(Color.BLACK)) result += 'k'; if (game.canCastleQueenside(Color.BLACK)) result += 'q'; return result || '-'; }}123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263
/** * Fifty-Move Rule Detection * * The fifty-move rule states that a player may claim a draw if * 50 consecutive moves have been made by both players without: * - Any pawn being moved * - Any piece being captured * * We track this with a "half-move clock" (reset on pawn move or capture) */class FiftyMoveDetector { private halfMoveClock: number = 0; // Counts half-moves (ply) /** * Update clock after a move * Reset if pawn move or capture, otherwise increment */ public updateAfterMove(move: Move): void { if (move.piece.type === PieceType.PAWN || move.capturedPiece) { this.halfMoveClock = 0; // Reset } else { this.halfMoveClock++; // Increment } } /** * Revert clock (for undo) * This requires storing the previous clock value in Move */ public revertToValue(previousValue: number): void { this.halfMoveClock = previousValue; } /** * Check if fifty-move rule applies * Note: 50 moves = 100 half-moves (ply) */ public isFiftyMoveRule(): boolean { return this.halfMoveClock >= 100; } /** * Get current half-move clock value */ public getClock(): number { return this.halfMoveClock; }} /** * Enhanced Move class to support undo of fifty-move clock */class Move { // ... existing properties ... // For fifty-move rule undo public readonly previousHalfMoveClock: number; constructor(/* ... */, previousHalfMoveClock: number) { // ... existing logic ... this.previousHalfMoveClock = previousHalfMoveClock; }}A well-designed game system emits events at key lifecycle points. This enables UI updates, analytics, logging, and extensibility without polluting core game logic.
Key Events:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134
/** * Game Events - Typed events for the Observer pattern */interface GameEvent { type: string; timestamp: Date; gameId: string;} interface GameStartedEvent extends GameEvent { type: 'GAME_STARTED'; whitePlayer: Player; blackPlayer: Player;} interface MoveMadeEvent extends GameEvent { type: 'MOVE_MADE'; move: Move; moveNumber: number; fen: string; // Current position isCheck: boolean; timeRemaining?: { white: number; black: number };} interface GameEndedEvent extends GameEvent { type: 'GAME_ENDED'; status: GameStatus; winner: Color | null; reason: string; finalFen: string; totalMoves: number; pgn: string;} interface CheckEvent extends GameEvent { type: 'CHECK'; checkedColor: Color; checkingPieces: Position[];} /** * Event emitter for game events */type GameEventListener = (event: GameEvent) => void; class GameEventEmitter { private listeners: Map<string, Set<GameEventListener>> = new Map(); public on(eventType: string, listener: GameEventListener): void { if (!this.listeners.has(eventType)) { this.listeners.set(eventType, new Set()); } this.listeners.get(eventType)!.add(listener); } public off(eventType: string, listener: GameEventListener): void { this.listeners.get(eventType)?.delete(listener); } public emit(event: GameEvent): void { const eventListeners = this.listeners.get(event.type); if (eventListeners) { eventListeners.forEach(listener => listener(event)); } // Also emit to wildcard listeners const wildcardListeners = this.listeners.get('*'); if (wildcardListeners) { wildcardListeners.forEach(listener => listener(event)); } }} /** * Integration in Game class */class Game { private eventEmitter: GameEventEmitter = new GameEventEmitter(); public on(eventType: string, listener: GameEventListener): void { this.eventEmitter.on(eventType, listener); } public start(): void { this.board.setupStandardPosition(); this.status = GameStatus.ACTIVE; this.eventEmitter.emit({ type: 'GAME_STARTED', timestamp: new Date(), gameId: this.id, whitePlayer: this.players[0], blackPlayer: this.players[1], } as GameStartedEvent); } private executeMove(move: Move): void { // ... execute move logic ... this.eventEmitter.emit({ type: 'MOVE_MADE', timestamp: new Date(), gameId: this.id, move: move, moveNumber: this.moveNumber, fen: this.toFEN(), isCheck: move.isCheck, } as MoveMadeEvent); if (move.isCheck) { this.eventEmitter.emit({ type: 'CHECK', timestamp: new Date(), gameId: this.id, checkedColor: this.getOpponentColor(), checkingPieces: this.getCheckingPiecePositions(), } as CheckEvent); } } private recordGameEnd(): void { this.eventEmitter.emit({ type: 'GAME_ENDED', timestamp: new Date(), gameId: this.id, status: this.status, winner: GameStatusHelper.getWinner(this.status), reason: this.getEndReason(), finalFen: this.toFEN(), totalMoves: this.moveHistory.length, pgn: this.toPGN(), } as GameEndedEvent); }}Events decouple the game engine from consumers like UI, analytics, or networking. The engine doesn't know or care what happens when a move is made—it just emits the event. This makes the engine testable in isolation and enables multiple frontends (CLI, web, mobile) to use the same core.
Players can end the game voluntarily through resignation or by mutually agreeing to a draw. These are simple state transitions but must be handled carefully to prevent misclicks or abuse.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112
/** * Handling voluntary game termination */class Game { private pendingDrawOffer: Color | null = null; /** * A player resigns, conceding the game */ public resign(resigningColor: Color): void { if (!GameStatusHelper.isActive(this.status)) { throw new GameError('Cannot resign: game is not active'); } this.status = resigningColor === Color.WHITE ? GameStatus.RESIGNATION_BLACK_WINS : GameStatus.RESIGNATION_WHITE_WINS; this.recordGameEnd(); } /** * A player offers a draw * The offer is only valid until the opponent moves */ public offerDraw(offeringColor: Color): void { if (!GameStatusHelper.isActive(this.status)) { throw new GameError('Cannot offer draw: game is not active'); } // Can only offer on your own turn (or immediately after your move) if (this.getCurrentColor() !== offeringColor) { throw new GameError('Can only offer draw on your turn'); } this.pendingDrawOffer = offeringColor; this.eventEmitter.emit({ type: 'DRAW_OFFERED', timestamp: new Date(), gameId: this.id, offeringColor: offeringColor, }); } /** * Opponent responds to draw offer */ public respondToDrawOffer(respondingColor: Color, accept: boolean): void { if (!this.pendingDrawOffer) { throw new GameError('No pending draw offer'); } if (this.pendingDrawOffer === respondingColor) { throw new GameError('Cannot respond to your own draw offer'); } if (accept) { this.status = GameStatus.DRAW_AGREEMENT; this.recordGameEnd(); } this.pendingDrawOffer = null; this.eventEmitter.emit({ type: 'DRAW_RESPONDED', timestamp: new Date(), gameId: this.id, accepted: accept, respondingColor: respondingColor, }); } /** * Making a move automatically declines a pending draw offer */ public makeMove(from: Position, to: Position, promoteTo?: PieceType): MoveResult { // ... validation ... // Decline pending draw offer by making a move if (this.pendingDrawOffer && this.pendingDrawOffer !== this.getCurrentColor()) { this.pendingDrawOffer = null; // Implicitly declined } // ... execute move ... } /** * Claim a draw (for rules that require claiming) * Threefold repetition and fifty-move rule can be claimed */ public claimDraw(claimingColor: Color, reason: 'REPETITION' | 'FIFTY_MOVE'): void { if (this.getCurrentColor() !== claimingColor) { throw new GameError('Can only claim draw on your turn'); } if (reason === 'REPETITION') { if (!this.isThreefoldRepetition()) { throw new GameError('Threefold repetition has not occurred'); } this.status = GameStatus.DRAW_THREEFOLD_REPETITION; } else { if (!this.isFiftyMoveRule()) { throw new GameError('Fifty-move rule does not apply'); } this.status = GameStatus.DRAW_FIFTY_MOVE_RULE; } this.recordGameEnd(); }}While time controls are out of scope for our core design, the architecture should accommodate them. Here's how time management integrates with game state:
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768
/** * Time Control Types */interface TimeControl { type: 'NONE' | 'BULLET' | 'BLITZ' | 'RAPID' | 'CLASSICAL' | 'CUSTOM'; initialTimeMs: number; incrementMs: number; // Added after each move} /** * Clock management (sketch) */class ChessClock { private timeRemaining: [number, number]; // [White, Black] in ms private activePlayer: number = 0; private lastTickTime: number | null = null; private intervalId: NodeJS.Timer | null = null; constructor(private timeControl: TimeControl) { this.timeRemaining = [ timeControl.initialTimeMs, timeControl.initialTimeMs ]; } public start(): void { this.lastTickTime = Date.now(); this.intervalId = setInterval(() => this.tick(), 100); } public switchPlayer(): void { // Add increment to player who just moved this.timeRemaining[this.activePlayer] += this.timeControl.incrementMs; // Switch this.activePlayer = this.activePlayer === 0 ? 1 : 0; this.lastTickTime = Date.now(); } public stop(): void { if (this.intervalId) { clearInterval(this.intervalId); } } private tick(): void { if (this.lastTickTime === null) return; const now = Date.now(); const elapsed = now - this.lastTickTime; this.lastTickTime = now; this.timeRemaining[this.activePlayer] -= elapsed; if (this.timeRemaining[this.activePlayer] <= 0) { this.timeRemaining[this.activePlayer] = 0; this.onTimeout(this.activePlayer === 0 ? Color.WHITE : Color.BLACK); } } private onTimeout(color: Color): void { // Emit timeout event for Game to handle } public getTimeRemaining(color: Color): number { return this.timeRemaining[color === Color.WHITE ? 0 : 1]; }}If a player runs out of time but the opponent has insufficient material to checkmate, the game is drawn (DRAW_TIMEOUT_VS_INSUFFICIENT). This edge case requires checking material before declaring a timeout loss.
Game state management transforms a collection of rules into a coherent game experience. Let's consolidate:
What's Next:
With core game mechanics complete, we now explore the design patterns that make this system elegant and extensible: the Strategy pattern for piece movement, the Command pattern for move execution and undo, and the Memento pattern for state snapshots.
You now understand the complete lifecycle of a chess game—from initialization through every possible termination condition. The state machine approach ensures robustness, while the event system enables rich integration with frontends and analytics.