The General and the Specific
A chess board is 8×8. A Go board is 19×19. A checkers board is 8×8 but uses only half the squares. An Othello board is 8×8.
These are all board games. They share something essential. Can we find it?
The Abstract Board Game
BoardGame:
Board:
positions: Set of Position
adjacency: Position → Set of Position // which positions connect
Piece:
owner: Player
kind: PieceKind
State:
board_contents: Position → (Piece OR empty)
current_player: Player
... game-specific state ...
valid_actions(state): Set of Action
apply_action(state, action): State
is_terminal(state): Boolean
winner(state): Maybe PlayerChess as an Instance
Chess fills the abstract framework:
Chess implements BoardGame:
Board:
positions: 64 squares (a1 through h8)
adjacency: defined by piece movement rules
Piece:
kinds: king, queen, rook, bishop, knight, pawn
owner: white or black
State:
64 squares each empty or containing a piece
current_player: white or black
castling_rights, en_passant, etc.
valid_actions(state):
all legal moves for current player
apply_action(state, move):
state with move applied
is_terminal(state):
is_checkmate(state) OR is_stalemate(state) OR is_draw(state)
winner(state):
if checkmate: opponent of player in checkmate
else: Nothing (draw or ongoing)Go as Another Instance
Go fills the same framework differently:
Go implements BoardGame:
Board:
positions: 361 intersections (19×19)
adjacency: orthogonal neighbors
Piece:
kinds: stone (just one kind!)
owner: black or white
State:
361 intersections, each empty or containing a stone
current_player: black or white
captured_count, ko_position, etc.
valid_actions(state):
place stone on empty intersection (if not suicide or ko)
OR pass
apply_action(state, action):
if placing: add stone, remove captured groups
if passing: switch player
is_terminal(state):
both players passed consecutively
winner(state):
count territory and captures, higher score winsThe Power of the Abstraction
With this abstraction, we can write code that works for any board game:
// Works for chess, Go, checkers, Othello...
play_game(game, player_white, player_black):
state = game.initial_state
while not game.is_terminal(state):
player = if state.current_player == white
then player_white
else player_black
action = player.choose_action(state, game.valid_actions(state))
state = game.apply_action(state, action)
return game.winner(state)A Design Choice: Who Validates?
Notice something subtle: the game provides valid_actions(state)---a list of all legal moves. The player then chooses from this list.
This is different from our earlier design in move validation! There we imagined:
// Earlier approach: player proposes, game validates
player_move = player.propose_move() // e.g., "e2 to e4"
if game.is_valid(state, player_move):
state = game.apply(state, player_move)
else:
// handle invalid moveBoth work. But they create very different architectures.
Connecting to the User Interface
Where this choice matters most: the UI. Imagine a graphical chess application.
Approach A: Validate after selection
The player selects a piece and a destination square. The game validates. If invalid, show an error.
UI workflow (validate after):
1. User clicks piece at e2
2. User clicks destination e5
3. Game checks: is e2-e5 valid?
4. If no: show error, user tries again
5. If yes: apply moveApproach B: Show valid moves upfront
The player selects a piece. The UI immediately shows which squares are valid targets. The player can only select from those.
UI workflow (valid moves upfront):
1. User clicks piece at e2
2. Game computes: valid_targets = [e3, e4]
3. UI highlights e3 and e4
4. User can only click highlighted squares
5. Apply the selected (guaranteed valid) move The second approach feels better---no errors, immediate feedback, guided interaction. But it requires the game to expose valid_actions(state) or valid_targets_for(piece).
The Trade-off
In Approach A, the UI is dumb. It just sends positions. The game logic is centralized in validation.
In Approach B, the UI must understand valid moves to show them. Does it compute them itself? Then we have duplicate logic---move rules in both game and UI. Or the game provides them, and the UI just displays.
Our abstract BoardGame with valid_actions(state) supports Approach B naturally. The UI asks the game: "What can I do?" The game answers. The UI presents choices. No validation needed---every choice is already valid.
This is the power of the abstraction: it doesn't just enable AI to play any game. It enables any interface---graphical, text, voice, gesture---to present valid options without understanding game rules.
AI That Plays Any Game
The abstraction enables generic AI. Consider the minimax algorithm---a decision-making strategy for two-player games where one player's gain is the other's loss.
The insight is beautifully simple: I want to maximize my advantage, while my opponent wants to minimize it (from my perspective). So at my turn, I pick the move that leads to the highest value. At my opponent's turn, I assume they'll pick the move that leads to the lowest value for me.
The algorithm explores possible futures like a tree---each branch a possible move, each level alternating between players. At the leaves (where we stop looking), we evaluate how good the position is. Then we work backward: minimize at opponent's levels, maximize at ours.
// Minimax works for ANY two-player zero-sum game
minimax(game, state, depth, maximizing):
if depth == 0 OR game.is_terminal(state):
return evaluate(game, state)
if maximizing:
game.valid_actions(state)
|> map((a) => minimax(game, game.apply_action(state, a),
depth-1, false))
|> max
else:
game.valid_actions(state)
|> map((a) => minimax(game, game.apply_action(state, a),
depth-1, true))
|> minThis same minimax code can play chess, checkers, Othello, Connect Four---any game that fits the framework.
What We Gain
From abstraction, we gain:
- Reuse: Game loops, AI, replay systems work across all games
- Clarity: The framework reveals what board games have in common
- Extensibility: New games plug into existing infrastructure
- Testing: Test the framework once, benefit everywhere
What We Lose
Abstraction has costs:
- Indirection: More layers between code and behavior
- Constraints: The framework may not fit every game perfectly
- Complexity: Understanding requires grasping the abstract structure
Not everything is a board game. The abstraction has boundaries.
The Art of Abstraction
Finding the right abstraction is an art:
- Too specific: only works for one case (useless)
- Too general: so vague it provides no guidance (also useless)
- Just right: captures the common essence while allowing variation
Our board game framework works because:
- Many games genuinely share this structure
- The variation points (board shape, piece types, rules) are cleanly separated
- The interface is simple: a few functions, clearly defined
The Complete Game: Chess in Full
Let's see the complete chess implementation---our abstraction made concrete. This is not pseudocode for illustration; it's a complete, working specification.
The Core Types
Color = white | black
PieceKind = king | queen | rook | bishop | knight | pawn
Piece:
kind: PieceKind
color: Color
has_moved: Boolean
Square = (file: a..h, rank: 1..8)
Move:
piece: Piece
from: Square
to: Square
promotion: Maybe PieceKind // for pawn promotion
is_castling: Boolean
is_en_passant: BooleanThe Board
Board = Square -> Maybe Piece // each square: piece or empty
initial_board:
// White pieces
a1: Rook(white), b1: Knight(white), c1: Bishop(white),
d1: Queen(white), e1: King(white), f1: Bishop(white),
g1: Knight(white), h1: Rook(white)
a2..h2: Pawn(white)
// Black pieces
a8: Rook(black), b8: Knight(black), c8: Bishop(black),
d8: Queen(black), e8: King(black), f8: Bishop(black),
g8: Knight(black), h8: Rook(black)
a7..h7: Pawn(black)
// All other squares: emptyGame State
ChessState:
board: Board
current_player: Color
castling_rights:
white_kingside: Boolean
white_queenside: Boolean
black_kingside: Boolean
black_queenside: Boolean
en_passant_target: Maybe Square
halfmove_clock: Number // for fifty-move rule
fullmove_number: Number
initial_state = ChessState(
board: initial_board,
current_player: white,
castling_rights: all true,
en_passant_target: Nothing,
halfmove_clock: 0,
fullmove_number: 1
)Movement Patterns
// Direction vectors for piece movement
orthogonal = [(0,1), (0,-1), (1,0), (-1,0)]
diagonal = [(1,1), (1,-1), (-1,1), (-1,-1)]
knight_jumps = [(2,1), (2,-1), (-2,1), (-2,-1),
(1,2), (1,-2), (-1,2), (-1,-2)]
// Squares reachable by sliding in a direction
slide(from, direction, board):
squares = []
current = from + direction
while current is on board:
squares += current
if board[current] is occupied:
break
current = current + direction
return squares
// Squares a piece can reach (ignoring check)
reachable(piece, from, board):
match piece.kind:
king: neighbors(from) // all 8 adjacent
queen: slide_all(from, orthogonal + diagonal, board)
rook: slide_all(from, orthogonal, board)
bishop: slide_all(from, diagonal, board)
knight: from + each of knight_jumps (if on board)
pawn: pawn_moves(piece, from, board) pawn_moves(pawn, from, board):
direction = if pawn.color == white then +1 else -1
moves = []
// Forward one
one_ahead = (from.file, from.rank + direction)
if board[one_ahead] is empty:
moves += one_ahead
// Forward two (from starting rank, if path clear)
if not pawn.has_moved:
two_ahead = (from.file, from.rank + 2*direction)
if board[two_ahead] is empty:
moves += two_ahead
// Diagonal captures
for capture_file in [from.file - 1, from.file + 1]:
target = (capture_file, from.rank + direction)
if board[target] contains enemy piece:
moves += target
return movesMove Validation
is_legal(move, state):
piece = move.piece
// Basic reachability
if move.to not in reachable(piece, move.from, state.board):
return false
// Can't capture own pieces
if state.board[move.to] contains same color:
return false
// Would this leave our king in check?
new_state = apply_move(move, state)
if is_in_check(move.piece.color, new_state):
return false
return true
is_in_check(color, state):
king_position = find_king(color, state.board)
opponent = opposite(color)
// Is any opponent piece attacking the king?
for (square, piece) in state.board:
if piece.color == opponent:
if king_position in reachable(piece, square, state.board):
return true
return falseSpecial Moves
// Castling
can_castle(state, side): // side = kingside | queenside
color = state.current_player
rights = state.castling_rights
// Must have rights
if not rights[color, side]: return false
// King and rook squares
(king_from, king_to, rook_from, rook_to, path) =
castling_squares(color, side)
// Path must be clear
if any square in path is occupied: return false
// King can't be in check, move through check, or end in check
if is_in_check(color, state): return false
for square in [king_from, path..., king_to]:
if square_is_attacked(square, opposite(color), state):
return false
return true
// En passant
can_en_passant(pawn, from, to, state):
state.en_passant_target == to AND
to is diagonal from from AND
pawn is a pawnApplying Moves
apply_move(move, state):
new_board = state.board
// Move the piece
new_board[move.from] = empty
moved_piece = move.piece with { has_moved: true }
// Handle promotion
if move.promotion:
moved_piece = Piece(move.promotion, move.piece.color, true)
new_board[move.to] = moved_piece
// Handle castling (also move the rook)
if move.is_castling:
(rook_from, rook_to) = castling_rook_squares(move)
new_board[rook_from] = empty
new_board[rook_to] = state.board[rook_from] with { has_moved: true }
// Handle en passant (remove captured pawn)
if move.is_en_passant:
captured_pawn_square = (move.to.file, move.from.rank)
new_board[captured_pawn_square] = empty
// Update castling rights
new_rights = update_castling_rights(state.castling_rights, move)
// Update en passant target
new_en_passant =
if move.piece.kind == pawn AND |move.to.rank - move.from.rank| == 2:
(move.to.file, (move.from.rank + move.to.rank) / 2)
else:
Nothing
return ChessState(
board: new_board,
current_player: opposite(state.current_player),
castling_rights: new_rights,
en_passant_target: new_en_passant,
halfmove_clock: updated_clock(move, state),
fullmove_number: state.fullmove_number +
(if state.current_player == black then 1 else 0)
)Game Outcomes
valid_moves(state):
all_pieces_of(state.current_player, state.board)
|> flatmap((piece, square) =>
reachable(piece, square, state.board)
|> map((to) => Move(piece, square, to))
|> filter((move) => is_legal(move, state)))
is_checkmate(state):
is_in_check(state.current_player, state) AND
valid_moves(state) is empty
is_stalemate(state):
not is_in_check(state.current_player, state) AND
valid_moves(state) is empty
is_draw(state):
is_stalemate(state) OR
state.halfmove_clock >= 100 OR // fifty-move rule
is_insufficient_material(state) OR
is_threefold_repetition(state) // needs history
game_result(state):
if is_checkmate(state):
winner: opposite(state.current_player)
else if is_draw(state):
draw
else:
ongoingThe Complete Game
Chess implements BoardGame:
initial_state = initial_state
valid_actions(state) = valid_moves(state)
apply_action(state, move) = apply_move(move, state)
is_terminal(state) =
is_checkmate(state) OR is_draw(state)
winner(state) =
if is_checkmate(state):
Just(opposite(state.current_player))
else:
NothingThis is the complete chess game. Every legal move, every special rule, every game outcome---all derived from these definitions. The same minimax algorithm that plays tic-tac-toe can now play chess.
We've traveled from concrete chess moves to abstract board games, from specific implementations to general frameworks. In the final part, we look ahead---to a new era where the boundary between human intent and machine execution is being redrawn.