Documentation Index
Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
Use this file to discover all available pages before exploring further.
🎯 Problem Statement
Design a chess game that can:- Support two players (human or computer)
- Validate moves according to chess rules
- Detect check, checkmate, and stalemate
- Track game history and support undo
- Handle special moves (castling, en passant, pawn promotion)
📋 Step 1: Clarify Requirements
Questions to Ask the Interviewer
| Category | Question | Typical Scope |
|---|---|---|
| Players | Human vs Human? Human vs AI? | Usually both players human for LLD |
| Rules | Which special moves to include? | Castling, en passant often in scope |
| Features | Undo/redo support? | Often asked as extension |
| UI | GUI or console-based? | Console for LLD interviews |
| Timer | Chess clock functionality? | Usually out of scope |
| Variants | Fischer random? 3-check? | Out of scope |
Functional Requirements
- Two players take turns (white moves first)
- Each piece type has unique movement rules
- Validate that moves don’t leave own king in check
- Detect game-ending conditions (checkmate, stalemate)
- Display current board state
- Support undo/redo of moves
Non-Functional Requirements
- Clean separation of concerns
- Easy to add new piece types (for variants)
- Testable move validation logic
- Command pattern for move history
🧩 Step 2: Identify Core Objects
get_valid_moves() but each implements it differently.Game Elements
Pieces
Players & Moves
Entity-Responsibility Mapping
| Entity | Responsibilities | Pattern Used |
|---|---|---|
Piece (abstract) | Define movement interface | Template Method |
King, Queen, etc. | Implement specific movements | Polymorphism |
Board | Track piece positions, validate moves | - |
Game | Manage turns, check game status | State Pattern (optional) |
Move | Encapsulate move action for undo/redo | Command Pattern |
MoveHistory | Store moves for undo | Memento Pattern |
📐 Step 3: Class Diagram
Step 4: Implementation
Enums and Constants
Move Class
Piece Base Class
Concrete Piece Classes
Cell Class
Board Class
Player Class
Game Class
Step 5: Usage Example
Key Design Decisions
Why Abstract Piece Class?
Why Abstract Piece Class?
get_possible_moves() with its unique movement pattern. This is classic polymorphism — and it has a concrete design payoff: when the Board checks for check/checkmate, it iterates over all opponent pieces and calls get_possible_moves() uniformly. Without polymorphism, you would need a separate code path for each piece type, turning the Board into a tangled mess of type-checking. The abstract class also provides the shared _get_moves_in_direction() helper for sliding pieces (Queen, Rook, Bishop), demonstrating the Template Method pattern within the hierarchy.Why Separate Board Validation?
Why Separate Board Validation?
Why Move Object?
Why Move Object?
How Would You Add Undo?
How Would You Add Undo?
Interview Deep-Dive Questions
Q1: Why use an abstract Piece class with polymorphism instead of a single Piece class with a type enum and a giant switch statement?
Q1: Why use an abstract Piece class with polymorphism instead of a single Piece class with a type enum and a giant switch statement?
- The polymorphic approach means each piece subclass owns its own movement logic inside
get_possible_moves(). Adding a new piece type (say for Chess960 or a custom variant) is a single new class — you never touch existing piece code. With an enum + switch, every function that cares about piece behavior (canMove,getSymbol,getValue) becomes a sprawling switch-case that grows linearly with each new type. - This is the textbook Open/Closed Principle win: the
Piecehierarchy is open for extension (new subclasses) but closed for modification (no edits toBoardorGame). In practice, I have seen codebases where aPieceTypeenum approach started simple but became unmaintainable once special-case logic piled up — en passant rules leaking into the board, castling logic duplicated in three places. - The tradeoff is that polymorphism introduces a class-per-piece overhead and makes it slightly harder to do cross-piece operations (like “list all movement directions across all types for a threat map”). But for a domain where each piece has genuinely distinct behavior, inheritance wins decisively.
- Example: The Knight is the clearest proof — its L-shaped, non-blocking movement has zero overlap with sliding pieces. Forcing that into a shared function with Rook logic via
if piece_type == KNIGHTis a code smell you can detect from orbit.
- The Queen’s movement is literally Rook + Bishop combined. How does the
_get_moves_in_directionhelper method avoid duplicating sliding logic across three classes, and what design pattern does this resemble? - If you needed to add a “custom piece” feature where users define movement rules at runtime (e.g., a piece that moves like a Knight but captures like a Bishop), how would you refactor the hierarchy to support composition over inheritance?
Q2: Walk me through exactly how move validation works — from the player selecting a piece to the move being executed. Where does each layer of validation live?
Q2: Walk me through exactly how move validation works — from the player selecting a piece to the move being executed. Where does each layer of validation live?
- Validation is deliberately split into three layers, each with a single responsibility. Layer 1 — Game level:
Game.make_move_by_position()checks that the game is active and the selected piece belongs to the current player. This is authorization, not chess logic. Layer 2 — Board level:Board.get_valid_moves()calls the piece’sget_possible_moves()and then filters out any move that would leave the player’s own king in check via_would_leave_king_in_check(). This is the legality filter. Layer 3 — Piece level: Each piece’sget_possible_moves()generates raw candidate squares based purely on movement geometry — the Rook slides along ranks and files, the Knight jumps in L-shapes, etc. This layer knows nothing about check or game state. - The critical subtlety is
_would_leave_king_in_check(). For every candidate move, it temporarily executes the move on the board, asksis_king_in_check(), then rolls back. This simulate-then-revert pattern is simple but has O(n) cost per candidate move sinceis_king_in_checkiterates all opponent pieces. For a typical midgame position with ~30 legal moves and ~16 opponent pieces, that is roughly 480get_possible_movescalls per turn — acceptable for a chess game, but worth noting. - The layering also matters for special moves: castling validation lives in the
Kingclass but calls back toBoard.is_square_attacked()because the king cannot castle through check. This cross-layer dependency is intentional and contained. - Example: A pinned Bishop (one that blocks a Rook attack on your King). The Bishop’s
get_possible_moves()happily returns diagonal squares. But_would_leave_king_in_check()catches every one of them, correctly returning an empty valid-move list. The piece does not need to know it is pinned — the board figures it out.
- The simulate-and-revert approach in
_would_leave_king_in_checkmutates the board temporarily. What could go wrong in a multithreaded or async context, and how would you make this thread-safe without deep-copying the entire board? - How would you optimize check detection if you needed this engine to evaluate thousands of positions per second for an AI player — what data structures or algorithms would replace the brute-force “iterate all opponent pieces” approach?
Q3: How does the system detect checkmate vs. stalemate, and why is the distinction architecturally important?
Q3: How does the system detect checkmate vs. stalemate, and why is the distinction architecturally important?
- Both conditions share the same trigger: the opponent has zero legal moves. The difference is purely whether the opponent’s king is currently in check.
_update_game_status()first calls_has_any_valid_moves(opponent_color). If false, it checksis_king_in_check(opponent_color). Check + no moves = checkmate (decisive result). No check + no moves = stalemate (draw). The code correctly handles this as two branches of the same condition, not two separate detection paths. _has_any_valid_movesis an early-exit scan: it iterates every piece of the given color, and for each, callsBoard.get_valid_moves(). The moment it finds a single legal move, it returnsTrue. This is important — in the common case (game continues), it exits almost immediately. Worst case (stalemate/checkmate), it must exhaustively check every piece, which involves the full simulate-and-revert pipeline for each candidate move of each piece.- Architecturally, the distinction matters because checkmate changes
GameStatusto a win, while stalemate changes it to a draw. If you conflated them, you would have a game-breaking bug where a player with an overwhelming advantage accidentally stalemating the opponent counts as a win. In competitive chess, stalemate-as-draw is a fundamental rule that creates deep endgame strategy. - Example: King + Queen vs lone King. A naive engine that does not distinguish stalemate might let the dominant side push the opponent’s King into a corner with no legal moves but no check — and incorrectly declare victory instead of a draw.
- The current
_has_any_valid_movesdoes a fullget_valid_movesper piece which includes the expensive simulate-and-revert. Could you short-circuit this further — for instance, if a piece is not near any opponent pieces and is not pinned, can you skip the check simulation? - Chess has other draw conditions: threefold repetition, the 50-move rule, and insufficient material. How would you extend
_update_game_status()to handle these without making it a monolithic method?
Q4: Explain how undo/redo would work using the Command pattern. What state must each Move object capture to make reversal possible?
Q4: Explain how undo/redo would work using the Command pattern. What state must each Move object capture to make reversal possible?
- The
Movedataclass is already halfway to a Command object — it stores the piece, origin, destination, and captured piece. To support full undo, each Move must capture everything that changed: thehas_movedflag of the piece before the move (critical for castling rights — if you undo a King’s first move,has_movedmust revert toFalse), thehas_movedflag of the Rook in a castling move, thelast_movereference on the Board (needed for en passant legality on the previous turn), and for pawn promotion, the original Pawn object that was replaced by a Queen. - The undo operation reverses each mutation: move the piece back to
from_pos, restore the captured piece toto_pos(or to the en passant capture square), restore allhas_movedflags, and pop the move frommove_history. Redo simply re-executes the stored move. Themove_historylist acts as a stack; you also maintain aredo_stackthat gets moves pushed onto it during undo and cleared whenever a new move is made (because making a new move invalidates the redo branch). - The gotcha most candidates miss: en passant eligibility depends on
board.last_move. If you undo move N, the board’slast_movemust revert to move N-1, not stay at N. This means undo must also restore the board’slast_movepointer — otherwise en passant validation will be wrong on the restored state. - Example: White plays e2-e4 (pawn’s first move,
has_movedbecomes True). Black captures en passant on the next turn. Now undo Black’s en passant: restore Black’s pawn, restore the captured White pawn to e4, and critically, restoreboard.last_moveto the e2-e4 move so that en passant remains legal if Black wants to play a different move.
- The Memento pattern suggests storing opaque state snapshots. The Command pattern suggests storing operations with explicit
execute()andundo()methods. Which is more appropriate here, and what are the tradeoffs in terms of memory, complexity, and debuggability? - If two players are watching the game remotely and one requests “go back 5 moves to review,” but the other player does not want to, how would you architect a “view-only rewind” that does not mutate the actual game state?
Q5: How would you implement an Observer pattern so that external systems (UI, logger, AI analysis engine) get notified of game events without the Game class knowing about them?
Q5: How would you implement an Observer pattern so that external systems (UI, logger, AI analysis engine) get notified of game events without the Game class knowing about them?
- Define a
GameEventListenerinterface (or abstract class) with callback methods likeon_move_made(move),on_check(color),on_game_over(status), andon_undo(move). TheGameclass maintains a list of listeners and calls the appropriate method at each event point. A console UI listener would re-render the board onon_move_made. A PGN logger would append notation. An AI engine might recalculate evaluation scores. - The key architectural benefit: the
Gameclass’s core logic stays clean — noif has_ui: render()conditionals scattered through the code. Adding a new listener (say, a WebSocket broadcaster for remote spectators) is zero changes toGame. This is the Observer pattern’s core value and why it appears in virtually every event-driven system. - In Python specifically, you could implement this with a simple list of callables, a more formal
abc.ABC-based listener, or even Python’s built-insignal-like patterns. For this scale, a typed interface is clearest. One subtlety: listeners should not mutate game state inside callbacks. If a listener throws an exception, the game should catch it and continue — a buggy logger should never crash the game. - Example: A tournament system needs to broadcast moves to spectators with a 15-second delay. You register a
DelayedBroadcastListenerthat buffers moves in a queue and emits them on a timer. The Game class has no idea this delay exists — it just fireson_move_madeand moves on.
board.display() after every move in the Game class” — this tightly couples rendering to game logic, makes testing harder (every test prints to console), and makes it impossible to add new consumers without editing Game. It is the procedural approach that Observer exists to replace.Follow-ups:- If a listener like an AI engine needs to process events asynchronously (evaluation takes 2 seconds) but the game should not wait, how would you handle async listeners without blocking the game loop?
- Some events are compound — castling is really two piece movements. Should listeners receive one
on_castlingevent or twoon_piece_movedevents? What are the tradeoffs for each approach in terms of listener complexity?
Q6: Castling has at least five preconditions that must all be true. Walk through them and explain why each one exists from a chess rules perspective and a code perspective.
Q6: Castling has at least five preconditions that must all be true. Walk through them and explain why each one exists from a chess rules perspective and a code perspective.
- Precondition 1 — King has not moved (
self.has_moved is False): Chess rules say castling is a privilege lost permanently once the King moves — even if it moves back. Thehas_movedboolean on the Piece base class tracks this. This is why undo must restorehas_movedtoFalse. - Precondition 2 — The relevant Rook has not moved (
rook_cell.piece.has_moved is False): Same logic as the King. Each Rook independently tracks whether it has moved, so you can lose king-side castling rights while retaining queen-side. - Precondition 3 — The Rook actually exists at its starting position (
isinstance(rook_cell.piece, Rook)): The Rook may have been captured. The code checks both that the cell contains a piece and that it is a Rook. - Precondition 4 — All squares between King and Rook are empty: For king-side, columns 5 and 6 must be clear. For queen-side, columns 1, 2, and 3. A single piece in the way blocks castling.
- Precondition 5 — The King does not pass through or land on an attacked square (
is_square_attackedfor each square in the path): The King cannot castle out of check, through check, or into check. The code checks columns 4, 5, 6 for king-side (current, transit, destination). Note: the Rook can pass through an attacked square — only the King’s path matters. - Example: A common interview gotcha: the King is not in check but column 5 (f1/f8) is attacked by an opponent’s Bishop. King-side castling is illegal even though the destination (g1/g8) is safe. Many implementations miss the “transit square” check.
- The code calls
is_square_attackedthree times for king-side castling (squares e1, f1, g1). Each call iterates all opponent pieces. How would you optimize this if profiling showed castling validation as a bottleneck in an AI search? - In Chess960 (Fischer Random), the King and Rooks start on random squares but castling still exists with modified rules. How would you refactor
_can_castle_kingsideto support configurable starting positions without hardcoding column indices?
Q7: The Pawn is the most complex piece in chess despite appearing simple. What makes it unique, and how does the code handle each special behavior?
Q7: The Pawn is the most complex piece in chess despite appearing simple. What makes it unique, and how does the code handle each special behavior?
- The Pawn has at least four behaviors no other piece has: directional movement (it can only advance forward, never retreat — the
directionvariable is +1 for White, -1 for Black), asymmetric capture (moves forward but captures diagonally — the only piece where movement and capture directions differ), double-move from start (can advance two squares from its starting row, but only if both the intermediate and destination squares are empty), and en passant (can capture a pawn that just made a double-move, but only on the immediately following turn). - En passant is the trickiest to implement because it depends on temporal state — what happened on the previous turn. The code checks
board.last_moveto see if the last move was a double pawn advance to an adjacent square. This creates a hidden dependency:last_movemust be correctly maintained (and correctly restored on undo) or en passant breaks silently. - Pawn promotion adds another layer: when a pawn reaches the opposite end (row 0 or 7), it must transform into another piece. The current code defaults to Queen, but a production implementation should allow the player to choose (under-promotion to a Knight is a real tactical choice in competitive chess). The
Moveobject storesis_promotionandpromotion_pieceto support undo — you need to know to replace the Queen back with the original Pawn. - Example: In the code,
_can_en_passantchecks four conditions: last move exists, last move was a Pawn, last move was a double advance (abs(row diff) == 2), and the pawn that double-advanced is adjacent. Missing any one of these creates a bug — either allowing illegal en passant or blocking legal ones.
- The current implementation defaults pawn promotion to Queen. How would you design the interface so the Game can prompt the player for their promotion choice, without the Pawn class needing to know anything about UI or input?
- En passant has a one-turn window — it must be played immediately after the opponent’s double pawn advance. If you add a “takeback” feature that lets a player undo their last move, does this re-open the en passant window for the previous turn? How would you handle this edge case?
Q8: If you needed to add a computer player (AI opponent), what changes would you make to the architecture, and where does the current design make this easy or hard?
Q8: If you needed to add a computer player (AI opponent), what changes would you make to the architecture, and where does the current design make this easy or hard?
- The current design makes this relatively easy because the
Playerclass is already decoupled from input source. You would create aComputerPlayersubclass (or use a Strategy pattern for move selection) that implements achoose_move(board)method. TheGameclass’s turn loop would check if the current player is human or computer: human waits for input, computer calls its strategy. TheBoard.get_valid_moves()API already returns all legal moves for any piece, which is exactly what an AI needs to enumerate its options. - For the AI algorithm itself, the classic approach is Minimax with alpha-beta pruning. You need a board evaluation function (material count, piece positioning, king safety) and a search depth. The
Boardclass needs a way to make/unmake moves efficiently for the search tree — and here the current design has a friction point:_would_leave_king_in_checkdoes simulate-and-revert, but a full AI search would needmake_move/undo_movepairs that are much faster than the current approach. You would want a dedicatedundo_move(move)method on Board that uses the Move object’s stored state to revert incrementally rather than deep-copying. - The Observer pattern integration discussed earlier becomes valuable here too: the AI can register as a listener to receive opponent moves and start computing its response during the opponent’s think time (pondering).
- Example: At the simplest level, a random AI is trivial with the current API: collect all pieces of the AI’s color, call
get_valid_movesfor each, pick a random valid move, callgame.make_move_by_position. Getting from random to Stockfish-level is an algorithm problem, but the architectural interface is already clean.
- Alpha-beta pruning requires evaluating millions of board states per second. The current
is_king_in_checkiterates all 16 opponent pieces for every candidate move. What data structures (attack maps, bitboards, Zobrist hashing) would you introduce to make this feasible? - If the AI player needs to run on a separate thread to avoid blocking the UI, how do you synchronize its move submission with the Game’s turn management? What happens if the human player resigns while the AI is mid-computation?