use super::{Action, Board, Team};
use crate::state::{
MASK_COL_A, MASK_COL_B, MASK_COL_G, MASK_COL_H, MASK_ROW_1, MASK_ROW_2, MASK_ROW_7, MASK_ROW_8,
MASK_ROW_PROMOTIONS,
};
const LEFT_RAY: [u64; 64] = {
let mut rays = [0u64; 64];
let mut sq = 0;
while sq < 64 {
let col = sq % 8;
let row_start = sq - col;
let mut mask = 0u64;
let mut c = 0;
while c < col {
mask |= 1u64 << (row_start + c);
c += 1;
}
rays[sq] = mask;
sq += 1;
}
rays
};
const RIGHT_RAY: [u64; 64] = {
let mut rays = [0u64; 64];
let mut sq = 0;
while sq < 64 {
let col = sq % 8;
let row_start = sq - col;
let mut mask = 0u64;
let mut c = col + 1;
while c < 8 {
mask |= 1u64 << (row_start + c);
c += 1;
}
rays[sq] = mask;
sq += 1;
}
rays
};
const UP_RAY: [u64; 64] = {
let mut rays = [0u64; 64];
let mut sq = 0;
while sq < 64 {
let col = sq % 8;
let row = sq / 8;
let mut mask = 0u64;
let mut r = row + 1;
while r < 8 {
mask |= 1u64 << (r * 8 + col);
r += 1;
}
rays[sq] = mask;
sq += 1;
}
rays
};
const DOWN_RAY: [u64; 64] = {
let mut rays = [0u64; 64];
let mut sq = 0;
while sq < 64 {
let col = sq % 8;
let row = sq / 8;
let mut mask = 0u64;
let mut r: i32 = row as i32 - 1;
while r >= 0 {
mask |= 1u64 << (r as usize * 8 + col);
r -= 1;
}
rays[sq] = mask;
sq += 1;
}
rays
};
#[allow(clippy::large_const_arrays)]
const RANK_ATTACKS: [[u64; 64]; 64] = {
let mut table = [[0u64; 64]; 64];
let mut sq = 0usize;
while sq < 64 {
let col = sq % 8;
let row_start = sq - col;
let mut occ6 = 0usize;
while occ6 < 64 {
let occ = (occ6 as u64) << 1;
let mut attacks = 0u64;
let mut c = col as i8 - 1;
while c >= 0 {
attacks |= 1u64 << (row_start + c as usize);
if (occ & (1u64 << c)) != 0 {
break;
}
c -= 1;
}
c = col as i8 + 1;
while c < 8 {
attacks |= 1u64 << (row_start + c as usize);
if (occ & (1u64 << c)) != 0 {
break;
}
c += 1;
}
table[sq][occ6] = attacks;
occ6 += 1;
}
sq += 1;
}
table
};
#[allow(clippy::large_const_arrays)]
const FILE_ATTACKS: [[u64; 64]; 64] = {
let mut table = [[0u64; 64]; 64];
let mut sq = 0usize;
while sq < 64 {
let col = sq % 8;
let row = sq / 8;
let mut occ6 = 0usize;
while occ6 < 64 {
let occ = (occ6 as u64) << 1;
let mut attacks = 0u64;
let mut r = row as i8 - 1;
while r >= 0 {
attacks |= 1u64 << (r as usize * 8 + col);
if (occ & (1u64 << r)) != 0 {
break;
}
r -= 1;
}
r = row as i8 + 1;
while r < 8 {
attacks |= 1u64 << (r as usize * 8 + col);
if (occ & (1u64 << r)) != 0 {
break;
}
r += 1;
}
table[sq][occ6] = attacks;
occ6 += 1;
}
sq += 1;
}
table
};
const RANK_OCC_MASK: [u64; 64] = {
let mut masks = [0u64; 64];
let mut sq = 0;
while sq < 64 {
let col = sq % 8;
let row_start = sq - col;
masks[sq] = 0x7Eu64 << row_start;
sq += 1;
}
masks
};
const WHITE_PAWN_MOVES: [u64; 64] = {
let mut moves = [0u64; 64];
let mut src_index = 0;
while src_index < 64 {
let src_mask = 1u64 << src_index;
moves[src_index] = (src_mask & !MASK_COL_A) >> 1u8 | (src_mask & !MASK_COL_H) << 1u8 | (src_mask & !MASK_ROW_8) << 8u8; src_index += 1;
}
moves
};
const BLACK_PAWN_MOVES: [u64; 64] = {
let mut moves = [0u64; 64];
let mut src_index = 0;
while src_index < 64 {
let src_mask = 1u64 << src_index;
moves[src_index] = (src_mask & !MASK_COL_A) >> 1u8 | (src_mask & !MASK_COL_H) << 1u8 | (src_mask & !MASK_ROW_1) >> 8u8; src_index += 1;
}
moves
};
impl Board {
#[must_use]
#[inline]
pub fn actions(&self) -> Vec<Action> {
let mut actions = Vec::with_capacity(32);
self.actions_into(&mut actions);
actions
}
#[inline]
pub fn actions_into(&self, actions: &mut Vec<Action>) {
actions.clear();
if self.turn == Team::White {
self.generate_captures::<0>(actions);
if actions.is_empty() {
self.generate_moves::<0>(actions);
}
} else {
self.generate_captures::<1>(actions);
if actions.is_empty() {
self.generate_moves::<1>(actions);
}
}
}
#[inline]
pub fn count_actions(&self, scratch: &mut Vec<Action>) -> u64 {
if self.turn == Team::White {
let capture_count = self.count_captures::<0>(scratch);
if capture_count > 0 {
capture_count
} else {
self.count_moves::<0>()
}
} else {
let capture_count = self.count_captures::<1>(scratch);
if capture_count > 0 {
capture_count
} else {
self.count_moves::<1>()
}
}
}
fn count_captures<const TEAM_INDEX: usize>(&self, scratch: &mut Vec<Action>) -> u64 {
let may_have_pawn_captures = self.has_any_pawn_captures::<TEAM_INDEX>();
let has_friendly_kings = (self.friendly_pieces() & self.state.kings) != 0;
if !may_have_pawn_captures && !has_friendly_kings {
return 0;
}
scratch.clear();
let mut max_length: u32 = 0;
let mut board = *self;
if has_friendly_kings {
Self::generate_king_captures_with_board::<TEAM_INDEX>(
&mut board,
scratch,
&mut max_length,
);
}
if may_have_pawn_captures {
Self::generate_pawn_captures_with_board::<TEAM_INDEX>(
&mut board,
scratch,
&mut max_length,
);
}
scratch.len() as u64
}
fn count_moves<const TEAM_INDEX: usize>(&self) -> u64 {
let empty = self.state.empty();
self.count_king_moves::<TEAM_INDEX>(empty) + self.count_pawn_moves::<TEAM_INDEX>(empty)
}
#[inline]
fn count_pawn_moves<const TEAM_INDEX: usize>(&self, empty: u64) -> u64 {
let friendly_pawns = self.friendly_pieces() & !self.state.kings;
let mut count = 0u64;
let mut pawns = friendly_pawns;
while pawns != 0 {
let src_mask = pawns & pawns.wrapping_neg();
let src_index = src_mask.trailing_zeros() as usize;
let moves = if TEAM_INDEX == 0 {
WHITE_PAWN_MOVES[src_index] & empty
} else {
BLACK_PAWN_MOVES[src_index] & empty
};
count += moves.count_ones() as u64;
pawns ^= src_mask;
}
count
}
#[inline]
fn count_king_moves<const TEAM_INDEX: usize>(&self, empty: u64) -> u64 {
let occupied = !empty;
let mut friendly_kings = self.friendly_pieces() & self.state.kings;
let mut count = 0u64;
while friendly_kings != 0 {
let src_mask = friendly_kings & friendly_kings.wrapping_neg();
let sq = src_mask.trailing_zeros() as usize;
let attacks = Self::king_attacks_lut(sq, occupied);
count += (attacks & empty).count_ones() as u64;
friendly_kings ^= src_mask;
}
count
}
#[inline(always)]
fn king_attacks_lut(sq: usize, occupied: u64) -> u64 {
let rank_occ = (occupied & RANK_OCC_MASK[sq]) >> (sq - sq % 8 + 1);
let rank_occ6 = rank_occ as usize & 0x3F;
let col = sq % 8;
let file_bits = (occupied >> col) & 0x0101_0101_0101_0101u64;
let file_occ6 = ((file_bits.wrapping_mul(0x0002_0408_1020_4080u64)) >> 57) as usize & 0x3F;
RANK_ATTACKS[sq][rank_occ6] | FILE_ATTACKS[sq][file_occ6]
}
const fn has_any_pawn_captures<const TEAM_INDEX: usize>(&self) -> bool {
let friendly_pawns = self.friendly_pieces() & !self.state.kings;
if friendly_pawns == 0 {
return false;
}
let hostile = self.hostile_pieces();
let empty = self.state.empty();
{
let eligible = friendly_pawns & !(MASK_COL_A | MASK_COL_B);
let adjacent_hostile = (eligible >> 1) & hostile; let landing_clear = (eligible >> 2) & empty; if (landing_clear << 1) & adjacent_hostile != 0 {
return true;
}
}
{
let eligible = friendly_pawns & !(MASK_COL_G | MASK_COL_H);
let adjacent_hostile = (eligible << 1) & hostile; let landing_clear = (eligible << 2) & empty; if (landing_clear >> 1) & adjacent_hostile != 0 {
return true;
}
}
if TEAM_INDEX == 0 {
let eligible = friendly_pawns & !(MASK_ROW_7 | MASK_ROW_8);
let adjacent_hostile = (eligible << 8) & hostile;
let landing_clear = (eligible << 16) & empty;
if (landing_clear >> 8) & adjacent_hostile != 0 {
return true;
}
} else {
let eligible = friendly_pawns & !(MASK_ROW_1 | MASK_ROW_2);
let adjacent_hostile = (eligible >> 8) & hostile;
let landing_clear = (eligible >> 16) & empty;
if (landing_clear << 8) & adjacent_hostile != 0 {
return true;
}
}
false
}
#[inline]
fn generate_captures<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>) {
let may_have_pawn_captures = self.has_any_pawn_captures::<TEAM_INDEX>();
let has_friendly_kings = (self.friendly_pieces() & self.state.kings) != 0;
if !may_have_pawn_captures && !has_friendly_kings {
return;
}
let mut max_length: u32 = 0;
let mut board = *self;
if has_friendly_kings {
Self::generate_king_captures_with_board::<TEAM_INDEX>(
&mut board,
actions,
&mut max_length,
);
}
if may_have_pawn_captures {
Self::generate_pawn_captures_with_board::<TEAM_INDEX>(
&mut board,
actions,
&mut max_length,
);
}
}
#[inline(always)]
fn push_capture_action<const TEAM_INDEX: usize>(
actions: &mut Vec<Action>,
max_length: &mut u32,
action: Action,
) {
let length = action.delta.pieces[1 - TEAM_INDEX].count_ones();
if length > *max_length {
actions.clear();
*max_length = length;
actions.push(action);
} else if length == *max_length {
actions.push(action);
}
}
#[inline]
fn generate_pawn_captures_with_board<const TEAM_INDEX: usize>(
board: &mut Self,
actions: &mut Vec<Action>,
max_length: &mut u32,
) {
let mut friendly_pawns = board.friendly_pieces() & !board.state.kings;
while friendly_pawns != 0u64 {
let src_mask = friendly_pawns & friendly_pawns.wrapping_neg();
Self::generate_pawn_captures_at::<TEAM_INDEX, 0>(
board,
actions,
max_length,
src_mask,
Action::EMPTY,
);
friendly_pawns ^= src_mask; }
}
#[inline]
fn generate_pawn_captures_at<const TEAM_INDEX: usize, const PREVIOUS_DIRECTION: i8>(
board: &mut Self,
actions: &mut Vec<Action>,
max_length: &mut u32,
src_mask: u64,
previous_action: Action,
) {
let mut has_more_captures = false;
let hostile_pieces = board.hostile_pieces();
let empty = board.state.empty();
if PREVIOUS_DIRECTION != 1 {
let left_capture_mask =
((src_mask & !(MASK_COL_A | MASK_COL_B)) >> 1u8) & hostile_pieces;
let left_dest_mask = ((src_mask & !(MASK_COL_A | MASK_COL_B)) >> 2u8) & empty;
if left_capture_mask != 0u64 && left_dest_mask != 0u64 {
has_more_captures = true;
let capture_action = Action::new_capture_as_pawn::<TEAM_INDEX>(
src_mask,
left_dest_mask,
left_capture_mask,
board.state.kings,
);
board.apply_(&capture_action);
Self::generate_pawn_captures_at::<TEAM_INDEX, -1>(
board,
actions,
max_length,
left_dest_mask,
previous_action.combine(&capture_action),
);
board.apply_(&capture_action);
}
}
if PREVIOUS_DIRECTION != -1 {
let right_capture_mask =
((src_mask & !(MASK_COL_G | MASK_COL_H)) << 1u8) & hostile_pieces;
let right_dest_mask = ((src_mask & !(MASK_COL_G | MASK_COL_H)) << 2u8) & empty;
if right_capture_mask != 0u64 && right_dest_mask != 0u64 {
has_more_captures = true;
let capture_action = Action::new_capture_as_pawn::<TEAM_INDEX>(
src_mask,
right_dest_mask,
right_capture_mask,
board.state.kings,
);
board.apply_(&capture_action);
Self::generate_pawn_captures_at::<TEAM_INDEX, 1>(
board,
actions,
max_length,
right_dest_mask,
previous_action.combine(&capture_action),
);
board.apply_(&capture_action);
}
}
let (vert_capture_mask, vert_dest_mask): (u64, u64) = if TEAM_INDEX == 0 {
let mask = !(MASK_ROW_7 | MASK_ROW_8);
(
((src_mask & mask) << 8u8) & hostile_pieces,
((src_mask & mask) << 16u8) & empty,
)
} else {
let mask = !(MASK_ROW_1 | MASK_ROW_2);
(
((src_mask & mask) >> 8u8) & hostile_pieces,
((src_mask & mask) >> 16u8) & empty,
)
};
if vert_capture_mask != 0u64 && vert_dest_mask != 0u64 {
has_more_captures = true;
let capture_action = Action::new_capture_as_pawn::<TEAM_INDEX>(
src_mask,
vert_dest_mask,
vert_capture_mask,
board.state.kings,
);
board.apply_(&capture_action);
if TEAM_INDEX == 0 {
Self::generate_pawn_captures_at::<TEAM_INDEX, 8>(
board,
actions,
max_length,
vert_dest_mask,
previous_action.combine(&capture_action),
);
} else {
Self::generate_pawn_captures_at::<TEAM_INDEX, -8>(
board,
actions,
max_length,
vert_dest_mask,
previous_action.combine(&capture_action),
);
}
board.apply_(&capture_action);
}
if !has_more_captures && !previous_action.is_empty() {
let mut final_action = previous_action;
let promotion_mask = MASK_ROW_PROMOTIONS[TEAM_INDEX];
if src_mask & promotion_mask != 0 {
final_action.delta.kings ^= src_mask;
}
Self::push_capture_action::<TEAM_INDEX>(actions, max_length, final_action);
}
}
#[inline]
fn generate_king_captures_with_board<const TEAM_INDEX: usize>(
board: &mut Self,
actions: &mut Vec<Action>,
max_length: &mut u32,
) {
let mut friendly_kings = board.friendly_pieces() & board.state.kings;
while friendly_kings != 0u64 {
let src_mask = friendly_kings & friendly_kings.wrapping_neg();
Self::generate_king_captures_at::<TEAM_INDEX, 0i8>(
board,
actions,
max_length,
src_mask,
Action::EMPTY,
);
friendly_kings ^= src_mask; }
}
#[inline]
fn generate_king_captures_at<const TEAM_INDEX: usize, const PREVIOUS_DIRECTION: i8>(
board: &mut Self,
actions: &mut Vec<Action>,
max_length: &mut u32,
src_mask: u64,
previous_action: Action,
) {
let mut has_more_captures = false;
let friendly_pieces = board.friendly_pieces();
let hostile_pieces = board.hostile_pieces();
let src_index = src_mask.trailing_zeros() as usize;
if PREVIOUS_DIRECTION != 1 && (hostile_pieces & LEFT_RAY[src_index]) != 0 {
board
.gen_inner::<-1i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_COL_A | MASK_COL_B) }>(
src_mask,
src_index as u8,
friendly_pieces,
hostile_pieces,
&mut has_more_captures,
actions,
max_length,
previous_action,
);
}
if PREVIOUS_DIRECTION != -1 && (hostile_pieces & RIGHT_RAY[src_index]) != 0 {
board.gen_inner::<1i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_COL_G | MASK_COL_H) }>(
src_mask,
src_index as u8,
friendly_pieces,
hostile_pieces,
&mut has_more_captures,
actions,
max_length,
previous_action,
);
}
if PREVIOUS_DIRECTION != -8 && (hostile_pieces & UP_RAY[src_index]) != 0 {
board.gen_inner::<8i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_ROW_7 | MASK_ROW_8) }>(
src_mask,
src_index as u8,
friendly_pieces,
hostile_pieces,
&mut has_more_captures,
actions,
max_length,
previous_action,
);
}
if PREVIOUS_DIRECTION != 8 && (hostile_pieces & DOWN_RAY[src_index]) != 0 {
board
.gen_inner::<-8i8, PREVIOUS_DIRECTION, TEAM_INDEX, { !(MASK_ROW_1 | MASK_ROW_2) }>(
src_mask,
src_index as u8,
friendly_pieces,
hostile_pieces,
&mut has_more_captures,
actions,
max_length,
previous_action,
);
}
if !has_more_captures && !previous_action.is_empty() {
Self::push_capture_action::<TEAM_INDEX>(actions, max_length, previous_action);
}
}
#[allow(clippy::too_many_arguments)] #[inline(always)]
fn gen_inner<
const DIRECTION: i8,
const PREVIOUS_DIRECTION: i8,
const TEAM_INDEX: usize,
const CHECKMASK: u64,
>(
&mut self,
src_mask: u64,
src_index: u8,
friendly_pieces: u64,
hostile_pieces: u64,
has_more_captures: &mut bool,
actions: &mut Vec<Action>,
max_length: &mut u32,
previous_action: Action,
) {
if (CHECKMASK >> src_index) & 1u64 != 1u64 {
return;
}
let mut temp_index = src_index as i8 + DIRECTION;
const NO_CAPTURE: u8 = 255; let mut capture_index: u8 = NO_CAPTURE;
let mut possible_move_indices: [u8; 7] = [0; 7]; let mut possible_move_count: usize = 0;
#[allow(clippy::manual_range_contains)]
while temp_index >= 0 && temp_index <= 63 {
let temp_index_mask = 1u64 << temp_index;
if friendly_pieces & temp_index_mask != 0 {
break;
}
if hostile_pieces & temp_index_mask != 0 {
if capture_index != NO_CAPTURE {
break;
}
capture_index = temp_index as u8;
} else if capture_index != NO_CAPTURE {
#[allow(clippy::cast_sign_loss)] {
possible_move_indices[possible_move_count] = temp_index as u8;
}
possible_move_count += 1;
}
if DIRECTION == 1 && temp_index % 8 == 7 {
break; }
if DIRECTION == -1 && temp_index % 8 == 0 {
break; }
temp_index += DIRECTION;
}
if possible_move_count == 0 {
return;
}
let capture_index_mask = 1u64 << capture_index;
for &possible_move_index in &possible_move_indices[..possible_move_count] {
let dest_mask = 1u64 << possible_move_index;
*has_more_captures = true;
let capture_action = Action::new_capture_as_king::<TEAM_INDEX>(
src_mask,
dest_mask,
capture_index_mask,
self.state.kings,
);
self.apply_(&capture_action);
Self::generate_king_captures_at::<TEAM_INDEX, DIRECTION>(
self,
actions,
max_length,
dest_mask,
previous_action.combine(&capture_action),
);
self.apply_(&capture_action);
}
}
#[inline]
fn generate_moves<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>) {
let empty = self.state.empty();
self.generate_king_moves::<TEAM_INDEX>(actions, empty);
self.generate_pawn_moves::<TEAM_INDEX>(actions, empty);
}
#[inline]
fn generate_pawn_moves<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>, empty: u64) {
let mut friendly_pawns = self.friendly_pieces() & !self.state.kings;
while friendly_pawns != 0u64 {
let src_mask = friendly_pawns & friendly_pawns.wrapping_neg(); let src_index: usize = src_mask.trailing_zeros() as usize;
let mut possible_dest_masks = if TEAM_INDEX == 0 {
WHITE_PAWN_MOVES[src_index] & empty
} else {
BLACK_PAWN_MOVES[src_index] & empty
};
while possible_dest_masks != 0u64 {
let dest_mask = possible_dest_masks & possible_dest_masks.wrapping_neg(); actions.push(Action::new_move_as_pawn::<TEAM_INDEX>(src_mask, dest_mask));
possible_dest_masks ^= dest_mask; }
friendly_pawns ^= src_mask; }
}
#[inline]
fn generate_king_moves<const TEAM_INDEX: usize>(&self, actions: &mut Vec<Action>, empty: u64) {
let occupied = !empty;
let mut friendly_kings = self.friendly_pieces() & self.state.kings;
while friendly_kings != 0u64 {
let src_mask = friendly_kings & friendly_kings.wrapping_neg();
let sq = src_mask.trailing_zeros() as usize;
let attacks = Self::king_attacks_lut(sq, occupied);
let mut moves = attacks & empty;
while moves != 0u64 {
let dest_mask = moves & moves.wrapping_neg();
actions.push(Action::new_move_as_king::<TEAM_INDEX>(src_mask, dest_mask));
moves ^= dest_mask;
}
friendly_kings ^= src_mask;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::game_status::GameStatus;
use crate::Square;
#[test]
fn initial_position_action_count() {
let board = Board::new_default();
let actions = board.actions();
assert!(!actions.is_empty(), "Initial position should have moves");
assert!(
!actions.is_empty(),
"Should have moves from initial position"
);
}
#[test]
fn initial_position_only_moves_no_captures() {
let board = Board::new_default();
let actions = board.actions();
for action in &actions {
assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
0,
"Initial position should only have moves, not captures"
);
}
}
#[test]
fn forced_capture_rule() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::D5, Square::H8], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have exactly one capture");
assert_ne!(
actions[0].delta.pieces[Team::Black.to_usize()],
0,
"Action should be a capture"
);
}
#[test]
fn multiple_capture_options() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::C4, Square::E4, Square::D5],
&[],
);
let actions = board.actions();
assert_eq!(actions.len(), 3, "Should have three capture options");
for action in &actions {
assert_ne!(
action.delta.pieces[Team::Black.to_usize()],
0,
"All actions should be captures"
);
}
}
#[test]
fn maximum_capture_rule_prefers_longer_chain() {
let board = Board::from_squares(
Team::White,
&[Square::A4],
&[Square::B4, Square::A5, Square::B6],
&[],
);
let actions = board.actions();
for action in &actions {
let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
assert_eq!(
capture_count, 2,
"Should only return maximum length captures (2)"
);
}
}
#[test]
fn maximum_capture_multiple_equal_length() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::D5, Square::E4], &[]);
let actions = board.actions();
assert_eq!(
actions.len(),
2,
"Should have two equal-length capture options"
);
}
#[test]
fn king_can_slide_multiple_squares() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[],
&[Square::D4], );
let actions = board.actions();
assert_eq!(actions.len(), 14, "King at D4 should have 14 moves");
}
#[test]
fn king_blocked_by_friendly_piece() {
let board = Board::from_squares(
Team::White,
&[Square::D4, Square::D6],
&[],
&[Square::D4], );
let actions = board.actions();
let king_up_moves: Vec<_> = actions
.iter()
.filter(|a| {
let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
dest == Square::D5.to_mask()
})
.collect();
assert_eq!(
king_up_moves.len(),
1,
"King should only be able to move to D5 going up"
);
}
#[test]
fn king_can_capture_from_distance() {
let board = Board::from_squares(
Team::White,
&[Square::A4],
&[Square::D4],
&[Square::A4], );
let actions = board.actions();
assert_eq!(
actions.len(),
4,
"King should have 4 landing options after capture"
);
for action in &actions {
assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::D4.to_mask(),
"Should capture D4"
);
}
}
#[test]
fn king_multi_direction_capture() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::B4, Square::D6],
&[Square::D4],
);
let actions = board.actions();
assert!(
actions.len() >= 2,
"King should be able to capture in multiple directions"
);
}
#[test]
fn white_pawn_moves_forward_and_sideways() {
let board = Board::from_squares(Team::White, &[Square::D4], &[], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 3, "Pawn at D4 should have 3 moves");
}
#[test]
fn black_pawn_moves_forward_and_sideways() {
let board = Board::from_squares(Team::Black, &[], &[Square::D5], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 3, "Black pawn at D5 should have 3 moves");
}
#[test]
fn pawn_at_edge_has_fewer_moves() {
let board = Board::from_squares(Team::White, &[Square::A4], &[], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 2, "Pawn at A4 should have 2 moves");
}
#[test]
fn pawn_promotes_on_last_rank() {
let board = Board::from_squares(Team::White, &[Square::D7], &[], &[]);
let actions = board.actions();
let promotion_action = actions
.iter()
.find(|a| a.delta.pieces[Team::White.to_usize()] & Square::D8.to_mask() != 0);
assert!(promotion_action.is_some(), "Should be able to move to D8");
let action = promotion_action.unwrap();
assert_ne!(
action.delta.kings & Square::D8.to_mask(),
0,
"Pawn should promote to king at D8"
);
}
#[test]
fn pawn_promotes_during_capture() {
let board = Board::from_squares(Team::White, &[Square::B6], &[Square::B7], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have one capture");
let action = &actions[0];
assert_ne!(
action.delta.kings & Square::B8.to_mask(),
0,
"Pawn should promote after capture landing on B8"
);
}
#[test]
fn no_pieces_no_actions() {
let board = Board::from_squares(Team::White, &[], &[Square::D4], &[]);
let actions = board.actions();
assert!(actions.is_empty(), "No friendly pieces means no actions");
}
#[test]
fn completely_blocked_pawn_can_capture() {
let board = Board::from_squares(
Team::White,
&[Square::B2],
&[Square::A2, Square::B3, Square::C2],
&[],
);
let actions = board.actions();
assert!(
!actions.is_empty(),
"Surrounded pawn can capture adjacent enemies"
);
for action in &actions {
assert_ne!(
action.delta.pieces[Team::Black.to_usize()],
0,
"Actions should be captures"
);
}
}
#[test]
fn truly_blocked_pawn() {
let board = Board::from_squares(
Team::White,
&[Square::B2, Square::A2, Square::B3, Square::C2],
&[Square::H8],
&[],
);
let actions = board.actions();
let b2_moves: Vec<_> = actions
.iter()
.filter(|a| {
let delta = a.delta.pieces[Team::White.to_usize()];
delta & Square::B2.to_mask() != 0
})
.collect();
assert!(b2_moves.is_empty(), "B2 pawn should have no moves");
}
#[test]
fn pawn_chain_capture_two_pieces() {
let board = Board::from_squares(Team::White, &[Square::A4], &[Square::A5, Square::B6], &[]);
let actions = board.actions();
assert!(!actions.is_empty(), "Should have capture chain");
for action in &actions {
let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
assert_eq!(capture_count, 2, "Should capture 2 pieces in chain");
}
}
#[test]
fn pawn_chain_capture_three_pieces() {
let board = Board::from_squares(
Team::White,
&[Square::A2],
&[Square::A3, Square::B4, Square::C5],
&[],
);
let actions = board.actions();
assert!(!actions.is_empty(), "Should have capture chain");
for action in &actions {
let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
assert_eq!(capture_count, 3, "Should capture 3 pieces in chain");
}
}
#[test]
fn king_chain_capture() {
let board = Board::from_squares(
Team::White,
&[Square::A4],
&[Square::C4, Square::E6],
&[Square::A4],
);
let actions = board.actions();
assert!(!actions.is_empty(), "King should have capture options");
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap();
assert_eq!(max_captures, 2, "King should capture 2 pieces in chain");
}
#[test]
fn king_cannot_pass_through_friendly() {
let board = Board::from_squares(
Team::White,
&[Square::A4, Square::C4],
&[Square::H8],
&[Square::A4],
);
let actions = board.actions();
let right_moves: Vec<_> = actions
.iter()
.filter(|a| {
let src = Square::A4.to_mask();
let delta = a.delta.pieces[Team::White.to_usize()];
delta & src != 0 && {
let dest = delta & !src;
let dest_sq = unsafe { Square::from_mask(dest) };
dest_sq.row() == 3 && dest_sq.column() > 0
}
})
.collect();
assert_eq!(
right_moves.len(),
1,
"King should only reach B4 going right"
);
}
#[test]
fn king_cannot_jump_over_hostile_without_capturing() {
let board = Board::from_squares(Team::White, &[Square::A4], &[Square::C4], &[Square::A4]);
let actions = board.actions();
for action in &actions {
let dest = action.delta.pieces[Team::White.to_usize()] & !Square::A4.to_mask();
if dest != 0 {
let dest_sq = unsafe { Square::from_mask(dest) };
if dest_sq.row() == 3 && dest_sq.column() >= 3 {
assert_ne!(
action.delta.pieces[Team::Black.to_usize()],
0,
"Moving past hostile must be a capture"
);
}
}
}
}
#[test]
fn black_pawn_captures() {
let board = Board::from_squares(Team::Black, &[Square::A1], &[Square::D5], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 3, "Black pawn at D5 should have 3 moves");
}
#[test]
fn black_king_movement() {
let board = Board::from_squares(Team::Black, &[Square::A1], &[Square::D4], &[Square::D4]);
let actions = board.actions();
assert_eq!(actions.len(), 14, "Black king at D4 should have 14 moves");
}
#[test]
fn multiple_pieces_all_can_move() {
let board = Board::from_squares(Team::White, &[Square::A4, Square::H4], &[Square::D8], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 4, "Both pawns should contribute moves");
}
#[test]
fn multiple_pieces_one_must_capture() {
let board = Board::from_squares(Team::White, &[Square::A4, Square::H4], &[Square::A5], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Only capture should be returned");
assert_ne!(
actions[0].delta.pieces[Team::Black.to_usize()],
0,
"Action should be a capture"
);
}
#[test]
fn black_pawn_promotes_on_row_1() {
let board = Board::from_squares(Team::Black, &[Square::H8], &[Square::D2], &[]);
let actions = board.actions();
let promotion = actions
.iter()
.find(|a| a.delta.pieces[Team::Black.to_usize()] & Square::D1.to_mask() != 0);
assert!(promotion.is_some(), "Should be able to move to D1");
assert_ne!(
promotion.unwrap().delta.kings & Square::D1.to_mask(),
0,
"Black pawn should promote at D1"
);
}
#[test]
fn pawn_capture_leads_to_promotion() {
let board = Board::from_squares(Team::White, &[Square::B6], &[Square::B7], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have one capture");
let action = &actions[0];
assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B7.to_mask(),
"Should capture B7"
);
assert_ne!(
action.delta.kings & Square::B8.to_mask(),
0,
"Pawn should promote at B8"
);
}
#[test]
fn pawn_in_corner() {
let board = Board::from_squares(
Team::Black,
&[Square::A8],
&[Square::H1],
&[Square::H1], );
let actions = board.actions();
assert_eq!(actions.len(), 14, "King at H1 should have 14 moves");
}
#[test]
fn king_cannot_reverse_up_down_during_capture() {
let board = Board::from_squares(
Team::White,
&[Square::D5],
&[Square::D3, Square::D7],
&[Square::D5], );
let actions = board.actions();
for action in &actions {
let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
assert_eq!(
capture_count, 1,
"King should NOT chain captures that require 180° turn (up then down)"
);
}
}
#[test]
fn king_cannot_reverse_left_right_during_capture() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::B4, Square::F4],
&[Square::D4], );
let actions = board.actions();
for action in &actions {
let capture_count = action.delta.pieces[Team::Black.to_usize()].count_ones();
assert_eq!(
capture_count, 1,
"King should NOT chain captures that require 180° turn (left then right)"
);
}
}
#[test]
fn king_can_turn_90_degrees_during_capture() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::D6, Square::F7],
&[Square::D4],
);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(
max_captures, 2,
"King should be able to chain captures with 90° turn"
);
}
#[test]
fn king_180_restriction_complex_scenario() {
let board = Board::from_squares(
Team::White,
&[Square::A1],
&[Square::A3, Square::C5, Square::E3],
&[Square::A1],
);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(
max_captures, 3,
"King should chain 3 captures with 90° turns"
);
}
#[test]
fn king_180_turn_prohibited_vertical() {
let board = Board::from_squares(
Team::Black,
&[Square::D2, Square::D6],
&[Square::D4],
&[Square::D4, Square::D6],
);
let actions = board.actions();
assert!(!actions.is_empty());
for action in &actions {
assert_eq!(
action.capture_count(Team::Black),
1,
"180° turn should prevent chaining North->South or South->North captures"
);
}
}
#[test]
fn king_180_turn_prohibited_horizontal() {
let board = Board::from_squares(
Team::Black,
&[Square::B4, Square::F4],
&[Square::D4],
&[Square::D4, Square::F4],
);
let actions = board.actions();
assert!(!actions.is_empty());
for action in &actions {
assert_eq!(
action.capture_count(Team::Black),
1,
"180° turn should prevent chaining East->West or West->East captures"
);
}
}
#[test]
fn can_cross_captured_square_pawn() {
let board = Board::from_squares(Team::White, &[Square::A2], &[Square::A3, Square::B4], &[]);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(max_captures, 2, "Pawn should chain 2 captures");
}
#[test]
fn king_can_cross_captured_square() {
let board = Board::from_squares(
Team::White,
&[Square::A4],
&[Square::C4, Square::E4],
&[Square::A4],
);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(
max_captures, 2,
"King should be able to chain captures (immediate removal allows path)"
);
}
#[test]
fn king_complex_crossing_pattern() {
let board = Board::from_squares(
Team::White,
&[Square::A4],
&[Square::C4, Square::E4, Square::G4],
&[Square::A4],
);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(max_captures, 3, "King should chain 3 captures in a line");
}
#[test]
fn pawn_promotes_at_end_of_capture_sequence() {
let board = Board::from_squares(Team::White, &[Square::D6], &[Square::D7], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have one capture");
let action = &actions[0];
let dest = action.delta.pieces[Team::White.to_usize()] & !Square::D6.to_mask();
assert_eq!(dest, Square::D8.to_mask(), "Should land on D8");
assert_ne!(
action.delta.kings & Square::D8.to_mask(),
0,
"Should promote to king at D8"
);
}
#[test]
fn pawn_captures_and_promotes_when_ending_on_back_row() {
let board = Board::from_squares(Team::White, &[Square::D6], &[Square::D7], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have one capture");
let action = &actions[0];
let dest = action.delta.pieces[Team::White.to_usize()] & !Square::D6.to_mask();
assert_eq!(dest, Square::D8.to_mask(), "Should land on D8");
assert_ne!(
action.delta.kings & Square::D8.to_mask(),
0,
"Should promote to king at D8"
);
}
#[test]
fn pawn_continues_capturing_from_promotion_row() {
let board = Board::from_squares(Team::White, &[Square::D6], &[Square::D7, Square::C8], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have one 2-capture action");
let action = &actions[0];
let captures = action.delta.pieces[Team::Black.to_usize()];
assert_eq!(
captures,
Square::D7.to_mask() | Square::C8.to_mask(),
"Should capture both D7 and C8"
);
let final_pos = action.delta.pieces[Team::White.to_usize()] & !Square::D6.to_mask();
assert_eq!(final_pos, Square::B8.to_mask(), "Should end on B8");
assert_ne!(
action.delta.kings & Square::B8.to_mask(),
0,
"Should promote to king at B8"
);
}
#[test]
fn pawn_chain_capture_ending_on_promotion_row() {
let board = Board::from_squares(Team::White, &[Square::A4], &[Square::A5, Square::B6], &[]);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(max_captures, 2, "Should capture both in chain");
}
#[test]
fn max_capture_three_vs_two() {
let board = Board::from_squares(
Team::White,
&[Square::A2],
&[Square::A3, Square::B4, Square::C5],
&[],
);
let actions = board.actions();
for action in &actions {
let count = action.delta.pieces[Team::Black.to_usize()].count_ones();
assert_eq!(count, 3, "Only 3-capture sequences should be returned");
}
}
#[test]
fn max_capture_equal_length_all_returned() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::D5, Square::C4, Square::E4],
&[],
);
let actions = board.actions();
assert_eq!(
actions.len(),
3,
"Should have 3 equal-length capture options"
);
for action in &actions {
let count = action.delta.pieces[Team::Black.to_usize()].count_ones();
assert_eq!(count, 1, "All captures should be length 1");
}
}
#[test]
fn max_capture_king_vs_pawn_count_equally() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::D5, Square::C4, Square::B5],
&[Square::D5], );
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(
max_captures, 2,
"Should prefer 2-pawn capture over 1-king capture"
);
}
#[test]
fn white_pawn_cannot_move_backward() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[]);
let actions = board.actions();
let backward_move = actions.iter().find(|a| {
let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
dest == Square::D3.to_mask()
});
assert!(
backward_move.is_none(),
"White pawn should not move backward to D3"
);
}
#[test]
fn black_pawn_cannot_move_backward() {
let board = Board::from_squares(Team::Black, &[Square::H1], &[Square::D5], &[]);
let actions = board.actions();
let backward_move = actions.iter().find(|a| {
let dest = a.delta.pieces[Team::Black.to_usize()] & !Square::D5.to_mask();
dest == Square::D6.to_mask()
});
assert!(
backward_move.is_none(),
"Black pawn should not move backward to D6"
);
}
#[test]
fn pawn_cannot_move_diagonally() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[]);
let actions = board.actions();
let diagonal_squares = [Square::C3, Square::C5, Square::E3, Square::E5];
for sq in &diagonal_squares {
let diag_move = actions.iter().find(|a| {
let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
dest == sq.to_mask()
});
assert!(
diag_move.is_none(),
"Pawn should not move diagonally to {sq:?}"
);
}
}
#[test]
fn pawn_moves_exactly_one_square() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[]);
let actions = board.actions();
let valid_dests = [Square::C4, Square::E4, Square::D5];
let invalid_dests = [Square::B4, Square::F4, Square::D6];
for sq in &valid_dests {
let found = actions.iter().any(|a| {
let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
dest == sq.to_mask()
});
assert!(found, "Pawn should be able to move to {sq:?}");
}
for sq in &invalid_dests {
let found = actions.iter().any(|a| {
let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
dest == sq.to_mask()
});
assert!(!found, "Pawn should NOT move 2 squares to {sq:?}");
}
}
#[test]
fn king_moves_multiple_squares_all_directions() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[Square::D4]);
let actions = board.actions();
assert_eq!(actions.len(), 14, "King at D4 should have 14 moves");
}
#[test]
fn king_cannot_move_diagonally() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::H8], &[Square::D4]);
let actions = board.actions();
let diagonal_squares = [
Square::A1,
Square::B2,
Square::C3,
Square::E5,
Square::F6,
Square::G7,
Square::A7,
Square::B6,
Square::C5,
Square::E3,
Square::F2,
Square::G1,
];
for sq in &diagonal_squares {
let diag_move = actions.iter().find(|a| {
let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
dest == sq.to_mask()
});
assert!(
diag_move.is_none(),
"King should not move diagonally to {sq:?}"
);
}
}
#[test]
fn white_pawn_cannot_capture_backward() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::D3, Square::D5], &[],
);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have only 1 capture");
assert_eq!(
actions[0].delta.pieces[Team::Black.to_usize()],
Square::D5.to_mask(),
"Should capture D5 (forward), not D3 (backward)"
);
}
#[test]
fn black_pawn_cannot_capture_backward() {
let board = Board::from_squares(
Team::Black,
&[Square::D6, Square::D4], &[Square::D5],
&[],
);
let actions = board.actions();
assert_eq!(actions.len(), 1, "Should have only 1 capture");
assert_eq!(
actions[0].delta.pieces[Team::White.to_usize()],
Square::D4.to_mask(),
"Should capture D4 (forward for black), not D6 (backward)"
);
}
#[test]
fn pawn_cannot_capture_diagonally() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::C3, Square::C5, Square::E3, Square::E5],
&[],
);
let actions = board.actions();
for action in &actions {
assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
0,
"Should not capture diagonal enemies"
);
}
}
#[test]
fn king_capture_from_distance() {
let board = Board::from_squares(Team::White, &[Square::A4], &[Square::E4], &[Square::A4]);
let actions = board.actions();
assert_eq!(actions.len(), 3, "King should have 3 landing options");
for action in &actions {
assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::E4.to_mask(),
"All captures should take E4"
);
}
}
#[test]
fn king_cannot_capture_two_in_line() {
let board = Board::from_squares(
Team::White,
&[Square::A4],
&[Square::C4, Square::E4],
&[Square::A4],
);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(
max_captures, 2,
"Should capture both in chain, not single jump"
);
}
#[test]
fn must_capture_when_available() {
let board = Board::from_squares(Team::White, &[Square::D4], &[Square::D5], &[]);
let actions = board.actions();
for action in &actions {
assert_ne!(
action.delta.pieces[Team::Black.to_usize()],
0,
"Must capture when capture is available"
);
}
}
#[test]
fn moves_allowed_when_no_capture() {
let board = Board::from_squares(
Team::White,
&[Square::D4],
&[Square::H8], &[],
);
let actions = board.actions();
for action in &actions {
assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
0,
"Should be moves, not captures"
);
}
assert!(!actions.is_empty(), "Should have moves available");
}
#[test]
fn one_piece_each_is_draw() {
let board = Board::from_squares(Team::White, &[Square::A1], &[Square::H8], &[]);
assert_eq!(
board.status(),
GameStatus::Draw,
"One piece each should be draw"
);
}
#[test]
fn one_king_each_is_draw() {
let board = Board::from_squares(
Team::White,
&[Square::A1],
&[Square::H8],
&[Square::A1, Square::H8],
);
assert_eq!(
board.status(),
GameStatus::Draw,
"One king each should be draw"
);
}
#[test]
fn king_vs_pawn_is_draw() {
let board = Board::from_squares(
Team::White,
&[Square::A1],
&[Square::H8],
&[Square::A1], );
assert_eq!(
board.status(),
GameStatus::Draw,
"King vs pawn (1v1) is draw in current implementation"
);
}
#[test]
fn no_pieces_means_loss() {
let board = Board::from_squares(Team::White, &[], &[Square::D4], &[]);
assert_eq!(
board.status(),
GameStatus::Won(Team::Black),
"No white pieces means black wins"
);
}
#[test]
fn blocked_means_loss() {
let board = Board::from_squares(
Team::White,
&[Square::A2, Square::A3],
&[
Square::A4, Square::A5, Square::B2, Square::B3, Square::C2, Square::C3, ],
&[],
);
assert_eq!(
board.status(),
GameStatus::Won(Team::Black),
"Completely blocked white should lose"
);
}
#[test]
fn pawn_four_capture_chain() {
let board = Board::from_squares(
Team::White,
&[Square::A2],
&[Square::A3, Square::B4, Square::C5, Square::D6],
&[],
);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert_eq!(max_captures, 4, "Should capture 4 pieces in chain");
}
#[test]
fn king_five_capture_chain() {
let board = Board::from_squares(
Team::White,
&[Square::A2],
&[Square::A4, Square::C5, Square::D3, Square::F2],
&[Square::A2],
);
let actions = board.actions();
let max_captures = actions
.iter()
.map(|a| a.delta.pieces[Team::Black.to_usize()].count_ones())
.max()
.unwrap_or(0);
assert!(max_captures >= 3, "King should capture at least 3 in chain");
}
#[test]
fn pawn_on_edge_limited_moves() {
let board = Board::from_squares(Team::White, &[Square::A4], &[Square::H8], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 2, "Edge pawn should have 2 moves");
}
#[test]
fn pawn_in_corner_very_limited() {
let board = Board::from_squares(Team::White, &[Square::H3], &[Square::A8], &[]);
let actions = board.actions();
assert_eq!(actions.len(), 2, "Corner-area pawn should have 2 moves");
}
#[test]
fn king_in_corner_moves() {
let board = Board::from_squares(Team::White, &[Square::A1], &[Square::H8], &[Square::A1]);
let actions = board.actions();
assert_eq!(actions.len(), 14, "Corner king should have 14 moves");
}
#[test]
fn pawn_blocked_by_friendly() {
let board = Board::from_squares(Team::White, &[Square::D4, Square::D5], &[Square::H8], &[]);
let actions = board.actions();
let d4_moves: Vec<_> = actions
.iter()
.filter(|a| {
let delta = a.delta.pieces[Team::White.to_usize()];
delta & Square::D4.to_mask() != 0
})
.collect();
let d4_to_d5 = d4_moves.iter().find(|a| {
let dest = a.delta.pieces[Team::White.to_usize()] & !Square::D4.to_mask();
dest == Square::D5.to_mask()
});
assert!(
d4_to_d5.is_none(),
"D4 should not be able to move to D5 (blocked)"
);
}
#[test]
fn king_blocked_by_friendly_cannot_pass() {
let board = Board::from_squares(
Team::White,
&[Square::A4, Square::C4],
&[Square::H8],
&[Square::A4],
);
let actions = board.actions();
let past_c4 = actions.iter().find(|a| {
let delta = a.delta.pieces[Team::White.to_usize()];
if delta & Square::A4.to_mask() != 0 {
let dest = delta & !Square::A4.to_mask() & !Square::C4.to_mask();
dest & (Square::D4.to_mask()
| Square::E4.to_mask()
| Square::F4.to_mask()
| Square::G4.to_mask()
| Square::H4.to_mask())
!= 0
} else {
false
}
});
assert!(
past_c4.is_none(),
"King should not pass through friendly piece"
);
}
#[test]
fn initial_position_piece_count() {
let board = Board::new_default();
assert_eq!(
board.friendly_pieces().count_ones(),
16,
"White should have 16 pieces"
);
assert_eq!(
board.hostile_pieces().count_ones(),
16,
"Black should have 16 pieces"
);
}
#[test]
fn initial_position_no_kings() {
let board = Board::new_default();
assert_eq!(board.state.kings, 0, "No kings at start");
}
#[test]
fn initial_position_white_to_move() {
let board = Board::new_default();
assert_eq!(board.turn, Team::White, "White moves first");
}
#[test]
fn king_10_capture_path_no_180_turns() {
let board = Board::from_squares(
Team::White,
&[Square::C2, Square::G2, Square::H4],
&[
Square::C1,
Square::E2,
Square::C3,
Square::B4,
Square::D4,
Square::A5,
Square::E5,
Square::B6,
Square::D6,
Square::H6,
Square::C7,
Square::H7,
],
&[
Square::C1,
Square::C2,
Square::D4,
Square::B6,
Square::D6,
Square::C7,
],
);
let actions = board.actions();
assert_eq!(actions.len(), 9, "Expected 9 maximum-capture actions");
for action in &actions {
assert_eq!(
action.capture_count(Team::White),
10,
"All actions should capture 10 pieces"
);
let detailed = action.to_detailed(board.turn, &board.state);
let path = detailed.path();
let mut prev_dir: Option<(i8, i8)> = None;
for i in 1..path.len() {
let from = path[i - 1];
let to = path[i];
let dcol = (to.column() as i8 - from.column() as i8).signum();
let drow = (to.row() as i8 - from.row() as i8).signum();
if let Some((pcol, prow)) = prev_dir {
assert!(
!(dcol == -pcol && drow == -prow && (dcol != 0 || drow != 0)),
"180° turn detected in path: {} -> {}",
from,
to
);
}
prev_dir = Some((dcol, drow));
}
}
}
}