Skip to main content

Chess Game

Chess is the interview prompt people fear because "the rules" feel enormous. The trick is that a design-level answer doesn't implement every rule - it builds a shape where each piece answers one question (can I legally move there?) and the board never has to know the answer itself.

Requirements

Functional

  • An 8x8 board holds pieces for two players (White and Black), each piece knowing its own movement rule (pawn, rook, knight, bishop, queen, king).
  • A move is only applied if the piece's movement rule allows it and the destination isn't occupied by the mover's own piece.
  • After every move, the game can answer "is either king currently in check?" and, from there, "is that check a checkmate?"
  • Turns alternate strictly between the two players.

Non-functional

  • Adding a new piece type (a custom variant piece, say) should mean one new class, not a change to Board or Game.
  • Check detection must reuse the same movement logic pieces already have for ordinary moves
    • a second, parallel "can this piece threaten this square" implementation would drift from the first the moment either one is patched.

Design

Every concrete piece extends an abstract Piece and implements one method, canMove(board, from, to). That single seam is doing two jobs at once: it's how a normal move gets validated, and it's how check gets detected - "is the king in check" is just "does any opposing piece's canMove say yes to the king's square," with no separate checking algorithm to keep in sync.

PlayerGameBoardPiece (e.g. Rook)makeMove(from, to)1pieceAt(from)2canMove(board, from, to)3apply(move)4canMove(board, *, kingSquare)5
  1. 1A player proposes a move; the game is the only entry point.
  2. 2The game looks up whichever piece is sitting on the source square.
  3. 3The piece itself, not the board or game, decides whether that path is legal for its type.
  4. 4If legal, the move is applied and recorded so it can be undone if it turns out to self-check.
  5. 5Check detection asks every opposing piece the same canMove question, aimed at the king's square.

Game never asks "what kind of piece is this" anywhere in its own code. It asks the piece.

Class diagram

«abstract»Piece# color: Color# position: Position+ canMove(board, from, to): boolGame- board: Board- turn: Color- history: List<Move>+ makeMove(from, to): bool+ isInCheck(color): bool+ isCheckmate(color): boolBoard- squares: Piece[8][8]+ pieceAt(pos): Piece+ place(piece, pos): voidPawn+ canMove(board, from, to): boolRook+ canMove(board, from, to): boolKnight+ canMove(board, from, to): boolKing+ canMove(board, from, to): boolMove- from: Position- to: Position- capturedPiece: PiecePlayer- name: string- color: Color
extendsusescreates
Piece is the one polymorphic seam - every concrete piece answers canMove for itself; Game and Board never branch on piece type.

Code

import java.util.*;
 
enum Color { WHITE, BLACK }
 
class Position {
final int row, col;
 
Position(int row, int col) {
this.row = row;
this.col = col;
}
}
 
abstract class Piece {
final Color color;
Position position;
 
Piece(Color color, Position position) {
this.color = color;
this.position = position;
}
 
abstract boolean canMove(Board board, Position from, Position to);
}
 
class Rook extends Piece {
Rook(Color color, Position position) { super(color, position); }
 
public boolean canMove(Board board, Position from, Position to) {
if (from.row != to.row && from.col != to.col) return false;
return board.isPathClear(from, to);
}
}
 
class Knight extends Piece {
Knight(Color color, Position position) { super(color, position); }
 
public boolean canMove(Board board, Position from, Position to) {
int dr = Math.abs(from.row - to.row);
int dc = Math.abs(from.col - to.col);
return (dr == 2 && dc == 1) || (dr == 1 && dc == 2);
}
}
 
class Pawn extends Piece {
Pawn(Color color, Position position) { super(color, position); }
 
public boolean canMove(Board board, Position from, Position to) {
int direction = color == Color.WHITE ? 1 : -1;
int dr = to.row - from.row;
int dc = to.col - from.col;
Piece target = board.pieceAt(to);
if (dc == 0 && target == null) return dr == direction;
return dc != 0 && Math.abs(dc) == 1 && dr == direction && target != null;
}
}
 
