use core::ops::ControlFlow;
use shogi_core::{Color, Hand, Move, Piece, PieceKind, Square};
use crate::bitboard::Bitboard;
use crate::position::Position;
use crate::tables;
const MAX_LEGAL_MOVES: usize = 593;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum MoveSet {
Normal {
piece: Piece,
from: Square,
promotions: Bitboard,
non_promotions: Bitboard,
},
Drop {
piece: Piece,
to: Bitboard,
},
}
impl MoveSet {
#[inline(always)]
pub fn len(&self) -> usize {
match *self {
MoveSet::Normal {
promotions,
non_promotions,
..
} => (promotions.count() + non_promotions.count()) as usize,
MoveSet::Drop { to, .. } => to.count() as usize,
}
}
#[inline(always)]
pub fn is_empty(&self) -> bool {
self.len() == 0
}
#[inline]
pub fn write_into(self, out: &mut Vec<Move>) {
match self {
MoveSet::Normal {
from,
promotions,
non_promotions,
..
} => {
promotions.for_each_square(|to| {
out.push(Move::Normal {
from,
to,
promote: true,
})
});
non_promotions.for_each_square(|to| {
out.push(Move::Normal {
from,
to,
promote: false,
})
});
}
MoveSet::Drop { piece, to } => {
to.for_each_square(|to| out.push(Move::Drop { piece, to }));
}
}
}
}
#[derive(Clone, Debug)]
pub struct MoveSetIter {
piece: Piece,
from: Option<Square>,
promotions: Bitboard,
others: Bitboard,
}
impl Iterator for MoveSetIter {
type Item = Move;
#[inline]
fn next(&mut self) -> Option<Move> {
match self.from {
Some(from) => {
if let Some(to) = self.promotions.pop() {
return Some(Move::Normal {
from,
to,
promote: true,
});
}
self.others.pop().map(|to| Move::Normal {
from,
to,
promote: false,
})
}
None => self.others.pop().map(|to| Move::Drop {
piece: self.piece,
to,
}),
}
}
fn size_hint(&self) -> (usize, Option<usize>) {
let count = (self.promotions.count() + self.others.count()) as usize;
(count, Some(count))
}
}
impl ExactSizeIterator for MoveSetIter {}
impl IntoIterator for MoveSet {
type Item = Move;
type IntoIter = MoveSetIter;
fn into_iter(self) -> MoveSetIter {
match self {
MoveSet::Normal {
piece,
from,
promotions,
non_promotions,
} => MoveSetIter {
piece,
from: Some(from),
promotions,
others: non_promotions,
},
MoveSet::Drop { piece, to } => MoveSetIter {
piece,
from: None,
promotions: Bitboard::EMPTY,
others: to,
},
}
}
}
impl Position {
pub fn generate_moves(
&self,
listener: impl FnMut(MoveSet) -> ControlFlow<()>,
) -> ControlFlow<()> {
generate_legal(self, listener)
}
pub fn legal_moves(&self) -> Vec<Move> {
let mut moves = Vec::with_capacity(MAX_LEGAL_MOVES);
let _ = self.generate_moves(|set| {
set.write_into(&mut moves);
ControlFlow::Continue(())
});
moves
}
pub fn has_legal_moves(&self) -> bool {
self.generate_moves(|_| ControlFlow::Break(())).is_break()
}
pub fn in_check(&self) -> bool {
let us = self.side_to_move();
match self.king_square(us) {
Some(king) => !attackers_to(
self,
king,
self.occupied(),
us.flip(),
self.player_bb(us.flip()),
)
.is_empty(),
None => false,
}
}
}
pub(crate) fn attackers_to(
position: &Position,
square: Square,
occupied: Bitboard,
attackers_color: Color,
mask: Bitboard,
) -> Bitboard {
let us = attackers_color.flip();
let golds = position.piece_kind_bb(PieceKind::Gold)
| position.piece_kind_bb(PieceKind::ProPawn)
| position.piece_kind_bb(PieceKind::ProLance)
| position.piece_kind_bb(PieceKind::ProKnight)
| position.piece_kind_bb(PieceKind::ProSilver);
let horses = position.piece_kind_bb(PieceKind::ProBishop);
let dragons = position.piece_kind_bb(PieceKind::ProRook);
let bishops = position.piece_kind_bb(PieceKind::Bishop) | horses;
let rooks = position.piece_kind_bb(PieceKind::Rook) | dragons;
let mut attackers = tables::pawn_attacks(us, square) & position.piece_kind_bb(PieceKind::Pawn);
attackers |= tables::knight_attacks(us, square) & position.piece_kind_bb(PieceKind::Knight);
attackers |= tables::silver_attacks(us, square) & position.piece_kind_bb(PieceKind::Silver);
attackers |= tables::gold_attacks(us, square) & golds;
attackers |= tables::king_attacks(square) & position.piece_kind_bb(PieceKind::King);
attackers |= tables::orthogonal_attacks(square) & horses;
attackers |= tables::diagonal_attacks(square) & dragons;
attackers |=
tables::lance_attacks(us, square, occupied) & position.piece_kind_bb(PieceKind::Lance);
attackers |= tables::bishop_attacks(square, occupied) & bishops;
attackers |= tables::rook_attacks(square, occupied) & rooks;
attackers & mask
}
pub(crate) fn generate_legal(
position: &Position,
mut listener: impl FnMut(MoveSet) -> ControlFlow<()>,
) -> ControlFlow<()> {
let us = position.side_to_move();
let occupied = position.occupied();
let our = position.player_bb(us);
let king = position.king_square(us);
let info = match king {
Some(king) => check_info(position, us, king, occupied),
None => CheckInfo::NONE,
};
generate_normal(position, us, occupied, our, king, info, &mut listener)?;
generate_drops(position, us, occupied, king, info.checkers, &mut listener)
}
#[inline]
fn emit_normal(
us: Color,
piece: Piece,
from: Square,
attacks: Bitboard,
listener: &mut impl FnMut(MoveSet) -> ControlFlow<()>,
) -> ControlFlow<()> {
if attacks.is_empty() {
return ControlFlow::Continue(());
}
let promotions = if tables::PROMOTABLE_KINDS >> (piece.as_u8() & 15) & 1 != 0 {
attacks & tables::promotion_mask(us, from)
} else {
Bitboard::EMPTY
};
let non_promotions = attacks & !tables::forced_promotion_zone(piece);
listener(MoveSet::Normal {
piece,
from,
promotions,
non_promotions,
})
}
fn generate_normal(
position: &Position,
us: Color,
occupied: Bitboard,
our: Bitboard,
king: Option<Square>,
info: CheckInfo,
listener: &mut impl FnMut(MoveSet) -> ControlFlow<()>,
) -> ControlFlow<()> {
let targets = match position.king_square(us.flip()) {
Some(enemy_king) => !(our | Bitboard::single(enemy_king)),
None => !our,
};
let king = match king {
Some(king) => king,
None => {
for from in our {
let piece = position.piece_at(from).unwrap();
let attacks = tables::attacks_of(piece, from, occupied) & targets;
emit_normal(us, piece, from, attacks, listener)?;
}
return ControlFlow::Continue(());
}
};
if info.checkers.count() < 2 {
let check_mask = match info.checkers.into_iter().next() {
Some(checker) => tables::between(king, checker) | Bitboard::single(checker),
None => Bitboard::ALL,
};
for from in our & !Bitboard::single(king) {
let piece = position.piece_at(from).unwrap();
let mut attacks = tables::attacks_of(piece, from, occupied) & targets & check_mask;
if info.pinned.contains(from) {
attacks &= tables::line(king, from);
}
emit_normal(us, piece, from, attacks, listener)?;
}
}
generate_king_moves(position, us, king, occupied, targets, listener)
}
fn generate_king_moves(
position: &Position,
us: Color,
king: Square,
occupied: Bitboard,
targets: Bitboard,
listener: &mut impl FnMut(MoveSet) -> ControlFlow<()>,
) -> ControlFlow<()> {
let candidates = tables::king_attacks(king) & targets;
if candidates.is_empty() {
return ControlFlow::Continue(());
}
let piece = position.piece_at(king).expect("king square holds a king");
let danger = king_danger(position, us, king, occupied);
emit_normal(us, piece, king, candidates & !danger, listener)
}
fn king_danger(position: &Position, us: Color, king: Square, occupied: Bitboard) -> Bitboard {
let occupied = occupied ^ Bitboard::single(king);
let rooks = position.piece_kind_bb(PieceKind::Rook)
| position.piece_kind_bb(PieceKind::ProRook)
| position.piece_kind_bb(PieceKind::Lance);
let bishops =
position.piece_kind_bb(PieceKind::Bishop) | position.piece_kind_bb(PieceKind::ProBishop);
let relevant = position.player_bb(us.flip())
& (tables::step_attacker_zone(king)
| (rooks & tables::orthogonal_attacker_zone(king))
| (bishops & tables::diagonal_attacker_zone(king)));
let mut danger = Bitboard::EMPTY;
for square in relevant {
let piece = position
.piece_at(square)
.expect("player_bb agrees with the mailbox");
danger |= tables::attacks_of(piece, square, occupied);
}
danger
}
#[derive(Clone, Copy)]
struct CheckInfo {
checkers: Bitboard,
pinned: Bitboard,
}
impl CheckInfo {
const NONE: Self = Self {
checkers: Bitboard::EMPTY,
pinned: Bitboard::EMPTY,
};
}
fn check_info(position: &Position, us: Color, king: Square, occupied: Bitboard) -> CheckInfo {
let their = position.player_bb(us.flip());
let rooks = (position.piece_kind_bb(PieceKind::Rook)
| position.piece_kind_bb(PieceKind::ProRook))
& their;
let bishops = (position.piece_kind_bb(PieceKind::Bishop)
| position.piece_kind_bb(PieceKind::ProBishop))
& their;
let lances = position.piece_kind_bb(PieceKind::Lance) & their;
let snipers = (tables::rook_rays(king) & rooks)
| (tables::bishop_rays(king) & bishops)
| (tables::lance_rays(us, king) & lances);
let mut checkers = Bitboard::EMPTY;
let mut pinned = Bitboard::EMPTY;
for sniper in snipers {
let blockers = tables::between(king, sniper) & occupied;
match blockers.count() {
0 => checkers |= Bitboard::single(sniper),
1 => pinned |= blockers,
_ => {}
}
}
let golds = position.piece_kind_bb(PieceKind::Gold)
| position.piece_kind_bb(PieceKind::ProPawn)
| position.piece_kind_bb(PieceKind::ProLance)
| position.piece_kind_bb(PieceKind::ProKnight)
| position.piece_kind_bb(PieceKind::ProSilver);
let mut steps = tables::pawn_attacks(us, king) & position.piece_kind_bb(PieceKind::Pawn);
steps |= tables::knight_attacks(us, king) & position.piece_kind_bb(PieceKind::Knight);
steps |= tables::silver_attacks(us, king) & position.piece_kind_bb(PieceKind::Silver);
steps |= tables::gold_attacks(us, king) & golds;
steps |= tables::orthogonal_attacks(king) & position.piece_kind_bb(PieceKind::ProBishop);
steps |= tables::diagonal_attacks(king) & position.piece_kind_bb(PieceKind::ProRook);
steps |= tables::king_attacks(king) & position.piece_kind_bb(PieceKind::King);
checkers |= steps & their;
CheckInfo {
checkers,
pinned: pinned & position.player_bb(us),
}
}
fn generate_drops(
position: &Position,
us: Color,
occupied: Bitboard,
king: Option<Square>,
checkers: Bitboard,
listener: &mut impl FnMut(MoveSet) -> ControlFlow<()>,
) -> ControlFlow<()> {
let hand = position.hand(us);
if hand == Hand::new() {
return ControlFlow::Continue(());
}
let targets = match checkers.count() {
0 => !occupied,
1 => {
let king = king.expect("in check without a king");
let mut checkers = checkers;
let checker = checkers.next().unwrap();
tables::between(king, checker) & !occupied
}
_ => return ControlFlow::Continue(()),
};
if targets.is_empty() {
return ControlFlow::Continue(());
}
let has_pawn = hand.count(PieceKind::Pawn).unwrap_or(0) > 0;
let pawn_files = if has_pawn {
let our_pawns = position.piece_kind_bb(PieceKind::Pawn) & position.player_bb(us);
(1..=9).fold(Bitboard::EMPTY, |files, file| {
let mask = Bitboard::file(file);
if (our_pawns & mask).is_empty() {
files
} else {
files | mask
}
})
} else {
Bitboard::EMPTY
};
let enemy_king = position.king_square(us.flip());
for piece_kind in Hand::all_hand_pieces() {
if hand.count(piece_kind).unwrap_or(0) == 0 {
continue;
}
let piece = Piece::new(piece_kind, us);
let mut drops = targets & !tables::forced_promotion_zone(piece);
if piece_kind == PieceKind::Pawn {
drops &= !pawn_files;
if let Some(enemy_king) = enemy_king {
let checking = drops & tables::pawn_attacks(us.flip(), enemy_king);
if let Some(to) = checking.into_iter().next()
&& is_pawn_drop_mate(position, piece, to)
{
drops ^= Bitboard::single(to);
}
}
}
if !drops.is_empty() {
listener(MoveSet::Drop { piece, to: drops })?;
}
}
ControlFlow::Continue(())
}
fn is_pawn_drop_mate(position: &Position, piece: Piece, to: Square) -> bool {
!position.with_drop(piece, to).has_legal_moves()
}
#[cfg(test)]
mod tests {
use super::*;
use shogi_core::PartialPosition;
#[test]
fn startpos_has_30_moves() {
let position = Position::startpos();
assert_eq!(position.legal_moves().len(), 30);
assert!(!position.in_check());
}
const CALLBACK_POSITIONS: &[&str] = &[
"startpos",
"l6nl/5+P1gk/2np1S3/p1p4Pp/3P2Sp1/1PPb2P1P/P5GS1/R8/LN4bKL w GR5pnsg 1",
"R8/2K1S1SSk/4B4/9/9/9/9/9/1L1L1L3 b RBGSNLP3g3n17p 1",
"lnsgk2nl/1r4gs1/p1pppp1pp/6p2/1p5P1/2P6/PPbPPPP1P/2G2G1R1/LNS1K1SNL b b 15",
"8k/4r4/6b2/7G1/4K4/9/9/9/9 b - 1",
"4r4/7b1/9/4GS3/4K4/9/9/9/8k b - 1",
"k4+R3/9/8+B/9/9/9/9/9/4K4 b - 1",
"4k4/9/9/9/9/9/6+r2/9/4K4 b - 1",
];
fn position_of(sfen: &str) -> Position {
if sfen == "startpos" {
return Position::startpos();
}
let partial =
<PartialPosition as shogi_usi_parser::FromUsi>::from_usi(&format!("sfen {sfen}"))
.expect("test SFEN must parse");
Position::new(partial)
}
#[test]
fn move_sets_expand_to_legal_moves() {
for sfen in CALLBACK_POSITIONS {
let position = position_of(sfen);
let mut expanded = Vec::new();
let mut counted = 0;
let _ = position.generate_moves(|set| {
assert!(!set.is_empty(), "empty MoveSet in {sfen}");
assert_eq!(set.len(), set.into_iter().count(), "len mismatch in {sfen}");
counted += set.len();
expanded.extend(set);
ControlFlow::Continue(())
});
let mut expected = position.legal_moves();
assert_eq!(counted, expected.len(), "count mismatch in {sfen}");
expanded.sort_by_key(|&mv| move_key(mv));
expected.sort_by_key(|&mv| move_key(mv));
assert_eq!(expanded, expected, "move mismatch in {sfen}");
}
}
#[test]
fn promotion_sets_are_consistent() {
for sfen in CALLBACK_POSITIONS {
let position = position_of(sfen);
let _ = position.generate_moves(|set| {
if let MoveSet::Normal {
piece,
promotions,
non_promotions,
..
} = set
{
if piece.piece_kind().promote().is_none() {
assert!(promotions.is_empty(), "unpromotable piece with promotions");
}
for to in promotions & !non_promotions {
assert!(tables::forced_promotion_zone(piece).contains(to));
}
}
ControlFlow::Continue(())
});
}
}
#[test]
fn write_into_matches_the_iterator() {
for sfen in CALLBACK_POSITIONS {
let position = position_of(sfen);
let mut sets = 0;
let mut expected = Vec::new();
let mut written = Vec::new();
let _ = position.generate_moves(|set| {
sets += 1;
expected.extend(set);
let before = written.len();
set.write_into(&mut written);
assert_eq!(
written.len() - before,
set.len(),
"write_into wrote a different count than MoveSet::len reports"
);
ControlFlow::Continue(())
});
assert!(sets > 0, "no move sets for {sfen}");
assert_eq!(written, expected, "write_into disagrees on {sfen}");
assert_eq!(position.legal_moves(), expected, "legal_moves disagrees");
}
}
#[test]
fn early_exit_stops_generation() {
for sfen in CALLBACK_POSITIONS {
let position = position_of(sfen);
let mut seen = 0;
let flow = position.generate_moves(|_| {
seen += 1;
ControlFlow::Break(())
});
assert!(flow.is_break(), "early exit was not reported to the caller");
assert_eq!(seen, 1, "generation did not stop after the first set");
assert_eq!(
position.has_legal_moves(),
!position.legal_moves().is_empty()
);
}
}
#[test]
fn has_legal_moves_is_false_when_mated() {
let mut partial = PartialPosition::empty();
for (file, rank, piece_kind, color) in [
(5, 9, PieceKind::King, Color::Black),
(5, 8, PieceKind::Gold, Color::White),
(5, 1, PieceKind::Rook, Color::White),
(1, 1, PieceKind::King, Color::White),
] {
partial.piece_set(
Square::new(file, rank).unwrap(),
Some(Piece::new(piece_kind, color)),
);
}
let position = Position::new(partial);
assert!(position.in_check());
assert!(position.legal_moves().is_empty());
assert!(!position.has_legal_moves());
}
fn king_danger_reference(
position: &Position,
us: Color,
king: Square,
occupied: Bitboard,
) -> Bitboard {
let occupied = occupied ^ Bitboard::single(king);
let sliders = position.piece_kind_bb(PieceKind::Lance)
| position.piece_kind_bb(PieceKind::Bishop)
| position.piece_kind_bb(PieceKind::Rook)
| position.piece_kind_bb(PieceKind::ProBishop)
| position.piece_kind_bb(PieceKind::ProRook);
let relevant = position.player_bb(us.flip()) & (tables::step_attacker_zone(king) | sliders);
let mut danger = Bitboard::EMPTY;
for square in relevant {
let piece = position.piece_at(square).expect("player_bb agrees");
danger |= tables::attacks_of(piece, square, occupied);
}
danger
}
#[test]
fn king_danger_agrees_with_the_unfiltered_scan() {
fn walk(
position: &mut Position,
depth: u32,
nodes: &mut u64,
dropped: &mut u64,
dragon_only: &mut u64,
horse_only: &mut u64,
) {
let us = position.side_to_move();
if let Some(king) = position.king_square(us) {
let occupied = position.occupied();
let neighbours = tables::king_attacks(king);
assert_eq!(
king_danger(position, us, king, occupied) & neighbours,
king_danger_reference(position, us, king, occupied) & neighbours,
"king_danger disagrees with the unfiltered scan in\n{position:?}"
);
let their = position.player_bb(us.flip());
let rooks = their
& (position.piece_kind_bb(PieceKind::Rook)
| position.piece_kind_bb(PieceKind::ProRook)
| position.piece_kind_bb(PieceKind::Lance));
let bishops = their
& (position.piece_kind_bb(PieceKind::Bishop)
| position.piece_kind_bb(PieceKind::ProBishop));
let steps = tables::step_attacker_zone(king);
let orthogonal = tables::orthogonal_attacker_zone(king);
let diagonal = tables::diagonal_attacker_zone(king);
*dropped += (rooks & !(steps | orthogonal)).count() as u64
+ (bishops & !(steps | diagonal)).count() as u64;
let dragons = rooks & position.piece_kind_bb(PieceKind::ProRook);
let horses = bishops & position.piece_kind_bb(PieceKind::ProBishop);
if !(dragons & steps & !orthogonal).is_empty() {
*dragon_only += 1;
}
if !(horses & steps & !diagonal).is_empty() {
*horse_only += 1;
}
*nodes += 1;
}
if depth == 0 {
return;
}
for mv in position.legal_moves() {
let undo = position.do_move(mv);
walk(position, depth - 1, nodes, dropped, dragon_only, horse_only);
position.undo_move(mv, undo);
}
}
let mut nodes = 0;
let mut dropped = 0;
let mut dragon_only = 0;
let mut horse_only = 0;
for sfen in CALLBACK_POSITIONS {
let mut position = position_of(sfen);
walk(
&mut position,
2,
&mut nodes,
&mut dropped,
&mut dragon_only,
&mut horse_only,
);
}
assert!(nodes > 10_000, "test covered only {nodes} nodes");
assert!(
dropped * 8 > nodes,
"only {dropped} sliders dropped over {nodes} nodes; the filter is barely exercised"
);
assert!(
dragon_only > 0,
"no enemy dragon was held by the step term alone; \
the dragon fixture is no longer covering its case"
);
assert!(
horse_only > 0,
"no enemy horse was held by the step term alone; \
the in-check fixture is no longer covering its case"
);
}
fn pinned_pieces_reference(
position: &Position,
us: Color,
king: Square,
occupied: Bitboard,
) -> Bitboard {
let their = position.player_bb(us.flip());
let rooks = (position.piece_kind_bb(PieceKind::Rook)
| position.piece_kind_bb(PieceKind::ProRook))
& their;
let bishops = (position.piece_kind_bb(PieceKind::Bishop)
| position.piece_kind_bb(PieceKind::ProBishop))
& their;
let lances = position.piece_kind_bb(PieceKind::Lance) & their;
let empty = Bitboard::EMPTY;
let snipers = (tables::rook_attacks(king, empty) & rooks)
| (tables::bishop_attacks(king, empty) & bishops)
| (tables::lance_attacks(us, king, empty) & lances);
let mut pinned = Bitboard::EMPTY;
for sniper in snipers {
let blockers = tables::between(king, sniper) & occupied;
if blockers.count() == 1 {
pinned |= blockers;
}
}
pinned & position.player_bb(us)
}
fn unblocked_snipers(position: &Position, us: Color, king: Square) -> Bitboard {
let occupied = position.occupied();
let their = position.player_bb(us.flip());
let rooks = (position.piece_kind_bb(PieceKind::Rook)
| position.piece_kind_bb(PieceKind::ProRook))
& their;
let bishops = (position.piece_kind_bb(PieceKind::Bishop)
| position.piece_kind_bb(PieceKind::ProBishop))
& their;
let lances = position.piece_kind_bb(PieceKind::Lance) & their;
let snipers = (tables::rook_rays(king) & rooks)
| (tables::bishop_rays(king) & bishops)
| (tables::lance_rays(us, king) & lances);
let mut unblocked = Bitboard::EMPTY;
for sniper in snipers {
if (tables::between(king, sniper) & occupied).is_empty() {
unblocked |= Bitboard::single(sniper);
}
}
unblocked
}
#[test]
fn check_info_agrees_with_attackers_to() {
fn walk(
position: &mut Position,
depth: u32,
nodes: &mut u64,
doubles: &mut u64,
double_pins: &mut u64,
) {
let us = position.side_to_move();
if let Some(king) = position.king_square(us) {
let occupied = position.occupied();
let them = us.flip();
let expected =
attackers_to(position, king, occupied, them, position.player_bb(them));
let info = check_info(position, us, king, occupied);
assert_eq!(
info.checkers, expected,
"checkers disagree in\n{position:?}"
);
assert_eq!(
info.pinned,
pinned_pieces_reference(position, us, king, occupied),
"pinned disagrees in\n{position:?}"
);
if unblocked_snipers(position, us, king).count() >= 2 {
*doubles += 1;
}
if info.pinned.count() >= 2 {
*double_pins += 1;
}
*nodes += 1;
}
if depth == 0 {
return;
}
for mv in position.legal_moves() {
let undo = position.do_move(mv);
walk(position, depth - 1, nodes, doubles, double_pins);
position.undo_move(mv, undo);
}
}
let mut nodes = 0;
let mut doubles = 0;
let mut double_pins = 0;
for sfen in CALLBACK_POSITIONS {
let mut position = position_of(sfen);
walk(&mut position, 2, &mut nodes, &mut doubles, &mut double_pins);
}
assert!(nodes > 10_000, "test covered only {nodes} nodes");
assert!(doubles > 0, "no double check reached; the OR is untested");
assert!(double_pins > 0, "no double pin reached; the OR is untested");
}
fn move_key(mv: Move) -> (u8, u8, u8, u8) {
match mv {
Move::Normal { from, to, promote } => (0, from.index(), to.index(), promote as u8),
Move::Drop { piece, to } => (1, piece.as_u8(), to.index(), 0),
}
}
#[test]
fn evasions_only_when_in_check() {
let mut partial = PartialPosition::empty();
partial.piece_set(
Square::new(5, 9).unwrap(),
Some(Piece::new(PieceKind::King, Color::Black)),
);
partial.piece_set(
Square::new(5, 5).unwrap(),
Some(Piece::new(PieceKind::Rook, Color::White)),
);
partial.piece_set(
Square::new(5, 1).unwrap(),
Some(Piece::new(PieceKind::King, Color::White)),
);
let position = Position::new(partial);
assert!(position.in_check());
for mv in position.legal_moves() {
let mut next = position.clone();
next.do_move(mv);
assert!(!in_check_of(&next, Color::Black));
}
}
fn in_check_of(position: &Position, color: Color) -> bool {
match position.king_square(color) {
Some(king) => !attackers_to(
position,
king,
position.occupied(),
color.flip(),
position.player_bb(color.flip()),
)
.is_empty(),
None => false,
}
}
}