Why this matters: in the implementation act the interviewer goes quiet and watches one thing — whether your code matches your words. You promised two layers, candidates-then-legality, king safety in one place. Here is what keeping that promise looks like on the page.
The data model
enum Color { WHITE, BLACK }
enum PieceType { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN }
class Piece { PieceType type; Color color; }
class Square { int file; int rank; } // 0..7, 0..7
class Move { Square from; Square to; } // a value object; no logic
class Position {
Piece[][] board = new Piece[8][8]; // null = empty
Color toMove;
Position copy() { /* deep-copy board, same toMove */ }
void apply(Move m) { board[m.to] = board[m.from]; board[m.from] = null; }
}Position.copy() is the simulation workhorse. At interactive rates a 64-cell copy per validation is nothing — say that, don't apologize for it.
Movement rules: one interface, six small implementations
interface MovementRules {
// Ignoring king safety: could this piece make this move here?
boolean canMove(Position p, Move m);
}The sliding pieces — rook, bishop, queen — share one helper, which is the only clever code in the file:
// Walk from 'from' toward 'to' one step at a time along (df, dr).
// True iff 'to' is reached with every intermediate square empty.
boolean rayClear(Position p, Move m, int df, int dr) {
int f = m.from.file + df, r = m.from.rank + dr;
while (f != m.to.file || r != m.to.rank) {
if (p.board[f][r] != null) return false; // blocker
f += df; r += dr;
}
return true;
}The rook accepts a move if it is on-file or on-rank and rayClear; the bishop the diagonal version; the queen is rook-or-bishop. The knight is a lookup against its eight offsets — no ray, jumps over everything. The king moves one square any direction. The pawn earns its reputation: forward one (two from its start rank) onto empty squares only, capture diagonally only — direction depending on color. Every rule also rejects landing on a friendly piece; that check lives in one shared guard, not six copies.
Attack detection: the reuse payoff
boolean isSquareAttacked(Position p, Square target, Color by) {
for (Square s : allSquares()) {
Piece pc = p.board[s.file][s.rank];
if (pc == null || pc.color != by) continue;
if (rulesFor(pc.type).canMove(p, new Move(s, target))) return true;
}
return false;
}No new chess knowledge — the movement rules ARE the attack rules. (One honest wrinkle to name if asked: the pawn attacks diagonally even when it couldn't move there, so the pawn's rule treats a diagonal onto an enemy-or-target square as a capture pattern. Naming the wrinkle unprompted reads as mastery.)
The legality layer: four steps, verbatim from the design
boolean isLegal(Position p, Move m) {
Piece pc = p.board[m.from.file][m.from.rank];
if (pc == null || pc.color != p.toMove) return false; // 1. turn
if (!rulesFor(pc.type).canMove(p, m)) return false; // 2. candidate
Position scratch = p.copy(); // 3. simulate
scratch.apply(m);
Square king = findKing(scratch, p.toMove);
return !isSquareAttacked(scratch, king, opposite(p.toMove)); // 4. king safe
}Ten lines, and the pin comes for free: slide a pinned bishop sideways, the scratch position exposes the king, step 4 says no. No pin-specific code exists anywhere — that absence is the design working, and it is worth pointing at during the round.
If the interviewer presses on the copy, show the alternative shape without switching to it: apply returns the captured piece, revert puts both pieces back — a disciplined pair, faster, one more way to write a bug. Copy first, measure, then earn the optimization.
Where validation lives — and doesn't
The UI calls isLegal and nothing else. Movement rules are package-private; nobody outside the module can ask a rook its opinion and mistake it for a verdict. That boundary is the module's contract: one entry point, one meaning of "legal."
Key takeaway
The implementation keeps the design's promise line by line: six small movement rules sharing one ray-walker, attack detection that reuses those same rules, and a four-step isLegal whose simulation makes pins and checks fall out of a single code path. The most senior thing on the page is what's absent — no pin logic, no per-piece king checks, no second definition of legality anywhere.