use std::fmt;
use super::{Square, State, Team};
use crate::state::MASK_ROW_PROMOTIONS;
#[cfg(debug_assertions)]
use crate::state::{MASK_COL_A, MASK_COL_H, MASK_ROW_1, MASK_ROW_8};
const MAX_PATH_LEN: usize = 17;
const FILE_CHARS: [u8; 8] = [b'a', b'b', b'c', b'd', b'e', b'f', b'g', b'h'];
const RANK_CHARS: [u8; 8] = [b'1', b'2', b'3', b'4', b'5', b'6', b'7', b'8'];
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ActionPath {
path: [Square; MAX_PATH_LEN],
path_len: u8,
is_capture: bool,
is_promotion: bool,
}
impl ActionPath {
#[inline]
#[must_use]
pub const fn new_move(src: Square, dest: Square, is_promotion: bool) -> Self {
let mut path = [Square::A1; MAX_PATH_LEN];
path[0] = src;
path[1] = dest;
Self {
path,
path_len: 2,
is_capture: false,
is_promotion,
}
}
#[inline]
#[must_use]
#[allow(clippy::cast_possible_truncation)] pub fn new_capture(src: Square, landings: &[Square], is_promotion: bool) -> Self {
debug_assert!(
!landings.is_empty(),
"capture must have at least one landing"
);
debug_assert!(landings.len() < MAX_PATH_LEN, "too many landing squares");
let mut path = [Square::A1; MAX_PATH_LEN];
path[0] = src;
for (i, &landing) in landings.iter().enumerate() {
path[i + 1] = landing;
}
Self {
path,
path_len: (1 + landings.len()) as u8,
is_capture: true,
is_promotion,
}
}
#[inline]
#[must_use]
pub const fn source(&self) -> Square {
self.path[0]
}
#[inline]
#[must_use]
pub const fn destination(&self) -> Square {
self.path[(self.path_len - 1) as usize]
}
#[inline]
#[must_use]
pub const fn is_capture(&self) -> bool {
self.is_capture
}
#[inline]
#[must_use]
pub const fn is_promotion(&self) -> bool {
self.is_promotion
}
#[inline]
#[must_use]
pub const fn path_len(&self) -> usize {
self.path_len as usize
}
#[inline]
#[must_use]
pub fn path(&self) -> &[Square] {
&self.path[..self.path_len as usize]
}
#[inline]
const fn square_to_notation(square: Square, buf: &mut [u8; 2]) {
let col = square.column();
let row = square.row();
buf[0] = FILE_CHARS[col as usize];
buf[1] = RANK_CHARS[row as usize];
}
#[must_use]
pub fn to_notation(&self) -> String {
let mut result = String::with_capacity(8);
let separator = if self.is_capture { 'x' } else { '-' };
let mut buf = [0u8; 2];
Self::square_to_notation(self.path[0], &mut buf);
result.push(buf[0] as char);
result.push(buf[1] as char);
for i in 1..self.path_len as usize {
result.push(separator);
Self::square_to_notation(self.path[i], &mut buf);
result.push(buf[0] as char);
result.push(buf[1] as char);
}
if self.is_promotion {
result.push_str("=K");
}
result
}
#[inline]
pub fn write_notation(&self, buf: &mut [u8]) -> usize {
debug_assert!(buf.len() >= 52, "buffer too small");
let separator = if self.is_capture { b'x' } else { b'-' };
let mut pos = 0;
let col = self.path[0].column();
let row = self.path[0].row();
buf[pos] = FILE_CHARS[col as usize];
buf[pos + 1] = RANK_CHARS[row as usize];
pos += 2;
for i in 1..self.path_len as usize {
buf[pos] = separator;
pos += 1;
let col = self.path[i].column();
let row = self.path[i].row();
buf[pos] = FILE_CHARS[col as usize];
buf[pos + 1] = RANK_CHARS[row as usize];
pos += 2;
}
if self.is_promotion {
buf[pos] = b'=';
buf[pos + 1] = b'K';
pos += 2;
}
pos
}
}
impl fmt::Display for ActionPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.to_notation())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Action {
pub delta: State,
}
impl Action {
pub(crate) const EMPTY: Self = Self {
delta: State::zeros(),
};
#[inline]
#[must_use]
pub(crate) const fn is_empty(&self) -> bool {
(self.delta.pieces[0] | self.delta.pieces[1] | self.delta.kings) == 0
}
#[must_use]
pub const fn new(
team: Team,
src_square: Square,
dest_square: Square,
capture_squares: &[Square],
kings_mask: u64,
) -> Self {
let src_mask = src_square.to_mask();
let dest_mask = dest_square.to_mask();
let is_src_king = kings_mask & src_mask != 0u64;
let mut capture_mask = 0u64;
let mut i = 0;
while i < capture_squares.len() {
let square = capture_squares[i];
capture_mask |= square.to_mask();
i += 1;
}
match team {
Team::White => {
if is_src_king {
if capture_mask == 0u64 {
Self::new_move_as_king::<0>(src_mask, dest_mask)
} else {
Self::new_capture_as_king::<0>(
src_mask,
dest_mask,
capture_mask,
kings_mask,
)
}
} else if capture_mask == 0u64 {
Self::new_move_as_pawn::<0>(src_mask, dest_mask)
} else {
Self::new_capture_as_pawn::<0>(src_mask, dest_mask, capture_mask, kings_mask)
}
}
Team::Black => {
if is_src_king {
if capture_mask == 0u64 {
Self::new_move_as_king::<1>(src_mask, dest_mask)
} else {
Self::new_capture_as_king::<1>(
src_mask,
dest_mask,
capture_mask,
kings_mask,
)
}
} else if capture_mask == 0u64 {
Self::new_move_as_pawn::<1>(src_mask, dest_mask)
} else {
Self::new_capture_as_pawn::<1>(src_mask, dest_mask, capture_mask, kings_mask)
}
}
}
}
#[inline]
#[must_use]
pub(crate) const fn new_move_as_pawn<const TEAM_INDEX: usize>(
src_mask: u64,
dest_mask: u64,
) -> Self {
#[cfg(debug_assertions)]
{
debug_assert!(src_mask.is_power_of_two(), "source must be a single square");
debug_assert!(
dest_mask.is_power_of_two(),
"destination must be a single square"
);
debug_assert!(
src_mask != dest_mask,
"source and destination cannot be the same square",
);
debug_assert!(
src_mask & MASK_ROW_PROMOTIONS[TEAM_INDEX] == 0,
"source cannot be on a promotion row",
);
if TEAM_INDEX == 0 {
debug_assert!(
(((src_mask & !MASK_COL_A) >> 1u8) == dest_mask) || (((src_mask & !MASK_COL_H) << 1u8) == dest_mask) || (((src_mask & !MASK_ROW_8) << 8u8) == dest_mask), "white pawn can only move 1 unit to the left, right, or up"
);
} else if TEAM_INDEX == 1 {
debug_assert!(
(((src_mask & !MASK_COL_A) >> 1u8) == dest_mask) || (((src_mask & !MASK_COL_H) << 1u8) == dest_mask) || (((src_mask & !MASK_ROW_1) >> 8u8) == dest_mask), "black pawn can only move 1 unit to the left, right, or down"
);
}
}
let mut delta = State::zeros();
delta.pieces[TEAM_INDEX] = src_mask ^ dest_mask;
let promotion_mask = MASK_ROW_PROMOTIONS[TEAM_INDEX];
delta.kings = dest_mask & promotion_mask;
Self { delta }
}
#[inline]
#[must_use]
pub(crate) const fn new_move_as_king<const TEAM_INDEX: usize>(
src_mask: u64,
dest_mask: u64,
) -> Self {
#[cfg(debug_assertions)]
{
debug_assert!(src_mask.is_power_of_two(), "source must be a single square");
debug_assert!(
dest_mask.is_power_of_two(),
"destination must be a single square"
);
debug_assert!(
src_mask != dest_mask,
"source and destination cannot be the same square",
);
let src_square = unsafe { Square::from_mask(src_mask) };
let dest_square = unsafe { Square::from_mask(dest_mask) };
debug_assert!(
src_square.row() == dest_square.row()
|| src_square.column() == dest_square.column(),
"king can only move to a square in the same row or column"
);
}
let mut delta = State::zeros();
delta.pieces[TEAM_INDEX] = src_mask ^ dest_mask;
delta.kings = src_mask ^ dest_mask;
Self { delta }
}
#[inline]
#[must_use]
pub(crate) const fn new_capture_as_pawn<const TEAM_INDEX: usize>(
src_mask: u64,
dest_mask: u64,
capture_mask: u64,
kings_mask: u64,
) -> Self {
#[cfg(debug_assertions)]
{
debug_assert!(src_mask.is_power_of_two(), "source must be a single square");
debug_assert!(
dest_mask.is_power_of_two(),
"destination must be a single square"
);
debug_assert!(capture_mask != 0, "capture mask cannot be empty");
debug_assert!(
src_mask & capture_mask == 0,
"source cannot be a capture square"
);
}
let mut delta = State::zeros();
delta.pieces[TEAM_INDEX] = src_mask ^ dest_mask;
delta.pieces[1 - TEAM_INDEX] = capture_mask;
delta.kings = (src_mask & kings_mask) | (capture_mask & kings_mask);
Self { delta }
}
#[inline]
#[must_use]
pub(crate) const fn new_capture_as_king<const TEAM_INDEX: usize>(
src_mask: u64,
dest_mask: u64,
capture_mask: u64,
kings_mask: u64,
) -> Self {
#[cfg(debug_assertions)]
{
debug_assert!(src_mask.is_power_of_two(), "source must be a single square");
debug_assert!(
dest_mask.is_power_of_two(),
"destination must be a single square"
);
debug_assert!(capture_mask != 0, "capture mask cannot be empty");
debug_assert!(src_mask & kings_mask != 0, "source must be a king");
debug_assert!(
src_mask & capture_mask == 0,
"source cannot be a capture square"
);
}
let mut delta = State::zeros();
delta.pieces[TEAM_INDEX] = src_mask ^ dest_mask;
delta.pieces[1 - TEAM_INDEX] = capture_mask;
delta.kings = (src_mask ^ dest_mask) | (capture_mask & kings_mask);
Self { delta }
}
#[inline(always)]
pub(crate) fn combine_(&mut self, other: &Self) {
self.delta.apply_(&other.delta);
}
#[inline(always)]
#[must_use]
pub(crate) fn combine(&self, action: &Self) -> Self {
let mut new_action = *self;
new_action.combine_(action);
new_action
}
#[must_use]
pub fn to_detailed(&self, team: Team, original_state: &State) -> ActionPath {
let team_index = team.to_usize();
let opponent_index = 1 - team_index;
let our_delta = self.delta.pieces[team_index];
let original_pieces = original_state.pieces[team_index];
let src_mask = our_delta & original_pieces;
let src = unsafe { Square::from_mask(src_mask) };
let dest_mask = our_delta & !original_pieces;
let dest = unsafe { Square::from_mask(dest_mask) };
let captured_mask = self.delta.pieces[opponent_index];
let is_capture = captured_mask != 0;
let promotion_row = MASK_ROW_PROMOTIONS[team_index];
let was_king = (original_state.kings & src_mask) != 0;
let is_dest_on_promotion_row = (dest_mask & promotion_row) != 0;
let dest_in_kings_delta = (self.delta.kings & dest_mask) != 0;
let is_promotion = !was_king && is_dest_on_promotion_row && dest_in_kings_delta;
if !is_capture {
return ActionPath::new_move(src, dest, is_promotion);
}
let capture_count = captured_mask.count_ones() as usize;
if capture_count == 1 {
return ActionPath::new_capture(src, &[dest], is_promotion);
}
let mut landings = [Square::A1; MAX_PATH_LEN - 1];
let mut landing_count = 0;
let found = Self::reconstruct_capture_path(
src,
captured_mask,
dest_mask,
was_king,
team,
None,
&mut landings,
&mut landing_count,
);
debug_assert!(
found,
"Failed to reconstruct valid capture path - this indicates a bug"
);
ActionPath::new_capture(src, &landings[..landing_count], is_promotion)
}
#[allow(clippy::too_many_arguments)]
fn reconstruct_capture_path(
current: Square,
remaining_captures: u64,
final_dest_mask: u64,
is_king: bool,
team: Team,
prev_direction: Option<(i8, i8)>,
landings: &mut [Square; MAX_PATH_LEN - 1],
landing_count: &mut usize,
) -> bool {
if remaining_captures == 0 {
if *landing_count > 0 {
let last_landing = landings[*landing_count - 1];
return last_landing.to_mask() == final_dest_mask;
}
return false;
}
let current_row = current.row() as i8;
let current_col = current.column() as i8;
const ALL_DIRECTIONS: [(i8, i8); 4] = [(0, 1), (0, -1), (1, 0), (-1, 0)];
const WHITE_PAWN_DIRECTIONS: [(i8, i8); 3] = [(0, 1), (0, -1), (1, 0)];
const BLACK_PAWN_DIRECTIONS: [(i8, i8); 3] = [(0, 1), (0, -1), (-1, 0)];
let directions: &[(i8, i8)] = if is_king {
&ALL_DIRECTIONS
} else if team == Team::White {
&WHITE_PAWN_DIRECTIONS
} else {
&BLACK_PAWN_DIRECTIONS
};
for &(row_dir, col_dir) in directions {
if let Some((prev_row, prev_col)) = prev_direction {
if row_dir == -prev_row && col_dir == -prev_col {
continue;
}
}
if is_king {
let mut dist = 1i8;
let mut found_capture: Option<Square> = None;
while dist < 8 {
let check_row = current_row + row_dir * dist;
let check_col = current_col + col_dir * dist;
if !(0..=7).contains(&check_row) || !(0..=7).contains(&check_col) {
break;
}
let check_sq =
unsafe { Square::from_row_column(check_row as u8, check_col as u8) };
let check_mask = check_sq.to_mask();
if let Some(captured) = found_capture {
if check_mask & remaining_captures != 0 {
dist += 1;
continue;
}
let old_count = *landing_count;
debug_assert!(
*landing_count < MAX_PATH_LEN - 1,
"landing count {} exceeds maximum path length {}",
*landing_count,
MAX_PATH_LEN - 1
);
landings[*landing_count] = check_sq;
*landing_count += 1;
let new_remaining = remaining_captures & !captured.to_mask();
if Self::reconstruct_capture_path(
check_sq,
new_remaining,
final_dest_mask,
is_king,
team,
Some((row_dir, col_dir)),
landings,
landing_count,
) {
return true;
}
*landing_count = old_count;
} else if check_mask & remaining_captures != 0 {
found_capture = Some(check_sq);
} else {
}
dist += 1;
}
} else {
let capture_row = current_row + row_dir;
let capture_col = current_col + col_dir;
let landing_row = current_row + row_dir * 2;
let landing_col = current_col + col_dir * 2;
if !(0..=7).contains(&landing_row) || !(0..=7).contains(&landing_col) {
continue;
}
let capture_sq =
unsafe { Square::from_row_column(capture_row as u8, capture_col as u8) };
let landing_sq =
unsafe { Square::from_row_column(landing_row as u8, landing_col as u8) };
if capture_sq.to_mask() & remaining_captures != 0 {
let old_count = *landing_count;
debug_assert!(
*landing_count < MAX_PATH_LEN - 1,
"landing count {} exceeds maximum path length {}",
*landing_count,
MAX_PATH_LEN - 1
);
landings[*landing_count] = landing_sq;
*landing_count += 1;
let new_remaining = remaining_captures & !capture_sq.to_mask();
if Self::reconstruct_capture_path(
landing_sq,
new_remaining,
final_dest_mask,
is_king,
team,
Some((row_dir, col_dir)),
landings,
landing_count,
) {
return true;
}
*landing_count = old_count;
}
}
}
false
}
#[inline]
#[must_use]
pub const fn source(&self, team: Team, original_pieces: u64) -> Square {
let team_index = team.to_usize();
let our_delta = self.delta.pieces[team_index];
let src_mask = our_delta & original_pieces;
unsafe { Square::from_mask(src_mask) }
}
#[inline]
#[must_use]
pub const fn destination(&self, team: Team, original_pieces: u64) -> Square {
let team_index = team.to_usize();
let our_delta = self.delta.pieces[team_index];
let dest_mask = our_delta & !original_pieces;
unsafe { Square::from_mask(dest_mask) }
}
#[inline]
#[must_use]
pub const fn is_capture(&self, team: Team) -> bool {
let opponent_index = 1 - team.to_usize();
self.delta.pieces[opponent_index] != 0
}
#[inline]
#[must_use]
pub const fn capture_count(&self, team: Team) -> u32 {
let opponent_index = 1 - team.to_usize();
self.delta.pieces[opponent_index].count_ones()
}
#[inline]
#[must_use]
pub const fn captured_pieces(&self, team: Team) -> u64 {
let opponent_index = 1 - team.to_usize();
self.delta.pieces[opponent_index]
}
#[inline]
#[must_use]
pub const fn is_promotion(&self, team: Team, original_state: &State) -> bool {
let team_index = team.to_usize();
let our_delta = self.delta.pieces[team_index];
let original_pieces = original_state.pieces[team_index];
let src_mask = our_delta & original_pieces;
let dest_mask = our_delta & !original_pieces;
let promotion_row = MASK_ROW_PROMOTIONS[team_index];
let was_king = (original_state.kings & src_mask) != 0;
let is_dest_on_promotion_row = (dest_mask & promotion_row) != 0;
let dest_in_kings_delta = (self.delta.kings & dest_mask) != 0;
!was_king && is_dest_on_promotion_row && dest_in_kings_delta
}
}
#[cfg(test)]
mod tests {
use super::*;
use test_case::test_case;
#[test]
fn move_as_white_pawn() {
let action = Action::new_move_as_pawn::<0>(Square::A4.to_mask(), Square::A5.to_mask());
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::A4.to_mask() | Square::A5.to_mask()
);
debug_assert_eq!(action.delta.pieces[Team::Black.to_usize()], 0u64);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn move_as_white_pawn_gets_promoted() {
let action = Action::new_move_as_pawn::<0>(Square::C7.to_mask(), Square::C8.to_mask());
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::C7.to_mask() | Square::C8.to_mask()
);
debug_assert_eq!(action.delta.pieces[Team::Black.to_usize()], 0u64);
debug_assert_eq!(action.delta.kings, Square::C8.to_mask());
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_move_as_white_pawn_src_must_be_single_square() {
let _ = Action::new_move_as_pawn::<0>(
Square::A2.to_mask() | Square::A3.to_mask(),
Square::A4.to_mask(),
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_move_as_white_pawn_dest_must_be_single_square() {
let _ = Action::new_move_as_pawn::<0>(
Square::A3.to_mask(),
Square::A4.to_mask() | Square::A5.to_mask(),
);
}
#[test]
#[should_panic(expected = "source and destination cannot be the same square")]
fn invalid_move_as_white_pawn_src_same_as_dest() {
let _ = Action::new_move_as_pawn::<0>(Square::A4.to_mask(), Square::A4.to_mask());
}
#[test]
#[should_panic(expected = "source cannot be on a promotion row")]
fn invalid_move_as_white_pawn_src_at_promotion_row() {
let _ = Action::new_move_as_pawn::<0>(Square::C8.to_mask(), Square::B8.to_mask());
}
#[test_case(Square::C4, Square::C3; "move down")]
#[test_case(Square::C4, Square::C2; "move far down")]
#[test_case(Square::B4, Square::D4; "move far left")]
#[test_case(Square::A6, Square::C6; "move far right")]
#[test_case(Square::D5, Square::C6; "move diagonal left")]
#[test_case(Square::D5, Square::E6; "move diagonal right")]
#[should_panic(expected = "white pawn can only move 1 unit to the left, right, or up")]
fn invalid_move_as_white_pawn(src_square: Square, dest_square: Square) {
let _ = Action::new_move_as_pawn::<0>(src_square.to_mask(), dest_square.to_mask());
}
#[test]
fn move_as_black_pawn() {
let action = Action::new_move_as_pawn::<1>(Square::B7.to_mask(), Square::B6.to_mask());
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B7.to_mask() | Square::B6.to_mask()
);
debug_assert_eq!(action.delta.pieces[Team::White.to_usize()], 0u64);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn move_as_black_pawn_gets_promoted() {
let action = Action::new_move_as_pawn::<1>(Square::D2.to_mask(), Square::D1.to_mask());
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::D2.to_mask() | Square::D1.to_mask()
);
debug_assert_eq!(action.delta.pieces[Team::White.to_usize()], 0u64);
debug_assert_eq!(action.delta.kings, Square::D1.to_mask());
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_move_as_black_pawn_src_must_be_single_square() {
let _ = Action::new_move_as_pawn::<1>(
Square::D6.to_mask() | Square::D7.to_mask(),
Square::D5.to_mask(),
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_move_as_black_pawn_dest_must_be_single_square() {
let _ = Action::new_move_as_pawn::<1>(
Square::D6.to_mask(),
Square::D4.to_mask() | Square::D5.to_mask(),
);
}
#[test]
#[should_panic(expected = "source and destination cannot be the same square")]
fn invalid_move_as_black_pawn_src_same_as_dest() {
let _ = Action::new_move_as_pawn::<1>(Square::C5.to_mask(), Square::C5.to_mask());
}
#[test]
#[should_panic(expected = "source cannot be on a promotion row")]
fn invalid_move_as_black_pawn_src_at_promotion_row() {
let _ = Action::new_move_as_pawn::<1>(Square::C1.to_mask(), Square::B1.to_mask());
}
#[test_case(Square::C4, Square::C5; "move up")]
#[test_case(Square::C4, Square::C2; "move far down")]
#[test_case(Square::B4, Square::D4; "move far left")]
#[test_case(Square::A6, Square::C6; "move far right")]
#[test_case(Square::D5, Square::C4; "move diagonal left")]
#[test_case(Square::D5, Square::E4; "move diagonal right")]
#[should_panic(expected = "black pawn can only move 1 unit to the left, right, or down")]
fn invalid_move_as_black_pawn(src_square: Square, dest_square: Square) {
let _ = Action::new_move_as_pawn::<1>(src_square.to_mask(), dest_square.to_mask());
}
#[test]
fn move_as_white_king() {
let action = Action::new_move_as_king::<0>(Square::B5.to_mask(), Square::B1.to_mask());
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::B5.to_mask() | Square::B1.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::B5.to_mask() | Square::B1.to_mask()
);
debug_assert_eq!(action.delta.pieces[Team::Black.to_usize()], 0u64);
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_move_as_white_king_src_must_be_single_square() {
let _ = Action::new_move_as_king::<0>(
Square::A2.to_mask() | Square::A3.to_mask(),
Square::A4.to_mask(),
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_move_as_white_king_dest_must_be_single_square() {
let _ = Action::new_move_as_king::<0>(
Square::A3.to_mask(),
Square::A4.to_mask() | Square::A5.to_mask(),
);
}
#[test]
#[should_panic(expected = "source and destination cannot be the same square")]
fn invalid_move_as_white_king_src_same_as_dest() {
let _ = Action::new_move_as_king::<0>(Square::B7.to_mask(), Square::B7.to_mask());
}
#[test_case(Square::A2, Square::B3; "diagonal")]
#[test_case(Square::F1, Square::G6; "far diagonal")]
#[should_panic(expected = "king can only move to a square in the same row or column")]
fn invalid_move_as_white_king(src_square: Square, dest_square: Square) {
let _ = Action::new_move_as_king::<0>(src_square.to_mask(), dest_square.to_mask());
}
#[test]
fn new_move_as_king_black() {
let action = Action::new_move_as_king::<1>(Square::H5.to_mask(), Square::A5.to_mask());
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::H5.to_mask() | Square::A5.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::H5.to_mask() | Square::A5.to_mask()
);
debug_assert_eq!(action.delta.pieces[Team::White.to_usize()], 0u64);
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_move_as_black_king_src_must_be_single_square() {
let _ = Action::new_move_as_king::<1>(
Square::D6.to_mask() | Square::D7.to_mask(),
Square::D5.to_mask(),
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_move_as_black_king_dest_must_be_single_square() {
let _ = Action::new_move_as_king::<1>(
Square::D6.to_mask(),
Square::D4.to_mask() | Square::D5.to_mask(),
);
}
#[test]
#[should_panic(expected = "source and destination cannot be the same square")]
fn invalid_move_as_black_king_src_same_as_dest() {
let _ = Action::new_move_as_king::<1>(Square::F3.to_mask(), Square::F3.to_mask());
}
#[test_case(Square::A4, Square::B3; "diagonal")]
#[test_case(Square::H8, Square::C4; "far diagonal")]
#[should_panic(expected = "king can only move to a square in the same row or column")]
fn invalid_move_as_black_king(src_square: Square, dest_square: Square) {
let _ = Action::new_move_as_king::<1>(src_square.to_mask(), dest_square.to_mask());
}
#[test]
fn capture_as_white_pawn() {
let action = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
Square::A5.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::A4.to_mask() | Square::A6.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::A5.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn capture_as_white_pawn_multiple_captures() {
let action = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::C6.to_mask(),
Square::A5.to_mask() | Square::B6.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::A4.to_mask() | Square::C6.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::A5.to_mask() | Square::B6.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn new_capture_as_pawn_white_lands_on_promotion_row() {
let action = Action::new_capture_as_pawn::<0>(
Square::B6.to_mask(),
Square::B8.to_mask(),
Square::B7.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::B6.to_mask() | Square::B8.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B7.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn new_capture_as_pawn_white_removes_captured_kings() {
let action = Action::new_capture_as_pawn::<0>(
Square::B6.to_mask(),
Square::B8.to_mask(),
Square::B7.to_mask(),
Square::B7.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::B6.to_mask() | Square::B8.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B7.to_mask()
);
debug_assert_eq!(action.delta.kings, Square::B7.to_mask());
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_capture_as_white_pawn_src_must_be_single_square() {
let _ = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask() | Square::B4.to_mask(),
Square::A6.to_mask(),
Square::A5.to_mask(),
0u64,
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_capture_as_white_pawn_dest_must_be_single_square() {
let _ = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::A6.to_mask() | Square::B6.to_mask(),
Square::A5.to_mask(),
0u64,
);
}
#[test]
fn capture_as_white_pawn_from_promotion_row() {
let action = Action::new_capture_as_pawn::<0>(
Square::C8.to_mask(),
Square::A8.to_mask(),
Square::B8.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::C8.to_mask() | Square::A8.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B8.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
#[should_panic(expected = "source cannot be a capture square")]
fn invalid_capture_as_white_pawn_src_cannot_be_capture_square() {
let _ = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
Square::A4.to_mask(),
0u64,
);
}
#[test]
#[should_panic(expected = "capture mask cannot be empty")]
fn invalid_capture_as_white_pawn_no_captures() {
let _ = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
0u64,
0u64,
);
}
#[test]
fn capture_as_black_pawn() {
let action = Action::new_capture_as_pawn::<1>(
Square::B6.to_mask(),
Square::B4.to_mask(),
Square::B5.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B6.to_mask() | Square::B4.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::B5.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn capture_as_black_pawn_multiple_captures() {
let action = Action::new_capture_as_pawn::<1>(
Square::B6.to_mask(),
Square::D4.to_mask(),
Square::B5.to_mask() | Square::C4.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B6.to_mask() | Square::D4.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::B5.to_mask() | Square::C4.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn new_capture_as_pawn_black_lands_on_promotion_row() {
let action = Action::new_capture_as_pawn::<1>(
Square::B3.to_mask(),
Square::B1.to_mask(),
Square::B2.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B3.to_mask() | Square::B1.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::B2.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
fn new_capture_as_pawn_black_removes_captured_kings() {
let action = Action::new_capture_as_pawn::<1>(
Square::B3.to_mask(),
Square::B1.to_mask(),
Square::B2.to_mask(),
Square::B2.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::B3.to_mask() | Square::B1.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::B2.to_mask()
);
debug_assert_eq!(action.delta.kings, Square::B2.to_mask());
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_capture_as_black_pawn_src_must_be_single_square() {
let _ = Action::new_capture_as_pawn::<1>(
Square::B6.to_mask() | Square::C6.to_mask(),
Square::B4.to_mask(),
Square::B5.to_mask(),
0u64,
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_capture_as_black_pawn_dest_must_be_single_square() {
let _ = Action::new_capture_as_pawn::<1>(
Square::B6.to_mask(),
Square::B4.to_mask() | Square::D4.to_mask(),
Square::B5.to_mask(),
0u64,
);
}
#[test]
fn capture_as_black_pawn_from_promotion_row() {
let action = Action::new_capture_as_pawn::<1>(
Square::E1.to_mask(),
Square::C1.to_mask(),
Square::D1.to_mask(),
0u64,
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::E1.to_mask() | Square::C1.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::D1.to_mask()
);
debug_assert_eq!(action.delta.kings, 0u64);
}
#[test]
#[should_panic(expected = "source cannot be a capture square")]
fn invalid_capture_as_black_pawn_src_cannot_be_capture_square() {
let _ = Action::new_capture_as_pawn::<1>(
Square::F6.to_mask(),
Square::F4.to_mask(),
Square::F6.to_mask(),
0u64,
);
}
#[test]
#[should_panic(expected = "capture mask cannot be empty")]
fn invalid_capture_as_black_pawn_no_captures() {
let _ = Action::new_capture_as_pawn::<1>(
Square::G6.to_mask(),
Square::G4.to_mask(),
0u64,
0u64,
);
}
#[test]
fn capture_as_white_king() {
let action = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::H4.to_mask(),
Square::D4.to_mask(),
Square::A4.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::A4.to_mask() | Square::H4.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::D4.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::A4.to_mask() | Square::H4.to_mask()
);
}
#[test]
fn capture_as_white_king_multiple_captures() {
let action = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::H1.to_mask(),
Square::D4.to_mask() | Square::H2.to_mask(),
Square::A4.to_mask() | Square::H2.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::A4.to_mask() | Square::H1.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::D4.to_mask() | Square::H2.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::A4.to_mask() | Square::H1.to_mask() | Square::H2.to_mask()
);
}
#[test]
fn capture_as_white_king_removes_captured_kings() {
let action = Action::new_capture_as_king::<0>(
Square::F5.to_mask(),
Square::F8.to_mask(),
Square::F7.to_mask(),
Square::F5.to_mask() | Square::F7.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::F5.to_mask() | Square::F8.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::F7.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::F5.to_mask() | Square::F8.to_mask() | Square::F7.to_mask()
);
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_capture_as_white_king_src_must_be_single_square() {
let _ = Action::new_capture_as_king::<0>(
Square::A4.to_mask() | Square::B4.to_mask(),
Square::A6.to_mask(),
Square::A5.to_mask(),
Square::A4.to_mask() | Square::B4.to_mask(),
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_capture_as_white_king_dest_must_be_single_square() {
let _ = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::A6.to_mask() | Square::B6.to_mask(),
Square::A5.to_mask(),
Square::A4.to_mask(),
);
}
#[test]
#[should_panic(expected = "source cannot be a capture square")]
fn invalid_capture_as_white_king_src_cannot_be_capture_square() {
let _ = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
Square::A4.to_mask(),
Square::A4.to_mask(),
);
}
#[test]
#[should_panic(expected = "capture mask cannot be empty")]
fn invalid_capture_as_white_king_no_captures() {
let _ = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
0u64,
Square::A4.to_mask(),
);
}
#[test]
fn capture_as_black_king() {
let action = Action::new_capture_as_king::<1>(
Square::A4.to_mask(),
Square::H4.to_mask(),
Square::D4.to_mask(),
Square::A4.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::A4.to_mask() | Square::H4.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::D4.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::A4.to_mask() | Square::H4.to_mask()
);
}
#[test]
fn capture_as_black_king_multiple_captures() {
let action = Action::new_capture_as_king::<1>(
Square::A4.to_mask(),
Square::H1.to_mask(),
Square::D4.to_mask() | Square::H2.to_mask(),
Square::A4.to_mask() | Square::H2.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::A4.to_mask() | Square::H1.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::D4.to_mask() | Square::H2.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::A4.to_mask() | Square::H1.to_mask() | Square::H2.to_mask()
);
}
#[test]
fn capture_as_black_king_removes_captured_kings() {
let action = Action::new_capture_as_king::<1>(
Square::F5.to_mask(),
Square::F8.to_mask(),
Square::F7.to_mask(),
Square::F5.to_mask() | Square::F7.to_mask(),
);
debug_assert_eq!(
action.delta.pieces[Team::Black.to_usize()],
Square::F5.to_mask() | Square::F8.to_mask()
);
debug_assert_eq!(
action.delta.pieces[Team::White.to_usize()],
Square::F7.to_mask()
);
debug_assert_eq!(
action.delta.kings,
Square::F5.to_mask() | Square::F8.to_mask() | Square::F7.to_mask()
);
}
#[test]
#[should_panic(expected = "source must be a single square")]
fn invalid_capture_as_black_king_src_must_be_single_square() {
let _ = Action::new_capture_as_king::<1>(
Square::A4.to_mask() | Square::B4.to_mask(),
Square::A6.to_mask(),
Square::A5.to_mask(),
Square::A4.to_mask() | Square::B4.to_mask(),
);
}
#[test]
#[should_panic(expected = "destination must be a single square")]
fn invalid_capture_as_black_king_dest_must_be_single_square() {
let _ = Action::new_capture_as_king::<1>(
Square::A4.to_mask(),
Square::A6.to_mask() | Square::B6.to_mask(),
Square::A5.to_mask(),
Square::A4.to_mask(),
);
}
#[test]
#[should_panic(expected = "source cannot be a capture square")]
fn invalid_capture_as_black_king_src_cannot_be_capture_square() {
let _ = Action::new_capture_as_king::<1>(
Square::A4.to_mask(),
Square::A6.to_mask(),
Square::A4.to_mask(),
Square::A4.to_mask(),
);
}
#[test]
#[should_panic(expected = "capture mask cannot be empty")]
fn invalid_capture_as_black_king_no_captures() {
let _ = Action::new_capture_as_king::<1>(
Square::A4.to_mask(),
Square::A6.to_mask(),
0u64,
Square::A4.to_mask(),
);
}
#[test]
fn combine() {
let action1 = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
Square::A5.to_mask(),
Square::A5.to_mask(),
);
let action2 = Action::new_capture_as_pawn::<0>(
Square::A6.to_mask(),
Square::C6.to_mask(),
Square::B6.to_mask(),
0u64,
);
let combined = action1.combine(&action2);
debug_assert_eq!(
combined.delta.pieces[Team::White.to_usize()],
Square::A4.to_mask() | Square::C6.to_mask()
);
debug_assert_eq!(
combined.delta.pieces[Team::Black.to_usize()],
Square::A5.to_mask() | Square::B6.to_mask()
);
debug_assert_eq!(combined.delta.kings, Square::A5.to_mask());
let mut combined_ = action1;
combined_.combine_(&action2);
debug_assert_eq!(combined_, combined);
}
#[test]
fn notation_simple_move() {
let notation = ActionPath::new_move(Square::E3, Square::E4, false);
assert_eq!(notation.to_notation(), "e3-e4");
assert_eq!(notation.source(), Square::E3);
assert_eq!(notation.destination(), Square::E4);
assert!(!notation.is_capture());
assert!(!notation.is_promotion());
assert_eq!(notation.path_len(), 2);
}
#[test]
fn notation_sideways_move() {
let notation = ActionPath::new_move(Square::D4, Square::E4, false);
assert_eq!(notation.to_notation(), "d4-e4");
}
#[test]
fn notation_king_long_move() {
let notation = ActionPath::new_move(Square::A1, Square::A7, false);
assert_eq!(notation.to_notation(), "a1-a7");
}
#[test]
fn notation_promotion() {
let notation = ActionPath::new_move(Square::C7, Square::C8, true);
assert_eq!(notation.to_notation(), "c7-c8=K");
assert!(notation.is_promotion());
assert!(!notation.is_capture());
}
#[test]
fn notation_single_capture() {
let notation = ActionPath::new_capture(Square::D4, &[Square::D6], false);
assert_eq!(notation.to_notation(), "d4xd6");
assert!(notation.is_capture());
assert!(!notation.is_promotion());
assert_eq!(notation.path_len(), 2);
}
#[test]
fn notation_multi_capture() {
let notation = ActionPath::new_capture(Square::B3, &[Square::D3, Square::D5], false);
assert_eq!(notation.to_notation(), "b3xd3xd5");
assert!(notation.is_capture());
assert_eq!(notation.path_len(), 3);
assert_eq!(notation.path(), &[Square::B3, Square::D3, Square::D5]);
}
#[test]
fn notation_capture_with_promotion() {
let notation = ActionPath::new_capture(Square::C6, &[Square::C8], true);
assert_eq!(notation.to_notation(), "c6xc8=K");
assert!(notation.is_capture());
assert!(notation.is_promotion());
}
#[test]
fn notation_complex_multi_capture() {
let notation =
ActionPath::new_capture(Square::A1, &[Square::A3, Square::A5, Square::A7], false);
assert_eq!(notation.to_notation(), "a1xa3xa5xa7");
assert_eq!(notation.path_len(), 4);
}
#[test]
fn notation_complex_multi_capture_with_promotion() {
let notation = ActionPath::new_capture(
Square::B2,
&[Square::B4, Square::D4, Square::D6, Square::D8],
true,
);
assert_eq!(notation.to_notation(), "b2xb4xd4xd6xd8=K");
assert!(notation.is_promotion());
}
#[test]
fn notation_display_trait() {
let notation = ActionPath::new_move(Square::D3, Square::D4, false);
assert_eq!(format!("{notation}"), "d3-d4");
}
#[test]
fn notation_write_to_buffer() {
let notation = ActionPath::new_capture(Square::B3, &[Square::D3, Square::D5], false);
let mut buf = [0u8; 52];
let len = notation.write_notation(&mut buf);
assert_eq!(len, 8); assert_eq!(&buf[..len], b"b3xd3xd5");
}
#[test]
fn notation_write_to_buffer_with_promotion() {
let notation = ActionPath::new_move(Square::C7, Square::C8, true);
let mut buf = [0u8; 52];
let len = notation.write_notation(&mut buf);
assert_eq!(len, 7); assert_eq!(&buf[..len], b"c7-c8=K");
}
#[test]
fn notation_all_corners() {
let a1 = ActionPath::new_move(Square::A1, Square::A2, false);
assert_eq!(a1.to_notation(), "a1-a2");
let h1 = ActionPath::new_move(Square::H1, Square::H2, false);
assert_eq!(h1.to_notation(), "h1-h2");
let a8 = ActionPath::new_move(Square::A7, Square::A8, false);
assert_eq!(a8.to_notation(), "a7-a8");
let h8 = ActionPath::new_move(Square::H7, Square::H8, false);
assert_eq!(h8.to_notation(), "h7-h8");
}
#[test_case(Square::A1, "a1"; "a1")]
#[test_case(Square::B2, "b2"; "b2")]
#[test_case(Square::C3, "c3"; "c3")]
#[test_case(Square::D4, "d4"; "d4")]
#[test_case(Square::E5, "e5"; "e5")]
#[test_case(Square::F6, "f6"; "f6")]
#[test_case(Square::G7, "g7"; "g7")]
#[test_case(Square::H8, "h8"; "h8")]
fn notation_square_format(square: Square, expected_prefix: &str) {
let notation = ActionPath::new_move(square, Square::A1, false);
assert!(notation.to_notation().starts_with(expected_prefix));
}
#[test]
fn action_to_notation_simple_move() {
let original_state = State::new([Square::D4.to_mask(), Square::H8.to_mask()], 0);
let action = Action::new_move_as_pawn::<0>(Square::D4.to_mask(), Square::D5.to_mask());
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::D4);
assert_eq!(notation.destination(), Square::D5);
assert!(!notation.is_capture());
assert!(!notation.is_promotion());
assert_eq!(notation.to_notation(), "d4-d5");
}
#[test]
fn action_to_notation_move_left() {
let original_state = State::new([Square::D4.to_mask(), Square::H8.to_mask()], 0);
let action = Action::new_move_as_pawn::<0>(Square::D4.to_mask(), Square::C4.to_mask());
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::D4);
assert_eq!(notation.destination(), Square::C4);
assert!(!notation.is_capture());
assert_eq!(notation.to_notation(), "d4-c4");
}
#[test]
fn action_to_notation_move_right() {
let original_state = State::new([Square::D4.to_mask(), Square::H8.to_mask()], 0);
let action = Action::new_move_as_pawn::<0>(Square::D4.to_mask(), Square::E4.to_mask());
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::D4);
assert_eq!(notation.destination(), Square::E4);
assert!(!notation.is_capture());
assert_eq!(notation.to_notation(), "d4-e4");
}
#[test]
fn action_to_notation_pawn_promotion() {
let original_state = State::new([Square::C7.to_mask(), Square::H1.to_mask()], 0);
let action = Action::new_move_as_pawn::<0>(Square::C7.to_mask(), Square::C8.to_mask());
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::C7);
assert_eq!(notation.destination(), Square::C8);
assert!(!notation.is_capture());
assert!(notation.is_promotion());
assert_eq!(notation.to_notation(), "c7-c8=K");
}
#[test]
fn action_to_notation_black_pawn_promotion() {
let original_state = State::new([Square::H8.to_mask(), Square::D2.to_mask()], 0);
let action = Action::new_move_as_pawn::<1>(Square::D2.to_mask(), Square::D1.to_mask());
let notation = action.to_detailed(Team::Black, &original_state);
assert_eq!(notation.source(), Square::D2);
assert_eq!(notation.destination(), Square::D1);
assert!(!notation.is_capture());
assert!(notation.is_promotion());
assert_eq!(notation.to_notation(), "d2-d1=K");
}
#[test]
fn action_to_notation_king_move() {
let original_state = State::new(
[Square::D4.to_mask(), Square::H1.to_mask()],
Square::D4.to_mask(),
);
let action = Action::new_move_as_king::<0>(Square::D4.to_mask(), Square::D8.to_mask());
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::D4);
assert_eq!(notation.destination(), Square::D8);
assert!(!notation.is_capture());
assert!(!notation.is_promotion()); assert_eq!(notation.to_notation(), "d4-d8");
}
#[test]
fn action_to_notation_single_capture() {
let original_state = State::new([Square::D4.to_mask(), Square::D5.to_mask()], 0);
let action = Action::new_capture_as_pawn::<0>(
Square::D4.to_mask(),
Square::D6.to_mask(),
Square::D5.to_mask(),
0,
);
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::D4);
assert_eq!(notation.destination(), Square::D6);
assert!(notation.is_capture());
assert!(!notation.is_promotion());
assert_eq!(notation.to_notation(), "d4xd6");
}
#[test]
fn action_to_notation_capture_with_promotion() {
let original_state = State::new([Square::B6.to_mask(), Square::B7.to_mask()], 0);
let mut action = Action::new_capture_as_pawn::<0>(
Square::B6.to_mask(),
Square::B8.to_mask(),
Square::B7.to_mask(),
0,
);
action.delta.kings ^= Square::B8.to_mask();
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::B6);
assert_eq!(notation.destination(), Square::B8);
assert!(notation.is_capture());
assert!(notation.is_promotion());
assert_eq!(notation.to_notation(), "b6xb8=K");
}
#[test]
fn action_to_notation_king_capture() {
let original_state = State::new(
[Square::A4.to_mask(), Square::D4.to_mask()],
Square::A4.to_mask(),
);
let action = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::H4.to_mask(),
Square::D4.to_mask(),
Square::A4.to_mask(),
);
let notation = action.to_detailed(Team::White, &original_state);
assert_eq!(notation.source(), Square::A4);
assert_eq!(notation.destination(), Square::H4);
assert!(notation.is_capture());
assert!(!notation.is_promotion());
assert_eq!(notation.to_notation(), "a4xh4");
}
#[test]
fn action_to_detailed_multi_capture_reconstructs_path() {
let original_state = State::new(
[
Square::A4.to_mask(),
Square::A5.to_mask() | Square::B6.to_mask(),
],
0,
);
let action1 = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
Square::A5.to_mask(),
0,
);
let action2 = Action::new_capture_as_pawn::<0>(
Square::A6.to_mask(),
Square::C6.to_mask(),
Square::B6.to_mask(),
0,
);
let combined = action1.combine(&action2);
let detailed = combined.to_detailed(Team::White, &original_state);
assert_eq!(detailed.source(), Square::A4);
assert_eq!(detailed.destination(), Square::C6);
assert!(detailed.is_capture());
assert!(!detailed.is_promotion());
assert_eq!(detailed.path(), &[Square::A4, Square::A6, Square::C6]);
assert_eq!(detailed.to_notation(), "a4xa6xc6");
}
#[test]
fn action_source_helper() {
let original_pieces = Square::D4.to_mask();
let action = Action::new_move_as_pawn::<0>(Square::D4.to_mask(), Square::D5.to_mask());
assert_eq!(action.source(Team::White, original_pieces), Square::D4);
}
#[test]
fn action_destination_helper() {
let original_pieces = Square::D4.to_mask();
let action = Action::new_move_as_pawn::<0>(Square::D4.to_mask(), Square::D5.to_mask());
assert_eq!(action.destination(Team::White, original_pieces), Square::D5);
}
#[test]
fn action_is_capture_helper() {
let move_action = Action::new_move_as_pawn::<0>(Square::D4.to_mask(), Square::D5.to_mask());
assert!(!move_action.is_capture(Team::White));
let capture_action = Action::new_capture_as_pawn::<0>(
Square::D4.to_mask(),
Square::D6.to_mask(),
Square::D5.to_mask(),
0,
);
assert!(capture_action.is_capture(Team::White));
}
#[test]
fn action_capture_count_helper() {
let single_capture = Action::new_capture_as_pawn::<0>(
Square::D4.to_mask(),
Square::D6.to_mask(),
Square::D5.to_mask(),
0,
);
assert_eq!(single_capture.capture_count(Team::White), 1);
let action1 = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::A6.to_mask(),
Square::A5.to_mask(),
0,
);
let action2 = Action::new_capture_as_pawn::<0>(
Square::A6.to_mask(),
Square::C6.to_mask(),
Square::B6.to_mask(),
0,
);
let combined = action1.combine(&action2);
assert_eq!(combined.capture_count(Team::White), 2);
}
#[test]
fn action_captured_pieces_helper() {
let action = Action::new_capture_as_pawn::<0>(
Square::D4.to_mask(),
Square::D6.to_mask(),
Square::D5.to_mask(),
0,
);
assert_eq!(action.captured_pieces(Team::White), Square::D5.to_mask());
}
#[test]
fn action_is_promotion_helper() {
let original_state = State::new([Square::D4.to_mask(), Square::H8.to_mask()], 0);
let move_action = Action::new_move_as_pawn::<0>(Square::D4.to_mask(), Square::D5.to_mask());
assert!(!move_action.is_promotion(Team::White, &original_state));
let promo_state = State::new([Square::C7.to_mask(), Square::H1.to_mask()], 0);
let promo_action =
Action::new_move_as_pawn::<0>(Square::C7.to_mask(), Square::C8.to_mask());
assert!(promo_action.is_promotion(Team::White, &promo_state));
let king_state = State::new(
[Square::C7.to_mask(), Square::H1.to_mask()],
Square::C7.to_mask(), );
let king_action = Action::new_move_as_king::<0>(Square::C7.to_mask(), Square::C8.to_mask());
assert!(!king_action.is_promotion(Team::White, &king_state));
}
#[test]
fn action_black_pawn_move() {
let original_state = State::new([Square::H1.to_mask(), Square::D5.to_mask()], 0);
let action = Action::new_move_as_pawn::<1>(Square::D5.to_mask(), Square::D4.to_mask());
let notation = action.to_detailed(Team::Black, &original_state);
assert_eq!(notation.source(), Square::D5);
assert_eq!(notation.destination(), Square::D4);
assert!(!notation.is_capture());
assert_eq!(notation.to_notation(), "d5-d4");
}
#[test]
fn action_black_capture() {
let original_state = State::new([Square::D4.to_mask(), Square::D5.to_mask()], 0);
let action = Action::new_capture_as_pawn::<1>(
Square::D5.to_mask(),
Square::D3.to_mask(),
Square::D4.to_mask(),
0,
);
let notation = action.to_detailed(Team::Black, &original_state);
assert_eq!(notation.source(), Square::D5);
assert_eq!(notation.destination(), Square::D3);
assert!(notation.is_capture());
assert_eq!(notation.to_notation(), "d5xd3");
}
#[test]
fn action_to_detailed_triple_capture_reconstructs_path() {
let original_state = State::new(
[
Square::D2.to_mask(),
Square::D3.to_mask() | Square::D5.to_mask() | Square::E6.to_mask(),
],
0,
);
let action1 = Action::new_capture_as_pawn::<0>(
Square::D2.to_mask(),
Square::D4.to_mask(),
Square::D3.to_mask(),
0,
);
let action2 = Action::new_capture_as_pawn::<0>(
Square::D4.to_mask(),
Square::D6.to_mask(),
Square::D5.to_mask(),
0,
);
let action3 = Action::new_capture_as_pawn::<0>(
Square::D6.to_mask(),
Square::F6.to_mask(),
Square::E6.to_mask(),
0,
);
let combined = action1.combine(&action2).combine(&action3);
let detailed = combined.to_detailed(Team::White, &original_state);
assert_eq!(detailed.source(), Square::D2);
assert_eq!(detailed.destination(), Square::F6);
assert!(detailed.is_capture());
assert_eq!(
detailed.path(),
&[Square::D2, Square::D4, Square::D6, Square::F6]
);
assert_eq!(detailed.to_notation(), "d2xd4xd6xf6");
}
#[test]
fn action_to_detailed_king_multi_capture() {
let original_state = State::new(
[
Square::A4.to_mask(),
Square::D4.to_mask() | Square::E7.to_mask(),
],
Square::A4.to_mask(), );
let action1 = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::E4.to_mask(),
Square::D4.to_mask(),
Square::A4.to_mask(),
);
let action2 = Action::new_capture_as_king::<0>(
Square::E4.to_mask(),
Square::E8.to_mask(),
Square::E7.to_mask(),
Square::E4.to_mask(),
);
let combined = action1.combine(&action2);
let detailed = combined.to_detailed(Team::White, &original_state);
assert_eq!(detailed.source(), Square::A4);
assert_eq!(detailed.destination(), Square::E8);
assert!(detailed.is_capture());
assert_eq!(detailed.path(), &[Square::A4, Square::E4, Square::E8]);
assert_eq!(detailed.to_notation(), "a4xe4xe8");
}
#[test]
fn action_new_white_pawn_move() {
let action = Action::new(Team::White, Square::D3, Square::D4, &[], 0);
let expected = Action::new_move_as_pawn::<0>(Square::D3.to_mask(), Square::D4.to_mask());
assert_eq!(action, expected);
}
#[test]
fn action_new_white_pawn_capture() {
let action = Action::new(Team::White, Square::D4, Square::D6, &[Square::D5], 0);
let expected = Action::new_capture_as_pawn::<0>(
Square::D4.to_mask(),
Square::D6.to_mask(),
Square::D5.to_mask(),
0,
);
assert_eq!(action, expected);
}
#[test]
fn action_new_white_king_move() {
let kings_mask = Square::D4.to_mask();
let action = Action::new(Team::White, Square::D4, Square::D8, &[], kings_mask);
let expected = Action::new_move_as_king::<0>(Square::D4.to_mask(), Square::D8.to_mask());
assert_eq!(action, expected);
}
#[test]
fn action_new_white_king_capture() {
let kings_mask = Square::A4.to_mask();
let action = Action::new(
Team::White,
Square::A4,
Square::H4,
&[Square::D4],
kings_mask,
);
let expected = Action::new_capture_as_king::<0>(
Square::A4.to_mask(),
Square::H4.to_mask(),
Square::D4.to_mask(),
kings_mask,
);
assert_eq!(action, expected);
}
#[test]
fn action_new_black_pawn_move() {
let action = Action::new(Team::Black, Square::D6, Square::D5, &[], 0);
let expected = Action::new_move_as_pawn::<1>(Square::D6.to_mask(), Square::D5.to_mask());
assert_eq!(action, expected);
}
#[test]
fn action_new_black_pawn_capture() {
let action = Action::new(Team::Black, Square::D5, Square::D3, &[Square::D4], 0);
let expected = Action::new_capture_as_pawn::<1>(
Square::D5.to_mask(),
Square::D3.to_mask(),
Square::D4.to_mask(),
0,
);
assert_eq!(action, expected);
}
#[test]
fn action_new_black_king_move() {
let kings_mask = Square::D5.to_mask();
let action = Action::new(Team::Black, Square::D5, Square::D1, &[], kings_mask);
let expected = Action::new_move_as_king::<1>(Square::D5.to_mask(), Square::D1.to_mask());
assert_eq!(action, expected);
}
#[test]
fn action_new_black_king_capture() {
let kings_mask = Square::H4.to_mask();
let action = Action::new(
Team::Black,
Square::H4,
Square::A4,
&[Square::D4],
kings_mask,
);
let expected = Action::new_capture_as_king::<1>(
Square::H4.to_mask(),
Square::A4.to_mask(),
Square::D4.to_mask(),
kings_mask,
);
assert_eq!(action, expected);
}
#[test]
fn action_new_multi_capture() {
let action = Action::new(
Team::White,
Square::A4,
Square::C6,
&[Square::A5, Square::B6],
0,
);
let expected = Action::new_capture_as_pawn::<0>(
Square::A4.to_mask(),
Square::C6.to_mask(),
Square::A5.to_mask() | Square::B6.to_mask(),
0,
);
assert_eq!(action, expected);
}
#[test]
fn action_is_empty() {
assert!(Action::EMPTY.is_empty());
let move_action = Action::new(Team::White, Square::D3, Square::D4, &[], 0);
assert!(!move_action.is_empty());
let capture_action = Action::new(Team::White, Square::D4, Square::D6, &[Square::D5], 0);
assert!(!capture_action.is_empty());
}
}