Low Level DesignWorked problems
Design chess
Design a chess game step by step for a senior low level design interview. Grow from one switch that knows every piece to move rules as strategies, legality checked by trying each move on a copy of the board, checkmate and stalemate, castling, en passant and promotion from a small piece of game history, and an online game where retried and out-of-date moves are refused, with code in Java, Python, C#, Go and TypeScript.
- Read first
- StrategyObject modeling
- Patterns
- Strategy
- Related
- Tic-tac-toe
“Design a chess game: two players, a board, and the rules.” Then: “how do you know the game is over?”
Chess is played by two players on a board of 8 by 8 squares. Each side has sixteen pieces, and each kind of piece moves in its own way. You win by checkmate: the other king is attacked (in check) and has no way to escape.
Everyone knows how the pieces move, so this question isn’t really about movement. It’s about the rules that cut across all the pieces. A move is only legal if it doesn’t leave your own king in check, and that one rule decides most of the hard cases. Three moves depend on what happened earlier in the game, not on the board alone. And in an online game, the same move can arrive at the server twice.
The answer in one minute
Section titled “The answer in one minute”If you only have a minute, this is the design. The rest of the page explains how we get here.
- Each kind of piece has a move rule (the Strategy pattern) that lists where it can go, ignoring check. Three rules cover six pieces: one for pieces that step, one for pieces that slide, and one for the pawn.
- A move is legal if the mover’s king isn’t attacked after it. We try the move on a copy of the board and ask. That one test handles moving into check, pinned pieces, and answering check.
- No legal moves ends the game. It’s checkmate if the king is attacked, and stalemate (a draw) if it isn’t.
- Castling and en passant need a little history: which rooks can still castle, and which square a pawn just skipped. The board alone can’t tell you.
- Online, every move carries the move number it was made at. The server plays it only if that number is still current. So a retried move is never played twice.
We’ll build this in four stages, then put the game online.
Scope it
Section titled “Scope it”Before drawing anything, we ask a few questions. Each answer changes a class. Anything that doesn’t, we assume and move on.
Design chess.
Whether you scope it before drawing a board. “Two people, standard rules, enforce legal moves, detect the end” is a design problem. Asking about a chess engine, ratings or the screen first is a red flag. Each is a separate system, and none of it decides this interview.
Here are the questions that matter:
- Two people, or against the computer? Two people. A computer opponent needs fast search, which is a different focus.
- Enforce the rules, or just record moves? Enforce every rule.
- What counts as “the end”? Checkmate and stalemate. Other draw rules come up at the end.
- Same device, or online? Online, each player on their own device.
- Standard chess only? Yes, but variants should be cheap to add.
These answers give us one promise that matters more than the others: no move leaves the mover’s own king in check. It’s the hard one, because it depends on the whole board, not on the piece moving. Two smaller promises come with it. A position with no legal moves always ends the game, the right way. And a move sent twice, or sent against a position that has since changed, is never played.
We leave out clocks, resigning, offering a draw, and saving games.
Go deeperThree more questions, and what each changes1 min
| Question | We assume | If the answer were different |
|---|---|---|
| Clocks? | Out of scope | A clock per side, paused and started on each move |
| Resign and offer a draw? | Out of scope | Two more ways to end, outside the move rules |
| Save and replay games? | Out of scope | Keep the move list; any position replays from the start |
Build it
Section titled “Build it”We’ll build the design in four stages. Each stage adds only the classes that a new requirement
needs. In each diagram, the new parts are outlined. The colors show what each class does (the key is
under every diagram). Squares have their usual names, like e4. In code, a square is a file
(the column, a to h, stored as 0 to 7) and a rank (the row, 1 to 8, stored as 0 to 7).
Stage 1: A board, and a switch that knows every piece
Section titled “Stage 1: A board, and a switch that knows every piece”Start simple. Set up the board and let players move pieces the way they move.
Whether you can get a correct board on the page quickly, and then name what the simple version gets wrong. The red flag is spending ten minutes here on how each piece moves.
Start with the smallest thing that works. A Board is an 8 by 8 grid of pieces. A Piece
is a kind and a color, nothing more. It’s a value, like a number: two white rooks are equal,
and a piece never changes. A Square is a value too. The Game holds the board and whose
turn it is.
To move, the game checks three things. There’s a piece on the starting square. It belongs to the
player whose turn it is. And the piece can make that move. Then it moves it. How a piece can move is
one big switch on its kind. The king steps one square. The knight jumps in an L. The rook, bishop
and queen slide until something is in the way. The pawn moves forward, and captures diagonally.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// The two sides. other() says whose turn comes next.enum Color { WHITE, BLACK;
Color other() { return this == WHITE ? BLACK : WHITE; }}
// The six kinds of piece. How each one moves lives in the rules, not here.enum Kind { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN }
// A piece is only its kind and color. It does not store its square: the board does, so moving// a piece changes one place, not two.record Piece(Kind kind, Color color) {}
// A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7).record Square(int file, int rank) { static Square of(String name) { return new Square(name.charAt(0) - 'a', name.charAt(1) - '1'); }
boolean onBoard() { return file >= 0 && file < 8 && rank >= 0 && rank < 8; }
Square plus(int df, int dr) { return new Square(file + df, rank + dr); }
@Override public String toString() { return "" + (char) ('a' + file) + (rank + 1); }}
class Board { private final Piece[][] cells = new Piece[8][8];
Piece at(Square s) { return cells[s.rank()][s.file()]; }
void put(Square s, Piece p) { cells[s.rank()][s.file()] = p; }
static Board standard() { Board b = new Board(); Kind[] backRank = {Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK}; for (int f = 0; f < 8; f++) { b.put(new Square(f, 0), new Piece(backRank[f], Color.WHITE)); b.put(new Square(f, 1), new Piece(Kind.PAWN, Color.WHITE)); b.put(new Square(f, 6), new Piece(Kind.PAWN, Color.BLACK)); b.put(new Square(f, 7), new Piece(backRank[f], Color.BLACK)); } return b; }}
enum Result { OK, NO_PIECE, NOT_YOUR_TURN, ILLEGAL_MOVE }
class Game { final Board board = Board.standard(); private Color turn = Color.WHITE;
Result move(String from, String to) { Square f = Square.of(from); Square t = Square.of(to); Piece p = board.at(f); if (p == null) return Result.NO_PIECE; if (p.color() != turn) return Result.NOT_YOUR_TURN; if (!canMove(p, f, t)) return Result.ILLEGAL_MOVE; board.put(t, p); board.put(f, null); turn = turn.other(); return Result.OK; }
// flaw 1: one switch knows how every piece moves, and every rule still to come (castling, en // passant, promotion) makes it longer. // flaw 2: nothing stops a move that leaves your own king in check. // flaw 3: the game never ends: there is no check, checkmate or stalemate. private boolean canMove(Piece p, Square f, Square t) { Piece target = board.at(t); if (target != null && target.color() == p.color()) return false; int df = t.file() - f.file(); int dr = t.rank() - f.rank(); int adf = Math.abs(df); int adr = Math.abs(dr); return switch (p.kind()) { case KING -> Math.max(adf, adr) == 1; case KNIGHT -> adf * adr == 2; case ROOK -> (df == 0 || dr == 0) && pathClear(f, t); case BISHOP -> adf == adr && pathClear(f, t); case QUEEN -> (df == 0 || dr == 0 || adf == adr) && pathClear(f, t); case PAWN -> { int forward = p.color() == Color.WHITE ? 1 : -1; int start = p.color() == Color.WHITE ? 1 : 6; if (df == 0 && target == null) { yield dr == forward || (dr == 2 * forward && f.rank() == start && board.at(f.plus(0, forward)) == null); } yield adf == 1 && dr == forward && target != null; } }; }
// Every square strictly between f and t is empty. private boolean pathClear(Square f, Square t) { int sf = Integer.signum(t.file() - f.file()); int sr = Integer.signum(t.rank() - f.rank()); for (Square s = f.plus(sf, sr); !s.equals(t); s = s.plus(sf, sr)) { if (board.at(s) != null) return false; } return true; }}# The two sides. other() says whose turn comes next.class Color(Enum): WHITE = auto() BLACK = auto()
def other(self) -> Color: return Color.BLACK if self is Color.WHITE else Color.WHITE
# The six kinds of piece. How each one moves lives in the rules, not here.class Kind(Enum): KING = auto() QUEEN = auto() ROOK = auto() BISHOP = auto() KNIGHT = auto() PAWN = auto()
# A piece is only its kind and color. It does not store its square: the board does, so# moving a piece changes one place, not two.@dataclass(frozen=True)class Piece: kind: Kind color: Color
# A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7).@dataclass(frozen=True)class Square: file: int rank: int
@staticmethod def of(name: str) -> Square: return Square(ord(name[0]) - ord("a"), int(name[1]) - 1)
def on_board(self) -> bool: return 0 <= self.file < 8 and 0 <= self.rank < 8
def plus(self, df: int, dr: int) -> Square: return Square(self.file + df, self.rank + dr)
def __str__(self) -> str: return chr(ord("a") + self.file) + str(self.rank + 1)
BACK_RANK = [Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK]
class Board: def __init__(self): self._cells: list[list[Piece | None]] = [[None] * 8 for _ in range(8)]
def at(self, s: Square) -> Piece | None: return self._cells[s.rank][s.file]
def put(self, s: Square, p: Piece | None) -> None: self._cells[s.rank][s.file] = p
@staticmethod def standard() -> Board: b = Board() for f, kind in enumerate(BACK_RANK): b.put(Square(f, 0), Piece(kind, Color.WHITE)) b.put(Square(f, 1), Piece(Kind.PAWN, Color.WHITE)) b.put(Square(f, 6), Piece(Kind.PAWN, Color.BLACK)) b.put(Square(f, 7), Piece(kind, Color.BLACK)) return b
class Result(Enum): OK = auto() NO_PIECE = auto() NOT_YOUR_TURN = auto() ILLEGAL_MOVE = auto()
class Game: def __init__(self): self.board = Board.standard() self._turn = Color.WHITE
def move(self, frm: str, to: str) -> Result: f, t = Square.of(frm), Square.of(to) p = self.board.at(f) if p is None: return Result.NO_PIECE if p.color is not self._turn: return Result.NOT_YOUR_TURN if not self._can_move(p, f, t): return Result.ILLEGAL_MOVE self.board.put(t, p) self.board.put(f, None) self._turn = self._turn.other() return Result.OK
# flaw 1: one function knows how every piece moves, and every rule still to come (castling, # en passant, promotion) makes it longer. # flaw 2: nothing stops a move that leaves your own king in check. # flaw 3: the game never ends: there is no check, checkmate or stalemate. def _can_move(self, p: Piece, f: Square, t: Square) -> bool: target = self.board.at(t) if target is not None and target.color is p.color: return False df, dr = t.file - f.file, t.rank - f.rank adf, adr = abs(df), abs(dr) match p.kind: case Kind.KING: return max(adf, adr) == 1 case Kind.KNIGHT: return adf * adr == 2 case Kind.ROOK: return (df == 0 or dr == 0) and self._path_clear(f, t) case Kind.BISHOP: return adf == adr and self._path_clear(f, t) case Kind.QUEEN: return (df == 0 or dr == 0 or adf == adr) and self._path_clear(f, t) case Kind.PAWN: forward = 1 if p.color is Color.WHITE else -1 start = 1 if p.color is Color.WHITE else 6 if df == 0 and target is None: return dr == forward or ( dr == 2 * forward and f.rank == start and self.board.at(f.plus(0, forward)) is None ) return adf == 1 and dr == forward and target is not None
# Every square strictly between f and t is empty. def _path_clear(self, f: Square, t: Square) -> bool: sf = (t.file > f.file) - (t.file < f.file) sr = (t.rank > f.rank) - (t.rank < f.rank) s = f.plus(sf, sr) while s != t: if self.board.at(s) is not None: return False s = s.plus(sf, sr) return True// The two sides. Other() says whose turn comes next.public enum Color { White, Black }
// The six kinds of piece. How each one moves lives in the rules, not here.public enum Kind { King, Queen, Rook, Bishop, Knight, Pawn }
// A C# enum can't have methods, so Other() lives here as an extension.public static class ColorExtensions{ public static Color Other(this Color c) => c == Color.White ? Color.Black : Color.White;}
// A piece is only its kind and color. It does not store its square: the board does, so moving// a piece changes one place, not two.public sealed record Piece(Kind Kind, Color Color);
// A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7). A record struct compares by// value, so squares can be looked up in lists and sets directly.public readonly record struct Square(int File, int Rank){ public static Square Of(string name) => new(name[0] - 'a', name[1] - '1'); public bool OnBoard => File is >= 0 and < 8 && Rank is >= 0 and < 8; public Square Plus(int df, int dr) => new(File + df, Rank + dr); public override string ToString() => $"{(char)('a' + File)}{Rank + 1}";}
public sealed class Board{ private readonly Piece?[,] _cells = new Piece?[8, 8];
public Piece? At(Square s) => _cells[s.Rank, s.File]; public void Put(Square s, Piece? p) => _cells[s.Rank, s.File] = p;
public static Board Standard() { var b = new Board(); Kind[] backRank = { Kind.Rook, Kind.Knight, Kind.Bishop, Kind.Queen, Kind.King, Kind.Bishop, Kind.Knight, Kind.Rook }; for (int f = 0; f < 8; f++) { b.Put(new Square(f, 0), new Piece(backRank[f], Color.White)); b.Put(new Square(f, 1), new Piece(Kind.Pawn, Color.White)); b.Put(new Square(f, 6), new Piece(Kind.Pawn, Color.Black)); b.Put(new Square(f, 7), new Piece(backRank[f], Color.Black)); } return b; }}
public enum Result { Ok, NoPiece, NotYourTurn, IllegalMove }
public sealed class Game{ public Board Board { get; } = Board.Standard(); private Color _turn = Color.White;
public Result Move(string from, string to) { var f = Square.Of(from); var t = Square.Of(to); var p = Board.At(f); if (p is null) return Result.NoPiece; if (p.Color != _turn) return Result.NotYourTurn; if (!CanMove(p, f, t)) return Result.IllegalMove; Board.Put(t, p); Board.Put(f, null); _turn = _turn.Other(); return Result.Ok; }
// flaw 1: one switch knows how every piece moves, and every rule still to come (castling, en // passant, promotion) makes it longer. // flaw 2: nothing stops a move that leaves your own king in check. // flaw 3: the game never ends: there is no check, checkmate or stalemate. private bool CanMove(Piece p, Square f, Square t) { var target = Board.At(t); if (target is not null && target.Color == p.Color) return false; int df = t.File - f.File, dr = t.Rank - f.Rank; int adf = Math.Abs(df), adr = Math.Abs(dr); int forward = p.Color == Color.White ? 1 : -1; int start = p.Color == Color.White ? 1 : 6; return p.Kind switch { Kind.King => Math.Max(adf, adr) == 1, Kind.Knight => adf * adr == 2, Kind.Rook => (df == 0 || dr == 0) && PathClear(f, t), Kind.Bishop => adf == adr && PathClear(f, t), Kind.Queen => (df == 0 || dr == 0 || adf == adr) && PathClear(f, t), Kind.Pawn when df == 0 && target is null => dr == forward || (dr == 2 * forward && f.Rank == start && Board.At(f.Plus(0, forward)) is null), Kind.Pawn => adf == 1 && dr == forward && target is not null, _ => false, }; }
// Every square strictly between f and t is empty. private bool PathClear(Square f, Square t) { int sf = Math.Sign(t.File - f.File), sr = Math.Sign(t.Rank - f.Rank); for (var s = f.Plus(sf, sr); s != t; s = s.Plus(sf, sr)) { if (Board.At(s) is not null) return false; } return true; }}// Color is one of the two sides. Other says whose turn comes next.type Color int
const ( White Color = iota Black)
func (c Color) Other() Color { return 1 - c }
func (c Color) String() string { return [...]string{"white", "black"}[c] }
// Kind starts at 1 so the zero Piece is an empty square.type Kind int
const ( King Kind = iota + 1 Queen Rook Bishop Knight Pawn)
// Piece is only a kind and a color. It does not store its square: the board does, so moving a// piece changes one place, not two.type Piece struct { Kind Kind Color Color}
func (p Piece) Empty() bool { return p.Kind == 0 }
// Square is a square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7).type Square struct{ File, Rank int }
func Sq(name string) Square { return Square{int(name[0] - 'a'), int(name[1] - '1')} }
func (s Square) OnBoard() bool { return s.File >= 0 && s.File < 8 && s.Rank >= 0 && s.Rank < 8 }
func (s Square) Plus(df, dr int) Square { return Square{s.File + df, s.Rank + dr} }
func (s Square) String() string { return fmt.Sprintf("%c%d", 'a'+s.File, s.Rank+1) }
// Board is an array of values, not pointers, so assigning a Board copies it.type Board [8][8]Piece
func (b *Board) At(s Square) Piece { return b[s.Rank][s.File] }
func (b *Board) Put(s Square, p Piece) { b[s.Rank][s.File] = p }
var backRank = [8]Kind{Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook}
// StandardBoard is the starting position: white on ranks 1 and 2, black on ranks 7 and 8.func StandardBoard() Board { var b Board for f, k := range backRank { b.Put(Square{f, 0}, Piece{k, White}) b.Put(Square{f, 1}, Piece{Pawn, White}) b.Put(Square{f, 6}, Piece{Pawn, Black}) b.Put(Square{f, 7}, Piece{k, Black}) } return b}
// Result is what a move request gets back, so a client can show why a move failed.type Result int
const ( OK Result = iota NoPiece NotYourTurn IllegalMove)
type Game struct { board Board turn Color}
func NewGame() *Game { return &Game{board: StandardBoard(), turn: White} }
func (g *Game) Move(from, to string) Result { f, t := Sq(from), Sq(to) p := g.board.At(f) if p.Empty() { return NoPiece } if p.Color != g.turn { return NotYourTurn } if !g.canMove(p, f, t) { return IllegalMove } g.board.Put(t, p) g.board.Put(f, Piece{}) g.turn = g.turn.Other() return OK}
func abs(x int) int { return max(x, -x) }
func sign(x int) int { return cmp.Compare(x, 0) }
// flaw 1: one switch knows how every piece moves, and every rule still to come (castling, en// passant, promotion) makes it longer.// flaw 2: nothing stops a move that leaves your own king in check.// flaw 3: the game never ends: there is no check, checkmate or stalemate.func (g *Game) canMove(p Piece, f, t Square) bool { target := g.board.At(t) if !target.Empty() && target.Color == p.Color { return false } df, dr := t.File-f.File, t.Rank-f.Rank adf, adr := abs(df), abs(dr) switch p.Kind { case King: return max(adf, adr) == 1 case Knight: return adf*adr == 2 case Rook: return (df == 0 || dr == 0) && g.pathClear(f, t) case Bishop: return adf == adr && g.pathClear(f, t) case Queen: return (df == 0 || dr == 0 || adf == adr) && g.pathClear(f, t) case Pawn: forward, start := 1, 1 if p.Color == Black { forward, start = -1, 6 } if df == 0 && target.Empty() { return dr == forward || (dr == 2*forward && f.Rank == start && g.board.At(f.Plus(0, forward)).Empty()) } return adf == 1 && dr == forward && !target.Empty() } return false}
// pathClear reports whether every square strictly between f and t is empty.func (g *Game) pathClear(f, t Square) bool { sf, sr := sign(t.File-f.File), sign(t.Rank-f.Rank) for s := f.Plus(sf, sr); s != t; s = s.Plus(sf, sr) { if !g.board.At(s).Empty() { return false } } return true}// The two sides. other() says whose turn comes next.type Color = 'white' | 'black';
const other = (c: Color): Color => (c === 'white' ? 'black' : 'white');
// The six kinds of piece. How each one moves lives in the rules, not here.type Kind = 'king' | 'queen' | 'rook' | 'bishop' | 'knight' | 'pawn';
// A piece is only its kind and color. It does not store its square: the board does, so moving// a piece changes one place, not two.interface Piece { readonly kind: Kind; readonly color: Color;}
// Objects compare by reference in JavaScript, so two pieces are compared field by field.const samePiece = (a: Piece | null, b: Piece | null) => a === b || (a !== null && b !== null && a.kind === b.kind && a.color === b.color);
// A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7). Objects compare by// reference in JavaScript, so squares are compared with equals.class Square { constructor( readonly file: number, readonly rank: number, ) {}
static of(name: string): Square { return new Square(name.charCodeAt(0) - 97, Number(name[1]) - 1); }
onBoard(): boolean { return this.file >= 0 && this.file < 8 && this.rank >= 0 && this.rank < 8; }
plus(df: number, dr: number): Square { return new Square(this.file + df, this.rank + dr); }
equals(o: Square | null): boolean { return o !== null && o.file === this.file && o.rank === this.rank; }
toString(): string { return String.fromCharCode(97 + this.file) + (this.rank + 1); }}
const BACK_RANK: Kind[] = ['rook', 'knight', 'bishop', 'queen', 'king', 'bishop', 'knight', 'rook'];
class Board { protected cells: (Piece | null)[][] = Array.from({ length: 8 }, () => Array<Piece | null>(8).fill(null));
at(s: Square): Piece | null { return this.cells[s.rank][s.file]; }
put(s: Square, p: Piece | null): void { this.cells[s.rank][s.file] = p; }
static standard(): Board { const b = new Board(); BACK_RANK.forEach((kind, f) => { b.put(new Square(f, 0), { kind, color: 'white' }); b.put(new Square(f, 1), { kind: 'pawn', color: 'white' }); b.put(new Square(f, 6), { kind: 'pawn', color: 'black' }); b.put(new Square(f, 7), { kind, color: 'black' }); }); return b; }}
type Result = 'ok' | 'no-piece' | 'not-your-turn' | 'illegal-move';
class Game { readonly board = Board.standard(); private turn: Color = 'white';
move(from: string, to: string): Result { const f = Square.of(from); const t = Square.of(to); const p = this.board.at(f); if (p === null) return 'no-piece'; if (p.color !== this.turn) return 'not-your-turn'; if (!this.canMove(p, f, t)) return 'illegal-move'; this.board.put(t, p); this.board.put(f, null); this.turn = other(this.turn); return 'ok'; }
// flaw 1: one switch knows how every piece moves, and every rule still to come (castling, en // passant, promotion) makes it longer. // flaw 2: nothing stops a move that leaves your own king in check. // flaw 3: the game never ends: there is no check, checkmate or stalemate. private canMove(p: Piece, f: Square, t: Square): boolean { const target = this.board.at(t); if (target !== null && target.color === p.color) return false; const df = t.file - f.file; const dr = t.rank - f.rank; const adf = Math.abs(df); const adr = Math.abs(dr); switch (p.kind) { case 'king': return Math.max(adf, adr) === 1; case 'knight': return adf * adr === 2; case 'rook': return (df === 0 || dr === 0) && this.pathClear(f, t); case 'bishop': return adf === adr && this.pathClear(f, t); case 'queen': return (df === 0 || dr === 0 || adf === adr) && this.pathClear(f, t); case 'pawn': { const forward = p.color === 'white' ? 1 : -1; const start = p.color === 'white' ? 1 : 6; if (df === 0 && target === null) { return ( dr === forward || (dr === 2 * forward && f.rank === start && this.board.at(f.plus(0, forward)) === null) ); } return adf === 1 && dr === forward && target !== null; } } }
// Every square strictly between f and t is empty. private pathClear(f: Square, t: Square): boolean { const sf = Math.sign(t.file - f.file); const sr = Math.sign(t.rank - f.rank); for (let s = f.plus(sf, sr); !s.equals(t); s = s.plus(sf, sr)) { if (this.board.at(s) !== null) return false; } return true; }}It works, but it has three problems, and we leave them in on purpose. The switch knows how every piece moves, and every rule still to come (castling, en passant, promotion) makes it longer. Nothing stops a player from moving into check, or leaving their king in check. And the game never ends.
Stage 2: Each piece’s moves as a rule
Section titled “Stage 2: Each piece’s moves as a rule”The switch is going to grow. How would you structure movement, so that adding a piece, or a variant with new pieces, doesn’t touch the game?
Whether you reach for the Strategy pattern and notice what the pieces share. A candidate who writes six classes with six copies of the sliding loop has missed that the queen is the rook and the bishop together.
Movement becomes a MoveRule. Given a board and a square, it returns the squares the piece
there can reach, ignoring check. The game looks up the rule for a piece’s kind, and never names a
piece itself. A family of interchangeable rules behind one interface is the Strategy pattern.
Six pieces need only three rules:
Step(directions, slides)walks in each of its directions. A stepper (king, knight) takes one step. A slider (rook, bishop, queen) keeps going until it reaches a piece. It can capture an enemy piece in the way, and stops before one of its own. The king and queen use all eight directions. The rook uses the four straight ones, and the bishop the four diagonal ones. The knight uses its eight L-shaped jumps.PawnRulemoves one square forward, or two from its starting row, only onto empty squares. It captures one square diagonally forward.
A rule also answers a second question: which squares does this piece attack? For every piece except the pawn, that’s the same as where it can move. A pawn attacks the two diagonal squares in front of it, even when they’re empty and it can’t move there. Stage 3 needs exactly this.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// How one kind of piece moves: where the piece on from can go, ignoring check. Adding a piece,// or a fairy-chess variant, is a new rule rather than a longer switch.interface MoveRule { List<Square> targets(Board board, Square from);
// The squares this piece threatens. The same as its targets for every piece but the pawn, // which moves straight and captures diagonally. default List<Square> attacks(Board board, Square from) { return targets(board, from); }}
// Steps in each direction, once (king, knight) or repeatedly until something is in the way// (rook, bishop, queen). An enemy piece in the way can be captured; an own piece can't.record Step(int[][] directions, boolean slides) implements MoveRule { public List<Square> targets(Board board, Square from) { Color own = board.at(from).color(); List<Square> out = new ArrayList<>(); for (int[] d : directions) { for (Square s = from.plus(d[0], d[1]); s.onBoard(); s = s.plus(d[0], d[1])) { Piece p = board.at(s); if (p == null || p.color() != own) out.add(s); if (p != null || !slides) break; } } return out; }}
// The pawn is the odd one out. It moves forward but captures diagonally, may move two squares// from its start, and only goes one way. So it gets its own rule.class PawnRule implements MoveRule { public List<Square> targets(Board board, Square from) { Color own = board.at(from).color(); int forward = own == Color.WHITE ? 1 : -1; List<Square> out = new ArrayList<>(); Square one = from.plus(0, forward); if (one.onBoard() && board.at(one) == null) { out.add(one); Square two = one.plus(0, forward); boolean onStart = from.rank() == (own == Color.WHITE ? 1 : 6); if (onStart && board.at(two) == null) out.add(two); } for (Square s : attacks(board, from)) { Piece p = board.at(s); if (p != null && p.color() != own) out.add(s); } return out; }
public List<Square> attacks(Board board, Square from) { int forward = board.at(from).color() == Color.WHITE ? 1 : -1; return Stream.of(from.plus(-1, forward), from.plus(1, forward)).filter(Square::onBoard).toList(); }}
// The one place that says how each kind of piece moves. Directions are {file, rank} steps.final class Rules { 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}}; static final int[][] KNIGHT = {{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}};
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and // the king and knight are steppers with different directions. static final Map<Kind, MoveRule> BY_KIND = Map.of( Kind.KING, new Step(ALL, false), Kind.QUEEN, new Step(ALL, true), Kind.ROOK, new Step(STRAIGHT, true), Kind.BISHOP, new Step(DIAGONAL, true), Kind.KNIGHT, new Step(KNIGHT, false), Kind.PAWN, new PawnRule());}
class Game { final Board board = Board.standard(); private Color turn = Color.WHITE;
// flaw: a move that leaves your own king in check is still allowed, and nothing ends the game. Result move(String from, String to) { Square f = Square.of(from); Square t = Square.of(to); Piece p = board.at(f); if (p == null) return Result.NO_PIECE; if (p.color() != turn) return Result.NOT_YOUR_TURN; if (!Rules.BY_KIND.get(p.kind()).targets(board, f).contains(t)) return Result.ILLEGAL_MOVE; board.put(t, p); board.put(f, null); turn = turn.other(); return Result.OK; }}# How one kind of piece moves: where the piece on frm can go, ignoring check. Adding a# piece, or a fairy-chess variant, is a new rule rather than a longer match.class MoveRule(Protocol): def targets(self, board: Board, frm: Square) -> list[Square]: ...
# The squares this piece threatens. The same as its targets for every piece but the # pawn, which moves straight and captures diagonally. def attacks(self, board: Board, frm: Square) -> list[Square]: ...
# Steps in each direction, once (king, knight) or repeatedly until something is in the way# (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.@dataclass(frozen=True)class Step: directions: tuple[tuple[int, int], ...] slides: bool
def targets(self, board: Board, frm: Square) -> list[Square]: own = board.at(frm).color out = [] for df, dr in self.directions: s = frm.plus(df, dr) while s.on_board(): p = board.at(s) if p is None or p.color is not own: out.append(s) if p is not None or not self.slides: break s = s.plus(df, dr) return out
def attacks(self, board: Board, frm: Square) -> list[Square]: return self.targets(board, frm)
# The pawn is the odd one out. It moves forward but captures diagonally, may move two# squares from its start, and only goes one way. So it gets its own rule.class PawnRule: def targets(self, board: Board, frm: Square) -> list[Square]: own = board.at(frm).color forward = 1 if own is Color.WHITE else -1 out = [] one = frm.plus(0, forward) if one.on_board() and board.at(one) is None: out.append(one) two = one.plus(0, forward) on_start = frm.rank == (1 if own is Color.WHITE else 6) if on_start and board.at(two) is None: out.append(two) for s in self.attacks(board, frm): p = board.at(s) if p is not None and p.color is not own: out.append(s) return out
def attacks(self, board: Board, frm: Square) -> list[Square]: forward = 1 if board.at(frm).color is Color.WHITE else -1 return [s for s in (frm.plus(-1, forward), frm.plus(1, forward)) if s.on_board()]
# Directions as (file, rank) steps.STRAIGHT = ((1, 0), (-1, 0), (0, 1), (0, -1))DIAGONAL = ((1, 1), (1, -1), (-1, 1), (-1, -1))KNIGHT_JUMPS = ((1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2))
# Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and# the king and knight are steppers with different directions.RULES: dict[Kind, MoveRule] = { Kind.KING: Step(STRAIGHT + DIAGONAL, slides=False), Kind.QUEEN: Step(STRAIGHT + DIAGONAL, slides=True), Kind.ROOK: Step(STRAIGHT, slides=True), Kind.BISHOP: Step(DIAGONAL, slides=True), Kind.KNIGHT: Step(KNIGHT_JUMPS, slides=False), Kind.PAWN: PawnRule(),}
class Game: def __init__(self): self.board = Board.standard() self._turn = Color.WHITE
# flaw: a move that leaves your own king in check is still allowed, and nothing ends the game. def move(self, frm: str, to: str) -> Result: f, t = Square.of(frm), Square.of(to) p = self.board.at(f) if p is None: return Result.NO_PIECE if p.color is not self._turn: return Result.NOT_YOUR_TURN if t not in RULES[p.kind].targets(self.board, f): return Result.ILLEGAL_MOVE self.board.put(t, p) self.board.put(f, None) self._turn = self._turn.other() return Result.OK// How one kind of piece moves: where the piece on from can go, ignoring check. Adding a piece,// or a fairy-chess variant, is a new rule rather than a longer switch.public interface IMoveRule{ IReadOnlyList<Square> Targets(Board board, Square from);
// The squares this piece threatens. The same as its targets for every piece but the pawn, // which moves straight and captures diagonally. IReadOnlyList<Square> Attacks(Board board, Square from) => Targets(board, from);}
// Steps in each direction, once (king, knight) or repeatedly until something is in the way// (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.public sealed record Step((int Df, int Dr)[] Directions, bool Slides) : IMoveRule{ public IReadOnlyList<Square> Targets(Board board, Square from) { var own = board.At(from)!.Color; var output = new List<Square>(); foreach (var (df, dr) in Directions) { for (var s = from.Plus(df, dr); s.OnBoard; s = s.Plus(df, dr)) { var p = board.At(s); if (p is null || p.Color != own) output.Add(s); if (p is not null || !Slides) break; } } return output; }}
// The pawn is the odd one out. It moves forward but captures diagonally, may move two squares// from its start, and only goes one way. So it gets its own rule.public sealed class PawnRule : IMoveRule{ public IReadOnlyList<Square> Targets(Board board, Square from) { var own = board.At(from)!.Color; int forward = own == Color.White ? 1 : -1; var output = new List<Square>(); var one = from.Plus(0, forward); if (one.OnBoard && board.At(one) is null) { output.Add(one); var two = one.Plus(0, forward); bool onStart = from.Rank == (own == Color.White ? 1 : 6); if (onStart && board.At(two) is null) output.Add(two); } foreach (var s in Attacks(board, from)) { var p = board.At(s); if (p is not null && p.Color != own) output.Add(s); } return output; }
public IReadOnlyList<Square> Attacks(Board board, Square from) { int forward = board.At(from)!.Color == Color.White ? 1 : -1; return new[] { from.Plus(-1, forward), from.Plus(1, forward) }.Where(s => s.OnBoard).ToList(); }}
// The one place that says how each kind of piece moves. Directions are (file, rank) steps.public static class Rules{ private static readonly (int, int)[] Straight = { (1, 0), (-1, 0), (0, 1), (0, -1) }; private static readonly (int, int)[] Diagonal = { (1, 1), (1, -1), (-1, 1), (-1, -1) }; private static readonly (int, int)[] All = Straight.Concat(Diagonal).ToArray(); private static readonly (int, int)[] KnightJumps = { (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2) };
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and // the king and knight are steppers with different directions. public static readonly IReadOnlyDictionary<Kind, IMoveRule> ByKind = new Dictionary<Kind, IMoveRule> { [Kind.King] = new Step(All, false), [Kind.Queen] = new Step(All, true), [Kind.Rook] = new Step(Straight, true), [Kind.Bishop] = new Step(Diagonal, true), [Kind.Knight] = new Step(KnightJumps, false), [Kind.Pawn] = new PawnRule(), };}
public sealed class Game{ public Board Board { get; } = Board.Standard(); private Color _turn = Color.White;
// flaw: a move that leaves your own king in check is still allowed, and nothing ends the game. public Result Move(string from, string to) { var f = Square.Of(from); var t = Square.Of(to); var p = Board.At(f); if (p is null) return Result.NoPiece; if (p.Color != _turn) return Result.NotYourTurn; if (!Rules.ByKind[p.Kind].Targets(Board, f).Contains(t)) return Result.IllegalMove; Board.Put(t, p); Board.Put(f, null); _turn = _turn.Other(); return Result.Ok; }}// MoveRule is how one kind of piece moves: where the piece on from can go, ignoring check.// Attacks are the squares it threatens, the same as its targets for every piece but the pawn,// which moves straight and captures diagonally. Adding a piece, or a fairy-chess variant, is a// new rule rather than a longer switch.type MoveRule interface { Targets(b *Board, from Square) []Square Attacks(b *Board, from Square) []Square}
// Step steps in each direction, once (king, knight) or repeatedly until something is in the// way (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.type Step struct { Dirs [][2]int Slides bool}
func (st Step) Targets(b *Board, from Square) []Square { own := b.At(from).Color var out []Square for _, d := range st.Dirs { for s := from.Plus(d[0], d[1]); s.OnBoard(); s = s.Plus(d[0], d[1]) { p := b.At(s) if p.Empty() || p.Color != own { out = append(out, s) } if !p.Empty() || !st.Slides { break } } } return out}
func (st Step) Attacks(b *Board, from Square) []Square { return st.Targets(b, from) }
// PawnRule is the odd one out. A pawn moves forward but captures diagonally, may move two// squares from its start, and only goes one way. So it gets its own rule.type PawnRule struct{}
func pawnForward(c Color) int { if c == White { return 1 } return -1}
func (PawnRule) Targets(b *Board, from Square) []Square { own := b.At(from).Color forward := pawnForward(own) var out []Square if one := from.Plus(0, forward); one.OnBoard() && b.At(one).Empty() { out = append(out, one) onStart := (own == White && from.Rank == 1) || (own == Black && from.Rank == 6) if two := one.Plus(0, forward); onStart && b.At(two).Empty() { out = append(out, two) } } for _, s := range (PawnRule{}).Attacks(b, from) { if p := b.At(s); !p.Empty() && p.Color != own { out = append(out, s) } } return out}
func (PawnRule) Attacks(b *Board, from Square) []Square { forward := pawnForward(b.At(from).Color) var out []Square for _, s := range []Square{from.Plus(-1, forward), from.Plus(1, forward)} { if s.OnBoard() { out = append(out, s) } } return out}
// Directions as {file, rank} steps.var ( straight = [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} diagonal = [][2]int{{1, 1}, {1, -1}, {-1, 1}, {-1, -1}} allDirs = slices.Concat(straight, diagonal) knightJumps = [][2]int{{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}})
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and// the king and knight are steppers with different directions.var Rules = map[Kind]MoveRule{ King: Step{allDirs, false}, Queen: Step{allDirs, true}, Rook: Step{straight, true}, Bishop: Step{diagonal, true}, Knight: Step{knightJumps, false}, Pawn: PawnRule{},}
// flaw: a move that leaves your own king in check is still allowed, and nothing ends the game.func (g *Game) Move(from, to string) Result { f, t := Sq(from), Sq(to) p := g.board.At(f) if p.Empty() { return NoPiece } if p.Color != g.turn { return NotYourTurn } if !slices.Contains(Rules[p.Kind].Targets(&g.board, f), t) { return IllegalMove } g.board.Put(t, p) g.board.Put(f, Piece{}) g.turn = g.turn.Other() return OK}// How one kind of piece moves: where the piece on from can go, ignoring check. attacks are the// squares it threatens, the same as its targets for every piece but the pawn, which moves// straight and captures diagonally. Adding a piece, or a fairy-chess variant, is a new rule// rather than a longer switch.interface MoveRule { targets(board: Board, from: Square): Square[]; attacks(board: Board, from: Square): Square[];}
type Dir = readonly [number, number];
// Steps in each direction, once (king, knight) or repeatedly until something is in the way// (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.class Step implements MoveRule { constructor( readonly directions: readonly Dir[], readonly slides: boolean, ) {}
targets(board: Board, from: Square): Square[] { const own = board.at(from)!.color; const out: Square[] = []; for (const [df, dr] of this.directions) { for (let s = from.plus(df, dr); s.onBoard(); s = s.plus(df, dr)) { const p = board.at(s); if (p === null || p.color !== own) out.push(s); if (p !== null || !this.slides) break; } } return out; }
attacks(board: Board, from: Square): Square[] { return this.targets(board, from); }}
// The pawn is the odd one out. It moves forward but captures diagonally, may move two squares// from its start, and only goes one way. So it gets its own rule.class PawnRule implements MoveRule { targets(board: Board, from: Square): Square[] { const own = board.at(from)!.color; const forward = own === 'white' ? 1 : -1; const out: Square[] = []; const one = from.plus(0, forward); if (one.onBoard() && board.at(one) === null) { out.push(one); const two = one.plus(0, forward); const onStart = from.rank === (own === 'white' ? 1 : 6); if (onStart && board.at(two) === null) out.push(two); } for (const s of this.attacks(board, from)) { const p = board.at(s); if (p !== null && p.color !== own) out.push(s); } return out; }
attacks(board: Board, from: Square): Square[] { const forward = board.at(from)!.color === 'white' ? 1 : -1; return [from.plus(-1, forward), from.plus(1, forward)].filter((s) => s.onBoard()); }}
// Directions as [file, rank] steps.const STRAIGHT: Dir[] = [ [1, 0], [-1, 0], [0, 1], [0, -1],];const DIAGONAL: Dir[] = [ [1, 1], [1, -1], [-1, 1], [-1, -1],];const KNIGHT_JUMPS: Dir[] = [ [1, 2], [2, 1], [2, -1], [1, -2], [-1, -2], [-2, -1], [-2, 1], [-1, 2],];
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and// the king and knight are steppers with different directions.const RULES: Record<Kind, MoveRule> = { king: new Step([...STRAIGHT, ...DIAGONAL], false), queen: new Step([...STRAIGHT, ...DIAGONAL], true), rook: new Step(STRAIGHT, true), bishop: new Step(DIAGONAL, true), knight: new Step(KNIGHT_JUMPS, false), pawn: new PawnRule(),};
class Game { readonly board = Board.standard(); private turn: Color = 'white';
// flaw: a move that leaves your own king in check is still allowed, and nothing ends the game. move(from: string, to: string): Result { const f = Square.of(from); const t = Square.of(to); const p = this.board.at(f); if (p === null) return 'no-piece'; if (p.color !== this.turn) return 'not-your-turn'; if (!RULES[p.kind].targets(this.board, f).some((s) => s.equals(t))) return 'illegal-move'; this.board.put(t, p); this.board.put(f, null); this.turn = other(this.turn); return 'ok'; }}Go deeperA rule per kind, or a class per piece1 min
A class per piece (Rook extends Piece) |
A rule per kind (our choice) | |
|---|---|---|
| What a piece is | An object with behaviour and identity | A value: kind and color |
| Shared sliding code | Copied, or pushed into a base class | One Step, used by three kinds |
| Promotion | Replace the object with a new class | Put a different value on the square |
| Copying the board | Copy objects, or share them carefully | Copy 64 references; values never change |
| A new piece in a variant | A new subclass | A new rule in the table |
A class per piece is a fine answer, if it stays about movement. What it mustn’t do is decide legality, which needs the whole board.
Stage 3: Check, checkmate and stalemate
Section titled “Stage 3: Check, checkmate and stalemate”A player moves a bishop that was blocking an attack on their own king. Your code allows it. Fix that, and tell me when the game is over.
Whether you find the single rule behind pins, check and the end of the game. The red flag is a separate special case for each: “if pinned”, “if in check, only these moves”. The strong answer is one test, applied to every move.
Here is the rule: a move is legal if, after it, the mover’s own king is not attacked. Three situations break it, and one test covers all three:
- Moving into check. The king steps onto an attacked square.
- Moving a pinned piece. A piece is pinned when it stands between its own king and an attacker. Moving it away opens the line to the king.
- Ignoring a check. The king is already attacked, and the move doesn’t fix that, so the king is still attacked afterwards.
The game checks the rule the direct way. For each move a rule allows, it copies the board, and
makes the move on the copy. It finds the mover’s king, and asks whether any enemy piece attacks that
square. “Attacks” uses each rule’s attacks method. That’s why stage 2 added it.
Isn’t copying the board slow? No. A board is 64 references, and pieces are values that never change, so the copy and the original can share them.
- Color key
- Facade
The end of the game comes from the same idea. After every move, the game lists the legal moves of the player who moves next. If there are none, the game is over. If that player’s king is attacked, it’s checkmate, and the other player wins. If it isn’t, it’s stalemate, a draw. The game also checks this when it’s created, so a puzzle position that is already checkmate starts finished.
- Color key
- State
- Fails or stops
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// The 64 squares and what stands on each. It knows nothing about the rules, which makes it// cheap to copy when the game wants to test a move.class Board { private final Piece[][] cells = new Piece[8][8]; // [rank][file]; null is an empty square
Piece at(Square s) { return cells[s.rank()][s.file()]; }
void put(Square s, Piece p) { cells[s.rank()][s.file()] = p; }
// Trying a move on a copy is how the game asks "would my king be safe?". 64 references // copy in nanoseconds. Board copy() { Board b = new Board(); for (int r = 0; r < 8; r++) b.cells[r] = cells[r].clone(); return b; }
// All 64 squares, so callers loop over squares, not array indexes. List<Square> squares() { List<Square> out = new ArrayList<>(); for (int r = 0; r < 8; r++) for (int f = 0; f < 8; f++) out.add(new Square(f, r)); return out; }
// Every check test starts from the king's square. Square kingOf(Color color) { for (Square s : squares()) { Piece p = at(s); if (p != null && p.kind() == Kind.KING && p.color() == color) return s; } throw new IllegalStateException("no " + color + " king"); }
// The starting position: white on ranks 1 and 2, black on ranks 7 and 8. static Board standard() { Board b = new Board(); Kind[] backRank = {Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK}; for (int f = 0; f < 8; f++) { b.put(new Square(f, 0), new Piece(backRank[f], Color.WHITE)); b.put(new Square(f, 1), new Piece(Kind.PAWN, Color.WHITE)); b.put(new Square(f, 6), new Piece(Kind.PAWN, Color.BLACK)); b.put(new Square(f, 7), new Piece(backRank[f], Color.BLACK)); } return b; }}
enum Result { OK, NO_PIECE, NOT_YOUR_TURN, ILLEGAL_MOVE, GAME_OVER }
// Whether the game is still on. Checkmate and stalemate are both final.enum State { PLAYING, CHECKMATE, STALEMATE }
record Move(Square from, Square to) {}
class Game { private final Board board; private Color turn; private State state = State.PLAYING;
Game() { this(Board.standard(), Color.WHITE); }
// Any position, for puzzles and tests. It may already be over. Game(Board board, Color turn) { this.board = board; this.turn = turn; updateState(); }
Result move(String from, String to) { if (state != State.PLAYING) return Result.GAME_OVER; Square f = Square.of(from); Piece p = board.at(f); if (p == null) return Result.NO_PIECE; if (p.color() != turn) return Result.NOT_YOUR_TURN; Move m = new Move(f, Square.of(to)); if (!legalMoves().contains(m)) return Result.ILLEGAL_MOVE; apply(board, m); turn = turn.other(); updateState(); return Result.OK; }
// A move is legal if the piece can make it and the mover's own king isn't attacked after it. // That one test covers moving into check, moving a pinned piece, and failing to answer check. List<Move> legalMoves() { List<Move> out = new ArrayList<>(); for (Square f : board.squares()) { Piece p = board.at(f); if (p == null || p.color() != turn) continue; for (Square t : Rules.BY_KIND.get(p.kind()).targets(board, f)) { Move m = new Move(f, t); if (safeAfter(m)) out.add(m); } } return out; }
boolean inCheck() { return attacked(board, board.kingOf(turn), turn.other()); }
State state() { return state; }
Color turn() { return turn; }
Color winner() { return state == State.CHECKMATE ? turn.other() : null; }
Piece at(String square) { return board.at(Square.of(square)); }
private boolean safeAfter(Move m) { Board after = board.copy(); apply(after, m); return !attacked(after, after.kingOf(turn), turn.other()); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private void updateState() { if (legalMoves().isEmpty()) state = inCheck() ? State.CHECKMATE : State.STALEMATE; }
static boolean attacked(Board b, Square target, Color by) { for (Square s : b.squares()) { Piece p = b.at(s); if (p != null && p.color() == by && Rules.BY_KIND.get(p.kind()).attacks(b, s).contains(target)) return true; } return false; }
static void apply(Board b, Move m) { b.put(m.to(), b.at(m.from())); b.put(m.from(), null); }}# The 64 squares and what stands on each. It knows nothing about the rules, which makes it# cheap to copy when the game wants to test a move.class Board: def __init__(self): # [rank][file]; None is an empty square self._cells: list[list[Piece | None]] = [[None] * 8 for _ in range(8)]
def at(self, s: Square) -> Piece | None: return self._cells[s.rank][s.file]
def put(self, s: Square, p: Piece | None) -> None: self._cells[s.rank][s.file] = p
# Trying a move on a copy is how the game asks "would my king be safe?". Pieces are # immutable, so copying the rows is enough. def copy(self) -> Board: b = Board() b._cells = [row[:] for row in self._cells] return b
# All 64 squares, so callers loop over squares, not list indexes. def squares(self) -> list[Square]: return [Square(f, r) for r in range(8) for f in range(8)]
# Every check test starts from the king's square. def king_of(self, color: Color) -> Square: for s in self.squares(): if self.at(s) == Piece(Kind.KING, color): return s raise ValueError(f"no {color} king")
# The starting position: white on ranks 1 and 2, black on ranks 7 and 8. @staticmethod def standard() -> Board: b = Board() for f, kind in enumerate(BACK_RANK): b.put(Square(f, 0), Piece(kind, Color.WHITE)) b.put(Square(f, 1), Piece(Kind.PAWN, Color.WHITE)) b.put(Square(f, 6), Piece(Kind.PAWN, Color.BLACK)) b.put(Square(f, 7), Piece(kind, Color.BLACK)) return b
class Result(Enum): OK = auto() NO_PIECE = auto() NOT_YOUR_TURN = auto() ILLEGAL_MOVE = auto() GAME_OVER = auto()
# Whether the game is still on. Checkmate and stalemate are both final.class State(Enum): PLAYING = auto() CHECKMATE = auto() STALEMATE = auto()
@dataclass(frozen=True)class Move: frm: Square to: Square
# Does any piece of color `by` threaten target? A plain function, so it can test a copied# board too.def attacked(b: Board, target: Square, by: Color) -> bool: for s in b.squares(): p = b.at(s) if p is not None and p.color is by and target in RULES[p.kind].attacks(b, s): return True return False
def apply(b: Board, m: Move) -> None: b.put(m.to, b.at(m.frm)) b.put(m.frm, None)
class Game: # Any position, for puzzles and tests. It may already be over. def __init__(self, board: Board | None = None, turn: Color = Color.WHITE): self._board = board or Board.standard() self._turn = turn self._state = State.PLAYING self._update_state()
def move(self, frm: str, to: str) -> Result: if self._state is not State.PLAYING: return Result.GAME_OVER f = Square.of(frm) p = self._board.at(f) if p is None: return Result.NO_PIECE if p.color is not self._turn: return Result.NOT_YOUR_TURN if Move(f, Square.of(to)) not in self.legal_moves(): return Result.ILLEGAL_MOVE apply(self._board, Move(f, Square.of(to))) self._turn = self._turn.other() self._update_state() return Result.OK
# A move is legal if the piece can make it and the mover's own king isn't attacked # after it. That one test covers moving into check, moving a pinned piece, and failing # to answer check. def legal_moves(self) -> list[Move]: out = [] for f in self._board.squares(): p = self._board.at(f) if p is None or p.color is not self._turn: continue for t in RULES[p.kind].targets(self._board, f): if self._safe_after(Move(f, t)): out.append(Move(f, t)) return out
def in_check(self) -> bool: return attacked(self._board, self._board.king_of(self._turn), self._turn.other())
@property def state(self) -> State: return self._state
@property def turn(self) -> Color: return self._turn
def winner(self) -> Color | None: return self._turn.other() if self._state is State.CHECKMATE else None
def at(self, square: str) -> Piece | None: return self._board.at(Square.of(square))
def _safe_after(self, m: Move) -> bool: after = self._board.copy() apply(after, m) return not attacked(after, after.king_of(self._turn), self._turn.other())
# No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. def _update_state(self) -> None: if not self.legal_moves(): self._state = State.CHECKMATE if self.in_check() else State.STALEMATE// The 64 squares and what stands on each. It knows nothing about the rules, which makes it// cheap to copy when the game wants to test a move.public sealed class Board{ private readonly Piece?[,] _cells = new Piece?[8, 8]; // [rank, file]; null is an empty square
public Piece? At(Square s) => _cells[s.Rank, s.File]; public void Put(Square s, Piece? p) => _cells[s.Rank, s.File] = p;
// Trying a move on a copy is how the game asks "would my king be safe?". Pieces are // immutable records, so copying the 64 references is enough. public Board Copy() { var b = new Board(); Array.Copy(_cells, b._cells, _cells.Length); return b; }
// All 64 squares, so callers loop over squares, not array indexes. public IEnumerable<Square> Squares() { for (int r = 0; r < 8; r++) for (int f = 0; f < 8; f++) yield return new Square(f, r); }
// Every check test starts from the king's square. public Square KingOf(Color color) => Squares().First(s => At(s) == new Piece(Kind.King, color));
// The starting position: white on ranks 1 and 2, black on ranks 7 and 8. public static Board Standard() { var b = new Board(); Kind[] backRank = { Kind.Rook, Kind.Knight, Kind.Bishop, Kind.Queen, Kind.King, Kind.Bishop, Kind.Knight, Kind.Rook }; for (int f = 0; f < 8; f++) { b.Put(new Square(f, 0), new Piece(backRank[f], Color.White)); b.Put(new Square(f, 1), new Piece(Kind.Pawn, Color.White)); b.Put(new Square(f, 6), new Piece(Kind.Pawn, Color.Black)); b.Put(new Square(f, 7), new Piece(backRank[f], Color.Black)); } return b; }}
public enum Result { Ok, NoPiece, NotYourTurn, IllegalMove, GameOver }
// Whether the game is still on. Checkmate and stalemate are both final.public enum State { Playing, Checkmate, Stalemate }
public readonly record struct Move(Square From, Square To);
public sealed class Game{ private readonly Board _board; private Color _turn;
public Game() : this(Board.Standard(), Color.White) { }
// Any position, for puzzles and tests. It may already be over. public Game(Board board, Color turn) { _board = board; _turn = turn; UpdateState(); }
public State State { get; private set; } = State.Playing; public Color Turn => _turn; public Color? Winner => State == State.Checkmate ? _turn.Other() : null; public bool InCheck => Attacked(_board, _board.KingOf(_turn), _turn.Other()); public Piece? At(string square) => _board.At(Square.Of(square));
public Result Move(string from, string to) { if (State != State.Playing) return Result.GameOver; var f = Square.Of(from); var p = _board.At(f); if (p is null) return Result.NoPiece; if (p.Color != _turn) return Result.NotYourTurn; var m = new Move(f, Square.Of(to)); if (!LegalMoves().Contains(m)) return Result.IllegalMove; Apply(_board, m); _turn = _turn.Other(); UpdateState(); return Result.Ok; }
// A move is legal if the piece can make it and the mover's own king isn't attacked after it. // That one test covers moving into check, moving a pinned piece, and failing to answer check. public List<Move> LegalMoves() { var output = new List<Move>(); foreach (var f in _board.Squares()) { var p = _board.At(f); if (p is null || p.Color != _turn) continue; foreach (var t in Rules.ByKind[p.Kind].Targets(_board, f)) { var m = new Move(f, t); if (SafeAfter(m)) output.Add(m); } } return output; }
private bool SafeAfter(Move m) { var after = _board.Copy(); Apply(after, m); return !Attacked(after, after.KingOf(_turn), _turn.Other()); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private void UpdateState() { if (LegalMoves().Count == 0) State = InCheck ? State.Checkmate : State.Stalemate; }
public static bool Attacked(Board b, Square target, Color by) => b.Squares().Any(s => b.At(s) is { } p && p.Color == by && Rules.ByKind[p.Kind].Attacks(b, s).Contains(target));
public static void Apply(Board b, Move m) { b.Put(m.To, b.At(m.From)); b.Put(m.From, null); }}// Squares lists all 64 squares, so callers loop over squares, not array indexes.func (b *Board) Squares() []Square { out := make([]Square, 0, 64) for r := range 8 { for f := range 8 { out = append(out, Square{f, r}) } } return out}
// KingOf finds a side's king. Every check test starts from its square.func (b *Board) KingOf(c Color) Square { for _, s := range b.Squares() { if b.At(s) == (Piece{King, c}) { return s } } panic("no " + c.String() + " king")}
const ( OK Result = iota NoPiece NotYourTurn IllegalMove GameOver)
// State is whether the game is still on. Checkmate and stalemate are both final.type State int
const ( Playing State = iota Checkmate Stalemate)
type Move struct{ From, To Square }
type Game struct { board Board turn Color state State}
func NewGame() *Game { return NewGameFrom(StandardBoard(), White) }
// NewGameFrom starts from any position, for puzzles and tests. It may already be over.func NewGameFrom(b Board, turn Color) *Game { g := &Game{board: b, turn: turn} g.updateState() return g}
func (g *Game) Move(from, to string) Result { if g.state != Playing { return GameOver } f := Sq(from) p := g.board.At(f) if p.Empty() { return NoPiece } if p.Color != g.turn { return NotYourTurn } m := Move{f, Sq(to)} if !slices.Contains(g.LegalMoves(), m) { return IllegalMove } apply(&g.board, m) g.turn = g.turn.Other() g.updateState() return OK}
// LegalMoves are the moves a piece can make that leave the mover's own king unattacked. That// one test covers moving into check, moving a pinned piece, and failing to answer check.func (g *Game) LegalMoves() []Move { var out []Move for _, f := range g.board.Squares() { p := g.board.At(f) if p.Empty() || p.Color != g.turn { continue } for _, t := range Rules[p.Kind].Targets(&g.board, f) { if m := (Move{f, t}); g.safeAfter(m) { out = append(out, m) } } } return out}
func (g *Game) InCheck() bool { return attacked(&g.board, g.board.KingOf(g.turn), g.turn.Other()) }
func (g *Game) State() State { return g.state }
func (g *Game) Turn() Color { return g.turn }
// Winner is the side that delivered checkmate, if the game ended that way.func (g *Game) Winner() (Color, bool) { return g.turn.Other(), g.state == Checkmate }
func (g *Game) At(square string) Piece { return g.board.At(Sq(square)) }
// safeAfter tries the move on a copy of the board, which is one assignment.func (g *Game) safeAfter(m Move) bool { after := g.board apply(&after, m) return !attacked(&after, after.KingOf(g.turn), g.turn.Other())}
// updateState ends the game when the side to move has no legal move: checkmate if its king is// attacked, stalemate (a draw) if not.func (g *Game) updateState() { if len(g.LegalMoves()) > 0 { return } if g.InCheck() { g.state = Checkmate } else { g.state = Stalemate }}
// attacked reports whether any piece of color by threatens target. It is a plain function, so// it can test a copied board too.func attacked(b *Board, target Square, by Color) bool { for _, s := range b.Squares() { if p := b.At(s); !p.Empty() && p.Color == by && slices.Contains(Rules[p.Kind].Attacks(b, s), target) { return true } } return false}
func apply(b *Board, m Move) { b.Put(m.To, b.At(m.From)) b.Put(m.From, Piece{})}// The 64 squares and what stands on each. It knows nothing about the rules, which makes it// cheap to copy when the game wants to test a move.class Board { // [rank][file]; null is an empty square protected cells: (Piece | null)[][] = Array.from({ length: 8 }, () => Array<Piece | null>(8).fill(null));
at(s: Square): Piece | null { return this.cells[s.rank][s.file]; }
put(s: Square, p: Piece | null): void { this.cells[s.rank][s.file] = p; }
// Trying a move on a copy is how the game asks "would my king be safe?". Pieces are never // changed in place, so copying the rows is enough. copy(): Board { const b = new Board(); b.cells = this.cells.map((row) => [...row]); return b; }
// All 64 squares, so callers loop over squares, not array indexes. squares(): Square[] { const out: Square[] = []; for (let r = 0; r < 8; r++) for (let f = 0; f < 8; f++) out.push(new Square(f, r)); return out; }
// Every check test starts from the king's square. kingOf(color: Color): Square { const s = this.squares().find((s) => samePiece(this.at(s), { kind: 'king', color })); if (!s) throw new Error(`no ${color} king`); return s; }
// The starting position: white on ranks 1 and 2, black on ranks 7 and 8. static standard(): Board { const b = new Board(); BACK_RANK.forEach((kind, f) => { b.put(new Square(f, 0), { kind, color: 'white' }); b.put(new Square(f, 1), { kind: 'pawn', color: 'white' }); b.put(new Square(f, 6), { kind: 'pawn', color: 'black' }); b.put(new Square(f, 7), { kind, color: 'black' }); }); return b; }}
type Result = 'ok' | 'no-piece' | 'not-your-turn' | 'illegal-move' | 'game-over';
// Whether the game is still on. Checkmate and stalemate are both final.type State = 'playing' | 'checkmate' | 'stalemate';
interface Move { readonly from: Square; readonly to: Square;}
// Does any piece of color `by` threaten target? A plain function, so it can test a copied board// too.function attacked(b: Board, target: Square, by: Color): boolean { return b.squares().some((s) => { const p = b.at(s); return p !== null && p.color === by && RULES[p.kind].attacks(b, s).some((a) => a.equals(target)); });}
function apply(b: Board, m: Move): void { b.put(m.to, b.at(m.from)); b.put(m.from, null);}
class Game { private board: Board; private turn: Color; private currentState: State = 'playing';
// Any position, for puzzles and tests. It may already be over. constructor(board = Board.standard(), turn: Color = 'white') { this.board = board; this.turn = turn; this.updateState(); }
move(from: string, to: string): Result { if (this.currentState !== 'playing') return 'game-over'; const f = Square.of(from); const t = Square.of(to); const p = this.board.at(f); if (p === null) return 'no-piece'; if (p.color !== this.turn) return 'not-your-turn'; const m = this.legalMoves().find((c) => c.from.equals(f) && c.to.equals(t)); if (!m) return 'illegal-move'; apply(this.board, m); this.turn = other(this.turn); this.updateState(); return 'ok'; }
// A move is legal if the piece can make it and the mover's own king isn't attacked after it. // That one test covers moving into check, moving a pinned piece, and failing to answer check. legalMoves(): Move[] { const out: Move[] = []; for (const from of this.board.squares()) { const p = this.board.at(from); if (p === null || p.color !== this.turn) continue; for (const to of RULES[p.kind].targets(this.board, from)) { if (this.safeAfter({ from, to })) out.push({ from, to }); } } return out; }
inCheck(): boolean { return attacked(this.board, this.board.kingOf(this.turn), other(this.turn)); }
get state(): State { return this.currentState; }
get toMove(): Color { return this.turn; }
winner(): Color | null { return this.currentState === 'checkmate' ? other(this.turn) : null; }
at(square: string): Piece | null { return this.board.at(Square.of(square)); }
private safeAfter(m: Move): boolean { const after = this.board.copy(); apply(after, m); return !attacked(after, after.kingOf(this.turn), other(this.turn)); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private updateState(): void { if (this.legalMoves().length === 0) this.currentState = this.inCheck() ? 'checkmate' : 'stalemate'; }}Stage 4: Moves that need the game’s history
Section titled “Stage 4: Moves that need the game’s history”Add castling, en passant and promotion. What does the game need to remember that the board doesn’t show?
Whether you find the smallest state that answers each rule. The red flag is a hasMoved flag on
every piece. It breaks as soon as a test sets up a position. Worse is reading the whole move
history every time.
Three special moves can’t be decided from the board alone.
- Castling. The king moves two squares toward a rook, and the rook jumps to the square the king crossed. It’s allowed only if neither piece has moved, every square between them is empty, the king isn’t in check, and the king doesn’t cross or land on an attacked square. “Neither has moved” is history. So the game keeps a set of rooks that can still castle, by square. A king move removes both rooks of its side. A rook moving, or being captured on its square, removes that rook. That set is everything castling needs to remember.
- En passant. Say a pawn moves two squares. An enemy pawn beside where it landed may capture it as if it had moved only one. But only on the very next move. The game keeps the square the pawn skipped, and clears it after every other move.
- Promotion. A pawn reaching the last row becomes a queen, rook, bishop or knight, as the
player chooses. This needs no history, but the move has to carry the choice, so
Movegains apromotionfield.
Go deeperHow the board recognises a special move1 min
The board can recognise each special move without being told. A pawn moving diagonally to an
empty square is en passant, so the captured pawn beside it is removed too. A king moving two
columns is castling, so the rook moves too. apply makes those changes on the real board and on
the copies used to test legality alike. So a castling move that would leave the king in check is
refused like any other move.
Moves are now made in two steps. candidates lists every move the rules allow, then adds en
passant, castling, and one move per promotion choice. legalMoves keeps those that pass the stage
3 test. The game also counts plies: moves by either side, so after white and black have each
moved once, the ply is 2. The online stage uses this count.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
- New in this step
// promotion is the piece a pawn becomes on the last rank, and null for every other move.record Move(Square from, Square to, Kind promotion) {}
// One game of chess. It owns the board, plus what the board alone can't tell you: whose turn it// is, which rooks may still castle, and where en passant is possible.class Game { // What a pawn may become: never a king, and it can't stay a pawn. private static final List<Kind> PROMOTIONS = List.of(Kind.QUEEN, Kind.ROOK, Kind.BISHOP, Kind.KNIGHT);
private final Board board; private Color turn; private State state = State.PLAYING; private int ply; // moves made so far, by both sides // The rooks that can still castle, by square. A king's move removes both of its side's; a // rook moving or being captured removes its own. That is everything castling remembers. private final Set<Square> castlingRooks = new HashSet<>(); // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may capture // it there, on this move only. private Square enPassant;
Game() { this(Board.standard(), Color.WHITE); }
// Any position, for puzzles and tests. A king and rook on their starting squares may castle. Game(Board board, Color turn) { this.board = board; this.turn = turn; for (Color c : Color.values()) { int rank = c == Color.WHITE ? 0 : 7; if (!new Piece(Kind.KING, c).equals(board.at(new Square(4, rank)))) continue; for (int file : new int[] {0, 7}) { Square corner = new Square(file, rank); if (new Piece(Kind.ROOK, c).equals(board.at(corner))) castlingRooks.add(corner); } } updateState(); }
Result move(String from, String to) { return move(from, to, null); }
// A pawn reaching the last rank becomes a queen unless another piece is asked for. Result move(String from, String to, Kind promotion) { if (state != State.PLAYING) return Result.GAME_OVER; Square f = Square.of(from); Square t = Square.of(to); Piece p = board.at(f); if (p == null) return Result.NO_PIECE; if (p.color() != turn) return Result.NOT_YOUR_TURN; Kind wanted = promotion == null ? Kind.QUEEN : promotion; Move m = legalMoves().stream() .filter(c -> c.from().equals(f) && c.to().equals(t) && (c.promotion() == null || c.promotion() == wanted)) .findFirst().orElse(null); if (m == null) return Result.ILLEGAL_MOVE; play(m, p); return Result.OK; }
// A move is legal if the pieces' rules allow it and the mover's own king isn't attacked // after it. List<Move> legalMoves() { List<Move> out = new ArrayList<>(); for (Move m : candidates()) if (safeAfter(m)) out.add(m); return out; }
boolean inCheck() { return attacked(board, board.kingOf(turn), turn.other()); }
State state() { return state; }
Color turn() { return turn; }
int ply() { return ply; }
// The side that gave checkmate. null while playing, and after a stalemate. Color winner() { return state == State.CHECKMATE ? turn.other() : null; }
Piece at(String square) { return board.at(Square.of(square)); }
// Plays a move already known to be legal, then updates what castling and en passant // remember. private void play(Move m, Piece p) { apply(board, m); if (p.kind() == Kind.KING) castlingRooks.removeIf(r -> r.rank() == m.from().rank()); castlingRooks.remove(m.from()); castlingRooks.remove(m.to()); boolean doublePush = p.kind() == Kind.PAWN && Math.abs(m.to().rank() - m.from().rank()) == 2; enPassant = doublePush ? new Square(m.from().file(), (m.from().rank() + m.to().rank()) / 2) : null; turn = turn.other(); ply++; updateState(); }
// Every move the pieces' rules allow, plus the three that depend on the game's history rather // than the board: en passant, castling, and promotion's choice of piece. private List<Move> candidates() { List<Move> out = new ArrayList<>(); int lastRank = turn == Color.WHITE ? 7 : 0; for (Square f : board.squares()) { Piece p = board.at(f); if (p == null || p.color() != turn) continue; MoveRule rule = Rules.BY_KIND.get(p.kind()); for (Square t : rule.targets(board, f)) { if (p.kind() == Kind.PAWN && t.rank() == lastRank) { for (Kind k : PROMOTIONS) out.add(new Move(f, t, k)); } else { out.add(new Move(f, t, null)); } } if (p.kind() == Kind.PAWN && enPassant != null && rule.attacks(board, f).contains(enPassant)) { out.add(new Move(f, enPassant, null)); } } addCastling(out); return out; }
// The king moves two squares toward a rook that can still castle, and the rook jumps over it. // Not out of check, not through an attacked square, and only with every square between empty. // The square the king lands on is checked by safeAfter like any other move. private void addCastling(List<Move> out) { if (inCheck()) return; Square king = new Square(4, turn == Color.WHITE ? 0 : 7); for (Square rook : castlingRooks) { if (rook.rank() != king.rank()) continue; int dir = rook.file() > king.file() ? 1 : -1; boolean empty = true; for (Square s = king.plus(dir, 0); !s.equals(rook); s = s.plus(dir, 0)) empty &= board.at(s) == null; if (empty && !attacked(board, king.plus(dir, 0), turn.other())) out.add(new Move(king, king.plus(2 * dir, 0), null)); } }
// Play the move on a copy and look: is our king attacked? It catches pins and discovered // checks with no special code. private boolean safeAfter(Move m) { Board after = board.copy(); apply(after, m); return !attacked(after, after.kingOf(turn), turn.other()); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private void updateState() { if (legalMoves().isEmpty()) state = inCheck() ? State.CHECKMATE : State.STALEMATE; }
// Does any piece of color `by` threaten target? Static, so it can test a copied board too. static boolean attacked(Board b, Square target, Color by) { for (Square s : b.squares()) { Piece p = b.at(s); if (p != null && p.color() == by && Rules.BY_KIND.get(p.kind()).attacks(b, s).contains(target)) return true; } return false; }
// The special moves are recognised from the board: a pawn moving diagonally to an empty // square is en passant, and a king moving two files is castling. static void apply(Board b, Move m) { Piece p = b.at(m.from()); if (p.kind() == Kind.PAWN && m.from().file() != m.to().file() && b.at(m.to()) == null) { b.put(new Square(m.to().file(), m.from().rank()), null); } if (p.kind() == Kind.KING && Math.abs(m.to().file() - m.from().file()) == 2) { int dir = Integer.signum(m.to().file() - m.from().file()); Square rook = new Square(dir > 0 ? 7 : 0, m.from().rank()); b.put(m.from().plus(dir, 0), b.at(rook)); b.put(rook, null); } b.put(m.to(), m.promotion() == null ? p : new Piece(m.promotion(), p.color())); b.put(m.from(), null); }}@dataclass(frozen=True)class Move: frm: Square to: Square promotion: Kind | None = None # the piece a pawn becomes on the last rank
# What a pawn may become: never a king, and it can't stay a pawn.PROMOTIONS = (Kind.QUEEN, Kind.ROOK, Kind.BISHOP, Kind.KNIGHT)
# The special moves are recognised from the board: a pawn moving diagonally to an empty# square is en passant, and a king moving two files is castling.def apply(b: Board, m: Move) -> None: p = b.at(m.frm) if p.kind is Kind.PAWN and m.frm.file != m.to.file and b.at(m.to) is None: b.put(Square(m.to.file, m.frm.rank), None) if p.kind is Kind.KING and abs(m.to.file - m.frm.file) == 2: d = 1 if m.to.file > m.frm.file else -1 rook = Square(7 if d > 0 else 0, m.frm.rank) b.put(m.frm.plus(d, 0), b.at(rook)) b.put(rook, None) b.put(m.to, p if m.promotion is None else Piece(m.promotion, p.color)) b.put(m.frm, None)
# One game of chess. It owns the board, plus what the board alone can't tell you: whose turn# it is, which rooks may still castle, and where en passant is possible.class Game: # Any position, for puzzles and tests. A king and rook on their starting squares may castle. def __init__(self, board: Board | None = None, turn: Color = Color.WHITE): self._board = board or Board.standard() self._turn = turn self._state = State.PLAYING self._ply = 0 # moves made so far, by both sides # The rooks that can still castle, by square. A king's move removes both of its side's, # a rook moving or being captured removes its own. That is everything castling remembers. self._castling_rooks: set[Square] = set() # The square a pawn skipped over on the last move, if it moved two: an enemy pawn may # capture it there, on this move only. self._en_passant: Square | None = None for c in Color: rank = 0 if c is Color.WHITE else 7 if self._board.at(Square(4, rank)) != Piece(Kind.KING, c): continue for file in (0, 7): if self._board.at(Square(file, rank)) == Piece(Kind.ROOK, c): self._castling_rooks.add(Square(file, rank)) self._update_state()
# A pawn reaching the last rank becomes a queen unless another piece is asked for. def move(self, frm: str, to: str, promotion: Kind | None = None) -> Result: if self._state is not State.PLAYING: return Result.GAME_OVER f, t = Square.of(frm), Square.of(to) p = self._board.at(f) if p is None: return Result.NO_PIECE if p.color is not self._turn: return Result.NOT_YOUR_TURN wanted = promotion or Kind.QUEEN m = next( (c for c in self.legal_moves() if c.frm == f and c.to == t and c.promotion in (None, wanted)), None, ) if m is None: return Result.ILLEGAL_MOVE self._play(m, p) return Result.OK
# A move is legal if the pieces' rules allow it and the mover's own king isn't # attacked after it. def legal_moves(self) -> list[Move]: return [m for m in self._candidates() if self._safe_after(m)]
def in_check(self) -> bool: return attacked(self._board, self._board.king_of(self._turn), self._turn.other())
@property def state(self) -> State: return self._state
@property def turn(self) -> Color: return self._turn
@property def ply(self) -> int: return self._ply
# The side that gave checkmate. None while playing, and after a stalemate. def winner(self) -> Color | None: return self._turn.other() if self._state is State.CHECKMATE else None
def at(self, square: str) -> Piece | None: return self._board.at(Square.of(square))
# Plays a move already known to be legal, then updates what castling and en passant # remember. def _play(self, m: Move, p: Piece) -> None: apply(self._board, m) if p.kind is Kind.KING: self._castling_rooks = {r for r in self._castling_rooks if r.rank != m.frm.rank} self._castling_rooks -= {m.frm, m.to} double_push = p.kind is Kind.PAWN and abs(m.to.rank - m.frm.rank) == 2 self._en_passant = Square(m.frm.file, (m.frm.rank + m.to.rank) // 2) if double_push else None self._turn = self._turn.other() self._ply += 1 self._update_state()
# Every move the pieces' rules allow, plus the three that depend on the game's history # rather than the board: en passant, castling, and promotion's choice of piece. def _candidates(self) -> list[Move]: out = [] last_rank = 7 if self._turn is Color.WHITE else 0 for f in self._board.squares(): p = self._board.at(f) if p is None or p.color is not self._turn: continue rule = RULES[p.kind] for t in rule.targets(self._board, f): if p.kind is Kind.PAWN and t.rank == last_rank: out.extend(Move(f, t, k) for k in PROMOTIONS) else: out.append(Move(f, t)) if p.kind is Kind.PAWN and self._en_passant in rule.attacks(self._board, f): out.append(Move(f, self._en_passant)) self._add_castling(out) return out
# The king moves two squares toward a rook that can still castle, and the rook jumps # over it. Not out of check, not through an attacked square, and only with every square # between empty. The square the king lands on is checked by _safe_after like any move. def _add_castling(self, out: list[Move]) -> None: if self.in_check(): return king = Square(4, 0 if self._turn is Color.WHITE else 7) for rook in self._castling_rooks: if rook.rank != king.rank: continue d = 1 if rook.file > king.file else -1 between = range(min(king.file, rook.file) + 1, max(king.file, rook.file)) empty = all(self._board.at(Square(f, king.rank)) is None for f in between) if empty and not attacked(self._board, king.plus(d, 0), self._turn.other()): out.append(Move(king, king.plus(2 * d, 0)))
# Play the move on a copy and look: is our king attacked? It catches pins and # discovered checks with no special code. def _safe_after(self, m: Move) -> bool: after = self._board.copy() apply(after, m) return not attacked(after, after.king_of(self._turn), self._turn.other())
# No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. def _update_state(self) -> None: if not self.legal_moves(): self._state = State.CHECKMATE if self.in_check() else State.STALEMATE// Promotion is the piece a pawn becomes on the last rank, and null for every other move.public readonly record struct Move(Square From, Square To, Kind? Promotion = null);
// One game of chess. It owns the board, plus what the board alone can't tell you: whose turn it// is, which rooks may still castle, and where en passant is possible.public sealed class Game{ // What a pawn may become: never a king, and it can't stay a pawn. private static readonly Kind[] Promotions = { Kind.Queen, Kind.Rook, Kind.Bishop, Kind.Knight };
private readonly Board _board; private Color _turn; // The rooks that can still castle, by square. A king's move removes both of its side's; a // rook moving or being captured removes its own. That is everything castling remembers. private readonly HashSet<Square> _castlingRooks = new(); // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may // capture it there, on this move only. private Square? _enPassant;
public Game() : this(Board.Standard(), Color.White) { }
// Any position, for puzzles and tests. A king and rook on their starting squares may castle. public Game(Board board, Color turn) { _board = board; _turn = turn; foreach (var c in new[] { Color.White, Color.Black }) { int rank = c == Color.White ? 0 : 7; if (board.At(new Square(4, rank)) != new Piece(Kind.King, c)) continue; foreach (int file in new[] { 0, 7 }) { var corner = new Square(file, rank); if (board.At(corner) == new Piece(Kind.Rook, c)) _castlingRooks.Add(corner); } } UpdateState(); }
public State State { get; private set; } = State.Playing; public Color Turn => _turn; public int Ply { get; private set; } // moves made so far, by both sides // The side that gave checkmate. null while playing, and after a stalemate. public Color? Winner => State == State.Checkmate ? _turn.Other() : null; public bool InCheck => Attacked(_board, _board.KingOf(_turn), _turn.Other()); public Piece? At(string square) => _board.At(Square.Of(square));
// A pawn reaching the last rank becomes a queen unless another piece is asked for. public Result Move(string from, string to, Kind? promotion = null) { if (State != State.Playing) return Result.GameOver; var f = Square.Of(from); var t = Square.Of(to); var p = _board.At(f); if (p is null) return Result.NoPiece; if (p.Color != _turn) return Result.NotYourTurn; var wanted = promotion ?? Kind.Queen; var found = LegalMoves().Where(c => c.From == f && c.To == t && (c.Promotion is null || c.Promotion == wanted)); if (!found.Any()) return Result.IllegalMove; Play(found.First(), p); return Result.Ok; }
// A move is legal if the pieces' rules allow it and the mover's own king isn't attacked // after it. public List<Move> LegalMoves() => Candidates().Where(SafeAfter).ToList();
// Plays a move already known to be legal, then updates what castling and en passant // remember. private void Play(Move m, Piece p) { Apply(_board, m); if (p.Kind == Kind.King) _castlingRooks.RemoveWhere(r => r.Rank == m.From.Rank); _castlingRooks.Remove(m.From); _castlingRooks.Remove(m.To); bool doublePush = p.Kind == Kind.Pawn && Math.Abs(m.To.Rank - m.From.Rank) == 2; _enPassant = doublePush ? new Square(m.From.File, (m.From.Rank + m.To.Rank) / 2) : null; _turn = _turn.Other(); Ply++; UpdateState(); }
// Every move the pieces' rules allow, plus the three that depend on the game's history rather // than the board: en passant, castling, and promotion's choice of piece. private List<Move> Candidates() { var output = new List<Move>(); int lastRank = _turn == Color.White ? 7 : 0; foreach (var f in _board.Squares()) { var p = _board.At(f); if (p is null || p.Color != _turn) continue; var rule = Rules.ByKind[p.Kind]; foreach (var t in rule.Targets(_board, f)) { if (p.Kind == Kind.Pawn && t.Rank == lastRank) output.AddRange(Promotions.Select(k => new Move(f, t, k))); else output.Add(new Move(f, t)); } if (p.Kind == Kind.Pawn && _enPassant is { } ep && rule.Attacks(_board, f).Contains(ep)) output.Add(new Move(f, ep)); } AddCastling(output); return output; }
// The king moves two squares toward a rook that can still castle, and the rook jumps over it. // Not out of check, not through an attacked square, and only with every square between empty. // The square the king lands on is checked by SafeAfter like any other move. private void AddCastling(List<Move> output) { if (InCheck) return; var king = new Square(4, _turn == Color.White ? 0 : 7); foreach (var rook in _castlingRooks) { if (rook.Rank != king.Rank) continue; int dir = Math.Sign(rook.File - king.File); bool empty = true; for (var s = king.Plus(dir, 0); s != rook; s = s.Plus(dir, 0)) empty &= _board.At(s) is null; if (empty && !Attacked(_board, king.Plus(dir, 0), _turn.Other())) output.Add(new Move(king, king.Plus(2 * dir, 0))); } }
// Play the move on a copy and look: is our king attacked? It catches pins and discovered // checks with no special code. private bool SafeAfter(Move m) { var after = _board.Copy(); Apply(after, m); return !Attacked(after, after.KingOf(_turn), _turn.Other()); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private void UpdateState() { if (LegalMoves().Count == 0) State = InCheck ? State.Checkmate : State.Stalemate; }
// Does any piece of color `by` threaten target? Static, so it can test a copied board too. public static bool Attacked(Board b, Square target, Color by) => b.Squares().Any(s => b.At(s) is { } p && p.Color == by && Rules.ByKind[p.Kind].Attacks(b, s).Contains(target));
// The special moves are recognised from the board: a pawn moving diagonally to an empty // square is en passant, and a king moving two files is castling. public static void Apply(Board b, Move m) { var p = b.At(m.From)!; if (p.Kind == Kind.Pawn && m.From.File != m.To.File && b.At(m.To) is null) b.Put(new Square(m.To.File, m.From.Rank), null); if (p.Kind == Kind.King && Math.Abs(m.To.File - m.From.File) == 2) { int dir = Math.Sign(m.To.File - m.From.File); var rook = new Square(dir > 0 ? 7 : 0, m.From.Rank); b.Put(m.From.Plus(dir, 0), b.At(rook)); b.Put(rook, null); } b.Put(m.To, m.Promotion is { } k ? new Piece(k, p.Color) : p); b.Put(m.From, null); }}// Move.Promotion is the piece a pawn becomes on the last rank, and zero for every other move.type Move struct { From, To Square Promotion Kind}
// promotions is what a pawn may become: never a king, and it can't stay a pawn.var promotions = []Kind{Queen, Rook, Bishop, Knight}
// Game is one game of chess. It owns the board, plus what the board alone can't tell you: whose// turn it is, which rooks may still castle, and where en passant is possible.type Game struct { board Board turn Color state State ply int // moves made so far, by both sides // The rooks that can still castle, by square. A king's move removes both of its side's, a // rook moving or being captured removes its own. That is everything castling remembers. castlingRooks map[Square]bool // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may // capture it there, on this move only. enPassant *Square}
// NewGameFrom starts from any position. A king and rook on their starting squares may castle.func NewGameFrom(b Board, turn Color) *Game { g := &Game{board: b, turn: turn, castlingRooks: map[Square]bool{}} for _, c := range []Color{White, Black} { rank := 0 if c == Black { rank = 7 } if b.At(Square{4, rank}) != (Piece{King, c}) { continue } for _, file := range []int{0, 7} { if b.At(Square{file, rank}) == (Piece{Rook, c}) { g.castlingRooks[Square{file, rank}] = true } } } g.updateState() return g}
func (g *Game) Move(from, to string) Result { return g.MovePromoting(from, to, 0) }
// MovePromoting makes a move, and a pawn reaching the last rank becomes the piece asked for,// or a queen if promotion is zero.func (g *Game) MovePromoting(from, to string, promotion Kind) Result { if g.state != Playing { return GameOver } f, t := Sq(from), Sq(to) p := g.board.At(f) if p.Empty() { return NoPiece } if p.Color != g.turn { return NotYourTurn } if promotion == 0 { promotion = Queen } for _, m := range g.LegalMoves() { if m.From == f && m.To == t && (m.Promotion == 0 || m.Promotion == promotion) { g.play(m, p) return OK } } return IllegalMove}
// LegalMoves are the moves the pieces' rules allow that leave the mover's own king unattacked.func (g *Game) LegalMoves() []Move { var out []Move for _, m := range g.candidates() { if g.safeAfter(m) { out = append(out, m) } } return out}
func (g *Game) InCheck() bool { return attacked(&g.board, g.board.KingOf(g.turn), g.turn.Other()) }
func (g *Game) State() State { return g.state }
func (g *Game) Turn() Color { return g.turn }
// Winner is the side that delivered checkmate, if the game ended that way.func (g *Game) Winner() (Color, bool) { return g.turn.Other(), g.state == Checkmate }
func (g *Game) At(square string) Piece { return g.board.At(Sq(square)) }
func (g *Game) Ply() int { return g.ply }
// play makes a move already known to be legal, then updates what castling and en passant// remember.func (g *Game) play(m Move, p Piece) { apply(&g.board, m) if p.Kind == King { maps.DeleteFunc(g.castlingRooks, func(r Square, _ bool) bool { return r.Rank == m.From.Rank }) } delete(g.castlingRooks, m.From) delete(g.castlingRooks, m.To) g.enPassant = nil if p.Kind == Pawn && abs(m.To.Rank-m.From.Rank) == 2 { g.enPassant = &Square{m.From.File, (m.From.Rank + m.To.Rank) / 2} } g.turn = g.turn.Other() g.ply++ g.updateState()}
// candidates are every move the pieces' rules allow, plus the three that depend on the game's// history rather than the board: en passant, castling, and promotion's choice of piece.func (g *Game) candidates() []Move { var out []Move lastRank := 7 if g.turn == Black { lastRank = 0 } for _, f := range g.board.Squares() { p := g.board.At(f) if p.Empty() || p.Color != g.turn { continue } rule := Rules[p.Kind] for _, t := range rule.Targets(&g.board, f) { if p.Kind == Pawn && t.Rank == lastRank { for _, k := range promotions { out = append(out, Move{f, t, k}) } } else { out = append(out, Move{f, t, 0}) } } if p.Kind == Pawn && g.enPassant != nil && slices.Contains(rule.Attacks(&g.board, f), *g.enPassant) { out = append(out, Move{f, *g.enPassant, 0}) } } return g.addCastling(out)}
// addCastling adds the king moving two squares toward a rook that can still castle. Not out of// check, not through an attacked square, and only with every square between empty. The square// the king lands on is checked by safeAfter like any other move.func (g *Game) addCastling(out []Move) []Move { if g.InCheck() { return out } king := Square{4, 0} if g.turn == Black { king = Square{4, 7} } for rook := range g.castlingRooks { if rook.Rank != king.Rank { continue } dir := sign(rook.File - king.File) empty := true for s := king.Plus(dir, 0); s != rook; s = s.Plus(dir, 0) { empty = empty && g.board.At(s).Empty() } if empty && !attacked(&g.board, king.Plus(dir, 0), g.turn.Other()) { out = append(out, Move{king, king.Plus(2*dir, 0), 0}) } } return out}
// safeAfter tries the move on a copy of the board, which is one assignment.func (g *Game) safeAfter(m Move) bool { after := g.board apply(&after, m) return !attacked(&after, after.KingOf(g.turn), g.turn.Other())}
// updateState ends the game when the side to move has no legal move: checkmate if its king is// attacked, stalemate (a draw) if not.func (g *Game) updateState() { if len(g.LegalMoves()) > 0 { return } if g.InCheck() { g.state = Checkmate } else { g.state = Stalemate }}
// apply recognises the special moves from the board: a pawn moving diagonally to an empty// square is en passant, and a king moving two files is castling.func apply(b *Board, m Move) { p := b.At(m.From) if p.Kind == Pawn && m.From.File != m.To.File && b.At(m.To).Empty() { b.Put(Square{m.To.File, m.From.Rank}, Piece{}) } if p.Kind == King && abs(m.To.File-m.From.File) == 2 { dir := sign(m.To.File - m.From.File) rook := Square{0, m.From.Rank} if dir > 0 { rook.File = 7 } b.Put(m.From.Plus(dir, 0), b.At(rook)) b.Put(rook, Piece{}) } if m.Promotion != 0 { p.Kind = m.Promotion } b.Put(m.To, p) b.Put(m.From, Piece{})}// promotion is the piece a pawn becomes on the last rank, and absent for every other move.interface Move { readonly from: Square; readonly to: Square; readonly promotion?: Kind;}
// What a pawn may become: never a king, and it can't stay a pawn.const PROMOTIONS: Kind[] = ['queen', 'rook', 'bishop', 'knight'];
// The special moves are recognised from the board: a pawn moving diagonally to an empty square// is en passant, and a king moving two files is castling.function apply(b: Board, m: Move): void { const p = b.at(m.from)!; if (p.kind === 'pawn' && m.from.file !== m.to.file && b.at(m.to) === null) { b.put(new Square(m.to.file, m.from.rank), null); } if (p.kind === 'king' && Math.abs(m.to.file - m.from.file) === 2) { const dir = Math.sign(m.to.file - m.from.file); const rook = new Square(dir > 0 ? 7 : 0, m.from.rank); b.put(m.from.plus(dir, 0), b.at(rook)); b.put(rook, null); } b.put(m.to, m.promotion ? { kind: m.promotion, color: p.color } : p); b.put(m.from, null);}
// One game of chess. It owns the board, plus what the board alone can't tell you: whose turn it// is, which rooks may still castle, and where en passant is possible.class Game { private board: Board; private turn: Color; private currentState: State = 'playing'; private moves = 0; // moves made so far, by both sides // The rooks that can still castle. A king's move removes both of its side's, a rook moving or // being captured removes its own. That is everything castling remembers. private castlingRooks: Square[] = []; // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may capture // it there, on this move only. private enPassant: Square | null = null;
// Any position, for puzzles and tests. A king and rook on their starting squares may castle. constructor(board = Board.standard(), turn: Color = 'white') { this.board = board; this.turn = turn; for (const color of ['white', 'black'] as const) { const rank = color === 'white' ? 0 : 7; if (!samePiece(board.at(new Square(4, rank)), { kind: 'king', color })) continue; for (const corner of [new Square(0, rank), new Square(7, rank)]) { if (samePiece(board.at(corner), { kind: 'rook', color })) this.castlingRooks.push(corner); } } this.updateState(); }
// A pawn reaching the last rank becomes a queen unless another piece is asked for. move(from: string, to: string, promotion: Kind = 'queen'): Result { if (this.currentState !== 'playing') return 'game-over'; const f = Square.of(from); const t = Square.of(to); const p = this.board.at(f); if (p === null) return 'no-piece'; if (p.color !== this.turn) return 'not-your-turn'; const m = this.legalMoves().find( (c) => c.from.equals(f) && c.to.equals(t) && (c.promotion === undefined || c.promotion === promotion), ); if (!m) return 'illegal-move'; this.play(m, p); return 'ok'; }
// A move is legal if the pieces' rules allow it and the mover's own king isn't attacked after // it. legalMoves(): Move[] { return this.candidates().filter((m) => this.safeAfter(m)); }
inCheck(): boolean { return attacked(this.board, this.board.kingOf(this.turn), other(this.turn)); }
get state(): State { return this.currentState; }
get toMove(): Color { return this.turn; }
get ply(): number { return this.moves; }
// The side that gave checkmate. null while playing, and after a stalemate. winner(): Color | null { return this.currentState === 'checkmate' ? other(this.turn) : null; }
at(square: string): Piece | null { return this.board.at(Square.of(square)); }
// Plays a move already known to be legal, then updates what castling and en passant remember. private play(m: Move, p: Piece): void { apply(this.board, m); this.castlingRooks = this.castlingRooks.filter( (r) => !(p.kind === 'king' && r.rank === m.from.rank) && !r.equals(m.from) && !r.equals(m.to), ); const doublePush = p.kind === 'pawn' && Math.abs(m.to.rank - m.from.rank) === 2; this.enPassant = doublePush ? new Square(m.from.file, (m.from.rank + m.to.rank) / 2) : null; this.turn = other(this.turn); this.moves++; this.updateState(); }
// Every move the pieces' rules allow, plus the three that depend on the game's history rather // than the board: en passant, castling, and promotion's choice of piece. private candidates(): Move[] { const out: Move[] = []; const lastRank = this.turn === 'white' ? 7 : 0; for (const from of this.board.squares()) { const p = this.board.at(from); if (p === null || p.color !== this.turn) continue; const rule = RULES[p.kind]; for (const to of rule.targets(this.board, from)) { if (p.kind === 'pawn' && to.rank === lastRank) { for (const promotion of PROMOTIONS) out.push({ from, to, promotion }); } else { out.push({ from, to }); } } const ep = this.enPassant; if (p.kind === 'pawn' && ep && rule.attacks(this.board, from).some((s) => s.equals(ep))) { out.push({ from, to: ep }); } } this.addCastling(out); return out; }
// The king moves two squares toward a rook that can still castle, and the rook jumps over it. // Not out of check, not through an attacked square, and only with every square between empty. // The square the king lands on is checked by safeAfter like any other move. private addCastling(out: Move[]): void { if (this.inCheck()) return; const king = new Square(4, this.turn === 'white' ? 0 : 7); for (const rook of this.castlingRooks) { if (rook.rank !== king.rank) continue; const dir = Math.sign(rook.file - king.file); let empty = true; for (let s = king.plus(dir, 0); !s.equals(rook); s = s.plus(dir, 0)) empty &&= this.board.at(s) === null; if (empty && !attacked(this.board, king.plus(dir, 0), other(this.turn))) { out.push({ from: king, to: king.plus(2 * dir, 0) }); } } }
// Play the move on a copy and look: is our king attacked? It catches pins and discovered checks // with no special code. private safeAfter(m: Move): boolean { const after = this.board.copy(); apply(after, m); return !attacked(after, after.kingOf(this.turn), other(this.turn)); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private updateState(): void { if (this.legalMoves().length === 0) this.currentState = this.inCheck() ? 'checkmate' : 'stalemate'; }}Go deeperWhere castling rights live, and why the legality test doesn't care2 min
| Option | How | Problem |
|---|---|---|
hasMoved on each piece |
Set when a piece moves | Pieces stop being values; a set-up position needs flags |
| Search the move history | “Has anything left e1 or h1?” | Every castling check reads the whole game |
| A set of rooks (our choice) | Removed on king move, rook move, capture | None: four squares at most, and easy to set up in tests |
Chess notation (FEN, the standard text format for a position) stores castling rights the same way, as up to four letters. That’s a sign it’s the right amount of state.
Why doesn’t the pinned-piece test need to know about castling? Because castling is just another
move handed to the same test. addCastling checks the parts the test can’t see: that the king
isn’t in check now, and doesn’t cross an attacked square. Then the move goes through safeAfter
like any other, which checks the square the king lands on. Every special move reuses the one
legality rule, instead of adding its own.
This is enough for a mid-level answer. A rule per kind. Legality by trying the move and checking the king. And the game ending when there are no legal moves. The rest of the page is what makes it a senior answer.
The full design
Section titled “The full design”Here is everything together. Game enforces the rules and owns the little history they need.
Board holds the pieces and copies itself cheaply. Each kind’s MoveRule lists where it can go
and what it attacks. SharedGame lets two players on a network submit moves safely.
- Color key
- Facade
- Entity
- Value object
- Enum
- Interface
- Implementation
// The complete design, built from every stage on this page.
import java.util.*;import java.util.stream.*;
// The two sides. other() says whose turn comes next.enum Color { WHITE, BLACK;
Color other() { return this == WHITE ? BLACK : WHITE; }}
// The six kinds of piece. How each one moves lives in the rules, not here.enum Kind { KING, QUEEN, ROOK, BISHOP, KNIGHT, PAWN }
// A piece is only its kind and color. It does not store its square: the board does, so moving// a piece changes one place, not two.record Piece(Kind kind, Color color) {}
// A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7).record Square(int file, int rank) { static Square of(String name) { return new Square(name.charAt(0) - 'a', name.charAt(1) - '1'); }
boolean onBoard() { return file >= 0 && file < 8 && rank >= 0 && rank < 8; }
Square plus(int df, int dr) { return new Square(file + df, rank + dr); }
@Override public String toString() { return "" + (char) ('a' + file) + (rank + 1); }}
// The 64 squares and what stands on each. It knows nothing about the rules, which makes it// cheap to copy when the game wants to test a move.class Board { private final Piece[][] cells = new Piece[8][8]; // [rank][file]; null is an empty square
Piece at(Square s) { return cells[s.rank()][s.file()]; }
void put(Square s, Piece p) { cells[s.rank()][s.file()] = p; }
// Trying a move on a copy is how the game asks "would my king be safe?". 64 references // copy in nanoseconds. Board copy() { Board b = new Board(); for (int r = 0; r < 8; r++) b.cells[r] = cells[r].clone(); return b; }
// All 64 squares, so callers loop over squares, not array indexes. List<Square> squares() { List<Square> out = new ArrayList<>(); for (int r = 0; r < 8; r++) for (int f = 0; f < 8; f++) out.add(new Square(f, r)); return out; }
// Every check test starts from the king's square. Square kingOf(Color color) { for (Square s : squares()) { Piece p = at(s); if (p != null && p.kind() == Kind.KING && p.color() == color) return s; } throw new IllegalStateException("no " + color + " king"); }
// The starting position: white on ranks 1 and 2, black on ranks 7 and 8. static Board standard() { Board b = new Board(); Kind[] backRank = {Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK}; for (int f = 0; f < 8; f++) { b.put(new Square(f, 0), new Piece(backRank[f], Color.WHITE)); b.put(new Square(f, 1), new Piece(Kind.PAWN, Color.WHITE)); b.put(new Square(f, 6), new Piece(Kind.PAWN, Color.BLACK)); b.put(new Square(f, 7), new Piece(backRank[f], Color.BLACK)); } return b; }}
// What a move request gets back. STALE is for online play: the move was made against an old// position.enum Result { OK, NO_PIECE, NOT_YOUR_TURN, ILLEGAL_MOVE, GAME_OVER, STALE }
// One game of chess. It owns the board, plus what the board alone can't tell you: whose turn it// is, which rooks may still castle, and where en passant is possible.class Game { // What a pawn may become: never a king, and it can't stay a pawn. private static final List<Kind> PROMOTIONS = List.of(Kind.QUEEN, Kind.ROOK, Kind.BISHOP, Kind.KNIGHT);
private final Board board; private Color turn; private State state = State.PLAYING; private int ply; // moves made so far, by both sides // The rooks that can still castle, by square. A king's move removes both of its side's; a // rook moving or being captured removes its own. That is everything castling remembers. private final Set<Square> castlingRooks = new HashSet<>(); // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may capture // it there, on this move only. private Square enPassant;
Game() { this(Board.standard(), Color.WHITE); }
// Any position, for puzzles and tests. A king and rook on their starting squares may castle. Game(Board board, Color turn) { this.board = board; this.turn = turn; for (Color c : Color.values()) { int rank = c == Color.WHITE ? 0 : 7; if (!new Piece(Kind.KING, c).equals(board.at(new Square(4, rank)))) continue; for (int file : new int[] {0, 7}) { Square corner = new Square(file, rank); if (new Piece(Kind.ROOK, c).equals(board.at(corner))) castlingRooks.add(corner); } } updateState(); }
Result move(String from, String to) { return move(from, to, null); }
// A pawn reaching the last rank becomes a queen unless another piece is asked for. Result move(String from, String to, Kind promotion) { if (state != State.PLAYING) return Result.GAME_OVER; Square f = Square.of(from); Square t = Square.of(to); Piece p = board.at(f); if (p == null) return Result.NO_PIECE; if (p.color() != turn) return Result.NOT_YOUR_TURN; Kind wanted = promotion == null ? Kind.QUEEN : promotion; Move m = legalMoves().stream() .filter(c -> c.from().equals(f) && c.to().equals(t) && (c.promotion() == null || c.promotion() == wanted)) .findFirst().orElse(null); if (m == null) return Result.ILLEGAL_MOVE; play(m, p); return Result.OK; }
// A move is legal if the pieces' rules allow it and the mover's own king isn't attacked // after it. List<Move> legalMoves() { List<Move> out = new ArrayList<>(); for (Move m : candidates()) if (safeAfter(m)) out.add(m); return out; }
boolean inCheck() { return attacked(board, board.kingOf(turn), turn.other()); }
State state() { return state; }
Color turn() { return turn; }
int ply() { return ply; }
// The side that gave checkmate. null while playing, and after a stalemate. Color winner() { return state == State.CHECKMATE ? turn.other() : null; }
Piece at(String square) { return board.at(Square.of(square)); }
// Plays a move already known to be legal, then updates what castling and en passant // remember. private void play(Move m, Piece p) { apply(board, m); if (p.kind() == Kind.KING) castlingRooks.removeIf(r -> r.rank() == m.from().rank()); castlingRooks.remove(m.from()); castlingRooks.remove(m.to()); boolean doublePush = p.kind() == Kind.PAWN && Math.abs(m.to().rank() - m.from().rank()) == 2; enPassant = doublePush ? new Square(m.from().file(), (m.from().rank() + m.to().rank()) / 2) : null; turn = turn.other(); ply++; updateState(); }
// Every move the pieces' rules allow, plus the three that depend on the game's history rather // than the board: en passant, castling, and promotion's choice of piece. private List<Move> candidates() { List<Move> out = new ArrayList<>(); int lastRank = turn == Color.WHITE ? 7 : 0; for (Square f : board.squares()) { Piece p = board.at(f); if (p == null || p.color() != turn) continue; MoveRule rule = Rules.BY_KIND.get(p.kind()); for (Square t : rule.targets(board, f)) { if (p.kind() == Kind.PAWN && t.rank() == lastRank) { for (Kind k : PROMOTIONS) out.add(new Move(f, t, k)); } else { out.add(new Move(f, t, null)); } } if (p.kind() == Kind.PAWN && enPassant != null && rule.attacks(board, f).contains(enPassant)) { out.add(new Move(f, enPassant, null)); } } addCastling(out); return out; }
// The king moves two squares toward a rook that can still castle, and the rook jumps over it. // Not out of check, not through an attacked square, and only with every square between empty. // The square the king lands on is checked by safeAfter like any other move. private void addCastling(List<Move> out) { if (inCheck()) return; Square king = new Square(4, turn == Color.WHITE ? 0 : 7); for (Square rook : castlingRooks) { if (rook.rank() != king.rank()) continue; int dir = rook.file() > king.file() ? 1 : -1; boolean empty = true; for (Square s = king.plus(dir, 0); !s.equals(rook); s = s.plus(dir, 0)) empty &= board.at(s) == null; if (empty && !attacked(board, king.plus(dir, 0), turn.other())) out.add(new Move(king, king.plus(2 * dir, 0), null)); } }
// Play the move on a copy and look: is our king attacked? It catches pins and discovered // checks with no special code. private boolean safeAfter(Move m) { Board after = board.copy(); apply(after, m); return !attacked(after, after.kingOf(turn), turn.other()); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private void updateState() { if (legalMoves().isEmpty()) state = inCheck() ? State.CHECKMATE : State.STALEMATE; }
// Does any piece of color `by` threaten target? Static, so it can test a copied board too. static boolean attacked(Board b, Square target, Color by) { for (Square s : b.squares()) { Piece p = b.at(s); if (p != null && p.color() == by && Rules.BY_KIND.get(p.kind()).attacks(b, s).contains(target)) return true; } return false; }
// The special moves are recognised from the board: a pawn moving diagonally to an empty // square is en passant, and a king moving two files is castling. static void apply(Board b, Move m) { Piece p = b.at(m.from()); if (p.kind() == Kind.PAWN && m.from().file() != m.to().file() && b.at(m.to()) == null) { b.put(new Square(m.to().file(), m.from().rank()), null); } if (p.kind() == Kind.KING && Math.abs(m.to().file() - m.from().file()) == 2) { int dir = Integer.signum(m.to().file() - m.from().file()); Square rook = new Square(dir > 0 ? 7 : 0, m.from().rank()); b.put(m.from().plus(dir, 0), b.at(rook)); b.put(rook, null); } b.put(m.to(), m.promotion() == null ? p : new Piece(m.promotion(), p.color())); b.put(m.from(), null); }}
// How one kind of piece moves: where the piece on from can go, ignoring check. Adding a piece,// or a fairy-chess variant, is a new rule rather than a longer switch.interface MoveRule { List<Square> targets(Board board, Square from);
// The squares this piece threatens. The same as its targets for every piece but the pawn, // which moves straight and captures diagonally. default List<Square> attacks(Board board, Square from) { return targets(board, from); }}
// Steps in each direction, once (king, knight) or repeatedly until something is in the way// (rook, bishop, queen). An enemy piece in the way can be captured; an own piece can't.record Step(int[][] directions, boolean slides) implements MoveRule { public List<Square> targets(Board board, Square from) { Color own = board.at(from).color(); List<Square> out = new ArrayList<>(); for (int[] d : directions) { for (Square s = from.plus(d[0], d[1]); s.onBoard(); s = s.plus(d[0], d[1])) { Piece p = board.at(s); if (p == null || p.color() != own) out.add(s); if (p != null || !slides) break; } } return out; }}
// The pawn is the odd one out. It moves forward but captures diagonally, may move two squares// from its start, and only goes one way. So it gets its own rule.class PawnRule implements MoveRule { public List<Square> targets(Board board, Square from) { Color own = board.at(from).color(); int forward = own == Color.WHITE ? 1 : -1; List<Square> out = new ArrayList<>(); Square one = from.plus(0, forward); if (one.onBoard() && board.at(one) == null) { out.add(one); Square two = one.plus(0, forward); boolean onStart = from.rank() == (own == Color.WHITE ? 1 : 6); if (onStart && board.at(two) == null) out.add(two); } for (Square s : attacks(board, from)) { Piece p = board.at(s); if (p != null && p.color() != own) out.add(s); } return out; }
public List<Square> attacks(Board board, Square from) { int forward = board.at(from).color() == Color.WHITE ? 1 : -1; return Stream.of(from.plus(-1, forward), from.plus(1, forward)).filter(Square::onBoard).toList(); }}
// The one place that says how each kind of piece moves. Directions are {file, rank} steps.final class Rules { 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}}; static final int[][] KNIGHT = {{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}};
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and // the king and knight are steppers with different directions. static final Map<Kind, MoveRule> BY_KIND = Map.of( Kind.KING, new Step(ALL, false), Kind.QUEEN, new Step(ALL, true), Kind.ROOK, new Step(STRAIGHT, true), Kind.BISHOP, new Step(DIAGONAL, true), Kind.KNIGHT, new Step(KNIGHT, false), Kind.PAWN, new PawnRule());}
// Whether the game is still on. Checkmate and stalemate are both final.enum State { PLAYING, CHECKMATE, STALEMATE }
// promotion is the piece a pawn becomes on the last rank, and null for every other move.record Move(Square from, Square to, Kind promotion) {}
// A game played online. Each player's client sends a move with the ply it was made at, the// number of moves it had seen. A move made against an old position, or the same move sent// twice because a request timed out and was retried, is STALE and changes nothing. The lock// makes checking the ply and playing the move one step, so two requests for the same ply can't// both pass the check.class SharedGame { private final Game game;
SharedGame(Game game) { this.game = game; }
synchronized Result submit(Color player, int expectedPly, String from, String to, Kind promotion) { if (expectedPly != game.ply()) return Result.STALE; if (player != game.turn()) return Result.NOT_YOUR_TURN; return game.move(from, to, promotion); }
// A client reads this before it moves, so it can send the ply it has seen. synchronized int ply() { return game.ply(); }
synchronized State state() { return game.state(); }}# The complete design, built from every stage on this page.
from __future__ import annotations
import threadingfrom dataclasses import dataclassfrom enum import Enum, autofrom typing import Protocol
# The two sides. other() says whose turn comes next.class Color(Enum): WHITE = auto() BLACK = auto()
def other(self) -> Color: return Color.BLACK if self is Color.WHITE else Color.WHITE
# The six kinds of piece. How each one moves lives in the rules, not here.class Kind(Enum): KING = auto() QUEEN = auto() ROOK = auto() BISHOP = auto() KNIGHT = auto() PAWN = auto()
# A piece is only its kind and color. It does not store its square: the board does, so# moving a piece changes one place, not two.@dataclass(frozen=True)class Piece: kind: Kind color: Color
# A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7).@dataclass(frozen=True)class Square: file: int rank: int
@staticmethod def of(name: str) -> Square: return Square(ord(name[0]) - ord("a"), int(name[1]) - 1)
def on_board(self) -> bool: return 0 <= self.file < 8 and 0 <= self.rank < 8
def plus(self, df: int, dr: int) -> Square: return Square(self.file + df, self.rank + dr)
def __str__(self) -> str: return chr(ord("a") + self.file) + str(self.rank + 1)
BACK_RANK = [Kind.ROOK, Kind.KNIGHT, Kind.BISHOP, Kind.QUEEN, Kind.KING, Kind.BISHOP, Kind.KNIGHT, Kind.ROOK]
# The 64 squares and what stands on each. It knows nothing about the rules, which makes it# cheap to copy when the game wants to test a move.class Board: def __init__(self): # [rank][file]; None is an empty square self._cells: list[list[Piece | None]] = [[None] * 8 for _ in range(8)]
def at(self, s: Square) -> Piece | None: return self._cells[s.rank][s.file]
def put(self, s: Square, p: Piece | None) -> None: self._cells[s.rank][s.file] = p
# Trying a move on a copy is how the game asks "would my king be safe?". Pieces are # immutable, so copying the rows is enough. def copy(self) -> Board: b = Board() b._cells = [row[:] for row in self._cells] return b
# All 64 squares, so callers loop over squares, not list indexes. def squares(self) -> list[Square]: return [Square(f, r) for r in range(8) for f in range(8)]
# Every check test starts from the king's square. def king_of(self, color: Color) -> Square: for s in self.squares(): if self.at(s) == Piece(Kind.KING, color): return s raise ValueError(f"no {color} king")
# The starting position: white on ranks 1 and 2, black on ranks 7 and 8. @staticmethod def standard() -> Board: b = Board() for f, kind in enumerate(BACK_RANK): b.put(Square(f, 0), Piece(kind, Color.WHITE)) b.put(Square(f, 1), Piece(Kind.PAWN, Color.WHITE)) b.put(Square(f, 6), Piece(Kind.PAWN, Color.BLACK)) b.put(Square(f, 7), Piece(kind, Color.BLACK)) return b
# What a move request gets back. STALE is for online play: the move was made against an# old position.class Result(Enum): OK = auto() NO_PIECE = auto() NOT_YOUR_TURN = auto() ILLEGAL_MOVE = auto() GAME_OVER = auto() STALE = auto()
# One game of chess. It owns the board, plus what the board alone can't tell you: whose turn# it is, which rooks may still castle, and where en passant is possible.class Game: # Any position, for puzzles and tests. A king and rook on their starting squares may castle. def __init__(self, board: Board | None = None, turn: Color = Color.WHITE): self._board = board or Board.standard() self._turn = turn self._state = State.PLAYING self._ply = 0 # moves made so far, by both sides # The rooks that can still castle, by square. A king's move removes both of its side's, # a rook moving or being captured removes its own. That is everything castling remembers. self._castling_rooks: set[Square] = set() # The square a pawn skipped over on the last move, if it moved two: an enemy pawn may # capture it there, on this move only. self._en_passant: Square | None = None for c in Color: rank = 0 if c is Color.WHITE else 7 if self._board.at(Square(4, rank)) != Piece(Kind.KING, c): continue for file in (0, 7): if self._board.at(Square(file, rank)) == Piece(Kind.ROOK, c): self._castling_rooks.add(Square(file, rank)) self._update_state()
# A pawn reaching the last rank becomes a queen unless another piece is asked for. def move(self, frm: str, to: str, promotion: Kind | None = None) -> Result: if self._state is not State.PLAYING: return Result.GAME_OVER f, t = Square.of(frm), Square.of(to) p = self._board.at(f) if p is None: return Result.NO_PIECE if p.color is not self._turn: return Result.NOT_YOUR_TURN wanted = promotion or Kind.QUEEN m = next( (c for c in self.legal_moves() if c.frm == f and c.to == t and c.promotion in (None, wanted)), None, ) if m is None: return Result.ILLEGAL_MOVE self._play(m, p) return Result.OK
# A move is legal if the pieces' rules allow it and the mover's own king isn't # attacked after it. def legal_moves(self) -> list[Move]: return [m for m in self._candidates() if self._safe_after(m)]
def in_check(self) -> bool: return attacked(self._board, self._board.king_of(self._turn), self._turn.other())
@property def state(self) -> State: return self._state
@property def turn(self) -> Color: return self._turn
@property def ply(self) -> int: return self._ply
# The side that gave checkmate. None while playing, and after a stalemate. def winner(self) -> Color | None: return self._turn.other() if self._state is State.CHECKMATE else None
def at(self, square: str) -> Piece | None: return self._board.at(Square.of(square))
# Plays a move already known to be legal, then updates what castling and en passant # remember. def _play(self, m: Move, p: Piece) -> None: apply(self._board, m) if p.kind is Kind.KING: self._castling_rooks = {r for r in self._castling_rooks if r.rank != m.frm.rank} self._castling_rooks -= {m.frm, m.to} double_push = p.kind is Kind.PAWN and abs(m.to.rank - m.frm.rank) == 2 self._en_passant = Square(m.frm.file, (m.frm.rank + m.to.rank) // 2) if double_push else None self._turn = self._turn.other() self._ply += 1 self._update_state()
# Every move the pieces' rules allow, plus the three that depend on the game's history # rather than the board: en passant, castling, and promotion's choice of piece. def _candidates(self) -> list[Move]: out = [] last_rank = 7 if self._turn is Color.WHITE else 0 for f in self._board.squares(): p = self._board.at(f) if p is None or p.color is not self._turn: continue rule = RULES[p.kind] for t in rule.targets(self._board, f): if p.kind is Kind.PAWN and t.rank == last_rank: out.extend(Move(f, t, k) for k in PROMOTIONS) else: out.append(Move(f, t)) if p.kind is Kind.PAWN and self._en_passant in rule.attacks(self._board, f): out.append(Move(f, self._en_passant)) self._add_castling(out) return out
# The king moves two squares toward a rook that can still castle, and the rook jumps # over it. Not out of check, not through an attacked square, and only with every square # between empty. The square the king lands on is checked by _safe_after like any move. def _add_castling(self, out: list[Move]) -> None: if self.in_check(): return king = Square(4, 0 if self._turn is Color.WHITE else 7) for rook in self._castling_rooks: if rook.rank != king.rank: continue d = 1 if rook.file > king.file else -1 between = range(min(king.file, rook.file) + 1, max(king.file, rook.file)) empty = all(self._board.at(Square(f, king.rank)) is None for f in between) if empty and not attacked(self._board, king.plus(d, 0), self._turn.other()): out.append(Move(king, king.plus(2 * d, 0)))
# Play the move on a copy and look: is our king attacked? It catches pins and # discovered checks with no special code. def _safe_after(self, m: Move) -> bool: after = self._board.copy() apply(after, m) return not attacked(after, after.king_of(self._turn), self._turn.other())
# No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. def _update_state(self) -> None: if not self.legal_moves(): self._state = State.CHECKMATE if self.in_check() else State.STALEMATE
# How one kind of piece moves: where the piece on frm can go, ignoring check. Adding a# piece, or a fairy-chess variant, is a new rule rather than a longer match.class MoveRule(Protocol): def targets(self, board: Board, frm: Square) -> list[Square]: ...
# The squares this piece threatens. The same as its targets for every piece but the # pawn, which moves straight and captures diagonally. def attacks(self, board: Board, frm: Square) -> list[Square]: ...
# Steps in each direction, once (king, knight) or repeatedly until something is in the way# (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.@dataclass(frozen=True)class Step: directions: tuple[tuple[int, int], ...] slides: bool
def targets(self, board: Board, frm: Square) -> list[Square]: own = board.at(frm).color out = [] for df, dr in self.directions: s = frm.plus(df, dr) while s.on_board(): p = board.at(s) if p is None or p.color is not own: out.append(s) if p is not None or not self.slides: break s = s.plus(df, dr) return out
def attacks(self, board: Board, frm: Square) -> list[Square]: return self.targets(board, frm)
# The pawn is the odd one out. It moves forward but captures diagonally, may move two# squares from its start, and only goes one way. So it gets its own rule.class PawnRule: def targets(self, board: Board, frm: Square) -> list[Square]: own = board.at(frm).color forward = 1 if own is Color.WHITE else -1 out = [] one = frm.plus(0, forward) if one.on_board() and board.at(one) is None: out.append(one) two = one.plus(0, forward) on_start = frm.rank == (1 if own is Color.WHITE else 6) if on_start and board.at(two) is None: out.append(two) for s in self.attacks(board, frm): p = board.at(s) if p is not None and p.color is not own: out.append(s) return out
def attacks(self, board: Board, frm: Square) -> list[Square]: forward = 1 if board.at(frm).color is Color.WHITE else -1 return [s for s in (frm.plus(-1, forward), frm.plus(1, forward)) if s.on_board()]
# Directions as (file, rank) steps.STRAIGHT = ((1, 0), (-1, 0), (0, 1), (0, -1))
DIAGONAL = ((1, 1), (1, -1), (-1, 1), (-1, -1))
KNIGHT_JUMPS = ((1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2))
# Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and# the king and knight are steppers with different directions.RULES: dict[Kind, MoveRule] = { Kind.KING: Step(STRAIGHT + DIAGONAL, slides=False), Kind.QUEEN: Step(STRAIGHT + DIAGONAL, slides=True), Kind.ROOK: Step(STRAIGHT, slides=True), Kind.BISHOP: Step(DIAGONAL, slides=True), Kind.KNIGHT: Step(KNIGHT_JUMPS, slides=False), Kind.PAWN: PawnRule(),}
# Whether the game is still on. Checkmate and stalemate are both final.class State(Enum): PLAYING = auto() CHECKMATE = auto() STALEMATE = auto()
@dataclass(frozen=True)class Move: frm: Square to: Square promotion: Kind | None = None # the piece a pawn becomes on the last rank
# Does any piece of color `by` threaten target? A plain function, so it can test a copied# board too.def attacked(b: Board, target: Square, by: Color) -> bool: for s in b.squares(): p = b.at(s) if p is not None and p.color is by and target in RULES[p.kind].attacks(b, s): return True return False
# The special moves are recognised from the board: a pawn moving diagonally to an empty# square is en passant, and a king moving two files is castling.def apply(b: Board, m: Move) -> None: p = b.at(m.frm) if p.kind is Kind.PAWN and m.frm.file != m.to.file and b.at(m.to) is None: b.put(Square(m.to.file, m.frm.rank), None) if p.kind is Kind.KING and abs(m.to.file - m.frm.file) == 2: d = 1 if m.to.file > m.frm.file else -1 rook = Square(7 if d > 0 else 0, m.frm.rank) b.put(m.frm.plus(d, 0), b.at(rook)) b.put(rook, None) b.put(m.to, p if m.promotion is None else Piece(m.promotion, p.color)) b.put(m.frm, None)
# What a pawn may become: never a king, and it can't stay a pawn.PROMOTIONS = (Kind.QUEEN, Kind.ROOK, Kind.BISHOP, Kind.KNIGHT)
# A game played online. Each player's client sends a move with the ply it was made at,# the number of moves it had seen. A move made against an old position, or the same move# sent twice because a request timed out and was retried, is STALE and changes nothing. The# lock makes checking the ply and playing the move one step, so two requests for the same# ply can't both pass the check.class SharedGame: def __init__(self, game: Game): self._game = game self._lock = threading.Lock()
def submit(self, player: Color, expected_ply: int, frm: str, to: str, promotion: Kind | None = None) -> Result: with self._lock: if expected_ply != self._game.ply: return Result.STALE if player is not self._game.turn: return Result.NOT_YOUR_TURN return self._game.move(frm, to, promotion)
# A client reads this before it moves, so it can send the ply it has seen. @property def ply(self) -> int: with self._lock: return self._game.ply
@property def state(self) -> State: with self._lock: return self._game.state// The complete design, built from every stage on this page.
// The two sides. Other() says whose turn comes next.public enum Color { White, Black }
// The six kinds of piece. How each one moves lives in the rules, not here.public enum Kind { King, Queen, Rook, Bishop, Knight, Pawn }
// A C# enum can't have methods, so Other() lives here as an extension.public static class ColorExtensions{ public static Color Other(this Color c) => c == Color.White ? Color.Black : Color.White;}
// A piece is only its kind and color. It does not store its square: the board does, so moving// a piece changes one place, not two.public sealed record Piece(Kind Kind, Color Color);
// A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7). A record struct compares by// value, so squares can be looked up in lists and sets directly.public readonly record struct Square(int File, int Rank){ public static Square Of(string name) => new(name[0] - 'a', name[1] - '1'); public bool OnBoard => File is >= 0 and < 8 && Rank is >= 0 and < 8; public Square Plus(int df, int dr) => new(File + df, Rank + dr); public override string ToString() => $"{(char)('a' + File)}{Rank + 1}";}
// The 64 squares and what stands on each. It knows nothing about the rules, which makes it// cheap to copy when the game wants to test a move.public sealed class Board{ private readonly Piece?[,] _cells = new Piece?[8, 8]; // [rank, file]; null is an empty square
public Piece? At(Square s) => _cells[s.Rank, s.File]; public void Put(Square s, Piece? p) => _cells[s.Rank, s.File] = p;
// Trying a move on a copy is how the game asks "would my king be safe?". Pieces are // immutable records, so copying the 64 references is enough. public Board Copy() { var b = new Board(); Array.Copy(_cells, b._cells, _cells.Length); return b; }
// All 64 squares, so callers loop over squares, not array indexes. public IEnumerable<Square> Squares() { for (int r = 0; r < 8; r++) for (int f = 0; f < 8; f++) yield return new Square(f, r); }
// Every check test starts from the king's square. public Square KingOf(Color color) => Squares().First(s => At(s) == new Piece(Kind.King, color));
// The starting position: white on ranks 1 and 2, black on ranks 7 and 8. public static Board Standard() { var b = new Board(); Kind[] backRank = { Kind.Rook, Kind.Knight, Kind.Bishop, Kind.Queen, Kind.King, Kind.Bishop, Kind.Knight, Kind.Rook }; for (int f = 0; f < 8; f++) { b.Put(new Square(f, 0), new Piece(backRank[f], Color.White)); b.Put(new Square(f, 1), new Piece(Kind.Pawn, Color.White)); b.Put(new Square(f, 6), new Piece(Kind.Pawn, Color.Black)); b.Put(new Square(f, 7), new Piece(backRank[f], Color.Black)); } return b; }}
// What a move request gets back. Stale is for online play: the move was made against an old// position.public enum Result { Ok, NoPiece, NotYourTurn, IllegalMove, GameOver, Stale }
// One game of chess. It owns the board, plus what the board alone can't tell you: whose turn it// is, which rooks may still castle, and where en passant is possible.public sealed class Game{ // What a pawn may become: never a king, and it can't stay a pawn. private static readonly Kind[] Promotions = { Kind.Queen, Kind.Rook, Kind.Bishop, Kind.Knight };
private readonly Board _board; private Color _turn; // The rooks that can still castle, by square. A king's move removes both of its side's; a // rook moving or being captured removes its own. That is everything castling remembers. private readonly HashSet<Square> _castlingRooks = new(); // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may // capture it there, on this move only. private Square? _enPassant;
public Game() : this(Board.Standard(), Color.White) { }
// Any position, for puzzles and tests. A king and rook on their starting squares may castle. public Game(Board board, Color turn) { _board = board; _turn = turn; foreach (var c in new[] { Color.White, Color.Black }) { int rank = c == Color.White ? 0 : 7; if (board.At(new Square(4, rank)) != new Piece(Kind.King, c)) continue; foreach (int file in new[] { 0, 7 }) { var corner = new Square(file, rank); if (board.At(corner) == new Piece(Kind.Rook, c)) _castlingRooks.Add(corner); } } UpdateState(); }
public State State { get; private set; } = State.Playing; public Color Turn => _turn; public int Ply { get; private set; } // moves made so far, by both sides // The side that gave checkmate. null while playing, and after a stalemate. public Color? Winner => State == State.Checkmate ? _turn.Other() : null; public bool InCheck => Attacked(_board, _board.KingOf(_turn), _turn.Other()); public Piece? At(string square) => _board.At(Square.Of(square));
// A pawn reaching the last rank becomes a queen unless another piece is asked for. public Result Move(string from, string to, Kind? promotion = null) { if (State != State.Playing) return Result.GameOver; var f = Square.Of(from); var t = Square.Of(to); var p = _board.At(f); if (p is null) return Result.NoPiece; if (p.Color != _turn) return Result.NotYourTurn; var wanted = promotion ?? Kind.Queen; var found = LegalMoves().Where(c => c.From == f && c.To == t && (c.Promotion is null || c.Promotion == wanted)); if (!found.Any()) return Result.IllegalMove; Play(found.First(), p); return Result.Ok; }
// A move is legal if the pieces' rules allow it and the mover's own king isn't attacked // after it. public List<Move> LegalMoves() => Candidates().Where(SafeAfter).ToList();
// Plays a move already known to be legal, then updates what castling and en passant // remember. private void Play(Move m, Piece p) { Apply(_board, m); if (p.Kind == Kind.King) _castlingRooks.RemoveWhere(r => r.Rank == m.From.Rank); _castlingRooks.Remove(m.From); _castlingRooks.Remove(m.To); bool doublePush = p.Kind == Kind.Pawn && Math.Abs(m.To.Rank - m.From.Rank) == 2; _enPassant = doublePush ? new Square(m.From.File, (m.From.Rank + m.To.Rank) / 2) : null; _turn = _turn.Other(); Ply++; UpdateState(); }
// Every move the pieces' rules allow, plus the three that depend on the game's history rather // than the board: en passant, castling, and promotion's choice of piece. private List<Move> Candidates() { var output = new List<Move>(); int lastRank = _turn == Color.White ? 7 : 0; foreach (var f in _board.Squares()) { var p = _board.At(f); if (p is null || p.Color != _turn) continue; var rule = Rules.ByKind[p.Kind]; foreach (var t in rule.Targets(_board, f)) { if (p.Kind == Kind.Pawn && t.Rank == lastRank) output.AddRange(Promotions.Select(k => new Move(f, t, k))); else output.Add(new Move(f, t)); } if (p.Kind == Kind.Pawn && _enPassant is { } ep && rule.Attacks(_board, f).Contains(ep)) output.Add(new Move(f, ep)); } AddCastling(output); return output; }
// The king moves two squares toward a rook that can still castle, and the rook jumps over it. // Not out of check, not through an attacked square, and only with every square between empty. // The square the king lands on is checked by SafeAfter like any other move. private void AddCastling(List<Move> output) { if (InCheck) return; var king = new Square(4, _turn == Color.White ? 0 : 7); foreach (var rook in _castlingRooks) { if (rook.Rank != king.Rank) continue; int dir = Math.Sign(rook.File - king.File); bool empty = true; for (var s = king.Plus(dir, 0); s != rook; s = s.Plus(dir, 0)) empty &= _board.At(s) is null; if (empty && !Attacked(_board, king.Plus(dir, 0), _turn.Other())) output.Add(new Move(king, king.Plus(2 * dir, 0))); } }
// Play the move on a copy and look: is our king attacked? It catches pins and discovered // checks with no special code. private bool SafeAfter(Move m) { var after = _board.Copy(); Apply(after, m); return !Attacked(after, after.KingOf(_turn), _turn.Other()); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private void UpdateState() { if (LegalMoves().Count == 0) State = InCheck ? State.Checkmate : State.Stalemate; }
// Does any piece of color `by` threaten target? Static, so it can test a copied board too. public static bool Attacked(Board b, Square target, Color by) => b.Squares().Any(s => b.At(s) is { } p && p.Color == by && Rules.ByKind[p.Kind].Attacks(b, s).Contains(target));
// The special moves are recognised from the board: a pawn moving diagonally to an empty // square is en passant, and a king moving two files is castling. public static void Apply(Board b, Move m) { var p = b.At(m.From)!; if (p.Kind == Kind.Pawn && m.From.File != m.To.File && b.At(m.To) is null) b.Put(new Square(m.To.File, m.From.Rank), null); if (p.Kind == Kind.King && Math.Abs(m.To.File - m.From.File) == 2) { int dir = Math.Sign(m.To.File - m.From.File); var rook = new Square(dir > 0 ? 7 : 0, m.From.Rank); b.Put(m.From.Plus(dir, 0), b.At(rook)); b.Put(rook, null); } b.Put(m.To, m.Promotion is { } k ? new Piece(k, p.Color) : p); b.Put(m.From, null); }}
// How one kind of piece moves: where the piece on from can go, ignoring check. Adding a piece,// or a fairy-chess variant, is a new rule rather than a longer switch.public interface IMoveRule{ IReadOnlyList<Square> Targets(Board board, Square from);
// The squares this piece threatens. The same as its targets for every piece but the pawn, // which moves straight and captures diagonally. IReadOnlyList<Square> Attacks(Board board, Square from) => Targets(board, from);}
// Steps in each direction, once (king, knight) or repeatedly until something is in the way// (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.public sealed record Step((int Df, int Dr)[] Directions, bool Slides) : IMoveRule{ public IReadOnlyList<Square> Targets(Board board, Square from) { var own = board.At(from)!.Color; var output = new List<Square>(); foreach (var (df, dr) in Directions) { for (var s = from.Plus(df, dr); s.OnBoard; s = s.Plus(df, dr)) { var p = board.At(s); if (p is null || p.Color != own) output.Add(s); if (p is not null || !Slides) break; } } return output; }}
// The pawn is the odd one out. It moves forward but captures diagonally, may move two squares// from its start, and only goes one way. So it gets its own rule.public sealed class PawnRule : IMoveRule{ public IReadOnlyList<Square> Targets(Board board, Square from) { var own = board.At(from)!.Color; int forward = own == Color.White ? 1 : -1; var output = new List<Square>(); var one = from.Plus(0, forward); if (one.OnBoard && board.At(one) is null) { output.Add(one); var two = one.Plus(0, forward); bool onStart = from.Rank == (own == Color.White ? 1 : 6); if (onStart && board.At(two) is null) output.Add(two); } foreach (var s in Attacks(board, from)) { var p = board.At(s); if (p is not null && p.Color != own) output.Add(s); } return output; }
public IReadOnlyList<Square> Attacks(Board board, Square from) { int forward = board.At(from)!.Color == Color.White ? 1 : -1; return new[] { from.Plus(-1, forward), from.Plus(1, forward) }.Where(s => s.OnBoard).ToList(); }}
// The one place that says how each kind of piece moves. Directions are (file, rank) steps.public static class Rules{ private static readonly (int, int)[] Straight = { (1, 0), (-1, 0), (0, 1), (0, -1) }; private static readonly (int, int)[] Diagonal = { (1, 1), (1, -1), (-1, 1), (-1, -1) }; private static readonly (int, int)[] All = Straight.Concat(Diagonal).ToArray(); private static readonly (int, int)[] KnightJumps = { (1, 2), (2, 1), (2, -1), (1, -2), (-1, -2), (-2, -1), (-2, 1), (-1, 2) };
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and // the king and knight are steppers with different directions. public static readonly IReadOnlyDictionary<Kind, IMoveRule> ByKind = new Dictionary<Kind, IMoveRule> { [Kind.King] = new Step(All, false), [Kind.Queen] = new Step(All, true), [Kind.Rook] = new Step(Straight, true), [Kind.Bishop] = new Step(Diagonal, true), [Kind.Knight] = new Step(KnightJumps, false), [Kind.Pawn] = new PawnRule(), };}
// Whether the game is still on. Checkmate and stalemate are both final.public enum State { Playing, Checkmate, Stalemate }
// Promotion is the piece a pawn becomes on the last rank, and null for every other move.public readonly record struct Move(Square From, Square To, Kind? Promotion = null);
// A game played online. Each player's client sends a move with the ply it was made at, the// number of moves it had seen. A move made against an old position, or the same move sent// twice because a request timed out and was retried, is Stale and changes nothing. The lock// makes checking the ply and playing the move one step, so two requests for the same ply can't// both pass the check.public sealed class SharedGame{ private readonly object _lock = new(); private readonly Game _game;
public SharedGame(Game game) => _game = game;
public Result Submit(Color player, int expectedPly, string from, string to, Kind? promotion = null) { lock (_lock) { if (expectedPly != _game.Ply) return Result.Stale; if (player != _game.Turn) return Result.NotYourTurn; return _game.Move(from, to, promotion); } }
// A client reads this before it moves, so it can send the ply it has seen. public int Ply { get { lock (_lock) return _game.Ply; } } public State State { get { lock (_lock) return _game.State; } }}// The complete design, built from every stage on this page.
package chess
import ( "cmp" "fmt" "maps" "slices" "sync")
// Color is one of the two sides. Other says whose turn comes next.type Color int
const ( White Color = iota Black)
func (c Color) Other() Color { return 1 - c }
func (c Color) String() string { return [...]string{"white", "black"}[c] }
// Kind starts at 1 so the zero Piece is an empty square.type Kind int
const ( King Kind = iota + 1 Queen Rook Bishop Knight Pawn)
// Piece is only a kind and a color. It does not store its square: the board does, so moving a// piece changes one place, not two.type Piece struct { Kind Kind Color Color}
func (p Piece) Empty() bool { return p.Kind == 0 }
// Square is a square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7).type Square struct{ File, Rank int }
func Sq(name string) Square { return Square{int(name[0] - 'a'), int(name[1] - '1')} }
func (s Square) OnBoard() bool { return s.File >= 0 && s.File < 8 && s.Rank >= 0 && s.Rank < 8 }
func (s Square) Plus(df, dr int) Square { return Square{s.File + df, s.Rank + dr} }
func (s Square) String() string { return fmt.Sprintf("%c%d", 'a'+s.File, s.Rank+1) }
// Board is an array of values, not pointers, so assigning a Board copies it.type Board [8][8]Piece
func (b *Board) At(s Square) Piece { return b[s.Rank][s.File] }
func (b *Board) Put(s Square, p Piece) { b[s.Rank][s.File] = p }
var backRank = [8]Kind{Rook, Knight, Bishop, Queen, King, Bishop, Knight, Rook}
// StandardBoard is the starting position: white on ranks 1 and 2, black on ranks 7 and 8.func StandardBoard() Board { var b Board for f, k := range backRank { b.Put(Square{f, 0}, Piece{k, White}) b.Put(Square{f, 1}, Piece{Pawn, White}) b.Put(Square{f, 6}, Piece{Pawn, Black}) b.Put(Square{f, 7}, Piece{k, Black}) } return b}
// Result is what a move request gets back, so a client can show why a move failed.type Result int
func abs(x int) int { return max(x, -x) }
func sign(x int) int { return cmp.Compare(x, 0) }
// MoveRule is how one kind of piece moves: where the piece on from can go, ignoring check.// Attacks are the squares it threatens, the same as its targets for every piece but the pawn,// which moves straight and captures diagonally. Adding a piece, or a fairy-chess variant, is a// new rule rather than a longer switch.type MoveRule interface { Targets(b *Board, from Square) []Square Attacks(b *Board, from Square) []Square}
// Step steps in each direction, once (king, knight) or repeatedly until something is in the// way (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.type Step struct { Dirs [][2]int Slides bool}
func (st Step) Targets(b *Board, from Square) []Square { own := b.At(from).Color var out []Square for _, d := range st.Dirs { for s := from.Plus(d[0], d[1]); s.OnBoard(); s = s.Plus(d[0], d[1]) { p := b.At(s) if p.Empty() || p.Color != own { out = append(out, s) } if !p.Empty() || !st.Slides { break } } } return out}
func (st Step) Attacks(b *Board, from Square) []Square { return st.Targets(b, from) }
// PawnRule is the odd one out. A pawn moves forward but captures diagonally, may move two// squares from its start, and only goes one way. So it gets its own rule.type PawnRule struct{}
func pawnForward(c Color) int { if c == White { return 1 } return -1}
func (PawnRule) Targets(b *Board, from Square) []Square { own := b.At(from).Color forward := pawnForward(own) var out []Square if one := from.Plus(0, forward); one.OnBoard() && b.At(one).Empty() { out = append(out, one) onStart := (own == White && from.Rank == 1) || (own == Black && from.Rank == 6) if two := one.Plus(0, forward); onStart && b.At(two).Empty() { out = append(out, two) } } for _, s := range (PawnRule{}).Attacks(b, from) { if p := b.At(s); !p.Empty() && p.Color != own { out = append(out, s) } } return out}
func (PawnRule) Attacks(b *Board, from Square) []Square { forward := pawnForward(b.At(from).Color) var out []Square for _, s := range []Square{from.Plus(-1, forward), from.Plus(1, forward)} { if s.OnBoard() { out = append(out, s) } } return out}
// Directions as {file, rank} steps.var ( straight = [][2]int{{1, 0}, {-1, 0}, {0, 1}, {0, -1}} diagonal = [][2]int{{1, 1}, {1, -1}, {-1, 1}, {-1, -1}} allDirs = slices.Concat(straight, diagonal) knightJumps = [][2]int{{1, 2}, {2, 1}, {2, -1}, {1, -2}, {-1, -2}, {-2, -1}, {-2, 1}, {-1, 2}})
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and// the king and knight are steppers with different directions.var Rules = map[Kind]MoveRule{ King: Step{allDirs, false}, Queen: Step{allDirs, true}, Rook: Step{straight, true}, Bishop: Step{diagonal, true}, Knight: Step{knightJumps, false}, Pawn: PawnRule{},}
// Squares lists all 64 squares, so callers loop over squares, not array indexes.func (b *Board) Squares() []Square { out := make([]Square, 0, 64) for r := range 8 { for f := range 8 { out = append(out, Square{f, r}) } } return out}
// KingOf finds a side's king. Every check test starts from its square.func (b *Board) KingOf(c Color) Square { for _, s := range b.Squares() { if b.At(s) == (Piece{King, c}) { return s } } panic("no " + c.String() + " king")}
// State is whether the game is still on. Checkmate and stalemate are both final.type State int
const ( Playing State = iota Checkmate Stalemate)
func NewGame() *Game { return NewGameFrom(StandardBoard(), White) }
// attacked reports whether any piece of color by threatens target. It is a plain function, so// it can test a copied board too.func attacked(b *Board, target Square, by Color) bool { for _, s := range b.Squares() { if p := b.At(s); !p.Empty() && p.Color == by && slices.Contains(Rules[p.Kind].Attacks(b, s), target) { return true } } return false}
// Move.Promotion is the piece a pawn becomes on the last rank, and zero for every other move.type Move struct { From, To Square Promotion Kind}
// promotions is what a pawn may become: never a king, and it can't stay a pawn.var promotions = []Kind{Queen, Rook, Bishop, Knight}
// Game is one game of chess. It owns the board, plus what the board alone can't tell you: whose// turn it is, which rooks may still castle, and where en passant is possible.type Game struct { board Board turn Color state State ply int // moves made so far, by both sides // The rooks that can still castle, by square. A king's move removes both of its side's, a // rook moving or being captured removes its own. That is everything castling remembers. castlingRooks map[Square]bool // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may // capture it there, on this move only. enPassant *Square}
// NewGameFrom starts from any position. A king and rook on their starting squares may castle.func NewGameFrom(b Board, turn Color) *Game { g := &Game{board: b, turn: turn, castlingRooks: map[Square]bool{}} for _, c := range []Color{White, Black} { rank := 0 if c == Black { rank = 7 } if b.At(Square{4, rank}) != (Piece{King, c}) { continue } for _, file := range []int{0, 7} { if b.At(Square{file, rank}) == (Piece{Rook, c}) { g.castlingRooks[Square{file, rank}] = true } } } g.updateState() return g}
func (g *Game) Move(from, to string) Result { return g.MovePromoting(from, to, 0) }
// MovePromoting makes a move, and a pawn reaching the last rank becomes the piece asked for,// or a queen if promotion is zero.func (g *Game) MovePromoting(from, to string, promotion Kind) Result { if g.state != Playing { return GameOver } f, t := Sq(from), Sq(to) p := g.board.At(f) if p.Empty() { return NoPiece } if p.Color != g.turn { return NotYourTurn } if promotion == 0 { promotion = Queen } for _, m := range g.LegalMoves() { if m.From == f && m.To == t && (m.Promotion == 0 || m.Promotion == promotion) { g.play(m, p) return OK } } return IllegalMove}
// LegalMoves are the moves the pieces' rules allow that leave the mover's own king unattacked.func (g *Game) LegalMoves() []Move { var out []Move for _, m := range g.candidates() { if g.safeAfter(m) { out = append(out, m) } } return out}
func (g *Game) InCheck() bool { return attacked(&g.board, g.board.KingOf(g.turn), g.turn.Other()) }
func (g *Game) State() State { return g.state }
func (g *Game) Turn() Color { return g.turn }
// Winner is the side that delivered checkmate, if the game ended that way.func (g *Game) Winner() (Color, bool) { return g.turn.Other(), g.state == Checkmate }
func (g *Game) At(square string) Piece { return g.board.At(Sq(square)) }
func (g *Game) Ply() int { return g.ply }
// play makes a move already known to be legal, then updates what castling and en passant// remember.func (g *Game) play(m Move, p Piece) { apply(&g.board, m) if p.Kind == King { maps.DeleteFunc(g.castlingRooks, func(r Square, _ bool) bool { return r.Rank == m.From.Rank }) } delete(g.castlingRooks, m.From) delete(g.castlingRooks, m.To) g.enPassant = nil if p.Kind == Pawn && abs(m.To.Rank-m.From.Rank) == 2 { g.enPassant = &Square{m.From.File, (m.From.Rank + m.To.Rank) / 2} } g.turn = g.turn.Other() g.ply++ g.updateState()}
// candidates are every move the pieces' rules allow, plus the three that depend on the game's// history rather than the board: en passant, castling, and promotion's choice of piece.func (g *Game) candidates() []Move { var out []Move lastRank := 7 if g.turn == Black { lastRank = 0 } for _, f := range g.board.Squares() { p := g.board.At(f) if p.Empty() || p.Color != g.turn { continue } rule := Rules[p.Kind] for _, t := range rule.Targets(&g.board, f) { if p.Kind == Pawn && t.Rank == lastRank { for _, k := range promotions { out = append(out, Move{f, t, k}) } } else { out = append(out, Move{f, t, 0}) } } if p.Kind == Pawn && g.enPassant != nil && slices.Contains(rule.Attacks(&g.board, f), *g.enPassant) { out = append(out, Move{f, *g.enPassant, 0}) } } return g.addCastling(out)}
// addCastling adds the king moving two squares toward a rook that can still castle. Not out of// check, not through an attacked square, and only with every square between empty. The square// the king lands on is checked by safeAfter like any other move.func (g *Game) addCastling(out []Move) []Move { if g.InCheck() { return out } king := Square{4, 0} if g.turn == Black { king = Square{4, 7} } for rook := range g.castlingRooks { if rook.Rank != king.Rank { continue } dir := sign(rook.File - king.File) empty := true for s := king.Plus(dir, 0); s != rook; s = s.Plus(dir, 0) { empty = empty && g.board.At(s).Empty() } if empty && !attacked(&g.board, king.Plus(dir, 0), g.turn.Other()) { out = append(out, Move{king, king.Plus(2*dir, 0), 0}) } } return out}
// safeAfter tries the move on a copy of the board, which is one assignment.func (g *Game) safeAfter(m Move) bool { after := g.board apply(&after, m) return !attacked(&after, after.KingOf(g.turn), g.turn.Other())}
// updateState ends the game when the side to move has no legal move: checkmate if its king is// attacked, stalemate (a draw) if not.func (g *Game) updateState() { if len(g.LegalMoves()) > 0 { return } if g.InCheck() { g.state = Checkmate } else { g.state = Stalemate }}
// apply recognises the special moves from the board: a pawn moving diagonally to an empty// square is en passant, and a king moving two files is castling.func apply(b *Board, m Move) { p := b.At(m.From) if p.Kind == Pawn && m.From.File != m.To.File && b.At(m.To).Empty() { b.Put(Square{m.To.File, m.From.Rank}, Piece{}) } if p.Kind == King && abs(m.To.File-m.From.File) == 2 { dir := sign(m.To.File - m.From.File) rook := Square{0, m.From.Rank} if dir > 0 { rook.File = 7 } b.Put(m.From.Plus(dir, 0), b.At(rook)) b.Put(rook, Piece{}) } if m.Promotion != 0 { p.Kind = m.Promotion } b.Put(m.To, p) b.Put(m.From, Piece{})}
const ( OK Result = iota NoPiece NotYourTurn IllegalMove GameOver Stale // online play: the move was made against an old position)
// SharedGame is a game played online. Each player's client sends a move with the ply it was// made at, the number of moves it had seen. A move made against an old position, or the same// move sent twice because a request timed out and was retried, is Stale and changes nothing.// The mutex makes checking the ply and playing the move one step, so two requests for the same// ply can't both pass the check.type SharedGame struct { mu sync.Mutex game *Game}
func NewSharedGame(g *Game) *SharedGame { return &SharedGame{game: g} }
func (s *SharedGame) Submit(player Color, expectedPly int, from, to string, promotion Kind) Result { s.mu.Lock() defer s.mu.Unlock() if expectedPly != s.game.Ply() { return Stale } if player != s.game.Turn() { return NotYourTurn } return s.game.MovePromoting(from, to, promotion)}
// Ply is read by a client before it moves, so it can send the ply it has seen.func (s *SharedGame) Ply() int { s.mu.Lock() defer s.mu.Unlock() return s.game.Ply()}
func (s *SharedGame) State() State { s.mu.Lock() defer s.mu.Unlock() return s.game.State()}// The complete design, built from every stage on this page.
// The two sides. other() says whose turn comes next.type Color = 'white' | 'black';
const other = (c: Color): Color => (c === 'white' ? 'black' : 'white');
// The six kinds of piece. How each one moves lives in the rules, not here.type Kind = 'king' | 'queen' | 'rook' | 'bishop' | 'knight' | 'pawn';
// A piece is only its kind and color. It does not store its square: the board does, so moving// a piece changes one place, not two.interface Piece { readonly kind: Kind; readonly color: Color;}
// Objects compare by reference in JavaScript, so two pieces are compared field by field.const samePiece = (a: Piece | null, b: Piece | null) => a === b || (a !== null && b !== null && a.kind === b.kind && a.color === b.color);
// A square by file (a to h as 0 to 7) and rank (1 to 8 as 0 to 7). Objects compare by// reference in JavaScript, so squares are compared with equals.class Square { constructor( readonly file: number, readonly rank: number, ) {}
static of(name: string): Square { return new Square(name.charCodeAt(0) - 97, Number(name[1]) - 1); }
onBoard(): boolean { return this.file >= 0 && this.file < 8 && this.rank >= 0 && this.rank < 8; }
plus(df: number, dr: number): Square { return new Square(this.file + df, this.rank + dr); }
equals(o: Square | null): boolean { return o !== null && o.file === this.file && o.rank === this.rank; }
toString(): string { return String.fromCharCode(97 + this.file) + (this.rank + 1); }}
const BACK_RANK: Kind[] = ['rook', 'knight', 'bishop', 'queen', 'king', 'bishop', 'knight', 'rook'];
// The 64 squares and what stands on each. It knows nothing about the rules, which makes it// cheap to copy when the game wants to test a move.class Board { // [rank][file]; null is an empty square protected cells: (Piece | null)[][] = Array.from({ length: 8 }, () => Array<Piece | null>(8).fill(null));
at(s: Square): Piece | null { return this.cells[s.rank][s.file]; }
put(s: Square, p: Piece | null): void { this.cells[s.rank][s.file] = p; }
// Trying a move on a copy is how the game asks "would my king be safe?". Pieces are never // changed in place, so copying the rows is enough. copy(): Board { const b = new Board(); b.cells = this.cells.map((row) => [...row]); return b; }
// All 64 squares, so callers loop over squares, not array indexes. squares(): Square[] { const out: Square[] = []; for (let r = 0; r < 8; r++) for (let f = 0; f < 8; f++) out.push(new Square(f, r)); return out; }
// Every check test starts from the king's square. kingOf(color: Color): Square { const s = this.squares().find((s) => samePiece(this.at(s), { kind: 'king', color })); if (!s) throw new Error(`no ${color} king`); return s; }
// The starting position: white on ranks 1 and 2, black on ranks 7 and 8. static standard(): Board { const b = new Board(); BACK_RANK.forEach((kind, f) => { b.put(new Square(f, 0), { kind, color: 'white' }); b.put(new Square(f, 1), { kind: 'pawn', color: 'white' }); b.put(new Square(f, 6), { kind: 'pawn', color: 'black' }); b.put(new Square(f, 7), { kind, color: 'black' }); }); return b; }}
// What a move request gets back. 'stale' is for online play: the move was made against an old// position.type Result = 'ok' | 'no-piece' | 'not-your-turn' | 'illegal-move' | 'game-over' | 'stale';
// One game of chess. It owns the board, plus what the board alone can't tell you: whose turn it// is, which rooks may still castle, and where en passant is possible.class Game { private board: Board; private turn: Color; private currentState: State = 'playing'; private moves = 0; // moves made so far, by both sides // The rooks that can still castle. A king's move removes both of its side's, a rook moving or // being captured removes its own. That is everything castling remembers. private castlingRooks: Square[] = []; // The square a pawn skipped over on the last move, if it moved two: an enemy pawn may capture // it there, on this move only. private enPassant: Square | null = null;
// Any position, for puzzles and tests. A king and rook on their starting squares may castle. constructor(board = Board.standard(), turn: Color = 'white') { this.board = board; this.turn = turn; for (const color of ['white', 'black'] as const) { const rank = color === 'white' ? 0 : 7; if (!samePiece(board.at(new Square(4, rank)), { kind: 'king', color })) continue; for (const corner of [new Square(0, rank), new Square(7, rank)]) { if (samePiece(board.at(corner), { kind: 'rook', color })) this.castlingRooks.push(corner); } } this.updateState(); }
// A pawn reaching the last rank becomes a queen unless another piece is asked for. move(from: string, to: string, promotion: Kind = 'queen'): Result { if (this.currentState !== 'playing') return 'game-over'; const f = Square.of(from); const t = Square.of(to); const p = this.board.at(f); if (p === null) return 'no-piece'; if (p.color !== this.turn) return 'not-your-turn'; const m = this.legalMoves().find( (c) => c.from.equals(f) && c.to.equals(t) && (c.promotion === undefined || c.promotion === promotion), ); if (!m) return 'illegal-move'; this.play(m, p); return 'ok'; }
// A move is legal if the pieces' rules allow it and the mover's own king isn't attacked after // it. legalMoves(): Move[] { return this.candidates().filter((m) => this.safeAfter(m)); }
inCheck(): boolean { return attacked(this.board, this.board.kingOf(this.turn), other(this.turn)); }
get state(): State { return this.currentState; }
get toMove(): Color { return this.turn; }
get ply(): number { return this.moves; }
// The side that gave checkmate. null while playing, and after a stalemate. winner(): Color | null { return this.currentState === 'checkmate' ? other(this.turn) : null; }
at(square: string): Piece | null { return this.board.at(Square.of(square)); }
// Plays a move already known to be legal, then updates what castling and en passant remember. private play(m: Move, p: Piece): void { apply(this.board, m); this.castlingRooks = this.castlingRooks.filter( (r) => !(p.kind === 'king' && r.rank === m.from.rank) && !r.equals(m.from) && !r.equals(m.to), ); const doublePush = p.kind === 'pawn' && Math.abs(m.to.rank - m.from.rank) === 2; this.enPassant = doublePush ? new Square(m.from.file, (m.from.rank + m.to.rank) / 2) : null; this.turn = other(this.turn); this.moves++; this.updateState(); }
// Every move the pieces' rules allow, plus the three that depend on the game's history rather // than the board: en passant, castling, and promotion's choice of piece. private candidates(): Move[] { const out: Move[] = []; const lastRank = this.turn === 'white' ? 7 : 0; for (const from of this.board.squares()) { const p = this.board.at(from); if (p === null || p.color !== this.turn) continue; const rule = RULES[p.kind]; for (const to of rule.targets(this.board, from)) { if (p.kind === 'pawn' && to.rank === lastRank) { for (const promotion of PROMOTIONS) out.push({ from, to, promotion }); } else { out.push({ from, to }); } } const ep = this.enPassant; if (p.kind === 'pawn' && ep && rule.attacks(this.board, from).some((s) => s.equals(ep))) { out.push({ from, to: ep }); } } this.addCastling(out); return out; }
// The king moves two squares toward a rook that can still castle, and the rook jumps over it. // Not out of check, not through an attacked square, and only with every square between empty. // The square the king lands on is checked by safeAfter like any other move. private addCastling(out: Move[]): void { if (this.inCheck()) return; const king = new Square(4, this.turn === 'white' ? 0 : 7); for (const rook of this.castlingRooks) { if (rook.rank !== king.rank) continue; const dir = Math.sign(rook.file - king.file); let empty = true; for (let s = king.plus(dir, 0); !s.equals(rook); s = s.plus(dir, 0)) empty &&= this.board.at(s) === null; if (empty && !attacked(this.board, king.plus(dir, 0), other(this.turn))) { out.push({ from: king, to: king.plus(2 * dir, 0) }); } } }
// Play the move on a copy and look: is our king attacked? It catches pins and discovered checks // with no special code. private safeAfter(m: Move): boolean { const after = this.board.copy(); apply(after, m); return !attacked(after, after.kingOf(this.turn), other(this.turn)); }
// No legal move is the end: checkmate if the king is attacked, stalemate (a draw) if not. private updateState(): void { if (this.legalMoves().length === 0) this.currentState = this.inCheck() ? 'checkmate' : 'stalemate'; }}
// How one kind of piece moves: where the piece on from can go, ignoring check. attacks are the// squares it threatens, the same as its targets for every piece but the pawn, which moves// straight and captures diagonally. Adding a piece, or a fairy-chess variant, is a new rule// rather than a longer switch.interface MoveRule { targets(board: Board, from: Square): Square[]; attacks(board: Board, from: Square): Square[];}
type Dir = readonly [number, number];
// Steps in each direction, once (king, knight) or repeatedly until something is in the way// (rook, bishop, queen). An enemy piece in the way can be captured, an own piece can't.class Step implements MoveRule { constructor( readonly directions: readonly Dir[], readonly slides: boolean, ) {}
targets(board: Board, from: Square): Square[] { const own = board.at(from)!.color; const out: Square[] = []; for (const [df, dr] of this.directions) { for (let s = from.plus(df, dr); s.onBoard(); s = s.plus(df, dr)) { const p = board.at(s); if (p === null || p.color !== own) out.push(s); if (p !== null || !this.slides) break; } } return out; }
attacks(board: Board, from: Square): Square[] { return this.targets(board, from); }}
// The pawn is the odd one out. It moves forward but captures diagonally, may move two squares// from its start, and only goes one way. So it gets its own rule.class PawnRule implements MoveRule { targets(board: Board, from: Square): Square[] { const own = board.at(from)!.color; const forward = own === 'white' ? 1 : -1; const out: Square[] = []; const one = from.plus(0, forward); if (one.onBoard() && board.at(one) === null) { out.push(one); const two = one.plus(0, forward); const onStart = from.rank === (own === 'white' ? 1 : 6); if (onStart && board.at(two) === null) out.push(two); } for (const s of this.attacks(board, from)) { const p = board.at(s); if (p !== null && p.color !== own) out.push(s); } return out; }
attacks(board: Board, from: Square): Square[] { const forward = board.at(from)!.color === 'white' ? 1 : -1; return [from.plus(-1, forward), from.plus(1, forward)].filter((s) => s.onBoard()); }}
// Directions as [file, rank] steps.const STRAIGHT: Dir[] = [ [1, 0], [-1, 0], [0, 1], [0, -1],];
const DIAGONAL: Dir[] = [ [1, 1], [1, -1], [-1, 1], [-1, -1],];
const KNIGHT_JUMPS: Dir[] = [ [1, 2], [2, 1], [2, -1], [1, -2], [-1, -2], [-2, -1], [-2, 1], [-1, 2],];
// Six kinds of piece, four shapes of movement: the queen is the rook and bishop together, and// the king and knight are steppers with different directions.const RULES: Record<Kind, MoveRule> = { king: new Step([...STRAIGHT, ...DIAGONAL], false), queen: new Step([...STRAIGHT, ...DIAGONAL], true), rook: new Step(STRAIGHT, true), bishop: new Step(DIAGONAL, true), knight: new Step(KNIGHT_JUMPS, false), pawn: new PawnRule(),};
// Whether the game is still on. Checkmate and stalemate are both final.type State = 'playing' | 'checkmate' | 'stalemate';
// promotion is the piece a pawn becomes on the last rank, and absent for every other move.interface Move { readonly from: Square; readonly to: Square; readonly promotion?: Kind;}
// Does any piece of color `by` threaten target? A plain function, so it can test a copied board// too.function attacked(b: Board, target: Square, by: Color): boolean { return b.squares().some((s) => { const p = b.at(s); return p !== null && p.color === by && RULES[p.kind].attacks(b, s).some((a) => a.equals(target)); });}
// The special moves are recognised from the board: a pawn moving diagonally to an empty square// is en passant, and a king moving two files is castling.function apply(b: Board, m: Move): void { const p = b.at(m.from)!; if (p.kind === 'pawn' && m.from.file !== m.to.file && b.at(m.to) === null) { b.put(new Square(m.to.file, m.from.rank), null); } if (p.kind === 'king' && Math.abs(m.to.file - m.from.file) === 2) { const dir = Math.sign(m.to.file - m.from.file); const rook = new Square(dir > 0 ? 7 : 0, m.from.rank); b.put(m.from.plus(dir, 0), b.at(rook)); b.put(rook, null); } b.put(m.to, m.promotion ? { kind: m.promotion, color: p.color } : p); b.put(m.from, null);}
// What a pawn may become: never a king, and it can't stay a pawn.const PROMOTIONS: Kind[] = ['queen', 'rook', 'bishop', 'knight'];
// A game played online. Each player's client sends a move with the ply it was made at, the// number of moves it had seen. A move made against an old position, or the same move sent twice// because a request timed out and was retried, is stale and changes nothing.//// JavaScript runs one callback at a time, so there is no lock. What keeps two requests for the// same ply from both passing the check is that the check and the move run with no await// between them. Saving the move is slow and comes after, once the ply has already moved on.class SharedGame { constructor( private readonly game: Game, private readonly save: (ply: number, move: string) => Promise<void>, ) {}
async submit(player: Color, expectedPly: number, from: string, to: string, promotion?: Kind): Promise<Result> { if (expectedPly !== this.game.ply) return 'stale'; if (player !== this.game.toMove) return 'not-your-turn'; const result = this.game.move(from, to, promotion); if (result === 'ok') await this.save(this.game.ply, from + to); return result; }
// A client reads this before it moves, so it can send the ply it has seen. get ply(): number { return this.game.ply; }
get state(): State { return this.game.state; }}Three ideas hold it together:
- Movement and legality are separate. Rules say where a piece can go (Strategy). The game says whether going there is allowed. No rule ever needs to know about check.
- One legality test for every move. Normal moves, captures, castling, en passant and promotion
all pass through
safeAfter. Players only callmove, never the board or the rules (Facade). - The smallest history that works. Two fields, the castling rooks and the en passant square,
plus the ply count. Everything else is read from the board. Once the game ends, its state turns
every move into
GAME_OVER(State).
Stress it
Section titled “Stress it”So far, both players share one device. Now each player’s app sends moves to a server.
A move that arrives twice
Section titled “A move that arrives twice”The game is online. Each player’s app sends moves to your server. What goes wrong?
Whether you see where the danger is. It isn’t two players moving at once, since turns already prevent that. It’s one player’s move arriving twice, or a move made against a position that has changed. The red flag is “add a lock”, without saying what it makes happen as one step.
Two players can’t both have the turn, so they don’t race each other. The problems are subtler.
A retried move. A player’s phone sends “e2 to e4”. The request times out, so the phone sends it again. If both copies arrive, the first is played, and the second is checked against the new position. Usually it’s refused as illegal, but that’s luck, not design. The server can’t tell a retry from a new move, and it’s judging the move against a position the player never saw.
A move made against an old position. A player taps a move while their screen still shows an older position. The server must not play a move the player chose while looking at a different board.
The fix for both: every request carries the ply it was made at, which is the number of moves the player’s app had seen. The server plays the move only if that is still the current ply. Otherwise it answers STALE, and the app refreshes. A retried request carries the same ply as the original, so only one of them can match.
The check and the move must be one step, under one lock. Without it, two copies of the same request could both read “ply 12” before either moves, and both pass.
- Color key
- Facade
| Option | How | Retry played twice? | Cost |
|---|---|---|---|
| 1. Expected ply, checked under a lock (our choice) | Refuse any move not made at this ply | No | One lock per game; apps send the ply |
| 2. A lock, no ply | Moves go one at a time, then are checked on the board | Possible | Moves judged on a board the player didn’t see |
| 3. A request id, remembered | Refuse an id seen before | No | Stores ids; old positions still accepted |
| 4. One worker or queue per game | Every move goes through one worker | Needs 1 or 3 too | One at a time, without a lock |
// What a move request gets back. STALE is for online play: the move was made against an old// position.enum Result { OK, NO_PIECE, NOT_YOUR_TURN, ILLEGAL_MOVE, GAME_OVER, STALE }
// A game played online. Each player's client sends a move with the ply it was made at, the// number of moves it had seen. A move made against an old position, or the same move sent// twice because a request timed out and was retried, is STALE and changes nothing. The lock// makes checking the ply and playing the move one step, so two requests for the same ply can't// both pass the check.class SharedGame { private final Game game;
SharedGame(Game game) { this.game = game; }
synchronized Result submit(Color player, int expectedPly, String from, String to, Kind promotion) { if (expectedPly != game.ply()) return Result.STALE; if (player != game.turn()) return Result.NOT_YOUR_TURN; return game.move(from, to, promotion); }
// A client reads this before it moves, so it can send the ply it has seen. synchronized int ply() { return game.ply(); }
synchronized State state() { return game.state(); }}# What a move request gets back. STALE is for online play: the move was made against an# old position.class Result(Enum): OK = auto() NO_PIECE = auto() NOT_YOUR_TURN = auto() ILLEGAL_MOVE = auto() GAME_OVER = auto() STALE = auto()
# A game played online. Each player's client sends a move with the ply it was made at,# the number of moves it had seen. A move made against an old position, or the same move# sent twice because a request timed out and was retried, is STALE and changes nothing. The# lock makes checking the ply and playing the move one step, so two requests for the same# ply can't both pass the check.class SharedGame: def __init__(self, game: Game): self._game = game self._lock = threading.Lock()
def submit(self, player: Color, expected_ply: int, frm: str, to: str, promotion: Kind | None = None) -> Result: with self._lock: if expected_ply != self._game.ply: return Result.STALE if player is not self._game.turn: return Result.NOT_YOUR_TURN return self._game.move(frm, to, promotion)
# A client reads this before it moves, so it can send the ply it has seen. @property def ply(self) -> int: with self._lock: return self._game.ply
@property def state(self) -> State: with self._lock: return self._game.state// What a move request gets back. Stale is for online play: the move was made against an old// position.public enum Result { Ok, NoPiece, NotYourTurn, IllegalMove, GameOver, Stale }
// A game played online. Each player's client sends a move with the ply it was made at, the// number of moves it had seen. A move made against an old position, or the same move sent// twice because a request timed out and was retried, is Stale and changes nothing. The lock// makes checking the ply and playing the move one step, so two requests for the same ply can't// both pass the check.public sealed class SharedGame{ private readonly object _lock = new(); private readonly Game _game;
public SharedGame(Game game) => _game = game;
public Result Submit(Color player, int expectedPly, string from, string to, Kind? promotion = null) { lock (_lock) { if (expectedPly != _game.Ply) return Result.Stale; if (player != _game.Turn) return Result.NotYourTurn; return _game.Move(from, to, promotion); } }
// A client reads this before it moves, so it can send the ply it has seen. public int Ply { get { lock (_lock) return _game.Ply; } } public State State { get { lock (_lock) return _game.State; } }}const ( OK Result = iota NoPiece NotYourTurn IllegalMove GameOver Stale // online play: the move was made against an old position)
// SharedGame is a game played online. Each player's client sends a move with the ply it was// made at, the number of moves it had seen. A move made against an old position, or the same// move sent twice because a request timed out and was retried, is Stale and changes nothing.// The mutex makes checking the ply and playing the move one step, so two requests for the same// ply can't both pass the check.type SharedGame struct { mu sync.Mutex game *Game}
func NewSharedGame(g *Game) *SharedGame { return &SharedGame{game: g} }
func (s *SharedGame) Submit(player Color, expectedPly int, from, to string, promotion Kind) Result { s.mu.Lock() defer s.mu.Unlock() if expectedPly != s.game.Ply() { return Stale } if player != s.game.Turn() { return NotYourTurn } return s.game.MovePromoting(from, to, promotion)}
// Ply is read by a client before it moves, so it can send the ply it has seen.func (s *SharedGame) Ply() int { s.mu.Lock() defer s.mu.Unlock() return s.game.Ply()}
func (s *SharedGame) State() State { s.mu.Lock() defer s.mu.Unlock() return s.game.State()}// What a move request gets back. 'stale' is for online play: the move was made against an old// position.type Result = 'ok' | 'no-piece' | 'not-your-turn' | 'illegal-move' | 'game-over' | 'stale';
// A game played online. Each player's client sends a move with the ply it was made at, the// number of moves it had seen. A move made against an old position, or the same move sent twice// because a request timed out and was retried, is stale and changes nothing.//// JavaScript runs one callback at a time, so there is no lock. What keeps two requests for the// same ply from both passing the check is that the check and the move run with no await// between them. Saving the move is slow and comes after, once the ply has already moved on.class SharedGame { constructor( private readonly game: Game, private readonly save: (ply: number, move: string) => Promise<void>, ) {}
async submit(player: Color, expectedPly: number, from: string, to: string, promotion?: Kind): Promise<Result> { if (expectedPly !== this.game.ply) return 'stale'; if (player !== this.game.toMove) return 'not-your-turn'; const result = this.game.move(from, to, promotion); if (result === 'ok') await this.save(this.game.ply, from + to); return result; }
// A client reads this before it moves, so it can send the ply it has seen. get ply(): number { return this.game.ply; }
get state(): State { return this.game.state; }}Why is this correct? Under the game’s lock, reading the ply, checking the move and playing it
happen as one step. The first copy of a request moves the ply on. Every later copy, and every move
made on an older screen, sees a different ply and gets STALE.
What does it cost? One lock per game, not one for the whole server, so games never wait for each other. And the app must send the ply with each move.
Go deeperHow each language does it1 min
In Java, Python, C# and Go, each request runs on its own thread. So the lock is what makes the check
and the move one step. TypeScript runs one callback at a time, so it needs no lock, as long as
nothing waits with await between the check and the move. Its version saves the move to a database
afterwards, once the ply has already moved on.
How you’d prove it works
Section titled “How you’d prove it works”A game can be created from any board. The tests build positions from short strings like
"Ke1 Rh1 ke8". The letter is the piece, upper case for white. Castling rights are worked out from
the position. So a test is one line to set up, then a few moves. No test plays twenty moves to
reach a position. The tests that matter:
- Each piece moves by its rule, and errors come back as the right result.
- A pinned rook can move along the pin, but not off it.
- A player in check must answer it.
- Fool’s mate (f3, e5, g4, Qh4) is checkmate, and the next move gets
GAME_OVER. - King f7, queen g6 against king h8 is stalemate, not checkmate.
- Castling moves both pieces, and is refused through an attacked square.
- A rook that moves away and back has lost its castling right.
- En passant works only on the very next move, and removes the captured pawn.
- Promotion makes the chosen piece, and a queen by default.
- Eight threads submit the same move for ply 0: exactly one is played, and seven are stale.
Go deeperThe tests in five languages2 min
The last test fails in Java, Python and Go if the lock is removed: several copies pass the ply check before any of them moves. The Python version makes threads switch often, so the race shows.
// Builds a position from pieces like "Ke1 Ra1 ke8": the letter is the piece (N for knight), upper// case for white and lower case for black.static Game position(String pieces, Color toMove) { Board b = new Board(); Map<Character, Kind> kinds = Map.of('K', Kind.KING, 'Q', Kind.QUEEN, 'R', Kind.ROOK, 'B', Kind.BISHOP, 'N', Kind.KNIGHT, 'P', Kind.PAWN); for (String p : pieces.split(" ")) { char c = p.charAt(0); Color color = Character.isUpperCase(c) ? Color.WHITE : Color.BLACK; b.put(Square.of(p.substring(1)), new Piece(kinds.get(Character.toUpperCase(c)), color)); } return new Game(b, toMove);}
static void play(Game g, String... moves) { for (String m : moves) assertEquals(Result.OK, g.move(m.substring(0, 2), m.substring(2, 4)));}
@Testvoid eachPieceMovesByItsOwnRule() { Game g = new Game(); assertEquals(Result.NO_PIECE, g.move("e4", "e5")); assertEquals(Result.NOT_YOUR_TURN, g.move("e7", "e5")); assertEquals(Result.ILLEGAL_MOVE, g.move("a1", "a3")); // the pawn on a2 is in the way assertEquals(Result.ILLEGAL_MOVE, g.move("f1", "f3")); // a bishop doesn't move straight play(g, "e2e4", "e7e5", "g1f3", "b8c6", "f1c4"); assertEquals(new Piece(Kind.BISHOP, Color.WHITE), g.at("c4"));}
@Testvoid aPinnedPieceCannotLeaveTheLine() { Game g = position("Ke1 Re2 re8 kh8", Color.WHITE); assertEquals(Result.ILLEGAL_MOVE, g.move("e2", "d2")); // would expose the king assertEquals(Result.OK, g.move("e2", "e5")); // stays between them}
@Testvoid checkMustBeAnswered() { Game g = position("Ke1 Rh1 re8 ka8", Color.WHITE); assertTrue(g.inCheck()); assertEquals(Result.ILLEGAL_MOVE, g.move("h1", "h2")); assertEquals(Result.OK, g.move("e1", "d1"));}
@Testvoid foolsMateEndsTheGame() { Game g = new Game(); play(g, "f2f3", "e7e5", "g2g4", "d8h4"); assertEquals(State.CHECKMATE, g.state()); assertEquals(Color.BLACK, g.winner()); assertEquals(Result.GAME_OVER, g.move("a2", "a3"));}
@Testvoid noMovesWithoutCheckIsStalemate() { Game g = position("Kf7 Qg5 kh8", Color.WHITE); play(g, "g5g6"); assertEquals(State.STALEMATE, g.state()); assertNull(g.winner());}
@Testvoid castlingMovesBothPiecesAndNeedsASafePath() { Game g = position("Ke1 Ra1 Rh1 rf8 ke8", Color.WHITE); assertEquals(Result.ILLEGAL_MOVE, g.move("e1", "g1")); // the king would cross f1, which is attacked assertEquals(Result.OK, g.move("e1", "c1")); assertEquals(new Piece(Kind.ROOK, Color.WHITE), g.at("d1")); assertNull(g.at("a1"));}
@Testvoid castlingIsLostOnceTheRookHasMoved() { Game g = position("Ke1 Rh1 ke8", Color.WHITE); play(g, "h1h2", "e8d8", "h2h1", "d8e8"); assertEquals(Result.ILLEGAL_MOVE, g.move("e1", "g1"));}
@Testvoid enPassantIsOnlyAvailableRightAway() { Game g = position("Ke1 Pe5 ke8 pd7", Color.BLACK); play(g, "d7d5", "e5d6"); assertNull(g.at("d5")); // the pawn that passed was captured Game late = position("Ke1 Pe5 ke8 pd7", Color.BLACK); play(late, "d7d5", "e1e2", "e8f8"); assertEquals(Result.ILLEGAL_MOVE, late.move("e5", "d6"));}
@Testvoid aPawnOnTheLastRankBecomesTheChosenPiece() { Game g = position("Ke1 Pa7 kh7", Color.WHITE); assertEquals(Result.OK, g.move("a7", "a8", Kind.KNIGHT)); assertEquals(new Piece(Kind.KNIGHT, Color.WHITE), g.at("a8")); Game q = position("Ke1 Pa7 kh7", Color.WHITE); assertEquals(Result.OK, q.move("a7", "a8")); assertEquals(new Piece(Kind.QUEEN, Color.WHITE), q.at("a8"));}
@Testvoid racingSubmissionsForOnePlyPlayOnce() throws Exception { SharedGame first = new SharedGame(new Game()); assertEquals(Result.NOT_YOUR_TURN, first.submit(Color.BLACK, 0, "e7", "e5", null)); for (int round = 0; round < 100; round++) { SharedGame shared = new SharedGame(new Game()); CountDownLatch start = new CountDownLatch(1); AtomicInteger accepted = new AtomicInteger(); AtomicInteger stale = new AtomicInteger(); List<Thread> threads = new ArrayList<>(); for (int i = 0; i < 8; i++) { // the same move, retried by an impatient client Thread t = new Thread(() -> { try { start.await(); } catch (InterruptedException e) { return; } Result r = shared.submit(Color.WHITE, 0, "e2", "e4", null); if (r == Result.OK) accepted.incrementAndGet(); if (r == Result.STALE) stale.incrementAndGet(); }); t.start(); threads.add(t); } start.countDown(); for (Thread t : threads) t.join(); assertEquals(1, accepted.get()); assertEquals(7, stale.get()); // every other copy was rejected cleanly, none half-played assertEquals(1, shared.ply()); }}import sys
KINDS = {"K": Kind.KING, "Q": Kind.QUEEN, "R": Kind.ROOK, "B": Kind.BISHOP, "N": Kind.KNIGHT, "P": Kind.PAWN}
# Builds a position from pieces like "Ke1 Ra1 ke8": the letter is the piece (N for# knight), upper case for white and lower case for black.def position(pieces: str, to_move: Color) -> Game: b = Board() for p in pieces.split(): color = Color.WHITE if p[0].isupper() else Color.BLACK b.put(Square.of(p[1:]), Piece(KINDS[p[0].upper()], color)) return Game(b, to_move)
def play(g: Game, *moves: str) -> None: for m in moves: assert g.move(m[:2], m[2:4]) is Result.OK, m
def test_each_piece_moves_by_its_own_rule(): g = Game() assert g.move("e4", "e5") is Result.NO_PIECE assert g.move("e7", "e5") is Result.NOT_YOUR_TURN assert g.move("a1", "a3") is Result.ILLEGAL_MOVE # the pawn on a2 is in the way assert g.move("f1", "f3") is Result.ILLEGAL_MOVE # a bishop doesn't move straight play(g, "e2e4", "e7e5", "g1f3", "b8c6", "f1c4") assert g.at("c4") == Piece(Kind.BISHOP, Color.WHITE)
def test_a_pinned_piece_cannot_leave_the_line(): g = position("Ke1 Re2 re8 kh8", Color.WHITE) assert g.move("e2", "d2") is Result.ILLEGAL_MOVE # would expose the king assert g.move("e2", "e5") is Result.OK # stays between them
def test_check_must_be_answered(): g = position("Ke1 Rh1 re8 ka8", Color.WHITE) assert g.in_check() assert g.move("h1", "h2") is Result.ILLEGAL_MOVE assert g.move("e1", "d1") is Result.OK
def test_fools_mate_ends_the_game(): g = Game() play(g, "f2f3", "e7e5", "g2g4", "d8h4") assert g.state is State.CHECKMATE assert g.winner() is Color.BLACK assert g.move("a2", "a3") is Result.GAME_OVER
def test_no_moves_without_check_is_stalemate(): g = position("Kf7 Qg5 kh8", Color.WHITE) play(g, "g5g6") assert g.state is State.STALEMATE assert g.winner() is None
def test_castling_moves_both_pieces_and_needs_a_safe_path(): g = position("Ke1 Ra1 Rh1 rf8 ke8", Color.WHITE) assert g.move("e1", "g1") is Result.ILLEGAL_MOVE # the king would cross f1, which is attacked assert g.move("e1", "c1") is Result.OK assert g.at("d1") == Piece(Kind.ROOK, Color.WHITE) assert g.at("a1") is None
def test_castling_is_lost_once_the_rook_has_moved(): g = position("Ke1 Rh1 ke8", Color.WHITE) play(g, "h1h2", "e8d8", "h2h1", "d8e8") assert g.move("e1", "g1") is Result.ILLEGAL_MOVE
def test_en_passant_is_only_available_right_away(): g = position("Ke1 Pe5 ke8 pd7", Color.BLACK) play(g, "d7d5", "e5d6") assert g.at("d5") is None # the pawn that passed was captured late = position("Ke1 Pe5 ke8 pd7", Color.BLACK) play(late, "d7d5", "e1e2", "e8f8") assert late.move("e5", "d6") is Result.ILLEGAL_MOVE
def test_a_pawn_on_the_last_rank_becomes_the_chosen_piece(): g = position("Ke1 Pa7 kh7", Color.WHITE) assert g.move("a7", "a8", Kind.KNIGHT) is Result.OK assert g.at("a8") == Piece(Kind.KNIGHT, Color.WHITE) q = position("Ke1 Pa7 kh7", Color.WHITE) assert q.move("a7", "a8") is Result.OK assert q.at("a8") == Piece(Kind.QUEEN, Color.WHITE)
def test_racing_submissions_for_one_ply_play_once(): assert SharedGame(Game()).submit(Color.BLACK, 0, "e7", "e5") is Result.NOT_YOUR_TURN old = sys.getswitchinterval() sys.setswitchinterval(1e-6) # switch threads often, so a missing lock shows up try: for _ in range(20): shared = SharedGame(Game()) start = threading.Barrier(8)
def send(): # the same move, retried by an impatient client start.wait() return shared.submit(Color.WHITE, 0, "e2", "e4")
with ThreadPoolExecutor(8) as pool: results = list(pool.map(lambda _: send(), range(8))) assert results.count(Result.OK) == 1 assert results.count(Result.STALE) == 7 # every other copy was rejected cleanly assert shared.ply == 1 finally: sys.setswitchinterval(old)private static readonly Dictionary<char, Kind> Kinds = new(){ ['K'] = Kind.King, ['Q'] = Kind.Queen, ['R'] = Kind.Rook, ['B'] = Kind.Bishop, ['N'] = Kind.Knight, ['P'] = Kind.Pawn,};
// Builds a position from pieces like "Ke1 Ra1 ke8": the letter is the piece (N for knight),// upper case for white and lower case for black.private static Game Position(string pieces, Color toMove){ var b = new Board(); foreach (var p in pieces.Split(' ')) { var color = char.IsUpper(p[0]) ? Color.White : Color.Black; b.Put(Square.Of(p[1..]), new Piece(Kinds[char.ToUpper(p[0])], color)); } return new Game(b, toMove);}
private static void Play(Game g, params string[] moves){ foreach (var m in moves) Assert.Equal(Result.Ok, g.Move(m[..2], m[2..4]));}
[Fact]public void EachPieceMovesByItsOwnRule(){ var g = new Game(); Assert.Equal(Result.NoPiece, g.Move("e4", "e5")); Assert.Equal(Result.NotYourTurn, g.Move("e7", "e5")); Assert.Equal(Result.IllegalMove, g.Move("a1", "a3")); // the pawn on a2 is in the way Assert.Equal(Result.IllegalMove, g.Move("f1", "f3")); // a bishop doesn't move straight Play(g, "e2e4", "e7e5", "g1f3", "b8c6", "f1c4"); Assert.Equal(new Piece(Kind.Bishop, Color.White), g.At("c4"));}
[Fact]public void APinnedPieceCannotLeaveTheLine(){ var g = Position("Ke1 Re2 re8 kh8", Color.White); Assert.Equal(Result.IllegalMove, g.Move("e2", "d2")); // would expose the king Assert.Equal(Result.Ok, g.Move("e2", "e5")); // stays between them}
[Fact]public void CheckMustBeAnswered(){ var g = Position("Ke1 Rh1 re8 ka8", Color.White); Assert.True(g.InCheck); Assert.Equal(Result.IllegalMove, g.Move("h1", "h2")); Assert.Equal(Result.Ok, g.Move("e1", "d1"));}
[Fact]public void FoolsMateEndsTheGame(){ var g = new Game(); Play(g, "f2f3", "e7e5", "g2g4", "d8h4"); Assert.Equal(State.Checkmate, g.State); Assert.Equal(Color.Black, g.Winner); Assert.Equal(Result.GameOver, g.Move("a2", "a3"));}
[Fact]public void NoMovesWithoutCheckIsStalemate(){ var g = Position("Kf7 Qg5 kh8", Color.White); Play(g, "g5g6"); Assert.Equal(State.Stalemate, g.State); Assert.Null(g.Winner);}
[Fact]public void CastlingMovesBothPiecesAndNeedsASafePath(){ var g = Position("Ke1 Ra1 Rh1 rf8 ke8", Color.White); Assert.Equal(Result.IllegalMove, g.Move("e1", "g1")); // the king would cross f1, which is attacked Assert.Equal(Result.Ok, g.Move("e1", "c1")); Assert.Equal(new Piece(Kind.Rook, Color.White), g.At("d1")); Assert.Null(g.At("a1"));}
[Fact]public void CastlingIsLostOnceTheRookHasMoved(){ var g = Position("Ke1 Rh1 ke8", Color.White); Play(g, "h1h2", "e8d8", "h2h1", "d8e8"); Assert.Equal(Result.IllegalMove, g.Move("e1", "g1"));}
[Fact]public void EnPassantIsOnlyAvailableRightAway(){ var g = Position("Ke1 Pe5 ke8 pd7", Color.Black); Play(g, "d7d5", "e5d6"); Assert.Null(g.At("d5")); // the pawn that passed was captured var late = Position("Ke1 Pe5 ke8 pd7", Color.Black); Play(late, "d7d5", "e1e2", "e8f8"); Assert.Equal(Result.IllegalMove, late.Move("e5", "d6"));}
[Fact]public void APawnOnTheLastRankBecomesTheChosenPiece(){ var g = Position("Ke1 Pa7 kh7", Color.White); Assert.Equal(Result.Ok, g.Move("a7", "a8", Kind.Knight)); Assert.Equal(new Piece(Kind.Knight, Color.White), g.At("a8")); var q = Position("Ke1 Pa7 kh7", Color.White); Assert.Equal(Result.Ok, q.Move("a7", "a8")); Assert.Equal(new Piece(Kind.Queen, Color.White), q.At("a8"));}
[Fact]public void RacingSubmissionsForOnePlyPlayOnce(){ Assert.Equal(Result.NotYourTurn, new SharedGame(new Game()).Submit(Color.Black, 0, "e7", "e5")); for (int round = 0; round < 20; round++) { var shared = new SharedGame(new Game()); using var start = new ManualResetEventSlim(); var results = new Result[8]; var threads = Enumerable.Range(0, 8).Select(i => new Thread(() => { start.Wait(); results[i] = shared.Submit(Color.White, 0, "e2", "e4"); // the same move, retried by an impatient client })).ToList(); threads.ForEach(t => t.Start()); start.Set(); threads.ForEach(t => t.Join()); Assert.Equal(1, results.Count(r => r == Result.Ok)); Assert.Equal(7, results.Count(r => r == Result.Stale)); // every other copy was rejected cleanly Assert.Equal(1, shared.Ply); }}var kinds = map[byte]Kind{'K': King, 'Q': Queen, 'R': Rook, 'B': Bishop, 'N': Knight, 'P': Pawn}
// position builds a game from pieces like "Ke1 Ra1 ke8": the letter is the piece (N for// knight), upper case for white and lower case for black.func position(pieces string, toMove Color) *Game { var b Board for _, p := range strings.Fields(pieces) { color := White if p[0] >= 'a' { color = Black } b.Put(Sq(p[1:]), Piece{kinds[strings.ToUpper(p[:1])[0]], color}) } return NewGameFrom(b, toMove)}
func expect[T comparable](t *testing.T, want, got T) { t.Helper() if want != got { t.Fatalf("want %v, got %v", want, got) }}
func play(t *testing.T, g *Game, moves ...string) { t.Helper() for _, m := range moves { expect(t, OK, g.Move(m[:2], m[2:4])) }}
func TestEachPieceMovesByItsOwnRule(t *testing.T) { g := NewGame() expect(t, NoPiece, g.Move("e4", "e5")) expect(t, NotYourTurn, g.Move("e7", "e5")) expect(t, IllegalMove, g.Move("a1", "a3")) // the pawn on a2 is in the way expect(t, IllegalMove, g.Move("f1", "f3")) // a bishop doesn't move straight play(t, g, "e2e4", "e7e5", "g1f3", "b8c6", "f1c4") expect(t, Piece{Bishop, White}, g.At("c4"))}
func TestAPinnedPieceCannotLeaveTheLine(t *testing.T) { g := position("Ke1 Re2 re8 kh8", White) expect(t, IllegalMove, g.Move("e2", "d2")) // would expose the king expect(t, OK, g.Move("e2", "e5")) // stays between them}
func TestCheckMustBeAnswered(t *testing.T) { g := position("Ke1 Rh1 re8 ka8", White) expect(t, true, g.InCheck()) expect(t, IllegalMove, g.Move("h1", "h2")) expect(t, OK, g.Move("e1", "d1"))}
func TestFoolsMateEndsTheGame(t *testing.T) { g := NewGame() play(t, g, "f2f3", "e7e5", "g2g4", "d8h4") expect(t, Checkmate, g.State()) winner, ok := g.Winner() expect(t, true, ok) expect(t, Black, winner) expect(t, GameOver, g.Move("a2", "a3"))}
func TestNoMovesWithoutCheckIsStalemate(t *testing.T) { g := position("Kf7 Qg5 kh8", White) play(t, g, "g5g6") expect(t, Stalemate, g.State()) _, ok := g.Winner() expect(t, false, ok)}
func TestCastlingMovesBothPiecesAndNeedsASafePath(t *testing.T) { g := position("Ke1 Ra1 Rh1 rf8 ke8", White) expect(t, IllegalMove, g.Move("e1", "g1")) // the king would cross f1, which is attacked expect(t, OK, g.Move("e1", "c1")) expect(t, Piece{Rook, White}, g.At("d1")) expect(t, true, g.At("a1").Empty())}
func TestCastlingIsLostOnceTheRookHasMoved(t *testing.T) { g := position("Ke1 Rh1 ke8", White) play(t, g, "h1h2", "e8d8", "h2h1", "d8e8") expect(t, IllegalMove, g.Move("e1", "g1"))}
func TestEnPassantIsOnlyAvailableRightAway(t *testing.T) { g := position("Ke1 Pe5 ke8 pd7", Black) play(t, g, "d7d5", "e5d6") expect(t, true, g.At("d5").Empty()) // the pawn that passed was captured late := position("Ke1 Pe5 ke8 pd7", Black) play(t, late, "d7d5", "e1e2", "e8f8") expect(t, IllegalMove, late.Move("e5", "d6"))}
func TestAPawnOnTheLastRankBecomesTheChosenPiece(t *testing.T) { g := position("Ke1 Pa7 kh7", White) expect(t, OK, g.MovePromoting("a7", "a8", Knight)) expect(t, Piece{Knight, White}, g.At("a8")) q := position("Ke1 Pa7 kh7", White) expect(t, OK, q.Move("a7", "a8")) expect(t, Piece{Queen, White}, q.At("a8"))}
func TestRacingSubmissionsForOnePlyPlayOnce(t *testing.T) { expect(t, NotYourTurn, NewSharedGame(NewGame()).Submit(Black, 0, "e7", "e5", 0)) for range 20 { shared := NewSharedGame(NewGame()) var accepted, stale atomic.Int32 var wg sync.WaitGroup for range 8 { // the same move, retried by an impatient client wg.Add(1) go func() { defer wg.Done() switch shared.Submit(White, 0, "e2", "e4", 0) { case OK: accepted.Add(1) case Stale: stale.Add(1) } }() } wg.Wait() expect(t, 1, int(accepted.Load())) expect(t, 7, int(stale.Load())) // every other copy was rejected cleanly expect(t, 1, shared.Ply()) }}import { test } from 'node:test';import assert from 'node:assert/strict';
const KINDS: Record<string, Kind> = { K: 'king', Q: 'queen', R: 'rook', B: 'bishop', N: 'knight', P: 'pawn' };
// Builds a position from pieces like "Ke1 Ra1 ke8": the letter is the piece (N for knight),// upper case for white and lower case for black.function position(pieces: string, toMove: Color): Game { const b = new Board(); for (const p of pieces.split(' ')) { const color: Color = p[0] === p[0].toUpperCase() ? 'white' : 'black'; b.put(Square.of(p.slice(1)), { kind: KINDS[p[0].toUpperCase()], color }); } return new Game(b, toMove);}
function play(g: Game, ...moves: string[]): void { for (const m of moves) assert.equal(g.move(m.slice(0, 2), m.slice(2, 4)), 'ok', m);}
test('each piece moves by its own rule', () => { const g = new Game(); assert.equal(g.move('e4', 'e5'), 'no-piece'); assert.equal(g.move('e7', 'e5'), 'not-your-turn'); assert.equal(g.move('a1', 'a3'), 'illegal-move'); // the pawn on a2 is in the way assert.equal(g.move('f1', 'f3'), 'illegal-move'); // a bishop doesn't move straight play(g, 'e2e4', 'e7e5', 'g1f3', 'b8c6', 'f1c4'); assert.deepEqual(g.at('c4'), { kind: 'bishop', color: 'white' });});
test('a pinned piece cannot leave the line', () => { const g = position('Ke1 Re2 re8 kh8', 'white'); assert.equal(g.move('e2', 'd2'), 'illegal-move'); // would expose the king assert.equal(g.move('e2', 'e5'), 'ok'); // stays between them});
test('check must be answered', () => { const g = position('Ke1 Rh1 re8 ka8', 'white'); assert.equal(g.inCheck(), true); assert.equal(g.move('h1', 'h2'), 'illegal-move'); assert.equal(g.move('e1', 'd1'), 'ok');});
test("fool's mate ends the game", () => { const g = new Game(); play(g, 'f2f3', 'e7e5', 'g2g4', 'd8h4'); assert.equal(g.state, 'checkmate'); assert.equal(g.winner(), 'black'); assert.equal(g.move('a2', 'a3'), 'game-over');});
test('no moves without check is stalemate', () => { const g = position('Kf7 Qg5 kh8', 'white'); play(g, 'g5g6'); assert.equal(g.state, 'stalemate'); assert.equal(g.winner(), null);});
test('castling moves both pieces and needs a safe path', () => { const g = position('Ke1 Ra1 Rh1 rf8 ke8', 'white'); assert.equal(g.move('e1', 'g1'), 'illegal-move'); // the king would cross f1, which is attacked assert.equal(g.move('e1', 'c1'), 'ok'); assert.deepEqual(g.at('d1'), { kind: 'rook', color: 'white' }); assert.equal(g.at('a1'), null);});
test('castling is lost once the rook has moved', () => { const g = position('Ke1 Rh1 ke8', 'white'); play(g, 'h1h2', 'e8d8', 'h2h1', 'd8e8'); assert.equal(g.move('e1', 'g1'), 'illegal-move');});
test('en passant is only available right away', () => { const g = position('Ke1 Pe5 ke8 pd7', 'black'); play(g, 'd7d5', 'e5d6'); assert.equal(g.at('d5'), null); // the pawn that passed was captured const late = position('Ke1 Pe5 ke8 pd7', 'black'); play(late, 'd7d5', 'e1e2', 'e8f8'); assert.equal(late.move('e5', 'd6'), 'illegal-move');});
test('a pawn on the last rank becomes the chosen piece', () => { const g = position('Ke1 Pa7 kh7', 'white'); assert.equal(g.move('a7', 'a8', 'knight'), 'ok'); assert.deepEqual(g.at('a8'), { kind: 'knight', color: 'white' }); const q = position('Ke1 Pa7 kh7', 'white'); assert.equal(q.move('a7', 'a8'), 'ok'); assert.deepEqual(q.at('a8'), { kind: 'queen', color: 'white' });});
test('racing submissions for one ply play once', async () => { const saved: string[] = []; const save = async (ply: number, move: string) => { await new Promise((r) => setTimeout(r, 5)); // a database write saved.push(`${ply}:${move}`); }; const shared = new SharedGame(new Game(), save); assert.equal(await shared.submit('black', 0, 'e7', 'e5'), 'not-your-turn'); // The same move, retried by an impatient client while the first is still saving. const results = await Promise.all(Array.from({ length: 8 }, () => shared.submit('white', 0, 'e2', 'e4'))); assert.equal(results.filter((r) => r === 'ok').length, 1); assert.equal(results.filter((r) => r === 'stale').length, 7); assert.deepEqual(saved, ['1:e2e4']);});Go deeperEvery edge case, and where it is handled2 min
| Case | What happens | Where |
|---|---|---|
| No piece on the starting square | NO_PIECE |
Game.move |
| Moving the opponent’s piece | NOT_YOUR_TURN |
Game.move |
| Capturing your own piece | Never listed by any rule | Step, PawnRule |
| Moving into check, or a pinned piece | ILLEGAL_MOVE |
safeAfter |
| Ignoring a check | ILLEGAL_MOVE: the king is still attacked |
safeAfter |
| Castling out of, through, or into check | Not listed, or refused by the same test | addCastling, safeAfter |
| Castling after the king or rook moved | Not listed: the rook left the set | play |
| Castling after the rook was captured | Not listed: captured on its square | play |
| En passant a move too late | Not listed: the square was cleared | play |
| En passant that exposes the king on a row | Refused: the copy shows the attack | safeAfter |
| Promotion with no choice given | A queen | Game.move |
| Any move after checkmate or stalemate | GAME_OVER |
Game.move |
| A position created already checkmated | Starts finished | constructor |
| The same move sent twice | The second is STALE |
SharedGame.submit |
Wrap up
Section titled “Wrap up”The decisions, and what each one costs
Section titled “The decisions, and what each one costs”| Decision | We chose | We gave up |
|---|---|---|
| How pieces move | A MoveRule per kind; three rules for six pieces |
Piece classes with behaviour |
| What a piece is | A value: kind and color | An identity for each piece |
| Legality | Try every move on a copy; refuse if the own king is hit | The speed of make-unmake or bitboards |
| The end | No legal move: checkmate if in check, else stalemate | Draw rules, left as an extension |
| Castling, en passant | A set of castling rooks and one en passant square | A full move history, until undo needs it |
| Online play | The expected ply, checked with the move under a lock | Accepting moves made on an out-of-date screen |
Go deeperGenuinely different designs, and when to pick them2 min
Bitboards. Chess engines store the board as twelve 64-bit numbers, one per kind and color. They work out moves with bit operations, and tables computed in advance. They generate millions of moves a second, instead of thousands. It’s the right choice for an engine. It’s too hard to read for a game server, where the object design is fast enough by a wide margin.
Make and unmake, instead of copying. Instead of copying the board to test each move, engines make the move in place and undo it afterwards. That avoids the copies, at the cost of having to restore everything a move changed, exactly. That’s the same data the undo extension needs.
Skip the legality test, and check for a king capture. Some engines skip the legality test while searching. They only throw a position away when a king could be captured. It’s faster for search, and wrong for a game that must refuse illegal moves straight away.
How to spend 45 minutes
Section titled “How to spend 45 minutes”- 0 to 3. Two people, rules enforced, checkmate and stalemate, online.
- 3 to 5. The promises. Name “no move leaves your king in check” as the hard one.
- 5 to 25. The switch and its problems. Move rules as strategies. Legality on a copy, and the end
of the game. Then castling, en passant and promotion. Code
Step,safeAfterandaddCastling. - 25 to 27. Walk through the full diagram: movement apart from legality, one test for every move.
- 27 to 32. Online play: the expected ply, checked under a lock.
- 32 to 45. Draw rules and undo from one move list, then your own questions.
If time is short, describe castling and en passant in a sentence each. Spend the time on the legality rule, and the end of the game. Those decide this question for most interviewers.
Follow-up questions
Section titled “Follow-up questions”Add draws by repetition and the fifty-move rule, and an undo button for casual games. What changes?
Whether you see that all three need the same thing: a record of past positions or moves. The strong answer adds one history and works out the rest from it.
Most extensions fit into one place:
| Extension | What changes | Touches the rules? |
|---|---|---|
| Undo | A move list storing what each move changed | No |
| Fifty-move rule | A counter of plies since a capture or pawn move | No |
| Threefold repetition | A count per position key | No |
| Clocks | A clock per side, switched in play |
No |
| Chess960 | Castling from other starting squares, kept in the same set | Only addCastling |
| A new kind of piece | One more MoveRule in the table |
Only the table |
| Save and replay | Store the move list; replay from the start | No |
Go deeperAdding undo and draw rules, step by step2 min
- Keep a list of moves, each with what it changed. Besides the move itself, each entry stores the piece it captured (if any). It also stores the castling rooks and en passant square from before the move. That’s everything the move destroyed.
- Undo takes the last entry off, and restores it. Move the piece back. Put the captured piece back on its square (for en passant, that’s beside the target square). Move the rook back if it was castling. Turn a promoted piece back into a pawn. Restore the two history fields. Then lower the ply count, and give the turn back.
- The fifty-move rule is a counter. It counts plies since the last capture or pawn move, and
resets in
play. At 100 plies (fifty moves each), either player may claim a draw. - Threefold repetition counts positions. After each move, build a key from the board, the side to move, the castling rooks and the en passant square. Count it in a map. When a count reaches three, the game can be drawn. Zobrist hashing makes that key a single 64-bit number. It’s updated in a few steps per move, instead of rebuilt from the whole board.
- Undo must undo these too. Lower the position’s count, and restore the fifty-move counter, which the entry can also store.
The move rules and the legality test don’t change at all. History was kept small on purpose, and these extensions add it only where a rule needs it.
Go deeperFour more follow-up questions2 min
Interviewer asksIsn't listing every legal move after every move slow?
A position has about 30 to 40 moves, each tested on a copy against at most 16 enemy pieces. That’s tens of thousands of simple steps, well under a millisecond. A server for human games makes a few moves a minute per game. It only matters for an engine searching millions of positions, which is where bitboards and make-unmake come in.
Interviewer asksHow would you add a computer opponent?
Behind the same submit as a human. A Player interface with a human and an engine
implementation, where the engine is given the position and returns a move. The engine has its own
fast board inside. The Game stays the referee, so a buggy engine still can’t make an illegal
move.
Interviewer asksWhy is a Piece a value and not an object with an id?
Nothing in chess cares which rook is which, only where the rooks are. Values make the board copy cheap. They make promotion a matter of putting a different value on the square. And they make two positions with the same pieces equal. Castling rights are the one place identity seems to matter, and they’re kept by square instead.
Interviewer asksHow do spectators see moves as they happen?
SharedGame publishes each accepted move, with its ply, to everyone watching that game. (That’s
the Observer pattern.) When watchers are on other servers, it uses a pub-sub channel. A watcher
who sees ply 12 right after ply 10 knows it missed one, and fetches the position.
Further reading
Section titled “Further reading”- Strategy pattern, the shape the move rules are built on
- Design tic-tac-toe, a smaller board game with the same split between the board and the game
- Laws of Chess, FIDE, the rules as they are officially written
- Chess Programming Wiki, how engines represent boards and generate moves
The designbeyondcode digest, every Saturday
New deep dives, interview tips and site news in one short email. Free. Leave with one click.
We never sell or share your email. We only use it to send this newsletter.