use super::piece::{Piece, Color, PieceType};
use super::position::Position;
use std::collections::HashMap;
use std::fmt::Write as FmtWrite;
#[derive(Clone, Debug)]
#[allow(clippy::struct_excessive_bools)]
pub struct Board {
pub squares: HashMap<Position, Piece>,
pub turn: Color,
pub game_state: GameState,
pub white_can_castle_kingside: bool,
pub white_can_castle_queenside: bool,
pub black_can_castle_kingside: bool,
pub black_can_castle_queenside: bool,
pub en_passant_target: Option<Position>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GameState {
Ongoing,
Checkmate(Color), Stalemate,
Draw, }
impl Board {
pub fn new() -> Self {
let mut board = Board {
squares: HashMap::new(),
turn: Color::White,
game_state: GameState::Ongoing,
white_can_castle_kingside: true,
white_can_castle_queenside: true,
black_can_castle_kingside: true,
black_can_castle_queenside: true,
en_passant_target: None,
};
board.reset();
board
}
pub fn initialize_custom(
&mut self,
pieces: Vec<(char, u8, Color, PieceType)>,
turn: Color,
game_state: GameState,
) {
self.squares.clear();
for (file, rank, color, kind) in pieces {
self.squares.insert(
Position::new(file, rank).expect("Invalid position construction"),
Piece { color, kind },
);
}
self.turn = turn;
self.game_state = game_state;
}
pub fn reset(&mut self) {
self.squares.clear();
self.turn = Color::White;
for file in 'a'..='h' {
self.squares.insert(Position::new(file, 2).unwrap(), Piece { color: Color::White, kind: PieceType::Pawn });
self.squares.insert(Position::new(file, 7).unwrap(), Piece { color: Color::Black, kind: PieceType::Pawn });
}
self.squares.insert(Position::new('a', 1).unwrap(), Piece { color: Color::White, kind: PieceType::Rook });
self.squares.insert(Position::new('h', 1).unwrap(), Piece { color: Color::White, kind: PieceType::Rook });
self.squares.insert(Position::new('a', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::Rook });
self.squares.insert(Position::new('h', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::Rook });
self.squares.insert(Position::new('b', 1).unwrap(), Piece { color: Color::White, kind: PieceType::Knight });
self.squares.insert(Position::new('g', 1).unwrap(), Piece { color: Color::White, kind: PieceType::Knight });
self.squares.insert(Position::new('b', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::Knight });
self.squares.insert(Position::new('g', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::Knight });
self.squares.insert(Position::new('c', 1).unwrap(), Piece { color: Color::White, kind: PieceType::Bishop });
self.squares.insert(Position::new('f', 1).unwrap(), Piece { color: Color::White, kind: PieceType::Bishop });
self.squares.insert(Position::new('c', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::Bishop });
self.squares.insert(Position::new('f', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::Bishop });
self.squares.insert(Position::new('d', 1).unwrap(), Piece { color: Color::White, kind: PieceType::Queen });
self.squares.insert(Position::new('d', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::Queen });
self.squares.insert(Position::new('e', 1).unwrap(), Piece { color: Color::White, kind: PieceType::King });
self.squares.insert(Position::new('e', 8).unwrap(), Piece { color: Color::Black, kind: PieceType::King });
self.white_can_castle_kingside = true;
self.white_can_castle_queenside = true;
self.black_can_castle_kingside = true;
self.black_can_castle_queenside = true;
self.en_passant_target = None;
}
pub fn try_move(&mut self, from: Position, to: Position, promotion: Option<PieceType>) -> Result<(), String> {
let piece = match self.squares.get(&from).copied() {
Some(p) => p,
None => return Err("No piece at starting position.".to_string()),
};
if piece.color != self.turn {
return Err("Not your turn.".to_string());
}
let legal_moves = self.get_legal_moves(from);
if !legal_moves.contains(&to) {
return Err("Illegal move.".to_string());
}
if piece.kind == PieceType::King {
let castle_rank = match piece.color {
Color::White => 1,
Color::Black => 8,
};
if from.rank == castle_rank && from.file == 'e' {
if to.file == 'g' && to.rank == castle_rank {
return self.try_castle(piece.color, true);
} else if to.file == 'c' && to.rank == castle_rank {
return self.try_castle(piece.color, false);
}
}
}
let mut clone = self.clone();
clone.force_move(from, to)?;
if clone.is_in_check(piece.color) {
return Err("Move would leave king in check.".to_string());
}
self.force_move(from, to)?;
if let Some(moved_piece) = self.squares.get_mut(&to) {
if moved_piece.kind == PieceType::Pawn {
let promote = match moved_piece.color {
Color::White if to.rank == 8 => true,
Color::Black if to.rank == 1 => true,
_ => false,
};
if promote {
let new_piece = match promotion {
Some(PieceType::Queen | PieceType::Rook | PieceType::Bishop | PieceType::Knight) => promotion.unwrap(),
None => PieceType::Queen, _ => return Err("Invalid promotion piece.".to_string()),
};
moved_piece.kind = new_piece;
}
}
}
self.en_passant_target = None;
if piece.kind == PieceType::Pawn {
let diff = (to.rank as i8 - from.rank as i8).abs();
if diff == 2 {
let file = from.file;
let target_rank = from.rank - 1;
self.en_passant_target = Some(Position::new(file, target_rank).unwrap());
}
}
else {
self.en_passant_target = None;
}
self.turn = Self::opponent_color(self.turn);
if self.is_checkmate(self.turn) {
self.game_state = GameState::Checkmate(self.turn);
} else if self.is_stalemate(self.turn) {
self.game_state = GameState::Draw;
} else {
self.game_state = GameState::Ongoing;
}
Ok(())
}
fn try_castle(&mut self, color: Color, kingside: bool) -> Result<(), String> {
let (rank, rook_file, king_from, king_to, rook_to) = match (color, kingside) {
(Color::White, true) => (1, 'h', Position::new('e', 1).unwrap(), Position::new('g', 1).unwrap(), Position::new('f', 1).unwrap()),
(Color::White, false) => (1, 'a', Position::new('e', 1).unwrap(), Position::new('c', 1).unwrap(), Position::new('d', 1).unwrap()),
(Color::Black, true) => (8, 'h', Position::new('e', 8).unwrap(), Position::new('g', 8).unwrap(), Position::new('f', 8).unwrap()),
(Color::Black, false) => (8, 'a', Position::new('e', 8).unwrap(), Position::new('c', 8).unwrap(), Position::new('d', 8).unwrap()),
};
let can_castle = match (color, kingside) {
(Color::White, true) => self.white_can_castle_kingside,
(Color::White, false) => self.white_can_castle_queenside,
(Color::Black, true) => self.black_can_castle_kingside,
(Color::Black, false) => self.black_can_castle_queenside,
};
if !can_castle {
return Err("Castling not allowed (king or rook has moved)".to_string());
}
let rook_pos = Position::new(rook_file, rank).unwrap();
match self.squares.get(&rook_pos) {
Some(piece) if piece.color == color && piece.kind == PieceType::Rook => {},
_ => return Err("Rook missing for castling".to_string()),
}
let files_between: Vec<char> = if kingside { vec!['f', 'g'] } else { vec!['b', 'c', 'd'] };
for file in files_between.iter() {
let pos = Position::new(*file, rank).unwrap();
if self.squares.contains_key(&pos) {
return Err("Cannot castle: path blocked".to_string());
}
}
if self.is_in_check(color) {
return Err("Cannot castle while in check".to_string());
}
let passing_files = if kingside { ['f', 'g'] } else { ['d', 'c'] };
for file in passing_files.iter() {
let mut clone = self.clone();
clone.force_move(king_from, Position::new(*file, rank).unwrap())?;
if clone.is_in_check(color) {
return Err("Cannot castle through check".to_string());
}
}
self.squares.remove(&king_from).unwrap();
self.squares.remove(&rook_pos).unwrap();
self.squares.insert(king_to, Piece { color, kind: PieceType::King });
self.squares.insert(rook_to, Piece { color, kind: PieceType::Rook });
match color {
Color::White => {
self.white_can_castle_kingside = false;
self.white_can_castle_queenside = false;
}
Color::Black => {
self.black_can_castle_kingside = false;
self.black_can_castle_queenside = false;
}
}
self.turn = Self::opponent_color(self.turn);
Ok(())
}
fn opponent_color(color: Color) -> Color {
match color {
Color::White => Color::Black,
Color::Black => Color::White,
}
}
fn force_move(&mut self, from: Position, to: Position) -> Result<(), String> {
let piece = match self.squares.remove(&from) {
Some(p) => p,
None => return Err("No piece at starting position.".to_string()),
};
if let Some(en_passant_pos) = self.en_passant_target {
if piece.kind == PieceType::Pawn && to == en_passant_pos {
let captured_pawn_rank = if piece.color == Color::White { to.rank - 1 } else { to.rank + 1 };
let captured_pawn_pos = Position::new(to.file, captured_pawn_rank).unwrap();
self.squares.remove(&captured_pawn_pos);
}
}
self.squares.insert(to, piece);
if let Some(moved_piece) = self.squares.get(&to) {
if moved_piece.kind == PieceType::King {
match moved_piece.color {
Color::White => {
self.white_can_castle_kingside = false;
self.white_can_castle_queenside = false;
}
Color::Black => {
self.black_can_castle_kingside = false;
self.black_can_castle_queenside = false;
}
}
}
if moved_piece.kind == PieceType::Rook {
if from.file == 'a' && from.rank == 1 {
self.white_can_castle_queenside = false;
}
if from.file == 'h' && from.rank == 1 {
self.white_can_castle_kingside = false;
}
if from.file == 'a' && from.rank == 8 {
self.black_can_castle_queenside = false;
}
if from.file == 'h' && from.rank == 8 {
self.black_can_castle_kingside = false;
}
}
}
Ok(())
}
pub fn is_in_check(&self, color: Color) -> bool {
let king_pos = match self.squares.iter()
.find(|(_, piece)| piece.color == color && piece.kind == PieceType::King)
{
Some((pos, _)) => pos,
None => return false, };
for (pos, piece) in &self.squares {
if piece.color != color {
let moves = self.get_legal_moves(*pos);
if moves.contains(king_pos) {
return true;
}
}
}
false
}
pub fn is_checkmate(&self, color: Color) -> bool {
if !self.is_in_check(color) {
return false;
}
for (from, piece) in &self.squares {
if piece.color != color {
continue;
}
let legal_moves = self.get_legal_moves(*from);
for to in legal_moves {
let mut cloned = self.clone();
if cloned.force_move(*from, to).is_ok() && !cloned.is_in_check(color) {
return false;
}
}
}
true
}
pub fn is_stalemate(&self, color: Color) -> bool {
if self.is_in_check(color) {
return false;
}
for (from, piece) in &self.squares {
if piece.color != color {
continue;
}
let legal_moves = self.get_legal_moves(*from);
for to in legal_moves {
let mut cloned = self.clone();
if cloned.force_move(*from, to).is_ok() && !cloned.is_in_check(color) {
return false;
}
}
}
true
}
pub fn get_legal_moves(&self, from: Position) -> Vec<Position> {
let piece = match self.squares.get(&from) {
Some(piece) => piece,
None => return vec![],
};
let mut moves = Vec::new();
match piece.kind {
PieceType::Pawn => {
let direction = match piece.color {
Color::White => 1, Color::Black => -1, };
if let Some(forward_pos) = Position::new(from.file, (from.rank as i8 + direction) as u8) {
if self.squares.get(&forward_pos).is_none() {
moves.push(forward_pos);
if (piece.color == Color::White && from.rank == 2) || (piece.color == Color::Black && from.rank == 7) {
if let Some(double_forward) = Position::new(from.file, (from.rank as i8 + 2 * direction) as u8) {
if self.squares.get(&double_forward).is_none() {
moves.push(double_forward);
}
}
}
}
}
for &file_offset in &[-1, 1] {
let next_file_i8 = (from.file as u8) as i8 + file_offset;
let next_rank_i8 = from.rank as i8 + direction;
if (b'a' as i8..=b'h' as i8).contains(&next_file_i8) && (1..=8).contains(&next_rank_i8) {
let capture_file = next_file_i8 as u8 as char;
let capture_rank = next_rank_i8 as u8;
if let Some(capture_pos) = Position::new(capture_file, capture_rank) {
if let Some(target_piece) = self.squares.get(&capture_pos) {
if target_piece.color != piece.color {
moves.push(capture_pos);
}
}
}
if let Some(target) = self.en_passant_target {
if target == Position::new(capture_file, capture_rank).unwrap() {
moves.push(target);
}
}
}
}
}
PieceType::Rook => {
moves.extend(self.moves_in_directions(from, &[(1,0), (-1,0), (0,1), (0,-1)], piece.color));
}
PieceType::Bishop => {
moves.extend(self.moves_in_directions(from, &[(1,1), (-1,1), (1,-1), (-1,-1)], piece.color));
}
PieceType::Queen => {
moves.extend(self.moves_in_directions(from, &[
(1,0), (-1,0), (0,1), (0,-1),
(1,1), (-1,1), (1,-1), (-1,-1)
], piece.color));
}
PieceType::Knight => {
let knight_moves = [
(2, 1), (1, 2), (-1, 2), (-2, 1),
(-2, -1), (-1, -2), (1, -2), (2, -1)
];
for (df, dr) in &knight_moves {
let next_file = (from.file as u8 as i8 + df) as u8 as char;
let next_rank = (from.rank as i8 + dr) as u8;
if let Some(pos) = Position::new(next_file, next_rank) {
if !self.squares.get(&pos).map_or(false, |p| p.color == piece.color) {
moves.push(pos);
}
}
}
}
PieceType::King => {
let king_moves = [
(1, 0), (-1, 0), (0, 1), (0, -1),
(1, 1), (-1, 1), (1, -1), (-1, -1)
];
for (df, dr) in &king_moves {
let next_file = (from.file as u8 as i8 + df) as u8 as char;
let next_rank = (from.rank as i8 + dr) as u8;
if let Some(pos) = Position::new(next_file, next_rank) {
if !self.squares.get(&pos).map_or(false, |p| p.color == piece.color) {
moves.push(pos);
}
}
}
let rank = match piece.color {
Color::White => 1,
Color::Black => 8,
};
if from == Position::new('e', rank).unwrap() {
let kingside_allowed = match piece.color {
Color::White => self.white_can_castle_kingside,
Color::Black => self.black_can_castle_kingside,
};
if kingside_allowed {
let f_pos = Position::new('f', rank).unwrap();
let g_pos = Position::new('g', rank).unwrap();
if self.squares.get(&f_pos).is_none() && self.squares.get(&g_pos).is_none() {
moves.push(g_pos);
}
}
let queenside_allowed = match piece.color {
Color::White => self.white_can_castle_queenside,
Color::Black => self.black_can_castle_queenside,
};
if queenside_allowed {
let b_pos = Position::new('b', rank).unwrap();
let c_pos = Position::new('c', rank).unwrap();
let d_pos = Position::new('d', rank).unwrap();
if self.squares.get(&b_pos).is_none() && self.squares.get(&c_pos).is_none() && self.squares.get(&d_pos).is_none() {
moves.push(c_pos);
}
}
}
}
}
moves
}
fn moves_in_directions(&self, from: Position, directions: &[(i8, i8)], color: Color) -> Vec<Position> {
let mut moves = Vec::new();
for (df, dr) in directions {
let mut next_file = from.file as u8 as i8;
let mut next_rank = from.rank as i8;
loop {
next_file += df;
next_rank += dr;
if !(b'a' as i8 <= next_file && next_file <= b'h' as i8 && 1 <= next_rank && next_rank <= 8) {
break;
}
let pos = Position {
file: next_file as u8 as char,
rank: next_rank as u8,
};
if let Some(other_piece) = self.squares.get(&pos) {
if other_piece.color != color {
moves.push(pos); }
break; } else {
moves.push(pos);
}
}
}
moves
}
pub fn display(&self) {
let mut output = String::new();
self.write_display(&mut output).unwrap();
println!("{}", output);
}
#[allow(clippy::missing_panics_doc)]
pub fn write_display<W: FmtWrite>(&self, w: &mut W) -> std::fmt::Result {
for rank in (1..=8).rev() {
write!(w, "{rank} ")?;
for file in 'a'..='h' {
let pos = Position::new(file, rank).unwrap();
if let Some(piece) = self.squares.get(&pos) {
let symbol = match (piece.color, piece.kind) {
(Color::White, PieceType::Pawn) => '♙',
(Color::White, PieceType::Rook) => '♖',
(Color::White, PieceType::Knight) => '♘',
(Color::White, PieceType::Bishop) => '♗',
(Color::White, PieceType::Queen) => '♕',
(Color::White, PieceType::King) => '♔',
(Color::Black, PieceType::Pawn) => '♟',
(Color::Black, PieceType::Rook) => '♜',
(Color::Black, PieceType::Knight) => '♞',
(Color::Black, PieceType::Bishop) => '♝',
(Color::Black, PieceType::Queen) => '♛',
(Color::Black, PieceType::King) => '♚',
};
write!(w, " {symbol} ")?;
} else {
write!(w, " . ")?;
}
}
writeln!(w)?;
}
writeln!(w, " a b c d e f g h")?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
fn verify_piece_at(board: &Board, file: char, rank: u8, expected_color: Color, expected_type: PieceType) {
let pos = Position::new(file, rank).expect("Invalid position construction");
let piece = board.squares.get(&pos)
.unwrap_or_else(|| panic!("Expected piece at {}{}", file, rank));
assert_eq!(
piece.color, expected_color,
"Piece at {}{} has wrong color. Expected {:?}, got {:?}.",
file, rank, expected_color, piece.color
);
assert_eq!(
piece.kind, expected_type,
"Piece at {}{} has wrong type. Expected {:?}, got {:?}.",
file, rank, expected_type, piece.kind
);
}
#[test]
fn test_reset_board_setup() {
let board = Board::new();
assert_eq!(board.turn, Color::White);
assert_eq!(board.squares.len(), 32);
for file in 'a'..='h' {
verify_piece_at(&board, file, 2, Color::White, PieceType::Pawn);
verify_piece_at(&board, file, 7, Color::Black, PieceType::Pawn);
}
for &(file, rank) in &[('a', 1), ('h', 1), ('a', 8), ('h', 8)] {
let color = if rank == 1 { Color::White } else { Color::Black };
verify_piece_at(&board, file, rank, color, PieceType::Rook);
}
for &(file, rank) in &[('b', 1), ('g', 1), ('b', 8), ('g', 8)] {
let color = if rank == 1 { Color::White } else { Color::Black };
verify_piece_at(&board, file, rank, color, PieceType::Knight);
}
for &(file, rank) in &[('c', 1), ('f', 1), ('c', 8), ('f', 8)] {
let color = if rank == 1 { Color::White } else { Color::Black };
verify_piece_at(&board, file, rank, color, PieceType::Bishop);
}
verify_piece_at(&board, 'd', 1, Color::White, PieceType::Queen);
verify_piece_at(&board, 'd', 8, Color::Black, PieceType::Queen);
verify_piece_at(&board, 'e', 1, Color::White, PieceType::King);
verify_piece_at(&board, 'e', 8, Color::Black, PieceType::King);
}
#[test]
fn test_display_initial_board() {
let board = Board::new();
let mut output = String::new();
board.write_display(&mut output).unwrap();
assert!(output.contains("8"), "Rank 8 missing");
assert!(output.contains("♜"), "Black rook missing");
assert!(output.contains("♞"), "Black knight missing");
assert!(output.contains("♝"), "Black bishop missing");
assert!(output.contains("♛"), "Black queen missing");
assert!(output.contains("♚"), "Black king missing");
assert!(output.contains("7"), "Rank 7 missing");
assert!(output.matches('♟').count() >= 8, "Not enough black pawns");
assert!(output.contains("2"), "Rank 2 missing");
assert!(output.matches('♙').count() >= 8, "Not enough white pawns");
assert!(output.contains("1"), "Rank 1 missing");
assert!(output.contains("♖"), "White rook missing");
assert!(output.contains("♘"), "White knight missing");
assert!(output.contains("♗"), "White bishop missing");
assert!(output.contains("♕"), "White queen missing");
assert!(output.contains("♔"), "White king missing");
for file in 'a'..='h' {
assert!(output.contains(file), "File {} missing", file);
}
}
}
#[cfg(test)]
mod moves_in_direction_tests {
use super::*;
#[test]
fn test_moves_in_directions_empty_board() {
let board = Board::new();
let from = Position::new('d', 4).unwrap();
let directions = &[(1, 0)];
let moves = board.moves_in_directions(from, directions, Color::White);
let expected_files = ['e', 'f', 'g', 'h'];
assert_eq!(moves.len(), expected_files.len());
for (i, pos) in moves.iter().enumerate() {
assert_eq!(pos.file, expected_files[i]);
assert_eq!(pos.rank, 4);
}
}
#[test]
fn test_moves_blocked_by_ally() {
let mut board = Board::new();
let from = Position::new('d', 4).unwrap();
board.squares.insert(Position::new('f', 4).unwrap(), Piece { color: Color::White, kind: PieceType::Pawn });
let directions = &[(1, 0)];
let moves = board.moves_in_directions(from, directions, Color::White);
let expected_files = ['e'];
assert_eq!(moves.len(), expected_files.len());
for (i, pos) in moves.iter().enumerate() {
assert_eq!(pos.file, expected_files[i]);
assert_eq!(pos.rank, 4);
}
}
#[test]
fn test_moves_blocked_by_enemy() {
let mut board = Board::new();
let from = Position::new('d', 4).unwrap();
board.squares.insert(Position::new('f', 4).unwrap(), Piece { color: Color::Black, kind: PieceType::Pawn });
let directions = &[(1, 0)];
let moves = board.moves_in_directions(from, directions, Color::White);
let expected_files = ['e', 'f'];
assert_eq!(moves.len(), expected_files.len());
for (i, pos) in moves.iter().enumerate() {
assert_eq!(pos.file, expected_files[i]);
assert_eq!(pos.rank, 4);
}
}
#[test]
fn test_moves_diagonal() {
let board = Board::new();
let from = Position::new('d', 4).unwrap();
let directions = &[(1, 1)];
let moves = board.moves_in_directions(from, directions, Color::White);
let expected = [('e', 5), ('f', 6), ('g', 7)];
assert_eq!(moves.len(), expected.len());
for (i, pos) in moves.iter().enumerate() {
assert_eq!(pos.file, expected[i].0);
assert_eq!(pos.rank, expected[i].1);
}
}
#[test]
fn test_moves_off_board_edge_case() {
let board = Board::new();
let from = Position::new('h', 8).unwrap();
let directions = &[(1, 0), (0, 1), (1, 1)];
let moves = board.moves_in_directions(from, directions, Color::White);
assert!(moves.is_empty(), "Should not generate any moves from h8 in these directions");
}
#[test]
fn test_moves_in_multiple_directions() {
let board = Board::new();
let from = Position::new('d', 4).unwrap();
let directions = &[(1, 0), (0, 1)];
let moves = board.moves_in_directions(from, directions, Color::White);
let expected_positions = [
('e', 4), ('f', 4), ('g', 4), ('h', 4),
('d', 5), ('d', 6), ('d', 7),
];
assert_eq!(moves.len(), expected_positions.len());
for expected in expected_positions.iter() {
assert!(moves.contains(&Position::new(expected.0, expected.1).unwrap()));
}
}
}
#[cfg(test)]
mod check_tests {
use super::*;
#[test]
fn test_not_in_check_starting_position() {
let board = Board::new();
assert!(!board.is_in_check(Color::White), "White should not be in check at start.");
assert!(!board.is_in_check(Color::Black), "Black should not be in check at start.");
}
#[test]
fn test_in_check_simple_rook_attack() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('e', 8, Color::Black, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check from black rook.");
}
#[test]
fn test_not_in_check_rook_blocked() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('e', 4, Color::White, PieceType::Pawn), ('e', 8, Color::Black, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(!board.is_in_check(Color::White), "White king should not be in check (rook is blocked).");
}
#[test]
fn test_in_check_diagonal_bishop_attack() {
let pieces = vec![
('c', 1, Color::White, PieceType::King),
('f', 4, Color::Black, PieceType::Bishop),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check from bishop on diagonal.");
}
#[test]
fn test_in_check_knight_attack() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('d', 3, Color::Black, PieceType::Knight),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check from knight.");
}
#[test]
fn test_in_check_queen_attack() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('e', 5, Color::Black, PieceType::Queen),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check from queen along file.");
}
#[test]
fn test_in_check_multiple_threats() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('e', 8, Color::Black, PieceType::Rook),
('a', 1, Color::Black, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check from two directions.");
}
#[test]
fn test_not_in_check_empty_board() {
let board = Board::new();
assert!(!board.is_in_check(Color::White), "Empty board should not cause check (no kings).");
assert!(!board.is_in_check(Color::Black), "Empty board should not cause check (no kings).");
}
#[test]
fn test_in_check_pawn_attack() {
let pieces = vec![
('e', 4, Color::White, PieceType::King),
('d', 5, Color::Black, PieceType::Pawn), ];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check from black pawn.");
}
#[test]
fn test_not_in_check_wrong_color_pawn() {
let pieces = vec![
('e', 4, Color::Black, PieceType::King),
('d', 5, Color::White, PieceType::Pawn), ];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(!board.is_in_check(Color::Black), "White pawn should not check black king from above.");
}
#[test]
fn test_not_checkmated_at_start() {
let board = Board::new();
assert!(!board.is_checkmate(Color::White), "White should not be checkmated at starting position.");
assert!(!board.is_checkmate(Color::Black), "Black should not be checkmated at starting position.");
}
}
#[cfg(test)]
mod checkmake_tests {
use super::*;
#[test]
fn test_simple_check_but_not_checkmate() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('e', 8, Color::Black, PieceType::Rook), ];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check.");
assert!(!board.is_checkmate(Color::White), "White king should not be checkmated (can escape).");
}
#[test]
fn test_simple_checkmate_by_rook_and_king_blocked() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('a', 2, Color::Black, PieceType::Rook),
('b', 2, Color::Black, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check.");
assert!(board.is_checkmate(Color::White), "White king should be checkmated (blocked on all sides).");
}
#[test]
fn test_simple_check_by_rook_and_king_can_take_to_escape() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('a', 2, Color::Black, PieceType::Rook),
('b', 3, Color::Black, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check.");
assert!(!board.is_checkmate(Color::White), "White king should be checkmated (blocked on all sides).");
}
#[test]
fn test_checkmate_by_queen() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('h', 1, Color::Black, PieceType::Queen),
('b', 3, Color::Black, PieceType::King),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check from queen.");
assert!(board.is_checkmate(Color::White), "White king should be checkmated (no escape).");
}
#[test]
fn test_not_checkmate_king_can_escape() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('a', 3, Color::Black, PieceType::Queen),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.is_in_check(Color::White), "White king should be in check.");
assert!(!board.is_checkmate(Color::White), "White king should be able to escape (move).");
}
#[test]
fn test_checkmate_fools_mate() {
let mut board = Board::new();
board.try_move(Position::new('f', 2).unwrap(), Position::new('f', 3).unwrap(), None).unwrap();
board.try_move(Position::new('e', 7).unwrap(), Position::new('e', 5).unwrap(), None).unwrap();
board.try_move(Position::new('g', 2).unwrap(), Position::new('g', 4).unwrap(), None).unwrap();
board.try_move(Position::new('d', 8).unwrap(), Position::new('h', 4).unwrap(), None).unwrap();
assert!(board.is_in_check(Color::White), "White should be in check.");
assert!(board.is_checkmate(Color::White), "White should be checkmated in Fool's Mate.");
}
#[test]
fn test_not_checkmate_not_in_check() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('a', 8, Color::Black, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(!board.is_in_check(Color::White), "White king should not be in check.");
assert!(!board.is_checkmate(Color::White), "White king should not be checkmated if not in check.");
}
#[test]
fn test_checkmate_edge_case_empty_board() {
let board = Board::new();
assert!(!board.is_checkmate(Color::White), "Empty board should not panic or cause checkmate.");
assert!(!board.is_checkmate(Color::Black), "Empty board should not panic or cause checkmate.");
}
#[test]
fn test_checkmate_smothered_mate() {
let pieces = vec![
('h', 8, Color::Black, PieceType::King),
('h', 7, Color::Black, PieceType::Pawn),
('g', 7, Color::Black, PieceType::King),
('g', 8, Color::Black, PieceType::Rook),
('f', 7, Color::White, PieceType::Knight),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(!board.is_checkmate(Color::White), "Empty board should not panic or cause checkmate.");
assert!(!board.is_checkmate(Color::Black), "Empty board should not panic or cause checkmate.");
}
}
#[cfg(test)]
mod try_move_tests {
use super::*;
#[test]
fn test_try_move_successful_normal_move() {
let mut board = Board::new();
let result = board.try_move(Position::new('e', 2).unwrap(), Position::new('e', 4).unwrap(), None);
assert!(result.is_ok(), "Expected pawn move from e2 to e4 to succeed.");
assert_eq!(board.turn, Color::Black, "Turn should switch to Black after move.");
assert_eq!(board.game_state, GameState::Ongoing, "Game should continue after normal move.");
}
#[test]
fn test_try_move_no_piece_at_start() {
let mut board = Board::new();
let result = board.try_move(Position::new('e', 3).unwrap(), Position::new('e', 4).unwrap(), None);
assert!(result.is_err(), "Expected error when no piece at starting position.");
}
#[test]
fn test_try_move_illegal_move_attempt() {
let mut board = Board::new();
let result = board.try_move(Position::new('e', 2).unwrap(), Position::new('e', 5).unwrap(), None); assert!(result.is_err(), "Expected illegal move error for pawn jumping 3 spaces.");
}
#[test]
fn test_try_move_not_players_turn() {
let mut board = Board::new();
board.turn = Color::Black;
let result = board.try_move(Position::new('e', 2).unwrap(), Position::new('e', 4).unwrap(), None);
assert!(result.is_err(), "Expected error when moving out of turn.");
}
#[test]
fn test_try_move_into_check_disallowed() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('b', 2, Color::Black, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
let result = board.try_move(Position::new('a', 1).unwrap(), Position::new('b', 1).unwrap(), None);
assert!(result.is_err(), "Expected error when moving into check.");
}
#[test]
fn test_try_move_checkmate_after_move() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('d', 8, Color::Black, PieceType::Queen),
('b', 3, Color::Black, PieceType::King),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
board.turn = Color::Black;
let result = board.try_move(Position::new('d', 8).unwrap(), Position::new('d', 1).unwrap(), None);
assert!(result.is_ok(), "Expected queen move to e5 to succeed.");
match board.game_state {
GameState::Checkmate(Color::White) => {},
other => panic!("Expected White to be checkmated after move, found {:?}", other),
}
}
#[test]
fn test_try_move_no_checkmate_if_king_can_escape() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('f', 3, Color::Black, PieceType::Queen),
];
let turn = Color::Black;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
let result = board.try_move(Position::new('f', 3).unwrap(), Position::new('f', 2).unwrap(), None);
assert!(result.is_ok(), "Expected move to succeed.");
assert_eq!(board.game_state, GameState::Ongoing, "Game should remain ongoing (king can escape).");
}
#[test]
fn test_try_move_stalemate_placeholder() {
let board = Board::new();
assert_eq!(board.game_state, GameState::Ongoing, "Empty board should be ongoing (no stalemate yet).");
}
#[test]
fn test_try_move_fools_mate_checkmate() {
let mut board = Board::new();
board.try_move(Position::new('f', 2).unwrap(), Position::new('f', 3).unwrap(), None).unwrap();
board.try_move(Position::new('e', 7).unwrap(), Position::new('e', 5).unwrap(), None).unwrap();
board.try_move(Position::new('g', 2).unwrap(), Position::new('g', 4).unwrap(), None).unwrap();
board.try_move(Position::new('d', 8).unwrap(), Position::new('h', 4).unwrap(), None).unwrap();
match board.game_state {
GameState::Checkmate(Color::White) => {},
other => panic!("Expected White to be checkmated in Fool's Mate, found {:?}", other),
}
}
}
#[cfg(test)]
mod promotion_tests {
use super::*;
#[test]
fn test_pawn_promotion_to_queen() {
let mut board = Board::new();
board.squares.clear();
board.squares.insert(Position::new('a', 7).unwrap(), Piece { color: Color::White, kind: PieceType::Pawn });
board.turn = Color::White;
let from = Position::new('a', 7).unwrap();
let to = Position::new('a', 8).unwrap();
board.try_move(from, to, Some(PieceType::Queen)).unwrap();
let piece = board.squares.get(&to).unwrap();
assert_eq!(piece.kind, PieceType::Queen);
assert_eq!(piece.color, Color::White);
}
#[test]
fn test_pawn_promotion_checkmate() {
let pieces = vec![
('h', 8, Color::Black, PieceType::Rook),
('h', 7, Color::Black, PieceType::King),
('g', 7, Color::Black, PieceType::Pawn),
('g', 8, Color::Black, PieceType::Bishop),
('g', 8, Color::Black, PieceType::Bishop),
('h', 6, Color::Black, PieceType::Rook),
('f', 7, Color::White, PieceType::Pawn),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
let from = {Position { file: ('f'), rank: (7) }};
let to = {Position { file: ('f'), rank: (8) }};
board.try_move(from, to, Some(PieceType::Knight)).unwrap();
assert_eq!(GameState::Checkmate(Color::Black), board.game_state);
}
}
#[cfg(test)]
mod castle_tests {
use super::*;
#[test]
fn test_white_kingside_castling() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('h', 1, Color::White, PieceType::Rook),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.try_move(Position::new('e', 1).unwrap(), Position::new('g', 1).unwrap(), None).is_ok());
assert_eq!(board.squares.get(&Position::new('g', 1).unwrap()).unwrap().kind, PieceType::King, "King not at g1");
assert_eq!(board.squares.get(&Position::new('f', 1).unwrap()).unwrap().kind, PieceType::Rook, "Rook not at f1");
}
#[test]
fn test_white_kingside_castling_with_piece_blocking() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('h', 1, Color::White, PieceType::Rook),
('g', 1, Color::White, PieceType::Queen),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.try_move(Position::new('e', 1).unwrap(), Position::new('g', 1).unwrap(), None).is_err());
}
#[test]
fn test_white_kingside_castling_with_check_blocking() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('h', 1, Color::White, PieceType::Rook),
('g', 5, Color::Black, PieceType::Queen),
];
let turn = Color::White;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.try_move(Position::new('e', 1).unwrap(), Position::new('g', 1).unwrap(), None).is_err());
}
#[test]
fn test_black_queenside_castling() {
let pieces = vec![
('e', 8, Color::Black, PieceType::King),
('a', 8, Color::Black, PieceType::Rook),
];
let turn = Color::Black;
let game_state = GameState::Ongoing;
let mut board = Board::new();
board.initialize_custom(pieces, turn, game_state);
assert!(board.try_move(Position::new('e', 8).unwrap(), Position::new('c', 8).unwrap(), None).is_ok());
assert_eq!(board.squares.get(&Position::new('c', 8).unwrap()).unwrap().kind, PieceType::King);
assert_eq!(board.squares.get(&Position::new('d', 8).unwrap()).unwrap().kind, PieceType::Rook);
}
}
#[cfg(test)]
mod get_legal_moves_tests {
use super::*;
#[test]
fn test_pawn_initial_double_move() {
let pieces = vec![
('e', 2, Color::White, PieceType::Pawn),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('e', 2).unwrap());
assert!(moves.contains(&Position::new('e', 3).unwrap()), "Pawn should move forward 1 square.");
assert!(moves.contains(&Position::new('e', 4).unwrap()), "Pawn should move forward 2 squares on first move.");
}
#[test]
fn test_pawn_blocked_forward() {
let pieces = vec![
('e', 2, Color::White, PieceType::Pawn),
('e', 3, Color::Black, PieceType::Pawn),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('e', 2).unwrap());
assert!(moves.is_empty(), "Pawn should not move if blocked.");
}
#[test]
fn test_pawn_capture_diagonal() {
let pieces = vec![
('d', 4, Color::White, PieceType::Pawn),
('c', 5, Color::Black, PieceType::Pawn),
('e', 5, Color::Black, PieceType::Pawn),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('d', 4).unwrap());
assert!(moves.contains(&Position::new('c', 5).unwrap()), "Pawn should capture diagonally left.");
assert!(moves.contains(&Position::new('e', 5).unwrap()), "Pawn should capture diagonally right.");
}
#[test]
fn test_knight_movement_open_board() {
let pieces = vec![
('d', 4, Color::White, PieceType::Knight),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('d', 4).unwrap());
let expected_positions = [
('c', 6), ('e', 6),
('b', 5), ('f', 5),
('b', 3), ('f', 3),
('c', 2), ('e', 2),
];
for (file, rank) in expected_positions.iter() {
assert!(moves.contains(&Position::new(*file, *rank).unwrap()), "Knight move to {}{} missing.", file, rank);
}
}
#[test]
fn test_rook_movement_clear() {
let pieces = vec![
('d', 4, Color::White, PieceType::Rook),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('d', 4).unwrap());
let expected_positions = [
('d', 5), ('d', 6), ('d', 7), ('d', 8),
('d', 3), ('d', 2), ('d', 1),
('e', 4), ('f', 4), ('g', 4), ('h', 4),
('c', 4), ('b', 4), ('a', 4),
];
for (file, rank) in expected_positions.iter() {
assert!(moves.contains(&Position::new(*file, *rank).unwrap()), "Rook move to {}{} missing.", file, rank);
}
}
#[test]
fn test_bishop_movement_clear() {
let pieces = vec![
('d', 4, Color::White, PieceType::Bishop),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('d', 4).unwrap());
let expected_positions = [
('e', 5), ('f', 6), ('g', 7), ('h', 8),
('c', 5), ('b', 6), ('a', 7),
('e', 3), ('f', 2), ('g', 1),
('c', 3), ('b', 2), ('a', 1),
];
for (file, rank) in expected_positions.iter() {
assert!(moves.contains(&Position::new(*file, *rank).unwrap()), "Bishop move to {}{} missing.", file, rank);
}
}
#[test]
fn test_queen_movement_clear() {
let pieces = vec![
('d', 4, Color::White, PieceType::Queen),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('d', 4).unwrap());
assert!(moves.contains(&Position::new('d', 5).unwrap()), "Queen vertical move missing.");
assert!(moves.contains(&Position::new('e', 5).unwrap()), "Queen diagonal move missing.");
assert!(moves.contains(&Position::new('h', 4).unwrap()), "Queen horizontal move missing.");
}
#[test]
fn test_king_movement_clear() {
let pieces = vec![
('e', 4, Color::White, PieceType::King),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('e', 4).unwrap());
let expected_positions = [
('d', 4), ('f', 4),
('e', 5), ('e', 3),
('d', 5), ('f', 5),
('d', 3), ('f', 3),
];
for (file, rank) in expected_positions.iter() {
assert!(moves.contains(&Position::new(*file, *rank).unwrap()), "King move to {}{} missing.", file, rank);
}
}
#[test]
fn test_king_castling_moves_allowed() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('h', 1, Color::White, PieceType::Rook),
('a', 1, Color::White, PieceType::Rook),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('e', 1).unwrap());
assert!(moves.contains(&Position::new('g', 1).unwrap()), "Kingside castling move should be allowed.");
assert!(moves.contains(&Position::new('c', 1).unwrap()), "Queenside castling move should be allowed.");
}
#[test]
fn test_king_castling_blocked_path() {
let pieces = vec![
('e', 1, Color::White, PieceType::King),
('h', 1, Color::White, PieceType::Rook),
('f', 1, Color::White, PieceType::Knight), ];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('e', 1).unwrap());
assert!(!moves.contains(&Position::new('g', 1).unwrap()), "Kingside castling should not be allowed if path is blocked.");
}
#[test]
fn test_get_legal_moves_empty_square() {
let pieces = vec![];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
let moves = board.get_legal_moves(Position::new('e', 4).unwrap());
assert!(moves.is_empty(), "No moves should exist for empty square.");
}
}
#[cfg(test)]
mod en_passant_tests {
use super::*;
#[test]
fn test_en_passant_available() {
let pieces = vec![
('e', 5, Color::White, PieceType::Pawn),
('d', 7, Color::Black, PieceType::Pawn),
('e', 8, Color::Black, PieceType::King),
('a', 8, Color::Black, PieceType::Rook),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::Black, GameState::Ongoing);
board.try_move(Position::new('d', 7).unwrap(), Position::new('d', 5).unwrap(), None).unwrap();
let moves = board.get_legal_moves(Position::new('e', 5).unwrap());
assert!(moves.contains(&Position::new('d', 6).unwrap()), "En passant should be available");
}
#[test]
fn test_en_passant_capture_execution() {
let pieces = vec![
('e', 5, Color::White, PieceType::Pawn),
('d', 7, Color::Black, PieceType::Pawn),
('e', 8, Color::Black, PieceType::King),
('a', 8, Color::Black, PieceType::Rook),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::Black, GameState::Ongoing);
board.try_move(Position::new('d', 7).unwrap(), Position::new('d', 5).unwrap(), None).unwrap();
board.try_move(Position::new('e', 5).unwrap(), Position::new('d', 6).unwrap(), None).unwrap();
assert!(board.squares.get(&Position::new('d', 5).unwrap()).is_none(), "Captured pawn should be gone after en passant.");
assert_eq!(board.squares.get(&Position::new('d', 6).unwrap()).unwrap().kind, PieceType::Pawn);
}
}
#[cfg(test)]
mod stalemate_tests {
use super::*;
#[test]
fn test_simple_stalemate_position() {
let pieces = vec![
('h', 1, Color::White, PieceType::King),
('f', 2, Color::Black, PieceType::Queen),
('g', 3, Color::Black, PieceType::King),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
assert!(board.is_stalemate(Color::White), "White should be stalemated in this position.");
}
#[test]
fn test_not_stalemate_king_can_move() {
let pieces = vec![
('h', 1, Color::White, PieceType::King),
('f', 1, Color::Black, PieceType::Queen),
('g', 2, Color::Black, PieceType::King),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
assert!(!board.is_stalemate(Color::White), "White should not be stalemated (king has legal moves).");
}
#[test]
fn test_stalemate_with_multiple_pieces_on_board() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('c', 2, Color::Black, PieceType::Queen),
('b', 4, Color::Black, PieceType::Bishop),
('b', 3, Color::White, PieceType::Pawn),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
assert!(board.is_stalemate(Color::White), "White should be stalemated even with other pieces on board.");
}
#[test]
fn test_not_stalemate_in_check() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('c', 3, Color::Black, PieceType::Queen),
('b', 4, Color::Black, PieceType::Bishop),
('b', 3, Color::White, PieceType::Pawn),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
assert!(!board.is_stalemate(Color::White), "White is in check, so not stalemate.");
}
#[test]
fn test_no_legal_moves_but_in_checkmate_not_stalemate() {
let pieces = vec![
('a', 1, Color::White, PieceType::King),
('b', 2, Color::Black, PieceType::Queen),
('c', 3, Color::Black, PieceType::Bishop),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::White, GameState::Ongoing);
assert!(!board.is_stalemate(Color::White), "White is in checkmate, not stalemate.");
assert!(board.is_checkmate(Color::White), "White is in checkmate, not stalemate.");
}
#[test]
fn test_black_stalemated_simple() {
let pieces = vec![
('a', 8, Color::Black, PieceType::King),
('c', 7, Color::White, PieceType::Queen),
('b', 6, Color::White, PieceType::King),
];
let mut board = Board::new();
board.initialize_custom(pieces, Color::Black, GameState::Ongoing);
assert!(board.is_stalemate(Color::Black), "Black should be stalemated.");
}
}