Low Level Design (LLD) Interview Questions
Design a Chess Game
Tier: CommonDifficulty: MediumAsked of: Mid, SeniorAsked at: Amazon, Adobe, Microsoft
Design chess for two players who take turns moving pieces. The game must reject illegal moves and keep each player's king safe.
The problem
White moves a pawn from e2 to e4. Black moves next. Moving a rook along an open row is still illegal if doing so exposes its own king to attack.
Start with the normal starting board, all six piece types, captures, check, checkmate and stalemate. Promotion always creates a queen in this version. Castling, en passant, alternative promotions and repetition draw rules are follow-ups. There is no computer opponent or network play.
How to explain the design
“I store the board and whose turn it is. To validate a move, I first check the piece's movement rule and whether its path is clear. Then I temporarily make the move and check whether my king is attacked. If it is, the move is illegal. I restore the board after the check, and only apply the move permanently when it passes.”
A Piece has a kind and a color. Chess owns the board, turn and status. The six movement rules are short cases in one function. Separate piece classes are useful if those rules grow, but they are not required for this first implementation.
Walk through one move
- Check the board coordinates, current player and destination.
- Check how that kind of piece moves. Sliding pieces also need a clear path.
- Try the move, test the moving player's king and restore both squares.
- If safe, make the move and switch turns.
- Check whether the next player has any legal move. No legal moves while in check means checkmate. No legal moves without check means stalemate.
Interview implementation
Build canReach, then legal, then move. The code numbers rows and columns from 0 to 7, with Black's back row at row 0. White's e2 to e4 is move(6, 4, 4, 4).
This is the longest core example here because six movement rules are part of the agreed scope. Explain and implement the move validation flow first, then fill in the remaining piece rules with the interviewer.
Java
Chess.java
package interview.chess;
public class Chess {
private enum Kind { ROOK, KNIGHT, BISHOP, QUEEN, KING, PAWN }
private record Piece(Kind kind, boolean white) {}
public enum Status { PLAYING, CHECK, CHECKMATE, STALEMATE }
private final Piece[] board = new Piece[64];
private boolean whiteToMove = true;
private Status status = Status.PLAYING;
public Chess() {
Kind[] back = {Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK};
for (int col = 0; col < 8; col++) {
board[col] = new Piece(back[col], false);
board[8 + col] = new Piece(Kind.PAWN, false);
board[48 + col] = new Piece(Kind.PAWN, true);
board[56 + col] = new Piece(back[col], true);
}
}
public Status status() { return status; }
public boolean whiteToMove() { return whiteToMove; }
public void move(int fromRow, int fromCol, int toRow, int toCol) {
if (status == Status.CHECKMATE || status == Status.STALEMATE) throw new IllegalStateException("Game over");
for (int value : new int[]{fromRow, fromCol, toRow, toCol}) {
if (value < 0 || value > 7) throw new IllegalArgumentException("Invalid square");
}
int from = fromRow * 8 + fromCol;
int to = toRow * 8 + toCol;
if (!legal(from, to)) throw new IllegalArgumentException("Illegal move");
Piece piece = board[from];
board[to] = piece.kind() == Kind.PAWN && (toRow == 0 || toRow == 7) ? new Piece(Kind.QUEEN, piece.white()) : piece;
board[from] = null;
whiteToMove = !whiteToMove;
boolean canMove = false;
for (int start = 0; start < 64 && !canMove; start++) {
for (int end = 0; end < 64 && !canMove; end++) canMove = legal(start, end);
}
boolean checked = inCheck(whiteToMove);
status = !canMove ? (checked ? Status.CHECKMATE : Status.STALEMATE) : (checked ? Status.CHECK : Status.PLAYING);
}
private boolean legal(int from, int to) {
Piece piece = board[from];
if (piece == null || piece.white() != whiteToMove ||
(board[to] != null && board[to].kind() == Kind.KING) || !canReach(from, to)) return false;
Piece captured = board[to];
board[to] = piece;
board[from] = null;
boolean safe = !inCheck(piece.white());
board[from] = piece;
board[to] = captured; // Undo the trial move.
return safe;
}
private boolean inCheck(boolean white) {
int king = -1;
for (int i = 0; i < 64; i++) {
if (board[i] != null && board[i].kind() == Kind.KING && board[i].white() == white) king = i;
}
for (int i = 0; i < 64; i++) {
if (board[i] != null && board[i].white() != white && canReach(i, king)) return true;
}
return false;
}
private boolean canReach(int from, int to) {
Piece piece = board[from];
if (piece == null || from == to || (board[to] != null && board[to].white() == piece.white())) return false;
int row = to / 8 - from / 8;
int col = to % 8 - from % 8;
return switch (piece.kind()) {
case ROOK -> (row == 0 || col == 0) && clearPath(from, to);
case BISHOP -> Math.abs(row) == Math.abs(col) && clearPath(from, to);
case QUEEN -> (row == 0 || col == 0 || Math.abs(row) == Math.abs(col)) && clearPath(from, to);
case KNIGHT -> Math.abs(row) * Math.abs(col) == 2;
case KING -> Math.max(Math.abs(row), Math.abs(col)) == 1;
case PAWN -> {
int direction = piece.white() ? -1 : 1;
int startRow = piece.white() ? 6 : 1;
if (col == 0 && board[to] == null) {
yield row == direction || (from / 8 == startRow && row == 2 * direction && board[from + 8 * direction] == null);
}
yield Math.abs(col) == 1 && row == direction && board[to] != null;
}
};
}
private boolean clearPath(int from, int to) {
int step = Integer.signum(to / 8 - from / 8) * 8 + Integer.signum(to % 8 - from % 8);
for (int square = from + step; square != to; square += step) {
if (board[square] != null) return false;
}
return true;
}
}
package interview.chess;
public class Chess {
private enum Kind { ROOK, KNIGHT, BISHOP, QUEEN, KING, PAWN }
private record Piece(Kind kind, boolean white) {}
public enum Status { PLAYING, CHECK, CHECKMATE, STALEMATE }
private final Piece[] board = new Piece[64];
private boolean whiteToMove = true;
private Status status = Status.PLAYING;
public Chess() {
Kind[] back = {Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK};
for (int col = 0; col < 8; col++) {
board[col] = new Piece(back[col], false);
board[8 + col] = new Piece(Kind.PAWN, false);
board[48 + col] = new Piece(Kind.PAWN, true);
board[56 + col] = new Piece(back[col], true);
}
}
public Status status() { return status; }
public boolean whiteToMove() { return whiteToMove; }
public void move(int fromRow, int fromCol, int toRow, int toCol) {
if (status == Status.CHECKMATE || status == Status.STALEMATE) throw new IllegalStateException("Game over");
for (int value : new int[]{fromRow, fromCol, toRow, toCol}) {
if (value < 0 || value > 7) throw new IllegalArgumentException("Invalid square");
}
int from = fromRow * 8 + fromCol;
int to = toRow * 8 + toCol;
if (!legal(from, to)) throw new IllegalArgumentException("Illegal move");
Piece piece = board[from];
board[to] = piece.kind() == Kind.PAWN && (toRow == 0 || toRow == 7) ? new Piece(Kind.QUEEN, piece.white()) : piece;
board[from] = null;
whiteToMove = !whiteToMove;
boolean canMove = false;
for (int start = 0; start < 64 && !canMove; start++) {
for (int end = 0; end < 64 && !canMove; end++) canMove = legal(start, end);
}
boolean checked = inCheck(whiteToMove);
status = !canMove ? (checked ? Status.CHECKMATE : Status.STALEMATE) : (checked ? Status.CHECK : Status.PLAYING);
}
private boolean legal(int from, int to) {
Piece piece = board[from];
if (piece == null || piece.white() != whiteToMove ||
(board[to] != null && board[to].kind() == Kind.KING) || !canReach(from, to)) return false;
Piece captured = board[to];
board[to] = piece;
board[from] = null;
boolean safe = !inCheck(piece.white());
board[from] = piece;
board[to] = captured;
return safe;
}
private boolean inCheck(boolean white) {
int king = -1;
for (int i = 0; i < 64; i++) {
if (board[i] != null && board[i].kind() == Kind.KING && board[i].white() == white) king = i;
}
for (int i = 0; i < 64; i++) {
if (board[i] != null && board[i].white() != white && canReach(i, king)) return true;
}
return false;
}
private boolean canReach(int from, int to) {
Piece piece = board[from];
if (piece == null || from == to || (board[to] != null && board[to].white() == piece.white())) return false;
int row = to / 8 - from / 8;
int col = to % 8 - from % 8;
return switch (piece.kind()) {
case ROOK -> (row == 0 || col == 0) && clearPath(from, to);
case BISHOP -> Math.abs(row) == Math.abs(col) && clearPath(from, to);
case QUEEN -> (row == 0 || col == 0 || Math.abs(row) == Math.abs(col)) && clearPath(from, to);
case KNIGHT -> Math.abs(row) * Math.abs(col) == 2;
case KING -> Math.max(Math.abs(row), Math.abs(col)) == 1;
case PAWN -> {
int direction = piece.white() ? -1 : 1;
int startRow = piece.white() ? 6 : 1;
if (col == 0 && board[to] == null) {
yield row == direction || (from / 8 == startRow && row == 2 * direction && board[from + 8 * direction] == null);
}
yield Math.abs(col) == 1 && row == direction && board[to] != null;
}
};
}
private boolean clearPath(int from, int to) {
int step = Integer.signum(to / 8 - from / 8) * 8 + Integer.signum(to % 8 - from % 8);
for (int square = from + step; square != to; square += step) {
if (board[square] != null) return false;
}
return true;
}
}
Kotlin
Chess.kt
package interview.chess
import kotlin.math.abs
import kotlin.math.sign
enum class Kind { ROOK, KNIGHT, BISHOP, QUEEN, KING, PAWN }
data class Piece(val kind: Kind, val white: Boolean)
enum class Status { PLAYING, CHECK, CHECKMATE, STALEMATE }
class Chess {
private val board = arrayOfNulls<Piece>(64)
var whiteToMove = true
private set
var status = Status.PLAYING
private set
init {
val back = listOf(Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK)
for (col in 0..7) {
board[col] = Piece(back[col], false)
board[8 + col] = Piece(Kind.PAWN, false)
board[48 + col] = Piece(Kind.PAWN, true)
board[56 + col] = Piece(back[col], true)
}
}
fun move(fromRow: Int, fromCol: Int, toRow: Int, toCol: Int) {
check(status != Status.CHECKMATE && status != Status.STALEMATE)
require(listOf(fromRow, fromCol, toRow, toCol).all { it in 0..7 })
val from = fromRow * 8 + fromCol
val to = toRow * 8 + toCol
require(legal(from, to)) { "Illegal move" }
val piece = checkNotNull(board[from])
board[to] = if (piece.kind == Kind.PAWN && toRow in listOf(0, 7)) Piece(Kind.QUEEN, piece.white) else piece
board[from] = null
whiteToMove = !whiteToMove
val canMove = (0..63).any { start -> (0..63).any { end -> legal(start, end) } }
val checked = inCheck(whiteToMove)
status = when {
!canMove -> if (checked) Status.CHECKMATE else Status.STALEMATE
checked -> Status.CHECK
else -> Status.PLAYING
}
}
private fun legal(from: Int, to: Int): Boolean {
val piece = board[from] ?: return false
if (piece.white != whiteToMove || board[to]?.kind == Kind.KING || !canReach(from, to)) return false
val captured = board[to]
board[to] = piece
board[from] = null
val safe = !inCheck(piece.white)
board[from] = piece
board[to] = captured // Undo the trial move.
return safe
}
private fun inCheck(white: Boolean): Boolean {
val king = board.indexOfFirst { it?.kind == Kind.KING && it.white == white }
return board.indices.any { board[it]?.white == !white && canReach(it, king) }
}
private fun canReach(from: Int, to: Int): Boolean {
val piece = board[from] ?: return false
if (from == to || board[to]?.white == piece.white) return false
val row = to / 8 - from / 8
val col = to % 8 - from % 8
return when (piece.kind) {
Kind.ROOK -> (row == 0 || col == 0) && clearPath(from, to)
Kind.BISHOP -> abs(row) == abs(col) && clearPath(from, to)
Kind.QUEEN -> (row == 0 || col == 0 || abs(row) == abs(col)) && clearPath(from, to)
Kind.KNIGHT -> abs(row) * abs(col) == 2
Kind.KING -> maxOf(abs(row), abs(col)) == 1
Kind.PAWN -> {
val direction = if (piece.white) -1 else 1
val startRow = if (piece.white) 6 else 1
when {
col == 0 && board[to] == null -> row == direction ||
(from / 8 == startRow && row == 2 * direction && board[from + 8 * direction] == null)
else -> abs(col) == 1 && row == direction && board[to] != null
}
}
}
}
private fun clearPath(from: Int, to: Int): Boolean {
val step = (to / 8 - from / 8).sign * 8 + (to % 8 - from % 8).sign
var square = from + step
while (square != to) {
if (board[square] != null) return false
square += step
}
return true
}
}
package interview.chess
import kotlin.math.abs
import kotlin.math.sign
enum class Kind { ROOK, KNIGHT, BISHOP, QUEEN, KING, PAWN }
data class Piece(val kind: Kind, val white: Boolean)
enum class Status { PLAYING, CHECK, CHECKMATE, STALEMATE }
class Chess {
private val board = arrayOfNulls<Piece>(64)
var whiteToMove = true
private set
var status = Status.PLAYING
private set
init {
val back = listOf(Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK)
for (col in 0..7) {
board[col] = Piece(back[col], false)
board[8 + col] = Piece(Kind.PAWN, false)
board[48 + col] = Piece(Kind.PAWN, true)
board[56 + col] = Piece(back[col], true)
}
}
fun move(fromRow: Int, fromCol: Int, toRow: Int, toCol: Int) {
check(status != Status.CHECKMATE && status != Status.STALEMATE)
require(listOf(fromRow, fromCol, toRow, toCol).all { it in 0..7 })
val from = fromRow * 8 + fromCol
val to = toRow * 8 + toCol
require(legal(from, to)) { "Illegal move" }
val piece = checkNotNull(board[from])
board[to] = if (piece.kind == Kind.PAWN && toRow in listOf(0, 7)) Piece(Kind.QUEEN, piece.white) else piece
board[from] = null
whiteToMove = !whiteToMove
val canMove = (0..63).any { start -> (0..63).any { end -> legal(start, end) } }
val checked = inCheck(whiteToMove)
status = when {
!canMove -> if (checked) Status.CHECKMATE else Status.STALEMATE
checked -> Status.CHECK
else -> Status.PLAYING
}
}
private fun legal(from: Int, to: Int): Boolean {
val piece = board[from] ?: return false
if (piece.white != whiteToMove || board[to]?.kind == Kind.KING || !canReach(from, to)) return false
val captured = board[to]
board[to] = piece
board[from] = null
val safe = !inCheck(piece.white)
board[from] = piece
board[to] = captured
return safe
}
private fun inCheck(white: Boolean): Boolean {
val king = board.indexOfFirst { it?.kind == Kind.KING && it.white == white }
return board.indices.any { board[it]?.white == !white && canReach(it, king) }
}
private fun canReach(from: Int, to: Int): Boolean {
val piece = board[from] ?: return false
if (from == to || board[to]?.white == piece.white) return false
val row = to / 8 - from / 8
val col = to % 8 - from % 8
return when (piece.kind) {
Kind.ROOK -> (row == 0 || col == 0) && clearPath(from, to)
Kind.BISHOP -> abs(row) == abs(col) && clearPath(from, to)
Kind.QUEEN -> (row == 0 || col == 0 || abs(row) == abs(col)) && clearPath(from, to)
Kind.KNIGHT -> abs(row) * abs(col) == 2
Kind.KING -> maxOf(abs(row), abs(col)) == 1
Kind.PAWN -> {
val direction = if (piece.white) -1 else 1
val startRow = if (piece.white) 6 else 1
when {
col == 0 && board[to] == null -> row == direction ||
(from / 8 == startRow && row == 2 * direction && board[from + 8 * direction] == null)
else -> abs(col) == 1 && row == direction && board[to] != null
}
}
}
}
private fun clearPath(from: Int, to: Int): Boolean {
val step = (to / 8 - from / 8).sign * 8 + (to % 8 - from % 8).sign
var square = from + step
while (square != to) {
if (board[square] != null) return false
square += step
}
return true
}
}
Follow-up questions
Why is movement alone insufficient?
“A move must also leave the player's king safe.” A rook might be allowed to move sideways, but moving it could expose its king to an opposing rook on the same column. First check the piece's movement and path, then try the move and check the king.
Why restore a trial move?
“Asking whether a move is legal must not actually make that move.” Save the destination's old piece, try the move, check the king and restore both squares. This also matters when searching for any legal move to decide checkmate.
This fragment belongs inside legal, after its movement checks. piece is the moving piece. Only the public move method commits an accepted move.
Kotlin
val captured = board[to]
board[to] = piece
board[from] = null
val safe = !inCheck(piece.white)
board[from] = piece
board[to] = captured
return safeJava
Piece captured = board[to];
board[to] = piece;
board[from] = null;
boolean safe = !inCheck(piece.white());
board[from] = piece;
board[to] = captured;
return safe;Special moves?
“I would add the state each rule needs, rather than only adding another movement check.” Castling moves the king and a rook together. Track whether they have moved, require an empty path, and check that the king is not in check or crossing an attacked square. En passant is a special pawn capture available immediately after a neighboring pawn advances two squares. Track that previous move and remove the captured pawn during the trial too. Letting the player choose a promotion piece requires an input beyond the current automatic queen promotion.
What should I test?
“Illegal moves should leave the board and turn unchanged.” Check a blocked sliding piece, moving the wrong color, capturing a friendly piece and exposing one's own king. No legal moves while in check should mean checkmate. No legal moves without check should mean stalemate. Either result should reject later moves.
Extended implementation and optional features
This reference explores a larger scope. Use it after you can explain and write the interview version. Its extra types and features are not required for the scope above.
Java
com.androidinterview.chess.game.Game.java
package com.androidinterview.chess.game;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.GameStatus;
import com.androidinterview.chess.model.Move;
import com.androidinterview.chess.model.Position;
import com.androidinterview.chess.piece.Piece;
// The orchestrator, and the only class that knows whose turn it is. Every rule
// about the whole position rather than about one piece is here, and there are
// three. Move your own piece, the piece must offer the square, and the move
// must not leave your own king attacked.
public final class Game {
private final Board board;
private final Deque<Move> history = new ArrayDeque<>();
private Color turn = Color.WHITE;
private GameStatus status;
public Game() {
this(Board.standard());
}
public Game(Board board) {
this.board = board;
this.status = statusFor(turn);
}
public Board board() {
return board;
}
public Color turn() {
return turn;
}
public GameStatus status() {
return status;
}
// On checkmate the side to move is the mated side, so the winner is
// whoever just played. No separate winner field to keep in step.
public Color winner() {
return status == GameStatus.CHECKMATE ? turn.opponent() : null;
}
public List<Move> legalMoves() {
return legalMoves(turn);
}
// Generate, then filter. The filter is apply, test, revert. Play the move
// on the real board, ask whether the king is attacked, put the board back.
// No copy is made, because the move carries the piece that was taken.
private List<Move> legalMoves(Color color) {
List<Move> legal = new ArrayList<>();
for (Position from : board.squaresHolding(color)) {
Piece piece = board.at(from);
for (Position to : piece.targets(board, from)) {
Move move = new Move(from, to, piece, board.at(to));
apply(move);
boolean safe = !isInCheck(color);
revert(move);
if (safe) {
legal.add(move);
}
}
}
return legal;
}
// Validation happens here and nowhere else.
public Move makeMove(Position from, Position to) {
if (status.isOver()) {
throw new IllegalStateException("The game already ended as " + status);
}
Piece piece = board.at(from);
if (piece == null || piece.color() != turn) {
throw new IllegalArgumentException(from + " does not hold a " + turn + " piece");
}
Move move = legalMoves().stream()
.filter(candidate -> candidate.from().equals(from) && candidate.to().equals(to))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(from + " to " + to + " is not legal for " + turn));
apply(move);
history.push(move);
turn = turn.opponent();
status = statusFor(turn);
return move;
}
// A pop and a revert. The move holds its own inverse, so taking one back
// never replays the game from the start.
public Move undo() {
Move move = history.poll();
if (move == null) {
throw new IllegalStateException("There is nothing to undo");
}
revert(move);
turn = turn.opponent();
status = statusFor(turn);
return move;
}
// Attack detection, not move legality. Every enemy piece is asked for its
// squares and the king's square is looked for among them. A push needs an
// empty square, so a pawn in front of a king does not check it.
public boolean isInCheck(Color color) {
Position king = board.kingSquare(color);
if (king == null) {
return false;
}
for (Position from : board.squaresHolding(color.opponent())) {
if (board.at(from).targets(board, from).contains(king)) {
return true;
}
}
return false;
}
// Checkmate and stalemate are the same sentence with one word changed. No
// legal move and attacked is mate, no legal move and safe is a draw. That
// costs a few thousand board reads, free at human speed.
private GameStatus statusFor(Color color) {
boolean check = isInCheck(color);
if (!legalMoves(color).isEmpty()) {
return check ? GameStatus.CHECK : GameStatus.IN_PROGRESS;
}
return check ? GameStatus.CHECKMATE : GameStatus.STALEMATE;
}
private void apply(Move move) {
board.set(move.from(), null);
board.set(move.to(), move.piece());
}
private void revert(Move move) {
board.set(move.from(), move.piece());
board.set(move.to(), move.captured());
}
}
package com.androidinterview.chess.game;
import java.util.ArrayDeque;
import java.util.ArrayList;
import java.util.Deque;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.GameStatus;
import com.androidinterview.chess.model.Move;
import com.androidinterview.chess.model.Position;
import com.androidinterview.chess.piece.Piece;
public final class Game {
private final Board board;
private final Deque<Move> history = new ArrayDeque<>();
private Color turn = Color.WHITE;
private GameStatus status;
public Game() {
this(Board.standard());
}
public Game(Board board) {
this.board = board;
this.status = statusFor(turn);
}
public Board board() {
return board;
}
public Color turn() {
return turn;
}
public GameStatus status() {
return status;
}
public Color winner() {
return status == GameStatus.CHECKMATE ? turn.opponent() : null;
}
public List<Move> legalMoves() {
return legalMoves(turn);
}
private List<Move> legalMoves(Color color) {
List<Move> legal = new ArrayList<>();
for (Position from : board.squaresHolding(color)) {
Piece piece = board.at(from);
for (Position to : piece.targets(board, from)) {
Move move = new Move(from, to, piece, board.at(to));
apply(move);
boolean safe = !isInCheck(color);
revert(move);
if (safe) {
legal.add(move);
}
}
}
return legal;
}
public Move makeMove(Position from, Position to) {
if (status.isOver()) {
throw new IllegalStateException("The game already ended as " + status);
}
Piece piece = board.at(from);
if (piece == null || piece.color() != turn) {
throw new IllegalArgumentException(from + " does not hold a " + turn + " piece");
}
Move move = legalMoves().stream()
.filter(candidate -> candidate.from().equals(from) && candidate.to().equals(to))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException(from + " to " + to + " is not legal for " + turn));
apply(move);
history.push(move);
turn = turn.opponent();
status = statusFor(turn);
return move;
}
public Move undo() {
Move move = history.poll();
if (move == null) {
throw new IllegalStateException("There is nothing to undo");
}
revert(move);
turn = turn.opponent();
status = statusFor(turn);
return move;
}
public boolean isInCheck(Color color) {
Position king = board.kingSquare(color);
if (king == null) {
return false;
}
for (Position from : board.squaresHolding(color.opponent())) {
if (board.at(from).targets(board, from).contains(king)) {
return true;
}
}
return false;
}
private GameStatus statusFor(Color color) {
boolean check = isInCheck(color);
if (!legalMoves(color).isEmpty()) {
return check ? GameStatus.CHECK : GameStatus.IN_PROGRESS;
}
return check ? GameStatus.CHECKMATE : GameStatus.STALEMATE;
}
private void apply(Move move) {
board.set(move.from(), null);
board.set(move.to(), move.piece());
}
private void revert(Move move) {
board.set(move.from(), move.piece());
board.set(move.to(), move.captured());
}
}
com.androidinterview.chess.model.Board.java
package com.androidinterview.chess.model;
import java.util.ArrayList;
import java.util.List;
import com.androidinterview.chess.piece.Bishop;
import com.androidinterview.chess.piece.King;
import com.androidinterview.chess.piece.Knight;
import com.androidinterview.chess.piece.Pawn;
import com.androidinterview.chess.piece.Piece;
import com.androidinterview.chess.piece.Queen;
import com.androidinterview.chess.piece.Rook;
// Storage and geometry, nothing else. It says what is standing on a square and
// it puts something there. It cannot tell you whether a move is legal, whose
// turn it is, or whether anybody is in check. The instant one rule lands here,
// every rule wants to live here too.
public final class Board {
public static final int SIZE = 8;
private final Piece[][] squares = new Piece[SIZE][SIZE];
// The opening position. Both colours use the same back rank order,
// because the layout is a mirror and not two arrangements.
public static Board standard() {
Board board = new Board();
for (Color color : Color.values()) {
int homeRow = color == Color.WHITE ? 0 : SIZE - 1;
Piece[] backRank = {
new Rook(color), new Knight(color), new Bishop(color), new Queen(color),
new King(color), new Bishop(color), new Knight(color), new Rook(color),
};
for (int col = 0; col < SIZE; col++) {
board.set(new Position(homeRow, col), backRank[col]);
board.set(new Position(color.pawnStartRow(), col), new Pawn(color));
}
}
return board;
}
public boolean contains(Position at) {
return at.row() >= 0 && at.row() < SIZE && at.col() >= 0 && at.col() < SIZE;
}
public Piece at(Position at) {
return squares[at.row()][at.col()];
}
// Passing null clears the square, which is how undo puts a piece back.
public void set(Position at, Piece piece) {
squares[at.row()][at.col()] = piece;
}
public List<Position> squaresHolding(Color color) {
List<Position> found = new ArrayList<>();
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
Piece piece = squares[row][col];
if (piece != null && piece.color() == color) {
found.add(new Position(row, col));
}
}
}
return found;
}
// Scanned rather than cached. A cached square is a second source of truth
// that every move and every undo has to keep in step.
public Position kingSquare(Color color) {
for (Position at : squaresHolding(color)) {
if (at(at) instanceof King) {
return at;
}
}
return null;
}
@Override
public String toString() {
StringBuilder out = new StringBuilder();
for (int row = SIZE - 1; row >= 0; row--) {
for (int col = 0; col < SIZE; col++) {
Piece piece = squares[row][col];
out.append(piece == null ? "." : piece.toString()).append(' ');
}
out.append('\n');
}
return out.toString();
}
}
package com.androidinterview.chess.model;
import java.util.ArrayList;
import java.util.List;
import com.androidinterview.chess.piece.Bishop;
import com.androidinterview.chess.piece.King;
import com.androidinterview.chess.piece.Knight;
import com.androidinterview.chess.piece.Pawn;
import com.androidinterview.chess.piece.Piece;
import com.androidinterview.chess.piece.Queen;
import com.androidinterview.chess.piece.Rook;
public final class Board {
public static final int SIZE = 8;
private final Piece[][] squares = new Piece[SIZE][SIZE];
public static Board standard() {
Board board = new Board();
for (Color color : Color.values()) {
int homeRow = color == Color.WHITE ? 0 : SIZE - 1;
Piece[] backRank = {
new Rook(color), new Knight(color), new Bishop(color), new Queen(color),
new King(color), new Bishop(color), new Knight(color), new Rook(color),
};
for (int col = 0; col < SIZE; col++) {
board.set(new Position(homeRow, col), backRank[col]);
board.set(new Position(color.pawnStartRow(), col), new Pawn(color));
}
}
return board;
}
public boolean contains(Position at) {
return at.row() >= 0 && at.row() < SIZE && at.col() >= 0 && at.col() < SIZE;
}
public Piece at(Position at) {
return squares[at.row()][at.col()];
}
public void set(Position at, Piece piece) {
squares[at.row()][at.col()] = piece;
}
public List<Position> squaresHolding(Color color) {
List<Position> found = new ArrayList<>();
for (int row = 0; row < SIZE; row++) {
for (int col = 0; col < SIZE; col++) {
Piece piece = squares[row][col];
if (piece != null && piece.color() == color) {
found.add(new Position(row, col));
}
}
}
return found;
}
public Position kingSquare(Color color) {
for (Position at : squaresHolding(color)) {
if (at(at) instanceof King) {
return at;
}
}
return null;
}
@Override
public String toString() {
StringBuilder out = new StringBuilder();
for (int row = SIZE - 1; row >= 0; row--) {
for (int col = 0; col < SIZE; col++) {
Piece piece = squares[row][col];
out.append(piece == null ? "." : piece.toString()).append(' ');
}
out.append('\n');
}
return out.toString();
}
}
com.androidinterview.chess.model.Color.java
package com.androidinterview.chess.model;
// The only place a per colour number lives. Five of the six pieces move the
// same way whichever side owns them, so colour is just a tag. The pawn walks
// one way only, so its direction and its start row hang off the colour rather
// than off if statements inside Pawn.
public enum Color {
WHITE(1, 1),
BLACK(-1, 6);
private final int pawnDirection;
private final int pawnStartRow;
Color(int pawnDirection, int pawnStartRow) {
this.pawnDirection = pawnDirection;
this.pawnStartRow = pawnStartRow;
}
// Plus one for white, minus one for black, in board rows.
public int pawnDirection() {
return pawnDirection;
}
public int pawnStartRow() {
return pawnStartRow;
}
public Color opponent() {
return this == WHITE ? BLACK : WHITE;
}
}
package com.androidinterview.chess.model;
public enum Color {
WHITE(1, 1),
BLACK(-1, 6);
private final int pawnDirection;
private final int pawnStartRow;
Color(int pawnDirection, int pawnStartRow) {
this.pawnDirection = pawnDirection;
this.pawnStartRow = pawnStartRow;
}
public int pawnDirection() {
return pawnDirection;
}
public int pawnStartRow() {
return pawnStartRow;
}
public Color opponent() {
return this == WHITE ? BLACK : WHITE;
}
}
com.androidinterview.chess.model.GameStatus.java
package com.androidinterview.chess.model;
// One enum rather than a handful of booleans, which would let you write down a
// game that is checkmate and still in progress. All four values are derived
// after every move from one question, does the side to move have a legal move
// at all, so nothing here can drift out of step with the board.
public enum GameStatus {
IN_PROGRESS,
// Attacked with somewhere to go, attacked with nowhere to go, and safe
// with nowhere to go, which is a draw.
CHECK,
CHECKMATE,
STALEMATE;
public boolean isOver() {
return this == CHECKMATE || this == STALEMATE;
}
}
package com.androidinterview.chess.model;
public enum GameStatus {
IN_PROGRESS,
CHECK,
CHECKMATE,
STALEMATE;
public boolean isOver() {
return this == CHECKMATE || this == STALEMATE;
}
}
com.androidinterview.chess.model.Move.java
package com.androidinterview.chess.model;
import com.androidinterview.chess.piece.Piece;
// Everything needed to play a move forwards and everything needed to play it
// backwards, in one value. The captured piece is the field that makes undo a
// pop rather than a replay from the opening position. It is null on a quiet
// move, and that means exactly one thing, this move took nothing.
public record Move(Position from, Position to, Piece piece, Piece captured) {
public boolean isCapture() {
return captured != null;
}
@Override
public String toString() {
return piece + " " + from + (isCapture() ? "x" : "-") + to;
}
}
package com.androidinterview.chess.model;
import com.androidinterview.chess.piece.Piece;
public record Move(Position from, Position to, Piece piece, Piece captured) {
public boolean isCapture() {
return captured != null;
}
@Override
public String toString() {
return piece + " " + from + (isCapture() ? "x" : "-") + to;
}
}
com.androidinterview.chess.model.Position.java
package com.androidinterview.chess.model;
// A square. Row zero is white's back rank, so a positive pawn direction walks
// up the board. Two loose ints would be two chances to pass them the wrong way
// round, and every piece adds an offset to a square, so that lives here.
public record Position(int row, int col) {
public Position plus(int rowStep, int colStep) {
return new Position(row + rowStep, col + colStep);
}
@Override
public String toString() {
return String.valueOf((char) ('a' + col)) + (row + 1);
}
}
package com.androidinterview.chess.model;
public record Position(int row, int col) {
public Position plus(int rowStep, int colStep) {
return new Position(row + rowStep, col + colStep);
}
@Override
public String toString() {
return String.valueOf((char) ('a' + col)) + (row + 1);
}
}
com.androidinterview.chess.piece.Bishop.java
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
// The rook with a different table.
public final class Bishop extends Piece {
public Bishop(Color color) {
super(color, 'B');
}
@Override
public List<Position> targets(Board board, Position from) {
return slide(board, from, DIAGONAL);
}
}
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
public final class Bishop extends Piece {
public Bishop(Color color) {
super(color, 'B');
}
@Override
public List<Position> targets(Board board, Position from) {
return slide(board, from, DIAGONAL);
}
}
com.androidinterview.chess.piece.King.java
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
// The queen's directions walked exactly once. Not stepping into an attacked
// square, and castling, are rules about the whole position, so the game owns
// them and the king does not.
public final class King extends Piece {
public King(Color color) {
super(color, 'K');
}
@Override
public List<Position> targets(Board board, Position from) {
return step(board, from, ALL_EIGHT);
}
}
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
public final class King extends Piece {
public King(Color color) {
super(color, 'K');
}
@Override
public List<Position> targets(Board board, Position from) {
return step(board, from, ALL_EIGHT);
}
}
com.androidinterview.chess.piece.Knight.java
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
// The only piece that jumps, and here that costs nothing. Jumping is the walk
// with no loop, so a knight never asks what it passes over.
public final class Knight extends Piece {
private static final int[][] LEAPS = {
{ 2, 1 }, { 2, -1 }, { -2, 1 }, { -2, -1 },
{ 1, 2 }, { 1, -2 }, { -1, 2 }, { -1, -2 },
};
public Knight(Color color) {
super(color, 'N');
}
@Override
public List<Position> targets(Board board, Position from) {
return step(board, from, LEAPS);
}
}
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
public final class Knight extends Piece {
private static final int[][] LEAPS = {
{ 2, 1 }, { 2, -1 }, { -2, 1 }, { -2, -1 },
{ 1, 2 }, { 1, -2 }, { -1, 2 }, { -1, -2 },
};
public Knight(Color color) {
super(color, 'N');
}
@Override
public List<Position> targets(Board board, Position from) {
return step(board, from, LEAPS);
}
}
com.androidinterview.chess.piece.Pawn.java
package com.androidinterview.chess.piece;
import java.util.ArrayList;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
// The awkward one, and the only piece that needed its own method body. It
// moves one way and captures another, so neither helper fits. Every per colour
// number comes off the colour, so there is no branch on white or black here.
public final class Pawn extends Piece {
public Pawn(Color color) {
super(color, 'P');
}
@Override
public List<Position> targets(Board board, Position from) {
List<Position> found = new ArrayList<>();
int forward = color().pawnDirection();
// A push is only a move onto an empty square and is never a capture,
// which is why a pawn in front of a king does not check it.
Position ahead = from.plus(forward, 0);
if (board.contains(ahead) && board.at(ahead) == null) {
found.add(ahead);
Position twoAhead = ahead.plus(forward, 0);
if (from.row() == color().pawnStartRow() && board.at(twoAhead) == null) {
found.add(twoAhead);
}
}
// A diagonal is only a move when an enemy is standing there. En
// passant is the exception, and it is a follow up.
for (int side : new int[] { -1, 1 }) {
Position diagonal = from.plus(forward, side);
if (board.contains(diagonal)) {
Piece occupant = board.at(diagonal);
if (occupant != null && occupant.color() != color()) {
found.add(diagonal);
}
}
}
return found;
}
}
package com.androidinterview.chess.piece;
import java.util.ArrayList;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
public final class Pawn extends Piece {
public Pawn(Color color) {
super(color, 'P');
}
@Override
public List<Position> targets(Board board, Position from) {
List<Position> found = new ArrayList<>();
int forward = color().pawnDirection();
Position ahead = from.plus(forward, 0);
if (board.contains(ahead) && board.at(ahead) == null) {
found.add(ahead);
Position twoAhead = ahead.plus(forward, 0);
if (from.row() == color().pawnStartRow() && board.at(twoAhead) == null) {
found.add(twoAhead);
}
}
for (int side : new int[] { -1, 1 }) {
Position diagonal = from.plus(forward, side);
if (board.contains(diagonal)) {
Piece occupant = board.at(diagonal);
if (occupant != null && occupant.color() != color()) {
found.add(diagonal);
}
}
}
return found;
}
}
com.androidinterview.chess.piece.Piece.java
package com.androidinterview.chess.piece;
import java.util.ArrayList;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
// A piece answers one question, which squares can I reach from here on this
// board. It does not know whose turn it is and it does not know whether the
// move would expose its own king. The rule lives on the piece rather than in a
// strategy because a piece type IS its movement rule, so there is nothing to
// swap at runtime.
public abstract class Piece {
protected static final int[][] STRAIGHT = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
protected static final int[][] DIAGONAL = { { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 } };
// The queen slides along these and the king takes one step along them.
protected static final int[][] ALL_EIGHT = {
{ 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 },
{ 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 },
};
private final Color color;
private final char letter;
protected Piece(Color color, char letter) {
this.color = color;
this.letter = letter;
}
public Color color() {
return color;
}
// Pseudo legal, every square this piece could land on if the king did not
// exist. The game narrows that down to the legal set.
public abstract List<Position> targets(Board board, Position from);
// The ray walker, and the reason rook, bishop and queen are five lines
// each. An empty square is a move, an enemy is a move and then a stop, a
// friend is a stop with no move.
protected final List<Position> slide(Board board, Position from, int[][] directions) {
List<Position> found = new ArrayList<>();
for (int[] step : directions) {
Position at = from.plus(step[0], step[1]);
while (board.contains(at)) {
Piece occupant = board.at(at);
if (occupant != null) {
if (occupant.color != color) {
found.add(at);
}
break;
}
found.add(at);
at = at.plus(step[0], step[1]);
}
}
return found;
}
// The same walk with the loop taken out, all a king or a knight needs.
protected final List<Position> step(Board board, Position from, int[][] offsets) {
List<Position> found = new ArrayList<>();
for (int[] offset : offsets) {
Position at = from.plus(offset[0], offset[1]);
if (board.contains(at)) {
Piece occupant = board.at(at);
if (occupant == null || occupant.color != color) {
found.add(at);
}
}
}
return found;
}
// Upper case for white, lower case for black.
@Override
public String toString() {
return String.valueOf(color == Color.WHITE ? Character.toUpperCase(letter) : Character.toLowerCase(letter));
}
}
package com.androidinterview.chess.piece;
import java.util.ArrayList;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
public abstract class Piece {
protected static final int[][] STRAIGHT = { { 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 } };
protected static final int[][] DIAGONAL = { { 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 } };
protected static final int[][] ALL_EIGHT = {
{ 1, 0 }, { -1, 0 }, { 0, 1 }, { 0, -1 },
{ 1, 1 }, { 1, -1 }, { -1, 1 }, { -1, -1 },
};
private final Color color;
private final char letter;
protected Piece(Color color, char letter) {
this.color = color;
this.letter = letter;
}
public Color color() {
return color;
}
public abstract List<Position> targets(Board board, Position from);
protected final List<Position> slide(Board board, Position from, int[][] directions) {
List<Position> found = new ArrayList<>();
for (int[] step : directions) {
Position at = from.plus(step[0], step[1]);
while (board.contains(at)) {
Piece occupant = board.at(at);
if (occupant != null) {
if (occupant.color != color) {
found.add(at);
}
break;
}
found.add(at);
at = at.plus(step[0], step[1]);
}
}
return found;
}
protected final List<Position> step(Board board, Position from, int[][] offsets) {
List<Position> found = new ArrayList<>();
for (int[] offset : offsets) {
Position at = from.plus(offset[0], offset[1]);
if (board.contains(at)) {
Piece occupant = board.at(at);
if (occupant == null || occupant.color != color) {
found.add(at);
}
}
}
return found;
}
@Override
public String toString() {
return String.valueOf(color == Color.WHITE ? Character.toUpperCase(letter) : Character.toLowerCase(letter));
}
}
com.androidinterview.chess.piece.Queen.java
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
// A rook and a bishop at once, which here is one table rather than multiple
// inheritance.
public final class Queen extends Piece {
public Queen(Color color) {
super(color, 'Q');
}
@Override
public List<Position> targets(Board board, Position from) {
return slide(board, from, ALL_EIGHT);
}
}
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
public final class Queen extends Piece {
public Queen(Color color) {
super(color, 'Q');
}
@Override
public List<Position> targets(Board board, Position from) {
return slide(board, from, ALL_EIGHT);
}
}
com.androidinterview.chess.piece.Rook.java
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
// Slides along ranks and files. The walking is in the base class, so this
// piece contributes a direction table.
public final class Rook extends Piece {
public Rook(Color color) {
super(color, 'R');
}
@Override
public List<Position> targets(Board board, Position from) {
return slide(board, from, STRAIGHT);
}
}
package com.androidinterview.chess.piece;
import java.util.List;
import com.androidinterview.chess.model.Board;
import com.androidinterview.chess.model.Color;
import com.androidinterview.chess.model.Position;
public final class Rook extends Piece {
public Rook(Color color) {
super(color, 'R');
}
@Override
public List<Position> targets(Board board, Position from) {
return slide(board, from, STRAIGHT);
}
}
Kotlin
com.androidinterview.chess.game.Game.kt
package com.androidinterview.chess.game
import com.androidinterview.chess.model.Board
import com.androidinterview.chess.model.Color
import com.androidinterview.chess.model.Move
import com.androidinterview.chess.model.Position
// A sealed hierarchy instead of an enum plus a nullable winner. Checkmate
// carries the side that won and Check carries the side that is attacked, so a
// finished game with no winner cannot be written down and a when over the
// status is exhaustive.
sealed interface GameStatus {
data object InProgress : GameStatus
data class Check(val color: Color) : GameStatus
data class Checkmate(val winner: Color) : GameStatus
data object Stalemate : GameStatus
val isOver: Boolean get() = this is Checkmate || this is Stalemate
}
// The orchestrator, and the only class that knows whose turn it is. Every rule
// about the whole position rather than about one piece is here, and there are
// three. Move your own piece, the piece must offer the square, and the move
// must not leave your own king attacked.
class Game(val board: Board = Board.standard()) {
private val history = ArrayDeque<Move>()
var turn: Color = Color.WHITE
private set
var status: GameStatus = statusFor(turn)
private set
// Generate, then filter. The filter is apply, test, revert. Play the move
// on the real board, ask whether the king is attacked, put the board back.
// No copy is made, because the move carries the piece that was taken.
fun legalMoves(color: Color = turn): List<Move> =
board.squaresHolding(color)
.flatMap { from ->
val piece = board[from]!!
piece.targets(board, from).map { to -> Move(from, to, piece, board[to]) }
}
.filter { move ->
apply(move)
val safe = !isInCheck(color)
revert(move)
safe
}
// Validation happens here and nowhere else.
fun move(from: Position, to: Position): Move {
check(!status.isOver) { "The game already ended as $status" }
val piece = board[from]
require(piece != null && piece.color == turn) { "$from does not hold a $turn piece" }
val move = requireNotNull(legalMoves().find { it.from == from && it.to == to }) {
"$from to $to is not legal for $turn"
}
apply(move)
history.addLast(move)
advance()
return move
}
// A pop and a revert. The move holds its own inverse, so taking one back
// never replays the game from the start.
fun undo(): Move {
val move = checkNotNull(history.removeLastOrNull()) { "There is nothing to undo" }
revert(move)
advance()
return move
}
// Attack detection, not move legality. Every enemy piece is asked for its
// squares and the king's square is looked for among them. A push needs an
// empty square, so a pawn in front of a king does not check it.
fun isInCheck(color: Color): Boolean {
val king = board.kingSquare(color) ?: return false
return board.squaresHolding(color.opponent).any { king in board[it]!!.targets(board, it) }
}
private fun advance() {
turn = turn.opponent
status = statusFor(turn)
}
// Checkmate and stalemate are the same sentence with one word changed. No
// legal move and attacked is mate, no legal move and safe is a draw. That
// costs a few thousand board reads, free at human speed.
private fun statusFor(color: Color): GameStatus = when {
legalMoves(color).isNotEmpty() ->
if (isInCheck(color)) GameStatus.Check(color) else GameStatus.InProgress
isInCheck(color) -> GameStatus.Checkmate(color.opponent)
else -> GameStatus.Stalemate
}
private fun apply(move: Move) {
board[move.from] = null
board[move.to] = move.piece
}
private fun revert(move: Move) {
board[move.from] = move.piece
board[move.to] = move.captured
}
}
package com.androidinterview.chess.game
import com.androidinterview.chess.model.Board
import com.androidinterview.chess.model.Color
import com.androidinterview.chess.model.Move
import com.androidinterview.chess.model.Position
sealed interface GameStatus {
data object InProgress : GameStatus
data class Check(val color: Color) : GameStatus
data class Checkmate(val winner: Color) : GameStatus
data object Stalemate : GameStatus
val isOver: Boolean get() = this is Checkmate || this is Stalemate
}
class Game(val board: Board = Board.standard()) {
private val history = ArrayDeque<Move>()
var turn: Color = Color.WHITE
private set
var status: GameStatus = statusFor(turn)
private set
fun legalMoves(color: Color = turn): List<Move> =
board.squaresHolding(color)
.flatMap { from ->
val piece = board[from]!!
piece.targets(board, from).map { to -> Move(from, to, piece, board[to]) }
}
.filter { move ->
apply(move)
val safe = !isInCheck(color)
revert(move)
safe
}
fun move(from: Position, to: Position): Move {
check(!status.isOver) { "The game already ended as $status" }
val piece = board[from]
require(piece != null && piece.color == turn) { "$from does not hold a $turn piece" }
val move = requireNotNull(legalMoves().find { it.from == from && it.to == to }) {
"$from to $to is not legal for $turn"
}
apply(move)
history.addLast(move)
advance()
return move
}
fun undo(): Move {
val move = checkNotNull(history.removeLastOrNull()) { "There is nothing to undo" }
revert(move)
advance()
return move
}
fun isInCheck(color: Color): Boolean {
val king = board.kingSquare(color) ?: return false
return board.squaresHolding(color.opponent).any { king in board[it]!!.targets(board, it) }
}
private fun advance() {
turn = turn.opponent
status = statusFor(turn)
}
private fun statusFor(color: Color): GameStatus = when {
legalMoves(color).isNotEmpty() ->
if (isInCheck(color)) GameStatus.Check(color) else GameStatus.InProgress
isInCheck(color) -> GameStatus.Checkmate(color.opponent)
else -> GameStatus.Stalemate
}
private fun apply(move: Move) {
board[move.from] = null
board[move.to] = move.piece
}
private fun revert(move: Move) {
board[move.from] = move.piece
board[move.to] = move.captured
}
}
com.androidinterview.chess.model.Board.kt
package com.androidinterview.chess.model
import com.androidinterview.chess.piece.Bishop
import com.androidinterview.chess.piece.King
import com.androidinterview.chess.piece.Knight
import com.androidinterview.chess.piece.Pawn
import com.androidinterview.chess.piece.Piece
import com.androidinterview.chess.piece.Queen
import com.androidinterview.chess.piece.Rook
// The only place a per colour number lives. Five of the six pieces move the
// same way whichever side owns them, so colour is just a tag. The pawn walks
// one way only, so its direction and its start row hang off the colour.
enum class Color(val pawnDirection: Int, val pawnStartRow: Int) {
WHITE(1, 1),
BLACK(-1, 6);
val opponent: Color get() = if (this == WHITE) BLACK else WHITE
}
// A step, in rows and columns. Every direction table in the game is a list of
// these, so the offset arithmetic is one operator and not six copies.
typealias Step = Pair<Int, Int>
// Row zero is white's back rank, so a positive pawn direction walks up the
// board and the algebraic name falls out of the two numbers.
data class Position(val row: Int, val col: Int) {
operator fun plus(step: Step) = Position(row + step.first, col + step.second)
override fun toString() = "${'a' + col}${row + 1}"
}
// Everything needed to play a move forwards and everything needed to play it
// backwards. The captured piece is the field that turns undo into a pop, and
// it is null on a quiet move.
data class Move(val from: Position, val to: Position, val piece: Piece, val captured: Piece? = null) {
override fun toString() = "$piece $from${if (captured == null) "-" else "x"}$to"
}
// Storage and geometry, nothing else. It says what stands on a square and puts
// something there. It cannot tell you whether a move is legal, whose turn it
// is, or whether anybody is in check. The instant one rule lands here, every
// rule wants to live here too.
class Board {
private val squares = Array(SIZE) { arrayOfNulls<Piece>(SIZE) }
operator fun contains(at: Position) = at.row in 0 until SIZE && at.col in 0 until SIZE
operator fun get(at: Position): Piece? = squares[at.row][at.col]
// Setting null clears the square, which is how undo puts a piece back.
operator fun set(at: Position, piece: Piece?) {
squares[at.row][at.col] = piece
}
fun squaresHolding(color: Color) = ALL_SQUARES.filter { this[it]?.color == color }
// Scanned rather than cached. A cached square is a second source of truth
// that every move and every undo has to keep in step.
fun kingSquare(color: Color) = squaresHolding(color).firstOrNull { this[it] is King }
override fun toString() = (SIZE - 1 downTo 0).joinToString("\n") { row ->
(0 until SIZE).joinToString(" ") { col -> squares[row][col]?.toString() ?: "." }
}
companion object {
const val SIZE = 8
val ALL_SQUARES = (0 until SIZE).flatMap { row -> (0 until SIZE).map { Position(row, it) } }
// The opening position. Both colours use the same back rank order,
// because the layout is a mirror and not two arrangements. Holding
// constructors as function values is why this is one loop.
private val BACK_RANK: List<(Color) -> Piece> =
listOf(::Rook, ::Knight, ::Bishop, ::Queen, ::King, ::Bishop, ::Knight, ::Rook)
fun standard() = Board().apply {
for (color in Color.entries) {
val homeRow = if (color == Color.WHITE) 0 else SIZE - 1
BACK_RANK.forEachIndexed { col, make ->
this[Position(homeRow, col)] = make(color)
this[Position(color.pawnStartRow, col)] = Pawn(color)
}
}
}
}
}
package com.androidinterview.chess.model
import com.androidinterview.chess.piece.Bishop
import com.androidinterview.chess.piece.King
import com.androidinterview.chess.piece.Knight
import com.androidinterview.chess.piece.Pawn
import com.androidinterview.chess.piece.Piece
import com.androidinterview.chess.piece.Queen
import com.androidinterview.chess.piece.Rook
enum class Color(val pawnDirection: Int, val pawnStartRow: Int) {
WHITE(1, 1),
BLACK(-1, 6);
val opponent: Color get() = if (this == WHITE) BLACK else WHITE
}
typealias Step = Pair<Int, Int>
data class Position(val row: Int, val col: Int) {
operator fun plus(step: Step) = Position(row + step.first, col + step.second)
override fun toString() = "${'a' + col}${row + 1}"
}
data class Move(val from: Position, val to: Position, val piece: Piece, val captured: Piece? = null) {
override fun toString() = "$piece $from${if (captured == null) "-" else "x"}$to"
}
class Board {
private val squares = Array(SIZE) { arrayOfNulls<Piece>(SIZE) }
operator fun contains(at: Position) = at.row in 0 until SIZE && at.col in 0 until SIZE
operator fun get(at: Position): Piece? = squares[at.row][at.col]
operator fun set(at: Position, piece: Piece?) {
squares[at.row][at.col] = piece
}
fun squaresHolding(color: Color) = ALL_SQUARES.filter { this[it]?.color == color }
fun kingSquare(color: Color) = squaresHolding(color).firstOrNull { this[it] is King }
override fun toString() = (SIZE - 1 downTo 0).joinToString("\n") { row ->
(0 until SIZE).joinToString(" ") { col -> squares[row][col]?.toString() ?: "." }
}
companion object {
const val SIZE = 8
val ALL_SQUARES = (0 until SIZE).flatMap { row -> (0 until SIZE).map { Position(row, it) } }
private val BACK_RANK: List<(Color) -> Piece> =
listOf(::Rook, ::Knight, ::Bishop, ::Queen, ::King, ::Bishop, ::Knight, ::Rook)
fun standard() = Board().apply {
for (color in Color.entries) {
val homeRow = if (color == Color.WHITE) 0 else SIZE - 1
BACK_RANK.forEachIndexed { col, make ->
this[Position(homeRow, col)] = make(color)
this[Position(color.pawnStartRow, col)] = Pawn(color)
}
}
}
}
}
com.androidinterview.chess.piece.Piece.kt
package com.androidinterview.chess.piece
import com.androidinterview.chess.model.Board
import com.androidinterview.chess.model.Color
import com.androidinterview.chess.model.Position
import com.androidinterview.chess.model.Step
private val STRAIGHT: List<Step> = listOf(1 to 0, -1 to 0, 0 to 1, 0 to -1)
private val DIAGONAL: List<Step> = listOf(1 to 1, 1 to -1, -1 to 1, -1 to -1)
// The queen slides along these and the king takes one step along them.
private val ALL_EIGHT: List<Step> = STRAIGHT + DIAGONAL
private val LEAPS: List<Step> =
listOf(2 to 1, 2 to -1, -2 to 1, -2 to -1, 1 to 2, 1 to -2, -1 to 2, -1 to -2)
// A piece answers one question, which squares can I reach from here on this
// board. It does not know whose turn it is and it does not know whether the
// move would expose its own king. The rule lives on the piece rather than in a
// strategy because a piece type IS its movement rule, so there is nothing to
// swap at runtime.
//
// Sealed rather than open, so a when over the six types is exhaustive and a
// seventh piece is a compile error everywhere that has to care.
sealed class Piece(val color: Color, private val letter: Char) {
// Pseudo legal, every square this piece could land on if the king did not
// exist. The game narrows that down to the legal set.
abstract fun targets(board: Board, from: Position): List<Position>
// The ray walker, and the reason rook, bishop and queen are one line each.
// An empty square is a move, an enemy is a move and then a stop, a friend
// is a stop with no move.
protected fun slide(board: Board, from: Position, directions: List<Step>): List<Position> =
directions.flatMap { step ->
buildList {
var at = from + step
while (at in board) {
val occupant = board[at]
if (occupant != null) {
if (occupant.color != color) add(at)
break
}
add(at)
at += step
}
}
}
// The same walk with the loop taken out, all a king or a knight needs.
protected fun step(board: Board, from: Position, offsets: List<Step>): List<Position> =
offsets.map { from + it }.filter { it in board && board[it]?.color != color }
// Upper case for white, lower case for black.
override fun toString() =
(if (color == Color.WHITE) letter.uppercaseChar() else letter.lowercaseChar()).toString()
}
class Rook(color: Color) : Piece(color, 'R') {
override fun targets(board: Board, from: Position) = slide(board, from, STRAIGHT)
}
class Bishop(color: Color) : Piece(color, 'B') {
override fun targets(board: Board, from: Position) = slide(board, from, DIAGONAL)
}
class Queen(color: Color) : Piece(color, 'Q') {
override fun targets(board: Board, from: Position) = slide(board, from, ALL_EIGHT)
}
// Not stepping into an attacked square, and castling, are rules about the whole
// position, so the game owns them and the king does not.
class King(color: Color) : Piece(color, 'K') {
override fun targets(board: Board, from: Position) = step(board, from, ALL_EIGHT)
}
// The only piece that jumps, and here that costs nothing. Jumping is the walk
// with no loop, so a knight never asks what it passes over.
class Knight(color: Color) : Piece(color, 'N') {
override fun targets(board: Board, from: Position) = step(board, from, LEAPS)
}
// The awkward one, and the only piece that needed a body. It moves one way and
// captures another, so neither helper fits. Every per colour number comes off
// the colour, so there is no branch on white or black here.
class Pawn(color: Color) : Piece(color, 'P') {
override fun targets(board: Board, from: Position): List<Position> {
val forward = color.pawnDirection
val ahead = from + (forward to 0)
// A push is only a move onto an empty square and is never a capture,
// which is why a pawn in front of a king does not check it.
val pushes = buildList {
if (ahead in board && board[ahead] == null) {
add(ahead)
val twoAhead = ahead + (forward to 0)
if (from.row == color.pawnStartRow && board[twoAhead] == null) add(twoAhead)
}
}
// A diagonal is only a move when an enemy is standing there. En
// passant is the exception, and it is a follow up.
val captures = listOf(forward to -1, forward to 1)
.map { from + it }
.filter { it in board && board[it]?.color == color.opponent }
return pushes + captures
}
}
package com.androidinterview.chess.piece
import com.androidinterview.chess.model.Board
import com.androidinterview.chess.model.Color
import com.androidinterview.chess.model.Position
import com.androidinterview.chess.model.Step
private val STRAIGHT: List<Step> = listOf(1 to 0, -1 to 0, 0 to 1, 0 to -1)
private val DIAGONAL: List<Step> = listOf(1 to 1, 1 to -1, -1 to 1, -1 to -1)
private val ALL_EIGHT: List<Step> = STRAIGHT + DIAGONAL
private val LEAPS: List<Step> =
listOf(2 to 1, 2 to -1, -2 to 1, -2 to -1, 1 to 2, 1 to -2, -1 to 2, -1 to -2)
sealed class Piece(val color: Color, private val letter: Char) {
abstract fun targets(board: Board, from: Position): List<Position>
protected fun slide(board: Board, from: Position, directions: List<Step>): List<Position> =
directions.flatMap { step ->
buildList {
var at = from + step
while (at in board) {
val occupant = board[at]
if (occupant != null) {
if (occupant.color != color) add(at)
break
}
add(at)
at += step
}
}
}
protected fun step(board: Board, from: Position, offsets: List<Step>): List<Position> =
offsets.map { from + it }.filter { it in board && board[it]?.color != color }
override fun toString() =
(if (color == Color.WHITE) letter.uppercaseChar() else letter.lowercaseChar()).toString()
}
class Rook(color: Color) : Piece(color, 'R') {
override fun targets(board: Board, from: Position) = slide(board, from, STRAIGHT)
}
class Bishop(color: Color) : Piece(color, 'B') {
override fun targets(board: Board, from: Position) = slide(board, from, DIAGONAL)
}
class Queen(color: Color) : Piece(color, 'Q') {
override fun targets(board: Board, from: Position) = slide(board, from, ALL_EIGHT)
}
class King(color: Color) : Piece(color, 'K') {
override fun targets(board: Board, from: Position) = step(board, from, ALL_EIGHT)
}
class Knight(color: Color) : Piece(color, 'N') {
override fun targets(board: Board, from: Position) = step(board, from, LEAPS)
}
class Pawn(color: Color) : Piece(color, 'P') {
override fun targets(board: Board, from: Position): List<Position> {
val forward = color.pawnDirection
val ahead = from + (forward to 0)
val pushes = buildList {
if (ahead in board && board[ahead] == null) {
add(ahead)
val twoAhead = ahead + (forward to 0)
if (from.row == color.pawnStartRow && board[twoAhead] == null) add(twoAhead)
}
}
val captures = listOf(forward to -1, forward to 1)
.map { from + it }
.filter { it in board && board[it]?.color == color.opponent }
return pushes + captures
}
}
Watch