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 buf = self.valid_moves();
let mve = *buf
.iter()
.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) -> Vec<Move> {
let mut buf = Vec::with_capacity(MAX_MOVES);
let ctx = LegalityContext::compute(self);
self.king_moves(&mut buf, &ctx);
if ctx.is_double_check {
return buf;
}
self.pawn_moves(&mut buf, &ctx);
self.enpassant_moves(&mut buf, &ctx);
self.knight_moves(&mut buf, &ctx);
self.bishop_moves(&mut buf, &ctx);
self.rook_moves(&mut buf, &ctx);
self.queen_moves(&mut buf, &ctx);
buf
}
pub fn has_moves(&self) -> bool {
!self.valid_moves().is_empty()
}
pub fn valid_moves_at(&self, orig: Square) -> Vec<Move> {
let mut buf = self.valid_moves();
buf.retain(|m| m.orig == orig);
buf
}
pub fn pawn_moves(&self, buf: &mut Vec<Move>, ctx: &LegalityContext) {
if ctx.is_double_check {
return;
}
let pawns = self.board.bypiece(Piece {
role: Role::Pawn,
color: self.color,
});
let them = self.board.bycolor(self.color.opponent());
for from in pawns {
let mask = ctx.target_mask(from);
for to in ATTACKS.pawn_attacks(self.color, from) & them & mask {
self.push_pawn_moves(buf, from, to, true);
}
}
let occupied = self.board.occupied();
let unpinned = pawns & !ctx.pinned;
let singles = !occupied
& (match self.color {
Color::White => unpinned << 8,
Color::Black => unpinned >> 8,
});
for to in singles & ctx.check_mask {
let from = Square(match self.color {
Color::White => to.0 - 8,
Color::Black => to.0 + 8,
});
self.push_pawn_moves(buf, from, to, false);
}
let doubles = !occupied
& (match self.color {
Color::White => singles << 8,
Color::Black => singles >> 8,
})
& self.color.fourth_rank();
for to in doubles & ctx.check_mask {
let from = Square(match self.color {
Color::White => to.0 - 16,
Color::Black => to.0 + 16,
});
self.push_pawn_moves(buf, from, to, false);
}
for from in pawns & ctx.pinned {
let mask = ctx.target_mask(from);
let single_to = Square(match self.color {
Color::White => from.0 + 8,
Color::Black => from.0 - 8,
});
if !occupied.is_set(single_to) {
if mask.is_set(single_to) {
self.push_pawn_moves(buf, from, single_to, false);
}
if from.rank() == self.color.second_rank() {
let double_to = Square(match self.color {
Color::White => from.0 + 16,
Color::Black => from.0 - 16,
});
if !occupied.is_set(double_to) && mask.is_set(double_to) {
self.push_pawn_moves(buf, from, double_to, false);
}
}
}
}
}
pub fn enpassant_moves(&self, buf: &mut Vec<Move>, ctx: &LegalityContext) {
if ctx.is_double_check {
return;
}
let Some(last_move) = self.history.last_move else {
return;
};
let Some(target) = potential_enpassant_sq(last_move, self.board, self.color) else {
return;
};
let our_pawns = self.board.bypiece(Piece {
role: Role::Pawn,
color: self.color,
});
for from in ATTACKS.pawn_attacks(self.color.opponent(), target) & our_pawns {
if let Some(m) = self.enpassant(from, target) {
let mut working = self.board;
apply_move(&mut working, &m);
if !working.is_check(self.color) {
buf.push(m);
}
}
}
}
pub fn king_moves(&self, buf: &mut Vec<Move>, _ctx: &LegalityContext) {
let orig = self.board.king(self.color);
let board_without_king = self.board.pop(orig).0;
for dest in ATTACKS.king_attacks(orig) {
if board_without_king.is_attacked(dest, self.color) {
continue;
}
if let Some(m) = self.normal(orig, dest, Role::King) {
buf.push(m);
}
}
self.push_castling_moves(buf);
}
pub fn knight_moves(&self, buf: &mut Vec<Move>, ctx: &LegalityContext) {
if ctx.is_double_check {
return;
}
let knights = self.board.bypiece(Piece {
role: Role::Knight,
color: self.color,
}) & !ctx.pinned;
for from in knights {
for to in ATTACKS.knight_attacks(from) & ctx.check_mask {
if let Some(m) = self.normal(from, to, Role::Knight) {
buf.push(m);
}
}
}
}
pub fn bishop_moves(&self, buf: &mut Vec<Move>, ctx: &LegalityContext) {
if ctx.is_double_check {
return;
}
let bishops = self.board.bypiece(Piece {
role: Role::Bishop,
color: self.color,
});
let occupied = self.board.occupied();
for from in bishops {
let mask = ctx.target_mask(from);
for to in ATTACKS.bishop_attacks(from, occupied) & mask {
if let Some(m) = self.normal(from, to, Role::Bishop) {
buf.push(m);
}
}
}
}
pub fn rook_moves(&self, buf: &mut Vec<Move>, ctx: &LegalityContext) {
if ctx.is_double_check {
return;
}
let rooks = self.board.bypiece(Piece {
role: Role::Rook,
color: self.color,
});
let occupied = self.board.occupied();
for from in rooks {
let mask = ctx.target_mask(from);
for to in ATTACKS.rook_attacks(from, occupied) & mask {
if let Some(m) = self.normal(from, to, Role::Rook) {
buf.push(m);
}
}
}
}
pub fn queen_moves(&self, buf: &mut Vec<Move>, ctx: &LegalityContext) {
if ctx.is_double_check {
return;
}
let queens = self.board.bypiece(Piece {
role: Role::Queen,
color: self.color,
});
let occupied = self.board.occupied();
for from in queens {
let mask = ctx.target_mask(from);
for to in ATTACKS.bishop_attacks(from, occupied) & mask {
if let Some(m) = self.normal(from, to, Role::Queen) {
buf.push(m);
}
}
for to in ATTACKS.rook_attacks(from, occupied) & mask {
if let Some(m) = self.normal(from, to, Role::Queen) {
buf.push(m);
}
}
}
}
fn push_castling_moves(&self, buf: &mut Vec<Move>) {
if self.board.is_check(self.color) {
return;
}
for side in [Side::King, Side::Queen] {
if let Some(m) = self.castle(side) {
buf.push(m);
}
}
}
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 push_pawn_moves(&self, buf: &mut Vec<Move>, from: Square, to: Square, is_capture: bool) {
let is_promotion = from.rank() == self.color.seventh_rank();
if is_promotion {
let captured = if is_capture {
self.board.role_at(to).map(|r| (to, r))
} else {
None
};
for r in PromotableRole::ROLES {
buf.push(Move::promotion(self.color, from, to, r, captured));
}
} else if let Some(m) = self.normal(from, to, Role::Pawn) {
buf.push(m);
}
}
}
pub const MAX_MOVES: usize = 256;
pub struct LegalityContext {
king_sq: Square,
check_mask: Bitboard,
is_double_check: bool,
pinned: Bitboard,
}
impl LegalityContext {
pub 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 |= 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 |= blockers;
}
}
Self {
king_sq,
check_mask,
is_double_check,
pinned,
}
}
#[inline(always)]
fn target_mask(&self, from: Square) -> Bitboard {
if self.pinned.is_set(from) {
self.check_mask & Bitboard(ATTACKS.rays[self.king_sq.0 as usize][from.0 as usize])
} else {
self.check_mask
}
}
}
#[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)
}
fn collect_valid(p: &Position) -> Vec<Move> {
p.valid_moves()
}
fn collect_at(p: &Position, s: Square) -> Vec<Move> {
p.valid_moves_at(s)
}
#[test]
fn starting_position_has_20_moves() {
assert_eq!(collect_valid(&Position::new()).len(), 20);
}
#[test]
fn starting_position_pawn_moves_count() {
let p = Position::new();
let ctx = LegalityContext::compute(&p);
let mut buf = Vec::new();
p.pawn_moves(&mut buf, &ctx);
assert_eq!(buf.len(), 16);
}
#[test]
fn starting_position_knight_moves_count() {
let p = Position::new();
let ctx = LegalityContext::compute(&p);
let mut buf = Vec::new();
p.knight_moves(&mut buf, &ctx);
assert_eq!(buf.len(), 4);
}
#[test]
fn starting_position_no_castling() {
let castles: Vec<_> = collect_valid(&Position::new())
.into_iter()
.filter(|m| m.castle.is_some())
.collect();
assert!(
castles.is_empty(),
"no castles legal from start, got {castles:?}"
);
}
#[test]
fn starting_position_no_enpassant() {
let p = Position::new();
assert_eq!(p.enpassant_square(), None);
let ctx = LegalityContext::compute(&p);
let mut buf = Vec::new();
p.enpassant_moves(&mut buf, &ctx);
assert!(buf.is_empty());
}
#[test]
fn starting_position_no_promotion() {
let proms: Vec<_> = collect_valid(&Position::new())
.into_iter()
.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 = collect_at(&Position::new(), square::E2);
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<_> = collect_at(&p, square::E2)
.into_iter()
.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 mut eps: Vec<Move> = Vec::new();
let ctx = LegalityContext::compute(&p);
p.enpassant_moves(&mut eps, &ctx);
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);
let mut eps: Vec<Move> = Vec::new();
let ctx = LegalityContext::compute(&p);
p.enpassant_moves(&mut eps, &ctx);
assert!(eps.is_empty());
}
#[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 = collect_at(&p, square::A7);
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 = collect_at(&p, square::A6);
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<_> = collect_valid(&p)
.into_iter()
.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<_> = collect_valid(&p)
.into_iter()
.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 = collect_valid(&p)
.into_iter()
.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 = collect_valid(&p).into_iter().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 = collect_valid(&p)
.into_iter()
.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 = collect_valid(&p).into_iter().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 = collect_valid(&p).into_iter().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();
let all = collect_valid(&p);
for i in 0u8..64 {
let s = Square(i);
let direct = collect_at(&p, s).len();
let filtered = all.iter().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 collect_valid(p: &Position) -> Vec<Move> {
p.valid_moves()
}
proptest! {
#[test]
fn move_color_matches_side_to_move(p in random_position()) {
for m in collect_valid(&p) {
prop_assert_eq!(m.piece.color, p.color());
}
}
#[test]
fn move_orig_not_equal_dest(p in random_position()) {
for m in collect_valid(&p) {
prop_assert_ne!(m.orig, m.dest);
}
}
#[test]
fn move_origin_holds_claimed_piece(p in random_position()) {
for m in collect_valid(&p) {
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 collect_valid(&p) {
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 collect_valid(&p) {
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()) {
prop_assert_eq!(collect_valid(&p), collect_valid(&p));
}
#[test]
fn partition_matches_per_piece_generators(p in random_position()) {
let total = collect_valid(&p).len();
let ctx = LegalityContext::compute(&p);
let mut pawns = Vec::new();
p.pawn_moves(&mut pawns, &ctx);
let mut ep = Vec::new();
p.enpassant_moves(&mut ep, &ctx);
let mut king = Vec::new();
p.king_moves(&mut king, &ctx);
let mut knight = Vec::new();
p.knight_moves(&mut knight, &ctx);
let mut bishop = Vec::new();
p.bishop_moves(&mut bishop, &ctx);
let mut rook = Vec::new();
p.rook_moves(&mut rook, &ctx);
let mut queen = Vec::new();
p.queen_moves(&mut queen, &ctx);
let sum = pawns.len() + ep.len() + king.len()
+ knight.len() + bishop.len() + rook.len() + queen.len();
prop_assert_eq!(total, sum);
}
#[test]
fn valid_moves_at_filter_consistent(p in random_position()) {
let all = collect_valid(&p);
for i in 0u8..64 {
let s = Square(i);
let buf = p.valid_moves_at(s);
let filtered = all.iter().filter(|m| m.orig == s).count();
prop_assert_eq!(buf.len(), filtered);
}
}
#[test]
fn has_moves_iff_any(p in random_position()) {
prop_assert_eq!(p.has_moves(), !collect_valid(&p).is_empty());
}
#[test]
fn mve_with_listed_move_works(p in random_position()) {
if let Some(m) = collect_valid(&p).into_iter().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 collect_valid(&p) {
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 _ = collect_valid(&p);
prop_assert_eq!(p, before);
}
#[test]
fn make_unmake_round_trip(p in random_position()) {
let before = p.clone();
for m in collect_valid(&p) {
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()) {
for m in collect_valid(&p) {
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);
}
}
}
}