use crate::board::attack_tables::{
GOLD_ATTACKS, KING_ATTACKS, KNIGHT_ATTACKS, PAWN_ATTACKS, SILVER_ATTACKS, bishop_attacks,
lance_attacks, rook_attacks,
};
use crate::board::movegen::{
Move32Sink, generate_checks_all_move32, generate_checks_all_move32_drop_first_into,
};
use crate::board::{Move32List, Ply, Position, generate_legal_all_move32};
use crate::types::{Bitboard, Color, Hand, Move32, Piece, PieceType, Rank, Square};
const MATE_WORK_HISTORY_PLY: Ply = 16;
struct KingNeighborhood {
squares: [Square; 8],
attackers: [Bitboard; 8],
discovery: [Bitboard; 8],
len: usize,
adjacent_their_pieces: Bitboard,
occupied_without_king: Bitboard,
}
fn piece_attacks(piece: Piece, sq: Square, occupied: Bitboard) -> Bitboard {
let color = piece.color();
match piece.piece_type() {
PieceType::PAWN => PAWN_ATTACKS[sq][color.to_index()],
PieceType::LANCE => lance_attacks(sq, occupied, color),
PieceType::KNIGHT => KNIGHT_ATTACKS[sq][color.to_index()],
PieceType::SILVER => SILVER_ATTACKS[sq][color.to_index()],
PieceType::GOLD
| PieceType::PRO_PAWN
| PieceType::PRO_LANCE
| PieceType::PRO_KNIGHT
| PieceType::PRO_SILVER => GOLD_ATTACKS[sq][color.to_index()],
PieceType::BISHOP => bishop_attacks(sq, occupied),
PieceType::ROOK => rook_attacks(sq, occupied),
PieceType::HORSE => bishop_attacks(sq, occupied) | KING_ATTACKS[sq],
PieceType::DRAGON => rook_attacks(sq, occupied) | KING_ATTACKS[sq],
PieceType::KING => KING_ATTACKS[sq],
_ => Bitboard::ALL,
}
}
fn dropped_piece_attacks(
piece_type: PieceType,
to: Square,
us: Color,
occupied: Bitboard,
) -> Bitboard {
piece_attacks(Piece::from_parts(us, piece_type), to, occupied)
}
fn king_neighborhood(position: &Position) -> Option<KingNeighborhood> {
let us = position.turn();
let them = us.flip();
let king = position.king_square(them);
if king.is_none() {
return None;
}
let bitboards = position.bitboards();
let their_pieces = bitboards.color_pieces(them);
let occupied_without_king = bitboards.occupied().and_not(Bitboard::from_square(king));
let adjacent = KING_ATTACKS[king];
let adjacent_their_pieces = adjacent & their_pieces;
let mut squares = [Square::NONE; 8];
let mut attackers = [Bitboard::EMPTY; 8];
let mut discovery = [Bitboard::EMPTY; 8];
let mut len = 0;
let sliders =
(bitboards.bishop_horse() | bitboards.rook_dragon() | bitboards.pieces(PieceType::LANCE))
& bitboards.color_pieces(us);
let mut remaining = adjacent.and_not(their_pieces);
while let Some(square) = remaining.pop_lsb() {
squares[len] = square;
attackers[len] = position.attackers_to_color(us, square, occupied_without_king);
let mut opened = Bitboard::EMPTY;
let mut candidates = sliders;
while let Some(slider) = candidates.pop_lsb() {
let piece = position.piece_on(slider);
if piece_attacks(piece, slider, Bitboard::EMPTY).test(square) {
opened |= Bitboard::between(slider, square);
}
}
discovery[len] = opened;
len += 1;
}
Some(KingNeighborhood {
squares,
attackers,
discovery,
len,
adjacent_their_pieces,
occupied_without_king,
})
}
impl KingNeighborhood {
fn drop_leaves_escape(&self, piece_type: PieceType, to: Square, us: Color) -> bool {
let occupied = self.occupied_without_king | Bitboard::from_square(to);
let attacks = dropped_piece_attacks(piece_type, to, us, occupied);
for index in 0..self.len {
let square = self.squares[index];
if attacks.test(square) || !self.attackers[index].is_empty() {
continue;
}
return true;
}
false
}
fn recomputed_attackers(
&self,
position: &Position,
square: Square,
from: Square,
to: Square,
piece_after: Piece,
occupied_after: Bitboard,
) -> Bitboard {
let delta = MoveDelta { from: Some(from), to, piece_after, occupied_after };
attackers_with_delta(position, piece_after.color(), square, &delta)
}
fn board_move_leaves_escape(
&self,
position: &Position,
from: Square,
to: Square,
piece_after: Piece,
) -> bool {
let to_bit = Bitboard::from_square(to);
let from_bit = Bitboard::from_square(from);
let occupied = self.occupied_without_king.and_not(from_bit) | to_bit;
if !(to_bit & self.adjacent_their_pieces).is_empty()
&& self.recomputed_attackers(position, to, from, to, piece_after, occupied).is_empty()
{
return true;
}
let attacks = piece_attacks(piece_after, to, occupied);
for index in 0..self.len {
let square = self.squares[index];
if attacks.test(square) {
continue;
}
if !self.attackers[index].and_not(from_bit).is_empty() {
continue;
}
if self.discovery[index].test(from)
&& !self
.recomputed_attackers(position, square, from, to, piece_after, occupied)
.is_empty()
{
continue;
}
return true;
}
false
}
}
struct MateContext {
us: Color,
their_king: Square,
their_king_bb: Bitboard,
their_blockers: Bitboard,
occupied: Bitboard,
their_hand: Hand,
our_king: Square,
}
fn mate_context(position: &Position) -> Option<MateContext> {
let us = position.turn();
let their_king = position.king_square(us.flip());
if their_king.is_none() {
return None;
}
Some(MateContext {
us,
their_king,
their_king_bb: Bitboard::from_square(their_king),
their_blockers: position.blockers_for_king(us.flip()),
occupied: position.bitboards().occupied(),
their_hand: position.hand(us.flip()),
our_king: position.king_square(us),
})
}
#[derive(Clone, Copy)]
struct MoveDelta {
from: Option<Square>,
to: Square,
piece_after: Piece,
occupied_after: Bitboard,
}
impl MoveDelta {
fn from_move32(mv: Move32, occupied: Bitboard) -> Self {
let to = mv.to_sq();
let to_bit = Bitboard::from_square(to);
let (from, occupied_after) = if mv.is_drop() {
(None, occupied | to_bit)
} else {
let from = mv.from_sq();
(Some(from), occupied.and_not(Bitboard::from_square(from)) | to_bit)
};
Self { from, to, piece_after: mv.piece_after_move(), occupied_after }
}
}
fn attackers_with_delta(
position: &Position,
color: Color,
target: Square,
delta: &MoveDelta,
) -> Bitboard {
let mut attackers = position.attackers_to_color(color, target, delta.occupied_after);
if let Some(from) = delta.from {
attackers.clear(from);
}
attackers.clear(delta.to);
if color == delta.piece_after.color()
&& piece_attacks(delta.piece_after, delta.to, delta.occupied_after).test(target)
{
attackers.set(delta.to);
}
attackers
}
#[derive(Clone, Copy, PartialEq, Eq)]
enum CheckClass {
Direct(Square),
Discovered(Square),
Double,
}
fn classify_check(position: &Position, ctx: &MateContext, mv: Move32) -> Option<CheckClass> {
let to = mv.to_sq();
if mv.is_drop() {
return Some(CheckClass::Direct(to));
}
let from = mv.from_sq();
let moved_pt = mv.piece_after_move().piece_type();
let direct = position.check_square(moved_pt).test(to);
let discovered =
ctx.their_blockers.test(from) && !Bitboard::is_aligned(from, to, ctx.their_king);
match (direct, discovered) {
(true, true) => Some(CheckClass::Double),
(true, false) => Some(CheckClass::Direct(to)),
(false, true) => discovered_checker_square(position, ctx, from).map(CheckClass::Discovered),
(false, false) => None,
}
}
fn discovered_checker_square(
position: &Position,
ctx: &MateContext,
from: Square,
) -> Option<Square> {
let bb = position.bitboards();
let mut sliders = (bb.bishop_horse() | bb.rook_dragon() | bb.pieces(PieceType::LANCE))
& bb.color_pieces(ctx.us)
& Bitboard::line(ctx.their_king, from);
let from_bit = Bitboard::from_square(from);
while let Some(slider) = sliders.pop_lsb() {
if (Bitboard::between(slider, ctx.their_king) & ctx.occupied) == from_bit
&& piece_attacks(position.piece_on(slider), slider, Bitboard::EMPTY)
.test(ctx.their_king)
{
return Some(slider);
}
}
None
}
fn is_slider(pt: PieceType) -> bool {
matches!(
pt,
PieceType::LANCE
| PieceType::BISHOP
| PieceType::ROOK
| PieceType::HORSE
| PieceType::DRAGON
)
}
fn surely_legal_defender_move(
ctx: &MateContext,
delta: &MoveDelta,
to_may_pin: bool,
mover: Square,
target: Square,
) -> bool {
if ctx.their_blockers.test(mover) && !Bitboard::is_aligned(mover, target, ctx.their_king) {
return false;
}
if let Some(from) = delta.from
&& Bitboard::is_aligned(mover, from, ctx.their_king)
{
return false;
}
if to_may_pin && Bitboard::is_aligned(mover, delta.to, ctx.their_king) {
return false;
}
true
}
fn refute_by_capture(
position: &Position,
ctx: &MateContext,
delta: &MoveDelta,
check: CheckClass,
) -> bool {
let (checker, discovered) = match check {
CheckClass::Direct(c) => (c, false),
CheckClass::Discovered(c) => (c, true),
CheckClass::Double => return false,
};
let them = ctx.us.flip();
let mut capturers =
attackers_with_delta(position, them, checker, delta).and_not(ctx.their_king_bb);
let to_may_pin = discovered && is_slider(delta.piece_after.piece_type());
while let Some(capturer) = capturers.pop_lsb() {
if surely_legal_defender_move(ctx, delta, to_may_pin, capturer, checker) {
return true;
}
}
false
}
fn refute_by_interpose(
position: &Position,
ctx: &MateContext,
delta: &MoveDelta,
check: CheckClass,
) -> bool {
let (checker, discovered) = match check {
CheckClass::Direct(c) => (c, false),
CheckClass::Discovered(c) => (c, true),
CheckClass::Double => return false,
};
let line = Bitboard::between(checker, ctx.their_king);
if line.is_empty() {
return false;
}
let them = ctx.us.flip();
let hand = ctx.their_hand;
if hand.has(PieceType::GOLD)
|| hand.has(PieceType::SILVER)
|| hand.has(PieceType::BISHOP)
|| hand.has(PieceType::ROOK)
{
return true;
}
if hand.has(PieceType::LANCE) || hand.has(PieceType::KNIGHT) || hand.has(PieceType::PAWN) {
let (last, second) = if them == Color::BLACK {
(Rank::RANK_1, Rank::RANK_2)
} else {
(Rank::RANK_9, Rank::RANK_8)
};
let last_bb = Bitboard::rank_mask(last);
if hand.has(PieceType::LANCE) && !line.and_not(last_bb).is_empty() {
return true;
}
if hand.has(PieceType::KNIGHT)
&& !line.and_not(last_bb | Bitboard::rank_mask(second)).is_empty()
{
return true;
}
if hand.has(PieceType::PAWN) {
let their_pawns = position.bitboards().pieces_for(PieceType::PAWN, them);
let mut targets = line.and_not(last_bb);
while let Some(target) = targets.pop_lsb() {
if !(their_pawns & Bitboard::file_mask(target.file())).is_empty() {
continue;
}
if !ctx.our_king.is_none()
&& PAWN_ATTACKS[target][them.to_index()].test(ctx.our_king)
{
continue;
}
return true;
}
}
}
let to_may_pin = discovered && is_slider(delta.piece_after.piece_type());
let mut targets = line;
while let Some(target) = targets.pop_lsb() {
let mut interposers =
attackers_with_delta(position, them, target, delta).and_not(ctx.their_king_bb);
while let Some(interposer) = interposers.pop_lsb() {
if surely_legal_defender_move(ctx, delta, to_may_pin, interposer, target) {
return true;
}
}
}
false
}
#[cfg(debug_assertions)]
fn assert_rejected_candidate_is_not_mate(position: &Position, mv: Move32) {
let mut next = position.clone();
next.init_stack();
next.apply_move32_with_gives_check(mv, position.gives_check_move32(mv));
assert!(
!next.is_mated(),
"static refutation filter rejected a mating move: sfen={} move_raw={:#010x} usi={}",
position.to_sfen(None),
mv.raw(),
mv.to_usi(),
);
}
#[must_use]
pub fn solve_mate_in_one(position: &Position) -> Option<Move32> {
if position.is_in_check() {
return solve_mate_in_one_accepted(position);
}
let mut work = None;
let mut sink = ImmutableMateSink { position, context: None, work: &mut work, found: None };
generate_checks_all_move32_drop_first_into(position, &mut sink);
sink.found
}
fn solve_mate_in_one_accepted(position: &Position) -> Option<Move32> {
let mut candidates = Move32List::new();
let prep = prepare_mate_candidates(position, &mut candidates)?;
let mut work: Option<Position> = None;
for &mv in candidates.iter() {
if prep.in_check && !position.gives_check_move32(mv) {
continue;
}
if is_statically_refuted(position, &prep, mv) {
continue;
}
let work = work.get_or_insert_with(|| {
let mut cloned = position.clone();
cloned.init_stack();
cloned
});
work.apply_move32_with_gives_check(mv, true);
if work.is_mated() {
return Some(mv);
}
work.undo_move32(mv).expect("mate search must undo the move it just applied");
}
None
}
struct ImmutableMateSink<'a> {
position: &'a Position,
context: Option<MateFilterContext>,
work: &'a mut Option<Position>,
found: Option<Move32>,
}
impl Move32Sink for ImmutableMateSink<'_> {
fn push_move32(&mut self, mv: Move32) {
if self.stop() || !self.position.is_legal_move32(mv) {
return;
}
if self.context.is_none() {
self.context = Some(MateFilterContext {
in_check: false,
us: self.position.turn(),
neighborhood: king_neighborhood(self.position),
context: mate_context(self.position),
});
}
let context = self.context.as_ref().expect("legal candidate must initialize mate context");
if is_statically_refuted(self.position, context, mv) {
return;
}
if self.work.is_none() {
*self.work = Some(self.position.clone_for_search_bounded(MATE_WORK_HISTORY_PLY));
}
let work = self.work.as_mut().expect("mate work position must be initialized");
work.apply_move32_with_gives_check(mv, true);
let mated = work.is_mated();
work.undo_move32(mv).expect("mate search must undo the move it just applied");
if mated {
self.found = Some(mv);
}
}
fn retain_unordered<F>(&mut self, _: F)
where
F: FnMut(Move32) -> bool,
{
unreachable!("streaming check generation does not retain candidates")
}
fn stop(&self) -> bool {
self.found.is_some()
}
}
#[must_use]
pub fn solve_mate_in_one_in_place(position: &mut Position) -> Option<Move32> {
position.debug_assert_partial_keys_consistent();
#[cfg(debug_assertions)]
let entry = (
position.board_key(),
position.hand(Color::BLACK),
position.hand(Color::WHITE),
position.turn(),
position.state_stack_depth(),
);
let result = solve_mate_in_one_impl(position);
#[cfg(debug_assertions)]
debug_assert_eq!(
entry,
(
position.board_key(),
position.hand(Color::BLACK),
position.hand(Color::WHITE),
position.turn(),
position.state_stack_depth(),
),
"solve_mate_in_one_in_place must restore the position it mutated"
);
result
}
fn solve_mate_in_one_impl(position: &mut Position) -> Option<Move32> {
let mut candidates = Move32List::new();
let prep = prepare_mate_candidates(position, &mut candidates)?;
for &mv in candidates.iter() {
if prep.in_check && !position.gives_check_move32(mv) {
continue;
}
if is_statically_refuted(position, &prep, mv) {
continue;
}
position.apply_move32_with_gives_check(mv, true);
let mated = position.is_mated();
position.undo_move32(mv).expect("mate search must undo the move it just applied");
if mated {
return Some(mv);
}
}
None
}
struct MateFilterContext {
in_check: bool,
us: Color,
neighborhood: Option<KingNeighborhood>,
context: Option<MateContext>,
}
fn prepare_mate_candidates(
position: &Position,
candidates: &mut Move32List,
) -> Option<MateFilterContext> {
let in_check = position.is_in_check();
if in_check {
generate_legal_all_move32(position, candidates);
} else {
generate_checks_all_move32(position, candidates);
candidates.retain_unordered(|mv| position.is_legal_move32(mv));
}
if candidates.is_empty() {
return None;
}
let us = position.turn();
let neighborhood = king_neighborhood(position);
let context = if in_check { None } else { mate_context(position) };
Some(MateFilterContext { in_check, us, neighborhood, context })
}
#[inline]
fn is_statically_refuted(position: &Position, prep: &MateFilterContext, mv: Move32) -> bool {
if !prep.in_check && mv.dropped_piece() == Some(PieceType::PAWN) {
#[cfg(debug_assertions)]
assert_rejected_candidate_is_not_mate(position, mv);
return true;
}
if let Some(neighborhood) = prep.neighborhood.as_ref() {
let leaves_escape = match mv.dropped_piece() {
Some(piece_type) => neighborhood.drop_leaves_escape(piece_type, mv.to_sq(), prep.us),
None => neighborhood.board_move_leaves_escape(
position,
mv.from_sq(),
mv.to_sq(),
mv.piece_after_move(),
),
};
if leaves_escape {
#[cfg(debug_assertions)]
assert_rejected_candidate_is_not_mate(position, mv);
return true;
}
}
if let Some(ctx) = prep.context.as_ref()
&& let Some(check) = classify_check(position, ctx, mv)
&& check != CheckClass::Double
{
let delta = MoveDelta::from_move32(mv, ctx.occupied);
if refute_by_capture(position, ctx, &delta, check) {
#[cfg(debug_assertions)]
assert_rejected_candidate_is_not_mate(position, mv);
return true;
}
if refute_by_interpose(position, ctx, &delta, check) {
#[cfg(debug_assertions)]
assert_rejected_candidate_is_not_mate(position, mv);
return true;
}
}
false
}
#[cfg(test)]
mod tests;