use std::io::{stdin, stdout, Write}; use rand::{rngs::StdRng,SeedableRng,Rng}; use crate::{bitboard::{Bitboard, BitboardFns}, moves::{Move, MoveKind}, attacks::Attacks, square::Square}; use self::ttable::{TranspositionTable, TTABLE_SIZE}; mod engine; mod ttable; pub enum CastlingSide { King, Queen, } #[derive(Debug, Clone, PartialEq)] pub struct Board { pub pieces: [Bitboard; 12], pub occupancy: Bitboard, pub ply: u16, /// En passsant target square pub ep_target: Option, /// Castling rights indexed by Color and CastlingSide pub castling_rights: [[bool; 2]; 2], /// Zobrist hash of the current position pub hash: u64, transposition_table: TranspositionTable, zobrist_seed: [u64; 781], attacks: Attacks, } #[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive)] #[repr(usize)] pub enum PieceType { #[default] Pawn, Knight, Bishop, Rook, Queen, King, PawnBlack, KnightBlack, BishopBlack, RookBlack, QueenBlack, KingBlack, } impl PieceType { pub fn without_color(&self) -> Self { let index = *self as usize; Self::from(index % 6) } // Return the price of the peice pub fn static_eval(&self) -> f32 { match self.without_color() { PieceType::Pawn => 1.0, PieceType::Bishop => 3.3, PieceType::Knight => 3.2, PieceType::Rook => 5.0, PieceType::Queen => 9.0, PieceType::King => 0., _ => panic!("Piece should be without color"), } } } const PIECE_CHARS: [&str; 12] = [ "♟︎", "♞", "♝", "♜", "♛", "♚", "♙", "♘", "♗", "♖", "♕", "♔", ]; impl Board { #[allow(non_snake_case)] pub fn from_FEN(fen: String) -> Self { let mut pieces = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; let mut rank = 7; let mut file = 0i32; for character in fen.chars() { let index = rank * 8 + file; let position = 1 << index.clamp(0, 63); if character.is_numeric() { let digit = match character.to_digit(10) { None => todo!("What to do here?"), Some(digit) => digit, }; if digit > 0 && digit <= 8 { file += digit as i32; } } else { match character { 'P' => pieces[PieceType::Pawn as usize] |= position, 'N' => pieces[PieceType::Knight as usize] |= position, 'B' => pieces[PieceType::Bishop as usize] |= position, 'R' => pieces[PieceType::Rook as usize] |= position, 'Q' => pieces[PieceType::Queen as usize] |= position, 'K' => pieces[PieceType::King as usize] |= position, 'p' => pieces[PieceType::PawnBlack as usize] |= position, 'n' => pieces[PieceType::KnightBlack as usize] |= position, 'b' => pieces[PieceType::BishopBlack as usize] |= position, 'r' => pieces[PieceType::RookBlack as usize] |= position, 'q' => pieces[PieceType::QueenBlack as usize] |= position, 'k' => pieces[PieceType::KingBlack as usize] |= position, '/' => { rank -= 1; file = -1; // So it becomes 0 }, ' ' => { break }, // TODO: break for now, parse everything else later '-' => {}, // TODO 'w' => {}, // TODO _ => todo!("Unexpected character!"), } file += 1; } } let mut rng = StdRng::seed_from_u64(228); let zobrist_seed = [(); 781].map(|_| rng.gen()); let mut board = Self { pieces, occupancy: 0, ply: 0, attacks: Attacks::new(), castling_rights: [[true; 2]; 2], // TODO: actualy parse from FEN ep_target: None, // TODO: parse from FEN hash: 0, transposition_table: vec![None; TTABLE_SIZE as usize], zobrist_seed, }; board.update_occupancy(); board.update_zobrist_hash(); board } pub fn new() -> Self { let default_fen = String::from("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); Self::from_FEN(default_fen) } pub fn read_move(&self) -> Result { print!("\nEnter a move: "); stdout().flush().unwrap(); let mut s = String::new(); stdin().read_line(&mut s).unwrap(); let chars = &mut s.chars(); let source = match Square::from_notation(chars) { Ok(s) => s, Err(e) => return Err(e), }; let target = match Square::from_notation(chars) { Ok(s) => s, Err(e) => return Err(e), }; let moves = self.generate_pseudolegal_moves(self.color()); let mov = match moves.iter().find(|m| m.source == source && m.target == target) { Some(m) => *m, None => return Err(String::from("Move is not valid")), }; Ok(mov) } /// Color to move at this ply pub fn color(&self) -> Color { Color::from(self.ply as u8 % 2) } fn update_occupancy(&mut self) { self.occupancy = self.pieces.iter().fold(0, |acc, bitboard| acc | bitboard) } fn empty(&self) -> Bitboard { !self.occupancy } fn pieces_by_color(&self, color: Color) -> &[Bitboard] { match color { Color::White => &self.pieces[0..6], Color::Black => &self.pieces[6..12], } } fn color_occupancy(&self, color: Color) -> Bitboard { self.pieces_by_color(color).iter().fold(0, |acc, bitboard| acc | bitboard) } /// Compute and store zobrist hash of the current position /// https://www.chessprogramming.org/Zobrist_Hashing fn update_zobrist_hash(&mut self) { self.hash = 0; if self.color() == Color::Black { self.hash ^= match self.zobrist_seed.last() { Some(x) => x, None => panic!("Something is wrong with zobrist seed list"), }; } for (piece_id, bitboard) in self.pieces.iter().enumerate() { for square in bitboard.serialize() { self.hash ^= self.zobrist_seed[piece_id * 64 + square as usize]; } } for color in 0..2 { for castle_side in 0..2 { if self.castling_rights[color][castle_side] { self.hash ^= self.zobrist_seed[(12 * 64) + color * 2 + castle_side]; } } } match self.ep_target { Some(square) => { self.hash ^= self.zobrist_seed[(12 * 64 + 4) + square.file() as usize]; }, None => {}, } } pub fn print(&self) { println!(); for rank in (0..8).rev() { print!("{}|", rank + 1); for file in 0..8 { let index = rank * 8 + file; let position: Bitboard = 1 << index; let mut found = false; for (piece_type, piece_bitboard) in self.pieces.iter().enumerate() { if (piece_bitboard & position) > 0 { found = true; print!("{} ", PIECE_CHARS[piece_type]); } } if !found { print!(". "); } } println!(); } println!(" a b c d e f g h"); } pub fn generate_pseudolegal_moves(&self, color: Color) -> Vec { let mut moves = Vec::with_capacity(256); let capture_targets = self.color_occupancy(color.flip()) ^ match color { // Exclude opponent king because we can't capture it Color::White => self.pieces[PieceType::KingBlack as usize], Color::Black => self.pieces[PieceType::King as usize], }; let empty = self.empty(); let player_pieces = self.pieces_by_color(color); for (piece_id, piece) in player_pieces.iter().enumerate() { match PieceType::from(piece_id) { PieceType::Pawn => { for source in piece.serialize() { let ep_bitboard = match self.ep_target { Some(square) => { let rank = square.rank(); if (rank == 2 && color == Color::Black) || (rank == 5 && color == Color::White) { square.to_bitboard() } else { 0 } } None => 0, }; for target in (self.attacks.pawn[color as usize][source as usize] & capture_targets).serialize() { moves.push(Move { source, target, kind: MoveKind::Capture }); if target.rank() == 7 { for promo_type in [PieceType::Bishop, PieceType::Knight, PieceType::Rook, PieceType::Queen] { moves.push(Move { source, target, kind: MoveKind::Promotion(promo_type)}) } } else if target.rank() == 0 { for promo_type in [PieceType::BishopBlack, PieceType::KnightBlack, PieceType::RookBlack, PieceType::QueenBlack] { moves.push(Move { source, target, kind: MoveKind::Promotion(promo_type)}) } } }; for target in (self.attacks.pawn[color as usize][source as usize] & ep_bitboard).serialize() { moves.push(Move { source, target, kind: MoveKind::EnPassant }); } for target in (self.attacks.pawn_pushes[color as usize][source as usize] & empty).serialize() { moves.push(Move { source, target, kind: MoveKind::Quiet }); if target.rank() == 7 { for promo_type in [PieceType::Bishop, PieceType::Knight, PieceType::Rook, PieceType::Queen] { moves.push(Move { source, target, kind: MoveKind::Promotion(promo_type)}) } } else if target.rank() == 0 { for promo_type in [PieceType::BishopBlack, PieceType::KnightBlack, PieceType::RookBlack, PieceType::QueenBlack] { moves.push(Move { source, target, kind: MoveKind::Promotion(promo_type)}) } } }; } // Make sure no blocking piece is standing in front of the pawns // that are potential double-push sources let able_to_double_push_mask = match color { Color::White => empty >> 8, Color::Black => empty << 8, }; for source in (*piece & able_to_double_push_mask).serialize() { for target in (self.attacks.pawn_double_pushes[color as usize][source as usize] & empty).serialize() { moves.push(Move { source, target, kind: MoveKind::DoublePush }); }; } } PieceType::King => { for source in piece.serialize() { for target in (self.attacks.king[source as usize] & empty).serialize() { moves.push(Move { source, target, kind: MoveKind::Quiet }); }; for target in (self.attacks.king[source as usize] & capture_targets).serialize() { moves.push(Move { source, target, kind: MoveKind::Capture }); }; // Castling let king_home_position = match color { Color::White => Square::E1, Color::Black => Square::E8, }; if *piece == king_home_position.to_bitboard() { for rook_square in (player_pieces[PieceType::Rook as usize]).serialize() .iter() .filter(|rook_square| rook_square.rank() == king_home_position.rank()) { match rook_square.file() { 0 => { let all_empty = [ king_home_position.west_one(), king_home_position.west_one().west_one(), king_home_position.west_one().west_one().west_one(), ].iter().all(|square| empty & square.to_bitboard() > 0); let any_checks = [ king_home_position, king_home_position.west_one(), king_home_position.west_one().west_one(), ].iter().any(|square| self.is_square_attacked(*square, color.flip())); if all_empty && !any_checks && self.castling_rights[color as usize][CastlingSide::Queen as usize] { moves.push(Move { source: king_home_position, target: king_home_position.west_one().west_one(), kind: MoveKind::Castle, }) } }, 7 => { let all_empty = [ king_home_position.east_one(), king_home_position.east_one().east_one(), ].iter().all(|square| empty & square.to_bitboard() > 0); let any_checks = [ king_home_position, king_home_position.east_one(), king_home_position.east_one().east_one(), ].iter().any(|square| self.is_square_attacked(*square, color.flip())); if all_empty && !any_checks && self.castling_rights[color as usize][CastlingSide::King as usize] { moves.push(Move { source: king_home_position, target: king_home_position.east_one().east_one(), kind: MoveKind::Castle, }) } }, _ => {}, } } } } } PieceType::Knight => { for source in piece.serialize() { for target in (self.attacks.knight[source as usize] & empty).serialize() { moves.push(Move { source, target, kind: MoveKind::Quiet }); }; for target in (self.attacks.knight[source as usize] & capture_targets).serialize() { moves.push(Move { source, target, kind: MoveKind::Capture }); }; } } PieceType::Bishop => { for source in piece.serialize() { for target in (self.attacks.bishop(self.occupancy, source) & empty).serialize() { moves.push(Move { source, target, kind: MoveKind::Quiet }); }; for target in (self.attacks.bishop(self.occupancy, source) & capture_targets).serialize() { moves.push(Move { source, target, kind: MoveKind::Capture }); }; } } PieceType::Rook => { for source in piece.serialize() { for target in (self.attacks.rook(self.occupancy, source) & empty).serialize() { moves.push(Move { source, target, kind: MoveKind::Quiet }); }; } for source in piece.serialize() { for target in (self.attacks.rook(self.occupancy, source) & capture_targets).serialize() { moves.push(Move { source, target, kind: MoveKind::Capture }); }; } } PieceType::Queen => { for source in piece.serialize() { for target in (self.attacks.queen(self.occupancy, source) & empty).serialize() { moves.push(Move { source, target, kind: MoveKind::Quiet }); }; for target in (self.attacks.queen(self.occupancy, source) & capture_targets).serialize() { moves.push(Move { source, target, kind: MoveKind::Capture }); }; } } _ => todo!("Incorrect piece type") } } moves } /// Count pseudo-legal moves without actually generating them /// Also exclude all moves that put a piece under attack of a pawn - so called safe mobility pub fn mobility(&self, color: Color) -> f32 { let mut mobility = 0.; let opponent_occupancy = self.color_occupancy(color.flip()); let player_pieces = self.pieces_by_color(color); let opponent_pawns = match color { Color::Black => self.pieces[PieceType::Pawn as usize], Color::White => self.pieces[PieceType::PawnBlack as usize], }; let pawn_attacked_squares = opponent_pawns.serialize().iter().fold(0u64, |acc, square| { acc | self.attacks.pawn[color.flip() as usize][*square as usize] }); // Exclude squares controlled by enemy pawns from mobility let empty = self.empty() & !pawn_attacked_squares; for (piece_type, piece) in player_pieces.iter().enumerate() { match PieceType::from(piece_type) { PieceType::Pawn => { for source in piece.serialize() { let ep_bitboard = match self.ep_target { Some(square) => { let rank = square.rank(); if (rank == 2 && color == Color::Black) || (rank == 5 && color == Color::White) { square.to_bitboard() } else { 0 } } None => 0, }; mobility += (self.attacks.pawn[color as usize][source as usize] & (opponent_occupancy | ep_bitboard)).pop_count() as f32; mobility += (self.attacks.pawn_pushes[color as usize][source as usize] & empty).pop_count() as f32; } let able_to_double_push_mask = match color { Color::White => empty >> 8, Color::Black => empty << 8, }; for source in (*piece & able_to_double_push_mask).serialize() { mobility += (self.attacks.pawn_double_pushes[color as usize][source as usize] & empty).pop_count() as f32; } } PieceType::King => { for source in piece.serialize() { mobility += (self.attacks.king[source as usize] & (empty | opponent_occupancy)).pop_count() as f32; // Castling let king_home_position = match color { Color::White => Square::E1, Color::Black => Square::E8, }; if *piece == king_home_position.to_bitboard() { for rook_square in player_pieces[PieceType::Rook as usize] .serialize() .iter() .filter(|rook_square| rook_square.rank() == king_home_position.rank()) { match rook_square.file() { 0 => { let castle_line = [ king_home_position.west_one(), king_home_position.west_one().west_one(), ]; let all_empty = castle_line.iter().all(|square| empty & square.to_bitboard() > 0); let any_checks = castle_line.iter().any(|square| self.is_square_attacked(*square, color.flip())); if all_empty && !any_checks && self.castling_rights[color as usize][CastlingSide::Queen as usize] { mobility += 1.; } }, 7 => { let castle_line = [ king_home_position.east_one(), king_home_position.east_one().east_one(), ]; let all_empty = castle_line.iter().all(|square| empty & square.to_bitboard() > 0); let any_checks = castle_line.iter().any(|square| self.is_square_attacked(*square, color.flip())); if all_empty && !any_checks && self.castling_rights[color as usize][CastlingSide::King as usize] { mobility += 1.; } }, _ => {}, } } } } } PieceType::Knight => { for source in piece.serialize() { mobility += (self.attacks.knight[source as usize] & (empty | opponent_occupancy)).pop_count() as f32; } } PieceType::Bishop => { for source in piece.serialize() { mobility += (self.attacks.bishop(self.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32; } } PieceType::Rook => { for source in piece.serialize() { mobility += (self.attacks.rook(self.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32; } } PieceType::Queen => { // Do not account queen in mobility } incorrect_type => panic!("Incorrect piece type: {:?}", incorrect_type), } } mobility } fn piece_by_square(&self, square: Square) -> Option { let square_bb = square.to_bitboard(); self.pieces .iter() .enumerate() .find(|(_, bitboard)| *bitboard & square_bb > 0) .and_then(|(pt, _)| Some(PieceType::from(pt))) } /// *Blindlessly* apply a move without any validation /// Legality test should still be performed pub fn make_move(&mut self, mov: Move) -> Option { let move_source_bb = mov.source.to_bitboard(); let move_target_bb = mov.target.to_bitboard(); // Remove existing piece (if any) from target square let mut captured_piece = match self.piece_by_square(mov.target) { Some(target_piece) => { self.pieces[target_piece as usize] ^= move_target_bb; self.hash ^= self.zobrist_seed[(target_piece as usize) * 64 + mov.target as usize]; Some(target_piece) }, None => None, }; // En Passant captures diffirently if mov.kind == MoveKind::EnPassant { debug_assert!(captured_piece.is_none(), "No capture should be found at this point"); let captured_square = Square::from_coords(mov.source.rank(), mov.target.file()); captured_piece = match self.piece_by_square(captured_square) { Some(pawn_type) => { let captured_bb = captured_square.to_bitboard(); self.pieces[pawn_type as usize] ^= captured_bb; self.occupancy ^= captured_bb; self.hash ^= self.zobrist_seed[pawn_type as usize* 64 + captured_square as usize]; Some(pawn_type) }, None => panic!("Pawn captured by En Passant was not found"), } } // Move a piece from source square to target let source_piece = match self.piece_by_square(mov.source) { Some(source_piece) => { let source_id = source_piece as usize; match mov.kind { MoveKind::Promotion(promotion_piece) => { let promo_id = promotion_piece as usize; self.pieces[source_id] ^= move_source_bb; self.occupancy ^= move_source_bb; self.hash ^= self.zobrist_seed[source_id * 64 + mov.source as usize]; self.pieces[promo_id] |= move_target_bb; self.occupancy |= move_target_bb; self.hash ^= self.zobrist_seed[promo_id * 64 + mov.target as usize]; }, _ => { self.pieces[source_id] ^= move_source_bb; self.occupancy ^= move_source_bb; self.hash ^= self.zobrist_seed[source_id * 64 + mov.source as usize]; self.pieces[source_id] |= move_target_bb; self.occupancy |= move_target_bb; self.hash ^= self.zobrist_seed[source_id * 64 + mov.target as usize]; } } PieceType::from(source_piece) }, None => { self.print(); panic!("{:?} is malformed: source piece not found", mov); } }; // When castling, also move a rook if mov.kind == MoveKind::Castle { debug_assert!(mov.source.file() == 4, "Castle can only be done from E file"); let (rook_source_file, rook_target_file) = match mov.target.file() { 2 => (0, 3), 6 => (7, 5), _ => panic!("Malformed castle, target square invalid: {:?}", mov), }; let rook_source_square = Square::from_coords(mov.target.rank(), rook_source_file); let rook_source_bb = rook_source_square.to_bitboard(); let rook_target_square = Square::from_coords(mov.target.rank(), rook_target_file); let rook_target_bb = rook_target_square.to_bitboard(); match self.piece_by_square(rook_source_square) { Some(rook_type) => { let rook_id = rook_type as usize; self.pieces[rook_id] ^= rook_source_bb; self.occupancy ^= rook_source_bb; self.hash ^= self.zobrist_seed[rook_id * 64 + rook_source_square as usize]; self.pieces[rook_id] |= rook_target_bb; self.occupancy |= rook_target_bb; self.hash ^= self.zobrist_seed[rook_id * 64 + rook_target_square as usize]; }, None => panic!("Rook was not found when castling"), } } // Double push should set En Passant target square self.ep_target = if mov.kind == MoveKind::DoublePush { self.hash ^= self.zobrist_seed[64 * 12 + 4 + mov.source.file() as usize]; match mov.source.rank() { 1 => Some(mov.source.nort_one()), 6 => Some(mov.source.sout_one()), rank => panic!("Double-push was used from invalid rank({}) when trying to make {:?}", rank, mov), } } else { None }; // Withdraw castling rights when moving rooks or king let source_color = Color::from_piece(source_piece) as usize; match source_piece.without_color() { PieceType::King => { self.castling_rights[source_color][CastlingSide::King as usize] = false; self.castling_rights[source_color][CastlingSide::Queen as usize] = false; self.hash ^= self.zobrist_seed[64 * 12 + source_color * 2 + 0]; self.hash ^= self.zobrist_seed[64 * 12 + source_color * 2 + 1]; }, PieceType::Rook => { match mov.source.file() { 0 => { self.castling_rights[source_color][CastlingSide::Queen as usize] = false; self.hash ^= self.zobrist_seed[64 * 12 + source_color * 2 + CastlingSide::Queen as usize]; } 7 => { self.castling_rights[source_color][CastlingSide::King as usize] = false; self.hash ^= self.zobrist_seed[64 * 12 + source_color * 2 + CastlingSide::King as usize]; } _ => {}, } }, _ => {}, } self.ply += 1; self.hash ^= self.zobrist_seed[780]; captured_piece } /// Completely reverse make_move as if it never happened pub fn unmake_move( &mut self, mov: Move, captured_piece: Option, previous_ep_target: Option, previous_castling_rights: [[bool; 2]; 2], previous_hash: u64, ) { let move_source_bb = mov.source.to_bitboard(); let move_target_bb = mov.target.to_bitboard(); // Move a piece from target square back to source square match self.piece_by_square(mov.target) { Some(source_piece) => { match mov.kind { MoveKind::Promotion(promotion_piece) => { let promo_id = promotion_piece as usize; self.pieces[promo_id] ^= move_target_bb; self.occupancy ^= move_target_bb; let source_id = match Color::from_piece(promotion_piece) { Color::White => PieceType::Pawn, Color::Black => PieceType::PawnBlack, } as usize; self.pieces[source_id] |= move_source_bb; self.occupancy |= move_source_bb; } _ => { let source_id = source_piece as usize; self.pieces[source_id] ^= move_target_bb; self.occupancy ^= move_target_bb; self.pieces[source_id] |= move_source_bb; self.occupancy |= move_source_bb; } } }, None => panic!("Trying to unmake move which was not made: no piece was found on target square"), }; // If unmaking castle, also return rook to its place if mov.kind == MoveKind::Castle { let (rook_source_file, rook_target_file) = match mov.target.file() { 2 => (0, 3), 6 => (7, 5), _ => panic!("Malformed castle, target square invalid: {:?}", mov), }; let rook_source_bb = Square::from_coords(mov.target.rank(), rook_source_file).to_bitboard(); let rook_target_bb = Square::from_coords(mov.target.rank(), rook_target_file).to_bitboard(); match self.pieces .iter() .enumerate() .find(|(_, bitboard)| *bitboard & rook_target_bb > 0) { Some((rook_type, _)) => { self.pieces[rook_type] |= rook_source_bb; self.occupancy |= rook_source_bb; self.pieces[rook_type] ^= rook_target_bb; self.occupancy ^= rook_target_bb; }, None => panic!("Rook was not found when castling"), } } // Return captured piece to target square match captured_piece { Some(target_piece) => { match mov.kind { // Return pawn captured by En Passant pawn if needed MoveKind::EnPassant => { let original_dead_pawn_bb = Square::from_coords(mov.source.rank(), mov.target.file()).to_bitboard(); self.pieces[target_piece as usize] |= original_dead_pawn_bb; self.occupancy |= original_dead_pawn_bb; }, _ => { self.pieces[target_piece as usize] |= move_target_bb; self.occupancy |= move_target_bb; }, } }, None => {} } self.ep_target = previous_ep_target; self.castling_rights = previous_castling_rights; self.hash = previous_hash; self.ply -= 1; } fn is_square_attacked(&self, square: Square, attacker_color: Color) -> bool { for (piece_type, piece) in self.pieces_by_color(attacker_color).iter().enumerate() { match PieceType::from(piece_type) { PieceType::Queen => { if self.attacks.queen(self.occupancy, square) & piece > 0 { return true } } PieceType::Knight => { if self.attacks.knight[square as usize] & piece > 0 { return true } } PieceType::Bishop => { if self.attacks.bishop(self.occupancy, square) & piece > 0 { return true } } PieceType::Rook => { if self.attacks.rook(self.occupancy, square) & piece > 0 { return true } } PieceType::Pawn => { if self.attacks.pawn[attacker_color.flip() as usize][square as usize] & piece > 0 { return true } } PieceType::King => { if self.attacks.king[square as usize] & piece > 0 { return true } } _ => panic!("Unexpected piece type! Pieces by color should be considered white") } } false } fn is_king_in_check(&self, color: Color) -> bool { let king_bb = match color { Color::White => self.pieces[PieceType::King as usize], Color::Black => self.pieces[PieceType::KingBlack as usize], }; let square = king_bb.bitscan(); self.is_square_attacked(square, color.flip()) } } #[derive(Debug, Clone, Copy, PartialEq, num_enum::FromPrimitive)] #[repr(u8)] pub enum Color { #[default] White, Black, } impl Color { pub fn flip(&self) -> Self { match self { Self::White => Self::Black, Self::Black => Self::White, } } pub fn from_piece(piece: PieceType) -> Self { if (piece as u8) < 6 { Self::White } else { Self::Black } } } #[cfg(test)] mod tests { use super::*; use crate::{bitboard::BitboardFns, square::Square}; #[test] fn square_enum() { assert_eq!(Square::A1 as u8, 0); assert_eq!(Square::F1 as u8, 5); assert_eq!(Square::H8 as u8, 63); } #[test] fn new_from_default_fen() { let board = Board::new(); board.print(); board.empty().print("Empty squares"); assert_eq!(board.pieces[PieceType::Pawn as usize].pop_count(), 8); assert_eq!(board.pieces[PieceType::Knight as usize].pop_count(), 2); assert_eq!(board.pieces[PieceType::Bishop as usize].pop_count(), 2); assert_eq!(board.pieces[PieceType::Rook as usize].pop_count(), 2); assert_eq!(board.pieces[PieceType::Queen as usize].pop_count(), 1); assert_eq!(board.pieces[PieceType::King as usize].pop_count(), 1); assert_eq!(board.pieces[PieceType::PawnBlack as usize].pop_count(), 8); assert_eq!(board.pieces[PieceType::KnightBlack as usize].pop_count(), 2); assert_eq!(board.pieces[PieceType::BishopBlack as usize].pop_count(), 2); assert_eq!(board.pieces[PieceType::RookBlack as usize].pop_count(), 2); assert_eq!(board.pieces[PieceType::QueenBlack as usize].pop_count(), 1); assert_eq!(board.pieces[PieceType::KingBlack as usize].pop_count(), 1); assert_eq!(board.pieces[PieceType::King as usize].bitscan(), Square::E1); assert_eq!(board.pieces[PieceType::QueenBlack as usize].bitscan(), Square::D8); assert_eq!(board.occupancy.pop_count(), 32); assert_eq!(board.empty().pop_count(), 32); assert_eq!(board.color_occupancy(Color::White).pop_count(), 16); assert_eq!(board.color_occupancy(Color::Black).pop_count(), 16); } #[test] fn generate_pseudolegal_moves_starting_position() { let board = Board::new(); let moves = board.generate_pseudolegal_moves(Color::White); let black_moves = board.generate_pseudolegal_moves(Color::Black); assert_eq!(moves.len(), 20); assert_eq!(black_moves.len(), 20); for mov in moves { mov.print(); } } #[test] fn mobility() { let board = Board::new(); let white = board.mobility(Color::White); let black = board.mobility(Color::Black); assert_eq!(white, 20.); assert_eq!(black, 20.); } #[test] fn make_move() { let fen = String::from("q1b2k2/5p1p/4p1pb/pPPp4/3N4/3nPB2/P2QKnR1/1R6 w - - 0 25"); let mut board = Board::from_FEN(fen); let initial_board = board.clone(); board.print(); let black_move = Move { source: Square::F7, target: Square::F5, kind: MoveKind::Quiet }; println!("\n{:?}", black_move); match board.make_move(black_move) { Some(..) => panic!("No piece should be captured"), None => {}, }; board.print(); let hash = board.hash; board.update_zobrist_hash(); assert_eq!(hash, board.hash, "Hash should be correctly updated after move"); assert!(board.pieces[PieceType::PawnBlack as usize] & Square::F7.to_bitboard() == 0); assert!(board.pieces[PieceType::PawnBlack as usize] & Square::F5.to_bitboard() > 0); assert!(board.ply == 1); let white_move = Move { source: Square::D2, target: Square::A5, kind: MoveKind::Capture }; println!("\n{:?}", white_move); match board.make_move(white_move) { Some(captured) => assert!(captured == PieceType::PawnBlack), None => panic!("A piece should be captured"), }; board.print(); assert!(board.pieces[PieceType::PawnBlack as usize] & Square::A5.to_bitboard() == 0, "Target piece should be captured"); assert!(board.pieces[PieceType::Queen as usize] & Square::D2.to_bitboard() == 0); assert!(board.pieces[PieceType::Queen as usize] & Square::A5.to_bitboard() > 0); assert_ne!(board.occupancy, initial_board.occupancy, "Occupancy should change after make_move"); assert!(board.ply == 2); } #[test] fn unmake_move() { let fen = String::from("q1b2k2/5p1p/4p1pb/pPPp4/3N4/3nPB2/P2QKnR1/1R6 w - - 0 25"); let mut board = Board::from_FEN(fen); let initial_board = board.clone(); let mov = Move { source: Square::D2, target: Square::A5, kind: MoveKind::Capture }; board.print(); let captured_piece = board.make_move(mov); board.print(); board.unmake_move(mov, captured_piece, None, board.castling_rights, initial_board.hash); board.print(); assert_eq!(board, initial_board, "Board state after unmake_move should be the same as before make_move"); } #[test] fn is_square_attacked() { let board = Board::new(); assert_eq!(board.is_square_attacked(Square::E2, Color::White), true); assert_eq!(board.is_square_attacked(Square::E2, Color::Black), false); assert_eq!(board.is_square_attacked(Square::E4, Color::White), false); assert_eq!(board.is_square_attacked(Square::B6, Color::Black), true); } #[test] fn moved_king_castle() { let fen = String::from("4k2r/ppp1n3/8/4R1Pp/5P2/q1P5/P1P1BP2/1K1R4 b - - 2 22"); let mut board = Board::from_FEN(fen); board.ply += 1; // Shuffle kings around, returning to the same position board.make_move(Move { source: Square::E8, target: Square::F8, kind: MoveKind::Quiet }); board.make_move(Move { source: Square::B1, target: Square::A1, kind: MoveKind::Quiet }); board.make_move(Move { source: Square::F8, target: Square::E8, kind: MoveKind::Quiet }); board.make_move(Move { source: Square::A1, target: Square::B1, kind: MoveKind::Quiet }); let moves = board.generate_pseudolegal_moves(board.color()); let castle = moves.iter().find(|m| **m == Move { source: Square::E8, target: Square::G8, kind: MoveKind::Castle }); println!("{:?}", board.castling_rights); assert!(castle.is_none(), "Castle should not be allowed after king has moved"); } }