diff options
author | eug-vs <eugene@eug-vs.xyz> | 2023-02-21 14:19:34 +0300 |
---|---|---|
committer | eug-vs <eugene@eug-vs.xyz> | 2023-02-21 14:48:06 +0300 |
commit | 69f3c48fb99d96f3fbc4ab49f5fb6d1d8e90e270 (patch) | |
tree | 57c53013f2742c3d05762c7fdd066f66bd631e09 | |
parent | 5e9543dcc6a012aef73d342080bfec46690b5446 (diff) | |
download | chessnost-69f3c48fb99d96f3fbc4ab49f5fb6d1d8e90e270.tar.gz |
refactor: split Board module into submodules
-rw-r--r-- | src/attacks.rs | 2 | ||||
-rw-r--r-- | src/board/color.rs | 24 | ||||
-rw-r--r-- | src/board/engine.rs | 310 | ||||
-rw-r--r-- | src/board/io.rs | 58 | ||||
-rw-r--r-- | src/board/mod.rs | 431 | ||||
-rw-r--r-- | src/board/piece.rs | 36 | ||||
-rw-r--r-- | src/board/zobrist.rs | 8 | ||||
-rw-r--r-- | src/main.rs | 3 | ||||
-rw-r--r-- | src/moves.rs | 4 |
9 files changed, 423 insertions, 453 deletions
diff --git a/src/attacks.rs b/src/attacks.rs index d32ac64..376e666 100644 --- a/src/attacks.rs +++ b/src/attacks.rs @@ -1,4 +1,4 @@ -use crate::{bitboard::Bitboard, board::Color, square::Square}; +use crate::{bitboard::Bitboard, square::Square, board::color::Color}; static NOT_A_FILE: Bitboard = 0xFEFEFEFEFEFEFEFE; static NOT_B_FILE: Bitboard = 0xFDFDFDFDFDFDFDFD; diff --git a/src/board/color.rs b/src/board/color.rs new file mode 100644 index 0000000..80d5b4e --- /dev/null +++ b/src/board/color.rs @@ -0,0 +1,24 @@ +use super::piece::Piece; + +#[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: Piece) -> Self { + if (piece as u8) < 6 { + Self::White + } else { + Self::Black + } + } +} diff --git a/src/board/engine.rs b/src/board/engine.rs index 5a2b2bf..2dc93eb 100644 --- a/src/board/engine.rs +++ b/src/board/engine.rs @@ -155,19 +155,303 @@ impl Board { result } + fn ep_bitboard(&self) -> Bitboard { + let color = self.color(); + 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, + } + } + + pub fn generate_pseudolegal_moves(&self) -> Vec<Move> { + let mut result = Vec::new(); + result.append(&mut self.generate_moves_core(true)); + result.append(&mut self.generate_moves_core(false)); + result + } + + pub fn generate_moves_core(&self, tactical_only: bool) -> Vec<Move> { + let color = self.color(); + let player_pieces = self.pieces_by_color(color); + let mut moves = Vec::with_capacity(256); + let empty = self.empty(); + + let targets = if tactical_only { + self.color_occupancy(color.flip()) ^ match color { + // Exclude opponent king because we can't capture it + Color::White => self.piece_sets[Piece::KingBlack as usize], + Color::Black => self.piece_sets[Piece::King as usize], + } + } else { + empty + }; + + let kind = if tactical_only { + MoveKind::Capture + } else { + MoveKind::Quiet + }; + + for (piece_id, piece) in player_pieces.iter().enumerate() { + for source in piece.serialize() { + let piece_type = Piece::from(piece_id); + let move_targets = match piece_type { + Piece::Bishop => self.attacks.bishop(self.occupancy, source), + Piece::Rook => self.attacks.rook(self.occupancy, source), + Piece::Queen => self.attacks.queen(self.occupancy, source), + Piece::Knight => self.attacks.knight[source as usize], + Piece::King => self.attacks.king[source as usize], + Piece::Pawn => if tactical_only { + self.attacks.pawn[color as usize][source as usize] + } else { + self.attacks.pawn_pushes[color as usize][source as usize] + }, + _ => panic!("Incorrect piece type"), + }; + + // Generalized attack/move pattern for all pieces + for target in (move_targets & targets).serialize() { + moves.push(Move { source, target, kind }); + + // Promotions + if piece_type == Piece::Pawn { + if target.rank() == 7 { + for promo_type in [Piece::Bishop, Piece::Knight, Piece::Rook, Piece::Queen] { + moves.push(Move { source, target, kind: MoveKind::Promotion(promo_type)}) + } + } else if target.rank() == 0 { + for promo_type in [Piece::BishopBlack, Piece::KnightBlack, Piece::RookBlack, Piece::QueenBlack] { + moves.push(Move { source, target, kind: MoveKind::Promotion(promo_type)}) + } + } + } + } + + // En Passant + if piece_type == Piece::Pawn && tactical_only { + for target in (move_targets & self.ep_bitboard()).serialize() { + moves.push(Move { source, target, kind: MoveKind::EnPassant }); + } + } + } + } + + if !tactical_only { + // Double Pushes + // 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 (player_pieces[Piece::Pawn as usize] & 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 }); + }; + } + + // Castling + let king_home_position = match color { + Color::White => Square::E1, + Color::Black => Square::E8, + }; + if player_pieces[Piece::King as usize].bitscan() == king_home_position { + for rook_square in player_pieces[Piece::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, + }) + } + }, + _ => {}, + } + } + } + } + return 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.piece_sets[Piece::Pawn as usize], + Color::White => self.piece_sets[Piece::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 Piece::from(piece_type) { + Piece::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; + } + } + Piece::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[Piece::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.; + } + }, + _ => {}, + } + } + } + } + } + Piece::Knight => { + for source in piece.serialize() { + mobility += (self.attacks.knight[source as usize] & (empty | opponent_occupancy)).pop_count() as f32; + } + } + Piece::Bishop => { + for source in piece.serialize() { + mobility += (self.attacks.bishop(self.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32; + } + } + Piece::Rook => { + for source in piece.serialize() { + mobility += (self.attacks.rook(self.occupancy, source) & (empty | opponent_occupancy)).pop_count() as f32; + } + } + Piece::Queen => { + // Do not account queen in mobility + } + incorrect_type => panic!("Incorrect piece type: {:?}", incorrect_type), + } + } + + mobility + } + + /// Count player pieces' material, giving bonus for pieces standing well pub fn material(&self, color: Color) -> f32 { let mut material = 0f32; for (piece_index, bitboard) in self.pieces_by_color(color).iter().enumerate() { - let piece_type = PieceType::from(piece_index); + let piece_type = Piece::from(piece_index); let bonus_table = match piece_type { - PieceType::Pawn => PAWN_BONUS, - PieceType::Knight => KNIGHT_BONUS, - PieceType::Bishop => BISHOP_BONUS, - PieceType::Rook => ROOK_BONUS, - PieceType::King => KING_BONUS, - PieceType::Queen => QUEEN_BONUS, + Piece::Pawn => PAWN_BONUS, + Piece::Knight => KNIGHT_BONUS, + Piece::Bishop => BISHOP_BONUS, + Piece::Rook => ROOK_BONUS, + Piece::King => KING_BONUS, + Piece::Queen => QUEEN_BONUS, _ => panic!("Unreachable") }; @@ -189,8 +473,8 @@ impl Board { let mut result = 0.0; let pawns = match color { - Color::White => self.piece_sets[PieceType::Pawn as usize], - Color::Black => self.piece_sets[PieceType::PawnBlack as usize], + Color::White => self.piece_sets[Piece::Pawn as usize], + Color::Black => self.piece_sets[Piece::PawnBlack as usize], }; for file in 0..8 { @@ -225,18 +509,18 @@ impl Board { let mut result = 0.0; let king_square = match color { - Color::White => self.piece_sets[PieceType::King as usize], - Color::Black => self.piece_sets[PieceType::KingBlack as usize], + Color::White => self.piece_sets[Piece::King as usize], + Color::Black => self.piece_sets[Piece::KingBlack as usize], }.bitscan(); for (piece_type, bitboard) in self.pieces_by_color(color.flip()).iter().enumerate() { - if piece_type != PieceType::King as usize && piece_type != PieceType::Pawn as usize { + if piece_type != Piece::King as usize && piece_type != Piece::Pawn as usize { for square in bitboard.serialize() { let distance = (king_square.rank() as f32 - square.rank() as f32).abs() + (king_square.file() as f32 - square.file() as f32).abs(); - result += distance / PieceType::from(piece_type).static_eval(); + result += distance / Piece::from(piece_type).static_eval(); } } } diff --git a/src/board/io.rs b/src/board/io.rs index b5cd95e..30d487f 100644 --- a/src/board/io.rs +++ b/src/board/io.rs @@ -2,7 +2,7 @@ use std::io::{stdin, stdout, Write}; use crate::{bitboard::Bitboard, attacks::Attacks, moves::Move, square::Square}; -use super::{Board, PieceType, ttable::TTABLE_SIZE, zobrist::Zobrist}; +use super::{Board, Piece, ttable::TTABLE_SIZE, zobrist::Zobrist}; const PIECE_CHARS: [&str; 12] = [ "♟︎", "♞", "♝", "♜", "♛", "♚", @@ -65,18 +65,18 @@ impl IO for Board { } } else { match character { - 'P' => piece_sets[PieceType::Pawn as usize] |= position, - 'N' => piece_sets[PieceType::Knight as usize] |= position, - 'B' => piece_sets[PieceType::Bishop as usize] |= position, - 'R' => piece_sets[PieceType::Rook as usize] |= position, - 'Q' => piece_sets[PieceType::Queen as usize] |= position, - 'K' => piece_sets[PieceType::King as usize] |= position, - 'p' => piece_sets[PieceType::PawnBlack as usize] |= position, - 'n' => piece_sets[PieceType::KnightBlack as usize] |= position, - 'b' => piece_sets[PieceType::BishopBlack as usize] |= position, - 'r' => piece_sets[PieceType::RookBlack as usize] |= position, - 'q' => piece_sets[PieceType::QueenBlack as usize] |= position, - 'k' => piece_sets[PieceType::KingBlack as usize] |= position, + 'P' => piece_sets[Piece::Pawn as usize] |= position, + 'N' => piece_sets[Piece::Knight as usize] |= position, + 'B' => piece_sets[Piece::Bishop as usize] |= position, + 'R' => piece_sets[Piece::Rook as usize] |= position, + 'Q' => piece_sets[Piece::Queen as usize] |= position, + 'K' => piece_sets[Piece::King as usize] |= position, + 'p' => piece_sets[Piece::PawnBlack as usize] |= position, + 'n' => piece_sets[Piece::KnightBlack as usize] |= position, + 'b' => piece_sets[Piece::BishopBlack as usize] |= position, + 'r' => piece_sets[Piece::RookBlack as usize] |= position, + 'q' => piece_sets[Piece::QueenBlack as usize] |= position, + 'k' => piece_sets[Piece::KingBlack as usize] |= position, '/' => { rank -= 1; file = -1; // So it becomes 0 @@ -151,22 +151,22 @@ mod tests { board.print(); board.empty().print("Empty squares"); - assert_eq!(board.piece_sets[PieceType::Pawn as usize].pop_count(), 8); - assert_eq!(board.piece_sets[PieceType::Knight as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::Bishop as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::Rook as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::Queen as usize].pop_count(), 1); - assert_eq!(board.piece_sets[PieceType::King as usize].pop_count(), 1); - - assert_eq!(board.piece_sets[PieceType::PawnBlack as usize].pop_count(), 8); - assert_eq!(board.piece_sets[PieceType::KnightBlack as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::BishopBlack as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::RookBlack as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::QueenBlack as usize].pop_count(), 1); - assert_eq!(board.piece_sets[PieceType::KingBlack as usize].pop_count(), 1); - - assert_eq!(board.piece_sets[PieceType::King as usize].bitscan(), Square::E1); - assert_eq!(board.piece_sets[PieceType::QueenBlack as usize].bitscan(), Square::D8); + assert_eq!(board.piece_sets[Piece::Pawn as usize].pop_count(), 8); + assert_eq!(board.piece_sets[Piece::Knight as usize].pop_count(), 2); + assert_eq!(board.piece_sets[Piece::Bishop as usize].pop_count(), 2); + assert_eq!(board.piece_sets[Piece::Rook as usize].pop_count(), 2); + assert_eq!(board.piece_sets[Piece::Queen as usize].pop_count(), 1); + assert_eq!(board.piece_sets[Piece::King as usize].pop_count(), 1); + + assert_eq!(board.piece_sets[Piece::PawnBlack as usize].pop_count(), 8); + assert_eq!(board.piece_sets[Piece::KnightBlack as usize].pop_count(), 2); + assert_eq!(board.piece_sets[Piece::BishopBlack as usize].pop_count(), 2); + assert_eq!(board.piece_sets[Piece::RookBlack as usize].pop_count(), 2); + assert_eq!(board.piece_sets[Piece::QueenBlack as usize].pop_count(), 1); + assert_eq!(board.piece_sets[Piece::KingBlack as usize].pop_count(), 1); + + assert_eq!(board.piece_sets[Piece::King as usize].bitscan(), Square::E1); + assert_eq!(board.piece_sets[Piece::QueenBlack as usize].bitscan(), Square::D8); assert_eq!(board.occupancy.pop_count(), 32); assert_eq!(board.empty().pop_count(), 32); diff --git a/src/board/mod.rs b/src/board/mod.rs index 5317cc2..e6a6446 100644 --- a/src/board/mod.rs +++ b/src/board/mod.rs @@ -1,7 +1,9 @@ use crate::{bitboard::{Bitboard, BitboardFns}, moves::{Move, MoveKind}, attacks::Attacks, square::Square, board::io::IO}; -use self::{ttable::{TranspositionTable, TTABLE_SIZE}, zobrist::{ZobristSeed, Zobrist}}; +use self::{ttable::{TranspositionTable, TTABLE_SIZE}, zobrist::{ZobristSeed, Zobrist}, piece::Piece, color::Color}; pub mod io; +pub mod color; +pub mod piece; mod zobrist; mod engine; mod ttable; @@ -32,45 +34,6 @@ pub struct Board { 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"), - } - } -} - - - impl Board { pub fn new() -> Self { let default_fen = String::from("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); @@ -101,303 +64,18 @@ impl Board { self.pieces_by_color(color).iter().fold(0, |acc, bitboard| acc | bitboard) } - - - fn ep_bitboard(&self) -> Bitboard { - let color = self.color(); - 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, - } - } - - pub fn generate_pseudolegal_moves(&self) -> Vec<Move> { - let mut result = Vec::new(); - result.append(&mut self.generate_moves_core(true)); - result.append(&mut self.generate_moves_core(false)); - result - } - - pub fn generate_moves_core(&self, tactical_only: bool) -> Vec<Move> { - let color = self.color(); - let player_pieces = self.pieces_by_color(color); - let mut moves = Vec::with_capacity(256); - let empty = self.empty(); - - let targets = if tactical_only { - self.color_occupancy(color.flip()) ^ match color { - // Exclude opponent king because we can't capture it - Color::White => self.piece_sets[PieceType::KingBlack as usize], - Color::Black => self.piece_sets[PieceType::King as usize], - } - } else { - empty - }; - - let kind = if tactical_only { - MoveKind::Capture - } else { - MoveKind::Quiet - }; - - for (piece_id, piece) in player_pieces.iter().enumerate() { - for source in piece.serialize() { - let piece_type = PieceType::from(piece_id); - let move_targets = match piece_type { - PieceType::Bishop => self.attacks.bishop(self.occupancy, source), - PieceType::Rook => self.attacks.rook(self.occupancy, source), - PieceType::Queen => self.attacks.queen(self.occupancy, source), - PieceType::Knight => self.attacks.knight[source as usize], - PieceType::King => self.attacks.king[source as usize], - PieceType::Pawn => if tactical_only { - self.attacks.pawn[color as usize][source as usize] - } else { - self.attacks.pawn_pushes[color as usize][source as usize] - }, - _ => panic!("Incorrect piece type"), - }; - - // Generalized attack/move pattern for all pieces - for target in (move_targets & targets).serialize() { - moves.push(Move { source, target, kind }); - - // Promotions - if piece_type == PieceType::Pawn { - 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)}) - } - } - } - } - - // En Passant - if piece_type == PieceType::Pawn && tactical_only { - for target in (move_targets & self.ep_bitboard()).serialize() { - moves.push(Move { source, target, kind: MoveKind::EnPassant }); - } - } - } - } - - if !tactical_only { - // Double Pushes - // 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 (player_pieces[PieceType::Pawn as usize] & 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 }); - }; - } - - // Castling - let king_home_position = match color { - Color::White => Square::E1, - Color::Black => Square::E8, - }; - if player_pieces[PieceType::King as usize].bitscan() == king_home_position { - 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, - }) - } - }, - _ => {}, - } - } - } - } - return 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.piece_sets[PieceType::Pawn as usize], - Color::White => self.piece_sets[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<PieceType> { + fn piece_by_square(&self, square: Square) -> Option<Piece> { let square_bb = square.to_bitboard(); self.piece_sets .iter() .enumerate() .find(|(_, bitboard)| *bitboard & square_bb > 0) - .and_then(|(pt, _)| Some(PieceType::from(pt))) + .and_then(|(pt, _)| Some(Piece::from(pt))) } /// *Blindlessly* apply a move without any validation /// Legality test should still be performed - pub fn make_move(&mut self, mov: Move) -> Option<PieceType> { + pub fn make_move(&mut self, mov: Move) -> Option<Piece> { let move_source_bb = mov.source.to_bitboard(); let move_target_bb = mov.target.to_bitboard(); @@ -452,7 +130,7 @@ impl Board { self.zobrist_toggle_piece(source_piece, mov.target); } } - PieceType::from(source_piece) + Piece::from(source_piece) }, None => { self.print(); @@ -502,13 +180,13 @@ impl Board { // Withdraw castling rights when moving rooks or king let source_color = Color::from_piece(source_piece); match source_piece.without_color() { - PieceType::King => { + Piece::King => { self.castling_rights[source_color as usize][CastlingSide::King as usize] = false; self.castling_rights[source_color as usize][CastlingSide::Queen as usize] = false; self.zobrist_toggle_castling_right(source_color, CastlingSide::King); self.zobrist_toggle_castling_right(source_color, CastlingSide::Queen); }, - PieceType::Rook => { + Piece::Rook => { match mov.source.file() { 0 => { self.castling_rights[source_color as usize][CastlingSide::Queen as usize] = false; @@ -534,7 +212,7 @@ impl Board { pub fn unmake_move( &mut self, mov: Move, - captured_piece: Option<PieceType>, + captured_piece: Option<Piece>, previous_ep_target: Option<Square>, previous_castling_rights: [[bool; 2]; 2], previous_hash: u64, @@ -552,8 +230,8 @@ impl Board { self.occupancy ^= move_target_bb; let source_id = match Color::from_piece(promotion_piece) { - Color::White => PieceType::Pawn, - Color::Black => PieceType::PawnBlack, + Color::White => Piece::Pawn, + Color::Black => Piece::PawnBlack, } as usize; self.piece_sets[source_id] |= move_source_bb; @@ -626,33 +304,33 @@ impl Board { 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 => { + match Piece::from(piece_type) { + Piece::Queen => { if self.attacks.queen(self.occupancy, square) & piece > 0 { return true } } - PieceType::Knight => { + Piece::Knight => { if self.attacks.knight[square as usize] & piece > 0 { return true } } - PieceType::Bishop => { + Piece::Bishop => { if self.attacks.bishop(self.occupancy, square) & piece > 0 { return true } } - PieceType::Rook => { + Piece::Rook => { if self.attacks.rook(self.occupancy, square) & piece > 0 { return true } } - PieceType::Pawn => { + Piece::Pawn => { if self.attacks.pawn[attacker_color.flip() as usize][square as usize] & piece > 0 { return true } } - PieceType::King => { + Piece::King => { if self.attacks.king[square as usize] & piece > 0 { return true } @@ -665,8 +343,8 @@ impl Board { fn is_king_in_check(&self, color: Color) -> bool { let king_bb = match color { - Color::White => self.piece_sets[PieceType::King as usize], - Color::Black => self.piece_sets[PieceType::KingBlack as usize], + Color::White => self.piece_sets[Piece::King as usize], + Color::Black => self.piece_sets[Piece::KingBlack as usize], }; let square = king_bb.bitscan(); self.is_square_attacked(square, color.flip()) @@ -674,33 +352,10 @@ impl Board { } -#[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, board::zobrist::Zobrist}; + use crate::{square::Square, board::zobrist::Zobrist}; #[test] fn square_enum() { @@ -710,36 +365,6 @@ mod tests { } #[test] - fn new_from_default_fen() { - let board = Board::new(); - - board.print(); - board.empty().print("Empty squares"); - - assert_eq!(board.piece_sets[PieceType::Pawn as usize].pop_count(), 8); - assert_eq!(board.piece_sets[PieceType::Knight as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::Bishop as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::Rook as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::Queen as usize].pop_count(), 1); - assert_eq!(board.piece_sets[PieceType::King as usize].pop_count(), 1); - - assert_eq!(board.piece_sets[PieceType::PawnBlack as usize].pop_count(), 8); - assert_eq!(board.piece_sets[PieceType::KnightBlack as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::BishopBlack as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::RookBlack as usize].pop_count(), 2); - assert_eq!(board.piece_sets[PieceType::QueenBlack as usize].pop_count(), 1); - assert_eq!(board.piece_sets[PieceType::KingBlack as usize].pop_count(), 1); - - assert_eq!(board.piece_sets[PieceType::King as usize].bitscan(), Square::E1); - assert_eq!(board.piece_sets[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 mut board = Board::new(); let moves = board.generate_pseudolegal_moves(); @@ -785,23 +410,23 @@ mod tests { board.compute_hash(); assert_eq!(hash, board.hash, "Hash should be correctly updated after move"); - assert!(board.piece_sets[PieceType::PawnBlack as usize] & Square::F7.to_bitboard() == 0); - assert!(board.piece_sets[PieceType::PawnBlack as usize] & Square::F5.to_bitboard() > 0); + assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::F7.to_bitboard() == 0); + assert!(board.piece_sets[Piece::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), + Some(captured) => assert!(captured == Piece::PawnBlack), None => panic!("A piece should be captured"), }; board.print(); - assert!(board.piece_sets[PieceType::PawnBlack as usize] & Square::A5.to_bitboard() == 0, "Target piece should be captured"); - assert!(board.piece_sets[PieceType::Queen as usize] & Square::D2.to_bitboard() == 0); - assert!(board.piece_sets[PieceType::Queen as usize] & Square::A5.to_bitboard() > 0); + assert!(board.piece_sets[Piece::PawnBlack as usize] & Square::A5.to_bitboard() == 0, "Target piece should be captured"); + assert!(board.piece_sets[Piece::Queen as usize] & Square::D2.to_bitboard() == 0); + assert!(board.piece_sets[Piece::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); } diff --git a/src/board/piece.rs b/src/board/piece.rs new file mode 100644 index 0000000..1889e94 --- /dev/null +++ b/src/board/piece.rs @@ -0,0 +1,36 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, num_enum::FromPrimitive)] +#[repr(usize)] +pub enum Piece { + #[default] + Pawn, + Knight, + Bishop, + Rook, + Queen, + King, + PawnBlack, + KnightBlack, + BishopBlack, + RookBlack, + QueenBlack, + KingBlack, +} + +impl Piece { + 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() { + Piece::Pawn => 1.0, + Piece::Bishop => 3.3, + Piece::Knight => 3.2, + Piece::Rook => 5.0, + Piece::Queen => 9.0, + Piece::King => 0., + _ => panic!("Piece should be without color"), + } + } +} diff --git a/src/board/zobrist.rs b/src/board/zobrist.rs index 8ea0038..5286a73 100644 --- a/src/board/zobrist.rs +++ b/src/board/zobrist.rs @@ -2,7 +2,7 @@ use rand::{rngs::StdRng,SeedableRng,Rng}; use crate::{square::Square, bitboard::BitboardFns}; -use super::{Board, Color, PieceType, CastlingSide}; +use super::{Board, Color, Piece, CastlingSide}; const TOTAL_PIECES: usize = 12; @@ -20,7 +20,7 @@ pub trait Zobrist { /// https://www.chessprogramming.org/Zobrist_Hashing fn compute_hash(&mut self) -> (); - fn zobrist_toggle_piece(&mut self, piece_type: PieceType, square: Square) -> (); + fn zobrist_toggle_piece(&mut self, piece_type: Piece, square: Square) -> (); fn zobrist_toggle_castling_right(&mut self, color: Color, side: CastlingSide) -> (); fn zobrist_toggle_ep_square(&mut self, ep_square: Square) -> (); fn zobrist_toggle_color(&mut self) -> (); @@ -37,7 +37,7 @@ impl Zobrist for Board { for piece_id in 0..self.piece_sets.len() { for square in self.piece_sets[piece_id].serialize() { - self.zobrist_toggle_piece(PieceType::from(piece_id), square); + self.zobrist_toggle_piece(Piece::from(piece_id), square); } } for color in [Color::White, Color::Black] { @@ -58,7 +58,7 @@ impl Zobrist for Board { } } - fn zobrist_toggle_piece(&mut self, piece_type: PieceType, square: Square) -> () { + fn zobrist_toggle_piece(&mut self, piece_type: Piece, square: Square) -> () { self.hash ^= self.zobrist_seed[piece_type as usize * TOTAL_SQUARES + square as usize]; } diff --git a/src/main.rs b/src/main.rs index 51bb57a..7788b73 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,8 +1,9 @@ use std::time::{Duration, Instant}; use std::env; +use chessnost::board::color::Color; use chessnost::board::io::IO; -use chessnost::board::{Board, Color}; +use chessnost::board::Board; fn opponent_move(board: &mut Board) { let mov = match board.read_move() { diff --git a/src/moves.rs b/src/moves.rs index 58fb979..858fe43 100644 --- a/src/moves.rs +++ b/src/moves.rs @@ -1,4 +1,4 @@ -use crate::{square::Square, bitboard::BitboardFns, board::PieceType}; +use crate::{square::Square, bitboard::BitboardFns, board::piece::Piece}; #[derive(Debug, Clone, PartialEq, Eq, Copy)] pub enum MoveKind { @@ -7,7 +7,7 @@ pub enum MoveKind { Castle, EnPassant, DoublePush, - Promotion(PieceType), + Promotion(Piece), } #[derive(Debug, Clone, Copy, PartialEq, Eq)] |