Chess: pieces, turns, check and a trial move
Part of the free Low-Level Design course on LogicWiz, in the chapter: Case studies, step by step. The class-diagram exercises run in the app.
Chess: pieces, turns, check and a trial move
"A queen is a rook plus a bishop, and you copied both loops into it. Now write the king. Does it extend the queen?"
Priya, reviewing Queen.java
The prompt, the first questions and the first classes
The prompt: design a chess game for two players. The board is 8 by 8, each kind of piece moves by its own rule, and players take turns. Refuse a move that is not allowed, including one that leaves your own king attacked. Tell the caller when a move gives check.
Chess has a lot of rules, so Arjun's first questions are about scope. In a 90-minute round, saying what you leave out is part of the answer. "I will skip castling, and here is where it would go" earns more credit than castling found half built at minute 85.
- Which rules are in? How each of the six piece types moves, captures, turns, and check. A move reports whether it gives check, and a move that leaves your own king attacked is refused.
- Which rules are out? Castling, en passant and pawn promotion. Also checkmate and stalemate: the program reports check, and the players decide when the game is over. Mate detection is a follow-up.
- Who plays, and how are squares named? Two people who call
move(from, to)in turn, with names from a1 to h8. Text such as "e2" becomes aSquareat the edge of the design. - What does the caller learn from a refused move? A reason: no piece there, not your turn, the piece cannot move like that, or the move leaves your king attacked. A refused move changes nothing.
- Do we need undo? Not now, but it is a likely follow-up. So each move is kept as an object.
Nouns and verbs: game, board, square, piece, colour, rule, move. Arjun draws Game, Board, Square, Piece, Color and PieceType. The verb "move" has two meanings, and Priya asks him to separate them. "Where could this piece go" is a question about a rule, so MoveRule joins the list. "This piece did go there" is a fact, a Move object that can be kept. Who decides whether a move is allowed is a third question. He leaves it open until the code forces it.
He leaves out a Player class, because a colour is enough, and Row and Column classes, because a square is two small numbers. Priya's plan: questions until minute 10, a diagram until minute 25, board and pieces working by minute 50, then check, then extension questions.
The first draft, and where it breaks
The natural first draft is one class per piece type, each with its own move logic. It matches how people talk about chess. For the draft the board is empty, so only the geometry matters.
abstract class Piece {
abstract List<int[]> moves(int file, int rank); // empty board, to keep the first draft small
}
class Rook extends Piece {
List<int[]> moves(int file, int rank) {
List<int[]> out = new ArrayList<>();
int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
for (int[] d : dirs) {
for (int f = file + d[0], r = rank + d[1]; f >= 0 && f < 8 && r >= 0 && r < 8; f += d[0], r += d[1]) {
out.add(new int[] {f, r});
}
}
return out;
}
}
class Queen extends Piece {
List<int[]> moves(int file, int rank) {
List<int[]> out = new ArrayList<>();
out.addAll(new Rook().moves(file, rank)); // copy of Rook's rule
out.addAll(new Bishop().moves(file, rank)); // copy of Bishop's rule
return out;
}
// ... Bishop is the same loop as Rook, with diagonal directions
}
Rook on d4: 14 moves
Bishop on d4: 13 moves
Queen on d4: 27 moves
It works: 14 and 13 make 27. Now Priya adds two requirements. "Write the king. Then write the black pawn."
The king moves like a queen, but one step only. Arjun is tempted to write King extends Queen and cut the distance. Then a king is a queen, and any code that asks "is this a queen?" gives a wrong answer. The pawn is worse. A white pawn moves up the board and a black pawn moves down. Either he writes WhitePawn and BlackPawn, and every other piece needs a colour version too, or every piece gets an if on colour.
Look at what the draft ties together: the kind of piece, its colour and the way it moves. They vary on their own. The movement is shared across kinds: a queen, a rook and a bishop all slide. Colour is only a value. This is the composition over inheritance rule from the object-oriented basics chapter.
The decision: a piece has a colour, a type and a rule
A Piece becomes a small class holding three things: a colour, a type and one MoveRule object. The piece has no movement code of its own. First the two small types it uses.
enum Color {
WHITE, BLACK;
Color opposite() { return this == WHITE ? BLACK : WHITE; }
/** The direction a pawn of this colour moves along the ranks. */
int forward() { return this == WHITE ? 1 : -1; }
}
/** A square on the board: file 0 to 7 is a to h, rank 0 to 7 is 1 to 8. */
record Square(int file, int rank) {
static Square parse(String text) {
if (text == null || text.length() != 2) return null;
int file = text.charAt(0) - 'a';
int rank = text.charAt(1) - '1';
return inBounds(file, rank) ? new Square(file, rank) : null;
}
static boolean inBounds(int file, int rank) { return file >= 0 && file < 8 && rank >= 0 && rank < 8; }
}
The rule interface has two questions, and the reason is the pawn. attacks asks: does a piece of this kind on from attack the square to, with nothing in the way? canMove asks: may it move there, ignoring check? For most pieces the two are the same, so canMove has a default built on attacks. A pawn is different: it moves straight but attacks diagonally. Check detection needs the attack, and a move needs the movement.
interface MoveRule {
/** True if a piece of this kind on `from` attacks `to`: the shape fits and nothing stands in the way. */
boolean attacks(Board board, Piece piece, Square from, Square to);
/** True if the piece may move there, ignoring check: it attacks the square and the square does not hold its own colour. */
default boolean canMove(Board board, Piece piece, Square from, Square to) {
Piece target = board.at(to);
return attacks(board, piece, from, to) && (target == null || target.color() != piece.color());
}
}
/** Rook, bishop and queen: slide any distance along the given directions until something blocks. */
final class SlidingRule implements MoveRule {
static final int[][] STRAIGHT = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
static final int[][] DIAGONAL = {{1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
static final int[][] ALL = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}, {1, 1}, {1, -1}, {-1, 1}, {-1, -1}};
private final int[][] directions;
SlidingRule(int[][] directions) { this.directions = directions; }
@Override
public boolean attacks(Board board, Piece piece, Square from, Square to) {
for (int[] d : directions) {
int f = from.file() + d[0], r = from.rank() + d[1];
while (Square.inBounds(f, r)) {
if (f == to.file() && r == to.rank()) return true;
if (board.at(new Square(f, r)) != null) break; // a piece blocks the rest of this line
f += d[0];
r += d[1];
}
}
return false;
}
}
Read the loop slowly. It walks one step at a time along a direction. If it reaches the target square, the piece attacks it. If it meets any piece first, that piece blocks the rest of the line, so it stops. The knight and the king need no walking: each makes fixed jumps that nothing can block.
/** King and knight: one fixed jump, which nothing can block. */
final class StepRule implements MoveRule {
static final int[][] KNIGHT_JUMPS = {{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}};
private final int[][] steps;
StepRule(int[][] steps) { this.steps = steps; }
@Override
public boolean attacks(Board board, Piece piece, Square from, Square to) {
for (int[] s : steps) if (from.file() + s[0] == to.file() && from.rank() + s[1] == to.rank()) return true;
return false;
}
}
/** The pawn is the odd one: it moves straight, but attacks and captures diagonally. */
final class PawnRule implements MoveRule {
@Override
public boolean attacks(Board board, Piece piece, Square from, Square to) {
return to.rank() == from.rank() + piece.color().forward() && Math.abs(to.file() - from.file()) == 1;
}
@Override
public boolean canMove(Board board, Piece piece, Square from, Square to) {
int forward = piece.color().forward();
Piece target = board.at(to);
if (to.file() == from.file()) {
if (target != null) return false; // straight moves never capture
if (to.rank() == from.rank() + forward) return true;
int startRank = piece.color() == Color.WHITE ? 1 : 6;
return from.rank() == startRank && to.rank() == from.rank() + 2 * forward
&& board.at(new Square(from.file(), from.rank() + forward)) == null; // both squares must be empty
}
return attacks(board, piece, from, to) && target != null && target.color() != piece.color();
}
}
Now a piece is a colour, a type and the right rule, chosen in one place.
final class Piece {
private final Color color;
private final PieceType type;
private final MoveRule rule;
private Piece(Color color, PieceType type, MoveRule rule) { this.color = color; this.type = type; this.rule = rule; }
static Piece of(Color color, PieceType type) {
MoveRule rule = switch (type) {
case PAWN -> new PawnRule();
case KNIGHT -> new StepRule(StepRule.KNIGHT_JUMPS);
case BISHOP -> new SlidingRule(SlidingRule.DIAGONAL);
case ROOK -> new SlidingRule(SlidingRule.STRAIGHT);
case QUEEN -> new SlidingRule(SlidingRule.ALL);
case KING -> new StepRule(SlidingRule.ALL);
};
return new Piece(color, type, rule);
}
}
ROOK on d4: 14 moves
BISHOP on d4: 13 moves
QUEEN on d4: 27 moves
KNIGHT on d4: 8 moves
KING on d4: 8 moves
The numbers match the draft. The queen is the rook and bishop directions in one list. The king is the same eight directions as a single jump, so no King extends Queen. Colour is a field, so there is no WhitePawn. Each rule is one interchangeable way of moving, which makes this the Strategy pattern.
One limit is honest to state. A piece holds one rule. A variant piece that moves like a rook and a knight would need a rule that combines two rules. That would be a small new class, not a new class tree.
classDiagram
class Game
class Board
class Piece
class MoveRule {
<<interface>>
+attacks(board, piece, from, to) boolean
+canMove(board, piece, from, to) boolean
}
class MoveValidator
Game *-- Board
Game --> MoveValidator
Board o-- Piece
Piece --> MoveRule
MoveRule <|.. SlidingRule
MoveRule <|.. StepRule
MoveRule <|.. PawnRule
The key method: try the move, look at the king, take it back
A rule only knows its own piece, so canMove can say yes to a move that leaves your own king attacked. That happens when a pinned piece moves away, or when a king steps onto an attacked square. A pin is when a piece shields its king from an attacker. Someone has to decide the whole question: who? Not the Board, which only stores pieces, and not a Piece, which knows only its own rule. Arjun adds a class for it: MoveValidator.
Its test is direct. Make the move, look at your own king, and take the move back. For that, a move must be an object that can apply itself and undo itself. This is the command pattern: a request turned into an object, so it can be run and reversed. The Move remembers what it captured, so that revert can put it back.
final class Move {
private final Square from, to;
private final Piece piece, captured;
Move(Board board, Square from, Square to) {
this.from = from;
this.to = to;
this.piece = board.at(from);
this.captured = board.at(to);
}
Piece captured() { return captured; }
void apply(Board board) {
board.set(from, null);
board.set(to, piece);
}
void revert(Board board) {
board.set(to, captured);
board.set(from, piece);
}
}
The validator checks in a fixed order, and each failure returns a reason: no piece, not your turn, illegal shape, or king in check. isInCheck asks the board whether any enemy piece attacks the king's square, and the board asks each enemy piece's own rule.
final class MoveValidator {
MoveStatus validate(Board board, Color turn, Square from, Square to) {
Piece piece = board.at(from);
if (piece == null) return MoveStatus.NO_PIECE;
if (piece.color() != turn) return MoveStatus.NOT_YOUR_TURN;
if (!piece.rule().canMove(board, piece, from, to)) return MoveStatus.ILLEGAL_MOVE;
Move trial = new Move(board, from, to);
trial.apply(board); // try it, look at the king, take it back
boolean exposed = isInCheck(board, turn);
trial.revert(board);
return exposed ? MoveStatus.KING_IN_CHECK : MoveStatus.OK;
}
boolean isInCheck(Board board, Color color) {
return board.isAttackedBy(color.opposite(), board.kingOf(color));
}
}
/** True if any piece of the given colour attacks the square. */
boolean isAttackedBy(Color color, Square target) {
for (int f = 0; f < 8; f++)
for (int r = 0; r < 8; r++) {
Piece p = grid[f][r];
if (p != null && p.color() == color && p.rule().attacks(this, p, new Square(f, r), target)) return true;
}
return false;
}
The whole board is checked after the trial move, so pins and king safety need no special code. Game.move ties it together: ask the validator, then apply the move for real, keep it in the history, pass the turn, and report whether the other king is now in check.
MoveResult move(Square from, Square to) {
MoveStatus status = validator.validate(board, turn, from, to);
if (status != MoveStatus.OK) return new MoveResult(status, null, false);
Move move = new Move(board, from, to);
move.apply(board);
history.push(move);
turn = turn.opposite();
return new MoveResult(MoveStatus.OK, move.captured(), validator.isInCheck(board, turn));
}
Arjun scripts a game that ends in check, and tries moves that must be refused.
e2 to e4 -> OK
d7 to d6 -> OK
f1 to b5 -> OK, check
a7 to a6 -> KING_IN_CHECK
b8 to c6 -> OK
g1 to f3 -> OK
c6 to d4 -> KING_IN_CHECK
a7 to a6 -> OK
b5 to c6 -> OK, captured knight, check
e8 to d7 -> KING_IN_CHECK
Read it as a story. The bishop's move to b5 gives check. Black's a7 to a6 does not answer it, so it is refused. The knight on c6 blocks the check, and it is now pinned: c6 to d4 is refused, because it would expose the king. After the bishop takes the knight with check, the king may not step to d7, which the bishop attacks. A refused move changes nothing, because the trial move was taken back.
Extend it, then try to break it
Extension: undo. It is out of scope for the first version, but the design is ready for it. Game already keeps each accepted Move on a stack. Undo pops the last one, reverts it and gives the turn back.
/** Takes back the last accepted move and gives the turn back. */
void undo() {
if (history.isEmpty()) return;
history.pop().revert(board);
turn = turn.opposite();
}
After the capture on c6, one undo() puts the black knight back and gives the turn to white.
board r1bqkbnr/1pp1pppp/p1Bp4/8/4P3/5N2/PPPP1PPP/RNBQK2R b
board r1bqkbnr/1pp1pppp/p1np4/1B6/4P3/5N2/PPPP1PPP/RNBQK2R w
The other way to undo is the memento pattern: save a copy of the whole board before each move, and restore it. A board is 64 references, so a copy is cheap, and it is the safer choice when a move changes many things. Castling moves two pieces, so with commands it becomes its own kind of Move that reverts both. En passant and promotion are the same story. Say that aloud, even though they are out of scope.
Break it: two clients. If a web server lets both players send moves, move first checks the turn and then acts. That is check-then-act, so two requests for the same turn could both pass the check. Making move synchronized on the game fixes it, because one game is one lock.
💡 Tip: Castling needs "is this empty square attacked?". Because a rule has
attacksapart fromcanMove,board.isAttackedByalready answers that, including for a pawn's diagonal squares.
Your turn
Draw the core of the piece design in the class designer. The starter is the draft: a Piece base class with a subclass per piece. Replace the subclasses with rule objects, add the Move command with revert, and add the validator.
Then open the Chess problem in the problem library. Design it again without the model answer, compare, and follow its "Build it on your machine" kit to write the working version in your IDE.
📌 Summary: Fix the rules scope first. A piece holds a colour, a type and one move rule. A validator tries each move as a command and reverts it, which handles pins and king safety.
What Arjun learns next
Eight case studies are behind him. Next, Priya walks him through the machine-coding round itself: how to spend 90 minutes, what to build first and what to skip.
Check your understanding
- Rook, bishop and queen slide, and knight and king jump. A queen is a rook and a bishop together. Which design shares the code?
- A white bishop is pinned: if it moves, the white king is attacked. Where does the program refuse that move?
- Why does MoveRule have both attacks and canMove, when most rules answer them the same way?
- Why is a move an object with apply and revert, and not a method on Game that edits the board?