class King extends Piece {
King(Color color, Position position) { super(color, position); }
 
public boolean canMove(Board board, Position from, Position to) {
return Math.abs(from.row - to.row) <= 1 && Math.abs(from.col - to.col) <= 1;
}
}
 
class Move {
final Position from;
final Position to;
final Piece capturedPiece;
 
Move(Position from, Position to, Piece capturedPiece) {
this.from = from;
this.to = to;
this.capturedPiece = capturedPiece;
}
}
 
class Board {
private final Piece[][] squares = new Piece[8][8];
 
Piece pieceAt(Position pos) {
return squares[pos.row][pos.col];
}
 
void place(Piece piece, Position pos) {
squares[pos.row][pos.col] = piece;
piece.position = pos;
}
 
boolean isPathClear(Position from, Position to) {
int dr = Integer.signum(to.row - from.row);
int dc = Integer.signum(to.col - from.col);
int r = from.row + dr, c = from.col + dc;
while (r != to.row || c != to.col) {
if (squares[r][c] != null) return false;
r += dr;
c += dc;
}
return true;
}
 
List<Piece> piecesOf(Color color) {
List<Piece> result = new ArrayList<>();
for (Piece[] row : squares)
for (Piece p : row)
if (p != null && p.color == color) result.add(p);
return result;
}
}
 
class Player {
final String name;
final Color color;
 
Player(String name, Color color) {
this.name = name;
this.color = color;
}
}
 
class Game {
private final Board board;
private Color turn = Color.WHITE;
private final List<Move> history = new ArrayList<>();
 
Game(Board board) {
this.board = board;
}
 
boolean makeMove(Position from, Position to) {
Piece piece = board.pieceAt(from);
if (piece == null || piece.color != turn) return false;
if (!piece.canMove(board, from, to)) return false;
Piece captured = board.pieceAt(to);
board.place(piece, to);
board.place(null, from);
history.add(new Move(from, to, captured));
turn = (turn == Color.WHITE) ? Color.BLACK : Color.WHITE;
return true;
}
 
boolean isInCheck(Color color) {
Position kingSquare = findKing(color);
Color enemy = (color == Color.WHITE) ? Color.BLACK : Color.WHITE;
for (Piece p : board.piecesOf(enemy)) {
if (p.canMove(board, p.position, kingSquare)) return true;
}
return false;
}
 
private Position findKing(Color color) {
for (Piece p : board.piecesOf(color))
if (p instanceof King) return p.position;
throw new IllegalStateException("King missing for " + color);
}
}

Design decisions

  • Piece is an abstract class with one abstract method, not a PieceType enum and a switch in Board. A switch statement over piece type grows a new case every time someone adds a variant piece, and it lives far from the rule it's implementing. Polymorphism puts a rook's rule inside Rook, where anyone reading that one file sees the whole rule.
  • Check detection is not a separate algorithm - it reuses canMove. "Is the king in check" is answered by asking every opposing piece "can you legally move to the king's square right now," the exact question Game.makeMove already asks before any ordinary move. One rule, two callers, instead of two rules that can silently diverge.
  • Move is its own object rather than two loose coordinates. Once check/checkmate logic needs to try a move, see if it leaves the mover's own king in check, and undo it if so, having a Move object (with the captured piece, if any) makes "undo" a single operation instead of reconstructing state by hand.
  • What's missing for a real system: this design validates raw piece movement and basic check/checkmate but stops short of pins (a move that's otherwise legal but would expose your own king), en passant, castling, draw conditions like threefold repetition, and even a couple of ordinary pawn rules - the two-square opening advance and promotion on reaching the back rank - each is a real rule, but naming them as future extension points is the point of a 45-minute design answer, not implementing all of them.
0%0 of 122 pages studied