├── src ├── traits.rs ├── main.rs ├── zobrist.rs ├── types.rs ├── move_list.rs ├── moov.rs ├── perft.rs ├── piece.rs ├── magics.rs ├── nnue.rs ├── tt.rs ├── search_master.rs ├── move_sorting.rs ├── attacks.rs ├── uci.rs ├── square.rs ├── timer.rs ├── bitboard.rs ├── search.rs └── board.rs ├── .github └── workflows │ ├── rust.yml │ └── release.yml ├── Cargo.toml ├── README.md └── LICENSE /src/traits.rs: -------------------------------------------------------------------------------- 1 | use super::piece::*; 2 | 3 | pub trait Mirror: Copy + Sized { 4 | fn mirror(&self) -> Self; 5 | 6 | fn relative(&self, color: Color) -> Self { 7 | match color { 8 | Color::White => *self, 9 | Color::Black => self.mirror(), 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /.github/workflows/rust.yml: -------------------------------------------------------------------------------- 1 | name: Rust 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | env: 10 | CARGO_TERM_COLOR: always 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Build 20 | run: cargo build --verbose 21 | - name: Run tests 22 | run: cargo test --verbose --release 23 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | mod bitboard; 3 | mod attacks; 4 | mod board; 5 | mod magics; 6 | mod moov; 7 | mod move_list; 8 | mod move_sorting; 9 | mod nnue; 10 | mod nnue_weights; 11 | mod perft; 12 | mod piece; 13 | mod search; 14 | mod search_master; 15 | mod square; 16 | mod timer; 17 | mod traits; 18 | mod tt; 19 | mod types; 20 | mod uci; 21 | mod zobrist; 22 | 23 | fn main() { 24 | let uci = uci::UCI::new(); 25 | uci.run(); 26 | } 27 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "Weiawaga" 3 | authors = ["Heiaha"] 4 | repository = "https://github.com/Heiaha/Weiawaga" 5 | version = "6.0.0" 6 | edition = "2024" 7 | readme = "README.md" 8 | rust-version = "1.85.0" 9 | 10 | [dependencies] 11 | arrayvec = "0.7.6" 12 | regex = "1.11.1" 13 | rand = "0.9.1" 14 | wide = "0.7.33" 15 | 16 | [profile.release] 17 | opt-level = 3 18 | debug = false 19 | rpath = false 20 | debug-assertions = false 21 | codegen-units = 1 22 | lto = true 23 | panic = "abort" 24 | -------------------------------------------------------------------------------- /src/zobrist.rs: -------------------------------------------------------------------------------- 1 | use super::piece::*; 2 | use super::square::*; 3 | use super::types::*; 4 | use std::sync::LazyLock; 5 | 6 | use rand::rngs::StdRng; 7 | use rand::{RngCore, SeedableRng}; 8 | 9 | pub static ZOBRIST: LazyLock = LazyLock::new(Hasher::new); 10 | 11 | #[derive(Clone)] 12 | pub struct Hasher { 13 | zobrist_table: PieceMap>, 14 | zobrist_ep: FileMap, 15 | zobrist_color: ColorMap, 16 | } 17 | 18 | impl Hasher { 19 | pub fn new() -> Self { 20 | let mut zobrist_table = PieceMap::new([SQMap::new([0; SQ::N_SQUARES]); Piece::N_PIECES]); 21 | let mut zobrist_ep = FileMap::new([0; File::N_FILES]); 22 | 23 | let mut rng = StdRng::seed_from_u64(1070372); 24 | 25 | zobrist_table 26 | .iter_mut() 27 | .flatten() 28 | .for_each(|hash| *hash = rng.next_u64()); 29 | 30 | zobrist_ep 31 | .iter_mut() 32 | .for_each(|hash| *hash = rng.next_u64()); 33 | 34 | let zobrist_color = ColorMap::new([rng.next_u64(), rng.next_u64()]); 35 | 36 | Self { 37 | zobrist_table, 38 | zobrist_ep, 39 | zobrist_color, 40 | } 41 | } 42 | 43 | pub fn move_hash(&self, pc: Piece, from_sq: SQ, to_sq: SQ) -> u64 { 44 | self.zobrist_table[pc][from_sq] ^ self.zobrist_table[pc][to_sq] 45 | } 46 | 47 | pub fn update_hash(&self, pc: Piece, sq: SQ) -> u64 { 48 | self.zobrist_table[pc][sq] 49 | } 50 | 51 | pub fn ep_hash(&self, epsq: SQ) -> u64 { 52 | self.zobrist_ep[epsq.file()] 53 | } 54 | 55 | pub fn color_hash(&self, color: Color) -> u64 { 56 | self.zobrist_color[color] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

Weiawaga

2 |

3 | 4 |

5 | 6 | A UCI chess engine written in Rust. If you find this repository, come play me on lichess! 7 | 8 | https://lichess.org/@/Weiawaga 9 | 10 | ## Features 11 | 12 | - Board representation 13 | - [Bitboards](https://en.wikipedia.org/wiki/Bitboard) 14 | - Move generation 15 | - [Fancy magic bitboard hashing](https://www.chessprogramming.org/Magic_Bitboards#Fancy) 16 | - Search 17 | - [Principal variation search](https://www.chessprogramming.org/Principal_Variation_Search) 18 | - [Lazy SMP](https://www.chessprogramming.org/Lazy_SMP) 19 | - [Iterative deepening](https://en.wikipedia.org/wiki/Iterative_deepening_depth-first_search) 20 | - [Quiescence search](https://en.wikipedia.org/wiki/Quiescence_search) 21 | - [Aspiration windows](https://www.chessprogramming.org/Aspiration_Windows) 22 | - [Reverse futility pruning](https://www.chessprogramming.org/Reverse_Futility_Pruning) 23 | - [Null move pruning](https://www.chessprogramming.org/Null_Move_Pruning) 24 | - [Check extensions](https://www.chessprogramming.org/Check_Extensions) 25 | - [NNUE](https://www.chessprogramming.org/NNUE) evaluation 26 | - Move ordering 27 | - [Hash move](https://www.chessprogramming.org/Hash_Move) 28 | - [Static exchange evaluation](https://www.chessprogramming.org/Static_Exchange_Evaluation) 29 | - [Killer heuristic](https://www.chessprogramming.org/Killer_Heuristic) 30 | - [History heuristic](https://www.chessprogramming.org/History_Heuristic) 31 | - Other 32 | - [Zobrist hashing](https://www.chessprogramming.org/Zobrist_Hashing) / [Transposition table](https://en.wikipedia.org/wiki/Transposition_table) 33 | 34 | Move generation inspired by [surge](https://github.com/nkarve/surge). A previous version of this engine written in Java can be found [here](https://github.com/Heiaha/WeiawagaJ). 35 | The NNUE training code can be found [here](https://github.com/Heiaha/Mimir). 36 | 37 | **[What's a Weiawaga?](https://www.youtube.com/watch?v=7lRpoYGzx0o)** -------------------------------------------------------------------------------- /src/types.rs: -------------------------------------------------------------------------------- 1 | use super::piece::*; 2 | use super::square::*; 3 | use std::ops::{Index, IndexMut}; 4 | use std::slice::{Iter, IterMut}; 5 | 6 | pub type ColorMap = EnumMap; 7 | pub type PieceMap = EnumMap; 8 | pub type PieceTypeMap = EnumMap; 9 | pub type SQMap = EnumMap; 10 | pub type FileMap = EnumMap; 11 | pub type RankMap = EnumMap; 12 | pub type DiagonalMap = EnumMap; 13 | 14 | #[derive(Copy, Clone)] 15 | pub struct EnumMap([T; N]); 16 | 17 | impl EnumMap { 18 | pub const fn new(data: [T; N]) -> EnumMap { 19 | Self(data) 20 | } 21 | 22 | pub fn iter_mut(&mut self) -> impl Iterator { 23 | self.0.iter_mut() 24 | } 25 | } 26 | 27 | impl<'a, T, const N: usize> IntoIterator for &'a mut EnumMap { 28 | type Item = &'a mut T; 29 | type IntoIter = IterMut<'a, T>; 30 | 31 | fn into_iter(self) -> Self::IntoIter { 32 | self.0.iter_mut() 33 | } 34 | } 35 | 36 | impl<'a, T, const N: usize> IntoIterator for &'a EnumMap { 37 | type Item = &'a T; 38 | type IntoIter = Iter<'a, T>; 39 | 40 | fn into_iter(self) -> Self::IntoIter { 41 | self.0.iter() 42 | } 43 | } 44 | 45 | impl Index for EnumMap 46 | where 47 | E: Into, 48 | { 49 | type Output = T; 50 | 51 | fn index(&self, key: E) -> &Self::Output { 52 | let idx = key.into(); 53 | debug_assert!(idx < N); 54 | &self.0[idx] 55 | } 56 | } 57 | 58 | impl IndexMut for EnumMap 59 | where 60 | E: Into, 61 | { 62 | fn index_mut(&mut self, key: E) -> &mut Self::Output { 63 | let idx = key.into(); 64 | debug_assert!(idx < N); 65 | &mut self.0[idx] 66 | } 67 | } 68 | 69 | pub trait Score { 70 | fn is_checkmate(&self) -> bool; 71 | 72 | const MATE: i32 = 32000; 73 | } 74 | 75 | impl Score for i32 { 76 | fn is_checkmate(&self) -> bool { 77 | self.abs() >= Self::MATE >> 1 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/move_list.rs: -------------------------------------------------------------------------------- 1 | use super::bitboard::*; 2 | use super::board::*; 3 | use super::moov::*; 4 | use super::square::*; 5 | 6 | use arrayvec::ArrayVec; 7 | 8 | pub const MAX_MOVES: usize = 252; 9 | 10 | #[derive(Default)] 11 | pub struct MoveList(ArrayVec); 12 | 13 | impl MoveList { 14 | pub fn new() -> Self { 15 | Self(ArrayVec::new()) 16 | } 17 | 18 | pub fn from(board: &Board) -> Self { 19 | let mut moves = Self::new(); 20 | board.generate_legal_moves::(&mut moves); 21 | moves 22 | } 23 | 24 | pub fn len(&self) -> usize { 25 | self.0.len() 26 | } 27 | 28 | pub fn push(&mut self, m: Move) { 29 | self.0.push(m); 30 | } 31 | 32 | pub fn get(&self, i: usize) -> Option<&Move> { 33 | self.0.get(i) 34 | } 35 | 36 | pub fn contains(&self, m: Move) -> bool { 37 | self.0.iter().any(|entry| entry == &m) 38 | } 39 | 40 | pub fn make_q(&mut self, from_sq: SQ, to: Bitboard) { 41 | self.0.extend( 42 | to.into_iter() 43 | .map(|to_sq| Move::new(from_sq, to_sq, MoveFlags::Quiet)), 44 | ); 45 | } 46 | 47 | pub fn make_c(&mut self, from_sq: SQ, to: Bitboard) { 48 | self.0.extend( 49 | to.into_iter() 50 | .map(|to_sq| Move::new(from_sq, to_sq, MoveFlags::Capture)), 51 | ); 52 | } 53 | 54 | pub fn make_dp(&mut self, from_sq: SQ, to: Bitboard) { 55 | self.0.extend( 56 | to.into_iter() 57 | .map(|to_sq| Move::new(from_sq, to_sq, MoveFlags::DoublePush)), 58 | ); 59 | } 60 | 61 | pub fn make_pc(&mut self, from_sq: SQ, to: Bitboard) { 62 | self.0.extend(to.into_iter().flat_map(|to_sq| { 63 | [ 64 | MoveFlags::PcQueen, 65 | MoveFlags::PcKnight, 66 | MoveFlags::PcRook, 67 | MoveFlags::PcBishop, 68 | ] 69 | .into_iter() 70 | .map(move |flag| Move::new(from_sq, to_sq, flag)) 71 | })); 72 | } 73 | 74 | pub fn swap(&mut self, i: usize, j: usize) { 75 | self.0.swap(i, j); 76 | } 77 | } 78 | 79 | impl<'a> IntoIterator for &'a MoveList { 80 | type Item = &'a Move; 81 | type IntoIter = std::slice::Iter<'a, Move>; 82 | 83 | fn into_iter(self) -> Self::IntoIter { 84 | self.0.iter() 85 | } 86 | } 87 | 88 | impl std::ops::Index for MoveList { 89 | type Output = Move; 90 | 91 | fn index(&self, i: usize) -> &Self::Output { 92 | &self.0[i] 93 | } 94 | } 95 | 96 | impl std::fmt::Debug for MoveList { 97 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 98 | f.debug_list() 99 | .entries(self.0.iter().map(|m| { 100 | let (from_sq, to_sq) = m.squares(); 101 | format!("{from_sq}{to_sq}") 102 | })) 103 | .finish() 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/moov.rs: -------------------------------------------------------------------------------- 1 | use super::piece::*; 2 | use super::square::*; 3 | use std::fmt; 4 | use std::num::NonZeroU16; 5 | 6 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 7 | pub struct Move(NonZeroU16); 8 | 9 | impl Move { 10 | pub fn new(from_sq: SQ, to_sq: SQ, flags: MoveFlags) -> Self { 11 | Self( 12 | NonZeroU16::new((flags as u16) << 12 | (from_sq as u16) << 6 | (to_sq as u16)) 13 | .expect("MoveInt is zero."), 14 | ) 15 | } 16 | 17 | pub fn to_sq(&self) -> SQ { 18 | SQ::from((self.0.get() & 0x3f) as u8) 19 | } 20 | 21 | pub fn from_sq(&self) -> SQ { 22 | SQ::from(((self.0.get() >> 6) & 0x3f) as u8) 23 | } 24 | 25 | pub fn squares(&self) -> (SQ, SQ) { 26 | (self.from_sq(), self.to_sq()) 27 | } 28 | 29 | pub fn flags(&self) -> MoveFlags { 30 | MoveFlags::from(((self.0.get() >> 12) & 0xf) as u8) 31 | } 32 | 33 | pub fn move_int(&self) -> u16 { 34 | self.0.get() 35 | } 36 | 37 | pub fn is_quiet(&self) -> bool { 38 | (self.0.get() >> 12) & 0b1100 == 0 39 | } 40 | 41 | pub fn is_capture(&self) -> bool { 42 | (self.0.get() >> 12) & 0b0100 != 0 43 | } 44 | 45 | pub fn is_ep(&self) -> bool { 46 | self.flags() == MoveFlags::EnPassant 47 | } 48 | 49 | pub fn promotion(&self) -> Option { 50 | match self.flags() { 51 | MoveFlags::PrKnight | MoveFlags::PcKnight => Some(PieceType::Knight), 52 | MoveFlags::PrBishop | MoveFlags::PcBishop => Some(PieceType::Bishop), 53 | MoveFlags::PrRook | MoveFlags::PcRook => Some(PieceType::Rook), 54 | MoveFlags::PrQueen | MoveFlags::PcQueen => Some(PieceType::Queen), 55 | _ => None, 56 | } 57 | } 58 | 59 | pub fn is_castling(&self) -> bool { 60 | matches!(self.flags(), MoveFlags::OO | MoveFlags::OOO) 61 | } 62 | } 63 | 64 | impl From for Move { 65 | fn from(m: u16) -> Self { 66 | Self(NonZeroU16::new(m).expect("MoveInt is zero.")) 67 | } 68 | } 69 | 70 | impl fmt::Display for Move { 71 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 72 | write!(f, "{}{}", self.from_sq(), self.to_sq())?; 73 | 74 | if let Some(promotion_pt) = self.promotion() { 75 | write!(f, "{promotion_pt}")?; 76 | } 77 | 78 | Ok(()) 79 | } 80 | } 81 | 82 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 83 | #[repr(u8)] 84 | pub enum MoveFlags { 85 | Quiet = 0b0000, 86 | DoublePush = 0b0001, 87 | OO = 0b0010, 88 | OOO = 0b0011, 89 | Capture = 0b0100, 90 | EnPassant = 0b0101, 91 | PrKnight = 0b1000, 92 | PrBishop = 0b1001, 93 | PrRook = 0b1010, 94 | PrQueen = 0b1011, 95 | PcKnight = 0b1100, 96 | PcBishop = 0b1101, 97 | PcRook = 0b1110, 98 | PcQueen = 0b1111, 99 | } 100 | 101 | impl From for MoveFlags { 102 | fn from(n: u8) -> Self { 103 | unsafe { std::mem::transmute::(n) } 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/perft.rs: -------------------------------------------------------------------------------- 1 | use std::time::Instant; 2 | 3 | use super::board::*; 4 | use super::move_list::*; 5 | 6 | fn perft(board: &mut Board, depth: i8) -> u128 { 7 | let moves: MoveList = MoveList::from::(board); 8 | 9 | if depth == 1 { 10 | return moves.len() as u128; 11 | } 12 | 13 | let mut nodes = 0; 14 | 15 | for m in moves.into_iter().cloned() { 16 | board.push(m); 17 | let count = perft::(board, depth - 1); 18 | board.pop(); 19 | 20 | if ROOT { 21 | println!("{m}: {count}") 22 | } 23 | 24 | nodes += count; 25 | } 26 | nodes 27 | } 28 | 29 | pub fn print_perft(board: &mut Board, depth: i8) -> u128 { 30 | let now = Instant::now(); 31 | 32 | let hash = board.hash(); 33 | let material_hash = board.material_hash(); 34 | 35 | let nodes = perft::(board, depth); 36 | 37 | assert_eq!(board.hash(), hash); 38 | assert_eq!(board.material_hash(), material_hash); 39 | 40 | let elapsed = now.elapsed().as_secs_f32(); 41 | println!(); 42 | println!("{board:?}"); 43 | println!("FEN: {board}"); 44 | println!("Hash: {:#x}", board.hash()); 45 | println!("Nodes: {nodes}"); 46 | if elapsed > 0.0 { 47 | let nps = nodes as f32 / elapsed; 48 | println!("NPS: {nps:.0}"); 49 | println!("Elapsed: {elapsed:.1} seconds"); 50 | } 51 | nodes 52 | } 53 | 54 | #[cfg(test)] 55 | mod tests { 56 | use crate::perft::perft; 57 | 58 | use super::*; 59 | 60 | #[test] 61 | fn test_perft() { 62 | assert_eq!(perft::(&mut Board::new(), 5), 4865609); 63 | assert_eq!( 64 | perft::( 65 | &mut Board::try_from( 66 | "r3k2r/p1ppqpb1/bn2pnp1/3PN3/1p2P3/2N2Q1p/PPPBBPPP/R3K2R w KQkq - " 67 | ) 68 | .unwrap(), 69 | 4 70 | ), 71 | 4085603 72 | ); 73 | assert_eq!( 74 | perft::( 75 | &mut Board::try_from("8/2p5/3p4/KP5r/1R3p1k/8/4P1P1/8 w - - ").unwrap(), 76 | 5 77 | ), 78 | 674624 79 | ); 80 | assert_eq!( 81 | perft::( 82 | &mut Board::try_from( 83 | "r3k2r/Pppp1ppp/1b3nbN/nP6/BBP1P3/q4N2/Pp1P2PP/R2Q1RK1 w kq - 0 1" 84 | ) 85 | .unwrap(), 86 | 4 87 | ), 88 | 422333 89 | ); 90 | assert_eq!( 91 | perft::( 92 | &mut Board::try_from("rnbq1k1r/pp1Pbppp/2p5/8/2B5/8/PPP1NnPP/RNBQK2R w KQ - 1 8") 93 | .unwrap(), 94 | 4 95 | ), 96 | 2103487 97 | ); 98 | assert_eq!( 99 | perft::( 100 | &mut Board::try_from( 101 | "r4rk1/1pp1qppp/p1np1n2/2b1p1B1/2B1P1b1/P1NP1N2/1PP1QPPP/R4RK1 w - - 0 10" 102 | ) 103 | .unwrap(), 104 | 5 105 | ), 106 | 164075551 107 | ); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release (baseline + AVX2) 2 | 3 | on: 4 | push: 5 | tags: ["v*"] 6 | 7 | env: 8 | BIN: Weiawaga 9 | 10 | jobs: 11 | build: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | include: 16 | # Linux (glibc) 17 | - os: ubuntu-latest 18 | target: x86_64-unknown-linux-gnu 19 | ext: tar.gz 20 | # Windows (MSVC toolchain) 21 | - os: windows-latest 22 | target: x86_64-pc-windows-msvc 23 | ext: zip 24 | # macOS Intel (x86_64) 25 | - os: macos-13 26 | target: x86_64-apple-darwin 27 | ext: tar.gz 28 | 29 | runs-on: ${{ matrix.os }} 30 | 31 | steps: 32 | - uses: actions/checkout@v4 33 | 34 | - name: Install Rust 35 | uses: dtolnay/rust-toolchain@stable 36 | with: 37 | targets: ${{ matrix.target }} 38 | 39 | - name: Build baseline (no AVX/AVX2) 40 | shell: bash 41 | run: | 42 | set -eux 43 | export RUSTFLAGS="-C target-cpu=x86-64-v2 -C target-feature=-avx,-avx2" 44 | cargo build --release --target ${{ matrix.target }} 45 | cp target/${{ matrix.target }}/release/${BIN}${{ matrix.os == 'windows-latest' && '.exe' || '' }} target/${{ matrix.target }}/release/${BIN}-baseline${{ matrix.os == 'windows-latest' && '.exe' || '' }} 46 | 47 | - name: Build AVX2 (x86-64-v3) 48 | shell: bash 49 | run: | 50 | set -eux 51 | export RUSTFLAGS="-C target-cpu=x86-64-v3" 52 | cargo build --release --target ${{ matrix.target }} 53 | cp target/${{ matrix.target }}/release/${BIN}${{ matrix.os == 'windows-latest' && '.exe' || '' }} target/${{ matrix.target }}/release/${BIN}-avx2${{ matrix.os == 'windows-latest' && '.exe' || '' }} 54 | 55 | - name: Package artifacts 56 | shell: bash 57 | run: | 58 | set -eux 59 | VERSION="${GITHUB_REF_NAME}" # e.g. v1.2.3 60 | REL="target/${{ matrix.target }}/release" 61 | OUT="dist" 62 | mkdir -p "$OUT" 63 | 64 | if [[ "${{ matrix.os }}" == "windows-latest" ]]; then 65 | powershell -NoProfile -Command "Compress-Archive -Path '${REL}/${BIN}-baseline.exe' -DestinationPath '${OUT}/${BIN}-${VERSION}-${{ matrix.target }}-baseline.zip'" 66 | powershell -NoProfile -Command "Compress-Archive -Path '${REL}/${BIN}-avx2.exe' -DestinationPath '${OUT}/${BIN}-${VERSION}-${{ matrix.target }}-avx2.zip'" 67 | else 68 | strip "${REL}/${BIN}-baseline" || true 69 | strip "${REL}/${BIN}-avx2" || true 70 | tar -C "${REL}" -czf "${OUT}/${BIN}-${VERSION}-${{ matrix.target }}-baseline.tar.gz" "${BIN}-baseline" 71 | tar -C "${REL}" -czf "${OUT}/${BIN}-${VERSION}-${{ matrix.target }}-avx2.tar.gz" "${BIN}-avx2" 72 | fi 73 | 74 | # Checksums 75 | (cd "$OUT" && sha256sum * > "SHA256SUMS.txt") || (cd "$OUT" && shasum -a 256 * > "SHA256SUMS.txt") 76 | 77 | - name: Upload to GitHub Release 78 | uses: softprops/action-gh-release@v2 79 | with: 80 | files: | 81 | dist/*.tar.gz 82 | dist/*.zip 83 | dist/SHA256SUMS.txt 84 | env: 85 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 86 | -------------------------------------------------------------------------------- /src/piece.rs: -------------------------------------------------------------------------------- 1 | use super::traits::*; 2 | use std::fmt; 3 | use std::ops::Not; 4 | 5 | #[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Debug)] 6 | #[repr(u8)] 7 | pub enum Piece { 8 | WhitePawn = 0b0000, 9 | WhiteKnight = 0b0001, 10 | WhiteBishop = 0b0010, 11 | WhiteRook = 0b0011, 12 | WhiteQueen = 0b0100, 13 | WhiteKing = 0b0101, 14 | BlackPawn = 0b1000, 15 | BlackKnight = 0b1001, 16 | BlackBishop = 0b1010, 17 | BlackRook = 0b1011, 18 | BlackQueen = 0b1100, 19 | BlackKing = 0b1101, 20 | } 21 | 22 | impl Piece { 23 | pub fn index(self) -> usize { 24 | self as usize - 2 * self.color_of().index() 25 | } 26 | 27 | pub fn type_of(self) -> PieceType { 28 | PieceType::from(self as u8 & 0b111) 29 | } 30 | 31 | pub fn color_of(self) -> Color { 32 | Color::from((self as u8 & 0b1000) >> 3) 33 | } 34 | 35 | pub fn make_piece(color: Color, pt: PieceType) -> Self { 36 | Self::from(((color as u8) << 3) + pt as u8) 37 | } 38 | 39 | // Use this iterator pattern for Piece, PieceType, and Bitboard iterator for SQ 40 | // until we can return to Step implementation once it's stabilized. 41 | // https://github.com/rust-lang/rust/issues/42168 42 | pub fn iter(start: Self, end: Self) -> impl Iterator { 43 | (start as u8..=end as u8) 44 | .filter(|n| !matches!(n, 0b0110 | 0b0111)) // Skip over 6 and 7, as they're not assigned to a piece so as to align color bits 45 | .map(Self::from) 46 | } 47 | } 48 | 49 | impl Mirror for Piece { 50 | fn mirror(&self) -> Self { 51 | Self::from(*self as u8 ^ 0b1000) 52 | } 53 | } 54 | 55 | impl From for Piece { 56 | fn from(n: u8) -> Self { 57 | unsafe { std::mem::transmute::(n) } 58 | } 59 | } 60 | 61 | impl TryFrom for Piece { 62 | type Error = &'static str; 63 | 64 | fn try_from(value: char) -> Result { 65 | Self::PIECE_STR 66 | .chars() 67 | .position(|c| c == value) 68 | .map(|x| Self::from(x as u8)) 69 | .ok_or("Piece symbols should be one of \"KQRBNPkqrbnp\"") 70 | } 71 | } 72 | 73 | impl Into for Piece { 74 | fn into(self) -> usize { 75 | self.index() 76 | } 77 | } 78 | 79 | impl fmt::Display for Piece { 80 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 81 | write!( 82 | f, 83 | "{}", 84 | Self::PIECE_STR 85 | .chars() 86 | .nth(*self as usize) 87 | .expect("Piece symbol should be valid.") 88 | ) 89 | } 90 | } 91 | 92 | impl Piece { 93 | pub const N_PIECES: usize = 12; 94 | const PIECE_STR: &'static str = "PNBRQK pnbrqk"; 95 | } 96 | 97 | #[derive(Copy, Clone, PartialEq, PartialOrd, Eq, Debug)] 98 | pub enum PieceType { 99 | Pawn, 100 | Knight, 101 | Bishop, 102 | Rook, 103 | Queen, 104 | King, 105 | } 106 | 107 | impl PieceType { 108 | pub fn index(self) -> usize { 109 | self as usize 110 | } 111 | 112 | pub fn iter(start: Self, end: Self) -> impl Iterator { 113 | (start as u8..=end as u8).map(Self::from) 114 | } 115 | } 116 | 117 | impl From for PieceType { 118 | fn from(n: u8) -> Self { 119 | unsafe { std::mem::transmute::(n) } 120 | } 121 | } 122 | 123 | impl Into for PieceType { 124 | fn into(self) -> usize { 125 | self.index() 126 | } 127 | } 128 | 129 | impl fmt::Display for PieceType { 130 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 131 | write!( 132 | f, 133 | "{}", 134 | Self::PIECE_TYPE_STR 135 | .chars() 136 | .nth(*self as usize) 137 | .expect("PieceType symbol should be valid.") 138 | ) 139 | } 140 | } 141 | 142 | impl PieceType { 143 | pub const N_PIECE_TYPES: usize = 6; 144 | pub const PIECE_TYPE_STR: &'static str = "pnbrqk"; 145 | } 146 | 147 | #[derive(Copy, Clone, Debug, PartialEq, Eq)] 148 | #[repr(u8)] 149 | pub enum Color { 150 | White, 151 | Black, 152 | } 153 | 154 | impl Color { 155 | pub fn index(self) -> usize { 156 | self as usize 157 | } 158 | 159 | pub fn factor(&self) -> i32 { 160 | match *self { 161 | Self::White => 1, 162 | Self::Black => -1, 163 | } 164 | } 165 | } 166 | 167 | impl From for Color { 168 | fn from(n: u8) -> Self { 169 | unsafe { std::mem::transmute::(n) } 170 | } 171 | } 172 | 173 | impl Into for Color { 174 | fn into(self) -> usize { 175 | self.index() 176 | } 177 | } 178 | 179 | impl Not for Color { 180 | type Output = Color; 181 | 182 | fn not(self) -> Self { 183 | Color::from((self as u8) ^ 1) 184 | } 185 | } 186 | 187 | impl fmt::Display for Color { 188 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 189 | write!( 190 | f, 191 | "{}", 192 | match *self { 193 | Self::White => "w", 194 | Self::Black => "b", 195 | } 196 | ) 197 | } 198 | } 199 | 200 | impl TryFrom for Color { 201 | type Error = &'static str; 202 | 203 | fn try_from(value: char) -> Result { 204 | match value { 205 | 'w' => Ok(Self::White), 206 | 'b' => Ok(Self::Black), 207 | _ => Err("Color must be either 'w' or 'b'."), 208 | } 209 | } 210 | } 211 | 212 | impl Color { 213 | pub const N_COLORS: usize = 2; 214 | } 215 | -------------------------------------------------------------------------------- /src/magics.rs: -------------------------------------------------------------------------------- 1 | use super::attacks::*; 2 | use super::bitboard::*; 3 | use super::square::*; 4 | use super::types::*; 5 | use std::sync::LazyLock; 6 | 7 | // Fancy magic bitboard implementation inspired by Rustfish's port of Stockfish 8 | 9 | #[rustfmt::skip] 10 | const BISHOP_MAGICS_INIT: SQMap = SQMap::new([ 11 | B!(0x007fbfbfbfbfbfff), B!(0x0000a060401007fc), B!(0x0001004008020000), B!(0x0000806004000000), 12 | B!(0x0000100400000000), B!(0x000021c100b20000), B!(0x0000040041008000), B!(0x00000fb0203fff80), 13 | B!(0x0000040100401004), B!(0x0000020080200802), B!(0x0000004010202000), B!(0x0000008060040000), 14 | B!(0x0000004402000000), B!(0x0000000801008000), B!(0x000007efe0bfff80), B!(0x0000000820820020), 15 | B!(0x0000400080808080), B!(0x00021f0100400808), B!(0x00018000c06f3fff), B!(0x0000258200801000), 16 | B!(0x0000240080840000), B!(0x000018000c03fff8), B!(0x00000a5840208020), B!(0x0000020008208020), 17 | B!(0x0000804000810100), B!(0x0001011900802008), B!(0x0000804000810100), B!(0x000100403c0403ff), 18 | B!(0x00078402a8802000), B!(0x0000101000804400), B!(0x0000080800104100), B!(0x00004004c0082008), 19 | B!(0x0001010120008020), B!(0x000080809a004010), B!(0x0007fefe08810010), B!(0x0003ff0f833fc080), 20 | B!(0x007fe08019003042), B!(0x003fffefea003000), B!(0x0000101010002080), B!(0x0000802005080804), 21 | B!(0x0000808080a80040), B!(0x0000104100200040), B!(0x0003ffdf7f833fc0), B!(0x0000008840450020), 22 | B!(0x00007ffc80180030), B!(0x007fffdd80140028), B!(0x00020080200a0004), B!(0x0000101010100020), 23 | B!(0x0007ffdfc1805000), B!(0x0003ffefe0c02200), B!(0x0000000820806000), B!(0x0000000008403000), 24 | B!(0x0000000100202000), B!(0x0000004040802000), B!(0x0004010040100400), B!(0x00006020601803f4), 25 | B!(0x0003ffdfdfc28048), B!(0x0000000820820020), B!(0x0000000008208060), B!(0x0000000000808020), 26 | B!(0x0000000001002020), B!(0x0000000401002008), B!(0x0000004040404040), B!(0x007fff9fdf7ff813), 27 | ]); 28 | 29 | #[rustfmt::skip] 30 | const ROOK_MAGICS_INIT: SQMap = SQMap::new([ 31 | B!(0x00280077ffebfffe), B!(0x2004010201097fff), B!(0x0010020010053fff), B!(0x0040040008004002), 32 | B!(0x7fd00441ffffd003), B!(0x4020008887dffffe), B!(0x004000888847ffff), B!(0x006800fbff75fffd), 33 | B!(0x000028010113ffff), B!(0x0020040201fcffff), B!(0x007fe80042ffffe8), B!(0x00001800217fffe8), 34 | B!(0x00001800073fffe8), B!(0x00001800e05fffe8), B!(0x00001800602fffe8), B!(0x000030002fffffa0), 35 | B!(0x00300018010bffff), B!(0x0003000c0085fffb), B!(0x0004000802010008), B!(0x0004002020020004), 36 | B!(0x0001002002002001), B!(0x0001001000801040), B!(0x0000004040008001), B!(0x0000006800cdfff4), 37 | B!(0x0040200010080010), B!(0x0000080010040010), B!(0x0004010008020008), B!(0x0000040020200200), 38 | B!(0x0002008010100100), B!(0x0000008020010020), B!(0x0000008020200040), B!(0x0000820020004020), 39 | B!(0x00fffd1800300030), B!(0x007fff7fbfd40020), B!(0x003fffbd00180018), B!(0x001fffde80180018), 40 | B!(0x000fffe0bfe80018), B!(0x0001000080202001), B!(0x0003fffbff980180), B!(0x0001fffdff9000e0), 41 | B!(0x00fffefeebffd800), B!(0x007ffff7ffc01400), B!(0x003fffbfe4ffe800), B!(0x001ffff01fc03000), 42 | B!(0x000fffe7f8bfe800), B!(0x0007ffdfdf3ff808), B!(0x0003fff85fffa804), B!(0x0001fffd75ffa802), 43 | B!(0x00ffffd7ffebffd8), B!(0x007fff75ff7fbfd8), B!(0x003fff863fbf7fd8), B!(0x001fffbfdfd7ffd8), 44 | B!(0x000ffff810280028), B!(0x0007ffd7f7feffd8), B!(0x0003fffc0c480048), B!(0x0001ffffafd7ffd8), 45 | B!(0x00ffffe4ffdfa3ba), B!(0x007fffef7ff3d3da), B!(0x003fffbfdfeff7fa), B!(0x001fffeff7fbfc22), 46 | B!(0x0000020408001001), B!(0x0007fffeffff77fd), B!(0x0003ffffbf7dfeec), B!(0x0001ffff9dffa333), 47 | ]); 48 | 49 | pub struct Magics { 50 | masks: SQMap, 51 | magics: SQMap, 52 | pub attacks: SQMap>, 53 | shift: u8, 54 | } 55 | 56 | impl Magics { 57 | pub fn index(&self, sq: SQ, occ: Bitboard) -> usize { 58 | (((occ & self.masks[sq]) * self.magics[sq]) >> self.shift).0 as usize 59 | } 60 | } 61 | 62 | pub static ROOK_MAGICS: LazyLock = 63 | LazyLock::new(|| init_magics_type(&ROOK_MAGICS_INIT, rook_attacks_for_init, 64 - 12)); 64 | 65 | pub static BISHOP_MAGICS: LazyLock = 66 | LazyLock::new(|| init_magics_type(&BISHOP_MAGICS_INIT, bishop_attacks_for_init, 64 - 9)); 67 | 68 | ////////////////////////////////////////////// 69 | // Inits 70 | ////////////////////////////////////////////// 71 | 72 | fn init_magics_type( 73 | magic_init: &SQMap, 74 | slow_attacks_gen: fn(SQ, Bitboard) -> Bitboard, 75 | shift: u8, 76 | ) -> Magics { 77 | let mut magics = Magics { 78 | masks: SQMap::new([Bitboard::ZERO; SQ::N_SQUARES]), 79 | magics: SQMap::new([Bitboard::ZERO; SQ::N_SQUARES]), 80 | attacks: SQMap::new([const { Vec::new() }; SQ::N_SQUARES]), 81 | shift, 82 | }; 83 | 84 | for sq in Bitboard::ALL { 85 | let edges = ((Rank::One.bb() | Rank::Eight.bb()) & !sq.rank().bb()) 86 | | ((File::A.bb() | File::H.bb()) & !sq.file().bb()); 87 | magics.masks[sq] = slow_attacks_gen(sq, Bitboard::ZERO) & !edges; 88 | magics.magics[sq] = magic_init[sq]; 89 | 90 | let mut subset = Bitboard::ZERO; 91 | let mut entries = Vec::new(); 92 | let mut max_index = 0; 93 | 94 | loop { 95 | let idx = magics.index(sq, subset); 96 | let attack = slow_attacks_gen(sq, subset); 97 | entries.push((idx, attack)); 98 | max_index = max_index.max(idx); 99 | 100 | subset = (subset - magics.masks[sq]) & magics.masks[sq]; 101 | if subset == Bitboard::ZERO { 102 | break; 103 | } 104 | } 105 | 106 | let mut table = vec![Bitboard::ZERO; max_index + 1]; 107 | for (idx, att) in entries { 108 | table[idx] = att; 109 | } 110 | magics.attacks[sq] = table; 111 | } 112 | magics 113 | } 114 | -------------------------------------------------------------------------------- /src/nnue.rs: -------------------------------------------------------------------------------- 1 | use super::nnue_weights::*; 2 | use super::piece::*; 3 | use super::square::*; 4 | use super::traits::*; 5 | use super::types::*; 6 | 7 | use wide::*; 8 | 9 | #[derive(Clone)] 10 | struct Embedding { 11 | weights: &'static [[i16x16; D]; N], 12 | } 13 | 14 | impl Embedding { 15 | pub fn new(weights: &'static [[i16x16; D]; N]) -> Self { 16 | Self { weights } 17 | } 18 | } 19 | 20 | #[derive(Clone)] 21 | struct Linear { 22 | weights: &'static [i16x16], 23 | biases: &'static [i16], 24 | } 25 | 26 | impl Linear { 27 | pub fn new(weights: &'static [i16x16], biases: &'static [i16]) -> Self { 28 | assert_eq!(weights.len(), IN * OUT); 29 | assert_eq!(biases.len(), OUT); 30 | Self { weights, biases } 31 | } 32 | } 33 | 34 | #[derive(Clone)] 35 | #[repr(C, align(64))] 36 | struct Accumulator { 37 | acc: ColorMap<[i16x16; Network::L1 / Network::LANES]>, 38 | pop_count: i16, 39 | } 40 | 41 | #[derive(Clone)] 42 | pub struct Network { 43 | input_layer: Embedding<{ Self::N_INPUTS }, { Self::L1 / Self::LANES }>, 44 | hidden_layers: [Linear<{ 2 * Self::L1 / Self::LANES }, 1>; Self::N_BUCKETS], 45 | 46 | stack: Vec, 47 | idx: usize, 48 | } 49 | 50 | impl Network { 51 | pub fn new() -> Self { 52 | Self { 53 | input_layer: Embedding::new(&INPUT_LAYER_WEIGHT), 54 | hidden_layers: [ 55 | Linear::new(&HIDDEN_LAYER_0_WEIGHT, &HIDDEN_LAYER_0_BIAS), 56 | Linear::new(&HIDDEN_LAYER_1_WEIGHT, &HIDDEN_LAYER_1_BIAS), 57 | Linear::new(&HIDDEN_LAYER_2_WEIGHT, &HIDDEN_LAYER_2_BIAS), 58 | Linear::new(&HIDDEN_LAYER_3_WEIGHT, &HIDDEN_LAYER_3_BIAS), 59 | Linear::new(&HIDDEN_LAYER_4_WEIGHT, &HIDDEN_LAYER_4_BIAS), 60 | Linear::new(&HIDDEN_LAYER_5_WEIGHT, &HIDDEN_LAYER_5_BIAS), 61 | Linear::new(&HIDDEN_LAYER_6_WEIGHT, &HIDDEN_LAYER_6_BIAS), 62 | Linear::new(&HIDDEN_LAYER_7_WEIGHT, &HIDDEN_LAYER_7_BIAS), 63 | ], 64 | stack: vec![ 65 | Accumulator { 66 | acc: ColorMap::new([INPUT_LAYER_BIAS; Color::N_COLORS]), 67 | pop_count: 0 68 | }; 69 | Self::N_ACCUMULATORS 70 | ], 71 | idx: 0, 72 | } 73 | } 74 | 75 | #[inline] 76 | pub fn push(&mut self) { 77 | debug_assert!(self.idx < Self::N_ACCUMULATORS); 78 | let next = self.idx + 1; 79 | self.stack[next] = self.stack[self.idx].clone(); 80 | self.idx = next; 81 | } 82 | 83 | #[inline] 84 | pub fn pop(&mut self) { 85 | debug_assert!(self.idx > 0); 86 | self.idx -= 1; 87 | } 88 | 89 | pub fn activate(&mut self, pc: Piece, sq: SQ) { 90 | self.update_activation::<1>(pc, sq); 91 | } 92 | 93 | pub fn deactivate(&mut self, pc: Piece, sq: SQ) { 94 | self.update_activation::<-1>(pc, sq); 95 | } 96 | 97 | pub fn move_piece_quiet(&mut self, pc: Piece, from_sq: SQ, to_sq: SQ) { 98 | let cur = &mut self.stack[self.idx]; 99 | for color in [Color::White, Color::Black] { 100 | let pc_idx = pc.relative(color).index(); 101 | let from_idx = pc_idx * SQ::N_SQUARES + from_sq.relative(color).index(); 102 | let to_idx = pc_idx * SQ::N_SQUARES + to_sq.relative(color).index(); 103 | 104 | let from_weights = self.input_layer.weights[from_idx].iter(); 105 | let to_weights = self.input_layer.weights[to_idx].iter(); 106 | 107 | cur.acc[color] 108 | .iter_mut() 109 | .zip(from_weights.zip(to_weights)) 110 | .for_each(|(act, (&w_from, &w_to))| *act += w_to - w_from); 111 | } 112 | } 113 | 114 | fn update_activation(&mut self, pc: Piece, sq: SQ) { 115 | let cur = &mut self.stack[self.idx]; 116 | 117 | for color in [Color::White, Color::Black] { 118 | let pc_idx = pc.relative(color).index(); 119 | let sq_idx = sq.relative(color).index(); 120 | let idx = pc_idx * SQ::N_SQUARES + sq_idx; 121 | 122 | cur.acc[color] 123 | .iter_mut() 124 | .zip(self.input_layer.weights[idx].iter()) 125 | .for_each(|(act, &w)| *act += SIGN * w); 126 | } 127 | cur.pop_count += SIGN; 128 | } 129 | 130 | pub fn eval(&self, ctm: Color) -> i32 { 131 | let acc = &self.stack[self.idx]; 132 | let bucket = (acc.pop_count as usize - 2) / Self::BUCKET_DIV; 133 | let hidden_layer = &self.hidden_layers[bucket]; 134 | 135 | let eval_color = |color, weights: &[i16x16]| -> i32x8 { 136 | acc.acc[color] 137 | .iter() 138 | .zip(weights) 139 | .map(|(&act, &w)| { 140 | let clamped = Self::clipped_relu(act); 141 | (w * clamped).dot(clamped) 142 | }) 143 | .sum() 144 | }; 145 | 146 | let output = eval_color(ctm, &hidden_layer.weights[..Self::L1 / Self::LANES]) 147 | + eval_color(!ctm, &hidden_layer.weights[Self::L1 / Self::LANES..]); 148 | 149 | i32::from(hidden_layer.biases[0]) * Self::NNUE2SCORE / Self::HIDDEN_SCALE 150 | + (output.reduce_add() / Self::INPUT_SCALE) * Self::NNUE2SCORE / Self::COMB_SCALE 151 | } 152 | 153 | fn clipped_relu(x: i16x16) -> i16x16 { 154 | x.max(i16x16::ZERO) 155 | .min(i16x16::splat(Self::INPUT_SCALE as i16)) 156 | } 157 | } 158 | 159 | impl Network { 160 | const N_INPUTS: usize = Piece::N_PIECES * SQ::N_SQUARES; 161 | const N_ACCUMULATORS: usize = 1024; 162 | const L1: usize = 512; 163 | const N_BUCKETS: usize = 8; 164 | const BUCKET_DIV: usize = 32_usize.div_ceil(Self::N_BUCKETS); 165 | const LANES: usize = i16x16::LANES as usize; 166 | const NNUE2SCORE: i32 = 400; 167 | const INPUT_SCALE: i32 = 255; 168 | const HIDDEN_SCALE: i32 = 64; 169 | const COMB_SCALE: i32 = Self::HIDDEN_SCALE * Self::INPUT_SCALE; 170 | } 171 | -------------------------------------------------------------------------------- /src/tt.rs: -------------------------------------------------------------------------------- 1 | use super::board::*; 2 | use super::moov::*; 3 | use super::search::*; 4 | use crate::types::Score; 5 | #[cfg(target_arch = "x86_64")] 6 | use core::arch::x86_64; 7 | use std::sync::atomic::{AtomicU64, Ordering}; 8 | 9 | /////////////////////////////////////////////////////////////////// 10 | // Transposition Table Entry 11 | /////////////////////////////////////////////////////////////////// 12 | 13 | #[derive(Eq, PartialEq, Copy, Clone, Default)] 14 | #[repr(transparent)] 15 | pub struct TTEntry(u64); 16 | 17 | impl TTEntry { 18 | fn new( 19 | hash: u64, 20 | value: i32, 21 | best_move: Option, 22 | depth: i8, 23 | bound: Bound, 24 | age: u8, 25 | ) -> Self { 26 | let key16 = (hash >> Self::KEY_SHIFT) as u16 as u64; 27 | let m16 = best_move.map_or(0, |m| m.move_int()) as u64; 28 | let value16 = value as i16 as u16 as u64; 29 | let depth8 = depth as u8 as u64; 30 | let bound2 = bound as u8 as u64; 31 | let age6 = (age & Self::AGE_MASK as u8) as u64; // mask to 6 bits 32 | 33 | Self( 34 | m16 | (value16 << Self::VALUE_SHIFT) 35 | | (depth8 << Self::DEPTH_SHIFT) 36 | | (bound2 << Self::BOUND_SHIFT) 37 | | (age6 << Self::AGE_SHIFT) 38 | | (key16 << Self::KEY_SHIFT), 39 | ) 40 | } 41 | 42 | pub fn key(self) -> u64 { 43 | self.0 >> Self::KEY_SHIFT 44 | } 45 | 46 | pub fn age(self) -> u8 { 47 | ((self.0 >> Self::AGE_SHIFT) & Self::AGE_MASK) as u8 48 | } 49 | 50 | pub fn depth(self) -> i8 { 51 | ((self.0 >> Self::DEPTH_SHIFT) & Self::DEPTH_MASK) as u8 as i8 52 | } 53 | 54 | pub fn bound(self) -> Bound { 55 | unsafe { core::mem::transmute(((self.0 >> Self::BOUND_SHIFT) & Self::BOUND_MASK) as u8) } 56 | } 57 | 58 | pub fn value(self) -> i32 { 59 | ((self.0 >> Self::VALUE_SHIFT) & Self::VALUE_MASK) as u16 as i16 as i32 60 | } 61 | 62 | pub fn best_move(self) -> Option { 63 | let m = (self.0 & Self::MOVE_MASK) as u16; 64 | (m != 0).then(|| Move::from(m)) 65 | } 66 | 67 | pub fn with_value(self, value: i32) -> Self { 68 | let value16 = (value as i16 as u16 as u64) << Self::VALUE_SHIFT; 69 | let cleared = self.0 & !(Self::VALUE_MASK << Self::VALUE_SHIFT); 70 | Self(cleared | value16) 71 | } 72 | } 73 | 74 | impl TTEntry { 75 | const AGE_MASK: u64 = 0x3F; 76 | const BOUND_MASK: u64 = 0x3; 77 | const DEPTH_MASK: u64 = 0xFF; 78 | const MOVE_MASK: u64 = 0xFFFF; 79 | const VALUE_MASK: u64 = 0xFFFF; 80 | 81 | const AGE_SHIFT: usize = 42; 82 | const BOUND_SHIFT: usize = 40; 83 | const DEPTH_SHIFT: usize = 32; 84 | const KEY_SHIFT: usize = 48; 85 | const VALUE_SHIFT: usize = 16; 86 | } 87 | 88 | /////////////////////////////////////////////////////////////////// 89 | // Transposition Table 90 | /////////////////////////////////////////////////////////////////// 91 | 92 | pub struct TT { 93 | table: Vec, 94 | age: u8, 95 | } 96 | 97 | impl TT { 98 | pub fn new(megabytes: usize) -> Self { 99 | let upper_limit = megabytes * 1024 * 1024 / size_of::() + 1; 100 | let count = upper_limit.next_power_of_two() / 2; 101 | let mut table = Vec::with_capacity(count); 102 | 103 | for _ in 0..count { 104 | table.push(AtomicU64::new(0)); 105 | } 106 | 107 | TT { table, age: 0 } 108 | } 109 | 110 | pub fn insert( 111 | &self, 112 | board: &Board, 113 | depth: i8, 114 | mut value: i32, 115 | best_move: Option, 116 | bound: Bound, 117 | ply: usize, 118 | ) { 119 | let hash = board.hash(); 120 | let idx = (hash as usize) & (self.table.len() - 1); 121 | debug_assert!(idx < self.table.len()); 122 | 123 | let aentry = &self.table[idx]; 124 | let data = aentry.load(Ordering::Relaxed); 125 | let entry = (data != 0).then_some(TTEntry(data)); 126 | 127 | if entry.is_none_or(|entry| { 128 | bound == Bound::Exact 129 | || self.age != entry.age() 130 | || depth >= entry.depth() - Self::DEPTH_MARGIN 131 | }) { 132 | if value.is_checkmate() { 133 | value += value.signum() * ply as i32; 134 | } 135 | 136 | aentry.store( 137 | TTEntry::new(hash, value, best_move, depth, bound, self.age).0, 138 | Ordering::Relaxed, 139 | ); 140 | } 141 | } 142 | 143 | pub fn get(&self, board: &Board, ply: usize) -> Option { 144 | let hash = board.hash(); 145 | let idx = (hash as usize) & (self.table.len() - 1); 146 | debug_assert!(idx < self.table.len()); 147 | 148 | let data = self.table[idx].load(Ordering::Relaxed); 149 | if data == 0 { 150 | return None; 151 | } 152 | 153 | let mut entry = TTEntry(data); 154 | if entry.key() != hash >> TTEntry::KEY_SHIFT { 155 | return None; 156 | } 157 | 158 | let value = entry.value(); 159 | if value.is_checkmate() { 160 | entry = entry.with_value(value - value.signum() * ply as i32); 161 | } 162 | 163 | Some(entry) 164 | } 165 | 166 | pub fn clear(&self) { 167 | self.table 168 | .iter() 169 | .for_each(|entry| entry.store(0, Ordering::Relaxed)); 170 | } 171 | 172 | pub fn age_up(&mut self) { 173 | self.age = (self.age + 1) & TTEntry::AGE_MASK as u8; 174 | } 175 | 176 | pub fn hashfull(&self) -> usize { 177 | // Sample the first 1000 entries to estimate how full the table is. 178 | self.table 179 | .iter() 180 | .take(1000) 181 | .filter(|&aentry| { 182 | let data = aentry.load(Ordering::Relaxed); 183 | let entry = (data != 0).then_some(TTEntry(data)); 184 | entry.is_some_and(|entry| entry.age() == self.age) 185 | }) 186 | .count() 187 | } 188 | 189 | #[allow(unused_variables)] 190 | pub fn prefetch(&self, board: &Board) { 191 | #[cfg(target_arch = "x86_64")] 192 | unsafe { 193 | let ptr = &self.table[(board.hash() as usize) & (self.table.len() - 1)] 194 | as *const AtomicU64 as *const i8; 195 | x86_64::_mm_prefetch(ptr, x86_64::_MM_HINT_T0); 196 | } 197 | } 198 | } 199 | 200 | impl TT { 201 | const DEPTH_MARGIN: i8 = 2; 202 | } 203 | -------------------------------------------------------------------------------- /src/search_master.rs: -------------------------------------------------------------------------------- 1 | use super::board::*; 2 | use super::perft::*; 3 | use super::search::*; 4 | use super::timer::*; 5 | use super::tt::*; 6 | use super::uci::*; 7 | use std::io::Write; 8 | use std::sync::Arc; 9 | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 10 | use std::sync::mpsc::Receiver; 11 | use std::thread; 12 | use std::time::Duration; 13 | 14 | pub struct SearchMaster { 15 | stop: Arc, 16 | pondering: Arc, 17 | ponder_enabled: bool, 18 | board: Board, 19 | n_threads: u16, 20 | tt: TT, 21 | overhead: Duration, 22 | } 23 | 24 | impl SearchMaster { 25 | pub fn new(stop: Arc, pondering: Arc) -> Self { 26 | Self { 27 | stop, 28 | pondering, 29 | ponder_enabled: false, 30 | board: Board::new(), 31 | n_threads: 1, 32 | tt: TT::new(16), 33 | overhead: Duration::from_millis(10), 34 | } 35 | } 36 | 37 | pub fn run(&mut self, main_rx: Receiver) { 38 | for cmd in main_rx { 39 | match cmd { 40 | UCICommand::IsReady => { 41 | println!("readyok"); 42 | } 43 | UCICommand::UCINewGame => { 44 | self.board.reset(); 45 | self.tt.clear(); 46 | } 47 | UCICommand::UCI => { 48 | println!("id name Weiawaga v{}", env!("CARGO_PKG_VERSION")); 49 | println!("id author {}", env!("CARGO_PKG_AUTHORS")); 50 | println!( 51 | "option name Hash type spin default {} min {} max {}", 52 | EngineOption::HASH_DEFAULT, 53 | EngineOption::HASH_MIN, 54 | EngineOption::HASH_MAX, 55 | ); 56 | println!( 57 | "option name Threads type spin default {} min {} max {}", 58 | EngineOption::THREADS_DEFAULT, 59 | EngineOption::THREADS_MIN, 60 | EngineOption::THREADS_MAX, 61 | ); 62 | println!( 63 | "option name Move Overhead type spin default {} min {} max {}", 64 | EngineOption::MOVE_OVERHEAD_DEFAULT.as_millis(), 65 | EngineOption::MOVE_OVERHEAD_MIN.as_millis(), 66 | EngineOption::MOVE_OVERHEAD_MAX.as_millis(), 67 | ); 68 | println!( 69 | "option name Ponder type check default {}", 70 | EngineOption::PONDER_DEFAULT, 71 | ); 72 | println!("option name Clear Hash type button"); 73 | println!("uciok"); 74 | } 75 | UCICommand::Position(board) => self.board = *board, 76 | UCICommand::Go { 77 | time_control, 78 | ponder, 79 | } => self.go(time_control, ponder), 80 | UCICommand::Perft(depth) => { 81 | let mut board = self.board.clone(); 82 | print_perft(&mut board, depth); 83 | } 84 | UCICommand::Option(engine_option) => match self.set_option(engine_option) { 85 | Ok(_) => (), 86 | Err(e) => eprintln!("{e}"), 87 | }, 88 | UCICommand::Eval => println!("{}", self.board.eval()), 89 | UCICommand::Fen => println!("{}", self.board), 90 | _ => eprintln!("Unexpected UCI Command."), 91 | } 92 | std::io::stdout().flush().unwrap(); 93 | std::io::stderr().flush().unwrap(); 94 | } 95 | } 96 | 97 | fn go(&mut self, time_control: TimeControl, ponder: bool) { 98 | if ponder && !self.ponder_enabled { 99 | eprintln!("Pondering is not enabled."); 100 | return; 101 | } 102 | 103 | let board = self.board.clone(); 104 | 105 | self.pondering.store(ponder, Ordering::Release); 106 | self.stop.store(false, Ordering::Release); 107 | let nodes = Arc::new(AtomicU64::new(0)); 108 | 109 | let (best_move, ponder_move) = thread::scope(|s| { 110 | // Create main search thread with the actual time control. This thread controls self.stop. 111 | let mut main_search_thread = Search::new( 112 | Timer::new( 113 | &board, 114 | time_control, 115 | self.pondering.clone(), 116 | self.stop.clone(), 117 | nodes.clone(), 118 | self.overhead, 119 | ), 120 | &self.tt, 121 | 0, 122 | ); 123 | 124 | // Create helper search threads which will stop when self.stop resolves to true. 125 | for id in 1..self.n_threads { 126 | let thread_board = board.clone(); 127 | let mut helper_search_thread = Search::new( 128 | Timer::new( 129 | &thread_board, 130 | TimeControl::Infinite, 131 | self.pondering.clone(), 132 | self.stop.clone(), 133 | nodes.clone(), 134 | self.overhead, 135 | ), 136 | &self.tt, 137 | id, 138 | ); 139 | s.spawn(move || helper_search_thread.go(thread_board)); 140 | } 141 | main_search_thread.go(board.clone()) 142 | }); 143 | 144 | match (best_move, ponder_move) { 145 | (Some(best), Some(ponder)) if self.ponder_enabled => { 146 | println!("bestmove {best} ponder {ponder}") 147 | } 148 | (Some(best), _) => println!("bestmove {best}"), 149 | (None, _) => println!("bestmove (none)"), 150 | } 151 | self.tt.age_up(); 152 | } 153 | 154 | fn set_option(&mut self, engine_option: EngineOption) -> Result<(), &'static str> { 155 | match engine_option { 156 | EngineOption::Hash(mb) => { 157 | if !(EngineOption::HASH_MIN..=EngineOption::HASH_MAX).contains(&mb) { 158 | return Err("Hash size out of range."); 159 | } 160 | self.tt = TT::new(mb); 161 | } 162 | EngineOption::Threads(n_threads) => { 163 | if !(EngineOption::THREADS_MIN..=EngineOption::THREADS_MAX).contains(&n_threads) { 164 | return Err("Threads out of range."); 165 | } 166 | self.n_threads = n_threads; 167 | } 168 | EngineOption::MoveOverhead(overhead) => { 169 | if !(EngineOption::MOVE_OVERHEAD_MIN..=EngineOption::MOVE_OVERHEAD_MAX) 170 | .contains(&overhead) 171 | { 172 | return Err("Hash size out of range."); 173 | } 174 | self.overhead = overhead; 175 | } 176 | EngineOption::Ponder(ponder_enabled) => { 177 | self.ponder_enabled = ponder_enabled; 178 | } 179 | EngineOption::ClearHash => { 180 | self.tt.clear(); 181 | } 182 | }; 183 | Ok(()) 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /src/move_sorting.rs: -------------------------------------------------------------------------------- 1 | use super::attacks; 2 | use super::bitboard::*; 3 | use super::board::*; 4 | use super::moov::*; 5 | use super::move_list::*; 6 | use super::piece::*; 7 | use super::square::*; 8 | use super::types::*; 9 | 10 | use arrayvec::ArrayVec; 11 | 12 | pub struct MoveSorter<'a> { 13 | moves: &'a mut MoveList, 14 | scores: ArrayVec, 15 | idx: usize, 16 | } 17 | 18 | impl Iterator for MoveSorter<'_> { 19 | type Item = Move; 20 | 21 | fn next(&mut self) -> Option { 22 | if self.idx >= self.moves.len() { 23 | return None; 24 | } 25 | 26 | let max_idx = (self.idx..self.moves.len()).max_by_key(|&i| self.scores[i])?; 27 | self.moves.swap(self.idx, max_idx); 28 | self.scores.swap(self.idx, max_idx); 29 | 30 | let next_move = self.moves.get(self.idx).copied(); 31 | self.idx += 1; 32 | next_move 33 | } 34 | } 35 | 36 | pub struct MoveScorer { 37 | killer_moves: [Option; MAX_MOVES], 38 | history_scores: ColorMap>>, 39 | counter_moves: SQMap>>, 40 | } 41 | 42 | impl MoveScorer { 43 | pub fn new() -> Self { 44 | Self { 45 | killer_moves: [None; MAX_MOVES], 46 | history_scores: ColorMap::new( 47 | [SQMap::new([SQMap::new([0; SQ::N_SQUARES]); SQ::N_SQUARES]); 2], 48 | ), 49 | counter_moves: SQMap::new([SQMap::new([None; SQ::N_SQUARES]); SQ::N_SQUARES]), 50 | } 51 | } 52 | 53 | pub fn create_sorter<'a, const QUIESCENCE: bool>( 54 | &self, 55 | moves: &'a mut MoveList, 56 | board: &Board, 57 | ply: usize, 58 | hash_move: Option, 59 | ) -> MoveSorter<'a> { 60 | MoveSorter { 61 | scores: ArrayVec::from_iter( 62 | moves 63 | .into_iter() 64 | .map(|&m| self.score_move::(m, board, ply, hash_move)), 65 | ), 66 | idx: 0, 67 | moves, 68 | } 69 | } 70 | 71 | fn score_move( 72 | &self, 73 | m: Move, 74 | board: &Board, 75 | ply: usize, 76 | hash_move: Option, 77 | ) -> i32 { 78 | if Some(m) == hash_move { 79 | return Self::HASH_MOVE_SCORE; 80 | } 81 | 82 | if !QUIESCENCE && m.is_quiet() { 83 | return if self.is_killer(m, ply) { 84 | Self::KILLER_MOVE_SCORE 85 | } else if self.is_counter(board, m) { 86 | Self::COUNTER_MOVE_SCORE 87 | } else if m.is_castling() { 88 | Self::CASTLING_SCORE 89 | } else { 90 | self.history_score(m, board.ctm()) 91 | }; 92 | } 93 | 94 | let mut score = 0; 95 | if m.is_capture() { 96 | if m.is_ep() { 97 | return Self::CAPTURE_SCORE; 98 | } 99 | 100 | score += Self::mvv_lva_score(board, m) 101 | + if QUIESCENCE || Self::see(board, m) { 102 | Self::CAPTURE_SCORE 103 | } else { 104 | -Self::CAPTURE_SCORE 105 | }; 106 | } 107 | 108 | score += m 109 | .promotion() 110 | .map_or(0, |pt| Self::PROMOTION_SCORE + Self::SEE_PIECE_TYPE[pt]); 111 | score 112 | } 113 | 114 | fn mvv_lva_score(board: &Board, m: Move) -> i32 { 115 | let (from_sq, to_sq) = m.squares(); 116 | let captured_pt = board.piece_type_at(to_sq).expect("No captured in MVVLVA."); 117 | let attacking_pt = board 118 | .piece_type_at(from_sq) 119 | .expect("No attacker in MVVLVA."); 120 | 121 | Self::MVV_LVA_SCORES[captured_pt.index() * PieceType::N_PIECE_TYPES + attacking_pt.index()] 122 | } 123 | 124 | pub fn add_killer(&mut self, m: Move, ply: usize) { 125 | self.killer_moves[ply] = Some(m); 126 | } 127 | 128 | pub fn add_history(&mut self, m: Move, ctm: Color, depth: i8) { 129 | let depth = depth as i32; 130 | let (from_sq, to_sq) = m.squares(); 131 | let score = &mut self.history_scores[ctm][from_sq][to_sq]; 132 | *score += depth * depth; 133 | 134 | if *score >= Self::HISTORY_MAX { 135 | self.history_scores 136 | .iter_mut() 137 | .flatten() 138 | .flatten() 139 | .for_each(|x| *x >>= 1); 140 | } 141 | } 142 | 143 | pub fn add_counter(&mut self, p_move: Move, m: Move) { 144 | self.counter_moves[p_move.from_sq()][p_move.to_sq()] = Some(m); 145 | } 146 | 147 | fn is_killer(&self, m: Move, ply: usize) -> bool { 148 | self.killer_moves[ply] == Some(m) 149 | } 150 | 151 | fn is_counter(&self, board: &Board, m: Move) -> bool { 152 | board 153 | .peek() 154 | .is_some_and(|p_move| self.counter_moves[p_move.from_sq()][p_move.to_sq()] == Some(m)) 155 | } 156 | 157 | fn history_score(&self, m: Move, ctm: Color) -> i32 { 158 | self.history_scores[ctm][m.from_sq()][m.to_sq()] 159 | } 160 | 161 | pub fn see(board: &Board, m: Move) -> bool { 162 | if m.promotion().is_some() { 163 | return true; 164 | } 165 | 166 | let (from_sq, to_sq) = m.squares(); 167 | 168 | let Some(captured_pt) = board.piece_type_at(to_sq) else { 169 | return true; 170 | }; 171 | 172 | let mut attacking_pt = board 173 | .piece_type_at(from_sq) 174 | .expect("No attacking pt in see."); 175 | 176 | let mut value = Self::SEE_PIECE_TYPE[captured_pt] - Self::SEE_PIECE_TYPE[attacking_pt]; 177 | 178 | if value >= 0 { 179 | return true; 180 | } 181 | 182 | let mut occ = board.all_pieces() ^ from_sq.bb(); 183 | let mut attackers = board.attackers(to_sq, occ); 184 | 185 | let diagonal_sliders = board.diagonal_sliders(); 186 | let orthogonal_sliders = board.orthogonal_sliders(); 187 | 188 | let mut ctm = !board.ctm(); 189 | loop { 190 | attackers &= occ; 191 | let stm_attackers = attackers & board.all_pieces_c(ctm); 192 | 193 | if stm_attackers == Bitboard::ZERO { 194 | break; 195 | } 196 | 197 | // We know at this point that there must be a piece, so find the least valuable attacker. 198 | attacking_pt = PieceType::iter(PieceType::Pawn, PieceType::King) 199 | .find(|&pt| stm_attackers & board.bitboard_of_pt(pt) != Bitboard::ZERO) 200 | .expect("No attacking pt found."); 201 | 202 | ctm = !ctm; 203 | 204 | value = -value - 1 - Self::SEE_PIECE_TYPE[attacking_pt]; 205 | 206 | if value >= 0 { 207 | if attacking_pt == PieceType::King 208 | && (attackers & board.all_pieces_c(ctm) != Bitboard::ZERO) 209 | { 210 | ctm = !ctm; 211 | } 212 | break; 213 | } 214 | 215 | occ ^= (stm_attackers & board.bitboard_of_pt(attacking_pt)) 216 | .lsb() 217 | .bb(); 218 | 219 | if matches!( 220 | attacking_pt, 221 | PieceType::Pawn | PieceType::Bishop | PieceType::Queen 222 | ) { 223 | attackers |= attacks::bishop_attacks(to_sq, occ) & diagonal_sliders; 224 | } 225 | 226 | if matches!(attacking_pt, PieceType::Rook | PieceType::Queen) { 227 | attackers |= attacks::rook_attacks(to_sq, occ) & orthogonal_sliders; 228 | } 229 | } 230 | 231 | ctm != board 232 | .piece_at(from_sq) 233 | .expect("No piece at original attacking square.") 234 | .color_of() 235 | } 236 | } 237 | 238 | impl MoveScorer { 239 | const HISTORY_MAX: i32 = i16::MAX as i32 / 2; 240 | const HASH_MOVE_SCORE: i32 = 100 * Self::HISTORY_MAX; 241 | const PROMOTION_SCORE: i32 = 50 * Self::HISTORY_MAX; 242 | const CAPTURE_SCORE: i32 = 10 * Self::HISTORY_MAX; 243 | const KILLER_MOVE_SCORE: i32 = 5 * Self::HISTORY_MAX; 244 | const COUNTER_MOVE_SCORE: i32 = 3 * Self::HISTORY_MAX; 245 | const CASTLING_SCORE: i32 = 2 * Self::HISTORY_MAX; 246 | 247 | const SEE_PIECE_TYPE: PieceTypeMap = PieceTypeMap::new([100, 375, 375, 500, 1025, 10000]); 248 | 249 | #[rustfmt::skip] 250 | const MVV_LVA_SCORES: [i32; PieceType::N_PIECE_TYPES * PieceType::N_PIECE_TYPES] = [ 251 | 105, 104, 103, 102, 101, 100, 252 | 205, 204, 203, 202, 201, 200, 253 | 305, 304, 303, 302, 301, 300, 254 | 405, 404, 403, 402, 401, 400, 255 | 505, 504, 503, 502, 501, 500, 256 | 605, 604, 603, 602, 601, 600 257 | ]; 258 | } 259 | -------------------------------------------------------------------------------- /src/attacks.rs: -------------------------------------------------------------------------------- 1 | use super::bitboard::*; 2 | use super::magics::*; 3 | use super::piece::*; 4 | use super::square::*; 5 | use super::traits::*; 6 | use super::types::*; 7 | 8 | #[rustfmt::skip] 9 | const KNIGHT_ATTACKS: SQMap = SQMap::new([ 10 | B!(0x0000000000020400), B!(0x0000000000050800), B!(0x00000000000a1100), B!(0x0000000000142200), 11 | B!(0x0000000000284400), B!(0x0000000000508800), B!(0x0000000000a01000), B!(0x0000000000402000), 12 | B!(0x0000000002040004), B!(0x0000000005080008), B!(0x000000000a110011), B!(0x0000000014220022), 13 | B!(0x0000000028440044), B!(0x0000000050880088), B!(0x00000000a0100010), B!(0x0000000040200020), 14 | B!(0x0000000204000402), B!(0x0000000508000805), B!(0x0000000a1100110a), B!(0x0000001422002214), 15 | B!(0x0000002844004428), B!(0x0000005088008850), B!(0x000000a0100010a0), B!(0x0000004020002040), 16 | B!(0x0000020400040200), B!(0x0000050800080500), B!(0x00000a1100110a00), B!(0x0000142200221400), 17 | B!(0x0000284400442800), B!(0x0000508800885000), B!(0x0000a0100010a000), B!(0x0000402000204000), 18 | B!(0x0002040004020000), B!(0x0005080008050000), B!(0x000a1100110a0000), B!(0x0014220022140000), 19 | B!(0x0028440044280000), B!(0x0050880088500000), B!(0x00a0100010a00000), B!(0x0040200020400000), 20 | B!(0x0204000402000000), B!(0x0508000805000000), B!(0x0a1100110a000000), B!(0x1422002214000000), 21 | B!(0x2844004428000000), B!(0x5088008850000000), B!(0xa0100010a0000000), B!(0x4020002040000000), 22 | B!(0x0400040200000000), B!(0x0800080500000000), B!(0x1100110a00000000), B!(0x2200221400000000), 23 | B!(0x4400442800000000), B!(0x8800885000000000), B!(0x100010a000000000), B!(0x2000204000000000), 24 | B!(0x0004020000000000), B!(0x0008050000000000), B!(0x00110a0000000000), B!(0x0022140000000000), 25 | B!(0x0044280000000000), B!(0x0088500000000000), B!(0x0010a00000000000), B!(0x0020400000000000) 26 | ]); 27 | 28 | #[rustfmt::skip] 29 | const ADJACENT_ATTACKS: SQMap = SQMap::new([ 30 | B!(0x0000000000000302), B!(0x0000000000000705), B!(0x0000000000000e0a), B!(0x0000000000001c14), 31 | B!(0x0000000000003828), B!(0x0000000000007050), B!(0x000000000000e0a0), B!(0x000000000000c040), 32 | B!(0x0000000000030203), B!(0x0000000000070507), B!(0x00000000000e0a0e), B!(0x00000000001c141c), 33 | B!(0x0000000000382838), B!(0x0000000000705070), B!(0x0000000000e0a0e0), B!(0x0000000000c040c0), 34 | B!(0x0000000003020300), B!(0x0000000007050700), B!(0x000000000e0a0e00), B!(0x000000001c141c00), 35 | B!(0x0000000038283800), B!(0x0000000070507000), B!(0x00000000e0a0e000), B!(0x00000000c040c000), 36 | B!(0x0000000302030000), B!(0x0000000705070000), B!(0x0000000e0a0e0000), B!(0x0000001c141c0000), 37 | B!(0x0000003828380000), B!(0x0000007050700000), B!(0x000000e0a0e00000), B!(0x000000c040c00000), 38 | B!(0x0000030203000000), B!(0x0000070507000000), B!(0x00000e0a0e000000), B!(0x00001c141c000000), 39 | B!(0x0000382838000000), B!(0x0000705070000000), B!(0x0000e0a0e0000000), B!(0x0000c040c0000000), 40 | B!(0x0003020300000000), B!(0x0007050700000000), B!(0x000e0a0e00000000), B!(0x001c141c00000000), 41 | B!(0x0038283800000000), B!(0x0070507000000000), B!(0x00e0a0e000000000), B!(0x00c040c000000000), 42 | B!(0x0302030000000000), B!(0x0705070000000000), B!(0x0e0a0e0000000000), B!(0x1c141c0000000000), 43 | B!(0x3828380000000000), B!(0x7050700000000000), B!(0xe0a0e00000000000), B!(0xc040c00000000000), 44 | B!(0x0203000000000000), B!(0x0507000000000000), B!(0x0a0e000000000000), B!(0x141c000000000000), 45 | B!(0x2838000000000000), B!(0x5070000000000000), B!(0xa0e0000000000000), B!(0x40c0000000000000) 46 | ]); 47 | 48 | #[rustfmt::skip] 49 | const PAWN_ATTACKS: ColorMap>= ColorMap::new([ 50 | SQMap::new([ 51 | B!(0x0000000000000200), B!(0x0000000000000500), B!(0x0000000000000a00), B!(0x0000000000001400), 52 | B!(0x0000000000002800), B!(0x0000000000005000), B!(0x000000000000a000), B!(0x0000000000004000), 53 | B!(0x0000000000020000), B!(0x0000000000050000), B!(0x00000000000a0000), B!(0x0000000000140000), 54 | B!(0x0000000000280000), B!(0x0000000000500000), B!(0x0000000000a00000), B!(0x0000000000400000), 55 | B!(0x0000000002000000), B!(0x0000000005000000), B!(0x000000000a000000), B!(0x0000000014000000), 56 | B!(0x0000000028000000), B!(0x0000000050000000), B!(0x00000000a0000000), B!(0x0000000040000000), 57 | B!(0x0000000200000000), B!(0x0000000500000000), B!(0x0000000a00000000), B!(0x0000001400000000), 58 | B!(0x0000002800000000), B!(0x0000005000000000), B!(0x000000a000000000), B!(0x0000004000000000), 59 | B!(0x0000020000000000), B!(0x0000050000000000), B!(0x00000a0000000000), B!(0x0000140000000000), 60 | B!(0x0000280000000000), B!(0x0000500000000000), B!(0x0000a00000000000), B!(0x0000400000000000), 61 | B!(0x0002000000000000), B!(0x0005000000000000), B!(0x000a000000000000), B!(0x0014000000000000), 62 | B!(0x0028000000000000), B!(0x0050000000000000), B!(0x00a0000000000000), B!(0x0040000000000000), 63 | B!(0x0200000000000000), B!(0x0500000000000000), B!(0x0a00000000000000), B!(0x1400000000000000), 64 | B!(0x2800000000000000), B!(0x5000000000000000), B!(0xa000000000000000), B!(0x4000000000000000), 65 | B!(0x0000000000000000), B!(0x0000000000000000), B!(0x0000000000000000), B!(0x0000000000000000), 66 | B!(0x0000000000000000), B!(0x0000000000000000), B!(0x0000000000000000), B!(0x0000000000000000), 67 | ]), 68 | SQMap::new([ 69 | B!(0x00000000000000), B!(0x00000000000000), B!(0x00000000000000), B!(0x00000000000000), 70 | B!(0x00000000000000), B!(0x00000000000000), B!(0x00000000000000), B!(0x00000000000000), 71 | B!(0x00000000000002), B!(0x00000000000005), B!(0x0000000000000a), B!(0x00000000000014), 72 | B!(0x00000000000028), B!(0x00000000000050), B!(0x000000000000a0), B!(0x00000000000040), 73 | B!(0x00000000000200), B!(0x00000000000500), B!(0x00000000000a00), B!(0x00000000001400), 74 | B!(0x00000000002800), B!(0x00000000005000), B!(0x0000000000a000), B!(0x00000000004000), 75 | B!(0x00000000020000), B!(0x00000000050000), B!(0x000000000a0000), B!(0x00000000140000), 76 | B!(0x00000000280000), B!(0x00000000500000), B!(0x00000000a00000), B!(0x00000000400000), 77 | B!(0x00000002000000), B!(0x00000005000000), B!(0x0000000a000000), B!(0x00000014000000), 78 | B!(0x00000028000000), B!(0x00000050000000), B!(0x000000a0000000), B!(0x00000040000000), 79 | B!(0x00000200000000), B!(0x00000500000000), B!(0x00000a00000000), B!(0x00001400000000), 80 | B!(0x00002800000000), B!(0x00005000000000), B!(0x0000a000000000), B!(0x00004000000000), 81 | B!(0x00020000000000), B!(0x00050000000000), B!(0x000a0000000000), B!(0x00140000000000), 82 | B!(0x00280000000000), B!(0x00500000000000), B!(0x00a00000000000), B!(0x00400000000000), 83 | B!(0x02000000000000), B!(0x05000000000000), B!(0x0a000000000000), B!(0x14000000000000), 84 | B!(0x28000000000000), B!(0x50000000000000), B!(0xa0000000000000), B!(0x40000000000000)]) 85 | 86 | ]); 87 | 88 | pub fn rook_attacks(sq: SQ, occ: Bitboard) -> Bitboard { 89 | ROOK_MAGICS.attacks[sq][ROOK_MAGICS.index(sq, occ)] 90 | } 91 | 92 | pub fn bishop_attacks(sq: SQ, occ: Bitboard) -> Bitboard { 93 | BISHOP_MAGICS.attacks[sq][BISHOP_MAGICS.index(sq, occ)] 94 | } 95 | 96 | pub fn knight_attacks(sq: SQ) -> Bitboard { 97 | KNIGHT_ATTACKS[sq] 98 | } 99 | 100 | pub fn king_attacks(sq: SQ) -> Bitboard { 101 | ADJACENT_ATTACKS[sq] 102 | } 103 | 104 | pub fn pawn_attacks_bb(bb: Bitboard, color: Color) -> Bitboard { 105 | bb.shift(Direction::NorthWest.relative(color)) | bb.shift(Direction::NorthEast.relative(color)) 106 | } 107 | 108 | pub fn pawn_attacks_sq(sq: SQ, color: Color) -> Bitboard { 109 | PAWN_ATTACKS[color][sq] 110 | } 111 | 112 | pub fn sliding_attacks(sq: SQ, occ: Bitboard, mask: Bitboard) -> Bitboard { 113 | (((mask & occ) - sq.bb() * Bitboard::TWO) 114 | ^ ((mask & occ).reverse() - sq.bb().reverse() * Bitboard::TWO).reverse()) 115 | & mask 116 | } 117 | 118 | pub fn attacks(pt: PieceType, sq: SQ, occ: Bitboard) -> Bitboard { 119 | match pt { 120 | PieceType::Knight => knight_attacks(sq), 121 | PieceType::Bishop => bishop_attacks(sq, occ), 122 | PieceType::Rook => rook_attacks(sq, occ), 123 | PieceType::Queen => bishop_attacks(sq, occ) | rook_attacks(sq, occ), 124 | PieceType::King => king_attacks(sq), 125 | _ => Bitboard::ZERO, 126 | } 127 | } 128 | 129 | ////////////////////////////////////////////// 130 | // Inits 131 | ////////////////////////////////////////////// 132 | 133 | pub fn rook_attacks_for_init(sq: SQ, blockers: Bitboard) -> Bitboard { 134 | sliding_attacks(sq, blockers, sq.file().bb()) | sliding_attacks(sq, blockers, sq.rank().bb()) 135 | } 136 | 137 | pub fn bishop_attacks_for_init(sq: SQ, blockers: Bitboard) -> Bitboard { 138 | sliding_attacks(sq, blockers, sq.diagonal().bb()) 139 | | sliding_attacks(sq, blockers, sq.antidiagonal().bb()) 140 | } 141 | -------------------------------------------------------------------------------- /src/uci.rs: -------------------------------------------------------------------------------- 1 | use super::board::Board; 2 | use super::search_master::*; 3 | use super::timer::*; 4 | use regex::Regex; 5 | use std::io::BufRead; 6 | use std::sync::LazyLock; 7 | use std::sync::{ 8 | Arc, 9 | atomic::{AtomicBool, Ordering}, 10 | mpsc, 11 | }; 12 | use std::time::Duration; 13 | use std::{io, thread}; 14 | // Asymptote inspired a lot of this nice uci implementation. 15 | 16 | pub struct UCI { 17 | _main_thread: thread::JoinHandle<()>, 18 | main_tx: mpsc::Sender, 19 | stop: Arc, 20 | pondering: Arc, 21 | } 22 | 23 | impl UCI { 24 | pub fn new() -> Self { 25 | let (main_tx, main_rx) = mpsc::channel(); 26 | let stop = Arc::new(AtomicBool::new(false)); 27 | let pondering = Arc::new(AtomicBool::new(false)); 28 | Self { 29 | main_tx, 30 | stop: stop.clone(), 31 | pondering: pondering.clone(), 32 | _main_thread: thread::spawn(move || SearchMaster::new(stop, pondering).run(main_rx)), 33 | } 34 | } 35 | 36 | pub fn run(&self) { 37 | println!("Weiawaga v{}", env!("CARGO_PKG_VERSION")); 38 | println!("{}", env!("CARGO_PKG_REPOSITORY")); 39 | 40 | let stdin = io::stdin(); 41 | let lock = stdin.lock(); 42 | 43 | for line in lock 44 | .lines() 45 | .map(|line| line.expect("Unable to parse line.")) 46 | { 47 | match UCICommand::try_from(line.as_str()) { 48 | Ok(cmd) => match cmd { 49 | UCICommand::Quit => return, 50 | UCICommand::Stop => { 51 | self.stop.store(true, Ordering::Release); 52 | self.pondering.store(false, Ordering::Release); 53 | } 54 | UCICommand::PonderHit => self.pondering.store(false, Ordering::Release), 55 | _ => self 56 | .main_tx 57 | .send(cmd) 58 | .expect("Unable to communicate with main thread."), 59 | }, 60 | Err(e) => { 61 | eprintln!("{e}"); 62 | } 63 | } 64 | } 65 | } 66 | } 67 | 68 | pub enum EngineOption { 69 | Hash(usize), 70 | Threads(u16), 71 | MoveOverhead(Duration), 72 | Ponder(bool), 73 | ClearHash, 74 | } 75 | 76 | impl EngineOption { 77 | // Constants for Hash 78 | pub const HASH_MIN: usize = 1; 79 | pub const HASH_MAX: usize = 1048576; 80 | pub const HASH_DEFAULT: usize = 16; 81 | 82 | // Constants for Threads 83 | pub const THREADS_MIN: u16 = 1; 84 | pub const THREADS_MAX: u16 = 512; 85 | pub const THREADS_DEFAULT: u16 = 1; 86 | 87 | // Constants for MoveOverhead 88 | pub const MOVE_OVERHEAD_MIN: Duration = Duration::from_millis(0); 89 | pub const MOVE_OVERHEAD_MAX: Duration = Duration::from_millis(5000); 90 | pub const MOVE_OVERHEAD_DEFAULT: Duration = Duration::from_millis(10); 91 | 92 | // Constants for Ponder 93 | pub const PONDER_DEFAULT: bool = false; 94 | } 95 | 96 | impl TryFrom<&str> for EngineOption { 97 | type Error = &'static str; 98 | 99 | fn try_from(line: &str) -> Result { 100 | let re_captures = OPTION_RE.captures(line).ok_or("Unable to parse option.")?; 101 | 102 | let name = re_captures 103 | .name("name") 104 | .map(|m| m.as_str().to_string()) 105 | .ok_or("Invalid name in option.")?; 106 | 107 | let value = re_captures.name("value").map(|m| m.as_str().to_string()); 108 | 109 | let result = match name.as_str() { 110 | "Hash" => { 111 | let mb = value 112 | .ok_or("No mb value specified.")? 113 | .parse::() 114 | .map_err(|_| "Unable to parse hash mb.")?; 115 | Self::Hash(mb) 116 | } 117 | "Threads" => { 118 | let num_threads = value 119 | .ok_or("No threads value specified.")? 120 | .parse::() 121 | .map_err(|_| "Unable to parse number of threads.")?; 122 | Self::Threads(num_threads) 123 | } 124 | "Move Overhead" => { 125 | let ms = value 126 | .ok_or("No overhead value specified.")? 127 | .parse::() 128 | .map_err(|_| "Unable to parse overhead ms.")?; 129 | let overhead = Duration::from_millis(ms); 130 | Self::MoveOverhead(overhead) 131 | } 132 | "Ponder" => { 133 | let enabled = match value 134 | .ok_or("No ponder value specified.")? 135 | .trim() 136 | .to_ascii_lowercase() 137 | .as_str() 138 | { 139 | "true" | "on" | "1" => Ok(true), 140 | "false" | "off" | "0" => Ok(false), 141 | _ => Err("Unrecognized ponder value."), 142 | }?; 143 | Self::Ponder(enabled) 144 | } 145 | "Clear Hash" => Self::ClearHash, 146 | _ => { 147 | return Err("Unable to parse option."); 148 | } 149 | }; 150 | 151 | Ok(result) 152 | } 153 | } 154 | 155 | pub enum UCICommand { 156 | UCINewGame, 157 | UCI, 158 | IsReady, 159 | Position(Box), 160 | Go { 161 | time_control: TimeControl, 162 | ponder: bool, 163 | }, 164 | PonderHit, 165 | Quit, 166 | Stop, 167 | Perft(i8), 168 | Option(EngineOption), 169 | Eval, 170 | Fen, 171 | } 172 | 173 | impl TryFrom<&str> for UCICommand { 174 | type Error = &'static str; 175 | 176 | fn try_from(line: &str) -> Result { 177 | let line = line.trim(); 178 | 179 | let command = match line { 180 | "ucinewgame" => Self::UCINewGame, 181 | "stop" => Self::Stop, 182 | "uci" => Self::UCI, 183 | "eval" => Self::Eval, 184 | "fen" => Self::Fen, 185 | "ponderhit" => Self::PonderHit, 186 | "quit" => Self::Quit, 187 | "isready" => Self::IsReady, 188 | _ => { 189 | if line.starts_with("go") { 190 | Self::parse_go(line)? 191 | } else if line.starts_with("position") { 192 | Self::parse_position(line)? 193 | } else if line.starts_with("perft") { 194 | Self::parse_perft(line)? 195 | } else if line.starts_with("setoption") { 196 | Self::parse_option(line)? 197 | } else { 198 | return Err("Unknown command."); 199 | } 200 | } 201 | }; 202 | Ok(command) 203 | } 204 | } 205 | 206 | impl UCICommand { 207 | fn parse_go(line: &str) -> Result { 208 | let ponder = line.contains("ponder"); 209 | let time_control = TimeControl::try_from(line)?; 210 | Ok(Self::Go { 211 | time_control, 212 | ponder, 213 | }) 214 | } 215 | 216 | fn parse_position(line: &str) -> Result { 217 | let re_captures = POSITION_RE 218 | .captures(line) 219 | .ok_or("Invalid position format.")?; 220 | 221 | let fen = re_captures 222 | .name("startpos") 223 | .is_none() 224 | .then(|| { 225 | re_captures 226 | .name("fen") 227 | .map(|m| m.as_str()) 228 | .ok_or("Missing starting position.") 229 | }) 230 | .transpose()?; 231 | 232 | let moves = re_captures 233 | .name("moves") 234 | .map(|m| m.as_str().split_whitespace().collect::>()) 235 | .unwrap_or_default(); 236 | 237 | let mut board = match fen { 238 | Some(fen) => Board::try_from(fen)?, 239 | None => Board::new(), 240 | }; 241 | 242 | for m in moves { 243 | board.push_str(m)?; 244 | } 245 | 246 | Ok(Self::Position(Box::new(board))) 247 | } 248 | 249 | fn parse_option(line: &str) -> Result { 250 | let engine_option = EngineOption::try_from(line)?; 251 | Ok(Self::Option(engine_option)) 252 | } 253 | 254 | fn parse_perft(line: &str) -> Result { 255 | let re_captures = PERFT_RE.captures(line).ok_or("Invalid perft format.")?; 256 | 257 | re_captures 258 | .name("depth") 259 | .ok_or("Invalid perft format.")? 260 | .as_str() 261 | .parse::() 262 | .map_err(|_| "Invalid depth.") 263 | .map(Self::Perft) 264 | } 265 | } 266 | 267 | static POSITION_RE: LazyLock = LazyLock::new(|| { 268 | Regex::new( 269 | r"(?x)^ 270 | position\s+ 271 | (?:(?Pstartpos)|fen\s+(?P.+?)) 272 | (\s+moves\s+(?P(?:.+?)+))? 273 | $", 274 | ) 275 | .expect("Failed to compile position regex.") 276 | }); 277 | 278 | static OPTION_RE: LazyLock = LazyLock::new(|| { 279 | Regex::new( 280 | r"(?x)^ 281 | setoption\s+ 282 | name\s+(?P.*?) 283 | (?:\s+value\s+(?P.+))? 284 | $", 285 | ) 286 | .expect("Failed to compile option regex.") 287 | }); 288 | 289 | static PERFT_RE: LazyLock = LazyLock::new(|| { 290 | Regex::new( 291 | r"(?x)^ 292 | perft\s+ 293 | (?P.*?) 294 | $", 295 | ) 296 | .expect("Failed to compile perft regex.") 297 | }); 298 | -------------------------------------------------------------------------------- /src/square.rs: -------------------------------------------------------------------------------- 1 | use super::bitboard::*; 2 | use super::traits::*; 3 | use super::types::*; 4 | use std::fmt; 5 | use std::ops::{Add, Sub}; 6 | 7 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Debug)] 8 | #[rustfmt::skip] 9 | #[repr(u8)] 10 | pub enum SQ { 11 | A1, B1, C1, D1, E1, F1, G1, H1, 12 | A2, B2, C2, D2, E2, F2, G2, H2, 13 | A3, B3, C3, D3, E3, F3, G3, H3, 14 | A4, B4, C4, D4, E4, F4, G4, H4, 15 | A5, B5, C5, D5, E5, F5, G5, H5, 16 | A6, B6, C6, D6, E6, F6, G6, H6, 17 | A7, B7, C7, D7, E7, F7, G7, H7, 18 | A8, B8, C8, D8, E8, F8, G8, H8, 19 | } 20 | 21 | impl SQ { 22 | pub fn encode(rank: Rank, file: File) -> Self { 23 | Self::from(((rank as u8) << 3) + (file as u8)) 24 | } 25 | 26 | pub fn bb(self) -> Bitboard { 27 | B!(1 << self as usize) 28 | } 29 | 30 | pub fn index(self) -> usize { 31 | self as usize 32 | } 33 | 34 | pub fn rank(self) -> Rank { 35 | Rank::from(self as u8 >> 3) 36 | } 37 | 38 | pub fn file(self) -> File { 39 | File::from(self as u8 & 7) 40 | } 41 | 42 | pub fn diagonal(self) -> Diagonal { 43 | let value = self as u8; 44 | Diagonal::from(7 + (value >> 3) - (value & 7)) 45 | } 46 | 47 | pub fn antidiagonal(self) -> AntiDiagonal { 48 | let value = self as u8; 49 | AntiDiagonal::from((value >> 3) + (value & 7)) 50 | } 51 | 52 | pub fn iter(start: Self, end: Self) -> impl Iterator { 53 | (start as u8..=end as u8).map(Self::from) 54 | } 55 | } 56 | 57 | impl Mirror for SQ { 58 | fn mirror(&self) -> Self { 59 | Self::from(*self as u8 ^ 0x38) 60 | } 61 | } 62 | 63 | impl Add for SQ { 64 | type Output = Self; 65 | 66 | fn add(self, dir: Direction) -> Self { 67 | Self::from((self as u8).wrapping_add(dir as u8)) 68 | } 69 | } 70 | 71 | impl Sub for SQ { 72 | type Output = Self; 73 | 74 | fn sub(self, dir: Direction) -> Self { 75 | Self::from((self as u8).wrapping_sub(dir as u8)) 76 | } 77 | } 78 | 79 | impl From for SQ { 80 | fn from(n: u8) -> Self { 81 | unsafe { std::mem::transmute::(n) } 82 | } 83 | } 84 | 85 | impl fmt::Display for SQ { 86 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 87 | let file = self.file(); 88 | let rank = self.rank(); 89 | write!(f, "{file}{rank}") 90 | } 91 | } 92 | 93 | impl TryFrom<&str> for SQ { 94 | type Error = &'static str; 95 | 96 | fn try_from(value: &str) -> Result { 97 | let mut chars = value.chars(); 98 | 99 | let file_char = chars.next().ok_or("Invalid square.")?; 100 | let rank_char = chars.next().ok_or("Invalid square.")?; 101 | 102 | if chars.next().is_some() { 103 | return Err("Invalid square."); 104 | } 105 | 106 | let file = File::try_from(file_char)?; 107 | let rank = Rank::try_from(rank_char)?; 108 | 109 | Ok(Self::encode(rank, file)) 110 | } 111 | } 112 | 113 | impl Into for SQ { 114 | fn into(self) -> usize { 115 | self.index() 116 | } 117 | } 118 | 119 | impl SQ { 120 | pub const N_SQUARES: usize = 64; 121 | } 122 | 123 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Debug)] 124 | #[repr(i8)] 125 | pub enum Direction { 126 | NorthNorth = 16, 127 | North = 8, 128 | South = -8, 129 | SouthSouth = -16, 130 | East = 1, 131 | West = -1, 132 | NorthEast = 9, 133 | NorthWest = 7, 134 | SouthEast = -7, 135 | SouthWest = -9, 136 | } 137 | 138 | impl Mirror for Direction { 139 | fn mirror(&self) -> Self { 140 | Direction::from(-(*self as i8)) 141 | } 142 | } 143 | 144 | impl From for Direction { 145 | fn from(n: i8) -> Self { 146 | unsafe { std::mem::transmute::(n) } 147 | } 148 | } 149 | 150 | #[derive(Clone, Copy, PartialEq, Eq, Debug)] 151 | #[repr(u8)] 152 | pub enum Rank { 153 | One, 154 | Two, 155 | Three, 156 | Four, 157 | Five, 158 | Six, 159 | Seven, 160 | Eight, 161 | } 162 | 163 | impl Rank { 164 | pub fn bb(self) -> Bitboard { 165 | Self::RANK_BB[self as usize] 166 | } 167 | 168 | pub fn index(self) -> usize { 169 | self as usize 170 | } 171 | } 172 | 173 | impl Mirror for Rank { 174 | fn mirror(&self) -> Self { 175 | Self::from((*self as u8) ^ 7) 176 | } 177 | } 178 | 179 | impl From for Rank { 180 | fn from(n: u8) -> Self { 181 | unsafe { std::mem::transmute::(n) } 182 | } 183 | } 184 | 185 | impl Into for Rank { 186 | fn into(self) -> usize { 187 | self.index() 188 | } 189 | } 190 | 191 | impl fmt::Display for Rank { 192 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 193 | write!(f, "{}", *self as u8 + 1) 194 | } 195 | } 196 | 197 | impl TryFrom for Rank { 198 | type Error = &'static str; 199 | 200 | fn try_from(value: char) -> Result { 201 | let n = value.to_digit(10).ok_or("Invalid rank.")? as u8; 202 | match n { 203 | 1..=8 => Ok(Self::from(n - 1)), 204 | _ => Err("Invalid rank."), 205 | } 206 | } 207 | } 208 | 209 | impl Rank { 210 | pub const N_RANKS: usize = 8; 211 | const RANK_BB: RankMap = RankMap::new([ 212 | B!(0x0000_0000_0000_00FF), 213 | B!(0x0000_0000_0000_FF00), 214 | B!(0x0000_0000_00FF_0000), 215 | B!(0x0000_0000_FF00_0000), 216 | B!(0x0000_00FF_0000_0000), 217 | B!(0x0000_FF00_0000_0000), 218 | B!(0x00FF_0000_0000_0000), 219 | B!(0xFF00_0000_0000_0000), 220 | ]); 221 | } 222 | 223 | #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Debug)] 224 | #[repr(u8)] 225 | pub enum File { 226 | A, 227 | B, 228 | C, 229 | D, 230 | E, 231 | F, 232 | G, 233 | H, 234 | } 235 | 236 | impl File { 237 | pub fn bb(self) -> Bitboard { 238 | Self::FILE_BB[self as usize] 239 | } 240 | 241 | pub fn index(self) -> usize { 242 | self as usize 243 | } 244 | } 245 | 246 | impl From for File { 247 | fn from(n: u8) -> Self { 248 | unsafe { std::mem::transmute::(n) } 249 | } 250 | } 251 | 252 | impl Into for File { 253 | fn into(self) -> usize { 254 | self.index() 255 | } 256 | } 257 | 258 | impl TryFrom for File { 259 | type Error = &'static str; 260 | 261 | fn try_from(value: char) -> Result { 262 | let byte = value as u8; 263 | match byte { 264 | b'a'..=b'h' => Ok(Self::from(byte - b'a')), 265 | _ => Err("Invalid file."), 266 | } 267 | } 268 | } 269 | 270 | impl fmt::Display for File { 271 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 272 | let file_char = b'a' + *self as u8; 273 | write!(f, "{}", file_char as char) 274 | } 275 | } 276 | 277 | impl File { 278 | pub const N_FILES: usize = 8; 279 | const FILE_BB: FileMap = FileMap::new([ 280 | B!(0x0101_0101_0101_0101), 281 | B!(0x0202_0202_0202_0202), 282 | B!(0x0404_0404_0404_0404), 283 | B!(0x0808_0808_0808_0808), 284 | B!(0x1010_1010_1010_1010), 285 | B!(0x2020_2020_2020_2020), 286 | B!(0x4040_4040_4040_4040), 287 | B!(0x8080_8080_8080_8080), 288 | ]); 289 | } 290 | 291 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 292 | #[repr(u8)] 293 | pub enum Diagonal { 294 | H1H1, 295 | G1H2, 296 | F1H3, 297 | E1H4, 298 | D1H5, 299 | C1H6, 300 | B1H7, 301 | H8A1, 302 | G8A2, 303 | F8A3, 304 | E8A4, 305 | D8A5, 306 | C8A6, 307 | B8A7, 308 | A8A8, 309 | } 310 | 311 | impl Diagonal { 312 | pub fn bb(self) -> Bitboard { 313 | Self::DIAGONAL_BB[self as usize] 314 | } 315 | } 316 | 317 | impl From for Diagonal { 318 | fn from(n: u8) -> Self { 319 | unsafe { std::mem::transmute::(n) } 320 | } 321 | } 322 | 323 | impl Diagonal { 324 | pub const N_DIAGONALS: usize = 15; 325 | const DIAGONAL_BB: DiagonalMap = DiagonalMap::new([ 326 | B!(0x0000_0000_0000_0080), 327 | B!(0x0000_0000_0000_8040), 328 | B!(0x0000_0000_0080_4020), 329 | B!(0x0000_0000_8040_2010), 330 | B!(0x0000_0080_4020_1008), 331 | B!(0x0000_8040_2010_0804), 332 | B!(0x0080_4020_1008_0402), 333 | B!(0x8040_2010_0804_0201), 334 | B!(0x4020_1008_0402_0100), 335 | B!(0x2010_0804_0201_0000), 336 | B!(0x1008_0402_0100_0000), 337 | B!(0x0804_0201_0000_0000), 338 | B!(0x0402_0100_0000_0000), 339 | B!(0x0201_0000_0000_0000), 340 | B!(0x0100_0000_0000_0000), 341 | ]); 342 | } 343 | 344 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 345 | #[repr(u8)] 346 | pub enum AntiDiagonal { 347 | A1A1, 348 | B1A2, 349 | C1A3, 350 | D1A4, 351 | E1A5, 352 | F1A6, 353 | G1A7, 354 | H1A8, 355 | B8H2, 356 | C8H3, 357 | D8H4, 358 | E8H5, 359 | F8H6, 360 | G8H7, 361 | H8H8, 362 | } 363 | 364 | impl AntiDiagonal { 365 | pub fn bb(self) -> Bitboard { 366 | Self::ANTIDIAGONAL_BB[self as usize] 367 | } 368 | } 369 | 370 | impl From for AntiDiagonal { 371 | fn from(n: u8) -> Self { 372 | unsafe { std::mem::transmute::(n) } 373 | } 374 | } 375 | 376 | impl AntiDiagonal { 377 | pub const N_ANTIDIAGONALS: usize = 15; 378 | const ANTIDIAGONAL_BB: DiagonalMap = DiagonalMap::new([ 379 | B!(0x0000_0000_0000_0001), 380 | B!(0x0000_0000_0000_0102), 381 | B!(0x0000_0000_0001_0204), 382 | B!(0x0000_0000_0102_0408), 383 | B!(0x0000_0001_0204_0810), 384 | B!(0x0000_0102_0408_1020), 385 | B!(0x0001_0204_0810_2040), 386 | B!(0x0102_0408_1020_4080), 387 | B!(0x0204_0810_2040_8000), 388 | B!(0x0408_1020_4080_0000), 389 | B!(0x0810_2040_8000_0000), 390 | B!(0x1020_4080_0000_0000), 391 | B!(0x2040_8000_0000_0000), 392 | B!(0x4080_0000_0000_0000), 393 | B!(0x8000_0000_0000_0000), 394 | ]); 395 | } 396 | -------------------------------------------------------------------------------- /src/timer.rs: -------------------------------------------------------------------------------- 1 | use super::board::*; 2 | use super::moov::*; 3 | use super::piece::*; 4 | use super::square::*; 5 | use super::types::*; 6 | use regex::{Captures, Regex}; 7 | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 8 | use std::sync::{Arc, LazyLock}; 9 | use std::time::{Duration, Instant}; 10 | use std::{convert::TryFrom, str::FromStr}; 11 | 12 | // Some ideas taken from asymptote, which has a very elegant timer implementation. 13 | #[derive(Copy, Clone, PartialEq, Eq, Debug)] 14 | pub enum TimeControl { 15 | Infinite, 16 | FixedDuration(Duration), 17 | FixedDepth(i8), 18 | FixedNodes(u64), 19 | Variable { 20 | wtime: Duration, 21 | btime: Duration, 22 | winc: Option, 23 | binc: Option, 24 | moves_to_go: Option, 25 | }, 26 | } 27 | 28 | impl TimeControl { 29 | fn opt_number( 30 | caps: &Captures, 31 | name: &'static str, 32 | err: &'static str, 33 | ) -> Result, &'static str> { 34 | caps.name(name) 35 | .map(|m| m.as_str().parse::().map_err(|_| err)) 36 | .transpose() 37 | } 38 | 39 | fn opt_duration(caps: &Captures, name: &'static str) -> Result, &'static str> { 40 | Self::opt_number::(caps, name, "Unable to parse time.")? 41 | .map(|ms| Ok(Duration::from_millis(ms.max(0) as u64))) 42 | .transpose() 43 | } 44 | 45 | fn parse_fixed(caps: &Captures) -> Result, &'static str> { 46 | let mut iter = [ 47 | Self::opt_number::(caps, "nodes", "Unable to parse nodes.")?.map(Self::FixedNodes), 48 | Self::opt_number::(caps, "depth", "Unable to parse depth.")?.map(Self::FixedDepth), 49 | Self::opt_duration(caps, "movetime")?.map(Self::FixedDuration), 50 | ] 51 | .into_iter() 52 | .flatten(); 53 | 54 | let first = iter.next(); 55 | if iter.next().is_some() { 56 | return Err("Only one of depth, nodes, or movetime may be given."); 57 | } 58 | 59 | Ok(first) 60 | } 61 | 62 | fn parse_variable(caps: &Captures) -> Result, &'static str> { 63 | let wtime = Self::opt_duration(caps, "wtime")?; 64 | let btime = Self::opt_duration(caps, "btime")?; 65 | 66 | if wtime.is_none() && btime.is_none() { 67 | return Ok(None); 68 | } 69 | 70 | let winc = Self::opt_duration(caps, "winc")?; 71 | let binc = Self::opt_duration(caps, "binc")?; 72 | let moves_to_go = Self::opt_number::(caps, "movestogo", "Unable to parse movestogo.")?; 73 | 74 | Ok(Some(Self::Variable { 75 | wtime: wtime.unwrap_or(Duration::ZERO), 76 | btime: btime.unwrap_or(Duration::ZERO), 77 | winc, 78 | binc, 79 | moves_to_go, 80 | })) 81 | } 82 | } 83 | 84 | impl TryFrom<&str> for TimeControl { 85 | type Error = &'static str; 86 | 87 | fn try_from(line: &str) -> Result { 88 | if matches!(line, "go" | "go ponder") { 89 | return Ok(TimeControl::Infinite); 90 | } 91 | 92 | let caps = GO_RE.captures(line).ok_or("Invalid go format.")?; 93 | 94 | if caps.name("searchmoves").is_some() || caps.name("mate").is_some() { 95 | return Err("Feature is not implemented."); 96 | } 97 | 98 | Self::parse_fixed(&caps)? 99 | .xor(Self::parse_variable(&caps)?) 100 | .ok_or("No recognizable or bad combination of go parameters provided.") 101 | } 102 | } 103 | 104 | static GO_RE: LazyLock = LazyLock::new(|| { 105 | Regex::new( 106 | r"(?x)^ 107 | go 108 | (?: 109 | \s+depth\s+(?P\d+) | 110 | \s+nodes\s+(?P\d+) | 111 | \s+movetime\s+(?P\d+) | 112 | \s+wtime\s+(?P-?\d+) | 113 | \s+btime\s+(?P-?\d+) | 114 | \s+winc\s+(?P\d+) | 115 | \s+binc\s+(?P\d+) | 116 | \s+mate\s+(?P\d+) | 117 | \s+movestogo\s+(?P\d+) | 118 | \s+ponder 119 | )* 120 | $", 121 | ) 122 | .expect("Go regex should be valid.") 123 | }); 124 | 125 | #[derive(Clone)] 126 | pub struct Timer { 127 | control: TimeControl, 128 | start_time: Instant, 129 | pondering: Arc, 130 | stop: Arc, 131 | nodes: Arc, 132 | batch: u64, 133 | time_target: Duration, 134 | time_maximum: Duration, 135 | overhead: Duration, 136 | current_nodes: u64, 137 | nodes_table: SQMap>, 138 | } 139 | 140 | impl Timer { 141 | pub fn new( 142 | board: &Board, 143 | control: TimeControl, 144 | pondering: Arc, 145 | stop: Arc, 146 | nodes: Arc, 147 | overhead: Duration, 148 | ) -> Self { 149 | let (time_target, time_maximum) = if let TimeControl::Variable { .. } = control { 150 | Self::calculate_time(board, control) 151 | } else { 152 | (Duration::ZERO, Duration::ZERO) 153 | }; 154 | 155 | Self { 156 | start_time: Instant::now(), 157 | pondering, 158 | stop, 159 | batch: 0, 160 | nodes, 161 | control, 162 | overhead, 163 | time_target, 164 | time_maximum, 165 | current_nodes: 0, 166 | nodes_table: SQMap::new([SQMap::new([0; SQ::N_SQUARES]); SQ::N_SQUARES]), 167 | } 168 | } 169 | 170 | fn calculate_time(board: &Board, control: TimeControl) -> (Duration, Duration) { 171 | let TimeControl::Variable { 172 | wtime, 173 | btime, 174 | winc, 175 | binc, 176 | moves_to_go, 177 | } = control 178 | else { 179 | unreachable!() 180 | }; 181 | 182 | let (time, inc) = match board.ctm() { 183 | Color::White => (wtime, winc), 184 | Color::Black => (btime, binc), 185 | }; 186 | 187 | let mtg = moves_to_go.unwrap_or(40); 188 | 189 | let time_target = time.min(time / mtg + inc.unwrap_or(Duration::ZERO)); 190 | let time_maximum = time_target + (time - time_target) / 4; 191 | 192 | (time_target, time_maximum) 193 | } 194 | 195 | pub fn start_check(&mut self, best_move: Option, depth: i8) -> bool { 196 | if self.stop.load(Ordering::Acquire) { 197 | return false; 198 | } 199 | 200 | if self.pondering.load(Ordering::Acquire) { 201 | return true; 202 | } 203 | 204 | let start = match self.control { 205 | TimeControl::Infinite => true, 206 | TimeControl::FixedDuration(duration) => self.elapsed() + self.overhead <= duration, 207 | TimeControl::FixedDepth(stop_depth) => depth <= stop_depth, 208 | TimeControl::FixedNodes(_) => true, 209 | TimeControl::Variable { .. } => { 210 | self.elapsed() + self.overhead 211 | <= self 212 | .time_target 213 | .mul_f64(self.scale_factor(best_move, depth)) 214 | / 2 215 | } 216 | }; 217 | 218 | if !start { 219 | self.set_stop(); 220 | } 221 | start 222 | } 223 | 224 | pub fn stop_check(&mut self) -> bool { 225 | self.increment(); 226 | 227 | if self.stop.load(Ordering::Acquire) { 228 | return true; 229 | } 230 | 231 | if self.pondering.load(Ordering::Acquire) { 232 | return false; 233 | } 234 | 235 | let stop = match self.control { 236 | TimeControl::Infinite => false, 237 | TimeControl::FixedDuration(duration) => self.elapsed() + self.overhead >= duration, 238 | TimeControl::Variable { .. } => self.elapsed() + self.overhead >= self.time_maximum, 239 | TimeControl::FixedDepth(_) => false, 240 | TimeControl::FixedNodes(stop_nodes) => self.nodes() >= stop_nodes, 241 | }; 242 | 243 | if stop { 244 | self.set_stop(); 245 | } 246 | 247 | stop 248 | } 249 | 250 | pub fn set_stop(&mut self) { 251 | self.stop.store(true, Ordering::Release); 252 | } 253 | 254 | pub fn elapsed(&self) -> Duration { 255 | self.start_time.elapsed() 256 | } 257 | 258 | pub fn increment(&mut self) { 259 | self.batch += 1; 260 | self.current_nodes += 1; 261 | if self.batch >= Self::BATCH_SIZE { 262 | self.nodes.fetch_add(self.batch, Ordering::Relaxed); 263 | self.batch = 0; 264 | } 265 | } 266 | 267 | pub fn nodes(&self) -> u64 { 268 | self.nodes.load(Ordering::Relaxed) + self.batch 269 | } 270 | 271 | pub fn is_stopped(&self) -> bool { 272 | self.stop.load(Ordering::Acquire) 273 | } 274 | 275 | pub fn update_node_table(&mut self, m: Move) { 276 | let (from_sq, to_sq) = m.squares(); 277 | self.nodes_table[from_sq][to_sq] += self.current_nodes; 278 | self.current_nodes = 0; 279 | } 280 | 281 | pub fn scale_factor(&self, best_move: Option, depth: i8) -> f64 { 282 | let Some(m) = best_move else { 283 | return 1.0; 284 | }; 285 | 286 | if depth <= Self::SEARCHES_WO_TIMER_UPDATE { 287 | return 1.0; 288 | } 289 | 290 | let total_nodes = self.nodes_table.into_iter().flatten().sum::(); 291 | if total_nodes == 0 { 292 | return 1.0; 293 | } 294 | 295 | let (from_sq, to_sq) = m.squares(); 296 | let effort_ratio = self.nodes_table[from_sq][to_sq] as f64 / total_nodes as f64; 297 | let logistic = 1.0 / (1.0 + (-Self::K * (effort_ratio - Self::X0)).exp()); 298 | Self::MIN_TIMER_UPDATE 299 | + (Self::MAX_TIMER_UPDATE - Self::MIN_TIMER_UPDATE) * (1.0 - logistic) 300 | } 301 | } 302 | 303 | impl Timer { 304 | const BATCH_SIZE: u64 = 4096; 305 | const K: f64 = 10.0; 306 | const X0: f64 = 0.5; 307 | const MIN_TIMER_UPDATE: f64 = 0.5; 308 | const MAX_TIMER_UPDATE: f64 = 3.0; 309 | const SEARCHES_WO_TIMER_UPDATE: i8 = 8; 310 | } 311 | -------------------------------------------------------------------------------- /src/bitboard.rs: -------------------------------------------------------------------------------- 1 | use super::attacks; 2 | use super::piece::*; 3 | use super::square::*; 4 | use super::types::*; 5 | use std::fmt; 6 | use std::ops::{ 7 | BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Mul, Not, Shl, ShlAssign, Shr, 8 | ShrAssign, Sub, 9 | }; 10 | use std::sync::LazyLock; 11 | 12 | #[derive(Clone, Copy, Default, PartialEq, Eq)] 13 | pub struct Bitboard(pub u64); 14 | 15 | #[macro_export] 16 | macro_rules! B { 17 | ($x:expr) => { 18 | Bitboard($x) 19 | }; 20 | } 21 | 22 | impl Bitboard { 23 | pub fn lsb(&self) -> SQ { 24 | SQ::from(self.0.trailing_zeros() as u8) 25 | } 26 | 27 | pub fn msb(&self) -> SQ { 28 | SQ::from((63 - self.0.leading_zeros()) as u8) 29 | } 30 | 31 | pub fn pop_lsb(&mut self) -> SQ { 32 | let sq = self.lsb(); 33 | self.0 &= self.0 - 1; 34 | sq 35 | } 36 | 37 | pub fn is_several(&self) -> bool { 38 | self.0 & (self.0.wrapping_sub(1)) != 0 39 | } 40 | 41 | pub fn is_single(&self) -> bool { 42 | self.0 != 0 && !self.is_several() 43 | } 44 | 45 | pub fn pop_count(&self) -> u8 { 46 | self.0.count_ones() as u8 47 | } 48 | 49 | pub fn shift(self, dir: Direction) -> Self { 50 | match dir { 51 | Direction::North => self << 8, 52 | Direction::South => self >> 8, 53 | Direction::NorthNorth => self << 16, 54 | Direction::SouthSouth => self >> 16, 55 | Direction::East => (self << 1) & !File::A.bb(), 56 | Direction::West => (self >> 1) & !File::H.bb(), 57 | Direction::NorthEast => (self & !File::H.bb()) << 9, 58 | Direction::NorthWest => (self & !File::A.bb()) << 7, 59 | Direction::SouthEast => (self & !File::H.bb()) >> 7, 60 | Direction::SouthWest => (self & !File::A.bb()) >> 9, 61 | } 62 | } 63 | 64 | pub fn reverse(self) -> Self { 65 | Self(self.0.reverse_bits()) 66 | } 67 | 68 | pub fn fill(self, dir: Direction) -> Self { 69 | match dir { 70 | Direction::North => self | (self << 8) | (self << 16) | (self << 32), 71 | Direction::South => self | (self >> 8) | (self >> 16) | (self >> 32), 72 | _ => { 73 | panic!("Filling a file by something other than North or South.") 74 | } 75 | } 76 | } 77 | } 78 | 79 | ////////////////////////////////////////////// 80 | // Static 81 | ////////////////////////////////////////////// 82 | 83 | impl Bitboard { 84 | pub fn line(sq1: SQ, sq2: SQ) -> Self { 85 | LINES_BB[sq1][sq2] 86 | } 87 | 88 | pub fn between(sq1: SQ, sq2: SQ) -> Self { 89 | BETWEEN_BB[sq1][sq2] 90 | } 91 | 92 | pub fn oo_mask(c: Color) -> Self { 93 | match c { 94 | Color::White => Self::WHITE_OO_MASK, 95 | Color::Black => Self::BLACK_OO_MASK, 96 | } 97 | } 98 | 99 | pub fn ooo_mask(c: Color) -> Self { 100 | match c { 101 | Color::White => Self::WHITE_OOO_MASK, 102 | Color::Black => Self::BLACK_OOO_MASK, 103 | } 104 | } 105 | 106 | pub fn oo_blockers_mask(c: Color) -> Self { 107 | match c { 108 | Color::White => Self::WHITE_OO_BLOCKERS_AND_ATTACKERS_MASK, 109 | Color::Black => Self::BLACK_OO_BLOCKERS_AND_ATTACKERS_MASK, 110 | } 111 | } 112 | 113 | pub fn ooo_blockers_mask(c: Color) -> Self { 114 | match c { 115 | Color::White => Self::WHITE_OOO_BLOCKERS_AND_ATTACKERS_MASK, 116 | Color::Black => Self::BLACK_OOO_BLOCKERS_AND_ATTACKERS_MASK, 117 | } 118 | } 119 | 120 | pub fn ignore_ooo_danger(c: Color) -> Self { 121 | match c { 122 | Color::White => Self::WHITE_OOO_DANGER, 123 | Color::Black => Self::BLACK_OOO_DANGER, 124 | } 125 | } 126 | } 127 | 128 | impl From for Bitboard { 129 | fn from(value: u64) -> Self { 130 | Self(value) 131 | } 132 | } 133 | 134 | ////////////////////////////////////////////// 135 | // Shifting Operations 136 | ////////////////////////////////////////////// 137 | 138 | impl Shl for Bitboard 139 | where 140 | u64: Shl, 141 | { 142 | type Output = Self; 143 | 144 | fn shl(self, rhs: T) -> Self::Output { 145 | Self(self.0 << rhs) 146 | } 147 | } 148 | 149 | impl ShlAssign for Bitboard 150 | where 151 | u64: ShlAssign, 152 | { 153 | fn shl_assign(&mut self, rhs: T) { 154 | self.0 <<= rhs; 155 | } 156 | } 157 | 158 | impl Shr for Bitboard 159 | where 160 | u64: Shr, 161 | { 162 | type Output = Self; 163 | 164 | fn shr(self, rhs: T) -> Self::Output { 165 | Self(self.0 >> rhs) 166 | } 167 | } 168 | 169 | impl ShrAssign for Bitboard 170 | where 171 | u64: ShrAssign, 172 | { 173 | fn shr_assign(&mut self, rhs: T) { 174 | self.0 >>= rhs; 175 | } 176 | } 177 | 178 | ////////////////////////////////////////////// 179 | // Bitwise Operations 180 | ////////////////////////////////////////////// 181 | 182 | impl BitAnd for Bitboard { 183 | type Output = Self; 184 | 185 | fn bitand(self, rhs: Self) -> Self::Output { 186 | Self(self.0 & rhs.0) 187 | } 188 | } 189 | 190 | impl BitAndAssign for Bitboard { 191 | fn bitand_assign(&mut self, rhs: Self) { 192 | self.0 &= rhs.0; 193 | } 194 | } 195 | 196 | impl BitOr for Bitboard { 197 | type Output = Self; 198 | 199 | fn bitor(self, rhs: Self) -> Self::Output { 200 | Self(self.0 | rhs.0) 201 | } 202 | } 203 | 204 | impl BitOrAssign for Bitboard { 205 | fn bitor_assign(&mut self, rhs: Self) { 206 | self.0 |= rhs.0; 207 | } 208 | } 209 | 210 | impl BitXor for Bitboard { 211 | type Output = Self; 212 | 213 | fn bitxor(self, rhs: Self) -> Self::Output { 214 | Self(self.0 ^ rhs.0) 215 | } 216 | } 217 | 218 | impl BitXorAssign for Bitboard { 219 | fn bitxor_assign(&mut self, rhs: Self) { 220 | self.0 ^= rhs.0; 221 | } 222 | } 223 | 224 | impl Not for Bitboard { 225 | type Output = Self; 226 | 227 | fn not(self) -> Self::Output { 228 | Self(!self.0) 229 | } 230 | } 231 | 232 | ////////////////////////////////////////////// 233 | // Arithmetic for Magic BitBoards 234 | ////////////////////////////////////////////// 235 | 236 | impl Mul for Bitboard { 237 | type Output = Self; 238 | 239 | fn mul(self, rhs: Self) -> Self::Output { 240 | Self(self.0.wrapping_mul(rhs.0)) 241 | } 242 | } 243 | 244 | impl Sub for Bitboard { 245 | type Output = Self; 246 | 247 | fn sub(self, rhs: Self) -> Self::Output { 248 | Self(self.0.wrapping_sub(rhs.0)) 249 | } 250 | } 251 | 252 | ////////////////////////////////////////////// 253 | // Iterator 254 | ////////////////////////////////////////////// 255 | 256 | impl Iterator for Bitboard { 257 | type Item = SQ; 258 | 259 | fn next(&mut self) -> Option { 260 | if self.0 == 0 { 261 | return None; 262 | } 263 | Some(self.pop_lsb()) 264 | } 265 | } 266 | 267 | ////////////////////////////////////////////// 268 | // Display 269 | ////////////////////////////////////////////// 270 | 271 | impl fmt::Debug for Bitboard { 272 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 273 | let mut result = String::new(); 274 | for i in (0..=56).rev().step_by(8) { 275 | for j in 0..8 { 276 | result.push_str(format!("{} ", self.0 >> (i + j) & 1).as_str()); 277 | } 278 | result.push('\n'); 279 | } 280 | write!(f, "{result}") 281 | } 282 | } 283 | 284 | impl fmt::Display for Bitboard { 285 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 286 | write!(f, "{:#x}", self.0) 287 | } 288 | } 289 | 290 | ////////////////////////////////////////////// 291 | // Constants 292 | ////////////////////////////////////////////// 293 | 294 | impl Bitboard { 295 | pub const ALL: Self = B!(0xffffffffffffffff); 296 | pub const ZERO: Self = B!(0); 297 | pub const ONE: Self = B!(1); 298 | pub const TWO: Self = B!(2); 299 | pub const LIGHT_SQUARES: Self = B!(0x55AA55AA55AA55AA); 300 | pub const DARK_SQUARES: Self = B!(0xAA55AA55AA55AA55); 301 | 302 | pub const WHITE_OO_MASK: Self = B!(0x90); 303 | pub const WHITE_OOO_MASK: Self = B!(0x11); 304 | pub const WHITE_OO_BLOCKERS_AND_ATTACKERS_MASK: Self = B!(0x60); 305 | pub const WHITE_OOO_BLOCKERS_AND_ATTACKERS_MASK: Self = B!(0xe); 306 | 307 | pub const BLACK_OO_MASK: Self = B!(0x9000000000000000); 308 | pub const BLACK_OOO_MASK: Self = B!(0x1100000000000000); 309 | pub const BLACK_OO_BLOCKERS_AND_ATTACKERS_MASK: Self = B!(0x6000000000000000); 310 | pub const BLACK_OOO_BLOCKERS_AND_ATTACKERS_MASK: Self = B!(0xE00000000000000); 311 | 312 | pub const ALL_CASTLING_MASK: Self = B!(0x9100000000000091); 313 | 314 | pub const WHITE_OOO_DANGER: Self = B!(0x2); 315 | pub const BLACK_OOO_DANGER: Self = B!(0x200000000000000); 316 | 317 | pub const CENTER: Self = B!(0x1818000000); 318 | } 319 | 320 | static BETWEEN_BB: LazyLock>> = LazyLock::new(|| { 321 | let mut between_bb = SQMap::new([SQMap::new([B!(0); SQ::N_SQUARES]); SQ::N_SQUARES]); 322 | for sq1 in Bitboard::ALL { 323 | for sq2 in Bitboard::ALL { 324 | let sqs = sq1.bb() | sq2.bb(); 325 | if sq1.file() == sq2.file() || sq1.rank() == sq2.rank() { 326 | between_bb[sq1][sq2] = attacks::rook_attacks_for_init(sq1, sqs) 327 | & attacks::rook_attacks_for_init(sq2, sqs); 328 | } else if sq1.diagonal() == sq2.diagonal() || sq1.antidiagonal() == sq2.antidiagonal() { 329 | between_bb[sq1][sq2] = attacks::bishop_attacks_for_init(sq1, sqs) 330 | & attacks::bishop_attacks_for_init(sq2, sqs); 331 | } 332 | } 333 | } 334 | between_bb 335 | }); 336 | static LINES_BB: LazyLock>> = LazyLock::new(|| { 337 | let mut lines_bb = SQMap::new([SQMap::new([B!(0); SQ::N_SQUARES]); SQ::N_SQUARES]); 338 | for sq1 in Bitboard::ALL { 339 | for sq2 in Bitboard::ALL { 340 | if sq1.file() == sq2.file() || sq1.rank() == sq2.rank() { 341 | lines_bb[sq1][sq2] = attacks::rook_attacks_for_init(sq1, Bitboard::ZERO) 342 | & attacks::rook_attacks_for_init(sq2, Bitboard::ZERO) 343 | | sq1.bb() 344 | | sq2.bb(); 345 | } else if sq1.diagonal() == sq2.diagonal() || sq1.antidiagonal() == sq2.antidiagonal() { 346 | lines_bb[sq1][sq2] = attacks::bishop_attacks_for_init(sq1, Bitboard::ZERO) 347 | & attacks::bishop_attacks_for_init(sq2, Bitboard::ZERO) 348 | | sq1.bb() 349 | | sq2.bb(); 350 | } 351 | } 352 | } 353 | lines_bb 354 | }); 355 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | 294 | Copyright (C) 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | , 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /src/search.rs: -------------------------------------------------------------------------------- 1 | use std::sync::LazyLock; 2 | use std::time::Duration; 3 | 4 | use super::board::*; 5 | use super::moov::*; 6 | use super::move_list::*; 7 | use super::move_sorting::*; 8 | use super::timer::*; 9 | use super::tt::*; 10 | use super::types::*; 11 | 12 | pub struct Search<'a> { 13 | id: u16, 14 | sel_depth: usize, 15 | timer: Timer, 16 | tt: &'a TT, 17 | scorer: MoveScorer, 18 | excluded_moves: [Option; MAX_MOVES], 19 | pv_table: Vec>, 20 | } 21 | 22 | impl<'a> Search<'a> { 23 | pub fn new(timer: Timer, tt: &'a TT, id: u16) -> Self { 24 | Self { 25 | id, 26 | timer, 27 | tt, 28 | sel_depth: 0, 29 | scorer: MoveScorer::new(), 30 | excluded_moves: [None; MAX_MOVES], 31 | pv_table: vec![Vec::new(); MAX_MOVES], 32 | } 33 | } 34 | 35 | pub fn go(&mut self, mut board: Board) -> (Option, Option) { 36 | let moves = MoveList::from::(&board); 37 | if moves.len() == 0 { 38 | return (None, None); 39 | } 40 | 41 | let (mut best_move, mut value) = self.search_root(&mut board, 1, -i32::MATE, i32::MATE); 42 | let mut pv = Vec::new(); 43 | 44 | for depth in 2..i8::MAX { 45 | if !self.timer.start_check(best_move, depth) { 46 | break; 47 | } 48 | 49 | (best_move, value) = self.aspiration(&mut board, depth, value); 50 | 51 | if !self.pv_table[0].is_empty() { 52 | pv = self.pv_table[0].clone(); 53 | } 54 | 55 | if self.id == 0 && !self.timer.is_stopped() { 56 | best_move.inspect(|&m| self.print_info(depth, m, value, &pv)); 57 | } 58 | self.sel_depth = 0; 59 | } 60 | 61 | if self.id == 0 { 62 | self.timer.set_stop(); 63 | } 64 | 65 | // Ensure the ponder move from the last pv is still legal. 66 | // It could be illegal if the last search was only partially completed and the best_move had changed. 67 | let ponder_move = best_move 68 | .zip(pv.get(1).cloned()) 69 | .and_then(|(best_move, ponder_move)| { 70 | board.push(best_move); 71 | let m = MoveList::from::(&board) 72 | .contains(ponder_move) 73 | .then_some(ponder_move); 74 | board.pop(); 75 | m 76 | }); 77 | 78 | (best_move, ponder_move) 79 | } 80 | 81 | fn aspiration(&mut self, board: &mut Board, depth: i8, pred: i32) -> (Option, i32) { 82 | let alpha = (pred - Self::ASPIRATION_WINDOW).max(-i32::MATE); 83 | let beta = (pred + Self::ASPIRATION_WINDOW).min(i32::MATE); 84 | 85 | let (best_move, value) = self.search_root(board, depth, alpha, beta); 86 | 87 | if value <= alpha { 88 | self.search_root(board, depth, -i32::MATE, beta) 89 | } else if value >= beta { 90 | self.search_root(board, depth, alpha, i32::MATE) 91 | } else { 92 | (best_move, value) 93 | } 94 | } 95 | 96 | fn search_root( 97 | &mut self, 98 | board: &mut Board, 99 | mut depth: i8, 100 | mut alpha: i32, 101 | beta: i32, 102 | ) -> (Option, i32) { 103 | /////////////////////////////////////////////////////////////////// 104 | // Clear the pv line and excluded moves. 105 | /////////////////////////////////////////////////////////////////// 106 | self.pv_table.iter_mut().for_each(|line| line.clear()); 107 | self.excluded_moves.iter_mut().for_each(|m| *m = None); 108 | 109 | /////////////////////////////////////////////////////////////////// 110 | // Check extension. 111 | /////////////////////////////////////////////////////////////////// 112 | if board.in_check() { 113 | depth += 1; 114 | } 115 | 116 | /////////////////////////////////////////////////////////////////// 117 | // Check the hash table for the current 118 | // position, primarily for move ordering. 119 | /////////////////////////////////////////////////////////////////// 120 | let hash_move = self.tt.get(board, 0).and_then(|entry| entry.best_move()); 121 | 122 | /////////////////////////////////////////////////////////////////// 123 | // Score moves and begin searching recursively. 124 | /////////////////////////////////////////////////////////////////// 125 | let mut best_move = None; 126 | let mut best_value = -i32::MATE; 127 | let mut tt_flag = Bound::Upper; 128 | let mut value = 0; 129 | 130 | let mut moves = MoveList::from::(board); 131 | let moves_sorter = self 132 | .scorer 133 | .create_sorter::(&mut moves, board, 0, hash_move); 134 | 135 | for (idx, m) in moves_sorter.enumerate() { 136 | if self.id == 0 137 | && self.timer.elapsed() >= Self::PRINT_CURRMOVENUMBER_TIME 138 | && !self.timer.is_stopped() 139 | { 140 | Self::print_currmovenumber(depth, m, idx); 141 | } 142 | 143 | board.push(m); 144 | if idx == 0 || -self.search(board, depth - 1, -alpha - 1, -alpha, 1) > alpha { 145 | value = -self.search(board, depth - 1, -beta, -alpha, 1) 146 | }; 147 | board.pop(); 148 | 149 | if self.timer.is_stopped() { 150 | break; 151 | } 152 | 153 | self.timer.update_node_table(m); 154 | 155 | if value > best_value { 156 | best_value = value; 157 | best_move = Some(m); 158 | 159 | if value > alpha { 160 | self.update_pv(m, 0); 161 | if value >= beta { 162 | tt_flag = Bound::Lower; 163 | break; 164 | } 165 | alpha = value; 166 | tt_flag = Bound::Exact; 167 | } 168 | } 169 | } 170 | 171 | best_move = best_move 172 | .or_else(|| self.tt.get(board, 0).and_then(|entry| entry.best_move())) 173 | .or_else(|| moves.into_iter().next().cloned()); 174 | 175 | if !self.timer.is_stopped() { 176 | self.tt 177 | .insert(board, depth, best_value, best_move, tt_flag, 0); 178 | } 179 | (best_move, best_value) 180 | } 181 | 182 | fn search( 183 | &mut self, 184 | board: &mut Board, 185 | mut depth: i8, 186 | mut alpha: i32, 187 | mut beta: i32, 188 | ply: usize, 189 | ) -> i32 { 190 | /////////////////////////////////////////////////////////////////// 191 | // Clear the pv line. 192 | /////////////////////////////////////////////////////////////////// 193 | self.pv_table[ply].clear(); 194 | self.sel_depth = self.sel_depth.max(ply); 195 | 196 | /////////////////////////////////////////////////////////////////// 197 | // Mate distance pruning - will help reduce 198 | // some nodes when checkmate is near. 199 | /////////////////////////////////////////////////////////////////// 200 | let mate_value = i32::MATE - (ply as i32); 201 | alpha = alpha.max(-mate_value); 202 | beta = beta.min(mate_value - 1); 203 | if alpha >= beta { 204 | return alpha; 205 | } 206 | 207 | /////////////////////////////////////////////////////////////////// 208 | // Extend search if position is in check. Check if we're in a pv 209 | /////////////////////////////////////////////////////////////////// 210 | let in_check = board.in_check(); 211 | if in_check { 212 | depth += 1; 213 | } 214 | 215 | /////////////////////////////////////////////////////////////////// 216 | // Quiescence search - here we search tactical 217 | // moves after the main search to prevent a 218 | // horizon effect. 219 | /////////////////////////////////////////////////////////////////// 220 | if depth <= 0 { 221 | return self.q_search(board, alpha, beta, ply); 222 | } 223 | 224 | if self.timer.stop_check() { 225 | return 0; 226 | } 227 | 228 | if board.is_draw() { 229 | return 0; 230 | } 231 | 232 | /////////////////////////////////////////////////////////////////// 233 | // Check if we're in a pv node 234 | /////////////////////////////////////////////////////////////////// 235 | let is_pv = alpha != beta - 1; 236 | let excluded_move = self.excluded_moves[ply]; 237 | 238 | /////////////////////////////////////////////////////////////////// 239 | // Probe the hash table and adjust the value. 240 | // If appropriate, produce a cutoff. 241 | /////////////////////////////////////////////////////////////////// 242 | let tt_entry = self.tt.get(board, ply); 243 | if let Some(tt_entry) = tt_entry { 244 | if tt_entry.depth() >= depth && !is_pv && excluded_move.is_none() { 245 | let tt_value = tt_entry.value(); 246 | 247 | match tt_entry.bound() { 248 | Bound::Exact => return tt_value, 249 | Bound::Lower => alpha = alpha.max(tt_value), 250 | Bound::Upper => beta = beta.min(tt_value), 251 | } 252 | if alpha >= beta { 253 | return tt_value; 254 | } 255 | } 256 | } 257 | /////////////////////////////////////////////////////////////////// 258 | // Reverse Futility Pruning 259 | /////////////////////////////////////////////////////////////////// 260 | if Self::can_apply_rfp(depth, in_check, is_pv, beta, excluded_move) { 261 | let eval = board.eval(); 262 | 263 | if eval - Self::rfp_margin(depth) >= beta { 264 | return eval; 265 | } 266 | } 267 | 268 | /////////////////////////////////////////////////////////////////// 269 | // Null move pruning. 270 | /////////////////////////////////////////////////////////////////// 271 | if Self::can_apply_null(board, depth, beta, in_check, is_pv, excluded_move) { 272 | let r = Self::null_reduction(depth); 273 | board.push_null(); 274 | let value = -self.search(board, depth - r - 1, -beta, -beta + 1, ply); 275 | board.pop_null(); 276 | if self.timer.is_stopped() { 277 | return 0; 278 | } 279 | if value >= beta { 280 | return value; 281 | } 282 | } 283 | 284 | if Self::can_apply_iid(tt_entry, depth) { 285 | depth -= Self::IID_DEPTH_REDUCTION; 286 | } 287 | 288 | /////////////////////////////////////////////////////////////////// 289 | // Generate moves, score, and begin searching 290 | // recursively. 291 | /////////////////////////////////////////////////////////////////// 292 | let mut tt_flag = Bound::Upper; 293 | let mut best_move = None; 294 | let mut best_value = -i32::MATE; 295 | 296 | let mut moves = MoveList::from::(board); 297 | let sorter = self.scorer.create_sorter::( 298 | &mut moves, 299 | board, 300 | ply, 301 | tt_entry.and_then(|entry| entry.best_move()), 302 | ); 303 | 304 | for (idx, m) in sorter.enumerate() { 305 | if Some(m) == excluded_move { 306 | continue; 307 | } 308 | 309 | let extension = tt_entry 310 | .filter(|&entry| Self::can_singular_extend(entry, m, depth, excluded_move)) 311 | .map_or(0, |entry| { 312 | let target = entry.value() - (2 * depth as i32); 313 | self.excluded_moves[ply] = Some(m); 314 | let extension = 315 | if self.search(board, (depth - 1) / 2, target - 1, target, ply) < target { 316 | 1 317 | } else { 318 | 0 319 | }; 320 | self.excluded_moves[ply] = None; 321 | extension 322 | }); 323 | 324 | /////////////////////////////////////////////////////////////////// 325 | // Make move and deepen search via principal variation search. 326 | /////////////////////////////////////////////////////////////////// 327 | board.push(m); 328 | 329 | if depth > 1 { 330 | self.tt.prefetch(board); 331 | } 332 | 333 | let mut value; 334 | if idx == 0 { 335 | value = -self.search(board, depth + extension - 1, -beta, -alpha, ply + 1); 336 | } else { 337 | /////////////////////////////////////////////////////////////////// 338 | // Late move reductions. 339 | /////////////////////////////////////////////////////////////////// 340 | let mut reduction = if Self::can_apply_lmr(m, depth, idx) { 341 | Self::late_move_reduction(depth, idx) 342 | } else { 343 | 0 344 | }; 345 | 346 | loop { 347 | value = -self.search( 348 | board, 349 | depth + extension - reduction - 1, 350 | -alpha - 1, 351 | -alpha, 352 | ply + 1, 353 | ); 354 | if value > alpha { 355 | value = -self.search( 356 | board, 357 | depth + extension - reduction - 1, 358 | -beta, 359 | -alpha, 360 | ply + 1, 361 | ); 362 | } 363 | 364 | /////////////////////////////////////////////////////////////////// 365 | // A reduced depth may bring us above alpha. This is relatively 366 | // unusual, but if so we need the exact score so we do a full search. 367 | /////////////////////////////////////////////////////////////////// 368 | if reduction > 0 && value > alpha { 369 | reduction = 0; 370 | } else { 371 | break; 372 | } 373 | } 374 | } 375 | 376 | board.pop(); 377 | 378 | if self.timer.is_stopped() { 379 | return 0; 380 | } 381 | 382 | /////////////////////////////////////////////////////////////////// 383 | // Re-bound, check for cutoffs, and add killers and history. 384 | /////////////////////////////////////////////////////////////////// 385 | if value > best_value { 386 | best_value = value; 387 | 388 | if value > alpha { 389 | best_move = Some(m); 390 | if is_pv { 391 | self.update_pv(m, ply); 392 | } 393 | 394 | if value >= beta { 395 | if m.is_quiet() { 396 | self.scorer.add_killer(m, ply); 397 | self.scorer.add_history(m, board.ctm(), depth); 398 | if let Some(p_move) = board.peek() { 399 | self.scorer.add_counter(p_move, m); 400 | } 401 | } 402 | tt_flag = Bound::Lower; 403 | break; 404 | } 405 | tt_flag = Bound::Exact; 406 | alpha = value; 407 | } 408 | } 409 | } 410 | 411 | /////////////////////////////////////////////////////////////////// 412 | // Checkmate and stalemate check. 413 | /////////////////////////////////////////////////////////////////// 414 | if moves.len() == 0 && excluded_move.is_none() { 415 | if in_check { 416 | best_value = -mate_value; 417 | } else { 418 | best_value = 0; 419 | } 420 | } 421 | 422 | if !self.timer.is_stopped() { 423 | best_move = best_move 424 | .or_else(|| self.tt.get(board, ply).and_then(|entry| entry.best_move())) 425 | .or_else(|| moves.into_iter().next().cloned()); 426 | 427 | self.tt 428 | .insert(board, depth, best_value, best_move, tt_flag, ply); 429 | } 430 | best_value 431 | } 432 | 433 | fn q_search(&mut self, board: &mut Board, mut alpha: i32, mut beta: i32, ply: usize) -> i32 { 434 | if self.timer.stop_check() { 435 | return 0; 436 | } 437 | 438 | if board.is_draw() { 439 | return 0; 440 | } 441 | 442 | self.sel_depth = self.sel_depth.max(ply); 443 | 444 | let tt_entry = self.tt.get(board, ply); 445 | if let Some(tt_entry) = tt_entry { 446 | let tt_value = tt_entry.value(); 447 | 448 | match tt_entry.bound() { 449 | Bound::Exact => return tt_value, 450 | Bound::Lower => alpha = alpha.max(tt_value), 451 | Bound::Upper => beta = beta.min(tt_value), 452 | } 453 | if alpha >= beta { 454 | return tt_value; 455 | } 456 | } 457 | 458 | let eval = board.eval(); 459 | 460 | if eval >= beta { 461 | return eval; 462 | } 463 | alpha = alpha.max(eval); 464 | 465 | let mut moves = MoveList::from::(board); 466 | let sorter = self.scorer.create_sorter::( 467 | &mut moves, 468 | board, 469 | ply, 470 | tt_entry.and_then(|entry| entry.best_move()), 471 | ); 472 | 473 | let mut best_value = eval; 474 | 475 | for m in sorter { 476 | if !MoveScorer::see(board, m) { 477 | continue; 478 | } 479 | 480 | board.push(m); 481 | let value = -self.q_search(board, -beta, -alpha, ply + 1); 482 | board.pop(); 483 | 484 | if self.timer.is_stopped() { 485 | return 0; 486 | } 487 | 488 | if value > best_value { 489 | best_value = value; 490 | 491 | if value > alpha { 492 | if value >= beta { 493 | break; 494 | } 495 | alpha = value; 496 | } 497 | } 498 | } 499 | 500 | best_value 501 | } 502 | 503 | fn can_apply_null( 504 | board: &Board, 505 | depth: i8, 506 | beta: i32, 507 | in_check: bool, 508 | is_pv: bool, 509 | excluded_move: Option, 510 | ) -> bool { 511 | !is_pv 512 | && !in_check 513 | && board.peek().is_some() 514 | && depth >= Self::NULL_MIN_DEPTH 515 | && board.has_non_pawn_material() 516 | && board.eval() >= beta 517 | && !beta.is_checkmate() 518 | && excluded_move.is_none() 519 | } 520 | 521 | fn can_apply_iid(tt_entry: Option, depth: i8) -> bool { 522 | depth >= Self::IID_MIN_DEPTH && tt_entry.is_none_or(|entry| entry.best_move().is_none()) 523 | } 524 | 525 | fn can_apply_rfp( 526 | depth: i8, 527 | in_check: bool, 528 | is_pv: bool, 529 | beta: i32, 530 | excluded_move: Option, 531 | ) -> bool { 532 | depth <= Self::RFP_MAX_DEPTH 533 | && !in_check 534 | && !is_pv 535 | && !beta.is_checkmate() 536 | && excluded_move.is_none() 537 | } 538 | 539 | fn can_apply_lmr(m: Move, depth: i8, move_index: usize) -> bool { 540 | depth >= Self::LMR_MIN_DEPTH && move_index >= Self::LMR_MOVE_WO_REDUCTION && m.is_quiet() 541 | } 542 | 543 | fn can_singular_extend( 544 | entry: TTEntry, 545 | m: Move, 546 | depth: i8, 547 | excluded_move: Option, 548 | ) -> bool { 549 | entry.best_move() == Some(m) 550 | && depth >= Self::SING_EXTEND_MIN_DEPTH 551 | && !entry.value().is_checkmate() 552 | && excluded_move.is_none() 553 | && entry.depth() + Self::SING_EXTEND_DEPTH_MARGIN >= depth 554 | && entry.bound() != Bound::Upper 555 | } 556 | 557 | fn null_reduction(depth: i8) -> i8 { 558 | // Idea of dividing in null move depth taken from Cosette 559 | Self::NULL_MIN_DEPTH_REDUCTION + (depth - Self::NULL_MIN_DEPTH) / Self::NULL_DEPTH_DIVIDER 560 | } 561 | 562 | fn rfp_margin(depth: i8) -> i32 { 563 | Self::RFP_MARGIN_MULTIPLIER * (depth as i32) 564 | } 565 | 566 | fn late_move_reduction(depth: i8, move_index: usize) -> i8 { 567 | // LMR table idea from Ethereal 568 | LMR_TABLE[depth.min(63) as usize][move_index.min(63)] 569 | } 570 | 571 | fn update_pv(&mut self, m: Move, ply: usize) { 572 | let (before, after) = self.pv_table.split_at_mut(ply + 1); 573 | 574 | let pv = &mut before[ply]; 575 | pv.clear(); 576 | pv.push(m); 577 | 578 | if let Some(next_pv) = after.first() { 579 | pv.extend(next_pv); 580 | } 581 | 582 | after.iter_mut().for_each(|line| line.clear()); 583 | } 584 | 585 | fn print_info(&self, depth: i8, m: Move, value: i32, pv: &[Move]) { 586 | let score_str = if value.is_checkmate() { 587 | let mate_value = (i32::MATE - value.abs() + 1) * value.signum() / 2; 588 | format!("mate {mate_value}") 589 | } else { 590 | format!("cp {value}") 591 | }; 592 | 593 | let elapsed = self.timer.elapsed(); 594 | let nodes = self.timer.nodes(); 595 | let hashfull = self.tt.hashfull(); 596 | let pv_str = pv 597 | .iter() 598 | .map(|m| m.to_string()) 599 | .collect::>() 600 | .join(" "); 601 | 602 | println!( 603 | "info currmove {m} depth {depth} seldepth {sel_depth} time {time} score {score_str} nodes {nodes} nps {nps} hashfull {hashfull} pv {pv_str}", 604 | m = m, 605 | depth = depth, 606 | sel_depth = self.sel_depth, 607 | time = elapsed.as_millis(), 608 | score_str = score_str, 609 | nodes = nodes, 610 | nps = (nodes as f64 / elapsed.as_secs_f64()) as u64, 611 | pv_str = pv_str 612 | ); 613 | } 614 | 615 | fn print_currmovenumber(depth: i8, m: Move, idx: usize) { 616 | println!( 617 | "info depth {depth} currmove {currmove} currmovenumber {currmovenumber}", 618 | depth = depth, 619 | currmove = m, 620 | currmovenumber = idx + 1, 621 | ) 622 | } 623 | } 624 | 625 | impl Search<'_> { 626 | const PRINT_CURRMOVENUMBER_TIME: Duration = Duration::from_millis(3000); 627 | const RFP_MAX_DEPTH: i8 = 9; 628 | const RFP_MARGIN_MULTIPLIER: i32 = 63; 629 | const ASPIRATION_WINDOW: i32 = 61; 630 | const NULL_MIN_DEPTH: i8 = 2; 631 | const NULL_MIN_DEPTH_REDUCTION: i8 = 1; 632 | const NULL_DEPTH_DIVIDER: i8 = 2; 633 | const IID_MIN_DEPTH: i8 = 4; 634 | const IID_DEPTH_REDUCTION: i8 = 1; 635 | const LMR_MOVE_WO_REDUCTION: usize = 3; 636 | const LMR_MIN_DEPTH: i8 = 2; 637 | const LMR_BASE_REDUCTION: f32 = 0.11; 638 | const LMR_MOVE_DIVIDER: f32 = 1.56; 639 | const SING_EXTEND_MIN_DEPTH: i8 = 4; 640 | const SING_EXTEND_DEPTH_MARGIN: i8 = 2; 641 | } 642 | 643 | #[derive(Clone, Copy, Debug, Eq, PartialEq)] 644 | #[repr(u8)] 645 | pub enum Bound { 646 | Exact, 647 | Lower, 648 | Upper, 649 | } 650 | 651 | static LMR_TABLE: LazyLock<[[i8; 64]; 64]> = LazyLock::new(|| { 652 | let mut lmr_table = [[0; 64]; 64]; 653 | for depth in 1..64 { 654 | for move_number in 1..64 { 655 | lmr_table[depth][move_number] = (Search::LMR_BASE_REDUCTION 656 | + (depth as f32).ln() * (move_number as f32).ln() / Search::LMR_MOVE_DIVIDER) 657 | as i8; 658 | } 659 | } 660 | lmr_table 661 | }); 662 | -------------------------------------------------------------------------------- /src/board.rs: -------------------------------------------------------------------------------- 1 | use super::attacks; 2 | use super::bitboard::*; 3 | use super::moov::*; 4 | use super::move_list::*; 5 | use super::nnue::*; 6 | use super::piece::*; 7 | use super::square::*; 8 | use super::traits::*; 9 | use super::types::*; 10 | use super::zobrist::*; 11 | use regex::Regex; 12 | use std::fmt; 13 | use std::sync::LazyLock; 14 | 15 | #[derive(Clone)] 16 | pub struct Board { 17 | board: SQMap>, 18 | piece_type_bb: PieceTypeMap, 19 | color_bb: ColorMap, 20 | history: [HistoryEntry; Self::N_HISTORIES], 21 | ctm: Color, 22 | ply: usize, 23 | material_hash: u64, 24 | network: Network, 25 | } 26 | 27 | impl Board { 28 | pub fn new() -> Self { 29 | Self::try_from(Self::STARTING_FEN).unwrap() 30 | } 31 | 32 | pub fn reset(&mut self) { 33 | self.set_fen(Self::STARTING_FEN).unwrap(); 34 | } 35 | 36 | pub fn clear(&mut self) { 37 | self.ply = 0; 38 | self.ctm = Color::White; 39 | self.history = [HistoryEntry::default(); Self::N_HISTORIES]; 40 | 41 | self.color_bb = ColorMap::new([Bitboard::ZERO; Color::N_COLORS]); 42 | self.piece_type_bb = PieceTypeMap::new([Bitboard::ZERO; PieceType::N_PIECE_TYPES]); 43 | self.board = SQMap::new([None; SQ::N_SQUARES]); 44 | 45 | self.material_hash = 0; 46 | 47 | self.network = Network::new(); 48 | } 49 | 50 | pub fn piece_at(&self, sq: SQ) -> Option { 51 | self.board[sq] 52 | } 53 | 54 | pub fn piece_type_at(&self, sq: SQ) -> Option { 55 | self.board[sq].map(|pc| pc.type_of()) 56 | } 57 | 58 | pub fn set_piece_at(&mut self, pc: Piece, sq: SQ) { 59 | self.set_piece_at_i::(pc, sq) 60 | } 61 | 62 | pub fn remove_piece(&mut self, sq: SQ) -> Option { 63 | self.remove_piece_i::(sq) 64 | } 65 | 66 | pub fn move_piece_quiet(&mut self, from_sq: SQ, to_sq: SQ) { 67 | self.move_piece_quiet_i::(from_sq, to_sq) 68 | } 69 | 70 | pub fn move_piece(&mut self, from_sq: SQ, to_sq: SQ) { 71 | self.remove_piece_i::(to_sq); 72 | self.move_piece_quiet_i::(from_sq, to_sq); 73 | } 74 | 75 | #[inline(always)] 76 | fn set_piece_at_i(&mut self, pc: Piece, sq: SQ) { 77 | if NNUE { 78 | self.network.activate(pc, sq); 79 | } 80 | self.material_hash ^= ZOBRIST.update_hash(pc, sq); 81 | 82 | let bb = sq.bb(); 83 | self.board[sq] = Some(pc); 84 | self.color_bb[pc.color_of()] |= bb; 85 | self.piece_type_bb[pc.type_of()] |= bb; 86 | } 87 | 88 | #[inline(always)] 89 | fn remove_piece_i(&mut self, sq: SQ) -> Option { 90 | let pc = self.board[sq]?; 91 | 92 | if NNUE { 93 | self.network.deactivate(pc, sq); 94 | } 95 | self.material_hash ^= ZOBRIST.update_hash(pc, sq); 96 | 97 | let bb_mask = !sq.bb(); 98 | self.color_bb[pc.color_of()] &= bb_mask; 99 | self.piece_type_bb[pc.type_of()] &= bb_mask; 100 | self.board[sq] = None; 101 | 102 | Some(pc) 103 | } 104 | 105 | #[inline(always)] 106 | fn move_piece_quiet_i(&mut self, from_sq: SQ, to_sq: SQ) { 107 | let pc = self.board[from_sq].expect("Tried to move a piece off an empty square"); 108 | 109 | if NNUE { 110 | self.network.move_piece_quiet(pc, from_sq, to_sq); 111 | } 112 | self.material_hash ^= ZOBRIST.move_hash(pc, from_sq, to_sq); 113 | 114 | let mask = from_sq.bb() | to_sq.bb(); 115 | self.color_bb[pc.color_of()] ^= mask; 116 | self.piece_type_bb[pc.type_of()] ^= mask; 117 | self.board[to_sq] = Some(pc); 118 | self.board[from_sq] = None; 119 | } 120 | 121 | #[inline(always)] 122 | fn move_piece_i(&mut self, from_sq: SQ, to_sq: SQ) { 123 | self.remove_piece_i::(to_sq); 124 | self.move_piece_quiet_i::(from_sq, to_sq); 125 | } 126 | 127 | pub fn eval(&self) -> i32 { 128 | self.network.eval(self.ctm) 129 | } 130 | 131 | pub fn bitboard_of(&self, c: Color, pt: PieceType) -> Bitboard { 132 | self.piece_type_bb[pt] & self.color_bb[c] 133 | } 134 | 135 | pub fn bitboard_of_pc(&self, pc: Piece) -> Bitboard { 136 | self.piece_type_bb[pc.type_of()] & self.color_bb[pc.color_of()] 137 | } 138 | 139 | pub fn bitboard_of_pt(&self, pt: PieceType) -> Bitboard { 140 | self.piece_type_bb[pt] 141 | } 142 | 143 | pub fn diagonal_sliders(&self) -> Bitboard { 144 | self.bitboard_of_pt(PieceType::Bishop) | self.bitboard_of_pt(PieceType::Queen) 145 | } 146 | 147 | pub fn orthogonal_sliders(&self) -> Bitboard { 148 | self.bitboard_of_pt(PieceType::Rook) | self.bitboard_of_pt(PieceType::Queen) 149 | } 150 | 151 | pub fn diagonal_sliders_c(&self, color: Color) -> Bitboard { 152 | self.bitboard_of(color, PieceType::Bishop) | self.bitboard_of(color, PieceType::Queen) 153 | } 154 | 155 | pub fn orthogonal_sliders_c(&self, color: Color) -> Bitboard { 156 | self.bitboard_of(color, PieceType::Rook) | self.bitboard_of(color, PieceType::Queen) 157 | } 158 | 159 | pub fn all_pieces(&self) -> Bitboard { 160 | self.color_bb[Color::White] | self.color_bb[Color::Black] 161 | } 162 | 163 | pub fn all_pieces_c(&self, color: Color) -> Bitboard { 164 | self.color_bb[color] 165 | } 166 | 167 | pub fn attackers(&self, sq: SQ, occ: Bitboard) -> Bitboard { 168 | (self.bitboard_of(Color::White, PieceType::Pawn) 169 | & attacks::pawn_attacks_sq(sq, Color::Black)) 170 | | (self.bitboard_of(Color::Black, PieceType::Pawn) 171 | & attacks::pawn_attacks_sq(sq, Color::White)) 172 | | (self.bitboard_of_pt(PieceType::Knight) & attacks::knight_attacks(sq)) 173 | | (self.diagonal_sliders() & attacks::bishop_attacks(sq, occ)) 174 | | (self.orthogonal_sliders() & attacks::rook_attacks(sq, occ)) 175 | } 176 | 177 | pub fn attackers_from_c(&self, sq: SQ, occ: Bitboard, color: Color) -> Bitboard { 178 | (self.bitboard_of(color, PieceType::Pawn) & attacks::pawn_attacks_sq(sq, !color)) 179 | | (self.bitboard_of(color, PieceType::Knight) & attacks::knight_attacks(sq)) 180 | | (self.diagonal_sliders_c(color) & attacks::bishop_attacks(sq, occ)) 181 | | (self.orthogonal_sliders_c(color) & attacks::rook_attacks(sq, occ)) 182 | } 183 | 184 | pub fn is_attacked(&self, sq: SQ) -> bool { 185 | let us = self.ctm; 186 | let them = !self.ctm; 187 | 188 | if attacks::knight_attacks(sq) & self.bitboard_of(them, PieceType::Knight) != Bitboard::ZERO 189 | { 190 | return true; 191 | } 192 | 193 | if attacks::pawn_attacks_sq(sq, us) & self.bitboard_of(them, PieceType::Pawn) 194 | != Bitboard::ZERO 195 | { 196 | return true; 197 | } 198 | 199 | let all = self.all_pieces(); 200 | if attacks::rook_attacks(sq, all) & self.orthogonal_sliders_c(them) != Bitboard::ZERO { 201 | return true; 202 | } 203 | 204 | if attacks::bishop_attacks(sq, all) & self.diagonal_sliders_c(them) != Bitboard::ZERO { 205 | return true; 206 | } 207 | false 208 | } 209 | 210 | pub fn in_check(&self) -> bool { 211 | self.is_attacked(self.bitboard_of(self.ctm, PieceType::King).lsb()) 212 | } 213 | 214 | pub fn peek(&self) -> Option { 215 | self.history[self.ply].moov 216 | } 217 | 218 | fn is_insufficient_material(&self) -> bool { 219 | match self.all_pieces().pop_count() { 220 | 2 => true, 221 | 3 => { 222 | self.bitboard_of_pt(PieceType::Rook) 223 | | self.bitboard_of_pt(PieceType::Queen) 224 | | self.bitboard_of_pt(PieceType::Pawn) 225 | == Bitboard::ZERO 226 | } 227 | _ => false, 228 | } 229 | } 230 | 231 | fn is_fifty(&self) -> bool { 232 | self.history[self.ply].half_move_counter >= 100 233 | } 234 | 235 | fn is_repetition(&self) -> bool { 236 | let lookback = self.history[self.ply] 237 | .plies_from_null 238 | .min(self.history[self.ply].half_move_counter) as usize; 239 | 240 | self.history[self.ply - lookback..self.ply] 241 | .iter() 242 | .rev() 243 | .skip(1) 244 | .step_by(2) 245 | .any(|entry| self.material_hash == entry.material_hash) 246 | } 247 | 248 | pub fn is_draw(&self) -> bool { 249 | self.is_fifty() || self.is_insufficient_material() || self.is_repetition() 250 | } 251 | 252 | pub fn has_non_pawn_material(&self) -> bool { 253 | self.bitboard_of(self.ctm, PieceType::Pawn) | self.bitboard_of(self.ctm, PieceType::King) 254 | != self.all_pieces_c(self.ctm) 255 | } 256 | 257 | pub fn push_null(&mut self) { 258 | self.ply += 1; 259 | 260 | self.history[self.ply] = HistoryEntry { 261 | entry: self.history[self.ply - 1].entry, 262 | half_move_counter: self.history[self.ply - 1].half_move_counter + 1, 263 | plies_from_null: 0, 264 | moov: None, 265 | captured: None, 266 | epsq: None, 267 | material_hash: self.history[self.ply - 1].material_hash, 268 | }; 269 | 270 | self.ctm = !self.ctm; 271 | } 272 | 273 | pub fn pop_null(&mut self) { 274 | self.ply -= 1; 275 | self.ctm = !self.ctm; 276 | } 277 | 278 | // By default, push updates the accumulator but pop doesn't. This is because the NNUE is copy-make. 279 | pub fn push(&mut self, m: Move) { 280 | self.ipush::(m); 281 | } 282 | 283 | pub fn pop(&mut self) -> Option { 284 | self.ipop::() 285 | } 286 | 287 | fn ipush(&mut self, m: Move) { 288 | let mut half_move_counter = self.history[self.ply].half_move_counter + 1; 289 | let mut captured = None; 290 | let mut epsq = None; 291 | let (from_sq, to_sq) = m.squares(); 292 | self.ply += 1; 293 | self.network.push(); 294 | 295 | if self.piece_type_at(from_sq) == Some(PieceType::Pawn) { 296 | half_move_counter = 0; 297 | } 298 | 299 | match m.flags() { 300 | MoveFlags::Quiet => { 301 | self.move_piece_quiet_i::(from_sq, to_sq); 302 | } 303 | MoveFlags::DoublePush => { 304 | self.move_piece_quiet_i::(from_sq, to_sq); 305 | epsq = Some(from_sq + Direction::North.relative(self.ctm)); 306 | } 307 | MoveFlags::OO => { 308 | self.move_piece_quiet_i::( 309 | SQ::E1.relative(self.ctm), 310 | SQ::G1.relative(self.ctm), 311 | ); 312 | self.move_piece_quiet_i::( 313 | SQ::H1.relative(self.ctm), 314 | SQ::F1.relative(self.ctm), 315 | ); 316 | } 317 | MoveFlags::OOO => { 318 | self.move_piece_quiet_i::( 319 | SQ::E1.relative(self.ctm), 320 | SQ::C1.relative(self.ctm), 321 | ); 322 | self.move_piece_quiet_i::( 323 | SQ::A1.relative(self.ctm), 324 | SQ::D1.relative(self.ctm), 325 | ); 326 | } 327 | MoveFlags::EnPassant => { 328 | self.move_piece_quiet_i::(from_sq, to_sq); 329 | self.remove_piece_i::(to_sq + Direction::South.relative(self.ctm)); 330 | } 331 | MoveFlags::Capture => { 332 | captured = self.piece_at(to_sq); 333 | half_move_counter = 0; 334 | self.move_piece_i::(from_sq, to_sq); 335 | } 336 | // Promotions: 337 | _ => { 338 | if m.is_capture() { 339 | captured = self.remove_piece_i::(to_sq); 340 | } 341 | self.remove_piece_i::(from_sq); 342 | self.set_piece_at_i::( 343 | Piece::make_piece( 344 | self.ctm, 345 | m.promotion() 346 | .expect("Tried to set a promotion piece for a non-promotion move."), 347 | ), 348 | to_sq, 349 | ); 350 | } 351 | }; 352 | self.history[self.ply] = HistoryEntry { 353 | entry: self.history[self.ply - 1].entry | to_sq.bb() | from_sq.bb(), 354 | moov: Some(m), 355 | plies_from_null: self.history[self.ply - 1].plies_from_null + 1, 356 | material_hash: self.material_hash, 357 | half_move_counter, 358 | captured, 359 | epsq, 360 | }; 361 | self.ctm = !self.ctm; 362 | } 363 | 364 | pub fn ipop(&mut self) -> Option { 365 | self.ctm = !self.ctm; 366 | 367 | let m = self.history[self.ply].moov?; 368 | let (from_sq, to_sq) = m.squares(); 369 | 370 | match m.flags() { 371 | MoveFlags::Quiet => { 372 | self.move_piece_quiet_i::(to_sq, from_sq); 373 | } 374 | MoveFlags::DoublePush => { 375 | self.move_piece_quiet_i::(to_sq, from_sq); 376 | } 377 | MoveFlags::OO => { 378 | self.move_piece_quiet_i::( 379 | SQ::G1.relative(self.ctm), 380 | SQ::E1.relative(self.ctm), 381 | ); 382 | self.move_piece_quiet_i::( 383 | SQ::F1.relative(self.ctm), 384 | SQ::H1.relative(self.ctm), 385 | ); 386 | } 387 | MoveFlags::OOO => { 388 | self.move_piece_quiet_i::( 389 | SQ::C1.relative(self.ctm), 390 | SQ::E1.relative(self.ctm), 391 | ); 392 | self.move_piece_quiet_i::( 393 | SQ::D1.relative(self.ctm), 394 | SQ::A1.relative(self.ctm), 395 | ); 396 | } 397 | MoveFlags::EnPassant => { 398 | self.move_piece_quiet_i::(to_sq, from_sq); 399 | self.set_piece_at_i::( 400 | Piece::make_piece(!self.ctm, PieceType::Pawn), 401 | to_sq + Direction::South.relative(self.ctm), 402 | ); 403 | } 404 | MoveFlags::PrKnight | MoveFlags::PrBishop | MoveFlags::PrRook | MoveFlags::PrQueen => { 405 | self.remove_piece_i::(to_sq); 406 | self.set_piece_at_i::( 407 | Piece::make_piece(self.ctm, PieceType::Pawn), 408 | from_sq, 409 | ); 410 | } 411 | MoveFlags::PcKnight | MoveFlags::PcBishop | MoveFlags::PcRook | MoveFlags::PcQueen => { 412 | self.remove_piece_i::(to_sq); 413 | self.set_piece_at_i::( 414 | Piece::make_piece(self.ctm, PieceType::Pawn), 415 | from_sq, 416 | ); 417 | self.set_piece_at_i::( 418 | self.history[self.ply] 419 | .captured 420 | .expect("Tried to revert a capture move with no capture."), 421 | to_sq, 422 | ); 423 | } 424 | MoveFlags::Capture => { 425 | self.move_piece_quiet_i::(to_sq, from_sq); 426 | self.set_piece_at_i::( 427 | self.history[self.ply] 428 | .captured 429 | .expect("Tried to revert a capture move with no capture."), 430 | to_sq, 431 | ); 432 | } 433 | } 434 | self.ply -= 1; 435 | self.network.pop(); 436 | Some(m) 437 | } 438 | 439 | pub fn generate_legal_moves(&self, moves: &mut MoveList) { 440 | let us = self.ctm; 441 | let them = !self.ctm; 442 | 443 | let us_bb = self.all_pieces_c(us); 444 | let them_bb = self.all_pieces_c(them); 445 | let all = us_bb | them_bb; 446 | 447 | let our_king = self.bitboard_of(us, PieceType::King).lsb(); 448 | 449 | let their_king = self.bitboard_of(them, PieceType::King).lsb(); 450 | 451 | let our_diag_sliders = self.diagonal_sliders_c(us); 452 | let their_diag_sliders = self.diagonal_sliders_c(them); 453 | let our_orth_sliders = self.orthogonal_sliders_c(us); 454 | let their_orth_sliders = self.orthogonal_sliders_c(them); 455 | 456 | /////////////////////////////////////////////////////////////////// 457 | // Danger squares for the king 458 | /////////////////////////////////////////////////////////////////// 459 | let mut danger = Bitboard::ZERO; 460 | 461 | /////////////////////////////////////////////////////////////////// 462 | // Add each enemy attack to the danger bitboard 463 | /////////////////////////////////////////////////////////////////// 464 | danger |= attacks::pawn_attacks_bb(self.bitboard_of(them, PieceType::Pawn), them) 465 | | attacks::king_attacks(their_king); 466 | 467 | danger |= self 468 | .bitboard_of(them, PieceType::Knight) 469 | .map(attacks::knight_attacks) 470 | .fold(Bitboard::ZERO, |a, b| a | b); 471 | 472 | danger |= their_diag_sliders 473 | .map(|sq| attacks::bishop_attacks(sq, all ^ our_king.bb())) 474 | .fold(Bitboard::ZERO, |a, b| a | b); 475 | 476 | danger |= their_orth_sliders 477 | .map(|sq| attacks::rook_attacks(sq, all ^ our_king.bb())) 478 | .fold(Bitboard::ZERO, |a, b| a | b); 479 | 480 | /////////////////////////////////////////////////////////////////// 481 | // The king can move to any square that isn't attacked or occupied 482 | // by one of our pieces. 483 | /////////////////////////////////////////////////////////////////// 484 | 485 | let king_attacks = attacks::king_attacks(our_king) & !(us_bb | danger); 486 | 487 | if !QUIESCENCE { 488 | moves.make_q(our_king, king_attacks & !them_bb); 489 | } 490 | moves.make_c(our_king, king_attacks & them_bb); 491 | 492 | /////////////////////////////////////////////////////////////////// 493 | // The capture mask consists of destination squares containing enemy 494 | // pieces that must be captured because they are checking the king. 495 | /////////////////////////////////////////////////////////////////// 496 | let capture_mask; 497 | 498 | /////////////////////////////////////////////////////////////////// 499 | // The quiet mask consists of squares where pieces must be moved 500 | // to block an attack checking the king. 501 | /////////////////////////////////////////////////////////////////// 502 | let quiet_mask; 503 | 504 | /////////////////////////////////////////////////////////////////// 505 | // Checkers are identified by projecting attacks from the king 506 | // square and then intersecting them with the enemy bitboard of the 507 | // respective piece. 508 | /////////////////////////////////////////////////////////////////// 509 | let mut checkers = (attacks::knight_attacks(our_king) 510 | & self.bitboard_of(them, PieceType::Knight)) 511 | | (attacks::pawn_attacks_sq(our_king, us) & self.bitboard_of(them, PieceType::Pawn)); 512 | 513 | /////////////////////////////////////////////////////////////////// 514 | // Candidates are potential slider checkers and pinners. 515 | /////////////////////////////////////////////////////////////////// 516 | let candidates = (attacks::rook_attacks(our_king, them_bb) & their_orth_sliders) 517 | | (attacks::bishop_attacks(our_king, them_bb) & their_diag_sliders); 518 | 519 | let mut pinned = Bitboard::ZERO; 520 | 521 | for sq in candidates { 522 | let potentially_pinned = Bitboard::between(our_king, sq) & us_bb; 523 | 524 | /////////////////////////////////////////////////////////////////// 525 | // Do the squares between an enemy slider and our king contain any 526 | // pieces? If yes, that piece is pinned. Otherwise, we are checked. 527 | /////////////////////////////////////////////////////////////////// 528 | if potentially_pinned == Bitboard::ZERO { 529 | checkers ^= sq.bb(); 530 | } else if potentially_pinned.is_single() { 531 | pinned ^= potentially_pinned; 532 | } 533 | } 534 | 535 | let not_pinned = !pinned; 536 | 537 | match checkers.pop_count() { 538 | 2 => { 539 | /////////////////////////////////////////////////////////////////// 540 | // If we're in a double check, we have to move the king. We've already 541 | // generated those moves, so just return. 542 | /////////////////////////////////////////////////////////////////// 543 | return; 544 | } 545 | 1 => { 546 | let checker_square = checkers.lsb(); 547 | let pt = self 548 | .piece_type_at(checker_square) 549 | .expect("Checker expected."); 550 | match pt { 551 | PieceType::Pawn | PieceType::Knight => { 552 | /////////////////////////////////////////////////////////////////// 553 | // If the checkers is a pawn, we have to look out for ep moves 554 | // that can capture it. 555 | /////////////////////////////////////////////////////////////////// 556 | if pt == PieceType::Pawn 557 | && self.history[self.ply].epsq.is_some_and(|epsq| { 558 | checkers == epsq.bb().shift(Direction::South.relative(us)) 559 | }) 560 | { 561 | let epsq = self.history[self.ply] 562 | .epsq 563 | .expect("No epsq found for checker."); 564 | let pawns = attacks::pawn_attacks_sq(epsq, them) 565 | & self.bitboard_of(us, PieceType::Pawn) 566 | & not_pinned; 567 | for sq in pawns { 568 | moves.push(Move::new(sq, epsq, MoveFlags::EnPassant)); 569 | } 570 | } 571 | let checker_attackers = 572 | self.attackers_from_c(checker_square, all, us) & not_pinned; 573 | for sq in checker_attackers { 574 | if self.piece_type_at(sq) == Some(PieceType::Pawn) 575 | && sq.rank().relative(us) == Rank::Seven 576 | { 577 | moves.push(Move::new(sq, checker_square, MoveFlags::PcQueen)); 578 | if !QUIESCENCE { 579 | moves.push(Move::new(sq, checker_square, MoveFlags::PcRook)); 580 | moves.push(Move::new(sq, checker_square, MoveFlags::PcKnight)); 581 | moves.push(Move::new(sq, checker_square, MoveFlags::PcBishop)); 582 | } 583 | } else { 584 | moves.push(Move::new(sq, checker_square, MoveFlags::Capture)); 585 | } 586 | } 587 | return; 588 | } 589 | _ => { 590 | /////////////////////////////////////////////////////////////////// 591 | // We have to either capture the piece or block it, since it must be 592 | // a slider. 593 | /////////////////////////////////////////////////////////////////// 594 | capture_mask = checkers; 595 | quiet_mask = Bitboard::between(our_king, checker_square); 596 | } 597 | } 598 | } 599 | _ => { 600 | /////////////////////////////////////////////////////////////////// 601 | // At this point, we can capture any enemy piece or play into any 602 | // quiet square. 603 | /////////////////////////////////////////////////////////////////// 604 | capture_mask = them_bb; 605 | quiet_mask = !all; 606 | if let Some(epsq) = self.history[self.ply].epsq { 607 | let epsq_attackers = attacks::pawn_attacks_sq(epsq, them) 608 | & self.bitboard_of(us, PieceType::Pawn); 609 | let unpinned_epsq_attackers = epsq_attackers & not_pinned; 610 | for sq in unpinned_epsq_attackers { 611 | /////////////////////////////////////////////////////////////////// 612 | // From surge: 613 | // This piece of evil bit-fiddling magic prevents the infamous 'pseudo-pinned' e.p. case, 614 | // where the pawn is not directly pinned, but on moving the pawn and capturing the enemy pawn 615 | // e.p., a rook or queen attack to the king is revealed 616 | // 617 | // 618 | // nbqkbnr 619 | // ppp.pppp 620 | // ........ 621 | // r..pP..K 622 | // ........ 623 | // ........ 624 | // PPPP.PPP 625 | // RNBQ.BNR 626 | // 627 | // Here, if white plays exd5 e.p., the black rook on a5 attacks the white king on h5 628 | /////////////////////////////////////////////////////////////////// 629 | let attacks = attacks::sliding_attacks( 630 | our_king, 631 | all ^ sq.bb() ^ epsq.bb().shift(Direction::South.relative(us)), 632 | our_king.rank().bb(), 633 | ); 634 | 635 | if (attacks & their_orth_sliders) == Bitboard::ZERO { 636 | moves.push(Move::new(sq, epsq, MoveFlags::EnPassant)); 637 | } 638 | } 639 | /////////////////////////////////////////////////////////////////// 640 | // Pinned pawns can only capture ep if they are pinned diagonally 641 | // and the ep square is in line with the king. 642 | /////////////////////////////////////////////////////////////////// 643 | let pinned_epsq_attackers = 644 | epsq_attackers & pinned & Bitboard::line(epsq, our_king); 645 | if pinned_epsq_attackers != Bitboard::ZERO { 646 | moves.push(Move::new( 647 | pinned_epsq_attackers.lsb(), 648 | epsq, 649 | MoveFlags::EnPassant, 650 | )); 651 | } 652 | } 653 | 654 | /////////////////////////////////////////////////////////////////// 655 | // Only castle if: 656 | // 1. Neither the king nor rook have moved. 657 | // 2. The king is not in check. 658 | // 3. The relevant squares are not attacked. 659 | /////////////////////////////////////////////////////////////////// 660 | if !QUIESCENCE { 661 | if ((self.history[self.ply].entry & Bitboard::oo_mask(us)) 662 | | ((all | danger) & Bitboard::oo_blockers_mask(us))) 663 | == Bitboard::ZERO 664 | { 665 | moves.push(match us { 666 | Color::White => Move::new(SQ::E1, SQ::G1, MoveFlags::OO), 667 | Color::Black => Move::new(SQ::E8, SQ::G8, MoveFlags::OO), 668 | }); 669 | } 670 | if ((self.history[self.ply].entry & Bitboard::ooo_mask(us)) 671 | | ((all | (danger & !Bitboard::ignore_ooo_danger(us))) 672 | & Bitboard::ooo_blockers_mask(us))) 673 | == Bitboard::ZERO 674 | { 675 | moves.push(match us { 676 | Color::White => Move::new(SQ::E1, SQ::C1, MoveFlags::OOO), 677 | Color::Black => Move::new(SQ::E8, SQ::C8, MoveFlags::OOO), 678 | }); 679 | } 680 | } 681 | /////////////////////////////////////////////////////////////////// 682 | // For each pinned rook, bishop, or queen, only include attacks 683 | // that are aligned with our king. 684 | /////////////////////////////////////////////////////////////////// 685 | let pinned_pieces = !(not_pinned | self.bitboard_of(us, PieceType::Knight)); 686 | for sq in pinned_pieces { 687 | let pt = self 688 | .piece_type_at(sq) 689 | .expect("Unexpected None for piece type."); 690 | let attacks_along_pin = 691 | attacks::attacks(pt, sq, all) & Bitboard::line(our_king, sq); 692 | if !QUIESCENCE { 693 | moves.make_q(sq, attacks_along_pin & quiet_mask); 694 | } 695 | moves.make_c(sq, attacks_along_pin & capture_mask); 696 | } 697 | 698 | /////////////////////////////////////////////////////////////////// 699 | // For each pinned pawn 700 | /////////////////////////////////////////////////////////////////// 701 | let pinned_pawns = !not_pinned & self.bitboard_of(us, PieceType::Pawn); 702 | for sq in pinned_pawns { 703 | /////////////////////////////////////////////////////////////////// 704 | // Quiet promotions are impossible since the square in front of the 705 | // pawn will be occupied 706 | /////////////////////////////////////////////////////////////////// 707 | if sq.rank() == Rank::Seven.relative(us) { 708 | moves.make_pc( 709 | sq, 710 | attacks::pawn_attacks_sq(sq, us) 711 | & capture_mask 712 | & Bitboard::line(our_king, sq), 713 | ); 714 | } else { 715 | moves.make_c( 716 | sq, 717 | attacks::pawn_attacks_sq(sq, us) 718 | & them_bb 719 | & Bitboard::line(sq, our_king), 720 | ); 721 | 722 | /////////////////////////////////////////////////////////////////// 723 | // Single and double pawn pushes 724 | /////////////////////////////////////////////////////////////////// 725 | if !QUIESCENCE { 726 | let single_pinned_pushes = sq.bb().shift(Direction::North.relative(us)) 727 | & !all 728 | & Bitboard::line(our_king, sq); 729 | let double_pinned_pushes = (single_pinned_pushes 730 | & Rank::Three.relative(us).bb()) 731 | .shift(Direction::North.relative(us)) 732 | & !all 733 | & Bitboard::line(our_king, sq); 734 | 735 | moves.make_q(sq, single_pinned_pushes); 736 | moves.make_dp(sq, double_pinned_pushes); 737 | } 738 | } 739 | } 740 | } 741 | } 742 | 743 | /////////////////////////////////////////////////////////////////// 744 | // Non-pinned moves from here 745 | /////////////////////////////////////////////////////////////////// 746 | for sq in self.bitboard_of(us, PieceType::Knight) & not_pinned { 747 | let knight_attacks = attacks::knight_attacks(sq); 748 | moves.make_c(sq, knight_attacks & capture_mask); 749 | if !QUIESCENCE { 750 | moves.make_q(sq, knight_attacks & quiet_mask); 751 | } 752 | } 753 | 754 | for sq in our_diag_sliders & not_pinned { 755 | let diag_attacks = attacks::bishop_attacks(sq, all); 756 | moves.make_c(sq, diag_attacks & capture_mask); 757 | if !QUIESCENCE { 758 | moves.make_q(sq, diag_attacks & quiet_mask); 759 | } 760 | } 761 | 762 | for sq in our_orth_sliders & not_pinned { 763 | let orth_attacks = attacks::rook_attacks(sq, all); 764 | moves.make_c(sq, orth_attacks & capture_mask); 765 | if !QUIESCENCE { 766 | moves.make_q(sq, orth_attacks & quiet_mask); 767 | } 768 | } 769 | 770 | let back_pawns = 771 | self.bitboard_of(us, PieceType::Pawn) & not_pinned & !Rank::Seven.relative(us).bb(); 772 | let mut single_pushes = back_pawns.shift(Direction::North.relative(us)) & !all; 773 | let double_pushes = (single_pushes & Rank::Three.relative(us).bb()) 774 | .shift(Direction::North.relative(us)) 775 | & quiet_mask; 776 | 777 | single_pushes &= quiet_mask; 778 | 779 | if !QUIESCENCE { 780 | for sq in single_pushes { 781 | moves.push(Move::new( 782 | sq - Direction::North.relative(us), 783 | sq, 784 | MoveFlags::Quiet, 785 | )); 786 | } 787 | 788 | for sq in double_pushes { 789 | moves.push(Move::new( 790 | sq - Direction::NorthNorth.relative(us), 791 | sq, 792 | MoveFlags::DoublePush, 793 | )); 794 | } 795 | } 796 | 797 | let northwest_captures = back_pawns.shift(Direction::NorthWest.relative(us)) & capture_mask; 798 | let northeast_captures = back_pawns.shift(Direction::NorthEast.relative(us)) & capture_mask; 799 | 800 | for sq in northwest_captures { 801 | moves.push(Move::new( 802 | sq - Direction::NorthWest.relative(us), 803 | sq, 804 | MoveFlags::Capture, 805 | )); 806 | } 807 | 808 | for sq in northeast_captures { 809 | moves.push(Move::new( 810 | sq - Direction::NorthEast.relative(us), 811 | sq, 812 | MoveFlags::Capture, 813 | )); 814 | } 815 | 816 | let seventh_rank_pawns = 817 | self.bitboard_of(us, PieceType::Pawn) & not_pinned & Rank::Seven.relative(us).bb(); 818 | 819 | if seventh_rank_pawns != Bitboard::ZERO { 820 | let quiet_promotions = 821 | seventh_rank_pawns.shift(Direction::North.relative(us)) & quiet_mask; 822 | for sq in quiet_promotions { 823 | moves.push(Move::new( 824 | sq - Direction::North.relative(us), 825 | sq, 826 | MoveFlags::PrQueen, 827 | )); 828 | if !QUIESCENCE { 829 | moves.push(Move::new( 830 | sq - Direction::North.relative(us), 831 | sq, 832 | MoveFlags::PrRook, 833 | )); 834 | moves.push(Move::new( 835 | sq - Direction::North.relative(us), 836 | sq, 837 | MoveFlags::PrKnight, 838 | )); 839 | moves.push(Move::new( 840 | sq - Direction::North.relative(us), 841 | sq, 842 | MoveFlags::PrBishop, 843 | )); 844 | } 845 | } 846 | 847 | let northwest_promotions = 848 | seventh_rank_pawns.shift(Direction::NorthWest.relative(us)) & capture_mask; 849 | let northeast_promotions = 850 | seventh_rank_pawns.shift(Direction::NorthEast.relative(us)) & capture_mask; 851 | for sq in northwest_promotions { 852 | moves.push(Move::new( 853 | sq - Direction::NorthWest.relative(us), 854 | sq, 855 | MoveFlags::PcQueen, 856 | )); 857 | if !QUIESCENCE { 858 | moves.push(Move::new( 859 | sq - Direction::NorthWest.relative(us), 860 | sq, 861 | MoveFlags::PcRook, 862 | )); 863 | moves.push(Move::new( 864 | sq - Direction::NorthWest.relative(us), 865 | sq, 866 | MoveFlags::PcKnight, 867 | )); 868 | moves.push(Move::new( 869 | sq - Direction::NorthWest.relative(us), 870 | sq, 871 | MoveFlags::PcBishop, 872 | )); 873 | } 874 | } 875 | 876 | for sq in northeast_promotions { 877 | moves.push(Move::new( 878 | sq - Direction::NorthEast.relative(us), 879 | sq, 880 | MoveFlags::PcQueen, 881 | )); 882 | if !QUIESCENCE { 883 | moves.push(Move::new( 884 | sq - Direction::NorthEast.relative(us), 885 | sq, 886 | MoveFlags::PcRook, 887 | )); 888 | moves.push(Move::new( 889 | sq - Direction::NorthEast.relative(us), 890 | sq, 891 | MoveFlags::PcKnight, 892 | )); 893 | moves.push(Move::new( 894 | sq - Direction::NorthEast.relative(us), 895 | sq, 896 | MoveFlags::PcBishop, 897 | )); 898 | } 899 | } 900 | } 901 | } 902 | 903 | pub fn push_str(&mut self, move_str: &str) -> Result<(), &'static str> { 904 | let moves = MoveList::from::(self); 905 | let m = moves 906 | .into_iter() 907 | .find(|m| m.to_string() == move_str) 908 | .ok_or("Invalid move.")?; 909 | 910 | self.push(*m); 911 | Ok(()) 912 | } 913 | 914 | pub fn set_fen(&mut self, fen: &str) -> Result<(), &'static str> { 915 | self.clear(); 916 | let fen = fen.trim(); 917 | if !fen.is_ascii() || fen.lines().count() != 1 { 918 | return Err("FEN should be a single ASCII line."); 919 | } 920 | 921 | let re_captures = FEN_RE.captures(fen).ok_or("Invalid fen format.")?; 922 | 923 | let piece_placement = re_captures 924 | .name("piece_placement") 925 | .ok_or("Invalid piece placement.")? 926 | .as_str(); 927 | let ctm = re_captures 928 | .name("active_color") 929 | .ok_or("Invalid color.")? 930 | .as_str(); 931 | let castling = re_captures 932 | .name("castling") 933 | .ok_or("Invalid castling rights.")? 934 | .as_str(); 935 | let en_passant_sq = re_captures.name("en_passant").map_or("-", |m| m.as_str()); 936 | let halfmove_clock = re_captures.name("halfmove").map_or("0", |m| m.as_str()); 937 | let fullmove_counter = re_captures.name("fullmove").map_or("1", |m| m.as_str()); 938 | 939 | if piece_placement.split('/').count() != Rank::N_RANKS { 940 | return Err("Pieces Placement FEN should have 8 ranks."); 941 | } 942 | 943 | self.ctm = Color::try_from(ctm.parse::().map_err(|_| "Invalid color.")?)?; 944 | 945 | self.ply = 2 946 | * (fullmove_counter 947 | .parse::() 948 | .map_err(|_| "Invalid full move counter.")? 949 | - 1); 950 | if self.ctm == Color::Black { 951 | self.ply += 1; 952 | } 953 | 954 | let ranks = piece_placement.split('/'); 955 | for (rank_idx, rank_fen) in ranks.enumerate() { 956 | let mut idx = (7 - rank_idx) * 8; 957 | 958 | for ch in rank_fen.chars() { 959 | if let Some(digit) = ch.to_digit(10) { 960 | if digit > 8 { 961 | return Err("Invalid digit in position."); 962 | } 963 | idx += digit as usize; 964 | } else { 965 | if idx > 63 { 966 | return Err("Invalid square index in FEN."); 967 | } 968 | let sq = SQ::from(idx as u8); 969 | let pc = Piece::try_from(ch)?; 970 | self.set_piece_at(pc, sq); 971 | idx += 1; 972 | } 973 | } 974 | 975 | if idx != 64 - 8 * rank_idx { 976 | return Err("FEN rank does not fill expected number of squares."); 977 | } 978 | } 979 | 980 | let mut castling_mask = Bitboard::ALL_CASTLING_MASK; 981 | for (symbol, mask) in [ 982 | ('K', Bitboard::WHITE_OO_MASK), 983 | ('Q', Bitboard::WHITE_OOO_MASK), 984 | ('k', Bitboard::BLACK_OO_MASK), 985 | ('q', Bitboard::BLACK_OOO_MASK), 986 | ] { 987 | if castling.contains(symbol) { 988 | castling_mask &= !mask; 989 | } 990 | } 991 | 992 | let epsq = if en_passant_sq != "-" { 993 | let epsq = SQ::try_from(en_passant_sq)?; 994 | Some(epsq) 995 | } else { 996 | None 997 | }; 998 | 999 | let half_move_counter = halfmove_clock 1000 | .parse::() 1001 | .map_err(|_| "Invalid half move counter.")?; 1002 | 1003 | self.history[self.ply] = HistoryEntry { 1004 | entry: castling_mask, 1005 | moov: None, 1006 | material_hash: self.material_hash, 1007 | plies_from_null: 0, 1008 | captured: None, 1009 | epsq, 1010 | half_move_counter, 1011 | }; 1012 | Ok(()) 1013 | } 1014 | 1015 | pub fn ctm(&self) -> Color { 1016 | self.ctm 1017 | } 1018 | 1019 | pub fn ply(&self) -> usize { 1020 | self.ply 1021 | } 1022 | 1023 | pub fn hash(&self) -> u64 { 1024 | let mut hash = self.material_hash; 1025 | 1026 | if let Some(sq) = self.history[self.ply].epsq { 1027 | hash ^= ZOBRIST.ep_hash(sq); 1028 | } 1029 | 1030 | hash ^ ZOBRIST.color_hash(self.ctm) 1031 | } 1032 | 1033 | pub fn material_hash(&self) -> u64 { 1034 | self.material_hash 1035 | } 1036 | 1037 | pub fn fullmove_number(&self) -> usize { 1038 | self.ply / 2 + 1 1039 | } 1040 | } 1041 | 1042 | impl Default for Board { 1043 | fn default() -> Self { 1044 | Self { 1045 | piece_type_bb: PieceTypeMap::new([Bitboard::ZERO; PieceType::N_PIECE_TYPES]), 1046 | color_bb: ColorMap::new([Bitboard::ZERO; Color::N_COLORS]), 1047 | board: SQMap::new([None; SQ::N_SQUARES]), 1048 | ctm: Color::White, 1049 | ply: 0, 1050 | material_hash: 0, 1051 | network: Network::new(), 1052 | history: [HistoryEntry::default(); Self::N_HISTORIES], 1053 | } 1054 | } 1055 | } 1056 | 1057 | impl TryFrom<&str> for Board { 1058 | type Error = &'static str; 1059 | 1060 | fn try_from(fen: &str) -> Result { 1061 | let mut board = Board::default(); 1062 | board.set_fen(fen)?; 1063 | Ok(board) 1064 | } 1065 | } 1066 | 1067 | impl fmt::Display for Board { 1068 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 1069 | let mut board_str = String::new(); 1070 | for rank_idx in (0..=7).rev() { 1071 | let rank = Rank::from(rank_idx); 1072 | let mut empty_squares = 0; 1073 | for file_idx in 0..=7 { 1074 | let file = File::from(file_idx); 1075 | let sq = SQ::encode(rank, file); 1076 | match self.board[sq] { 1077 | Some(pc) => { 1078 | if empty_squares != 0 { 1079 | board_str.push_str(empty_squares.to_string().as_str()); 1080 | empty_squares = 0; 1081 | } 1082 | board_str.push_str(pc.to_string().as_str()); 1083 | } 1084 | None => { 1085 | empty_squares += 1; 1086 | } 1087 | } 1088 | } 1089 | if empty_squares != 0 { 1090 | board_str.push_str(empty_squares.to_string().as_str()); 1091 | } 1092 | if rank != Rank::One { 1093 | board_str.push('/'); 1094 | } 1095 | } 1096 | 1097 | let mut castling_rights_str = String::new(); 1098 | for (symbol, mask) in "KQkq".chars().zip([ 1099 | Bitboard::WHITE_OO_MASK, 1100 | Bitboard::WHITE_OOO_MASK, 1101 | Bitboard::BLACK_OO_MASK, 1102 | Bitboard::BLACK_OOO_MASK, 1103 | ]) { 1104 | if mask & self.history[self.ply].entry == Bitboard::ZERO { 1105 | castling_rights_str.push(symbol); 1106 | } 1107 | } 1108 | if castling_rights_str.is_empty() { 1109 | castling_rights_str = "-".to_string(); 1110 | } 1111 | 1112 | let epsq_str = match self.history[self.ply].epsq { 1113 | Some(epsq) => epsq.to_string(), 1114 | None => "-".to_string(), 1115 | }; 1116 | 1117 | write!( 1118 | f, 1119 | "{} {} {} {} {} {}", 1120 | board_str, 1121 | self.ctm, 1122 | castling_rights_str, 1123 | epsq_str, 1124 | self.history[self.ply].half_move_counter, 1125 | self.ply / 2 + 1, 1126 | ) 1127 | } 1128 | } 1129 | 1130 | impl fmt::Debug for Board { 1131 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 1132 | let mut s = String::with_capacity(SQ::N_SQUARES * 2 + 8); 1133 | for rank_idx in (0..=7).rev() { 1134 | let rank = Rank::from(rank_idx); 1135 | for file_idx in 0..=7 { 1136 | let file = File::from(file_idx); 1137 | let sq = SQ::encode(rank, file); 1138 | let pc_str = self 1139 | .piece_at(sq) 1140 | .map_or("-".to_string(), |pc| pc.to_string()); 1141 | s.push_str(&pc_str); 1142 | s.push(' '); 1143 | if sq.file() == File::H { 1144 | s.push('\n'); 1145 | } 1146 | } 1147 | } 1148 | write!(f, "{s}") 1149 | } 1150 | } 1151 | 1152 | impl Board { 1153 | const N_HISTORIES: usize = 1024; 1154 | const STARTING_FEN: &'static str = "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"; 1155 | } 1156 | 1157 | static FEN_RE: LazyLock = LazyLock::new(|| { 1158 | Regex::new( 1159 | r"(?x)^ 1160 | (?P[KQRBNPkqrbnp1-8/]+)\s+ 1161 | (?P[wb])\s+ 1162 | (?P[KQkq\-]+)\s+ 1163 | (?P[a-h1-8\-]+) 1164 | (?:\s+(?P\d+))? 1165 | (?:\s+(?P\d+))? 1166 | $", 1167 | ) 1168 | .expect("Failed to compile fen regex.") 1169 | }); 1170 | 1171 | #[derive(Clone, Copy, Debug, Default)] 1172 | pub struct HistoryEntry { 1173 | entry: Bitboard, 1174 | captured: Option, 1175 | epsq: Option, 1176 | moov: Option, 1177 | material_hash: u64, 1178 | half_move_counter: u16, 1179 | plies_from_null: u16, 1180 | } 1181 | 1182 | #[cfg(test)] 1183 | mod tests { 1184 | use crate::board::*; 1185 | 1186 | #[test] 1187 | fn threefold_repetition() { 1188 | let mut board = Board::new(); 1189 | assert_eq!(board.is_repetition(), false); 1190 | board.push_str("e2e4").unwrap(); 1191 | assert_eq!(board.is_repetition(), false); 1192 | board.push_str("e7e5").unwrap(); 1193 | assert_eq!(board.is_repetition(), false); 1194 | board.push_str("f1c4").unwrap(); 1195 | assert_eq!(board.is_repetition(), false); 1196 | board.push_str("f8c5").unwrap(); 1197 | assert_eq!(board.is_repetition(), false); 1198 | board.push_str("c4f1").unwrap(); 1199 | assert_eq!(board.is_repetition(), false); 1200 | board.push_str("c5f8").unwrap(); 1201 | assert_eq!(board.is_repetition(), true); 1202 | } 1203 | } 1204 | --------------------------------------------------------------------------------