424 lines
13 KiB
Rust
424 lines
13 KiB
Rust
//! Move representation.
|
|
|
|
use crate::array_vec::*;
|
|
use crate::bitboard::*;
|
|
use crate::board::*;
|
|
use crate::position::*;
|
|
use crate::san::*;
|
|
use crate::uci::*;
|
|
|
|
use core::convert::Infallible;
|
|
use core::iter::{ExactSizeIterator, FusedIterator};
|
|
use core::ops::ControlFlow;
|
|
|
|
/// A legal move.
|
|
#[must_use]
|
|
#[derive(Clone, Copy)]
|
|
pub struct Move<'l> {
|
|
position: &'l Position,
|
|
raw: RawMove,
|
|
}
|
|
|
|
impl<'l> Move<'l> {
|
|
#[inline]
|
|
pub(crate) unsafe fn new(position: &'l Position, raw: RawMove) -> Self {
|
|
Self { position, raw }
|
|
}
|
|
|
|
/// Returns the position after playing the move.
|
|
///
|
|
/// This sets the en passant square after a double pawn advance, even when there is no pawn to
|
|
/// capture or when capturing is not legal.
|
|
#[inline]
|
|
pub fn make(self) -> Position {
|
|
let mut position = self.position.clone();
|
|
unsafe { position.play_unchecked(self.raw) };
|
|
position
|
|
}
|
|
|
|
/// Returns the position the move is played on.
|
|
#[inline]
|
|
pub fn position(self) -> &'l Position {
|
|
self.position
|
|
}
|
|
|
|
/// Returns the type of piece that moves.
|
|
#[inline]
|
|
pub fn role(self) -> Role {
|
|
self.raw.role()
|
|
}
|
|
|
|
/// Returns the origin square of the move.
|
|
#[inline]
|
|
pub fn from(self) -> Square {
|
|
self.raw.from()
|
|
}
|
|
|
|
/// Returns the target square of the move.
|
|
#[inline]
|
|
pub fn to(self) -> Square {
|
|
self.raw.to()
|
|
}
|
|
|
|
/// Returns the type of piece that the pawn is promoted to, if the move is a promotion.
|
|
#[inline]
|
|
pub fn promotion(self) -> Option<Role> {
|
|
self.raw.promotion()
|
|
}
|
|
|
|
/// Returns `true` if the move is a capture.
|
|
#[inline]
|
|
pub fn is_capture(self) -> bool {
|
|
let [p_b_q, n_b_k, r_q_k, _] = self.position.as_setup().bitboards;
|
|
self.raw.kind == MoveType::EnPassant
|
|
|| !((p_b_q | n_b_k | r_q_k) & self.to().bitboard()).is_empty()
|
|
}
|
|
|
|
/// Returns the type of piece that is captured, if the move is a capture.
|
|
#[inline]
|
|
pub fn captured(self) -> Option<Role> {
|
|
match self.raw.kind {
|
|
MoveType::EnPassant => Some(Role::Pawn),
|
|
_ => self.position.as_setup().role(self.raw.to),
|
|
}
|
|
}
|
|
|
|
/// Returns the raw data of the move.
|
|
#[inline]
|
|
pub fn raw(self) -> RawMove {
|
|
self.raw
|
|
}
|
|
|
|
/// Returns the UCI notation of the move.
|
|
#[inline]
|
|
pub fn to_uci(self) -> UciMove {
|
|
self.raw.to_uci()
|
|
}
|
|
|
|
/// Returns the standard algebraic notation of the move.
|
|
pub fn to_san(self) -> San {
|
|
struct MoveGenImpl<const ROLE: u8> {
|
|
to: Bitboard,
|
|
candidates: Bitboard,
|
|
}
|
|
impl<const ROLE: u8> MoveGenImpl<ROLE> {
|
|
#[inline]
|
|
fn new(to: Square) -> Self {
|
|
Self {
|
|
to: to.bitboard(),
|
|
candidates: Bitboard::new(),
|
|
}
|
|
}
|
|
}
|
|
impl<const ROLE: u8> MoveGen<Infallible> for MoveGenImpl<ROLE> {
|
|
#[inline]
|
|
fn roles(&self, role: Role) -> bool {
|
|
role as u8 == ROLE
|
|
}
|
|
#[inline]
|
|
fn to(&self) -> Bitboard {
|
|
self.to
|
|
}
|
|
#[inline]
|
|
fn extend<I>(&mut self, iter: I) -> ControlFlow<Infallible>
|
|
where
|
|
I: Iterator<Item = RawMove> + ExactSizeIterator,
|
|
{
|
|
iter.for_each(|raw| {
|
|
debug_assert!(raw.role() as u8 == ROLE);
|
|
debug_assert!(self.to.contains(raw.to()));
|
|
self.candidates.insert(raw.from);
|
|
});
|
|
ControlFlow::Continue(())
|
|
}
|
|
}
|
|
San {
|
|
inner: match self.raw.kind {
|
|
MoveType::CastleShort => SanInner::Castle(CastlingSide::Short),
|
|
MoveType::CastleLong => SanInner::Castle(CastlingSide::Long),
|
|
MoveType::PawnAdvance
|
|
| MoveType::PawnAdvancePromotion
|
|
| MoveType::PawnDoubleAdvance => SanInner::Normal {
|
|
role: Role::Pawn,
|
|
file: None,
|
|
rank: None,
|
|
capture: false,
|
|
target: self.to(),
|
|
promotion: self.promotion(),
|
|
},
|
|
MoveType::PawnAttack | MoveType::PawnAttackPromotion | MoveType::EnPassant => {
|
|
SanInner::Normal {
|
|
role: Role::Pawn,
|
|
file: Some(self.from().file()),
|
|
rank: None,
|
|
capture: true,
|
|
target: self.to(),
|
|
promotion: self.promotion(),
|
|
}
|
|
}
|
|
_ => {
|
|
fn aux<const ROLE: u8>(m: &Move) -> SanInner {
|
|
let mut moves = MoveGenImpl::<ROLE>::new(m.to());
|
|
let ControlFlow::Continue(()) = m.position().generate_moves(&mut moves);
|
|
let candidates = moves.candidates;
|
|
let (file, rank) = if candidates == m.from().bitboard() {
|
|
(None, None)
|
|
} else if candidates & m.from().file().bitboard() == m.from().bitboard() {
|
|
(Some(m.from().file()), None)
|
|
} else if candidates & m.from().rank().bitboard() == m.from().bitboard() {
|
|
(None, Some(m.from().rank()))
|
|
} else {
|
|
(Some(m.from().file()), Some(m.from().rank()))
|
|
};
|
|
SanInner::Normal {
|
|
role: m.role(),
|
|
file,
|
|
rank,
|
|
capture: m.is_capture(),
|
|
target: m.to(),
|
|
promotion: None,
|
|
}
|
|
}
|
|
match self.role() {
|
|
Role::Pawn => aux::<1>(&self),
|
|
Role::Knight => aux::<2>(&self),
|
|
Role::Bishop => aux::<3>(&self),
|
|
Role::Rook => aux::<4>(&self),
|
|
Role::Queen => aux::<5>(&self),
|
|
Role::King => aux::<6>(&self),
|
|
}
|
|
}
|
|
},
|
|
suffix: {
|
|
let pos = self.make();
|
|
let mut moves = MateMoveGenImpl::new();
|
|
let ControlFlow::Continue(()) = pos.generate_moves(&mut moves);
|
|
let MateMoveGenImpl { is_check, is_mate } = moves;
|
|
match (is_check, is_mate) {
|
|
(false, _) => None,
|
|
(true, false) => Some(SanSuffix::Check),
|
|
(true, true) => Some(SanSuffix::Checkmate),
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The list of all the legal moves on a position. It is returned by [`Position::legal_moves`].
|
|
///
|
|
/// This type lies on the stack and has a fixed capacity of 218 moves, as there exists no reachable
|
|
/// position with more legal moves[^1].
|
|
///
|
|
/// [^1]: In 1964, Nenad Petrović published a
|
|
/// [composition](https://lichess.org/editor/R6R/3Q4/1Q4Q1/4Q3/2Q4Q/Q4Q2/pp1Q4/kBNN1KB1_w_-_-_0_1)
|
|
/// with 218 legal moves. 60 years later, this was proven to be optimal by
|
|
/// [Tobs40](https://lichess.org/@/Tobs40/blog/why-a-reachable-position-can-have-at-most-218-playable-moves/a5xdxeqs).
|
|
/// Even though the [`Position`] type can represent some unreachable positions the proof still
|
|
/// holds, as its definition of a position is even less restrictive.
|
|
#[must_use]
|
|
#[derive(Clone)]
|
|
pub struct Moves<'l> {
|
|
position: &'l Position,
|
|
is_check: bool,
|
|
en_passant_is_legal: bool,
|
|
array: ArrayVec<RawMove, 218>,
|
|
}
|
|
|
|
impl<'l> MoveGen<Infallible> for Moves<'l> {
|
|
#[inline]
|
|
fn is_check(&mut self) -> ControlFlow<Infallible> {
|
|
self.is_check = true;
|
|
ControlFlow::Continue(())
|
|
}
|
|
#[inline]
|
|
fn en_passant_is_legal(&mut self) -> ControlFlow<Infallible> {
|
|
self.en_passant_is_legal = true;
|
|
ControlFlow::Continue(())
|
|
}
|
|
#[inline]
|
|
fn extend<I>(&mut self, iter: I) -> ControlFlow<Infallible>
|
|
where
|
|
I: Iterator<Item = RawMove> + ExactSizeIterator,
|
|
{
|
|
iter.for_each(|raw| unsafe { self.array.push_unchecked(raw) });
|
|
ControlFlow::Continue(())
|
|
}
|
|
}
|
|
|
|
impl<'l> Moves<'l> {
|
|
#[inline]
|
|
pub(crate) fn compute(position: &'l Position) -> Moves<'l> {
|
|
fn aux(position: &Position, moves: &mut Moves) {
|
|
let ControlFlow::Continue(()) = position.generate_moves(moves);
|
|
}
|
|
let mut moves = Moves {
|
|
position,
|
|
is_check: false,
|
|
en_passant_is_legal: false,
|
|
array: ArrayVec::new(),
|
|
};
|
|
aux(position, &mut moves);
|
|
moves
|
|
}
|
|
|
|
/// Returns the position on which the moves are played.
|
|
#[inline]
|
|
pub fn position(&self) -> &Position {
|
|
self.position
|
|
}
|
|
|
|
/// Iterates over the moves.
|
|
#[inline]
|
|
pub fn iter(&'l self) -> MovesIter<'l> {
|
|
MovesIter {
|
|
position: self.position,
|
|
iter: self.array.as_slice().into_iter(),
|
|
}
|
|
}
|
|
|
|
/// Returns the number of moves in the list.
|
|
#[inline]
|
|
pub fn len(&self) -> usize {
|
|
self.array.len()
|
|
}
|
|
|
|
/// Returns `true` if the king is in check.
|
|
#[inline]
|
|
pub fn is_check(&self) -> bool {
|
|
self.is_check
|
|
}
|
|
|
|
/// Returns `true` if there is no legal move on the position.
|
|
#[inline]
|
|
pub fn is_mate(&self) -> bool {
|
|
self.array.len() == 0
|
|
}
|
|
|
|
/// Returns `true` if taking en passant is legal on the position.
|
|
#[inline]
|
|
pub fn en_passant_is_legal(&self) -> bool {
|
|
self.en_passant_is_legal
|
|
}
|
|
|
|
/// Returns the move at the given index, if it exists.
|
|
#[inline]
|
|
pub fn get(&self, index: usize) -> Option<Move<'l>> {
|
|
self.array.get(index).copied().map(|raw| Move {
|
|
position: self.position,
|
|
raw,
|
|
})
|
|
}
|
|
|
|
/// Sorts the list in ascending order with a comparison function, preserving initial order of
|
|
/// equal elements. See `std::slice::sort_by` for details.
|
|
#[inline]
|
|
pub fn sort_by<F>(&mut self, mut compare: F)
|
|
where
|
|
F: FnMut(Move, Move) -> core::cmp::Ordering,
|
|
{
|
|
self.array.as_mut_slice().sort_by(|a, b| {
|
|
compare(
|
|
Move {
|
|
position: self.position,
|
|
raw: *a,
|
|
},
|
|
Move {
|
|
position: self.position,
|
|
raw: *b,
|
|
},
|
|
)
|
|
});
|
|
}
|
|
|
|
/// Sorts the list in ascending order with a comparison function, **without** preserving the
|
|
/// initial order of equal elements. See [`sort_unstable_by`](`slice::sort_unstable_by`) for
|
|
/// details.
|
|
#[inline]
|
|
pub fn sort_unstable_by<F>(&mut self, mut compare: F)
|
|
where
|
|
F: FnMut(Move, Move) -> core::cmp::Ordering,
|
|
{
|
|
self.array.as_mut_slice().sort_unstable_by(|a, b| {
|
|
compare(
|
|
Move {
|
|
position: self.position,
|
|
raw: *a,
|
|
},
|
|
Move {
|
|
position: self.position,
|
|
raw: *b,
|
|
},
|
|
)
|
|
});
|
|
}
|
|
}
|
|
|
|
/// An iterator over legal moves.
|
|
#[derive(Clone)]
|
|
pub struct MovesIter<'l> {
|
|
position: &'l Position,
|
|
iter: core::slice::Iter<'l, RawMove>,
|
|
}
|
|
impl<'l> Iterator for MovesIter<'l> {
|
|
type Item = Move<'l>;
|
|
#[inline]
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
self.iter.next().map(|raw| Move {
|
|
position: self.position,
|
|
raw: *raw,
|
|
})
|
|
}
|
|
}
|
|
impl<'l> FusedIterator for MovesIter<'l> {}
|
|
impl<'l> ExactSizeIterator for MovesIter<'l> {
|
|
#[inline]
|
|
fn len(&self) -> usize {
|
|
self.iter.len()
|
|
}
|
|
}
|
|
impl<'l> IntoIterator for &'l Moves<'l> {
|
|
type Item = Move<'l>;
|
|
type IntoIter = MovesIter<'l>;
|
|
#[inline]
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
self.iter()
|
|
}
|
|
}
|
|
|
|
/// An iterator over legal moves.
|
|
pub struct MovesIntoIter<'l> {
|
|
position: &'l Position,
|
|
iter: ArrayVecIntoIter<RawMove, 218>,
|
|
}
|
|
impl<'l> Iterator for MovesIntoIter<'l> {
|
|
type Item = Move<'l>;
|
|
#[inline]
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
self.iter.next().map(|raw| Move {
|
|
position: self.position,
|
|
raw,
|
|
})
|
|
}
|
|
#[inline]
|
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
self.iter.size_hint()
|
|
}
|
|
}
|
|
impl<'l> FusedIterator for MovesIntoIter<'l> {}
|
|
impl<'l> ExactSizeIterator for MovesIntoIter<'l> {
|
|
#[inline]
|
|
fn len(&self) -> usize {
|
|
self.iter.len()
|
|
}
|
|
}
|
|
impl<'l> IntoIterator for Moves<'l> {
|
|
type Item = Move<'l>;
|
|
type IntoIter = MovesIntoIter<'l>;
|
|
#[inline]
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
MovesIntoIter {
|
|
position: self.position,
|
|
iter: self.array.into_iter(),
|
|
}
|
|
}
|
|
}
|