use super::board_effect::{
BoardEffects, LongEffects, compute_board_effects_and_long_effects, rewind_by_capturing_piece,
rewind_by_dropping_piece, rewind_by_no_capturing_piece, update_by_capturing_piece,
update_by_dropping_piece, update_by_no_capturing_piece,
};
use super::state::StateInfo;
use super::zobrist::{zobrist_hand, zobrist_pass_rights, zobrist_psq, zobrist_side};
use crate::bitboard::{
Bitboard, RANK_BB, bishop_effect, dragon_effect, gold_effect, horse_effect, king_effect,
knight_effect, lance_effect, lance_step_effect, pawn_effect, rook_effect, silver_effect,
};
use crate::eval::material::{hand_piece_value, material_needs_board_effects, signed_piece_value};
use crate::nnue::piece_list::PieceList;
use crate::nnue::{ChangedBonaPiece, DirtyPiece, ExtBonaPiece};
use crate::prefetch::{NoPrefetch, TtPrefetch};
use crate::types::{
Color, EnteringKingRule, File, Hand, Move, Piece, PieceType, PieceTypeSet, Rank,
RepetitionState, Square, Value,
};
#[inline]
pub(super) fn is_minor_piece(pc: Piece) -> bool {
matches!(
pc.piece_type(),
PieceType::Lance
| PieceType::Knight
| PieceType::Silver
| PieceType::Gold
| PieceType::ProPawn
| PieceType::ProLance
| PieceType::ProKnight
| PieceType::ProSilver
)
}
#[derive(Clone)]
pub struct Position {
pub(super) board: [Piece; Square::NUM],
pub(super) by_type: [Bitboard; PieceType::NUM + 1],
pub(super) by_color: [Bitboard; Color::NUM],
board_effects: BoardEffects,
long_effects: LongEffects,
board_effects_dirty: bool,
golds_bb: Bitboard,
bishop_horse_bb: Bitboard,
rook_dragon_bb: Bitboard,
hdk_bb: Bitboard,
pub(super) hand: [Hand; Color::NUM],
pub(super) state_stack: Vec<StateInfo>,
state_idx: usize,
pub(super) game_ply: i32,
pub(super) side_to_move: Color,
pub(super) king_square: [Square; Color::NUM],
pass_rights_enabled: bool,
pub(super) piece_list: PieceList,
}
impl Position {
#[inline]
fn xor_partial_keys(&self, st: &mut StateInfo, pc: Piece, sq: Square) {
if pc.piece_type() == PieceType::Pawn {
st.pawn_key ^= zobrist_psq(pc, sq);
} else {
if is_minor_piece(pc) {
st.minor_piece_key ^= zobrist_psq(pc, sq);
}
st.non_pawn_key[pc.color().index()] ^= zobrist_psq(pc, sq);
}
}
#[inline]
fn cur_state(&self) -> &StateInfo {
debug_assert!(self.state_idx < self.state_stack.len());
unsafe { self.state_stack.get_unchecked(self.state_idx) }
}
#[inline]
fn cur_state_mut(&mut self) -> &mut StateInfo {
debug_assert!(self.state_idx < self.state_stack.len());
unsafe { self.state_stack.get_unchecked_mut(self.state_idx) }
}
#[inline]
fn push_state(&mut self, mut st: StateInfo) {
let next_idx = self.state_idx + 1;
st.previous = self.state_idx;
if self.state_stack.len() > next_idx {
unsafe { *self.state_stack.get_unchecked_mut(next_idx) = st };
} else {
self.state_stack.push(st);
}
self.state_idx = next_idx;
}
pub fn new() -> Self {
Position {
board: [Piece::NONE; Square::NUM],
by_type: [Bitboard::EMPTY; PieceType::NUM + 1],
by_color: [Bitboard::EMPTY; Color::NUM],
board_effects: BoardEffects::new(),
long_effects: LongEffects::new(),
board_effects_dirty: false,
golds_bb: Bitboard::EMPTY,
bishop_horse_bb: Bitboard::EMPTY,
rook_dragon_bb: Bitboard::EMPTY,
hdk_bb: Bitboard::EMPTY,
hand: [Hand::EMPTY; Color::NUM],
state_stack: vec![StateInfo::new()],
state_idx: 0,
game_ply: 0,
side_to_move: Color::Black,
king_square: [Square::SQ_11; Color::NUM],
pass_rights_enabled: false,
piece_list: PieceList::new(),
}
}
#[inline]
pub fn piece_on(&self, sq: Square) -> Piece {
debug_assert!(sq.index() < Square::NUM);
unsafe { *self.board.get_unchecked(sq.index()) }
}
#[inline]
pub fn captured_piece(&self) -> Piece {
self.cur_state().captured_piece
}
#[inline]
pub fn occupied(&self) -> Bitboard {
unsafe {
*self.by_color.get_unchecked(Color::Black.index())
| *self.by_color.get_unchecked(Color::White.index())
}
}
#[inline]
pub fn pieces_pt(&self, pt: PieceType) -> Bitboard {
debug_assert!((pt as usize) < self.by_type.len());
unsafe { *self.by_type.get_unchecked(pt as usize) }
}
#[inline]
pub fn pieces_c(&self, c: Color) -> Bitboard {
debug_assert!(c.index() < Color::NUM);
unsafe { *self.by_color.get_unchecked(c.index()) }
}
#[inline]
pub fn pieces(&self, c: Color, pt: PieceType) -> Bitboard {
debug_assert!(c.index() < Color::NUM);
debug_assert!((pt as usize) < self.by_type.len());
unsafe {
*self.by_color.get_unchecked(c.index()) & *self.by_type.get_unchecked(pt as usize)
}
}
#[inline]
pub fn pieces_by_types(&self, set: PieceTypeSet) -> Bitboard {
if set.is_empty() {
return Bitboard::EMPTY;
}
if set.is_all() {
return self.occupied();
}
let mut bb = Bitboard::EMPTY;
for pt in set.iter() {
bb |= self.by_type[pt as usize];
}
bb
}
#[inline]
pub fn pieces_c_by_types(&self, c: Color, set: PieceTypeSet) -> Bitboard {
if set.is_empty() {
return Bitboard::EMPTY;
}
if set.is_all() {
return self.by_color[c.index()];
}
let mut bb = Bitboard::EMPTY;
for pt in set.iter() {
bb |= self.by_type[pt as usize] & self.by_color[c.index()];
}
bb
}
#[inline]
const fn is_gold_like(pt: PieceType) -> bool {
matches!(
pt,
PieceType::Gold
| PieceType::ProPawn
| PieceType::ProLance
| PieceType::ProKnight
| PieceType::ProSilver
)
}
#[inline]
const fn is_bishop_like(pt: PieceType) -> bool {
matches!(pt, PieceType::Bishop | PieceType::Horse)
}
#[inline]
const fn is_rook_like(pt: PieceType) -> bool {
matches!(pt, PieceType::Rook | PieceType::Dragon)
}
#[inline]
const fn is_hdk(pt: PieceType) -> bool {
matches!(pt, PieceType::Horse | PieceType::Dragon | PieceType::King)
}
#[inline]
pub fn golds(&self) -> Bitboard {
self.golds_bb
}
#[inline]
pub fn golds_c(&self, c: Color) -> Bitboard {
self.golds_bb & self.by_color[c.index()]
}
#[inline]
pub fn bishop_horse(&self) -> Bitboard {
self.bishop_horse_bb
}
#[inline]
pub fn rook_dragon(&self) -> Bitboard {
self.rook_dragon_bb
}
#[inline]
pub fn hand(&self, c: Color) -> Hand {
self.hand[c.index()]
}
#[inline]
pub fn king_square(&self, c: Color) -> Square {
self.king_square[c.index()]
}
#[inline]
pub fn piece_list(&self) -> &PieceList {
&self.piece_list
}
#[inline]
pub fn side_to_move(&self) -> Color {
self.side_to_move
}
pub fn to_move(&self, mv: Move) -> Option<Move> {
Move::from_u16_checked(mv.raw())?;
if mv.is_none() {
return Some(Move::NONE);
}
if mv.is_drop() {
let pt = mv.drop_piece_type();
let dropped_pc = Piece::make(self.side_to_move, pt);
Some(mv.with_piece(dropped_pc))
} else {
let from = mv.from();
let pc = self.piece_on(from);
if pc.is_some() && pc.color() == self.side_to_move {
if mv.is_promote() && !pc.piece_type().can_promote() {
return None;
}
let moved_pc = if mv.is_promote() {
pc.promote().expect("already validated can_promote")
} else {
pc
};
Some(mv.with_piece(moved_pc))
} else {
None
}
}
}
#[inline]
pub fn game_ply(&self) -> i32 {
self.game_ply
}
pub fn repetition_state(&self, ply: i32) -> RepetitionState {
let rep = self.cur_state().repetition;
if rep != 0 && rep < ply {
return self.cur_state().repetition_type;
}
RepetitionState::None
}
#[inline]
pub fn state(&self) -> &StateInfo {
self.cur_state()
}
pub fn previous_state(&self) -> Option<&StateInfo> {
self.cur_state().previous_index().map(|idx| &self.state_stack[idx])
}
#[inline]
pub fn state_at(&self, idx: usize) -> &StateInfo {
&self.state_stack[idx]
}
#[inline]
pub fn state_index(&self) -> usize {
self.state_idx
}
#[inline]
pub fn state_mut(&mut self) -> &mut StateInfo {
self.cur_state_mut()
}
#[inline]
pub fn key(&self) -> u64 {
self.cur_state().key()
}
#[inline]
pub fn board_effect(&self, color: Color, sq: Square) -> u8 {
debug_assert!(!self.board_effects_dirty, "board_effects is dirty");
self.board_effects.effect(color, sq)
}
#[inline]
pub(crate) fn board_effects(&self) -> &BoardEffects {
debug_assert!(!self.board_effects_dirty, "board_effects is dirty");
&self.board_effects
}
#[inline]
fn should_update_board_effects() -> bool {
if !crate::eval::is_material_enabled() && crate::nnue::is_nnue_initialized() {
return false;
}
material_needs_board_effects()
}
#[inline]
fn ensure_board_effects(&mut self) {
if self.board_effects_dirty {
self.recompute_board_effects();
}
}
pub(crate) fn recompute_board_effects(&mut self) {
let (effects, long_effects) = compute_board_effects_and_long_effects(self);
self.board_effects = effects;
self.long_effects = long_effects;
self.board_effects_dirty = false;
}
#[cfg(debug_assertions)]
fn debug_verify_board_effects(&self) {
let (expected, expected_long) = compute_board_effects_and_long_effects(self);
if expected != self.board_effects {
for color in [Color::Black, Color::White] {
for sq in Square::all() {
let actual = self.board_effects.effect(color, sq);
let want = expected.effect(color, sq);
if actual != want {
eprintln!(
"board_effect mismatch: color={color:?}, sq={sq:?}, actual={actual}, expected={want}, sfen={}",
self.to_sfen()
);
break;
}
}
}
panic!("board_effect mismatch");
}
if expected_long != self.long_effects {
for sq in Square::all() {
let actual = self.long_effects.long_effect16(sq);
let want = expected_long.long_effect16(sq);
if actual != want {
eprintln!(
"long_effect mismatch: sq={sq:?}, actual=0x{actual:04x}, expected=0x{want:04x}, sfen={}",
self.to_sfen()
);
break;
}
}
panic!("long_effect mismatch");
}
}
#[inline]
pub fn pawn_key(&self) -> u64 {
self.cur_state().pawn_key
}
#[inline]
pub fn minor_piece_key(&self) -> u64 {
self.cur_state().minor_piece_key
}
#[inline]
pub fn non_pawn_key(&self, c: Color) -> u64 {
self.cur_state().non_pawn_key[c.index()]
}
pub fn attackers_to(&self, sq: Square) -> Bitboard {
self.attackers_to_occ(sq, self.occupied())
}
pub fn attackers_to_occ(&self, sq: Square, occupied: Bitboard) -> Bitboard {
let silver_hdk = self.pieces_pt(PieceType::Silver) | self.hdk_bb;
let golds_hdk = self.golds_bb | self.hdk_bb;
let black_attackers = ((pawn_effect(Color::White, sq) & self.pieces_pt(PieceType::Pawn))
| (knight_effect(Color::White, sq) & self.pieces_pt(PieceType::Knight))
| (silver_effect(Color::White, sq) & silver_hdk)
| (gold_effect(Color::White, sq) & golds_hdk))
& self.pieces_c(Color::Black);
let white_attackers = ((pawn_effect(Color::Black, sq) & self.pieces_pt(PieceType::Pawn))
| (knight_effect(Color::Black, sq) & self.pieces_pt(PieceType::Knight))
| (silver_effect(Color::Black, sq) & silver_hdk)
| (gold_effect(Color::Black, sq) & golds_hdk))
& self.pieces_c(Color::White);
let bishop = bishop_effect(sq, occupied) & self.bishop_horse_bb;
let rook_eff = rook_effect(sq, occupied);
let rook_lance = rook_eff
& (self.rook_dragon_bb
| (lance_step_effect(Color::White, sq)
& self.pieces(Color::Black, PieceType::Lance))
| (lance_step_effect(Color::Black, sq)
& self.pieces(Color::White, PieceType::Lance)));
black_attackers | white_attackers | bishop | rook_lance
}
pub fn attackers_to_c(&self, sq: Square, c: Color) -> Bitboard {
self.attackers_to_occ(sq, self.occupied()) & self.pieces_c(c)
}
#[inline]
pub fn blockers_for_king(&self, c: Color) -> Bitboard {
self.cur_state().blockers_for_king[c.index()]
}
#[inline]
pub fn checkers(&self) -> Bitboard {
self.cur_state().checkers
}
#[inline]
pub fn in_check(&self) -> bool {
!self.cur_state().checkers.is_empty()
}
#[inline]
pub fn check_squares(&self, pt: PieceType) -> Bitboard {
self.cur_state().check_squares[pt as usize]
}
pub fn pinned_pieces(&self, them: Color, avoid: Square) -> Bitboard {
self.pinned_pieces_excluding(them, avoid)
}
pub fn pinned_pieces_excluding(&self, them: Color, avoid: Square) -> Bitboard {
let avoid_bb = Bitboard::from_square(avoid);
let avoid_not = !avoid_bb;
let ksq = self.king_square[them.index()];
let enemy = !them;
let lance_bb = self.pieces(enemy, PieceType::Lance) & avoid_not;
let bishop_bb = (self.bishop_horse_bb & self.by_color[enemy.index()]) & avoid_not;
let rook_bb = (self.rook_dragon_bb & self.by_color[enemy.index()]) & avoid_not;
let pinners = (lance_effect(them, ksq, Bitboard::EMPTY) & lance_bb)
| (bishop_effect(ksq, Bitboard::EMPTY) & bishop_bb)
| (rook_effect(ksq, Bitboard::EMPTY) & rook_bb);
let pieces_without_avoid = self.occupied() & avoid_not;
let mut result = Bitboard::EMPTY;
for pinner_sq in pinners.iter() {
let between = crate::bitboard::between_bb(ksq, pinner_sq) & pieces_without_avoid;
if !between.is_empty() && !between.more_than_one() {
result |= between & self.pieces_c(them);
}
}
result
}
pub fn discovered(&self, from: Square, to: Square, ksq: Square, pinned: Bitboard) -> bool {
pinned.contains(from) && !crate::mate::aligned(from, to, ksq)
}
pub(super) fn put_piece(&mut self, pc: Piece, sq: Square) {
self.put_piece_internal(pc, sq);
self.board_effects_dirty = true;
}
fn put_piece_internal(&mut self, pc: Piece, sq: Square) {
debug_assert!(sq.index() < Square::NUM);
debug_assert!(self.board[sq.index()].is_none());
let pt = pc.piece_type();
unsafe { *self.board.get_unchecked_mut(sq.index()) = pc };
debug_assert!((pt as usize) < self.by_type.len());
debug_assert!(pc.color().index() < Color::NUM);
unsafe { self.by_type.get_unchecked_mut(pt as usize) }.set(sq);
unsafe { self.by_color.get_unchecked_mut(pc.color().index()) }.set(sq);
if Self::is_gold_like(pt) {
self.golds_bb.set(sq);
} else if Self::is_bishop_like(pt) {
self.bishop_horse_bb.set(sq);
} else if Self::is_rook_like(pt) {
self.rook_dragon_bb.set(sq);
}
if Self::is_hdk(pt) {
self.hdk_bb.set(sq);
}
}
#[cfg(test)]
fn remove_piece(&mut self, sq: Square) {
self.remove_piece_internal(sq);
self.board_effects_dirty = true;
}
fn remove_piece_internal(&mut self, sq: Square) {
debug_assert!(sq.index() < Square::NUM);
let pc = unsafe { *self.board.get_unchecked(sq.index()) };
debug_assert!(pc.is_some());
let pt = pc.piece_type();
unsafe { *self.board.get_unchecked_mut(sq.index()) = Piece::NONE };
debug_assert!((pt as usize) < self.by_type.len());
debug_assert!(pc.color().index() < Color::NUM);
unsafe { self.by_type.get_unchecked_mut(pt as usize) }.clear(sq);
unsafe { self.by_color.get_unchecked_mut(pc.color().index()) }.clear(sq);
if Self::is_gold_like(pt) {
self.golds_bb.clear(sq);
} else if Self::is_bishop_like(pt) {
self.bishop_horse_bb.clear(sq);
} else if Self::is_rook_like(pt) {
self.rook_dragon_bb.clear(sq);
}
if Self::is_hdk(pt) {
self.hdk_bb.clear(sq);
}
}
pub(super) fn update_blockers_and_pinners(&mut self) {
for c in [Color::Black, Color::White] {
let (blockers, pinners) =
self.compute_blockers_and_pinners(c, self.occupied(), Bitboard::EMPTY);
let st = self.cur_state_mut();
st.blockers_for_king[c.index()] = blockers;
st.pinners[c.index()] = pinners;
}
}
fn compute_blockers_and_pinners(
&self,
king_color: Color,
occupied: Bitboard,
enemy_removed: Bitboard,
) -> (Bitboard, Bitboard) {
let ksq = self.king_square[king_color.index()];
let enemy = !king_color;
let lance_bb = self.pieces(enemy, PieceType::Lance) & !enemy_removed;
let bishop_bb = (self.bishop_horse_bb & self.by_color[enemy.index()]) & !enemy_removed;
let rook_bb = (self.rook_dragon_bb & self.by_color[enemy.index()]) & !enemy_removed;
let snipers = (lance_effect(king_color, ksq, Bitboard::EMPTY) & lance_bb)
| (bishop_effect(ksq, Bitboard::EMPTY) & bishop_bb)
| (rook_effect(ksq, Bitboard::EMPTY) & rook_bb);
let mut blockers = Bitboard::EMPTY;
let mut pinners = Bitboard::EMPTY;
let occ_without_snipers = occupied & !snipers;
for sniper_sq in snipers.iter() {
let between = crate::bitboard::between_bb(ksq, sniper_sq) & occ_without_snipers;
if between.is_empty() || between.more_than_one() {
continue;
}
if (between & self.pieces_c(enemy)).is_empty() {
blockers |= between;
pinners.set(sniper_sq);
} else {
blockers |= between;
}
}
(blockers, pinners)
}
pub(super) fn update_check_squares(&mut self) {
let them = !self.side_to_move;
let ksq = self.king_square[them.index()];
let occupied = self.occupied();
let st = self.cur_state_mut();
unsafe {
*st.check_squares.get_unchecked_mut(PieceType::Pawn as usize) = pawn_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::Knight as usize) =
knight_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::Silver as usize) =
silver_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::Gold as usize) = gold_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::King as usize) = Bitboard::EMPTY;
*st.check_squares.get_unchecked_mut(PieceType::Lance as usize) =
lance_effect(them, ksq, occupied);
*st.check_squares.get_unchecked_mut(PieceType::Bishop as usize) =
bishop_effect(ksq, occupied);
*st.check_squares.get_unchecked_mut(PieceType::Rook as usize) =
rook_effect(ksq, occupied);
*st.check_squares.get_unchecked_mut(PieceType::ProPawn as usize) =
gold_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::ProLance as usize) =
gold_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::ProKnight as usize) =
gold_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::ProSilver as usize) =
gold_effect(them, ksq);
*st.check_squares.get_unchecked_mut(PieceType::Horse as usize) =
horse_effect(ksq, occupied);
*st.check_squares.get_unchecked_mut(PieceType::Dragon as usize) =
dragon_effect(ksq, occupied);
}
}
#[inline]
pub fn pass_rights(&self, color: Color) -> u8 {
self.cur_state().get_pass_rights(color)
}
#[inline]
pub fn is_pass_rights_enabled(&self) -> bool {
self.pass_rights_enabled
}
#[inline]
pub fn can_pass(&self) -> bool {
self.pass_rights_enabled && !self.in_check() && self.pass_rights(self.side_to_move) > 0
}
pub fn set_pass_rights_enabled(&mut self, enabled: bool) {
self.pass_rights_enabled = enabled;
if !enabled {
self.set_pass_rights_pair(0, 0);
}
}
pub(crate) fn set_pass_rights_pair(&mut self, black: u8, white: u8) {
let black = black.min(15);
let white = white.min(15);
let st = self.cur_state_mut();
let old_black = st.get_pass_rights(Color::Black);
let old_white = st.get_pass_rights(Color::White);
st.set_pass_rights_internal(Color::Black, black);
st.set_pass_rights_internal(Color::White, white);
st.board_key ^= zobrist_pass_rights(old_black, old_white);
st.board_key ^= zobrist_pass_rights(black, white);
}
pub fn set_sfen_with_pass_rights(
&mut self,
sfen: &str,
black_rights: u8,
white_rights: u8,
) -> Result<(), super::sfen::SfenError> {
self.set_sfen(sfen)?;
self.set_pass_rights_enabled(true);
self.set_pass_rights_pair(black_rights, white_rights);
Ok(())
}
pub fn enable_pass_rights(&mut self, black_rights: u8, white_rights: u8) {
self.set_pass_rights_enabled(true);
self.set_pass_rights_pair(black_rights, white_rights);
}
pub fn set_startpos_with_pass_rights(&mut self, black_rights: u8, white_rights: u8) {
self.set_hirate();
self.set_pass_rights_enabled(true);
self.set_pass_rights_pair(black_rights, white_rights);
}
pub fn do_move(&mut self, m: Move, gives_check: bool) -> DirtyPiece {
if m.is_pass() {
return self.do_pass_move();
}
let noop = NoPrefetch;
self.do_move_with_prefetch(m, gives_check, &noop)
}
pub(crate) fn do_move_with_prefetch<P: TtPrefetch>(
&mut self,
m: Move,
gives_check: bool,
prefetcher: &P,
) -> DirtyPiece {
if m.is_pass() {
return self.do_pass_move();
}
let us = self.side_to_move;
let them = !us;
let prev_continuous = self.cur_state().continuous_check;
let update_board_effects = Self::should_update_board_effects();
let prev_blockers = self.cur_state().blockers_for_king;
let prev_pinners = self.cur_state().pinners;
let prev_king_sq = self.king_square;
let mut new_state = self.cur_state().partial_clone();
let mut dirty_piece = DirtyPiece::new();
let mut material_value = new_state.material_value.raw();
self.game_ply += 1;
new_state.plies_from_null += 1;
new_state.board_key ^= zobrist_side();
let mut moved_from: Option<Square> = None;
let moved_to: Square;
let moved_pt: PieceType;
if update_board_effects {
self.ensure_board_effects();
} else {
self.board_effects_dirty = true;
}
if m.is_drop() {
let pt = m.drop_piece_type();
let to = m.to();
let pc = Piece::new(us, pt);
moved_to = to;
moved_pt = pt;
if update_board_effects {
let occupied_before = self.occupied();
update_by_dropping_piece(
&mut self.board_effects,
&mut self.long_effects,
occupied_before,
to,
pc,
);
}
let old_count = self.hand[us.index()].count(pt) as u8;
let old_bp = ExtBonaPiece::from_hand(us, pt, old_count);
let piece_no = self.piece_list.piece_no_of_hand(old_bp.fb);
self.hand[us.index()] = self.hand[us.index()].sub(pt);
new_state.hand_key = new_state.hand_key.wrapping_sub(zobrist_hand(us, pt));
self.put_piece_internal(pc, to);
new_state.board_key ^= zobrist_psq(pc, to);
self.xor_partial_keys(&mut new_state, pc, to);
let new_bp = ExtBonaPiece::from_board(pc, to);
self.piece_list.put_piece_on_board(piece_no, new_bp, to);
dirty_piece.dirty_num = 1;
dirty_piece.piece_no[0] = piece_no;
dirty_piece.changed_piece[0] = ChangedBonaPiece {
old_piece: old_bp,
new_piece: new_bp,
};
new_state.captured_piece = Piece::NONE;
} else {
let from = m.from();
let to = m.to();
moved_from = Some(from);
moved_to = to;
let pc = self.piece_on(from);
let captured = self.piece_on(to);
debug_assert!(
!m.is_promote() || pc.piece_type().can_promote(),
"Cannot promote piece {pc:?} (type={:?}) at {from:?} with move {} in position {}\n\
move raw bits: 0x{:08x}, is_drop={}, is_promote={}",
pc.piece_type(),
m.to_usi(),
self.to_sfen(),
m.raw(),
m.is_drop(),
m.is_promote()
);
moved_pt = if m.is_promote() {
pc.piece_type().promote().unwrap()
} else {
pc.piece_type()
};
let moved_after_pc = if m.is_promote() {
pc.promote().unwrap()
} else {
pc
};
if update_board_effects {
let occupied_before = self.occupied();
if captured.is_some() {
update_by_capturing_piece(
&mut self.board_effects,
&mut self.long_effects,
occupied_before,
from,
to,
pc,
moved_after_pc,
captured,
);
} else {
update_by_no_capturing_piece(
&mut self.board_effects,
&mut self.long_effects,
occupied_before,
from,
to,
pc,
moved_after_pc,
);
}
}
let piece_no_moved = self.piece_list.piece_no_of_board(from);
let old_bp_moved = self.piece_list.bona_piece(piece_no_moved);
if captured.is_some() {
let captured_pt = captured.piece_type().unpromote();
debug_assert!(
captured_pt != PieceType::King,
"illegal capture of king at {} by move {} in position {}",
to.to_usi(),
m.to_usi(),
self.to_sfen()
);
let piece_no_cap = self.piece_list.piece_no_of_board(to);
let old_bp_cap = self.piece_list.bona_piece(piece_no_cap);
self.remove_piece_internal(to);
new_state.board_key ^= zobrist_psq(captured, to);
self.xor_partial_keys(&mut new_state, captured, to);
material_value -= signed_piece_value(captured);
if matches!(
captured_pt,
PieceType::Pawn
| PieceType::Lance
| PieceType::Knight
| PieceType::Silver
| PieceType::Gold
| PieceType::Bishop
| PieceType::Rook
) {
self.hand[us.index()] = self.hand[us.index()].add(captured_pt);
new_state.hand_key =
new_state.hand_key.wrapping_add(zobrist_hand(us, captured_pt));
material_value += hand_piece_value(us, captured_pt);
}
let new_count = self.hand[us.index()].count(captured_pt) as u8;
let new_bp_cap = ExtBonaPiece::from_hand(us, captured_pt, new_count);
self.piece_list.put_piece_on_hand(piece_no_cap, new_bp_cap);
dirty_piece.piece_no[1] = piece_no_cap;
dirty_piece.changed_piece[1] = ChangedBonaPiece {
old_piece: old_bp_cap,
new_piece: new_bp_cap,
};
dirty_piece.dirty_num = 2;
} else {
dirty_piece.dirty_num = 1;
}
new_state.captured_piece = captured;
self.remove_piece_internal(from);
new_state.board_key ^= zobrist_psq(pc, from);
self.xor_partial_keys(&mut new_state, pc, from);
self.put_piece_internal(moved_after_pc, to);
new_state.board_key ^= zobrist_psq(moved_after_pc, to);
self.xor_partial_keys(&mut new_state, moved_after_pc, to);
if moved_after_pc != pc {
material_value += signed_piece_value(moved_after_pc) - signed_piece_value(pc);
}
if pc.piece_type() == PieceType::King {
self.king_square[us.index()] = to;
dirty_piece.king_moved[us.index()] = true;
}
let new_bp_moved = ExtBonaPiece::from_board(moved_after_pc, to);
self.piece_list.put_piece_on_board(piece_no_moved, new_bp_moved, to);
dirty_piece.piece_no[0] = piece_no_moved;
dirty_piece.changed_piece[0] = ChangedBonaPiece {
old_piece: old_bp_moved,
new_piece: new_bp_moved,
};
}
prefetcher.prefetch(new_state.key(), them);
let mut checkers = Bitboard::EMPTY;
if gives_check {
let ksq = self.king_square[them.index()];
debug_assert!((moved_pt as usize) < PieceType::NUM + 1);
checkers |= unsafe { *self.cur_state().check_squares.get_unchecked(moved_pt as usize) }
& Bitboard::from_square(moved_to);
if let Some(from_sq) = moved_from {
let prev_blockers = self.cur_state().blockers_for_king[them.index()];
if prev_blockers.contains(from_sq)
&& !crate::mate::aligned(from_sq, moved_to, ksq)
&& let Some(dir) = crate::bitboard::direct_of(ksq, from_sq)
{
let ray = crate::bitboard::direct_effect(from_sq, dir, self.occupied());
checkers |= ray & self.pieces_c(us);
}
}
}
debug_assert!(
{
let expected = self.attackers_to_c(self.king_square[them.index()], us);
let result = if gives_check {
checkers == expected
} else {
expected.is_empty()
};
if !result {
eprintln!(
"gives_check mismatch: gives_check={gives_check}, checkers={checkers:?}, actual={expected:?}"
);
}
result
},
"gives_check mismatch detected"
);
let is_check = !checkers.is_empty();
if is_check {
new_state.continuous_check[us.index()] = prev_continuous[us.index()] + 2;
} else {
new_state.continuous_check[us.index()] = 0;
}
self.side_to_move = them;
new_state.checkers = checkers;
new_state.hand_snapshot = self.hand;
new_state.material_value = Value::new(material_value);
new_state.last_move = m;
self.push_state(new_state);
self.update_repetition_info();
{
let occ_after = self.occupied();
let changed_sqs: [Option<Square>; 2] = [moved_from, Some(moved_to)];
for c in [Color::Black, Color::White] {
let king_sq_prev = prev_king_sq[c.index()];
let king_sq_now = self.king_square[c.index()];
let king_moved = king_sq_prev != king_sq_now;
let mut needs_recompute = king_moved;
if !needs_recompute {
for sq in changed_sqs.iter().flatten().copied() {
if prev_blockers[c.index()].contains(sq)
|| prev_pinners[c.index()].contains(sq)
|| crate::bitboard::direct_of(king_sq_now, sq).is_some()
{
needs_recompute = true;
break;
}
}
}
if !needs_recompute {
let st = self.cur_state_mut();
st.blockers_for_king[c.index()] = prev_blockers[c.index()];
st.pinners[c.index()] = prev_pinners[c.index()];
continue;
}
let (blockers, pinners) =
self.compute_blockers_and_pinners(c, occ_after, Bitboard::EMPTY);
let st = self.cur_state_mut();
st.blockers_for_king[c.index()] = blockers;
st.pinners[c.index()] = pinners;
}
}
self.update_check_squares();
#[cfg(debug_assertions)]
if update_board_effects {
self.debug_verify_board_effects();
}
dirty_piece
}
pub fn undo_move(&mut self, m: Move) {
if m.is_pass() {
return self.undo_pass_move();
}
self.side_to_move = !self.side_to_move;
self.game_ply -= 1;
let us = self.side_to_move;
let captured = self.cur_state().captured_piece;
let prev_idx = self.cur_state().previous;
assert_ne!(prev_idx, StateInfo::NO_PREVIOUS, "No previous state for undo");
let update_board_effects = Self::should_update_board_effects();
if update_board_effects {
self.ensure_board_effects();
} else {
self.board_effects_dirty = true;
}
if m.is_drop() {
let pt = m.drop_piece_type();
let to = m.to();
let moved_pc = self.piece_on(to);
let piece_no = self.piece_list.piece_no_of_board(to);
self.remove_piece_internal(to);
debug_assert!(us.index() < Color::NUM);
let hand_ref = unsafe { self.hand.get_unchecked_mut(us.index()) };
*hand_ref = hand_ref.add(pt);
let new_count = unsafe { self.hand.get_unchecked(us.index()) }.count(pt) as u8;
let hand_bp = ExtBonaPiece::from_hand(us, pt, new_count);
self.piece_list.put_piece_on_hand(piece_no, hand_bp);
if update_board_effects {
let occupied_after = self.occupied();
rewind_by_dropping_piece(
&mut self.board_effects,
&mut self.long_effects,
occupied_after,
to,
moved_pc,
);
}
} else {
let from = m.from();
let to = m.to();
let moved_pc = self.piece_on(to);
let original_pc = if m.is_promote() {
moved_pc.unpromote()
} else {
moved_pc
};
let piece_no_moved = self.piece_list.piece_no_of_board(to);
let from_bp = ExtBonaPiece::from_board(original_pc, from);
if captured.is_some() {
let cap_pt = captured.piece_type().unpromote();
let hand_count = unsafe { self.hand.get_unchecked(us.index()) }.count(cap_pt) as u8;
let hand_bp_fb =
crate::nnue::BonaPiece::from_hand_piece(Color::Black, us, cap_pt, hand_count);
let piece_no_cap = self.piece_list.piece_no_of_hand(hand_bp_fb);
let cap_board_bp = ExtBonaPiece::from_board(captured, to);
self.remove_piece_internal(to);
self.put_piece_internal(captured, to);
let hand_ref = unsafe { self.hand.get_unchecked_mut(us.index()) };
*hand_ref = hand_ref.sub(cap_pt);
self.put_piece_internal(original_pc, from);
self.piece_list.put_piece_on_board(piece_no_cap, cap_board_bp, to);
self.piece_list.put_piece_on_board(piece_no_moved, from_bp, from);
if original_pc.piece_type() == PieceType::King {
self.king_square[us.index()] = from;
}
if update_board_effects {
let occupied_after = self.occupied();
rewind_by_capturing_piece(
&mut self.board_effects,
&mut self.long_effects,
occupied_after,
from,
to,
original_pc,
moved_pc,
captured,
);
}
} else {
self.remove_piece_internal(to);
self.put_piece_internal(original_pc, from);
self.piece_list.put_piece_on_board(piece_no_moved, from_bp, from);
if original_pc.piece_type() == PieceType::King {
self.king_square[us.index()] = from;
}
if update_board_effects {
let occupied_after = self.occupied();
rewind_by_no_capturing_piece(
&mut self.board_effects,
&mut self.long_effects,
occupied_after,
from,
to,
original_pc,
moved_pc,
);
}
}
}
self.state_idx = prev_idx;
#[cfg(debug_assertions)]
if update_board_effects {
self.debug_verify_board_effects();
}
}
pub fn do_null_move(&mut self) {
let noop = NoPrefetch;
self.do_null_move_with_prefetch(&noop);
}
pub(crate) fn do_null_move_with_prefetch<P: TtPrefetch>(&mut self, prefetcher: &P) {
let mut new_state = self.cur_state().partial_clone();
new_state.board_key ^= zobrist_side();
new_state.plies_from_null = 0;
new_state.captured_piece = Piece::NONE;
new_state.last_move = Move::NULL;
new_state.hand_snapshot = self.hand;
let next_side = !self.side_to_move;
prefetcher.prefetch(new_state.key(), next_side);
self.side_to_move = next_side;
self.push_state(new_state);
self.cur_state_mut().checkers = Bitboard::EMPTY;
self.update_blockers_and_pinners();
self.update_check_squares();
}
pub fn undo_null_move(&mut self) {
self.side_to_move = !self.side_to_move;
let prev_idx = self.cur_state().previous;
assert_ne!(prev_idx, StateInfo::NO_PREVIOUS, "No previous state for undo_null_move");
self.state_idx = prev_idx;
}
pub fn do_pass_move(&mut self) -> DirtyPiece {
assert!(self.can_pass(), "Cannot pass: rule disabled, in check, or no pass rights");
let us = self.side_to_move;
let them = !us;
let mut new_state = self.cur_state().partial_clone();
let old_black = new_state.get_pass_rights(Color::Black);
let old_white = new_state.get_pass_rights(Color::White);
let new_black = if us == Color::Black {
old_black.checked_sub(1).expect("Black pass rights underflow")
} else {
old_black
};
let new_white = if us == Color::White {
old_white.checked_sub(1).expect("White pass rights underflow")
} else {
old_white
};
new_state.set_pass_rights_internal(Color::Black, new_black);
new_state.set_pass_rights_internal(Color::White, new_white);
new_state.board_key ^= zobrist_side();
new_state.board_key ^= zobrist_pass_rights(old_black, old_white);
new_state.board_key ^= zobrist_pass_rights(new_black, new_white);
new_state.captured_piece = Piece::NONE;
new_state.last_move = Move::PASS;
new_state.hand_snapshot = self.hand;
new_state.plies_from_null += 1;
let their_king = self.king_square(them);
let gives_check = !self.attackers_to_c(their_king, us).is_empty();
if gives_check {
new_state.continuous_check[us.index()] += 2;
} else {
new_state.continuous_check[us.index()] = 0;
}
self.side_to_move = them;
self.game_ply += 1;
self.push_state(new_state);
self.cur_state_mut().checkers =
self.attackers_to_c(self.king_square(self.side_to_move), !self.side_to_move);
self.update_blockers_and_pinners();
self.update_check_squares();
self.update_repetition_info();
DirtyPiece::new()
}
pub fn undo_pass_move(&mut self) {
self.side_to_move = !self.side_to_move;
self.game_ply -= 1;
let prev_idx = self.cur_state().previous;
assert_ne!(prev_idx, StateInfo::NO_PREVIOUS, "No previous state for undo_pass_move");
self.state_idx = prev_idx;
}
fn update_repetition_info(&mut self) {
let side = self.side_to_move;
let (plies_from_null, board_key, hand_snapshot, prev_idx, cc_side, cc_opp) = {
let st = self.cur_state();
(
st.plies_from_null,
st.board_key,
st.hand_snapshot,
st.previous,
st.continuous_check[side.index()],
st.continuous_check[(!side).index()],
)
};
let max_back = plies_from_null.min(16);
let mut repetition = 0;
let mut repetition_times = 0;
let mut repetition_type = RepetitionState::None;
if max_back >= 4 && prev_idx != StateInfo::NO_PREVIOUS {
let mut dist = 4;
let mut st_idx = prev_idx;
for _ in 0..3 {
debug_assert!(st_idx < self.state_stack.len());
st_idx = unsafe { self.state_stack.get_unchecked(st_idx) }.previous;
if st_idx == StateInfo::NO_PREVIOUS {
break;
}
}
while dist <= max_back && st_idx != StateInfo::NO_PREVIOUS {
debug_assert!(st_idx < self.state_stack.len());
let stp = unsafe { self.state_stack.get_unchecked(st_idx) };
if stp.board_key == board_key {
let prev_hand = stp.hand_snapshot[side.index()];
let cur_hand = hand_snapshot[side.index()];
if cur_hand == prev_hand {
let times = stp.repetition_times + 1;
repetition_times = times;
repetition = if times >= 3 { -dist } else { dist };
let mut rep_type = if dist <= cc_side {
RepetitionState::Lose
} else if dist <= cc_opp {
RepetitionState::Win
} else {
RepetitionState::Draw
};
if stp.repetition_times > 0 && stp.repetition_type != rep_type {
rep_type = RepetitionState::Draw;
}
repetition_type = rep_type;
break;
}
if cur_hand.is_superior_or_equal(prev_hand) {
repetition_type = RepetitionState::Superior;
repetition = dist;
break;
}
if prev_hand.is_superior_or_equal(cur_hand) {
repetition_type = RepetitionState::Inferior;
repetition = dist;
break;
}
}
let prev_same_side = stp.previous;
if prev_same_side == StateInfo::NO_PREVIOUS {
break;
}
debug_assert!(prev_same_side < self.state_stack.len());
st_idx = unsafe { self.state_stack.get_unchecked(prev_same_side) }.previous;
dist += 2;
}
}
let st = self.cur_state_mut();
st.repetition = repetition;
st.repetition_times = repetition_times;
st.repetition_type = repetition_type;
}
pub fn gives_check(&self, m: Move) -> bool {
if m.is_pass() {
let them = !self.side_to_move;
let their_king = self.king_square(them);
return !self.attackers_to_c(their_king, self.side_to_move).is_empty();
}
let us = self.side_to_move;
let to = m.to();
if m.is_drop() {
let pt = m.drop_piece_type();
return self.check_squares(pt).contains(to);
}
let from = m.from();
let pc = self.piece_on(from);
let pt = pc.piece_type();
let moved_pt = if m.is_promote() {
pt.promote().unwrap_or(pt)
} else {
pt
};
if self.check_squares(moved_pt).contains(to) {
return true;
}
let them = !us;
debug_assert!(them.index() < Color::NUM);
let ksq = unsafe { *self.king_square.get_unchecked(them.index()) };
let blockers = self.blockers_for_king(them) & self.pieces_c(us);
if blockers.contains(from) {
let dir_from = crate::bitboard::direct_of(ksq, from);
debug_assert!(
dir_from.is_some(),
"blocker at {from:?} must be on line with king at {ksq:?}"
);
let dir_to = crate::bitboard::direct_of(ksq, to);
if dir_from != dir_to {
return true;
}
}
false
}
pub fn mate_1ply(&mut self) -> Move {
crate::mate::mate_1ply(self).unwrap_or(Move::NONE)
}
pub(crate) fn enemy_field(us: Color) -> Bitboard {
match us {
Color::Black => RANK_BB[0] | RANK_BB[1] | RANK_BB[2],
Color::White => RANK_BB[6] | RANK_BB[7] | RANK_BB[8],
}
}
pub fn declaration_win(&self, rule: EnteringKingRule) -> Move {
if rule == EnteringKingRule::None {
return Move::NONE;
}
let us = self.side_to_move();
if rule == EnteringKingRule::TryRule {
return self.try_rule_move(us);
}
let ksq = self.king_square(us);
let ef = Self::enemy_field(us);
if !ef.contains(ksq) {
return Move::NONE;
}
if self.in_check() {
return Move::NONE;
}
let our_in_enemy = self.pieces_c(us) & ef;
if our_in_enemy.count() < 11 {
return Move::NONE;
}
let big_set = PieceTypeSet::bishop_horse() | PieceTypeSet::rook_dragon();
let big_in_enemy = (self.pieces_c_by_types(us, big_set) & ef).count();
let h = self.hand(us);
let score = our_in_enemy.count() + big_in_enemy * 4 - 1
+ h.count(PieceType::Pawn)
+ h.count(PieceType::Lance)
+ h.count(PieceType::Knight)
+ h.count(PieceType::Silver)
+ h.count(PieceType::Gold)
+ (h.count(PieceType::Bishop) + h.count(PieceType::Rook)) * 5;
let mut required = match rule {
EnteringKingRule::Point24 | EnteringKingRule::Point24H => 31u32,
EnteringKingRule::Point27 | EnteringKingRule::Point27H => match us {
Color::Black => 28,
Color::White => 27,
},
EnteringKingRule::None | EnteringKingRule::TryRule => unreachable!(),
};
if matches!(rule, EnteringKingRule::Point24H | EnteringKingRule::Point27H) {
let total = self.count_total_piece_points();
if total < 56 {
let deficit = 56 - total;
if us == Color::White {
required = required.saturating_sub(deficit);
}
}
}
if score >= required {
Move::WIN
} else {
Move::NONE
}
}
fn try_rule_move(&self, us: Color) -> Move {
let ksq = self.king_square(us);
let try_sq = match us {
Color::Black => Square::new(File::File5, Rank::Rank1),
Color::White => Square::new(File::File5, Rank::Rank9),
};
if !king_effect(ksq).contains(try_sq) {
return Move::NONE;
}
if self.pieces_c(us).contains(try_sq) {
return Move::NONE;
}
let occ_without_king = self.occupied() ^ Bitboard::from_square(ksq);
let enemy_attackers =
self.attackers_to_occ(try_sq, occ_without_king) & self.pieces_c(us.opponent());
if !enemy_attackers.is_empty() {
return Move::NONE;
}
let king_piece = Piece::new(us, PieceType::King);
Move::new_move(ksq, try_sq, false).with_piece(king_piece)
}
fn count_total_piece_points(&self) -> u32 {
let big_set = PieceTypeSet::bishop_horse() | PieceTypeSet::rook_dragon();
let all_count = self.occupied().count();
let big_count = self.pieces_by_types(big_set).count();
let board_score = all_count + big_count * 4;
let mut hand_score = 0u32;
for c in [Color::Black, Color::White] {
let h = self.hand(c);
hand_score += h.count(PieceType::Pawn)
+ h.count(PieceType::Lance)
+ h.count(PieceType::Knight)
+ h.count(PieceType::Silver)
+ h.count(PieceType::Gold)
+ (h.count(PieceType::Bishop) + h.count(PieceType::Rook)) * 5;
}
board_score + hand_score
}
}
impl Default for Position {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{EnteringKingRule, File, Rank};
#[test]
fn test_position_new() {
let pos = Position::new();
assert_eq!(pos.side_to_move(), Color::Black);
assert_eq!(pos.game_ply(), 0);
assert!(pos.occupied().is_empty());
}
#[test]
fn test_put_and_remove_piece() {
let mut pos = Position::new();
let sq = Square::new(File::File5, Rank::Rank5);
pos.put_piece(Piece::B_PAWN, sq);
assert_eq!(pos.piece_on(sq), Piece::B_PAWN);
assert!(pos.pieces(Color::Black, PieceType::Pawn).contains(sq));
pos.remove_piece(sq);
assert_eq!(pos.piece_on(sq), Piece::NONE);
assert!(!pos.pieces(Color::Black, PieceType::Pawn).contains(sq));
}
#[test]
fn test_blockers_pinners_incremental_matches_full() {
let mut pos = Position::new();
let bk = Square::new(File::File5, Rank::Rank9);
let wk = Square::new(File::File1, Rank::Rank1);
let rook = Square::new(File::File5, Rank::Rank6);
let blocker = Square::new(File::File5, Rank::Rank8);
let knight = Square::new(File::File1, Rank::Rank3);
pos.put_piece(Piece::B_KING, bk);
pos.king_square[Color::Black.index()] = bk;
pos.put_piece(Piece::W_KING, wk);
pos.king_square[Color::White.index()] = wk;
pos.put_piece(Piece::W_ROOK, rook);
pos.put_piece(Piece::B_GOLD, blocker);
pos.put_piece(Piece::B_KNIGHT, knight);
pos.init_piece_list();
pos.update_blockers_and_pinners();
pos.update_check_squares();
let prev_blockers = pos.blockers_for_king(Color::Black);
let prev_pinners = pos.cur_state().pinners[Color::White.index()];
let mv_offline = Move::new_move(knight, Square::new(File::File1, Rank::Rank2), false);
let gives_check = pos.gives_check(mv_offline);
pos.do_move(mv_offline, gives_check);
assert_eq!(pos.blockers_for_king(Color::Black), prev_blockers);
assert_eq!(pos.cur_state().pinners[Color::White.index()], prev_pinners);
pos.side_to_move = Color::Black;
pos.update_check_squares();
let mv_unblock = Move::new_move(blocker, Square::new(File::File6, Rank::Rank8), false);
let gives_check = pos.gives_check(mv_unblock);
pos.do_move(mv_unblock, gives_check);
let (blockers_full, pinners_full) =
pos.compute_blockers_and_pinners(Color::Black, pos.occupied(), Bitboard::EMPTY);
assert_eq!(pos.blockers_for_king(Color::Black), blockers_full);
assert_eq!(pos.cur_state().pinners[Color::White.index()], pinners_full);
let mut pos = Position::new();
let wk = Square::new(File::File1, Rank::Rank9);
let br = Square::new(File::File1, Rank::Rank1);
let b_blocker = Square::new(File::File1, Rank::Rank7);
let w_target = Square::new(File::File2, Rank::Rank7);
let bk = Square::new(File::File5, Rank::Rank9); pos.put_piece(Piece::W_KING, wk);
pos.king_square[Color::White.index()] = wk;
pos.put_piece(Piece::B_KING, bk);
pos.king_square[Color::Black.index()] = bk;
pos.put_piece(Piece::B_ROOK, br);
pos.put_piece(Piece::B_GOLD, b_blocker);
pos.put_piece(Piece::W_PAWN, w_target);
pos.init_piece_list();
pos.side_to_move = Color::Black;
pos.update_blockers_and_pinners();
pos.update_check_squares();
let mv_capture = Move::new_move(b_blocker, w_target, false);
assert!(
pos.blockers_for_king(Color::White).contains(b_blocker),
"Gold at 1七 should be a blocker for White king"
);
let gives_check = pos.gives_check(mv_capture);
assert!(gives_check, "Move should give check (discovered check)");
pos.do_move(mv_capture, gives_check);
assert!(pos.cur_state().checkers.contains(br));
let mut pos = Position::new();
let bk = Square::new(File::File5, Rank::Rank9);
let wk = Square::new(File::File1, Rank::Rank1);
let wr = Square::new(File::File5, Rank::Rank6);
pos.put_piece(Piece::B_KING, bk);
pos.king_square[Color::Black.index()] = bk;
pos.put_piece(Piece::W_KING, wk);
pos.king_square[Color::White.index()] = wk;
pos.put_piece(Piece::W_ROOK, wr);
pos.init_piece_list();
pos.side_to_move = Color::Black;
pos.update_blockers_and_pinners();
pos.update_check_squares();
let king_from = bk;
let king_to = Square::new(File::File6, Rank::Rank9);
let king_move = Move::new_move(king_from, king_to, false);
let gives_check = pos.gives_check(king_move);
assert!(!gives_check, "King move should not give check");
pos.do_move(king_move, gives_check);
let (blockers_full, pinners_full) =
pos.compute_blockers_and_pinners(Color::Black, pos.occupied(), Bitboard::EMPTY);
assert_eq!(pos.blockers_for_king(Color::Black), blockers_full);
assert_eq!(pos.cur_state().pinners[Color::White.index()], pinners_full);
}
#[test]
fn test_pieces_by_type_set() {
let mut pos = Position::new();
let gold_sq = Square::new(File::File5, Rank::Rank5);
let pro_sq = Square::new(File::File4, Rank::Rank4);
let dragon_sq = Square::new(File::File9, Rank::Rank9);
pos.put_piece(Piece::B_GOLD, gold_sq);
pos.put_piece(Piece::B_PRO_PAWN, pro_sq);
pos.put_piece(Piece::W_DRAGON, dragon_sq);
let gold_like = pos.pieces_c_by_types(Color::Black, PieceTypeSet::golds());
assert!(gold_like.contains(gold_sq));
assert!(gold_like.contains(pro_sq));
assert!(!gold_like.contains(dragon_sq));
let sliders = pos.pieces_by_types(PieceTypeSet::rook_dragon());
assert!(sliders.contains(dragon_sq));
assert!(!sliders.contains(gold_sq));
let all_black = pos.pieces_c_by_types(Color::Black, PieceTypeSet::ALL);
assert_eq!(all_black.count(), 2);
}
#[test]
fn test_pinned_pieces_excluding_removes_pinner_itself() {
let mut pos = Position::new();
let ksq = Square::new(File::File5, Rank::Rank9);
let pinner_sq = Square::new(File::File5, Rank::Rank1);
let blocker_sq = Square::new(File::File5, Rank::Rank5);
pos.put_piece(Piece::B_KING, ksq);
pos.put_piece(Piece::W_ROOK, pinner_sq);
pos.put_piece(Piece::B_GOLD, blocker_sq);
pos.king_square[Color::Black.index()] = ksq;
pos.king_square[Color::White.index()] = Square::new(File::File1, Rank::Rank1);
let pinned_normal = pos.pinned_pieces_excluding(Color::Black, Square::SQ_11);
assert!(pinned_normal.contains(blocker_sq));
let pinned_without_pinner = pos.pinned_pieces_excluding(Color::Black, pinner_sq);
assert!(pinned_without_pinner.is_empty(), "avoidがpinner自身のときpinは消える必要がある");
}
#[test]
fn test_attackers_to_pawn() {
let mut pos = Position::new();
let sq55 = Square::new(File::File5, Rank::Rank5);
let sq54 = Square::new(File::File5, Rank::Rank4);
pos.put_piece(Piece::B_PAWN, sq55);
let attackers = pos.attackers_to(sq54);
assert!(attackers.contains(sq55));
}
#[test]
fn test_do_move_drop() {
let mut pos = Position::new();
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
pos.hand[Color::Black.index()] = pos.hand[Color::Black.index()].add(PieceType::Pawn);
pos.init_piece_list();
let to = Square::new(File::File5, Rank::Rank5);
let m = Move::new_drop(PieceType::Pawn, to);
pos.do_move(m, false);
assert_eq!(pos.piece_on(to), Piece::B_PAWN);
assert_eq!(pos.side_to_move(), Color::White);
assert!(!pos.hand(Color::Black).has(PieceType::Pawn));
pos.undo_move(m);
assert_eq!(pos.piece_on(to), Piece::NONE);
assert_eq!(pos.side_to_move(), Color::Black);
assert!(pos.hand(Color::Black).has(PieceType::Pawn));
}
#[test]
fn test_do_move_normal() {
let mut pos = Position::new();
let sq77 = Square::new(File::File7, Rank::Rank7);
let sq76 = Square::new(File::File7, Rank::Rank6);
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
pos.put_piece(Piece::B_PAWN, sq77);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
pos.init_piece_list();
let m = Move::new_move(sq77, sq76, false);
pos.do_move(m, false);
assert_eq!(pos.piece_on(sq77), Piece::NONE);
assert_eq!(pos.piece_on(sq76), Piece::B_PAWN);
assert_eq!(pos.side_to_move(), Color::White);
pos.undo_move(m);
assert_eq!(pos.piece_on(sq77), Piece::B_PAWN);
assert_eq!(pos.piece_on(sq76), Piece::NONE);
assert_eq!(pos.side_to_move(), Color::Black);
}
#[test]
fn test_do_move_capture() {
let mut pos = Position::new();
let sq76 = Square::new(File::File7, Rank::Rank6);
let sq75 = Square::new(File::File7, Rank::Rank5);
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
pos.put_piece(Piece::B_PAWN, sq76);
pos.put_piece(Piece::W_PAWN, sq75);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
pos.init_piece_list();
let m = Move::new_move(sq76, sq75, false);
pos.do_move(m, false);
assert_eq!(pos.piece_on(sq76), Piece::NONE);
assert_eq!(pos.piece_on(sq75), Piece::B_PAWN);
assert!(pos.hand(Color::Black).has(PieceType::Pawn));
assert_eq!(pos.side_to_move(), Color::White);
pos.undo_move(m);
assert_eq!(pos.piece_on(sq76), Piece::B_PAWN);
assert_eq!(pos.piece_on(sq75), Piece::W_PAWN);
assert!(!pos.hand(Color::Black).has(PieceType::Pawn));
assert_eq!(pos.side_to_move(), Color::Black);
}
#[test]
fn test_do_move_promote() {
let mut pos = Position::new();
let sq23 = Square::new(File::File2, Rank::Rank3);
let sq22 = Square::new(File::File2, Rank::Rank2);
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
pos.put_piece(Piece::B_PAWN, sq23);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
pos.init_piece_list();
let m = Move::new_move(sq23, sq22, true);
pos.do_move(m, false);
assert_eq!(pos.piece_on(sq23), Piece::NONE);
assert_eq!(pos.piece_on(sq22), Piece::B_PRO_PAWN);
pos.undo_move(m);
assert_eq!(pos.piece_on(sq23), Piece::B_PAWN);
assert_eq!(pos.piece_on(sq22), Piece::NONE);
}
#[test]
fn test_checkers_matches_attackers_after_moves() {
let mut pos = Position::new();
pos.set_hirate();
let moves = [
"7g7f", "4a3b", "1g1f", "5a5b", "4g4f", "3c3d", "6g6f", "1c1d", "5i4h", "9c9d", "4h4g",
"4c4d", "2h3h", "9a9c", "1i1g", "3a4b", "3h7h", "5c5d", "5g5f", "6c6d", "7h1h", "8b6b",
"1h5h", "6d6e", "6f6e", "6b6e", "5h6h", "P*6g", "6h4h", "4d4e", "8h2b+", "3b2b",
"B*7g", "4e4f",
];
for (idx, mv_str) in moves.iter().enumerate() {
let mv = Move::from_usi(mv_str).unwrap_or_else(|| panic!("invalid move: {mv_str}"));
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
let king_sq = pos.king_square(pos.side_to_move());
let expected_checkers = pos.attackers_to_c(king_sq, !pos.side_to_move());
assert_eq!(
pos.checkers(),
expected_checkers,
"checkers mismatch at ply {} after move {} in sfen {}",
idx + 1,
mv_str,
pos.to_sfen()
);
}
}
#[test]
fn test_do_move_sets_checkers_with_gives_check() {
let mut pos = Position::new();
let b_king = Square::new(File::File5, Rank::Rank9);
let w_king = Square::new(File::File5, Rank::Rank1);
pos.put_piece(Piece::B_KING, b_king);
pos.put_piece(Piece::W_KING, w_king);
pos.king_square[Color::Black.index()] = b_king;
pos.king_square[Color::White.index()] = w_king;
pos.hand[Color::Black.index()] = pos.hand[Color::Black.index()].add(PieceType::Gold);
pos.init_piece_list();
pos.update_check_squares();
let drop_sq = Square::from_usi("4a").unwrap();
let mv = Move::new_drop(PieceType::Gold, drop_sq);
let gives_check = pos.gives_check(mv);
assert!(gives_check, "gives_check should detect the check");
pos.do_move(mv, gives_check);
let expected_checkers = pos.attackers_to_c(pos.king_square(Color::White), Color::Black);
assert!(!expected_checkers.is_empty(), "drop should give check");
assert_eq!(pos.checkers(), expected_checkers);
assert_eq!(pos.state().continuous_check[Color::Black.index()], 2);
assert_eq!(pos.state().continuous_check[Color::White.index()], 0);
assert_eq!(pos.side_to_move(), Color::White);
}
#[test]
fn panic_position_disallows_king_capture() {
let sfen = "ln2k1+L1+R/2s2s3/p1pl1p3/1+r2p1p1p/9/4B4/5PPPP/4Gg3/2+b2GKNL w S2NPgs7p 107";
let mut pos = Position::new();
pos.set_sfen(sfen).unwrap();
let capture_king = Move::from_usi("4h3i").unwrap();
assert!(!pos.is_legal(capture_king));
let mut pos_black = Position::new();
pos_black.set_sfen(sfen).unwrap();
pos_black.side_to_move = Color::Black;
let b_king = pos_black.king_square(Color::Black);
pos_black.remove_piece(b_king);
let king_from = Square::from_usi("3h").unwrap();
let king_to = Square::from_usi("3i").unwrap();
pos_black.put_piece(Piece::B_KING, king_from);
pos_black.king_square[Color::Black.index()] = king_from;
pos_black.update_blockers_and_pinners();
pos_black.update_check_squares();
let king_move = Move::new_move(king_from, king_to, false);
assert!(!pos_black.is_legal(king_move));
}
#[test]
fn test_to_move_rejects_invalid_promote_flag_for_gold() {
let mut pos = Position::new();
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
let sq58 = Square::new(File::File5, Rank::Rank8);
let sq57 = Square::new(File::File5, Rank::Rank7);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.put_piece(Piece::B_GOLD, sq58);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
let invalid_move = Move::new_move(sq58, sq57, true);
assert!(invalid_move.is_promote(), "テスト用の指し手は成りフラグが立っている必要がある");
assert_eq!(
pos.to_move(invalid_move),
None,
"成れない駒(金)の成りフラグ付き指し手は弾かれるべき"
);
}
#[test]
fn test_to_move_rejects_invalid_promote_flag_for_king() {
let mut pos = Position::new();
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
let sq58 = Square::new(File::File5, Rank::Rank8);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
let invalid_move = Move::new_move(sq59, sq58, true);
assert_eq!(
pos.to_move(invalid_move),
None,
"成れない駒(玉)の成りフラグ付き指し手は弾かれるべき"
);
}
#[test]
fn test_to_move_rejects_invalid_promote_flag_for_promoted_piece() {
let mut pos = Position::new();
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
let sq55 = Square::new(File::File5, Rank::Rank5);
let sq54 = Square::new(File::File5, Rank::Rank4);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.put_piece(Piece::B_PRO_PAWN, sq55); pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
let invalid_move = Move::new_move(sq55, sq54, true);
assert_eq!(
pos.to_move(invalid_move),
None,
"既に成っている駒(と金)の成りフラグ付き指し手は弾かれるべき"
);
}
#[test]
fn test_to_move_accepts_valid_pawn_promotion() {
let mut pos = Position::new();
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
let sq23 = Square::new(File::File2, Rank::Rank3);
let sq22 = Square::new(File::File2, Rank::Rank2);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.put_piece(Piece::B_PAWN, sq23);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
let valid_move = Move::new_move(sq23, sq22, true);
let result = pos.to_move(valid_move);
assert!(result.is_some(), "成れる駒(歩)の成りは受け入れられるべき");
let mv = result.unwrap();
assert_eq!(
mv.moved_piece_after(),
Piece::B_PRO_PAWN,
"成りの場合、moved_piece_after はと金であるべき"
);
}
#[test]
fn test_to_move_accepts_valid_pawn_no_promotion() {
let mut pos = Position::new();
let sq59 = Square::new(File::File5, Rank::Rank9);
let sq51 = Square::new(File::File5, Rank::Rank1);
let sq24 = Square::new(File::File2, Rank::Rank4);
let sq23 = Square::new(File::File2, Rank::Rank3);
pos.put_piece(Piece::B_KING, sq59);
pos.put_piece(Piece::W_KING, sq51);
pos.put_piece(Piece::B_PAWN, sq24);
pos.king_square[Color::Black.index()] = sq59;
pos.king_square[Color::White.index()] = sq51;
let valid_move = Move::new_move(sq24, sq23, false);
let result = pos.to_move(valid_move);
assert!(result.is_some(), "不成の指し手は受け入れられるべき");
let mv = result.unwrap();
assert_eq!(
mv.moved_piece_after(),
Piece::B_PAWN,
"不成の場合、moved_piece_after は歩であるべき"
);
}
#[test]
fn test_composite_bitboard_consistency() {
let mut pos = Position::new();
pos.set_hirate();
let expected_golds = pos.pieces_pt(PieceType::Gold)
| pos.pieces_pt(PieceType::ProPawn)
| pos.pieces_pt(PieceType::ProLance)
| pos.pieces_pt(PieceType::ProKnight)
| pos.pieces_pt(PieceType::ProSilver);
assert_eq!(pos.golds(), expected_golds, "golds_bb mismatch");
let expected_bh = pos.pieces_pt(PieceType::Bishop) | pos.pieces_pt(PieceType::Horse);
assert_eq!(pos.bishop_horse(), expected_bh, "bishop_horse_bb mismatch");
let expected_rd = pos.pieces_pt(PieceType::Rook) | pos.pieces_pt(PieceType::Dragon);
assert_eq!(pos.rook_dragon(), expected_rd, "rook_dragon_bb mismatch");
let expected_hdk = pos.pieces_pt(PieceType::Horse)
| pos.pieces_pt(PieceType::Dragon)
| pos.pieces_pt(PieceType::King);
assert_eq!(pos.hdk_bb, expected_hdk, "hdk_bb mismatch");
}
#[test]
fn test_composite_bitboard_after_moves() {
let mut pos = Position::new();
pos.set_hirate();
let moves = ["7g7f", "3c3d", "8h2b+", "3a2b"];
for mv_str in moves {
let mv = Move::from_usi(mv_str).unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
let expected_golds = pos.pieces_pt(PieceType::Gold)
| pos.pieces_pt(PieceType::ProPawn)
| pos.pieces_pt(PieceType::ProLance)
| pos.pieces_pt(PieceType::ProKnight)
| pos.pieces_pt(PieceType::ProSilver);
assert_eq!(pos.golds(), expected_golds, "golds_bb mismatch after {mv_str}");
let expected_bh = pos.pieces_pt(PieceType::Bishop) | pos.pieces_pt(PieceType::Horse);
assert_eq!(pos.bishop_horse(), expected_bh, "bishop_horse_bb mismatch after {mv_str}");
let expected_rd = pos.pieces_pt(PieceType::Rook) | pos.pieces_pt(PieceType::Dragon);
assert_eq!(pos.rook_dragon(), expected_rd, "rook_dragon_bb mismatch after {mv_str}");
let expected_hdk = pos.pieces_pt(PieceType::Horse)
| pos.pieces_pt(PieceType::Dragon)
| pos.pieces_pt(PieceType::King);
assert_eq!(pos.hdk_bb, expected_hdk, "hdk_bb mismatch after {mv_str}");
}
for mv_str in moves.iter().rev() {
let mv = Move::from_usi(mv_str).unwrap();
pos.undo_move(mv);
let expected_golds = pos.pieces_pt(PieceType::Gold)
| pos.pieces_pt(PieceType::ProPawn)
| pos.pieces_pt(PieceType::ProLance)
| pos.pieces_pt(PieceType::ProKnight)
| pos.pieces_pt(PieceType::ProSilver);
assert_eq!(pos.golds(), expected_golds, "golds_bb mismatch after undo {mv_str}");
let expected_bh = pos.pieces_pt(PieceType::Bishop) | pos.pieces_pt(PieceType::Horse);
assert_eq!(
pos.bishop_horse(),
expected_bh,
"bishop_horse_bb mismatch after undo {mv_str}"
);
let expected_rd = pos.pieces_pt(PieceType::Rook) | pos.pieces_pt(PieceType::Dragon);
assert_eq!(
pos.rook_dragon(),
expected_rd,
"rook_dragon_bb mismatch after undo {mv_str}"
);
let expected_hdk = pos.pieces_pt(PieceType::Horse)
| pos.pieces_pt(PieceType::Dragon)
| pos.pieces_pt(PieceType::King);
assert_eq!(pos.hdk_bb, expected_hdk, "hdk_bb mismatch after undo {mv_str}");
}
}
#[test]
fn test_composite_bitboard_with_promotions() {
let mut pos = Position::new();
pos.set_sfen("4k4/9/9/9/4P4/9/9/9/4K4 b - 1").unwrap();
let to = Square::from_usi("5d").unwrap();
let mv = Move::from_usi("5e5d+").unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
assert!(pos.golds().contains(to), "と金がgolds_bbに含まれていない");
pos.undo_move(mv);
assert!(!pos.golds().contains(to), "undo後にと金がgolds_bbに残っている");
}
#[test]
fn test_composite_bitboard_rook_promotion() {
let mut pos = Position::new();
pos.set_sfen("4k4/9/9/9/9/9/4R4/9/4K4 b - 1").unwrap();
let to = Square::from_usi("5b").unwrap();
let mv = Move::from_usi("5g5b+").unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
assert!(pos.rook_dragon().contains(to), "龍がrook_dragon_bbに含まれていない");
let expected_rd = pos.pieces_pt(PieceType::Rook) | pos.pieces_pt(PieceType::Dragon);
assert_eq!(pos.rook_dragon(), expected_rd, "rook_dragon_bb mismatch");
pos.undo_move(mv);
assert!(!pos.rook_dragon().contains(to), "undo後に龍がrook_dragon_bbに残っている");
}
#[test]
fn test_composite_bitboard_lance_knight_silver_promotions() {
let mut pos = Position::new();
pos.set_sfen("4k4/9/9/4L4/9/9/9/9/4K4 b - 1").unwrap();
let mv = Move::from_usi("5d5c+").unwrap();
let to = Square::from_usi("5c").unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
assert!(pos.golds().contains(to), "成香がgolds_bbに含まれていない");
let expected_golds = pos.pieces_pt(PieceType::Gold)
| pos.pieces_pt(PieceType::ProPawn)
| pos.pieces_pt(PieceType::ProLance)
| pos.pieces_pt(PieceType::ProKnight)
| pos.pieces_pt(PieceType::ProSilver);
assert_eq!(pos.golds(), expected_golds, "golds_bb mismatch after 香成");
pos.undo_move(mv);
let mut pos = Position::new();
pos.set_sfen("4k4/9/9/9/4N4/9/9/9/4K4 b - 1").unwrap();
let mv = Move::from_usi("5e6c+").unwrap();
let to = Square::from_usi("6c").unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
assert!(pos.golds().contains(to), "成桂がgolds_bbに含まれていない");
let expected_golds = pos.pieces_pt(PieceType::Gold)
| pos.pieces_pt(PieceType::ProPawn)
| pos.pieces_pt(PieceType::ProLance)
| pos.pieces_pt(PieceType::ProKnight)
| pos.pieces_pt(PieceType::ProSilver);
assert_eq!(pos.golds(), expected_golds, "golds_bb mismatch after 桂成");
pos.undo_move(mv);
let mut pos = Position::new();
pos.set_sfen("4k4/9/9/9/4S4/9/9/9/4K4 b - 1").unwrap();
let mv = Move::from_usi("5e5d+").unwrap();
let to = Square::from_usi("5d").unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
assert!(pos.golds().contains(to), "成銀がgolds_bbに含まれていない");
let expected_golds = pos.pieces_pt(PieceType::Gold)
| pos.pieces_pt(PieceType::ProPawn)
| pos.pieces_pt(PieceType::ProLance)
| pos.pieces_pt(PieceType::ProKnight)
| pos.pieces_pt(PieceType::ProSilver);
assert_eq!(pos.golds(), expected_golds, "golds_bb mismatch after 銀成");
pos.undo_move(mv);
}
#[test]
fn test_composite_bitboard_capture_and_promote() {
let mut pos = Position::new();
pos.set_sfen("4k4/9/9/4p4/4P4/9/9/9/4K4 b - 1").unwrap();
let mv = Move::from_usi("5e5d+").unwrap();
let to = Square::from_usi("5d").unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
assert!(pos.golds().contains(to), "駒を取って成った後、と金がgolds_bbに含まれていない");
let expected_golds = pos.pieces_pt(PieceType::Gold)
| pos.pieces_pt(PieceType::ProPawn)
| pos.pieces_pt(PieceType::ProLance)
| pos.pieces_pt(PieceType::ProKnight)
| pos.pieces_pt(PieceType::ProSilver);
assert_eq!(pos.golds(), expected_golds, "golds_bb mismatch after capture and promote");
pos.undo_move(mv);
assert!(!pos.golds().contains(to), "undo後にと金がgolds_bbに残っている");
}
#[test]
fn test_composite_bitboard_bishop_promotion() {
let mut pos = Position::new();
pos.set_sfen("4k4/9/9/9/9/9/9/4B4/4K4 b - 1").unwrap();
let to = Square::from_usi("2b").unwrap();
let mv = Move::from_usi("5h2b+").unwrap();
let gives_check = pos.gives_check(mv);
pos.do_move(mv, gives_check);
assert!(pos.bishop_horse().contains(to), "馬がbishop_horse_bbに含まれていない");
let expected_bh = pos.pieces_pt(PieceType::Bishop) | pos.pieces_pt(PieceType::Horse);
assert_eq!(pos.bishop_horse(), expected_bh, "bishop_horse_bb mismatch");
pos.undo_move(mv);
assert!(!pos.bishop_horse().contains(to), "undo後に馬がbishop_horse_bbに残っている");
}
#[test]
fn test_pass_rights_enabled_default() {
let mut pos = Position::new();
pos.set_hirate();
assert!(!pos.is_pass_rights_enabled());
assert_eq!(pos.pass_rights(Color::Black), 0);
assert_eq!(pos.pass_rights(Color::White), 0);
}
#[test]
fn test_set_startpos_with_pass_rights() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 2);
assert!(pos.is_pass_rights_enabled());
assert_eq!(pos.pass_rights(Color::Black), 2);
assert_eq!(pos.pass_rights(Color::White), 2);
assert!(pos.can_pass()); }
#[test]
fn test_set_sfen_with_pass_rights() {
let mut pos = Position::new();
pos.set_sfen_with_pass_rights(
"lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1",
3,
5,
)
.unwrap();
assert!(pos.is_pass_rights_enabled());
assert_eq!(pos.pass_rights(Color::Black), 3);
assert_eq!(pos.pass_rights(Color::White), 5);
}
#[test]
fn test_can_pass_requires_enabled() {
let mut pos = Position::new();
pos.set_hirate();
assert!(!pos.is_pass_rights_enabled());
assert!(!pos.can_pass());
pos.set_pass_rights_enabled(true);
assert!(pos.is_pass_rights_enabled());
assert!(!pos.can_pass());
}
#[test]
fn test_can_pass_requires_no_check() {
let sfen = "4k4/4G4/9/9/9/9/9/9/4K4 w - 1";
let mut pos = Position::new();
pos.set_sfen_with_pass_rights(sfen, 2, 2).unwrap();
assert!(pos.in_check(), "White king at 5a should be in check from Gold at 5b");
assert!(!pos.can_pass()); }
#[test]
fn test_do_pass_move_basic() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 2);
let key_before = pos.state().key();
let game_ply_before = pos.game_ply();
pos.do_pass_move();
assert_eq!(pos.side_to_move(), Color::White);
assert_eq!(pos.pass_rights(Color::Black), 1);
assert_eq!(pos.pass_rights(Color::White), 2);
assert_eq!(pos.game_ply(), game_ply_before + 1);
assert_ne!(pos.state().key(), key_before);
}
#[test]
fn test_undo_pass_move_restores_state() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 2);
let key_before = pos.state().key();
let side_before = pos.side_to_move();
let black_rights_before = pos.pass_rights(Color::Black);
let white_rights_before = pos.pass_rights(Color::White);
let game_ply_before = pos.game_ply();
pos.do_pass_move();
pos.undo_pass_move();
assert_eq!(pos.side_to_move(), side_before);
assert_eq!(pos.pass_rights(Color::Black), black_rights_before);
assert_eq!(pos.pass_rights(Color::White), white_rights_before);
assert_eq!(pos.game_ply(), game_ply_before);
assert_eq!(pos.state().key(), key_before);
}
#[test]
fn test_do_move_delegates_pass() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 2);
let key_before = pos.state().key();
pos.do_move(Move::PASS, false);
assert_eq!(pos.side_to_move(), Color::White);
assert_eq!(pos.pass_rights(Color::Black), 1);
assert_ne!(pos.state().key(), key_before);
}
#[test]
fn test_undo_move_delegates_pass() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 2);
let key_before = pos.state().key();
pos.do_move(Move::PASS, false);
pos.undo_move(Move::PASS);
assert_eq!(pos.side_to_move(), Color::Black);
assert_eq!(pos.pass_rights(Color::Black), 2);
assert_eq!(pos.state().key(), key_before);
}
#[test]
fn test_pass_rights_hash_consistency() {
let mut pos_normal = Position::new();
pos_normal.set_hirate();
let mut pos_pass = Position::new();
pos_pass.set_startpos_with_pass_rights(0, 0);
assert_eq!(pos_normal.state().key(), pos_pass.state().key());
pos_pass.set_pass_rights_pair(2, 2);
assert_ne!(pos_normal.state().key(), pos_pass.state().key());
}
#[test]
fn test_set_pass_rights_enabled_normalizes_on_disable() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 3);
assert_eq!(pos.pass_rights(Color::Black), 2);
assert_eq!(pos.pass_rights(Color::White), 3);
pos.set_pass_rights_enabled(false);
assert!(!pos.is_pass_rights_enabled());
assert_eq!(pos.pass_rights(Color::Black), 0);
assert_eq!(pos.pass_rights(Color::White), 0);
}
#[test]
fn test_multiple_passes_decrement_correctly() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(3, 2);
pos.do_pass_move();
assert_eq!(pos.pass_rights(Color::Black), 2);
assert_eq!(pos.side_to_move(), Color::White);
pos.do_pass_move();
assert_eq!(pos.pass_rights(Color::White), 1);
assert_eq!(pos.side_to_move(), Color::Black);
pos.do_pass_move();
assert_eq!(pos.pass_rights(Color::Black), 1);
assert_eq!(pos.side_to_move(), Color::White);
pos.undo_pass_move();
pos.undo_pass_move();
pos.undo_pass_move();
assert_eq!(pos.pass_rights(Color::Black), 3);
assert_eq!(pos.pass_rights(Color::White), 2);
assert_eq!(pos.side_to_move(), Color::Black);
}
#[test]
fn test_pass_checkers_computed_correctly() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 2);
pos.do_pass_move();
assert!(!pos.in_check());
pos.do_pass_move();
assert!(!pos.in_check());
}
#[test]
fn test_pass_while_giving_check() {
let sfen = "4k4/4G4/9/9/9/9/9/9/4K4 b - 1";
let mut pos = Position::new();
pos.set_sfen_with_pass_rights(sfen, 2, 2).unwrap();
assert_eq!(pos.side_to_move(), Color::Black);
assert!(!pos.in_check()); assert!(pos.can_pass());
pos.do_pass_move();
assert_eq!(pos.side_to_move(), Color::White);
assert!(pos.in_check(), "White should be in check after Black's pass");
assert!(!pos.can_pass());
}
#[test]
fn test_set_pass_rights_idempotent() {
let mut pos = Position::new();
pos.set_startpos_with_pass_rights(2, 3);
let key1 = pos.state().key();
pos.set_pass_rights_pair(2, 3);
let key2 = pos.state().key();
assert_eq!(key1, key2, "Setting same pass rights should not change key");
pos.set_pass_rights_pair(5, 5);
let key3 = pos.state().key();
assert_ne!(key1, key3, "Different pass rights should change key");
pos.set_pass_rights_pair(2, 3);
let key4 = pos.state().key();
assert_eq!(key1, key4, "Restoring original pass rights should restore key");
}
fn make_pos(sfen: &str) -> Position {
let mut pos = Position::new();
pos.set_sfen(sfen).unwrap();
pos
}
#[test]
fn test_declaration_win_none_rule() {
let pos = make_pos("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1");
assert_eq!(pos.declaration_win(EnteringKingRule::None), Move::NONE);
}
#[test]
fn test_declaration_win_startpos() {
let pos = make_pos("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1");
assert_eq!(pos.declaration_win(EnteringKingRule::Point27), Move::NONE);
}
#[test]
fn test_declaration_win_27point_success() {
let sfen = "KGG6/SS7/PPPPPP3/9/9/9/2pppppp1/1ss1gg1nl/4k2nl b 2R2B3p 1";
let pos = make_pos(sfen);
let result = pos.declaration_win(EnteringKingRule::Point27);
assert_eq!(result, Move::WIN, "先手28点以上で宣言勝ち");
}
#[test]
fn test_declaration_win_king_not_in_enemy() {
let pos = make_pos("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1");
assert_eq!(pos.declaration_win(EnteringKingRule::Point27), Move::NONE);
}
#[test]
fn test_declaration_win_in_check() {
let sfen = "K7r/GG7/SSPPPPPP1/9/9/9/2pppppp1/1ss1gg1nl/4k2nl b 2B3p 1";
let pos = make_pos(sfen);
assert_eq!(pos.declaration_win(EnteringKingRule::Point27), Move::NONE);
}
#[test]
fn test_declaration_win_insufficient_pieces() {
let sfen = "KGGSS1rnl/P8/9/9/9/pp1pppppp/1ss1gg2l/1r5b1/4k2n1 b BNP 1";
let pos = make_pos(sfen);
assert_eq!(pos.declaration_win(EnteringKingRule::Point27), Move::NONE);
}
#[test]
fn test_declaration_win_insufficient_points() {
let sfen = "KGGSS4/PPPPPP3/PPPP5/9/9/pp1pppppp/1ss1gg1nl/1r5b1/4k2nl b - 1";
let pos = make_pos(sfen);
assert_eq!(pos.declaration_win(EnteringKingRule::Point27), Move::NONE);
}
#[test]
fn test_count_total_piece_points_startpos() {
let pos = make_pos("lnsgkgsnl/1r5b1/ppppppppp/9/9/9/PPPPPPPPP/1B5R1/LNSGKGSNL b - 1");
assert_eq!(pos.count_total_piece_points(), 56);
}
#[test]
fn test_enemy_field() {
let ef_black = Position::enemy_field(Color::Black);
assert!(ef_black.contains(Square::new(File::File5, Rank::Rank1)));
assert!(ef_black.contains(Square::new(File::File5, Rank::Rank3)));
assert!(!ef_black.contains(Square::new(File::File5, Rank::Rank4)));
let ef_white = Position::enemy_field(Color::White);
assert!(ef_white.contains(Square::new(File::File5, Rank::Rank7)));
assert!(ef_white.contains(Square::new(File::File5, Rank::Rank9)));
assert!(!ef_white.contains(Square::new(File::File5, Rank::Rank6)));
}
#[test]
fn test_try_rule_success() {
let pos = make_pos("3K5/9/9/9/9/9/9/9/4k4 b 2r2b4g4s4n4l18p 1");
let result = pos.declaration_win(EnteringKingRule::TryRule);
assert!(result.is_normal(), "トライ成功時は玉の移動手を返す");
assert_eq!(result.to(), Square::new(File::File5, Rank::Rank1));
}
#[test]
fn test_try_rule_not_adjacent() {
let pos = make_pos("2K6/9/9/9/9/9/9/9/4k4 b 2r2b4g4s4n4l18p 1");
assert_eq!(
pos.declaration_win(EnteringKingRule::TryRule),
Move::NONE,
"トライ升に隣接していなければ NONE"
);
}
#[test]
fn test_try_rule_own_piece_on_target() {
let pos = make_pos("3GK4/9/9/9/9/9/9/9/4k4 b 2r2b3g4s4n4l18p 1");
assert_eq!(
pos.declaration_win(EnteringKingRule::TryRule),
Move::NONE,
"トライ升に自駒があれば NONE"
);
}
#[test]
fn test_try_rule_enemy_attacks_target() {
let pos = make_pos("4K4/9/9/9/9/9/9/9/4kr3 b 2b4g4s4n4l18p 1");
assert_eq!(
pos.declaration_win(EnteringKingRule::TryRule),
Move::NONE,
"トライ升に敵の利きがあれば NONE"
);
}
}