Files
dartchess/lib/src/position.dart
T
Vincent Velociter 8b47f9f778 Improve documentation
2022-09-06 12:40:03 +02:00

1134 lines
36 KiB
Dart

import './constants.dart';
import './square_set.dart';
import './attacks.dart';
import './models.dart';
import './board.dart';
import './setup.dart';
import './utils.dart';
/// A base class for playable chess or chess variant positions.
///
/// See [Chess] for a concrete implementation of standard rules.
abstract class Position<T extends Position<T>> {
const Position({
required this.board,
required this.turn,
required this.castles,
this.epSquare,
required this.halfmoves,
required this.fullmoves,
});
/// Piece positions on the board.
final Board board;
/// Side to move.
final Color turn;
/// Castling paths and unmoved rooks.
final Castles castles;
/// En passant target square.
final Square? epSquare;
/// Number of half-moves since the last capture or pawn move.
final int halfmoves;
/// Current move number.
final int fullmoves;
/// Abstract const constructor to be used by subclasses.
const Position._initial()
: board = Board.standard,
turn = Color.white,
castles = Castles.standard,
epSquare = null,
halfmoves = 0,
fullmoves = 1;
Position.fromSetupUnchecked(Setup setup)
: board = setup.board,
turn = setup.turn,
castles = Castles.fromSetup(setup),
epSquare = _validEpSquare(setup),
halfmoves = setup.halfmoves,
fullmoves = setup.fullmoves;
Position<T> _copyWith({
Board? board,
Color? turn,
Castles? castles,
Square? epSquare,
int? halfmoves,
int? fullmoves,
});
/// Checks if the game is over due to a special variant end condition.
bool get isVariantEnd;
/// Tests special variant winning, losing and drawing conditions.
Outcome? get variantOutcome;
/// Gets the FEN string of this position.
///
/// Contrary to the FEN given by [Setup], this should always be a legal
/// position.
String get fen {
return Setup(
board: board,
turn: turn,
unmovedRooks: castles.unmovedRooks,
epSquare: _legalEpSquare(),
halfmoves: halfmoves,
fullmoves: fullmoves,
).fen;
}
/// Tests if the king is in check.
bool get isCheck {
final king = board.kingOf(turn);
return king != null && checkers.isNotEmpty;
}
/// Tests if the game is over.
bool get isGameOver => isVariantEnd || isInsufficientMaterial || !hasSomeLegalMoves;
/// Tests for checkmate.
bool get isCheckmate => !isVariantEnd && checkers.isNotEmpty && !hasSomeLegalMoves;
/// Tests for stalemate.
bool get isStalemate => !isVariantEnd && checkers.isEmpty && !hasSomeLegalMoves;
/// The outcome of the game, or `null` if the game is not over.
Outcome? get outcome {
if (variantOutcome != null) {
return variantOutcome;
} else if (isCheckmate) {
return Outcome(winner: opposite(turn));
} else if (isInsufficientMaterial || isStalemate) {
return Outcome(winner: null);
} else {
return null;
}
}
/// Tests if both [Color] have insufficient winning material.
bool get isInsufficientMaterial => Color.values.every((color) => hasInsufficientMaterial(color));
/// Tests if the position has at least one legal move.
bool get hasSomeLegalMoves {
final context = _makeContext();
for (final square in board.byColor(turn).squares) {
if (_legalMovesOf(square, context: context).isNotEmpty) return true;
}
return false;
}
/// Gets all the legal moves of this position.
Map<Square, SquareSet> get legalMoves {
final context = _makeContext();
if (context.isVariantEnd) return Map.unmodifiable({});
return Map.unmodifiable(
{for (final s in board.byColor(turn).squares) s: _legalMovesOf(s, context: context)});
}
/// SquareSet of pieces giving check.
SquareSet get checkers {
final king = board.kingOf(turn);
return king != null ? kingAttackers(king, opposite(turn)) : SquareSet.empty;
}
/// Attacks that a king on `square` would have to deal with.
SquareSet kingAttackers(Square square, Color attacker, {SquareSet? occupied}) {
return board.attacksTo(square, attacker, occupied: occupied);
}
/// Tests if a [Color] has insufficient winning material.
bool hasInsufficientMaterial(Color color) {
if (board.byColor(color).isIntersected(board.pawns | board.rooksAndQueens)) {
return false;
}
if (board.byColor(color).isIntersected(board.knights)) {
return (board.byColor(color).size <= 2 &&
board.byColor(opposite(color)).diff(board.kings).diff(board.queens).isEmpty);
}
if (board.byColor(color).isIntersected(board.bishops)) {
final sameColor = !board.bishops.isIntersected(SquareSet.darkSquares) ||
!board.bishops.isIntersected(SquareSet.lightSquares);
return sameColor && board.pawns.isEmpty && board.knights.isEmpty;
}
return true;
}
/// Tests a move for legality.
bool isLegal(Move move) {
if (move.promotion == Role.pawn) return false;
if (move.promotion == Role.king) return false;
if (move.promotion != null &&
(!board.pawns.has(move.from) || !SquareSet.backranks.has(move.to))) {
return false;
}
final legalMoves = _legalMovesOf(move.from);
return legalMoves.has(move.to) || legalMoves.has(normalizeMove(move).to);
}
/// Gets the legal moves for that [Square].
SquareSet legalMovesOf(Square square) {
return _legalMovesOf(square);
}
/// Returns the Standard Algebraic Notation of this [Move] from the current [Position].
String toSan(Move move) {
final san = _makeSanWithoutSuffix(move);
final newPos = playUnchecked(move);
if (newPos.outcome?.winner != null) return '$san#';
if (newPos.isCheck) return '$san+';
return san;
}
/// Plays a move and returns the SAN representation of the [Move] from the [Position].
///
/// Throws a [PlayError] if the move is not legal.
Tuple2<Position<T>, String> playToSan(Move move) {
if (isLegal(move)) {
final san = _makeSanWithoutSuffix(move);
final newPos = playUnchecked(move);
final suffixed = newPos.outcome?.winner != null
? '$san#'
: newPos.isCheck
? '$san+'
: san;
return Tuple2(newPos, suffixed);
} else {
throw PlayError('Invalid move');
}
}
/// Plays a move.
///
/// Throws a [PlayError] if the move is not legal.
Position<T> play(Move move) {
if (isLegal(move)) {
return playUnchecked(move);
} else {
throw PlayError('Invalid move');
}
}
/// Plays a move without checking if the move is legal.
Position<T> playUnchecked(Move move) {
final piece = board.pieceAt(move.from);
if (piece == null) {
return _copyWith();
}
final castlingSide = _getCastlingSide(move);
Square? newEpSquare;
Board newBoard = board.removePieceAt(move.from);
Castles newCastles = castles;
if (piece.role == Role.pawn) {
if (move.to == epSquare) {
newBoard = newBoard.removePieceAt(move.to + (turn == Color.white ? -8 : 8));
}
final delta = move.from - move.to;
if (delta.abs() == 16 && move.from >= 8 && move.from <= 55) {
newEpSquare = (move.from + move.to) >>> 1;
}
} else if (piece.role == Role.rook) {
newCastles = newCastles.discardRookAt(move.from);
} else if (piece.role == Role.king) {
if (castlingSide != null) {
final rookFrom = castles.rookOf(turn, castlingSide);
if (rookFrom != null) {
final rook = board.pieceAt(rookFrom);
newBoard = newBoard
.removePieceAt(rookFrom)
.setPieceAt(_kingCastlesTo(turn, castlingSide), piece);
if (rook != null) {
newBoard = newBoard.setPieceAt(_rookCastlesTo(turn, castlingSide), rook);
}
}
}
newCastles = newCastles.discardColor(turn);
}
if (castlingSide == null) {
final newPiece = move.promotion != null ? piece.copyWith(role: move.promotion!) : piece;
newBoard = newBoard.setPieceAt(move.to, newPiece);
}
final capturedPiece = castlingSide == null ? board.pieceAt(move.to) : null;
final isCapture = capturedPiece != null || move.to == epSquare;
if (capturedPiece != null && capturedPiece.role == Role.rook) {
newCastles = newCastles.discardRookAt(move.to);
}
return _copyWith(
halfmoves: isCapture || piece.role == Role.pawn ? 0 : halfmoves + 1,
fullmoves: turn == Color.black ? fullmoves + 1 : fullmoves,
board: newBoard,
turn: opposite(turn),
castles: newCastles,
epSquare: newEpSquare,
);
}
/// Returns the normalized form of a [Move] to avoid castling inconsistencies.
Move normalizeMove(Move move) {
final side = _getCastlingSide(move);
if (side == null) return move;
final castlingRook = castles.rookOf(turn, side);
return Move(
from: move.from,
to: castlingRook ?? move.to,
);
}
/// Checks the legality of this position.
///
/// Throws a [PositionError] if it does not meet basic validity requirements.
void validate({bool? ignoreImpossibleCheck}) {
if (board.occupied.isEmpty) {
throw PositionError(IllegalSetup.empty);
}
if (board.kings.size != 2) {
throw PositionError(IllegalSetup.kings);
}
final ourKing = board.kingOf(turn);
if (ourKing == null) {
throw PositionError(IllegalSetup.kings);
}
final otherKing = board.kingOf(opposite(turn));
if (otherKing == null) {
throw PositionError(IllegalSetup.kings);
}
if (kingAttackers(otherKing, turn).isNotEmpty) {
throw PositionError(IllegalSetup.oppositeCheck);
}
if (SquareSet.backranks.isIntersected(board.pawns)) {
throw PositionError(IllegalSetup.pawnsOnBackrank);
}
final skipImpossibleCheck = ignoreImpossibleCheck ?? false;
if (!skipImpossibleCheck) {
_validateCheckers(ourKing);
}
}
/// Checks if checkers are legal in this position.
///
/// Throws a [PositionError.impossibleCheck] if it does not meet validity
/// requirements.
void _validateCheckers(Square ourKing) {
final checkers = kingAttackers(ourKing, opposite(turn));
if (checkers.isNotEmpty) {
if (epSquare != null) {
// The pushed pawn must be the only checker, or it has uncovered
// check by a single sliding piece.
final pushedTo = epSquare! ^ 8;
final pushedFrom = epSquare! ^ 24;
if (checkers.moreThanOne ||
(checkers.first != pushedTo &&
board
.attacksTo(ourKing, opposite(turn),
occupied: board.occupied.withoutSquare(pushedTo).withSquare(pushedFrom))
.isNotEmpty)) {
throw PositionError(IllegalSetup.impossibleCheck);
}
} else {
// Multiple sliding checkers aligned with king.
if (checkers.size > 2 ||
(checkers.size == 2 && ray(checkers.first!, checkers.last!).has(ourKing))) {
throw PositionError(IllegalSetup.impossibleCheck);
}
}
}
}
String _makeSanWithoutSuffix(Move move) {
String san = '';
final role = board.roleAt(move.from);
if (role == null) return '--';
if (role == Role.king &&
(board.byColor(turn).has(move.to) || (move.to - move.from).abs() == 2)) {
san = move.to > move.from ? 'O-O' : 'O-O-O';
} else {
final capture = board.occupied.has(move.to) ||
(role == Role.pawn && squareFile(move.from) != squareFile(move.to));
if (role != Role.pawn) {
san = role.char.toUpperCase();
// Disambiguation
SquareSet others;
if (role == Role.king) {
others = kingAttacks(move.to) & board.kings;
} else if (role == Role.queen) {
others = queenAttacks(move.to, board.occupied) & board.queens;
} else if (role == Role.rook) {
others = rookAttacks(move.to, board.occupied) & board.rooks;
} else if (role == Role.bishop) {
others = bishopAttacks(move.to, board.occupied) & board.bishops;
} else {
others = knightAttacks(move.to) & board.knights;
}
others = others.intersect(board.byColor(turn)).withoutSquare(move.from);
if (others.isNotEmpty) {
final ctx = _makeContext();
for (final from in others.squares) {
if (!_legalMovesOf(from, context: ctx).has(move.to)) {
others = others.withoutSquare(from);
}
}
if (others.isNotEmpty) {
bool row = false;
bool column = others.isIntersected(SquareSet.fromRank(squareRank(move.from)));
if (others.isIntersected(SquareSet.fromFile(squareFile(move.from)))) {
row = true;
} else {
column = true;
}
if (column) {
san += kFileNames[squareFile(move.from)];
}
if (row) {
san += kRankNames[squareRank(move.from)];
}
}
}
} else if (capture) {
san = kFileNames[squareFile(move.from)];
}
if (capture) san += 'x';
san += toAlgebraic(move.to);
if (move.promotion != null) san += '=${move.promotion!.char.toUpperCase()}';
}
return san;
}
/// Gets the legal moves for that [Square].
///
/// Optionnaly pass a [_Context] of the position, to optimize performance when
/// calling this method several times.
SquareSet _legalMovesOf(Square square, {_Context? context}) {
final ctx = context ?? _makeContext();
if (ctx.isVariantEnd) return SquareSet.empty;
final piece = board.pieceAt(square);
if (piece == null || piece.color != turn) return SquareSet.empty;
final king = ctx.king;
if (king == null) return SquareSet.empty;
SquareSet pseudo;
SquareSet? legalEpSquare;
if (piece.role == Role.pawn) {
pseudo = pawnAttacks(turn, square) & board.byColor(opposite(turn));
final delta = turn == Color.white ? 8 : -8;
final step = square + delta;
if (0 <= step && step < 64 && !board.occupied.has(step)) {
pseudo = pseudo.withSquare(step);
final canDoubleStep = turn == Color.white ? square < 16 : square >= 64 - 16;
final doubleStep = step + delta;
if (canDoubleStep && !board.occupied.has(doubleStep)) {
pseudo = pseudo.withSquare(doubleStep);
}
}
if (epSquare != null && _canCaptureEp(square)) {
final pawn = epSquare! - delta;
if (ctx.checkers.isEmpty || ctx.checkers.singleSquare == pawn) {
legalEpSquare = SquareSet.fromSquare(epSquare!);
}
}
} else if (piece.role == Role.bishop) {
pseudo = bishopAttacks(square, board.occupied);
} else if (piece.role == Role.knight) {
pseudo = knightAttacks(square);
} else if (piece.role == Role.rook) {
pseudo = rookAttacks(square, board.occupied);
} else if (piece.role == Role.queen) {
pseudo = queenAttacks(square, board.occupied);
} else {
pseudo = kingAttacks(square);
}
pseudo = pseudo.diff(board.byColor(turn));
if (piece.role == Role.king) {
final occ = board.occupied.withoutSquare(square);
for (final to in pseudo.squares) {
if (kingAttackers(to, opposite(turn), occupied: occ).isNotEmpty) {
pseudo = pseudo.withoutSquare(to);
}
}
return pseudo
.union(_castlingMove(CastlingSide.queen, ctx))
.union(_castlingMove(CastlingSide.king, ctx));
}
if (ctx.checkers.isNotEmpty) {
final checker = ctx.checkers.singleSquare;
if (checker == null) return SquareSet.empty;
pseudo = pseudo & between(checker, king).withSquare(checker);
}
if (ctx.blockers.has(square)) {
pseudo = pseudo & ray(square, king);
}
if (legalEpSquare != null) {
pseudo = pseudo | legalEpSquare;
}
return pseudo;
}
_Context _makeContext() {
final king = board.kingOf(turn);
if (king == null) {
return _Context(
isVariantEnd: isVariantEnd,
king: king,
blockers: SquareSet.empty,
checkers: SquareSet.empty);
}
return _Context(
isVariantEnd: isVariantEnd,
king: king,
blockers: _sliderBlockers(king),
checkers: checkers,
);
}
SquareSet _sliderBlockers(Square king) {
final snipers = rookAttacks(king, SquareSet.empty)
.intersect(board.rooksAndQueens)
.union(bishopAttacks(king, SquareSet.empty).intersect(board.bishopsAndQueens))
.intersect(board.byColor(opposite(turn)));
SquareSet blockers = SquareSet.empty;
for (final sniper in snipers.squares) {
final b = between(king, sniper) & board.occupied;
if (!b.moreThanOne) blockers = blockers | b;
}
return blockers;
}
SquareSet _castlingMove(CastlingSide side, _Context context) {
final king = context.king;
if (king == null || context.checkers.isNotEmpty) {
return SquareSet.empty;
}
final rook = castles.rookOf(turn, side);
if (rook == null) return SquareSet.empty;
if (castles.pathOf(turn, side).isIntersected(board.occupied)) {
return SquareSet.empty;
}
final kingTo = _kingCastlesTo(turn, side);
final kingPath = between(king, kingTo);
final occ = board.occupied.withoutSquare(king);
for (final sq in kingPath.squares) {
if (kingAttackers(sq, opposite(turn), occupied: occ).isNotEmpty) {
return SquareSet.empty;
}
}
final rookTo = _rookCastlesTo(turn, side);
final after = board.occupied.toggleSquare(king).toggleSquare(rook).toggleSquare(rookTo);
if (kingAttackers(kingTo, opposite(turn), occupied: after).isNotEmpty) {
return SquareSet.empty;
}
return SquareSet.fromSquare(rook);
}
bool _canCaptureEp(Square pawn) {
if (epSquare == null) return false;
if (!pawnAttacks(turn, pawn).has(epSquare!)) return false;
final king = board.kingOf(turn);
if (king == null) return true;
final captured = epSquare! + (turn == Color.white ? -8 : 8);
final occupied =
board.occupied.toggleSquare(pawn).toggleSquare(epSquare!).toggleSquare(captured);
return !board.attacksTo(king, opposite(turn), occupied: occupied).isIntersected(occupied);
}
/// Detects if a move is a castling move.
///
/// Returns the [CastlingSide] or `null` if the move is a regular move.
CastlingSide? _getCastlingSide(Move move) {
final delta = move.to - move.from;
if (delta.abs() != 2 && !board.byColor(turn).has(move.to)) {
return null;
}
if (!board.kings.has(move.from)) {
return null;
}
return delta > 0 ? CastlingSide.king : CastlingSide.queen;
}
Square? _legalEpSquare() {
if (epSquare == null) return null;
final ourPawns = board.piecesOf(turn, Role.pawn);
final candidates = ourPawns & pawnAttacks(opposite(turn), epSquare!);
for (final candidate in candidates.squares) {
if (_legalMovesOf(candidate).has(epSquare!)) {
return epSquare;
}
}
return null;
}
}
/// A standard chess position.
class Chess extends Position<Chess> {
const Chess({
required super.board,
required super.turn,
required super.castles,
super.epSquare,
required super.halfmoves,
required super.fullmoves,
});
Chess.fromSetupUnchecked(Setup setup) : super.fromSetupUnchecked(setup);
const Chess._initial() : super._initial();
static const initial = Chess._initial();
@override
bool get isVariantEnd => false;
@override
Outcome? get variantOutcome => null;
/// Set up a playable [Chess] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
factory Chess.fromSetup(Setup setup, {bool? ignoreImpossibleCheck}) {
final unchecked = Chess.fromSetupUnchecked(setup);
unchecked.validate(ignoreImpossibleCheck: ignoreImpossibleCheck);
return unchecked;
}
@override
Position<Chess> _copyWith({
Board? board,
Color? turn,
Castles? castles,
Square? epSquare,
int? halfmoves,
int? fullmoves,
}) {
return Chess(
board: board ?? this.board,
turn: turn ?? this.turn,
castles: castles ?? this.castles,
epSquare: epSquare,
halfmoves: halfmoves ?? this.halfmoves,
fullmoves: fullmoves ?? this.fullmoves,
);
}
}
/// A variant of chess where captures cause an explosion to the surrounding pieces.
class Atomic extends Position<Atomic> {
const Atomic({
required super.board,
required super.turn,
required super.castles,
super.epSquare,
required super.halfmoves,
required super.fullmoves,
});
Atomic.fromSetupUnchecked(Setup setup) : super.fromSetupUnchecked(setup);
const Atomic._initial() : super._initial();
static const initial = Atomic._initial();
@override
bool get isVariantEnd => variantOutcome != null;
@override
Outcome? get variantOutcome {
for (final color in Color.values) {
if (board.piecesOf(color, Role.king).isEmpty) {
return Outcome(winner: opposite(color));
}
}
return null;
}
/// Set up a playable [Atomic] position.
///
/// Throws a [PositionError] if the [Setup] does not meet basic validity
/// requirements.
/// Optionnaly pass a `ignoreImpossibleCheck` boolean if you want to skip that
/// requirement.
factory Atomic.fromSetup(Setup setup, {bool? ignoreImpossibleCheck}) {
final unchecked = Atomic.fromSetupUnchecked(setup);
unchecked.validate(ignoreImpossibleCheck: ignoreImpossibleCheck);
return unchecked;
}
/// Attacks that a king on `square` would have to deal with.
///
/// Contrary to chess, in Atomic kings can attack each other, without causing
/// check.
@override
SquareSet kingAttackers(Square square, Color attacker, {SquareSet? occupied}) {
final attackerKings = board.piecesOf(attacker, Role.king);
if (attackerKings.isEmpty || kingAttacks(square).isIntersected(attackerKings)) {
return SquareSet.empty;
}
return super.kingAttackers(square, attacker, occupied: occupied);
}
/// Checks the legality of this position.
///
/// Validation is kike chess, but it allows our king to be missing.
/// Throws a [PositionError] if it does not meet basic validity requirements.
@override
void validate({bool? ignoreImpossibleCheck}) {
if (board.occupied.isEmpty) {
throw PositionError.empty;
}
if (board.kings.size > 2) {
throw PositionError.kings;
}
final otherKing = board.kingOf(opposite(turn));
if (otherKing == null) {
throw PositionError.kings;
}
if (kingAttackers(otherKing, turn).isNotEmpty) {
throw PositionError(IllegalSetup.oppositeCheck);
}
if (SquareSet.backranks.isIntersected(board.pawns)) {
throw PositionError(IllegalSetup.pawnsOnBackrank);
}
final skipImpossibleCheck = ignoreImpossibleCheck ?? false;
final ourKing = board.kingOf(turn);
if (!skipImpossibleCheck && ourKing != null) {
_validateCheckers(ourKing);
}
}
@override
void _validateCheckers(Square ourKing) {
// Other king moving away can cause many checks to be given at the
// same time. Not checking details or even that the king is close enough.
if (epSquare == null) {
super._validateCheckers(ourKing);
}
}
/// Plays a move without checking if the move is legal.
///
/// In addition to standard rules, all captures cause an explosion by which
/// the captured piece, the piece used to capture, and all surrounding pieces
/// except pawns that are within a one square radius are removed from the
/// board.
@override
Position<Atomic> playUnchecked(Move move) {
final castlingSide = _getCastlingSide(move);
final capturedPiece = castlingSide == null ? board.pieceAt(move.to) : null;
final isCapture = capturedPiece != null || move.to == epSquare;
final newPos = super.playUnchecked(move);
if (isCapture) {
Castles newCastles = newPos.castles;
Board newBoard = newPos.board.removePieceAt(move.to);
for (final explode
in kingAttacks(move.to).intersect(newBoard.occupied).diff(newBoard.pawns).squares) {
final piece = newBoard.pieceAt(explode);
newBoard = newBoard.removePieceAt(explode);
if (piece != null) {
if (piece.role == Role.rook) {
newCastles = newCastles.discardRookAt(explode);
}
if (piece.role == Role.king) {
newCastles = newCastles.discardColor(piece.color);
}
}
}
return newPos._copyWith(board: newBoard, castles: newCastles);
} else {
return newPos;
}
}
/// Tests if a [Color] has insufficient winning material.
@override
bool hasInsufficientMaterial(Color color) {
// Remaining material does not matter if the enemy king is already
// exploded.
if (board.piecesOf(opposite(color), Role.king).isEmpty) return false;
// Bare king cannot mate.
if (board.byColor(color).diff(board.kings).isEmpty) return true;
// As long as the enemy king is not alone, there is always a chance their
// own pieces explode next to it.
if (board.byColor(opposite(color)).diff(board.kings).isNotEmpty) {
// Unless there are only bishops that cannot explode each other.
if (board.occupied == board.bishops | board.kings) {
if (!(board.bishops & board.white).isIntersected(SquareSet.darkSquares)) {
return !(board.bishops & board.black).isIntersected(SquareSet.lightSquares);
}
if (!(board.bishops & board.white).isIntersected(SquareSet.lightSquares)) {
return !(board.bishops & board.black).isIntersected(SquareSet.darkSquares);
}
}
return false;
}
// Queen or pawn (future queen) can give mate against bare king.
if (board.queens.isNotEmpty || board.pawns.isNotEmpty) return false;
// Single knight, bishop or rook cannot mate against bare king.
if ((board.knights | board.bishops | board.rooks).size == 1) {
return true;
}
// If only knights, more than two are required to mate bare king.
if (board.occupied == board.knights | board.kings) {
return board.knights.size <= 2;
}
return false;
}
@override
SquareSet _legalMovesOf(Square square, {_Context? context}) {
SquareSet moves = SquareSet.empty;
final ctx = context ?? _makeContext();
for (final to in _pseudoLegalMoves(this, square, ctx).squares) {
final after = playUnchecked(Move(from: square, to: to));
final ourKing = after.board.kingOf(turn);
if (ourKing != null &&
(after.board.kingOf(after.turn) == null ||
after.kingAttackers(ourKing, after.turn).isEmpty)) {
moves = moves.withSquare(to);
}
}
return moves;
}
@override
Position<Atomic> _copyWith({
Board? board,
Color? turn,
Castles? castles,
Square? epSquare,
int? halfmoves,
int? fullmoves,
}) {
return Atomic(
board: board ?? this.board,
turn: turn ?? this.turn,
castles: castles ?? this.castles,
epSquare: epSquare,
halfmoves: halfmoves ?? this.halfmoves,
fullmoves: fullmoves ?? this.fullmoves,
);
}
}
/// The outcome of a [Position]. No `winner` means a draw.
class Outcome {
const Outcome({this.winner});
final Color? winner;
static const whiteWins = Outcome(winner: Color.white);
static const blackWins = Outcome(winner: Color.black);
static const draw = Outcome();
@override
toString() {
return 'winner: $winner';
}
@override
bool operator ==(Object other) => other is Outcome && winner == other.winner;
@override
int get hashCode => winner.hashCode;
}
enum IllegalSetup {
/// There are no pieces on the board.
empty,
/// The player not to move is in check.
oppositeCheck,
/// There are impossibly many checkers, two sliding checkers are
/// aligned, or check is not possible because the last move was a
/// double pawn push.
///
/// Such a position cannot be reached by any sequence of legal moves.
impossibleCheck,
/// There are pawns on the backrank.
pawnsOnBackrank,
/// A king is missing, or there are too many kings.
kings,
/// A variant specific rule is violated.
variant,
}
class PlayError implements Exception {
final String message;
const PlayError(this.message);
}
/// Error when trying to create a [Position] from an illegal [Setup].
class PositionError implements Exception {
final IllegalSetup cause;
const PositionError(this.cause);
static const empty = PositionError(IllegalSetup.empty);
static const oppositeCheck = PositionError(IllegalSetup.oppositeCheck);
static const impossibleCheck = PositionError(IllegalSetup.impossibleCheck);
static const pawnsOnBackrank = PositionError(IllegalSetup.pawnsOnBackrank);
static const kings = PositionError(IllegalSetup.kings);
static const variant = PositionError(IllegalSetup.variant);
}
enum CastlingSide {
queen,
king;
}
class Castles {
final SquareSet unmovedRooks;
/// Rooks positions pair.
///
/// First item is queen side, second is king side.
final ByColor<Tuple2<Square?, Square?>> rook;
/// Squares between the white king and rooks.
///
/// First item is queen side, second is king side.
final ByColor<Tuple2<SquareSet, SquareSet>> path;
const Castles({
required this.unmovedRooks,
required this.rook,
required this.path,
});
static const standard = Castles(
unmovedRooks: SquareSet.corners,
rook: {Color.white: Tuple2(0, 7), Color.black: Tuple2(56, 63)},
path: {
Color.white: Tuple2(SquareSet(0x000000000000000e), SquareSet(0x0000000000000060)),
Color.black: Tuple2(SquareSet(0x0e00000000000000), SquareSet(0x6000000000000000))
},
);
static const empty = Castles(
unmovedRooks: SquareSet.empty,
rook: {Color.white: Tuple2(null, null), Color.black: Tuple2(null, null)},
path: {
Color.white: Tuple2(SquareSet.empty, SquareSet.empty),
Color.black: Tuple2(SquareSet.empty, SquareSet.empty)
},
);
factory Castles.fromSetup(Setup setup) {
Castles castles = Castles.empty;
final rooks = setup.unmovedRooks & setup.board.rooks;
for (final color in Color.values) {
final backrank = SquareSet.backrankOf(color);
final king = setup.board.kingOf(color);
if (king == null || !backrank.has(king)) continue;
final side = rooks & setup.board.byColor(color) & backrank;
if (side.first != null && side.first! < king) {
castles = castles._add(color, CastlingSide.queen, king, side.first!);
}
if (side.last != null && king < side.last!) {
castles = castles._add(color, CastlingSide.king, king, side.last!);
}
}
return castles;
}
/// Gets the rook [Square] by color and castling side.
Square? rookOf(Color color, CastlingSide side) =>
side == CastlingSide.queen ? rook[color]!.item1 : rook[color]!.item2;
/// Gets the squares that need to be empty so that castling is possible
/// on the given side.
///
/// We're assuming the player still has the required castling rigths.
SquareSet pathOf(Color color, CastlingSide side) =>
side == CastlingSide.queen ? path[color]!.item1 : path[color]!.item2;
Castles discardRookAt(Square square) {
final whiteRook = rook[Color.white]!;
final blackRook = rook[Color.black]!;
return unmovedRooks.has(square)
? _copyWith(
unmovedRooks: unmovedRooks.withoutSquare(square),
rook: {
if (square <= 7)
Color.white: whiteRook.item1 == square
? whiteRook.withItem1(null)
: whiteRook.item2 == square
? whiteRook.withItem2(null)
: whiteRook,
if (square >= 56)
Color.black: blackRook.item1 == square
? blackRook.withItem1(null)
: blackRook.item2 == square
? blackRook.withItem2(null)
: blackRook,
},
)
: this;
}
Castles discardColor(Color color) {
return _copyWith(
unmovedRooks: unmovedRooks.diff(SquareSet.backrankOf(color)),
rook: {
color: Tuple2(null, null),
},
);
}
Castles _add(Color color, CastlingSide side, Square king, Square rook) {
final kingTo = _kingCastlesTo(color, side);
final rookTo = _rookCastlesTo(color, side);
final path = between(rook, rookTo)
.withSquare(rookTo)
.union(between(king, kingTo).withSquare(kingTo))
.withoutSquare(king)
.withoutSquare(rook);
return _copyWith(
unmovedRooks: unmovedRooks.withSquare(rook),
rook: {
color: side == CastlingSide.queen
? this.rook[color]!.withItem1(rook)
: this.rook[color]!.withItem2(rook),
},
path: {
color: side == CastlingSide.queen
? this.path[color]!.withItem1(path)
: this.path[color]!.withItem2(path),
},
);
}
Castles _copyWith({
SquareSet? unmovedRooks,
ByColor<Tuple2<Square?, Square?>>? rook,
ByColor<Tuple2<SquareSet, SquareSet>>? path,
}) {
return Castles(
unmovedRooks: unmovedRooks ?? this.unmovedRooks,
rook: rook != null ? Map.unmodifiable({...this.rook, ...rook}) : this.rook,
path: path != null ? Map.unmodifiable({...this.path, ...path}) : this.path,
);
}
@override
toString() => 'Castles(unmovedRooks: $unmovedRooks, rook: $rook, path: $path)';
@override
bool operator ==(Object other) =>
other is Castles &&
other.unmovedRooks == unmovedRooks &&
other.rook[Color.white] == rook[Color.white] &&
other.rook[Color.black] == rook[Color.black] &&
other.path[Color.white] == path[Color.white] &&
other.path[Color.black] == path[Color.black];
@override
int get hashCode => Object.hash(
unmovedRooks, rook[Color.white], rook[Color.black], path[Color.white], path[Color.black]);
}
class _Context {
const _Context({
required this.isVariantEnd,
required this.king,
required this.blockers,
required this.checkers,
});
final bool isVariantEnd;
final Square? king;
final SquareSet blockers;
final SquareSet checkers;
}
Square _rookCastlesTo(Color color, CastlingSide side) {
return color == Color.white
? (side == CastlingSide.queen ? 3 : 5)
: side == CastlingSide.queen
? 59
: 61;
}
Square _kingCastlesTo(Color color, CastlingSide side) {
return color == Color.white
? (side == CastlingSide.queen ? 2 : 6)
: side == CastlingSide.queen
? 58
: 62;
}
Square? _validEpSquare(Setup setup) {
if (setup.epSquare == null) return null;
final epRank = setup.turn == Color.white ? 5 : 2;
final forward = setup.turn == Color.white ? 8 : -8;
if (squareRank(setup.epSquare!) != epRank) return null;
if (setup.board.occupied.has(setup.epSquare! + forward)) return null;
final pawn = setup.epSquare! - forward;
if (!setup.board.pawns.has(pawn) || !setup.board.byColor(opposite(setup.turn)).has(pawn)) {
return null;
}
return setup.epSquare;
}
SquareSet _pseudoLegalMoves(Position pos, Square square, _Context context) {
if (pos.isVariantEnd) return SquareSet.empty;
final piece = pos.board.pieceAt(square);
if (piece == null || piece.color != pos.turn) return SquareSet.empty;
SquareSet pseudo = attacks(piece, square, pos.board.occupied);
if (piece.role == Role.pawn) {
SquareSet captureTargets = pos.board.byColor(opposite(pos.turn));
if (pos.epSquare != null) {
captureTargets = captureTargets.withSquare(pos.epSquare!);
}
pseudo = pseudo & captureTargets;
final delta = pos.turn == Color.white ? 8 : -8;
final step = square + delta;
if (0 <= step && step < 64 && !pos.board.occupied.has(step)) {
pseudo = pseudo.withSquare(step);
final canDoubleStep = pos.turn == Color.white ? square < 16 : square >= 64 - 16;
final doubleStep = step + delta;
if (canDoubleStep && !pos.board.occupied.has(doubleStep)) {
pseudo = pseudo.withSquare(doubleStep);
}
}
return pseudo;
} else {
pseudo = pseudo.diff(pos.board.byColor(pos.turn));
}
if (square == context.king) {
return pseudo
.union(pos._castlingMove(CastlingSide.queen, context))
.union(pos._castlingMove(CastlingSide.king, context));
} else {
return pseudo;
}
}