use crate::facts::scan::{attackers, attacks_of, between, line, order_value};
use crate::game::Game;
use crate::moves::{Move, MoveKind};
use crate::position::Position;
use crate::types::{Colour, File, Piece, Rank, Role, Square, SquareSet};
const CLAIM_CLOCK: u32 = 100;
const AUTOMATIC_CLOCK: u32 = 150;
const CLAIM_REPETITIONS: u32 = 3;
const AUTOMATIC_REPETITIONS: u32 = 5;
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Wing {
Short,
Long,
}
impl Wing {
pub const ALL: [Wing; 2] = [Wing::Short, Wing::Long];
const fn king_file(self) -> File {
match self {
Wing::Short => File::G,
Wing::Long => File::C,
}
}
const fn rook_file(self) -> File {
match self {
Wing::Short => File::F,
Wing::Long => File::D,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Castling {
pub right: bool,
pub rook_present: bool,
pub king_in_check_by: SquareSet,
pub path_attacked: Vec<(Square, SquareSet)>,
pub path_blocked: SquareSet,
pub allowed: bool,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum EnPassant {
None,
Available {
target: Square,
captures: Vec<EpCapture>,
},
}
impl EnPassant {
pub fn target(&self) -> Option<Square> {
match self {
EnPassant::None => None,
EnPassant::Available { target, .. } => Some(*target),
}
}
pub fn captures(&self) -> &[EpCapture] {
match self {
EnPassant::None => &[],
EnPassant::Available { captures, .. } => captures,
}
}
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct EpCapture {
pub from: Square,
pub legal: bool,
pub forbidden_by: Option<EpObstacle>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum EpObstacle {
Pinned {
ray: SquareSet,
pinner: Square,
},
ExposesKing {
attacker: Square,
},
InCheck {
by: SquareSet,
},
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Pin {
pub pinned: Square,
pub pinner: Square,
pub king: Square,
pub ray: SquareSet,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Skewer {
pub attacker: Square,
pub front: Square,
pub behind: Square,
pub ray: SquareSet,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct Repetition {
pub count: u32,
pub plies: Vec<u32>,
pub near_misses: Vec<NearMiss>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct NearMiss {
pub ply: u32,
pub differs: Vec<Difference>,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum Difference {
CastlingRights,
EnPassant,
SideToMove,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct FiftyMove {
pub clock: u32,
pub plies_to_claim: u32,
pub plies_to_automatic: u32,
pub last_reset: Option<Reset>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub struct Reset {
pub ply: u32,
pub kind: ResetKind,
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum ResetKind {
Capture,
PawnMove,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct DrawStatus {
pub automatic: Vec<AutomaticDraw>,
pub claimable: Vec<ClaimableDraw>,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum AutomaticDraw {
Stalemate(StalemateDetail),
InsufficientMaterial(MaterialConfig),
Fivefold(Repetition),
SeventyFiveMoves(FiftyMove),
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub enum ClaimableDraw {
Threefold(Repetition),
FiftyMoves(FiftyMove),
}
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub enum MaterialConfig {
KvK,
KNvK,
KBvK,
KBvKBSameColour,
}
#[derive(Clone, PartialEq, Eq, Debug)]
pub struct StalemateDetail {
pub king: Square,
pub escape_squares: Vec<(Square, SquareSet)>,
pub stuck_units: Vec<(Square, Stuck)>,
}
#[derive(Clone, Copy, PartialEq, Eq, Debug)]
pub enum Stuck {
Pinned {
ray: SquareSet,
pinner: Square,
},
Blocked,
NoMoves,
}
fn attackers_on(
position: &Position,
square: Square,
colour: Colour,
occupied: SquareSet,
) -> SquareSet {
let role_units = Role::ALL.map(|role| position.by_colour(colour) & position.by_role(role));
attackers(square, colour, &role_units, occupied) & occupied
}
fn placement_of(position: &Position) -> ([SquareSet; 2], [SquareSet; 6]) {
(
Colour::ALL.map(|colour| position.by_colour(colour)),
Role::ALL.map(|role| position.by_role(role)),
)
}
fn playable_en_passant(position: &Position) -> Option<Square> {
let status = position.en_passant_status();
status
.target()
.filter(|_| status.captures().iter().any(|capture| capture.legal))
}
fn material_config(position: &Position) -> Option<MaterialConfig> {
let heavy =
position.by_role(Role::Pawn) | position.by_role(Role::Rook) | position.by_role(Role::Queen);
if !heavy.is_empty() {
return None;
}
let knights = position.by_role(Role::Knight);
let bishops = position.by_role(Role::Bishop);
match (knights.len(), bishops.len()) {
(0, 0) => Some(MaterialConfig::KvK),
(1, 0) => Some(MaterialConfig::KNvK),
(0, 1) => Some(MaterialConfig::KBvK),
(0, _) if bishops.is_subset(SquareSet::DARK) || bishops.is_subset(SquareSet::LIGHT) => {
Some(MaterialConfig::KBvKBSameColour)
}
_ => None,
}
}
fn forward(square: Square, colour: Colour) -> Option<Square> {
let step = match colour {
Colour::White => 1isize,
Colour::Black => -1,
};
let rank = usize::try_from(square.rank().index() as isize + step).ok()?;
Rank::from_index(rank).map(|rank| Square::new(square.file(), rank))
}
fn pseudo_moves(position: &Position, square: Square) -> SquareSet {
let Some(piece) = position.piece_at(square) else {
return SquareSet::EMPTY;
};
let occupied = position.occupied();
let mine = position.by_colour(piece.colour);
if piece.role != Role::Pawn {
return attacks_of(piece.role, square, piece.colour, occupied) - mine;
}
let mut moves = SquareSet::EMPTY;
if let Some(one) = forward(square, piece.colour) {
if !occupied.contains(one) {
moves.insert(one);
if square.rank().relative_to(piece.colour).index() == 1 {
if let Some(two) = forward(one, piece.colour) {
if !occupied.contains(two) {
moves.insert(two);
}
}
}
}
}
let targets = position.by_colour(!piece.colour)
| position
.en_passant()
.map_or(SquareSet::EMPTY, Square::to_set);
moves | (attacks_of(Role::Pawn, square, piece.colour, occupied) & targets)
}
impl Position {
pub fn castling(&self, colour: Colour, wing: Wing) -> Castling {
let king = self.king_of(colour);
let rights = self.castling_rights();
let file = match wing {
Wing::Short => rights.short(colour),
Wing::Long => rights.long(colour),
};
let back = Rank::First.relative_to(colour);
let rook = file.map(|file| Square::new(file, back));
let rook_present = rook
.is_some_and(|square| self.piece_at(square) == Some(Piece::new(Role::Rook, colour)));
let mut castling = Castling {
right: file.is_some(),
rook_present,
king_in_check_by: self.attackers(king, !colour),
path_attacked: Vec::new(),
path_blocked: SquareSet::EMPTY,
allowed: false,
};
let Some(rook) = rook.filter(|_| rook_present) else {
return castling;
};
let king_to = Square::new(wing.king_file(), back);
let rook_to = Square::new(wing.rook_file(), back);
let king_path = (between(king, king_to) | king_to.to_set()) - king.to_set();
let rook_path = between(rook, rook_to) | rook_to.to_set();
castling.path_blocked =
((king_path | rook_path) & self.occupied()) - king.to_set() - rook.to_set();
for square in king_path {
let by = self.attackers(square, !colour);
if !by.is_empty() {
castling.path_attacked.push((square, by));
}
}
castling.allowed = castling.king_in_check_by.is_empty()
&& castling.path_attacked.is_empty()
&& castling.path_blocked.is_empty();
castling
}
pub fn en_passant_status(&self) -> EnPassant {
let Some(target) = self.en_passant() else {
return EnPassant::None;
};
let colour = self.side_to_move();
let pawn = Piece::new(Role::Pawn, colour);
let rank = Rank::Fifth.relative_to(colour);
let mut captures = Vec::new();
for offset in [-1isize, 1] {
let file = target
.file()
.index()
.checked_add_signed(offset)
.and_then(File::from_index);
let Some(file) = file else { continue };
let from = Square::new(file, rank);
if self.piece_at(from) != Some(pawn) {
continue;
}
let legal = self.allows(Move::new(from, target, None, MoveKind::EnPassant));
let forbidden_by = if legal {
None
} else {
self.en_passant_obstacle(from, target, colour)
};
captures.push(EpCapture {
from,
legal,
forbidden_by,
});
}
EnPassant::Available { target, captures }
}
fn en_passant_obstacle(
&self,
from: Square,
target: Square,
colour: Colour,
) -> Option<EpObstacle> {
let checkers = self.checkers();
if !checkers.is_empty() {
return Some(EpObstacle::InCheck { by: checkers });
}
let pin = self.pins(colour).into_iter().find(|pin| pin.pinned == from);
if let Some(pin) = pin {
if !(pin.ray | pin.pinner.to_set()).contains(target) {
return Some(EpObstacle::Pinned {
ray: pin.ray,
pinner: pin.pinner,
});
}
}
let taken = Square::new(target.file(), from.rank());
let after = ((self.occupied() - from.to_set()) - taken.to_set()) | target.to_set();
let king = self.king_of(colour);
let revealed = attackers_on(self, king, !colour, after) - self.attackers(king, !colour);
revealed
.first()
.map(|attacker| EpObstacle::ExposesKing { attacker })
}
pub fn attackers(&self, square: Square, colour: Colour) -> SquareSet {
attackers_on(self, square, colour, self.occupied())
}
pub fn between(&self, a: Square, b: Square) -> SquareSet {
between(a, b)
}
pub fn pins(&self, colour: Colour) -> Vec<Pin> {
let king = self.king_of(colour);
let theirs = self.by_colour(!colour);
let mine = self.by_colour(colour);
let bishops = (self.by_role(Role::Bishop) | self.by_role(Role::Queen)) & theirs;
let rooks = (self.by_role(Role::Rook) | self.by_role(Role::Queen)) & theirs;
let candidates = (attacks_of(Role::Bishop, king, colour, SquareSet::EMPTY) & bishops)
| (attacks_of(Role::Rook, king, colour, SquareSet::EMPTY) & rooks);
let mut pins = Vec::new();
for pinner in candidates {
let ray = between(pinner, king);
let blockers = ray & self.occupied();
if blockers.len() == 1 && blockers.is_subset(mine) {
let pinned = blockers.first().expect("one blocker stands on the ray");
pins.push(Pin {
pinned,
pinner,
king,
ray,
});
}
}
pins.sort_by_key(|pin| pin.pinned.index());
pins
}
pub fn skewers(&self, colour: Colour) -> Vec<Skewer> {
let occupied = self.occupied();
let mine = self.by_colour(colour);
let theirs = self.by_colour(!colour);
let mut skewers = Vec::new();
for role in [Role::Bishop, Role::Rook, Role::Queen] {
for attacker in theirs & self.by_role(role) {
let attacks = attacks_of(role, attacker, !colour, occupied);
for front in attacks & mine {
let xray = attacks_of(role, attacker, !colour, occupied - front.to_set());
let behind = (xray - attacks) & line(attacker, front) & mine;
let Some(behind) = behind.first() else {
continue;
};
let value = |square: Square| {
order_value(
self.piece_at(square)
.expect("a unit stands on its own square")
.role,
)
};
if value(front) > value(behind) {
skewers.push(Skewer {
attacker,
front,
behind,
ray: between(attacker, behind),
});
}
}
}
}
skewers.sort_by_key(|skewer| skewer.front.index());
skewers
}
}
impl Game {
pub fn repetition_status(&self) -> Repetition {
let now = self.position();
let placement = placement_of(now);
let en_passant = playable_en_passant(now);
let rights = now.castling_rights();
let side = now.side_to_move();
let mut plies = Vec::new();
let mut near_misses = Vec::new();
for (ply, position) in self.positions().enumerate() {
if placement_of(position) != placement {
continue;
}
let mut differs = Vec::new();
if position.castling_rights() != rights {
differs.push(Difference::CastlingRights);
}
if playable_en_passant(position) != en_passant {
differs.push(Difference::EnPassant);
}
if position.side_to_move() != side {
differs.push(Difference::SideToMove);
}
if differs.is_empty() {
plies.push(ply as u32);
} else {
near_misses.push(NearMiss {
ply: ply as u32,
differs,
});
}
}
Repetition {
count: plies.len() as u32,
plies,
near_misses,
}
}
pub fn fifty_move_status(&self) -> FiftyMove {
let clock = self.position().halfmove_clock();
let mut last_reset = None;
for (index, (before, &mv)) in self.positions().zip(self.moves()).enumerate() {
let kind = if mv.is_capture() {
Some(ResetKind::Capture)
} else if before
.piece_at(mv.from())
.is_some_and(|p| p.role == Role::Pawn)
{
Some(ResetKind::PawnMove)
} else {
None
};
if let Some(kind) = kind {
last_reset = Some(Reset {
ply: index as u32 + 1,
kind,
});
}
}
FiftyMove {
clock,
plies_to_claim: CLAIM_CLOCK.saturating_sub(clock),
plies_to_automatic: AUTOMATIC_CLOCK.saturating_sub(clock),
last_reset,
}
}
pub fn draw_status(&self) -> DrawStatus {
let mut status = DrawStatus {
automatic: Vec::new(),
claimable: Vec::new(),
};
let position = self.position();
let stuck = self.legal_moves().is_empty();
if stuck && position.in_check() {
return status;
}
if stuck {
status
.automatic
.push(AutomaticDraw::Stalemate(self.stalemate_detail()));
}
if let Some(config) = material_config(position) {
status
.automatic
.push(AutomaticDraw::InsufficientMaterial(config));
}
let repetition = self.repetition_status();
let fifty = self.fifty_move_status();
if repetition.count >= AUTOMATIC_REPETITIONS {
status
.automatic
.push(AutomaticDraw::Fivefold(repetition.clone()));
}
if fifty.clock >= AUTOMATIC_CLOCK {
status
.automatic
.push(AutomaticDraw::SeventyFiveMoves(fifty.clone()));
}
if repetition.count >= CLAIM_REPETITIONS {
status.claimable.push(ClaimableDraw::Threefold(repetition));
}
if fifty.clock >= CLAIM_CLOCK {
status.claimable.push(ClaimableDraw::FiftyMoves(fifty));
}
status
}
pub fn claims_after(&self, mv: Move) -> Vec<ClaimableDraw> {
let mut next = self.clone();
if next.play(mv).is_err() {
return Vec::new();
}
next.draw_status().claimable
}
fn stalemate_detail(&self) -> StalemateDetail {
let position = self.position();
let colour = position.side_to_move();
let king = position.king_of(colour);
let mine = position.by_colour(colour);
let without_king = position.occupied() - king.to_set();
let escape_squares = (attacks_of(Role::King, king, colour, without_king) - mine)
.into_iter()
.map(|square| {
(
square,
attackers_on(position, square, !colour, without_king),
)
})
.collect();
let pins = position.pins(colour);
let stuck_units = (mine - king.to_set())
.into_iter()
.map(|square| {
let stuck = match pins.iter().find(|pin| pin.pinned == square) {
Some(pin) => Stuck::Pinned {
ray: pin.ray,
pinner: pin.pinner,
},
None if pseudo_moves(position, square).is_empty() => Stuck::Blocked,
None => Stuck::NoMoves,
};
(square, stuck)
})
.collect();
StalemateDetail {
king,
escape_squares,
stuck_units,
}
}
}