Loading content...
With requirements firmly established, we now embark on the most creative and foundational phase of Low-Level Design: identifying the core entities that will form the backbone of our chess game system. These entities are not arbitrary—they emerge organically from the problem domain, refined through careful analysis of the requirements we've gathered.
Entity identification is both art and science. We draw upon the noun extraction technique from our requirements, filter out non-essential concepts, distinguish between entities and value objects, and validate our choices against the use cases. The result is a candidate class list that directly reflects the chess domain—a design that would be immediately recognizable to chess players and software engineers alike.
By the end of this page, you will have identified and deeply understood the core entities of a chess game: Board, Square, Piece (and its variants), Position, Move, Player, and Game. You'll understand the rationale behind each entity, its responsibilities, key attributes, and initial method candidates.
The noun extraction technique is a systematic method for discovering candidate classes. We review our requirements and use cases, extract all significant nouns, then filter and categorize them. Let's apply this to our chess requirements:
Extracted nouns from requirements:
Primary Nouns (clearly central to domain):- Board, Square, Piece, King, Queen, Rook, Bishop, Knight, Pawn- Player, Move, Capture, Game, Turn, Check, Checkmate, Stalemate Secondary Nouns (potentially important):- Position, Coordinate, File, Rank- Castling, En Passant, Promotion- Move History, Game Record, Notation Supporting Nouns (may become attributes or helpers):- Color (White/Black), Time, Clock, Status- Error, Message, ResultFiltering the candidate list:
Not every noun becomes a class. We apply several filters:
| Noun | Classification | Rationale |
|---|---|---|
| Board | Entity | Central object with state and identity; contains squares and pieces |
| Square | Entity or Value Object | Represents a position on the board; may hold a piece |
| Piece | Entity | Has identity (each piece is unique), state (position), and behavior (movement) |
| King, Queen, Rook, etc. | Entity subclasses | Specific piece types with distinct behaviors; extend Piece |
| Player | Entity | Represents a participant with identity and associated color |
| Move | Entity or Command | Represents an action; may be stored for history; candidate for Command pattern |
| Position | Value Object | Immutable representation of coordinates (file, rank) |
| Color | Enum or Value Object | Simple value distinguishing White vs Black |
| Game | Entity | Aggregate root managing overall game state, turns, outcomes |
| Turn | Attribute of Game | Just tracks whose turn it is; absorbed into Game state |
| Check/Checkmate/Stalemate | Game states or methods | States within Game; methods for detection |
| Castling/En Passant/Promotion | Special move types | May be Move subtypes or handled via Strategy pattern |
| Move History | Collection in Game | List of moves; not a separate entity, but a collection |
| Capture | Move attribute | A move may or may not be a capture; attribute of Move |
Entities have identity—two pieces at the same position are different pieces. Value Objects are defined purely by attributes—two Position objects with (e, 4) are interchangeable. This distinction profoundly impacts equality semantics, immutability decisions, and storage strategies.
The Board is the physical playing surface—a container of squares and the current arrangement of pieces. It's the central data structure that most operations query and modify.
Responsibilities:
1234567891011121314151617181920212223242526272829303132333435363738394041424344
/** * Board - The 8x8 chessboard containing squares and pieces * * Design decisions: * - Uses a Map for O(1) position-to-piece lookup * - Immutable initial setup; pieces are placed/removed via methods * - Separates board state from game rules (SRP) */class Board { // 8x8 grid represented as position → piece mapping private squares: Map<string, Piece | null>; // Constants for board dimensions public static readonly FILES = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h']; public static readonly RANKS = [1, 2, 3, 4, 5, 6, 7, 8]; constructor() { this.squares = new Map(); this.initializeEmptyBoard(); } // Query methods getPieceAt(position: Position): Piece | null; isOccupied(position: Position): boolean; isOccupiedByColor(position: Position, color: Color): boolean; isEmpty(position: Position): boolean; // Get all pieces (optionally filtered by color) getAllPieces(color?: Color): Piece[]; findKing(color: Color): Position; // Mutation methods (used by Move execution) placePiece(piece: Piece, position: Position): void; removePiece(position: Position): Piece | null; movePiece(from: Position, to: Position): void; // Initialization setupStandardPosition(): void; setupFromFEN(fen: string): void; // For custom positions // Utility isValidPosition(position: Position): boolean; clone(): Board; // For move validation without modifying actual state}We can represent the board as a 2D array (Piece[8][8]) or a Map<Position, Piece>. The Map approach is more flexible for sparse boards and variant games. The array approach offers marginally faster access. For standard chess, either works well. We'll use Map for clarity.
A Position represents a square on the board, identified by file (a-h) and rank (1-8). In chess notation, squares are written like 'e4', 'd2', 'h8'.
Positions are Value Objects—they have no identity beyond their coordinates. Two Position instances with the same file and rank are semantically equivalent and should be interchangeable. This has critical implications:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869
/** * Position - Immutable value object representing a board square * * Design decisions: * - Immutable (readonly properties) * - Factory method for validation * - Cached instances for all 64 positions (Flyweight) * - Provides algebraic notation (e.g., "e4") */class Position { // Static cache of all 64 positions private static readonly cache: Map<string, Position> = new Map(); // Immutable coordinates public readonly file: string; // 'a' through 'h' public readonly rank: number; // 1 through 8 private constructor(file: string, rank: number) { this.file = file; this.rank = rank; } // Factory method with validation and caching public static at(file: string, rank: number): Position { const key = `${file}${rank}`; if (!Position.cache.has(key)) { if (!this.isValidFile(file) || !this.isValidRank(rank)) { throw new Error(`Invalid position: ${key}`); } Position.cache.set(key, new Position(file, rank)); } return Position.cache.get(key)!; } // Parse from algebraic notation public static fromAlgebraic(notation: string): Position { const file = notation[0]; const rank = parseInt(notation[1]); return Position.at(file, rank); } // Navigation methods (returns new Position or null if off-board) public up(steps: number = 1): Position | null; public down(steps: number = 1): Position | null; public left(steps: number = 1): Position | null; public right(steps: number = 1): Position | null; public diagonal(fileOffset: number, rankOffset: number): Position | null; // Utility public toAlgebraic(): string { return `${this.file}${this.rank}`; } public equals(other: Position): boolean { return this.file === other.file && this.rank === other.rank; } // File index (0-7) for array operations public getFileIndex(): number { return this.file.charCodeAt(0) - 'a'.charCodeAt(0); } // Validation helpers private static isValidFile(file: string): boolean { return file >= 'a' && file <= 'h'; } private static isValidRank(rank: number): boolean { return rank >= 1 && rank <= 8; }}The static cache ensures we never create more than 64 Position instances. This is the Flyweight pattern—sharing immutable objects to reduce memory overhead. When processing thousands of move evaluations, this matters.
The Piece is central to chess. Each piece has:
Polymorphism is essential here. Different piece types (King, Queen, Rook, Bishop, Knight, Pawn) have dramatically different movement behaviors. Rather than a massive switch statement, we model this as an inheritance hierarchy with each subclass implementing its own movement logic.
The Strategy Pattern Alternative:
We could also model movement as a strategy, injected into a general Piece class. This is discussed in a later page on patterns. For now, we use inheritance for clarity.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778
/** * PieceType - Enumeration of chess piece types */enum PieceType { KING = 'KING', QUEEN = 'QUEEN', ROOK = 'ROOK', BISHOP = 'BISHOP', KNIGHT = 'KNIGHT', PAWN = 'PAWN'} /** * Color - Enumeration representing piece/player colors */enum Color { WHITE = 'WHITE', BLACK = 'BLACK'} /** * Piece - Abstract base class for all chess pieces * * Responsibilities: * - Hold piece identity (type, color) * - Track state (position, hasMoved) * - Define abstract movement interface */abstract class Piece { public readonly type: PieceType; public readonly color: Color; protected position: Position; protected hasMoved: boolean = false; constructor(type: PieceType, color: Color, position: Position) { this.type = type; this.color = color; this.position = position; } // Position management public getPosition(): Position { return this.position; } public setPosition(position: Position): void { this.position = position; this.hasMoved = true; } public hasBeenMoved(): boolean { return this.hasMoved; } // Color utilities public isWhite(): boolean { return this.color === Color.WHITE; } public isBlack(): boolean { return this.color === Color.BLACK; } public isOpponent(other: Piece): boolean { return this.color !== other.color; } /** * Returns all squares this piece can potentially move to, * ignoring check constraints (pseudo-legal moves). * The board is needed to detect obstacles and captures. */ abstract getPossibleMoves(board: Board): Position[]; /** * Determines if this piece can attack a specific position. * Used for check detection. */ abstract canAttack(target: Position, board: Board): boolean; /** * Returns the algebraic symbol for this piece (K, Q, R, B, N, or empty for pawn) */ abstract getSymbol(): string; /** * Clone for undo/redo support */ abstract clone(): Piece;}Concrete piece implementations specialize the movement behavior:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140
/** * King - Can move one square in any direction * Special: Cannot move into check, can castle under conditions */class King extends Piece { constructor(color: Color, position: Position) { super(PieceType.KING, color, position); } getPossibleMoves(board: Board): Position[] { const moves: Position[] = []; const directions = [ [-1, -1], [-1, 0], [-1, 1], [0, -1], [0, 1], [1, -1], [1, 0], [1, 1] ]; for (const [df, dr] of directions) { const target = this.position.diagonal(df, dr); if (target && !board.isOccupiedByColor(target, this.color)) { moves.push(target); } } // Castling moves are handled separately (special move logic) return moves; } canAttack(target: Position, board: Board): boolean { const fileDiff = Math.abs( target.getFileIndex() - this.position.getFileIndex() ); const rankDiff = Math.abs(target.rank - this.position.rank); return fileDiff <= 1 && rankDiff <= 1 && (fileDiff + rankDiff > 0); } getSymbol(): string { return 'K'; } clone(): Piece { const copy = new King(this.color, this.position); copy.hasMoved = this.hasMoved; return copy; }} /** * Knight - Moves in L-shape (2+1), can jump over pieces */class Knight extends Piece { constructor(color: Color, position: Position) { super(PieceType.KNIGHT, color, position); } getPossibleMoves(board: Board): Position[] { const moves: Position[] = []; const jumps = [ [-2, -1], [-2, 1], [-1, -2], [-1, 2], [1, -2], [1, 2], [2, -1], [2, 1] ]; for (const [df, dr] of jumps) { const target = this.position.diagonal(df, dr); if (target && !board.isOccupiedByColor(target, this.color)) { moves.push(target); } } return moves; } canAttack(target: Position, board: Board): boolean { const fileDiff = Math.abs( target.getFileIndex() - this.position.getFileIndex() ); const rankDiff = Math.abs(target.rank - this.position.rank); return (fileDiff === 2 && rankDiff === 1) || (fileDiff === 1 && rankDiff === 2); } getSymbol(): string { return 'N'; } // 'N' to avoid confusion with King clone(): Piece { const copy = new Knight(this.color, this.position); copy.hasMoved = this.hasMoved; return copy; }} /** * Rook - Moves horizontally or vertically any number of squares */class Rook extends Piece { constructor(color: Color, position: Position) { super(PieceType.ROOK, color, position); } getPossibleMoves(board: Board): Position[] { return this.getSlidingMoves(board, [ [0, 1], [0, -1], [1, 0], [-1, 0] // vertical and horizontal ]); } private getSlidingMoves(board: Board, directions: number[][]): Position[] { const moves: Position[] = []; for (const [df, dr] of directions) { let steps = 1; while (true) { const target = this.position.diagonal(df * steps, dr * steps); if (!target) break; // Off board if (board.isEmpty(target)) { moves.push(target); steps++; } else if (board.isOccupiedByColor(target, this.color === Color.WHITE ? Color.BLACK : Color.WHITE)) { moves.push(target); // Can capture break; } else { break; // Blocked by own piece } } } return moves; } canAttack(target: Position, board: Board): boolean { return this.getPossibleMoves(board).some(p => p.equals(target)); } getSymbol(): string { return 'R'; } clone(): Piece { const copy = new Rook(this.color, this.position); copy.hasMoved = this.hasMoved; return copy; }} // Bishop and Queen follow similar patterns with their respective directions// Pawn is more complex due to directional movement, double-first-move, // en passant, and promotion - detailed in special moves sectiongetPossibleMoves returns 'pseudo-legal' moves—moves that are valid per piece movement rules but may leave the King in check. Legal move calculation requires filtering these against the check constraint, which is done at the Game level, not the Piece level. This separation of concerns keeps Piece classes focused.
The Player represents a participant in the game. Each game has exactly two players: one controlling White pieces, one controlling Black.
Player Responsibilities:
Why a separate Player class?
We could just use "Color" to represent whose turn it is. But having a Player entity enables:
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
/** * Player - Represents a game participant * * Design for extension: This is a concrete class that could be * refactored to an interface with HumanPlayer/AIPlayer implementations * when AI support is added. */class Player { private readonly id: string; private readonly name: string; private readonly color: Color; private timeRemaining?: number; // For timed games (milliseconds) constructor(id: string, name: string, color: Color) { this.id = id; this.name = name; this.color = color; } // Getters public getId(): string { return this.id; } public getName(): string { return this.name; } public getColor(): Color { return this.color; } // Time management (for optional timed play) public getTimeRemaining(): number | undefined { return this.timeRemaining; } public setTimeRemaining(ms: number): void { this.timeRemaining = ms; } public deductTime(ms: number): void { if (this.timeRemaining !== undefined) { this.timeRemaining = Math.max(0, this.timeRemaining - ms); } } public hasTimeLeft(): boolean { return this.timeRemaining === undefined || this.timeRemaining > 0; } // Check if this player controls a piece public owns(piece: Piece): boolean { return piece.color === this.color; }} /** * Future extension: Player interface for AI support * * interface IPlayer { * getId(): string; * getColor(): Color; * selectMove(game: Game): Promise<Move>; // Async for AI thinking * } * * class HumanPlayer implements IPlayer { ... } * class AIPlayer implements IPlayer { ... } */The Move is perhaps the most crucial entity in our design. Moves are the primary interaction point—every player action is expressed as a Move. Moves also need to be stored for history, undone for takebacks, and serialized for game recording.
This makes Move an excellent candidate for the Command Pattern, which we'll explore in detail later.
Move Responsibilities:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166
/** * MoveType - Distinguishes regular moves from special moves */enum MoveType { NORMAL = 'NORMAL', CAPTURE = 'CAPTURE', CASTLING_KINGSIDE = 'CASTLING_KINGSIDE', CASTLING_QUEENSIDE = 'CASTLING_QUEENSIDE', EN_PASSANT = 'EN_PASSANT', PAWN_DOUBLE_MOVE = 'PAWN_DOUBLE_MOVE', PROMOTION = 'PROMOTION', PROMOTION_WITH_CAPTURE = 'PROMOTION_WITH_CAPTURE'} /** * Move - Represents a chess move with all information for execution and undo * * Design for Command Pattern: Move encapsulates all state needed to: * 1. Execute the move (forward) * 2. Undo the move (backward) * 3. Describe the move (notation) */class Move { // What move was made public readonly piece: Piece; public readonly from: Position; public readonly to: Position; public readonly type: MoveType; // For undo support public readonly capturedPiece: Piece | null; public readonly wasFirstMove: boolean; // Did the moving piece's hasMoved change? // For special moves public readonly promotionPiece?: PieceType; // What to promote pawn to public readonly castlingRook?: Piece; // Rook involved in castling public readonly castlingRookFrom?: Position; // Rook's original position public readonly castlingRookTo?: Position; // Rook's destination // Metadata public readonly isCheck: boolean; public readonly isCheckmate: boolean; public readonly notation: string; // Algebraic notation (e.g., "Nf3", "O-O") private constructor( piece: Piece, from: Position, to: Position, type: MoveType, options: Partial<{ capturedPiece: Piece | null; wasFirstMove: boolean; promotionPiece: PieceType; castlingRook: Piece; castlingRookFrom: Position; castlingRookTo: Position; isCheck: boolean; isCheckmate: boolean; }> = {} ) { this.piece = piece; this.from = from; this.to = to; this.type = type; this.capturedPiece = options.capturedPiece ?? null; this.wasFirstMove = options.wasFirstMove ?? !piece.hasBeenMoved(); this.promotionPiece = options.promotionPiece; this.castlingRook = options.castlingRook; this.castlingRookFrom = options.castlingRookFrom; this.castlingRookTo = options.castlingRookTo; this.isCheck = options.isCheck ?? false; this.isCheckmate = options.isCheckmate ?? false; this.notation = this.generateNotation(); } // Factory methods for different move types static createNormal(piece: Piece, from: Position, to: Position): Move { return new Move(piece, from, to, MoveType.NORMAL); } static createCapture( piece: Piece, from: Position, to: Position, captured: Piece ): Move { return new Move(piece, from, to, MoveType.CAPTURE, { capturedPiece: captured }); } static createCastling( king: Piece, kingFrom: Position, kingTo: Position, rook: Piece, rookFrom: Position, rookTo: Position, isKingside: boolean ): Move { const type = isKingside ? MoveType.CASTLING_KINGSIDE : MoveType.CASTLING_QUEENSIDE; return new Move(king, kingFrom, kingTo, type, { castlingRook: rook, castlingRookFrom: rookFrom, castlingRookTo: rookTo }); } static createEnPassant( pawn: Piece, from: Position, to: Position, capturedPawn: Piece ): Move { return new Move(pawn, from, to, MoveType.EN_PASSANT, { capturedPiece: capturedPawn }); } static createPromotion( pawn: Piece, from: Position, to: Position, promoteTo: PieceType, captured?: Piece ): Move { const type = captured ? MoveType.PROMOTION_WITH_CAPTURE : MoveType.PROMOTION; return new Move(pawn, from, to, type, { capturedPiece: captured, promotionPiece: promoteTo }); } // Generate standard algebraic notation private generateNotation(): string { if (this.type === MoveType.CASTLING_KINGSIDE) return 'O-O'; if (this.type === MoveType.CASTLING_QUEENSIDE) return 'O-O-O'; let notation = ''; notation += this.piece.getSymbol(); if (this.capturedPiece) notation += 'x'; notation += this.to.toAlgebraic(); if (this.promotionPiece) notation += '=' + this.getPromotionSymbol(); if (this.isCheckmate) notation += '#'; else if (this.isCheck) notation += '+'; return notation; } private getPromotionSymbol(): string { switch(this.promotionPiece) { case PieceType.QUEEN: return 'Q'; case PieceType.ROOK: return 'R'; case PieceType.BISHOP: return 'B'; case PieceType.KNIGHT: return 'N'; default: return 'Q'; } } // Display toString(): string { return this.notation; }}Notice that Move stores not just what changed but what we need to reverse the change: the captured piece, whether it was the piece's first move, the rook positions during castling. This is the Memento aspect—the Move itself serves as a snapshot enabling reversal.
The Game entity is the aggregate root—the central coordinator that manages the overall game state and enforces invariants. All external interactions flow through Game; it's the facade for the entire chess engine.
Aggregate Root Pattern:
In Domain-Driven Design, an aggregate is a cluster of associated objects treated as a unit for data changes. The aggregate root is the only entry point. This ensures consistency—all state changes go through Game, which can enforce rules and maintain invariants.
Game Responsibilities:
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201
/** * GameStatus - Current state of the game */enum GameStatus { NOT_STARTED = 'NOT_STARTED', ACTIVE = 'ACTIVE', CHECK = 'CHECK', // Game continues but king is threatened CHECKMATE = 'CHECKMATE', // Game over, current player lost STALEMATE = 'STALEMATE', // Draw, no legal moves DRAW_AGREED = 'DRAW_AGREED', DRAW_INSUFFICIENT_MATERIAL = 'DRAW_INSUFFICIENT_MATERIAL', DRAW_THREEFOLD_REPETITION = 'DRAW_THREEFOLD_REPETITION', DRAW_FIFTY_MOVES = 'DRAW_FIFTY_MOVES', RESIGNED = 'RESIGNED', TIMEOUT = 'TIMEOUT'} /** * Game - Aggregate root coordinating all chess game logic * * This is the primary interface for game interaction. * All operations flow through Game to maintain consistency. */class Game { private readonly id: string; private board: Board; private readonly players: [Player, Player]; // [White, Black] private currentPlayerIndex: number = 0; // 0 = White, 1 = Black private status: GameStatus = GameStatus.NOT_STARTED; // Move history for undo/redo and game record private moveHistory: Move[] = []; private undoneHistory: Move[] = []; // For redo // Special state tracking private enPassantTarget: Position | null = null; private halfMoveClock: number = 0; // For 50-move rule private positionHistory: Map<string, number> = new Map(); // For repetition constructor(id: string, whitePlayer: Player, blackPlayer: Player) { this.id = id; this.players = [whitePlayer, blackPlayer]; this.board = new Board(); } // Game lifecycle public start(): void { this.board.setupStandardPosition(); this.status = GameStatus.ACTIVE; this.currentPlayerIndex = 0; // White moves first } public getCurrentPlayer(): Player { return this.players[this.currentPlayerIndex]; } public getStatus(): GameStatus { return this.status; } public getBoard(): Board { return this.board; } // Core move operations public makeMove(from: Position, to: Position, promoteTo?: PieceType): MoveResult { // 1. Validate game is active if (!this.isGameActive()) { return MoveResult.error('Game is not active'); } // 2. Validate piece at source belongs to current player const piece = this.board.getPieceAt(from); if (!piece || piece.color !== this.getCurrentPlayer().getColor()) { return MoveResult.error('No valid piece at source position'); } // 3. Validate move is legal (not just pseudo-legal) const legalMoves = this.getLegalMoves(from); if (!legalMoves.some(pos => pos.equals(to))) { return MoveResult.error('Illegal move'); } // 4. Create and execute the move const move = this.createMove(piece, from, to, promoteTo); this.executeMove(move); // 5. Update game state this.updateGameState(); // 6. Clear redo history (new branch) this.undoneHistory = []; return MoveResult.success(move); } public getLegalMoves(from: Position): Position[] { const piece = this.board.getPieceAt(from); if (!piece) return []; // Get pseudo-legal moves, then filter out those that leave king in check const pseudoLegal = piece.getPossibleMoves(this.board); return pseudoLegal.filter(to => !this.moveLeavesKingInCheck(piece, from, to)); } // Check detection public isInCheck(color: Color): boolean { const kingPos = this.board.findKing(color); return this.isSquareAttacked(kingPos, color === Color.WHITE ? Color.BLACK : Color.WHITE); } private isSquareAttacked(position: Position, byColor: Color): boolean { const attackingPieces = this.board.getAllPieces(byColor); return attackingPieces.some(piece => piece.canAttack(position, this.board)); } // Undo/Redo support public undo(): boolean { if (this.moveHistory.length === 0) return false; const move = this.moveHistory.pop()!; this.reverseMove(move); this.undoneHistory.push(move); this.switchPlayer(); return true; } public redo(): boolean { if (this.undoneHistory.length === 0) return false; const move = this.undoneHistory.pop()!; this.executeMove(move); this.switchPlayer(); return true; } // Game termination public resign(player: Player): void { this.status = GameStatus.RESIGNED; // Winner is the other player } public offerDraw(): void { /* ... */ } public acceptDraw(): void { this.status = GameStatus.DRAW_AGREED; } // Private helpers private switchPlayer(): void { this.currentPlayerIndex = this.currentPlayerIndex === 0 ? 1 : 0; } private isGameActive(): boolean { return this.status === GameStatus.ACTIVE || this.status === GameStatus.CHECK; } private createMove(piece: Piece, from: Position, to: Position, promoteTo?: PieceType): Move { // Determine move type and create appropriate Move object // ... (castling, en passant, promotion detection) } private executeMove(move: Move): void { // Execute on board, update piece positions // Handle captures, promotions, castling rook movement this.moveHistory.push(move); } private reverseMove(move: Move): void { // Undo the move: restore captured pieces, move pieces back } private updateGameState(): void { // Check for check, checkmate, stalemate // Update status accordingly } private moveLeavesKingInCheck(piece: Piece, from: Position, to: Position): boolean { // Clone board, make move, check if king is in check const testBoard = this.board.clone(); testBoard.movePiece(from, to); // ... (more complex for en passant, castling) }} /** * MoveResult - Outcome of a move attempt (Result pattern) */class MoveResult { public readonly success: boolean; public readonly move?: Move; public readonly errorMessage?: string; private constructor(success: boolean, move?: Move, error?: string) { this.success = success; this.move = move; this.errorMessage = error; } static success(move: Move): MoveResult { return new MoveResult(true, move); } static error(message: string): MoveResult { return new MoveResult(false, undefined, message); }}Let's consolidate our entity model before moving to detailed relationship diagrams:
| Entity | Type | Responsibility | Key Relationships |
|---|---|---|---|
| Game | Aggregate Root | Coordinate gameplay, enforce rules | Owns Board, Players, MoveHistory |
| Board | Entity | Maintain piece positions | Contains Pieces, uses Positions |
| Piece (abstract) | Entity | Define piece identity and movement | Subclasses: King, Queen, Rook, Bishop, Knight, Pawn |
| Position | Value Object | Identify board squares | Used by all entities |
| Move | Entity/Command | Represent and execute moves | References Piece, Positions, captured Piece |
| Player | Entity | Represent participants | Has Color, associated with Game |
What's Next:
With entities defined, the next page dives deep into move validation design—the heart of chess rule enforcement. We'll explore how to validate basic piece movements, handle special moves, detect check constraints, and ensure all moves are legal before execution.
You now have a comprehensive understanding of the core entities in a chess game system: Board, Position, Piece hierarchy, Player, Move, and Game. These form the foundational vocabulary of our design. Next, we'll see how these entities collaborate to enforce chess rules.