Polymorphic move validation per piece instead of a giant switch, check/checkmate as a separate concern, turn management with move history, and which edge cases to discuss rather than fully implement.
Published September 23, 2026
The most structurally demanding machine-coding prompt in this course — chess has real complexity (per-piece movement rules, check/checkmate, special moves) that rewards correct separation of concerns far more than tic-tac-toe or a parking lot do.
abstract class Piece {
protected final Color color;
protected Position position;
Piece(Color color, Position position) { this.color = color; this.position = position; }
abstract List<Position> getValidMoves(Board board); // polymorphism — no piece-type switch anywhere else
}
class King extends Piece {
List<Position> getValidMoves(Board board) { /* one square any direction */ return List.of(); }
}
class Rook extends Piece {
List<Position> getValidMoves(Board board) { /* straight lines until blocked */ return List.of(); }
}
class Bishop extends Piece {
List<Position> getValidMoves(Board board) { /* diagonals until blocked */ return List.of(); }
}
// Queen, Knight, Pawn follow the same shape
class Move {
final Position from, to;
final Piece movedPiece;
final Piece capturedPiece; // null if no capture
}
class Player {
final Color color;
final String name;
}
class ChessGame {
boolean isValidMove(Piece piece, Position target) {
return piece.getValidMoves(board).contains(target); // delegates entirely — no if/else on piece type here
}
}
The alternative — one method containing if (piece instanceof Rook) { ... } else if (piece instanceof Bishop) { ... } for all six piece types — is exactly the OCP violation Single Responsibility & Open/Closed warns about: adding a new piece type (a custom variant piece, say) would mean editing this central method. With getValidMoves() as an abstract method each Piece subclass implements, adding a new piece type means writing one new subclass, touching nothing else.
class CheckDetector {
boolean isInCheck(Board board, Color kingColor) {
Position kingPos = board.findKing(kingColor);
return board.getAllPieces(oppositeColor(kingColor)).stream()
.anyMatch(p -> p.getValidMoves(board).contains(kingPos));
}
boolean isCheckmate(Board board, Color kingColor) {
if (!isInCheck(board, kingColor)) return false;
// checkmate = in check AND no legal move exists that escapes check
return board.getAllPieces(kingColor).stream()
.allMatch(p -> p.getValidMoves(board).stream()
.noneMatch(move -> moveEscapesCheck(board, p, move, kingColor)));
}
}
Keeping this in a separate CheckDetector rather than folding it into Piece.getValidMoves() matters for a subtle correctness reason: a piece's raw movement pattern ("a rook moves in straight lines") is a different question from "is this specific move legal right now" (a move that would leave your own king in check is illegal, even if it matches the piece's raw movement pattern) — conflating the two inside each piece class would mean every piece needs to know about check detection, a much larger coupling than piece-movement logic should have.
class ChessGame {
private final Deque<Move> moveHistory = new ArrayDeque<>();
private Player currentPlayer;
void makeMove(Position from, Position to) {
Piece piece = board.getPieceAt(from);
Move move = new Move(from, to, piece, board.getPieceAt(to));
board.applyMove(move);
moveHistory.push(move); // enables undo AND is exactly the Command Pattern's command-history idea
currentPlayer = getOpponent(currentPlayer);
}
void undoLastMove() {
Move last = moveHistory.pop();
board.reverseMove(last); // uses captured piece info from the Move object to restore it
currentPlayer = getOpponent(currentPlayer);
}
}
Storing each Move as its own object (not just mutating board state and discarding the record) is precisely the Command Pattern's shape — undo works because each Move carries enough information (capturedPiece, in particular) to reverse itself, the same principle as InsertCommand.undo() in Command Pattern.
Castling, en passant, and pawn promotion are legitimate edge cases worth naming explicitly in an interview (showing awareness they exist and roughly how they'd fit — e.g. castling needs to track whether the king/rook have ever moved, since that affects legality) rather than fully implementing under interview time pressure. Similarly, extending to online multiplayer is explicitly an HLD concern (network synchronization, move broadcasting, reconnection handling) layered on top of this LLD design, not a change to the class structure itself — naming that distinction clearly is itself a signal of architectural maturity.
Q: Why does getValidMoves() take the board as a parameter instead of the Piece holding a board reference? A: Passing the board explicitly keeps Piece subclasses stateless with respect to the game (they hold only their own color/position), making them easier to test in isolation and avoiding a piece needing to be told about board changes it doesn't directly cause.
Q: How would you detect stalemate, distinct from checkmate? A: Nearly identical logic to isCheckmate(), with one flipped condition — stalemate is 'no legal move exists' while NOT in check (checkmate requires being in check). Both share the 'does any legal move exist' computation; only the check-status precondition differs.
Q: Where would castling's legality state (has the king/rook moved) actually live? A: Most naturally as boolean flags on the King and Rook pieces themselves (hasMoved), checked by a CastlingRule (or an extension to getValidMoves() specifically for King) — this is exactly why 'discuss, don't implement' is the right call under time pressure: the state genuinely belongs somewhere non-obvious, and getting it right takes real design thought.
Q: Isn't storing capturedPiece on every Move wasteful when most moves don't capture anything? A: A null capturedPiece field costs essentially nothing (a single reference, unset) — this is a non-issue in practice, and the alternative (a separate capture-tracking structure) would add complexity without a real performance benefit at the scale a single chess game operates at.