use crate::{
attacks::ATTACKS,
bitboard::Bitboard,
board::Board,
castles::Castles,
color::Color,
history::History,
mve::Move,
piece::Piece,
ply::Ply,
role::{PromotableRole, Role},
side::Side,
square::{self, Square},
uci::Uci,
};
#[derive(Debug, Clone, PartialEq)]
pub struct Position {
board: Board,
history: History,
color: Color,
ply: Ply,
}
#[derive(Debug, Clone)]
pub struct Undo {
prior_board: Board,
prior_castles: crate::castles::Castles,
prior_unmoved_rooks: crate::unmoved_rooks::UnmovedRooks,
prior_half_move_clock: crate::halfmoveclock::HalfMoveClock,
prior_last_move: Option<Uci>,
prior_position_hashes: crate::hash::PositionHash,
}
impl Position {
pub fn new() -> Self {
Self {
board: Board::new(),
history: History::new(),
color: Color::White,
ply: Ply::new(),
}
}
pub fn new_no_repetition() -> Self {
Self {
board: Board::new(),
history: History::new_no_repetition(),
color: Color::White,
ply: Ply::new(),
}
}
#[must_use]
pub fn without_repetition(self) -> Self {
Self {
history: History {
position_hashes: crate::hash::PositionHash::disabled(),
..self.history
},
..self
}
}
pub fn with_board(self, board: Board) -> Self {
Self { board, ..self }
}
pub fn with_color(self, color: Color) -> Self {
Self { color, ..self }
}
pub fn change_color(self) -> Self {
let color = self.color;
self.with_color(color.opponent())
}
pub fn with_history(self, history: History) -> Self {
Self { history, ..self }
}
pub fn with_castles(self, castles: Castles) -> Self {
Self {
history: self.history.with_castles(castles),
..self
}
}
pub fn with_ply(self, ply: Ply) -> Self {
Self { ply, ..self }
}
pub fn update_history<F>(self, f: F) -> Self
where
F: FnOnce(&History) -> History,
{
Self {
history: f(&self.history),
..self
}
}
pub fn mve(
mut self,
orig: Square,
dest: Square,
promotion: Option<PromotableRole>,
) -> Option<Self> {
let mve = self
.valid_moves()
.find(|m| m.orig == orig && m.dest == dest && m.promotion == promotion)?;
self.make(&mve);
Some(self)
}
pub fn make(&mut self, mve: &Move) -> Undo {
let prior_board = self.board;
let prior_castles = self.history.castles;
let prior_unmoved_rooks = self.history.unmoved_rooks;
let prior_half_move_clock = self.history.half_move_clock;
let prior_last_move = self.history.last_move;
let prior_position_hashes = std::mem::replace(
&mut self.history.position_hashes,
crate::hash::PositionHash::Disabled,
);
let new_hashes = if prior_position_hashes.is_disabled() {
crate::hash::PositionHash::Disabled
} else {
let entry = crate::hash::PositionHash::from_hash(crate::hash::Hash::from_position(self));
entry.combine(&prior_position_hashes)
};
self.history.position_hashes = new_hashes;
self.history.last_move = Some((*mve).into());
self.history.castles = self.history.castles.update(mve);
self.history.unmoved_rooks = self.history.unmoved_rooks.update(mve);
self.history.half_move_clock = if mve.piece.role == Role::Pawn || mve.capture.is_some() {
self.history.half_move_clock.reset()
} else {
self.history.half_move_clock.incr()
};
apply_move(&mut self.board, mve);
self.color = self.color.opponent();
self.ply = self.ply.incr();
Undo {
prior_board,
prior_castles,
prior_unmoved_rooks,
prior_half_move_clock,
prior_last_move,
prior_position_hashes,
}
}
pub fn unmake(&mut self, undo: Undo) {
self.ply = self.ply.decr();
self.color = self.color.opponent();
self.board = undo.prior_board;
self.history.castles = undo.prior_castles;
self.history.unmoved_rooks = undo.prior_unmoved_rooks;
self.history.half_move_clock = undo.prior_half_move_clock;
self.history.last_move = undo.prior_last_move;
self.history.position_hashes = undo.prior_position_hashes;
}
pub fn board(&self) -> &Board {
&self.board
}
pub fn color(&self) -> Color {
self.color
}
pub fn history(&self) -> &History {
&self.history
}
pub fn ply(&self) -> Ply {
self.ply
}
pub fn is_check(&self) -> bool {
self.board.is_check(self.color)
}
pub fn enpassant_square(&self) -> Option<Square> {
self.history
.last_move
.and_then(|lm| potential_enpassant_sq(lm, self.board, self.color))
}
pub fn valid_moves(&self) -> impl Iterator<Item = Move> {
let ctx = LegalityContext::compute(self);
let board = self.board;
let color = self.color;
self.pawn_moves()
.chain(self.enpassant_moves())
.chain(self.king_moves())
.chain(self.knight_moves())
.chain(self.bishop_moves())
.chain(self.rook_moves())
.chain(self.queen_moves())
.filter(move |m| ctx.is_legal(m, board, color))
}
pub fn has_moves(&self) -> bool {
self.valid_moves().any(|_| true)
}
pub fn valid_moves_at(&self, orig: Square) -> impl Iterator<Item = Move> {
self.valid_moves().filter(move |m| m.orig == orig)
}
pub fn pawn_moves(&self) -> impl Iterator<Item = Move> {
let pawns = self.board.bypiece(Piece {
role: Role::Pawn,
color: self.color,
});
let captures = pawns
.flat_map(|from| {
(ATTACKS.pawn_attacks(self.color, from) & self.board.bycolor(self.color.opponent()))
.into_iter()
.map(move |to| (from, to))
})
.flat_map(|(from, to)| self.gen_pawn_moves(from, to, true));
let singles = !self.board.occupied()
& (match self.color {
Color::White => (self.board.white() & pawns) << 8,
Color::Black => (self.board.black() & pawns) >> 8,
});
let single_moves = singles.flat_map(|to| {
let from = Square(match self.color {
Color::White => to.0 - 8,
Color::Black => to.0 + 8,
});
self.gen_pawn_moves(from, to, false)
});
let doubles = !self.board.occupied()
& (match self.color {
Color::White => singles << 8,
Color::Black => singles >> 8,
})
& self.color.fourth_rank();
let double_moves = doubles.flat_map(|to| {
let from = Square(match self.color {
Color::White => to.0 - 16,
Color::Black => to.0 + 16,
});
self.gen_pawn_moves(from, to, false)
});
captures.chain(single_moves).chain(double_moves)
}
pub fn enpassant_moves(&self) -> impl Iterator<Item = Move> {
self.history
.last_move
.and_then(|last_move| {
let target = potential_enpassant_sq(last_move, self.board, self.color)?;
let our_pawns = self.board.bypiece(Piece {
role: Role::Pawn,
color: self.color,
});
Some(
(ATTACKS.pawn_attacks(self.color.opponent(), target) & our_pawns)
.into_iter()
.filter_map(move |from| self.enpassant(from, target)),
)
})
.into_iter()
.flatten()
}
pub fn king_moves(&self) -> impl Iterator<Item = Move> {
let orig = self.board.king(self.color);
let board_without_king = self.board.pop(orig).0;
let color = self.color;
let moves = ATTACKS.king_attacks(orig).filter_map(move |dest| {
(!board_without_king.is_attacked(dest, color)).then_some(self.normal(
orig,
dest,
Role::King,
)?)
});
moves.chain(self.castling_moves().into_iter().flatten())
}
pub fn knight_moves(&self) -> impl Iterator<Item = Move> {
let knights = self.board.bypiece(Piece {
role: Role::Knight,
color: self.color,
});
knights
.flat_map(|from| ATTACKS.knight_attacks(from).map(move |to| (from, to)))
.filter_map(|(from, to)| self.normal(from, to, Role::Knight))
}
pub fn bishop_moves(&self) -> impl Iterator<Item = Move> {
let bishops = self.board.bypiece(Piece {
role: Role::Bishop,
color: self.color,
});
bishops
.flat_map(|from| {
ATTACKS
.bishop_attacks(from, self.board.occupied())
.map(move |to| (from, to))
})
.filter_map(|(from, to)| self.normal(from, to, Role::Bishop))
}
pub fn rook_moves(&self) -> impl Iterator<Item = Move> {
let rooks = self.board.bypiece(Piece {
role: Role::Rook,
color: self.color,
});
rooks
.flat_map(|from| {
ATTACKS
.rook_attacks(from, self.board.occupied())
.map(move |to| (from, to))
})
.filter_map(|(from, to)| self.normal(from, to, Role::Rook))
}
pub fn queen_moves(&self) -> impl Iterator<Item = Move> {
let queens = self.board.bypiece(Piece {
role: Role::Queen,
color: self.color,
});
queens
.flat_map(|from| {
let bishops = ATTACKS
.bishop_attacks(from, self.board.occupied())
.map(move |to| (from, to));
let rooks = ATTACKS
.rook_attacks(from, self.board.occupied())
.map(move |to| (from, to));
bishops.chain(rooks)
})
.filter_map(|(from, to)| self.normal(from, to, Role::Queen))
}
fn castling_moves(&self) -> Option<impl Iterator<Item = Move>> {
if self.board.is_check(self.color) {
return None;
}
Some(
[Side::King, Side::Queen]
.into_iter()
.filter_map(|side| self.castle(side)),
)
}
fn castle(&self, side: Side) -> Option<Move> {
if !self.history.castles.can_side(self.color, side) {
return None;
}
let king_from = self.board.king(self.color);
let rook_from = self.color.castle_square(side);
if !self.history.unmoved_rooks.contains(rook_from) {
return None;
}
let (king_to, _rook_to, between, king_path) = castle_squares(self.color, side);
if (self.board.occupied() & between).is_non_empty() {
return None;
}
if king_path
.into_iter()
.any(|sq| self.board.is_attacked(sq, self.color))
{
return None;
}
Some(Move::castle(self.color, side, king_from, king_to))
}
fn normal(&self, orig: Square, dest: Square, role: Role) -> Option<Move> {
let piece = Piece {
role,
color: self.color,
};
if self.board.is_occupied(dest) {
if self.board.color_at(dest) == Some(self.color) {
return None;
}
let captured_role = self.board.role_at(dest)?;
Some(Move::capture(piece, orig, dest, dest, captured_role))
} else {
Some(Move::quiet(piece, orig, dest))
}
}
fn enpassant(&self, orig: Square, dest: Square) -> Option<Move> {
let captured_sq = Square::from_file_and_rank(dest.file(), orig.rank());
Some(Move::enpassant(self.color, orig, dest, captured_sq))
}
fn gen_pawn_moves(
&self,
from: Square,
to: Square,
is_capture: bool,
) -> impl Iterator<Item = Move> + '_ {
let is_promotion = from.rank() == self.color.seventh_rank();
let captured = if is_capture {
self.board.role_at(to).map(|r| (to, r))
} else {
None
};
let promotions = is_promotion
.then_some(PromotableRole::ROLES)
.into_iter()
.flatten()
.map(move |r| Move::promotion(self.color, from, to, r, captured));
let normal = (!is_promotion)
.then(|| self.normal(from, to, Role::Pawn))
.flatten()
.into_iter();
promotions.chain(normal)
}
}
struct LegalityContext {
king_sq: Square,
check_mask: Bitboard,
is_double_check: bool,
pinned: Bitboard,
}
impl LegalityContext {
fn compute(pos: &Position) -> Self {
let board = &pos.board;
let us = pos.color;
let them = us.opponent();
let king_sq = board.king(us);
let checkers = board.attackers(king_sq, them);
let n_checkers = checkers.0.count_ones();
let (check_mask, is_double_check) = if n_checkers == 0 {
(Bitboard(!0u64), false)
} else if n_checkers == 1 {
let checker_sq = Square(checkers.0.trailing_zeros() as u8);
let between =
Bitboard(ATTACKS.between[king_sq.0 as usize][checker_sq.0 as usize]);
(between | Bitboard::from(checker_sq), false)
} else {
(Bitboard::EMPTY, true)
};
let occupied = board.occupied();
let our_pieces = board.bycolor(us);
let opp_rq = board.bycolor(them) & (board.rooks() | board.queens());
let opp_bq = board.bycolor(them) & (board.bishops() | board.queens());
let mut pinned = Bitboard::EMPTY;
let rook_candidates = ATTACKS.rook_attacks(king_sq, Bitboard::EMPTY) & opp_rq;
for pinner_sq in rook_candidates {
let between =
Bitboard(ATTACKS.between[king_sq.0 as usize][pinner_sq.0 as usize]);
let blockers = between & occupied;
if blockers.0.count_ones() == 1 && (blockers & our_pieces).is_non_empty() {
pinned = pinned | blockers;
}
}
let bishop_candidates = ATTACKS.bishop_attacks(king_sq, Bitboard::EMPTY) & opp_bq;
for pinner_sq in bishop_candidates {
let between =
Bitboard(ATTACKS.between[king_sq.0 as usize][pinner_sq.0 as usize]);
let blockers = between & occupied;
if blockers.0.count_ones() == 1 && (blockers & our_pieces).is_non_empty() {
pinned = pinned | blockers;
}
}
Self {
king_sq,
check_mask,
is_double_check,
pinned,
}
}
#[inline(always)]
fn is_legal(&self, mve: &Move, board: Board, color: Color) -> bool {
if mve.piece.role == Role::King {
return true;
}
if self.is_double_check {
return false;
}
if mve.enpassant.is_some() {
let mut working = board;
apply_move(&mut working, mve);
return !working.is_check(color);
}
if !self.check_mask.is_set(mve.dest) {
return false;
}
if self.pinned.is_set(mve.orig) {
let ray =
Bitboard(ATTACKS.rays[self.king_sq.0 as usize][mve.orig.0 as usize]);
return ray.is_set(mve.dest);
}
true
}
}
#[inline(always)]
pub(crate) fn apply_move(board: &mut Board, mve: &Move) {
let mover = mve.piece;
if let Some(side) = mve.castle {
let color = mover.color;
let king = Piece {
role: Role::King,
color,
};
let rook = Piece {
role: Role::Rook,
color,
};
let rook_from = color.castle_square(side);
let (king_to, rook_to, _, _) = castle_squares(color, side);
board.toggle_piece(mve.orig, king);
board.toggle_piece(king_to, king);
board.toggle_piece(rook_from, rook);
board.toggle_piece(rook_to, rook);
return;
}
if let (Some(cap_sq), Some(cap_role)) = (mve.capture, mve.captured_role) {
let captured = Piece {
role: cap_role,
color: mover.color.opponent(),
};
board.toggle_piece(cap_sq, captured);
}
board.toggle_piece(mve.orig, mover);
let landed = match mve.promotion {
Some(p) => Piece {
role: match p {
PromotableRole::Queen => Role::Queen,
PromotableRole::Rook => Role::Rook,
PromotableRole::Bishop => Role::Bishop,
PromotableRole::Knight => Role::Knight,
},
color: mover.color,
},
None => mover,
};
board.toggle_piece(mve.dest, landed);
}
fn castle_squares(color: Color, side: Side) -> (Square, Square, Bitboard, Bitboard) {
match (color, side) {
(Color::White, Side::King) => (
square::G1,
square::F1,
Bitboard::from(square::F1) | Bitboard::from(square::G1),
Bitboard::from(square::F1) | Bitboard::from(square::G1),
),
(Color::White, Side::Queen) => (
square::C1,
square::D1,
Bitboard::from(square::B1) | Bitboard::from(square::C1) | Bitboard::from(square::D1),
Bitboard::from(square::C1) | Bitboard::from(square::D1),
),
(Color::Black, Side::King) => (
square::G8,
square::F8,
Bitboard::from(square::F8) | Bitboard::from(square::G8),
Bitboard::from(square::F8) | Bitboard::from(square::G8),
),
(Color::Black, Side::Queen) => (
square::C8,
square::D8,
Bitboard::from(square::B8) | Bitboard::from(square::C8) | Bitboard::from(square::D8),
Bitboard::from(square::C8) | Bitboard::from(square::D8),
),
}
}
fn potential_enpassant_sq(last_move: Uci, board: Board, color: Color) -> Option<Square> {
board.piece_at(last_move.dest).and_then(|piece| {
if piece.color != color
&& piece.role == Role::Pawn
&& last_move.orig.ydist(last_move.dest) == 2
{
last_move.dest.prev_rank(piece.color)
} else {
None
}
})
}
impl Default for Position {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::unmoved_rooks::UnmovedRooks;
fn pc(role: Role, color: Color) -> Piece {
Piece { role, color }
}
fn pos_from(b: Board) -> Position {
let history = History {
unmoved_rooks: UnmovedRooks::from_board(b),
..History::new()
};
Position::new().with_board(b).with_history(history)
}
#[test]
fn starting_position_has_20_moves() {
assert_eq!(Position::new().valid_moves().count(), 20);
}
#[test]
fn starting_position_pawn_moves_count() {
assert_eq!(Position::new().pawn_moves().count(), 16);
}
#[test]
fn starting_position_knight_moves_count() {
assert_eq!(Position::new().knight_moves().count(), 4);
}
#[test]
fn starting_position_no_castling() {
let castles: Vec<_> = Position::new()
.valid_moves()
.filter(|m| m.castle.is_some())
.collect();
assert!(
castles.is_empty(),
"no castles legal from start, got {castles:?}"
);
}
#[test]
fn starting_position_no_enpassant() {
assert_eq!(Position::new().enpassant_square(), None);
assert_eq!(Position::new().enpassant_moves().count(), 0);
}
#[test]
fn starting_position_no_promotion() {
let proms: Vec<_> = Position::new()
.valid_moves()
.filter(|m| m.promotion.is_some())
.collect();
assert!(proms.is_empty());
}
#[test]
fn starting_position_not_in_check() {
assert!(!Position::new().is_check());
}
#[test]
fn starting_position_has_moves() {
assert!(Position::new().has_moves());
}
#[test]
fn mve_flips_color() {
let after = Position::new().mve(square::E2, square::E4, None).unwrap();
assert_eq!(after.color(), Color::Black);
}
#[test]
fn mve_applies_move_to_board() {
let after = Position::new().mve(square::E2, square::E4, None).unwrap();
assert!(!after.board().is_occupied(square::E2));
assert_eq!(
after.board().piece_at(square::E4),
Some(pc(Role::Pawn, Color::White))
);
}
#[test]
fn mve_invalid_returns_none() {
assert!(Position::new().mve(square::E2, square::E5, None).is_none());
}
#[test]
fn mve_from_empty_square_returns_none() {
assert!(Position::new().mve(square::E4, square::E5, None).is_none());
}
#[test]
fn mve_updates_history_last_move() {
let after = Position::new().mve(square::E2, square::E4, None).unwrap();
let lm = after.history().last_move.expect("last_move set after mve");
assert_eq!(lm.orig, square::E2);
assert_eq!(lm.dest, square::E4);
}
#[test]
fn change_color_actually_flips() {
let p = Position::new();
assert_eq!(p.color(), Color::White);
let q = p.change_color();
assert_eq!(q.color(), Color::Black);
}
#[test]
fn pawn_double_push_emits_two_destinations() {
let moves: Vec<_> = Position::new().valid_moves_at(square::E2).collect();
assert_eq!(moves.len(), 2);
let dests: Vec<_> = moves.iter().map(|m| m.dest).collect();
assert!(dests.contains(&square::E3));
assert!(dests.contains(&square::E4));
}
#[test]
fn pawn_blocked_cannot_push() {
let b = Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::E8, pc(Role::King, Color::Black))
.set(square::E2, pc(Role::Pawn, Color::White))
.set(square::E3, pc(Role::Knight, Color::Black));
let p = pos_from(b);
let pushes: Vec<_> = p
.valid_moves_at(square::E2)
.filter(|m| m.capture.is_none())
.collect();
assert!(
pushes.is_empty(),
"pawn blocked on E3 cannot push to E3 or E4, got {pushes:?}"
);
}
#[test]
fn enpassant_offered_after_opposing_double_push() {
let b = Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::E8, pc(Role::King, Color::Black))
.set(square::E5, pc(Role::Pawn, Color::White))
.set(square::D5, pc(Role::Pawn, Color::Black));
let history = History {
last_move: Some(crate::uci::Uci {
orig: square::D7,
dest: square::D5,
promotion: None,
}),
unmoved_rooks: UnmovedRooks::from_board(b),
..History::new()
};
let p = Position::new().with_board(b).with_history(history);
assert_eq!(
p.enpassant_square(),
Some(square::D6),
"en passant target should be D6 (the square the black pawn passed)"
);
let eps: Vec<_> = p.enpassant_moves().collect();
assert_eq!(eps.len(), 1, "exactly one en-passant move available");
let m = eps[0];
assert_eq!(m.orig, square::E5);
assert_eq!(m.dest, square::D6);
assert_eq!(m.capture, Some(square::D5));
assert!(m.enpassant.is_some());
}
#[test]
fn enpassant_not_offered_after_single_push() {
let b = Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::E8, pc(Role::King, Color::Black))
.set(square::E5, pc(Role::Pawn, Color::White))
.set(square::D5, pc(Role::Pawn, Color::Black));
let history = History {
last_move: Some(crate::uci::Uci {
orig: square::D6,
dest: square::D5,
promotion: None,
}),
unmoved_rooks: UnmovedRooks::from_board(b),
..History::new()
};
let p = Position::new().with_board(b).with_history(history);
assert_eq!(p.enpassant_square(), None);
assert_eq!(p.enpassant_moves().count(), 0);
}
#[test]
fn pawn_on_seventh_promotes_to_four_pieces() {
let b = Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::H8, pc(Role::King, Color::Black))
.set(square::A7, pc(Role::Pawn, Color::White));
let p = pos_from(b);
let moves: Vec<_> = p.valid_moves_at(square::A7).collect();
assert_eq!(moves.len(), 4, "4 promotion choices; got {moves:?}");
let promos: Vec<_> = moves.iter().filter_map(|m| m.promotion).collect();
assert!(promos.contains(&PromotableRole::Queen));
assert!(promos.contains(&PromotableRole::Rook));
assert!(promos.contains(&PromotableRole::Bishop));
assert!(promos.contains(&PromotableRole::Knight));
for m in &moves {
assert_eq!(m.dest, square::A8);
assert_eq!(m.capture, None);
}
}
#[test]
fn pawn_pre_promotion_push_has_no_promotion_field() {
let b = Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::E8, pc(Role::King, Color::Black))
.set(square::A6, pc(Role::Pawn, Color::White));
let p = pos_from(b);
let moves: Vec<_> = p.valid_moves_at(square::A6).collect();
assert!(
moves.iter().all(|m| m.promotion.is_none()),
"no promotion on rank-6 push, got {moves:?}"
);
}
fn castling_board() -> Board {
Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::A1, pc(Role::Rook, Color::White))
.set(square::H1, pc(Role::Rook, Color::White))
.set(square::E8, pc(Role::King, Color::Black))
.set(square::A8, pc(Role::Rook, Color::Black))
.set(square::H8, pc(Role::Rook, Color::Black))
}
#[test]
fn castle_kingside_white_available() {
let p = pos_from(castling_board());
let castles: Vec<_> = p
.valid_moves()
.filter(|m| m.castle == Some(Side::King))
.collect();
assert_eq!(castles.len(), 1);
let m = castles[0];
assert_eq!(m.orig, square::E1);
assert_eq!(m.dest, square::G1);
}
#[test]
fn castle_queenside_white_available() {
let p = pos_from(castling_board());
let castles: Vec<_> = p
.valid_moves()
.filter(|m| m.castle == Some(Side::Queen))
.collect();
assert_eq!(castles.len(), 1);
assert_eq!(castles[0].orig, square::E1);
assert_eq!(castles[0].dest, square::C1);
}
#[test]
fn cannot_castle_kingside_through_check() {
let b = castling_board().set(square::F8, pc(Role::Rook, Color::Black));
let p = pos_from(b);
let king_castle = p.valid_moves().find(|m| m.castle == Some(Side::King));
assert!(
king_castle.is_none(),
"F-file rook prevents kingside castle"
);
}
#[test]
fn cannot_castle_while_in_check() {
let b = castling_board().set(square::E4, pc(Role::Queen, Color::Black));
let p = pos_from(b);
assert!(
p.is_check(),
"white king on E1 is in check from black Q on E4"
);
let any_castle = p.valid_moves().find(|m| m.castle.is_some());
assert!(
any_castle.is_none(),
"no castles allowed while in check, got {any_castle:?}"
);
}
#[test]
fn cannot_castle_kingside_when_blocked() {
let b = castling_board().set(square::F1, pc(Role::Bishop, Color::White));
let p = pos_from(b);
let king_castle = p.valid_moves().find(|m| m.castle == Some(Side::King));
assert!(king_castle.is_none(), "bishop on F1 blocks kingside castle");
}
#[test]
fn cannot_castle_without_rights() {
let p = pos_from(castling_board()).with_castles(Castles::new(false, false, false, false));
let any_castle = p.valid_moves().find(|m| m.castle.is_some());
assert!(any_castle.is_none());
}
#[test]
fn cannot_castle_with_moved_rooks() {
let history = History {
unmoved_rooks: UnmovedRooks::from_board(Board::EMPTY),
..History::new()
};
let p = Position::new()
.with_board(castling_board())
.with_history(history);
let any_castle = p.valid_moves().find(|m| m.castle.is_some());
assert!(any_castle.is_none(), "no castle when rooks have moved");
}
#[test]
fn is_check_detects_back_rank_rook() {
let b = Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::E8, pc(Role::Rook, Color::Black))
.set(square::A8, pc(Role::King, Color::Black));
let p = pos_from(b);
assert!(p.is_check());
}
#[test]
fn is_check_false_when_blocked() {
let b = Board::EMPTY
.set(square::E1, pc(Role::King, Color::White))
.set(square::E4, pc(Role::Pawn, Color::White))
.set(square::E8, pc(Role::Rook, Color::Black))
.set(square::A8, pc(Role::King, Color::Black));
let p = pos_from(b);
assert!(!p.is_check(), "rook attack blocked by own pawn on E4");
}
#[test]
fn valid_moves_at_matches_filter_on_start() {
let p = Position::new();
for i in 0u8..64 {
let s = Square(i);
let direct = p.valid_moves_at(s).count();
let filtered = p.valid_moves().filter(|m| m.orig == s).count();
assert_eq!(direct, filtered, "mismatch at square {s}");
}
}
}
#[cfg(test)]
mod proptests {
use super::*;
use crate::unmoved_rooks::UnmovedRooks;
use proptest::prelude::*;
fn sq() -> impl Strategy<Value = Square> {
(0u8..64).prop_map(Square)
}
fn color() -> impl Strategy<Value = Color> {
prop_oneof![Just(Color::White), Just(Color::Black)]
}
fn non_king_role() -> impl Strategy<Value = Role> {
prop_oneof![
Just(Role::Pawn),
Just(Role::Knight),
Just(Role::Bishop),
Just(Role::Rook),
Just(Role::Queen),
]
}
fn non_king_piece() -> impl Strategy<Value = Piece> {
(non_king_role(), color()).prop_map(|(role, color)| Piece { role, color })
}
fn build_position(wk: Square, bk: Square, ops: Vec<(Square, Piece)>, c: Color) -> Position {
let mut b = Board::EMPTY
.set(
wk,
Piece {
role: Role::King,
color: Color::White,
},
)
.set(
bk,
Piece {
role: Role::King,
color: Color::Black,
},
);
for (s, p) in ops {
if s == wk || s == bk {
continue;
}
if p.role == Role::Pawn {
let r = s.rank().as_u8();
if r == 0 || r == 7 {
continue;
}
}
b = b.set(s, p);
}
let history = History {
castles: Castles::new(false, false, false, false),
unmoved_rooks: UnmovedRooks::from_board(b),
..History::new()
};
Position::new()
.with_board(b)
.with_color(c)
.with_history(history)
}
fn random_position() -> impl Strategy<Value = Position> {
(
sq(),
sq(),
prop::collection::vec((sq(), non_king_piece()), 0..12),
color(),
)
.prop_filter("kings distinct", |(wk, bk, _, _)| wk != bk)
.prop_filter("kings non-adjacent", |(wk, bk, _, _)| {
let dx = (wk.file().as_u8() as i32 - bk.file().as_u8() as i32).abs();
let dy = (wk.rank().as_u8() as i32 - bk.rank().as_u8() as i32).abs();
dx > 1 || dy > 1
})
.prop_map(|(wk, bk, ops, c)| build_position(wk, bk, ops, c))
.prop_filter("side-not-to-move not in check", |p| {
!p.board().is_check(p.color().opponent())
})
}
fn piece_count(b: &Board, role: Role, color: Color) -> u32 {
b.bypiece(Piece { role, color }).0.count_ones()
}
fn after_board(p: &Position, mve: &Move) -> Board {
let mut b = *p.board();
apply_move(&mut b, mve);
b
}
fn legal_count(p: &Position, it: impl Iterator<Item = Move>) -> usize {
it.filter(|m| !after_board(p, m).is_check(p.color)).count()
}
proptest! {
#[test]
fn move_color_matches_side_to_move(p in random_position()) {
for m in p.valid_moves() {
prop_assert_eq!(m.piece.color, p.color());
}
}
#[test]
fn move_orig_not_equal_dest(p in random_position()) {
for m in p.valid_moves() {
prop_assert_ne!(m.orig, m.dest);
}
}
#[test]
fn move_origin_holds_claimed_piece(p in random_position()) {
for m in p.valid_moves() {
prop_assert_eq!(p.board().piece_at(m.orig), Some(m.piece));
}
}
#[test]
fn after_board_keeps_both_kings(p in random_position()) {
for m in p.valid_moves() {
let after = after_board(&p, &m);
let white_kings = piece_count(&after, Role::King, Color::White);
let black_kings = piece_count(&after, Role::King, Color::Black);
prop_assert_eq!(white_kings, 1);
prop_assert_eq!(black_kings, 1);
}
}
#[test]
fn after_board_invariants(p in random_position()) {
for m in p.valid_moves() {
let b = after_board(&p, &m);
let white = b.bycolor(Color::White);
let black = b.bycolor(Color::Black);
prop_assert_eq!(b.occupied(), white | black);
prop_assert_eq!(white & black, crate::bitboard::Bitboard::EMPTY);
}
}
#[test]
fn deterministic(p in random_position()) {
let a: Vec<Move> = p.valid_moves().collect();
let b: Vec<Move> = p.valid_moves().collect();
prop_assert_eq!(a, b);
}
#[test]
fn partition_matches_per_piece_generators(p in random_position()) {
let total = p.valid_moves().count();
let sum = legal_count(&p, p.pawn_moves())
+ legal_count(&p, p.enpassant_moves())
+ legal_count(&p, p.king_moves())
+ legal_count(&p, p.knight_moves())
+ legal_count(&p, p.bishop_moves())
+ legal_count(&p, p.rook_moves())
+ legal_count(&p, p.queen_moves());
prop_assert_eq!(total, sum);
}
#[test]
fn valid_moves_at_filter_consistent(p in random_position()) {
for i in 0u8..64 {
let s = Square(i);
let direct = p.valid_moves_at(s).count();
let filtered = p.valid_moves().filter(|m| m.orig == s).count();
prop_assert_eq!(direct, filtered);
}
}
#[test]
fn has_moves_iff_any(p in random_position()) {
prop_assert_eq!(p.has_moves(), p.valid_moves().next().is_some());
}
#[test]
fn mve_with_listed_move_works(p in random_position()) {
if let Some(m) = p.valid_moves().next() {
let expected_after = after_board(&p, &m);
let next = p.clone().mve(m.orig, m.dest, m.promotion).expect("listed move must succeed");
prop_assert_eq!(next.color(), p.color().opponent());
if m.promotion.is_none() {
prop_assert_eq!(*next.board(), expected_after);
}
}
}
#[test]
fn promotion_only_on_opponent_back_rank(p in random_position()) {
let opp_back = p.color().opponent().back_rank();
for m in p.valid_moves() {
if m.promotion.is_some() {
prop_assert_eq!(m.dest.rank(), opp_back);
prop_assert_eq!(m.piece.role, Role::Pawn);
}
}
}
#[test]
fn valid_moves_does_not_mutate(p in random_position()) {
let before = p.clone();
let _: Vec<Move> = p.valid_moves().collect();
prop_assert_eq!(p, before);
}
#[test]
fn make_unmake_round_trip(p in random_position()) {
let before = p.clone();
let moves: Vec<Move> = p.valid_moves().collect();
for m in moves {
let mut q = before.clone();
let undo = q.make(&m);
q.unmake(undo);
prop_assert_eq!(&q, &before);
}
}
#[test]
fn make_matches_persistent_mve(p in random_position()) {
let moves: Vec<Move> = p.valid_moves().collect();
for m in moves {
let persistent = p.clone().mve(m.orig, m.dest, m.promotion).unwrap();
let mut mutable = p.clone();
mutable.make(&m);
prop_assert_eq!(&mutable, &persistent);
}
}
}
}