diff --git a/Cargo.lock b/Cargo.lock index da8d577f..455107a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -732,9 +732,9 @@ checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" [[package]] name = "smallvec" -version = "1.15.1" +version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" [[package]] name = "strsim" diff --git a/src/engine/mod.rs b/src/engine/mod.rs index 316671a4..8d24aab5 100644 --- a/src/engine/mod.rs +++ b/src/engine/mod.rs @@ -15,7 +15,6 @@ use crate::{ search::{ Depth, MAX_DEPTH, Searcher, game_history::GameHistory, - picker::MovePicker, score_table::{DEFAULT_TABLE_SIZE_MB, ScoreTable}, status::{SearchStatus, SearchStatusValue}, }, @@ -111,10 +110,11 @@ impl Engine { status.set(SearchStatusValue::Stopped); - if let Some(best_move) = pv.first().cloned().or_else(|| { - let mut picker = MovePicker::new(&position, false, None, [None, None]); - picker.next().map(|mov| mov.to_string()) - }) { + if let Some(best_move) = pv + .first() + .cloned() + .or_else(|| position.moves(MoveStage::All).into_iter().next().map(|m| m.to_string())) + { if let Some(ponder_move) = pv.get(1) { println!("bestmove {} ponder {}", best_move, ponder_move); } else { diff --git a/src/main.rs b/src/main.rs index 98bcd27d..0b8458bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,9 +11,9 @@ use crate::{ repl::{Command, DumpCommand, PositionCommand, repl}, time::get_duration, }, - moves::{Move, generate::magics::save_magics}, + moves::generate::magics::save_magics, position::{ - Position, + MoveStage, Position, color::Color, fen::{KIWIPETE_POSITION, START_POSITION}, hash::save_zobrist_numbers, @@ -21,7 +21,6 @@ use crate::{ search::{ game_history::GameHistory, perft::perft, - picker::MovePicker, score_table::{DEFAULT_TABLE_SIZE_MB, MAX_TABLE_SIZE_MB, MIN_TABLE_SIZE_MB}, status::SearchStatusValue, }, @@ -157,14 +156,13 @@ fn main() { engine.score_table.lock().unwrap().clear(); } Command::List => { - let picker = MovePicker::new(&engine.position, false, None, [None, None]); - let moves = picker.into_iter().collect::>(); - let fmt = moves.iter().map(|m| format!("{} ", m)).collect::(); + let mut moves = engine.position.moves(MoveStage::All); + let fmt = moves.into_iter().map(|m| format!("{} ", m)).collect::(); println!("{}\n{} moves", fmt, moves.len()); } Command::Move { r#move } => { - let picker = MovePicker::new(&engine.position, false, None, [None, None]); - if let Some(mov) = picker.into_iter().find(|&m| m.to_string() == r#move) { + let mut moves = engine.position.moves(MoveStage::All); + if let Some(mov) = moves.into_iter().find(|&m| m.to_string() == r#move) { let reversible = mov.is_reversible(&engine.position); engine.position = engine.position.make_move(mov); engine.game_history.lock().unwrap().push(&engine.position, reversible); diff --git a/src/moves/generate/castle.rs b/src/moves/generate/castle.rs index e0a707c7..bbe5ce4d 100644 --- a/src/moves/generate/castle.rs +++ b/src/moves/generate/castle.rs @@ -1,9 +1,10 @@ +use crate::moves::vec::MoveVec; use crate::moves::{Move, MoveFlag}; use crate::position::{ MoveStage, Position, bitboard::Bitboard, castling_rights::CastlingSide, piece::Piece, square::Square, }; -use super::{MoveVec, square_attackers}; +use super::square_attackers; const COLOR_CASTLE_RANKS: [Bitboard; 2] = [Bitboard::rank_mask(0), Bitboard::rank_mask(7)]; const FINAL_KING_SQUARES: [Square; 4] = [Square::G1, Square::C1, Square::G8, Square::C8]; @@ -90,7 +91,7 @@ pub fn castle_moves(position: &Position, stage: MoveStage, moves: &mut MoveVec) mod tests { use super::castle_moves; use crate::{ - moves::generate::MoveVec, + moves::vec::MoveVec, position::{MoveStage, Position}, }; use std::str::FromStr; @@ -102,7 +103,7 @@ mod tests { assert_eq!(buf.len(), moves.len()); - for m in buf { + for m in buf.into_iter() { assert!(moves.contains(&m.to_string().as_str())); } } diff --git a/src/moves/generate/leapers.rs b/src/moves/generate/leapers.rs index fb00b8e6..991c27d0 100644 --- a/src/moves/generate/leapers.rs +++ b/src/moves/generate/leapers.rs @@ -1,5 +1,5 @@ use crate::{ - moves::{Move, MoveFlag}, + moves::{Move, MoveFlag, vec::MoveVec}, position::{ Position, bitboard::Bitboard, @@ -9,7 +9,7 @@ use crate::{ }, }; -use super::{MoveStage, MoveVec}; +use super::MoveStage; pub type LeaperAttackMap = [Bitboard; 64]; diff --git a/src/moves/generate/mod.rs b/src/moves/generate/mod.rs index 688ff23c..63f768ea 100644 --- a/src/moves/generate/mod.rs +++ b/src/moves/generate/mod.rs @@ -1,11 +1,13 @@ -use super::{Move, MoveFlag, make::make_move}; -use crate::position::{MoveStage, Position, bitboard::Bitboard, color::Color, piece::Piece, square::Square}; +use super::{MoveFlag, make::make_move}; +use crate::{ + moves::vec::MoveVec, + position::{MoveStage, Position, bitboard::Bitboard, color::Color, piece::Piece, square::Square}, +}; use castle::castle_moves; use leapers::{king_attackers, king_regular_moves, knight_attackers, knight_moves}; use magics::queen_attacks; use pawns::{pawn_attackers, pawn_moves}; use sliders::{bishop_moves, diagonal_attackers, file_attackers, queen_moves, rook_moves}; -use smallvec::SmallVec; pub mod castle; pub mod leapers; @@ -13,10 +15,8 @@ pub mod magics; pub mod pawns; pub mod sliders; -pub type MoveVec = SmallVec<[Move; 64]>; - -pub fn generate_moves(position: &Position, stage: MoveStage) -> MoveVec { - let mut moves = MoveVec::new(); +pub fn generate_moves(position: &Position, stage: MoveStage, mut moves: &mut MoveVec) { + moves.clear(); let our_king = position .pieces_color_bb(Piece::King, position.side_to_move()) @@ -62,11 +62,9 @@ pub fn generate_moves(position: &Position, stage: MoveStage) -> MoveVec { _ => {} }; - let new_position = make_move::(position, *mov); + let new_position = make_move::(position, mov); !new_position.is_check() }); - - moves } pub fn square_attackers(position: &Position, square: Square, color: Color) -> Bitboard { @@ -81,17 +79,16 @@ pub fn square_attackers(position: &Position, square: Square, color: Color) -> Bi mod tests { use std::str::FromStr; - use crate::{moves::Move, position::Position}; + use crate::position::Position; use super::{MoveStage, MoveVec}; - fn assert_eq_vec_move(moves: &[Move], expected: &[&str]) { + fn assert_eq_vec_move(moves: &mut MoveVec, expected: &[&str]) { assert_eq!(moves.len(), expected.len()); - let mov_strs = moves.iter().map(|m| m.to_string()).collect::>(); - moves + let move_vec = moves.into_iter().map(|m| m.to_string()).collect::>(); + move_vec .iter() - .map(|m| m.to_string()) - .for_each(|m| assert!(expected.contains(&m.as_str()), "got: {:?}, expected: {:?}", mov_strs, expected)); + .for_each(|m| assert!(expected.contains(&m.as_str()), "got: {:?}, expected: {:?}", move_vec, expected)); } pub fn assert_staged_moves( @@ -103,14 +100,14 @@ mod tests { let mut moves1 = MoveVec::new(); function(&position, MoveStage::All, &mut moves1); - assert_eq_vec_move(&moves1, &expected[0]); + assert_eq_vec_move(&mut moves1, &expected[0]); let mut moves2 = MoveVec::new(); function(&position, MoveStage::CapturesAndPromotions, &mut moves2); - assert_eq_vec_move(&moves2, &expected[1]); + assert_eq_vec_move(&mut moves2, &expected[1]); let mut moves3 = MoveVec::new(); function(&position, MoveStage::Quiet, &mut moves3); - assert_eq_vec_move(&moves3, &expected[2]); + assert_eq_vec_move(&mut moves3, &expected[2]); } } diff --git a/src/moves/generate/pawns.rs b/src/moves/generate/pawns.rs index 8c818e67..026d0da7 100644 --- a/src/moves/generate/pawns.rs +++ b/src/moves/generate/pawns.rs @@ -1,5 +1,5 @@ use crate::{ - moves::{Move, MoveFlag}, + moves::{Move, MoveFlag, vec::MoveVec}, position::{ MoveStage, Position, bitboard::Bitboard, @@ -9,10 +9,7 @@ use crate::{ }, }; -use super::{ - MoveVec, - leapers::{LeaperAttackMap, init_leaper_attacks}, -}; +use super::leapers::{LeaperAttackMap, init_leaper_attacks}; const PAWN_DIRECTIONS: [Direction; 2] = [Square::NORTH, Square::SOUTH]; diff --git a/src/moves/mod.rs b/src/moves/mod.rs index 530b9981..32cad707 100644 --- a/src/moves/mod.rs +++ b/src/moves/mod.rs @@ -5,6 +5,7 @@ use std::{fmt::Display, mem}; pub mod generate; pub mod make; +pub mod vec; primitive_enum! { MoveFlag u8; Quiet, @@ -205,9 +206,7 @@ mod tests { #[case("8/8/4k3/8/2p5/8/B2P2K1/8 w - - 0 1")] fn legal_are_pseudo_legal(#[case] fen: Fen) { let position = Position::try_from(fen.clone()).unwrap(); - position - .moves(MoveStage::All) - .iter() - .for_each(|m| assert!(m.pseudo_legal(&position))); + let mut moves = position.moves(MoveStage::All); + moves.into_iter().for_each(|m| assert!(m.pseudo_legal(&position))); } } diff --git a/src/moves/vec.rs b/src/moves/vec.rs new file mode 100644 index 00000000..01daf265 --- /dev/null +++ b/src/moves/vec.rs @@ -0,0 +1,252 @@ +use std::mem::MaybeUninit; + +use crate::moves::Move; + +const MAX_SIZE: usize = 128; + +pub struct MoveVec { + data: [MaybeUninit<(Option, i8)>; MAX_SIZE], + raw_size: usize, + length: usize, +} + +impl Default for MoveVec { + fn default() -> Self { + Self { + data: [MaybeUninit::uninit(); MAX_SIZE], + raw_size: 0, + length: 0, + } + } +} + +pub struct MoveVecIterator<'a> { + move_list: &'a mut MoveVec, + index: usize, +} + +impl<'a> Iterator for MoveVecIterator<'a> { + type Item = Move; + + fn next(&mut self) -> Option { + if self.index >= self.move_list.raw_size || self.index >= MAX_SIZE { + return None; + } + + let mut best_score = self.move_list.entry(self.index).1; + let mut currently_empty = self.move_list.entry(self.index).0.is_none(); + let mut curr_idx = self.index + 1; + + // Put the best move at the front without sorting the entire vector. + // This laziness fits the minimax model where most moves can be discarded. + while curr_idx < self.move_list.raw_size { + let (mov, score) = *self.move_list.entry(curr_idx); + if mov.is_some() && (score > best_score || currently_empty) { + best_score = score; + self.move_list.data.swap(self.index, curr_idx); + currently_empty = false; + } + curr_idx += 1; + } + + self.index += 1; + self.move_list.entry(self.index - 1).0 + } +} + +impl<'a> IntoIterator for &'a mut MoveVec { + type Item = Move; + type IntoIter = MoveVecIterator<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl MoveVec { + pub fn new() -> Self { + Self::default() + } + + pub fn len(&self) -> usize { + self.length + } + + pub fn is_empty(&self) -> bool { + self.length == 0 + } + + pub fn clear(&mut self) { + self.raw_size = 0; + self.length = 0; + } + + fn entry(&self, index: usize) -> &(Option, i8) { + // SAFETY: push_scored writes an entry before incrementing raw_size. + unsafe { self.data[index].assume_init_ref() } + } + + fn entry_mut(&mut self, index: usize) -> &mut (Option, i8) { + // SAFETY: push_scored writes an entry before incrementing raw_size. + unsafe { self.data[index].assume_init_mut() } + } + + pub fn push(&mut self, mov: Move) { + self.push_scored(mov, i8::MIN); + } + + pub fn push_scored(&mut self, mov: Move, score: i8) { + if self.raw_size < MAX_SIZE { + self.data[self.raw_size].write((Some(mov), score)); + self.raw_size += 1; + self.length += 1; + } + } + + pub fn iter_mut<'a>(&'a mut self) -> MoveVecIterator<'a> { + MoveVecIterator { + move_list: self, + index: 0, + } + } + + pub fn retain(&mut self, f: F) + where + F: Fn(Move) -> bool, + { + for i in 0..self.raw_size { + if let Some(mov) = self.entry(i).0 + && !f(mov) + { + *self.entry_mut(i) = (None, i8::MIN); + self.length -= 1; + } + } + } + + pub fn evaluate(&mut self, f: F) + where + F: Fn(Move) -> i8, + { + for i in 0..self.raw_size { + if let Some(mov) = self.entry(i).0 { + self.entry_mut(i).1 = f(mov); + } + } + } + + pub fn contains(&self, mov: Move) -> bool { + for i in 0..self.raw_size { + if let Some(m) = self.entry(i).0 + && m == mov + { + return true; + } + } + false + } +} + +#[cfg(test)] +mod tests { + use crate::{ + moves::{Move, MoveFlag, vec::MoveVec}, + position::square::Square, + }; + + #[test] + fn push_len_clear() { + let mut list = MoveVec::default(); + assert_eq!(list.len(), 0); + list.push(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush)); + list.push(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush)); + assert_eq!(list.len(), 2); + list.clear(); + assert_eq!(list.len(), 0); + list.push(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush)); + assert_eq!(list.len(), 1); + } + + #[test] + fn iter() { + let mut list = MoveVec::default(); + list.push_scored(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush), 0); + list.push_scored(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush), 2); + assert_eq!(list.entry(0).0, Some(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush))); + + let mut iter = list.iter_mut(); + assert_eq!(iter.next(), Some(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush))); + assert_eq!(iter.next(), Some(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush))); + assert_eq!(iter.next(), None); + assert_eq!(list.entry(0).0, Some(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush))); + + *list.entry_mut(0) = (None, i8::MIN); + let mut iter2 = list.iter_mut(); + assert_eq!(iter2.next(), Some(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush))); + assert_eq!(iter2.next(), None); + } + + #[test] + pub fn retain_iter() { + let mut list = MoveVec::default(); + list.push_scored(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush), 0); + list.push_scored(Move::new(Square::G1, Square::F3, MoveFlag::Quiet), 2); + list.push_scored(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush), 4); + list.push_scored(Move::new(Square::B8, Square::C6, MoveFlag::Quiet), 6); + assert_eq!(list.len(), 4); + assert_eq!(list.raw_size, 4); + + list.retain(|m| m.flag() == MoveFlag::Quiet); + assert_eq!(list.len(), 2); + assert_eq!(list.raw_size, 4); + + let mut iter = list.iter_mut(); + assert_eq!(iter.next(), Some(Move::new(Square::B8, Square::C6, MoveFlag::Quiet))); + assert_eq!(iter.next(), Some(Move::new(Square::G1, Square::F3, MoveFlag::Quiet))); + assert_eq!(iter.next(), None); + + assert_eq!(list.len(), 2); + assert_eq!(list.raw_size, 4); + + assert_eq!(list.entry(0).0, Some(Move::new(Square::B8, Square::C6, MoveFlag::Quiet))); + assert_eq!(list.entry(1).0, Some(Move::new(Square::G1, Square::F3, MoveFlag::Quiet))); + assert_eq!(list.entry(2).0, None); + assert_eq!(list.entry(3).0, None); + } + + #[test] + fn evaluate_iter() { + let mut list = MoveVec::default(); + list.push(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush)); + list.push(Move::new(Square::G1, Square::F3, MoveFlag::Quiet)); + + let mut iter = list.iter_mut(); + assert_eq!(iter.next(), Some(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush))); + assert_eq!(iter.next(), Some(Move::new(Square::G1, Square::F3, MoveFlag::Quiet))); + assert_eq!(iter.next(), None); + + list.evaluate(|m| match m.flag() { + MoveFlag::DoublePawnPush => 1, + MoveFlag::Quiet => 2, + _ => 0, + }); + + let mut iter = list.iter_mut(); + assert_eq!(iter.next(), Some(Move::new(Square::G1, Square::F3, MoveFlag::Quiet))); + assert_eq!(iter.next(), Some(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush))); + assert_eq!(iter.next(), None); + } + + #[test] + fn contains() { + let mut list = MoveVec::default(); + let mov1 = Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush); + let mov2 = Move::new(Square::G1, Square::F3, MoveFlag::Quiet); + list.push(mov1); + list.push(mov2); + + assert!(list.contains(mov1)); + assert!(list.contains(mov2)); + assert!(!list.contains(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush))); + } +} diff --git a/src/position/mod.rs b/src/position/mod.rs index 77b00ec7..824d7f93 100644 --- a/src/position/mod.rs +++ b/src/position/mod.rs @@ -4,9 +4,11 @@ use std::{ str::FromStr, }; +use crate::moves::vec::MoveVec; + use super::moves::{ Move, - generate::{MoveVec, generate_moves, square_attackers}, + generate::{generate_moves, square_attackers}, make::make_move, }; use bitboard::Bitboard; @@ -272,7 +274,9 @@ impl Position { } pub fn moves(&self, stage: MoveStage) -> MoveVec { - generate_moves(self, stage) + let mut move_vec = MoveVec::new(); + generate_moves(self, stage, &mut move_vec); + move_vec } pub fn make_move(&self, mov: Move) -> Self { @@ -284,8 +288,8 @@ impl Position { } pub fn get_move_str(&self, mov: &str) -> Option { - let moves = generate_moves(self, MoveStage::All); - moves.iter().find(|&m| m.to_string().as_str() == mov).copied() + let mut moves = self.moves(MoveStage::All); + moves.into_iter().find(|m| m.to_string().as_str() == mov) } pub fn is_check(&self) -> bool { diff --git a/src/search/mod.rs b/src/search/mod.rs index 63aeabff..b1eb382f 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -1,8 +1,8 @@ pub mod game_history; pub mod heuristics; pub mod killer_table; +pub mod move_evaluator; pub mod perft; -pub mod picker; pub mod score_table; pub mod see; pub mod status; @@ -23,7 +23,7 @@ use crate::{ game_history::GameHistory, heuristics::maybe_zug, killer_table::KillerTable, - picker::MovePicker, + move_evaluator::MoveEvaluator, score_table::ScoreTable, see::static_exchange, status::{SearchStatus, SearchStatusValue}, @@ -78,7 +78,7 @@ impl<'a> Searcher<'a> { while let Some(mov) = self .table .hash_move(&position) - .filter(|m| position.moves(MoveStage::All).contains(m)) + .filter(|m| position.moves(MoveStage::All).contains(*m)) { moves.push(mov); let next = position.make_move(mov); @@ -130,13 +130,16 @@ impl<'a> Searcher<'a> { } } - let picker = MovePicker::new(position, !is_check, None, [None, None]); + let mut moves = position.moves(if !is_check { MoveStage::CapturesAndPromotions } else { MoveStage::All }); - if picker.is_empty() { + if moves.is_empty() { return if is_check { MATE_SCORE + ply as ValueScore } else { window.best() }; } - for mov in picker { + let evaluator = MoveEvaluator::new(position, None, [None, None]); + moves.evaluate(|m| evaluator.evaluate(m)); + + for mov in moves.iter_mut() { if !is_check && static_exchange(position, mov) < 0 { continue; } @@ -203,12 +206,15 @@ impl<'a> Searcher<'a> { } let hash_move = self.table.hash_move(position); - let picker = MovePicker::new(position, false, hash_move, self.killers.get(ply)); + let mut moves = position.moves(MoveStage::All); - if picker.is_empty() { + if moves.is_empty() { return if is_check { MATE_SCORE + ply as ValueScore } else { 0 }; } + let evaluator = MoveEvaluator::new(position, hash_move, self.killers.get(ply)); + moves.evaluate(|m| evaluator.evaluate(m)); + let mut node_type = NodeType::AllNode; let is_frontier_node = depth < 3; let may_futility_prune = is_frontier_node @@ -220,7 +226,7 @@ impl<'a> Searcher<'a> { depth = depth.saturating_add(1).min(MAX_DEPTH); } - for (i, mov) in picker.enumerate() { + for (i, mov) in moves.into_iter().enumerate() { if i > 0 && mov.is_quiet() && may_futility_prune { continue; } diff --git a/src/search/move_evaluator.rs b/src/search/move_evaluator.rs new file mode 100644 index 00000000..3d4f6440 --- /dev/null +++ b/src/search/move_evaluator.rs @@ -0,0 +1,186 @@ +use crate::{ + moves::Move, + position::{Position, piece::Piece}, + search::see::static_exchange, +}; + +pub struct MoveEvaluator<'a> { + position: &'a Position, + hash_move: Option, + killer_moves: [Option; 2], +} + +#[rustfmt::skip] +const QUIET_PSQT: [i8; 64] = [ + 0, 0, 0, 0, 0, 0, 0, 0, + 1, 2, 2, 2, 2, 2, 2, 1, + 1, 2, 4, 4, 4, 4, 2, 1, + 1, 2, 4, 6, 6, 4, 2, 1, + 1, 2, 4, 6, 6, 4, 2, 1, + 1, 2, 4, 4, 4, 4, 2, 1, + 1, 2, 2, 2, 2, 2, 2, 1, + 0, 0, 0, 0, 0, 0, 0, 0, +]; + +impl<'a> MoveEvaluator<'a> { + pub fn new(position: &'a Position, hash_move: Option, killer_moves: [Option; 2]) -> Self { + Self { + position, + hash_move, + killer_moves, + } + } + + pub fn evaluate(&self, mov: Move) -> i8 { + if Some(mov) == self.hash_move { + i8::MAX + } else if mov.promotion_piece() == Some(Piece::Queen) { + 72 + mov.is_capture() as i8 + } else if mov.promotion_piece().is_some() { + -72 + } else if mov.is_capture() { + let see = static_exchange(self.position, mov); + if see >= 0 { + let mvv_lva = self.position.piece_at(mov.to()).unwrap_or(Piece::Pawn).value() + - self.position.piece_at(mov.from()).unwrap().value(); + 32 + see + mvv_lva + } else { + -16 + see + } + } else if Some(mov) == self.killer_moves[0] || Some(mov) == self.killer_moves[1] { + 0 + } else if self.position.piece_at(mov.from()).unwrap().value() <= 3 { + -9 + QUIET_PSQT[mov.to() as usize] - QUIET_PSQT[mov.from() as usize] + } else { + -9 + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + moves::{Move, MoveFlag}, + position::{MoveStage, Position, fen::START_POSITION, square::Square}, + search::{move_evaluator::MoveEvaluator, see::static_exchange}, + }; + use std::str::FromStr; + + fn mocks() -> (Position, [Option; 2]) { + let position = Position::from_str("3rk1nr/1p3pbp/p1npb1pP/4p1q1/P1B1P3/8/1PP2PP1/RNBQNRK1 w k - 2 15").unwrap(); + let killers = [ + Some(Move::new(Square::E1, Square::F3, MoveFlag::Quiet)), + Some(Move::new(Square::C1, Square::E3, MoveFlag::Quiet)), + ]; + (position, killers) + } + + fn moves( + position: Position, + only_captures: bool, + hash_move: Option, + killer_moves: [Option; 2], + ) -> Vec { + let evaluator = MoveEvaluator::new(&position, hash_move, killer_moves); + let mut moves = position.moves(if only_captures { MoveStage::CapturesAndPromotions } else { MoveStage::All }); + let mut scored_moves = moves + .into_iter() + .map(|mov| (mov, evaluator.evaluate(mov))) + .collect::>(); + scored_moves.sort_by(|a, b| b.1.cmp(&a.1)); + scored_moves.into_iter().map(|(mov, _)| mov).collect::>() + } + + #[test] + fn hash_first() { + let position = Position::from_str(START_POSITION).unwrap(); + let hash_move = Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush); + let vec = moves(position, false, Some(hash_move), [None, None]); + assert_eq!(vec.len(), 20); + assert_eq!(vec[0], hash_move); + } + + #[test] + fn discard_illegal_hash_move() { + let position = Position::from_str("r1b2rk1/1p1n1ppp/4p3/p1P1bq2/3P4/1Q2BN2/2N2PPP/R3R1K1 b - -").unwrap(); + let hash_move = Move::new(Square::E5, Square::F6, MoveFlag::EnpassantCapture); + let vec = moves(position, false, Some(hash_move), [None, None]); + assert_ne!(vec[0], hash_move); + } + + #[test] + fn queen_promotion_first() { + let position = Position::from_str("8/5P2/8/7p/8/1Kp5/2N3kP/8 w - - 1 51").unwrap(); + let vec = moves(position, false, None, [None, None]); + assert_eq!(vec[0], Move::new(Square::F7, Square::F8, MoveFlag::QueenPromotion)); + assert!(vec[1].promotion_piece().is_none()); + } + + #[test] + fn underpromotions_last() { + let position = Position::from_str("8/5P2/8/7p/8/1Kp5/2N3kP/8 w - - 1 51").unwrap(); + let mut vec = moves(position, false, None, [None, None]); + let number_of_moves = position.moves(MoveStage::All).len(); + for _ in 0..(number_of_moves - 3) { + vec.remove(0); + } + assert!(vec[0].promotion_piece().is_some()); + assert!(vec[1].promotion_piece().is_some()); + assert!(vec[2].promotion_piece().is_some()); + assert!(vec.len() == 3); + } + + #[test] + fn winning_captures_first() { + let (position, killers) = mocks(); + let vec = moves(position, false, None, killers); + assert_eq!(vec[0], Move::new(Square::C1, Square::G5, MoveFlag::Capture)); + assert_eq!(vec[1], Move::new(Square::H6, Square::G7, MoveFlag::Capture)); + assert_eq!(vec[2], Move::new(Square::C4, Square::E6, MoveFlag::Capture)); + } + + #[test] + fn good_captures_then_killers_then_bad_captures() { + let (position, killers) = mocks(); + let vec = moves(position, false, None, killers); + + let first_killer_idx = vec.iter().position(|mov| killers.contains(&Some(*mov))).unwrap(); + let last_good_capture_idx = vec + .iter() + .rposition(|mov| mov.is_capture() && static_exchange(&position, *mov) >= 0) + .unwrap(); + assert!(last_good_capture_idx < first_killer_idx); + + let first_bad_capture_idx = vec + .iter() + .position(|mov| mov.is_capture() && static_exchange(&position, *mov) < 0) + .unwrap(); + assert!(first_killer_idx < first_bad_capture_idx); + } + + #[test] + fn quiet_center_heuristic() { + let (position, killers) = mocks(); + let vec = moves(position, false, None, killers); + + let knight_to_corner_idx = vec + .iter() + .position(|mov| mov.from() == Square::B1 && mov.to() == Square::A3) + .unwrap(); + let knight_to_center_idx = vec + .iter() + .position(|mov| mov.from() == Square::B1 && mov.to() == Square::C3) + .unwrap(); + assert!(knight_to_center_idx < knight_to_corner_idx); + + let bishop_retreat_idx = vec + .iter() + .position(|mov| mov.from() == Square::C4 && mov.to() == Square::E2) + .unwrap(); + let bishop_to_center_idx = vec + .iter() + .position(|mov| mov.from() == Square::C4 && mov.to() == Square::D5) + .unwrap(); + assert!(bishop_to_center_idx < bishop_retreat_idx); + } +} diff --git a/src/search/perft.rs b/src/search/perft.rs index cde4b6d9..4781fc3f 100644 --- a/src/search/perft.rs +++ b/src/search/perft.rs @@ -1,5 +1,5 @@ use crate::{ - moves::{Move, generate::generate_moves, make::make_move}, + moves::{Move, make::make_move}, position::{MoveStage, Position}, search::{ Depth, @@ -12,13 +12,13 @@ pub fn perft(position: &Position, depth: Depth, status: &SearchStatus) -> (usize if depth == 0 || status.get() == SearchStatusValue::Stopped { return 1; } - let moves = generate_moves(position, MoveStage::All); + let mut moves = position.moves(MoveStage::All); if depth == 1 { moves.len() } else { moves - .iter() - .map(|&m| go(&make_move::(position, m), depth - 1, status)) + .into_iter() + .map(|m| go(&make_move::(position, m), depth - 1, status)) .sum() } } @@ -27,9 +27,9 @@ pub fn perft(position: &Position, depth: Depth, status: &SearchStatus) -> (usize return (1, vec![]); } let mut divided = Vec::with_capacity(64); - let moves = generate_moves(position, MoveStage::All); + let mut moves = position.moves(MoveStage::All); let mut count = 0; - for m in moves { + for m in &mut moves { let branch = go(&make_move::(position, m), depth - 1, status); println!("{}: {}", m, branch); divided.push((m, branch)); diff --git a/src/search/picker.rs b/src/search/picker.rs deleted file mode 100644 index f5697c20..00000000 --- a/src/search/picker.rs +++ /dev/null @@ -1,243 +0,0 @@ -use smallvec::SmallVec; - -use crate::{ - moves::Move, - position::{MoveStage, Position, piece::Piece}, - search::see::static_exchange, -}; - -type ScoredMoveVec = SmallVec<[(Move, i8); 64]>; - -#[rustfmt::skip] -const QUIET_PSQT: [i8; 64] = [ - 0, 0, 0, 0, 0, 0, 0, 0, - 1, 2, 2, 2, 2, 2, 2, 1, - 1, 2, 4, 4, 4, 4, 2, 1, - 1, 2, 4, 6, 6, 4, 2, 1, - 1, 2, 4, 6, 6, 4, 2, 1, - 1, 2, 4, 4, 4, 4, 2, 1, - 1, 2, 2, 2, 2, 2, 2, 1, - 0, 0, 0, 0, 0, 0, 0, 0, -]; - -pub struct MovePicker { - moves: ScoredMoveVec, - current: usize, -} - -impl MovePicker { - pub fn new( - position: &Position, - captures_only: bool, - hash_move: Option, - killer_moves: [Option; 2], - ) -> Self { - let move_value = |mov: Move| -> i8 { - if Some(mov) == hash_move { - i8::MAX - } else if mov.promotion_piece() == Some(Piece::Queen) { - 72 + mov.is_capture() as i8 - } else if mov.promotion_piece().is_some() { - -72 - } else if mov.is_capture() { - let see = static_exchange(position, mov); - if see >= 0 { - let mvv_lva = position.piece_at(mov.to()).unwrap_or(Piece::Pawn).value() - - position.piece_at(mov.from()).unwrap().value(); - 32 + see + mvv_lva - } else { - -16 + see - } - } else if Some(mov) == killer_moves[0] || Some(mov) == killer_moves[1] { - 0 - } else if position.piece_at(mov.from()).unwrap().value() <= 3 { - -9 + QUIET_PSQT[mov.to() as usize] - QUIET_PSQT[mov.from() as usize] - } else { - -9 - } - }; - - let generate = if captures_only { MoveStage::CapturesAndPromotions } else { MoveStage::All }; - let moves = position - .moves(generate) - .iter() - .map(|&mov| (mov, move_value(mov))) - .collect::(); - - Self { moves, current: 0 } - } - - pub fn len(&self) -> usize { - self.moves.len().saturating_sub(self.current) - } - - pub fn is_empty(&self) -> bool { - self.len() == 0 - } -} - -impl Iterator for MovePicker { - type Item = Move; - - fn next(&mut self) -> Option { - if self.current >= self.moves.len() { - return None; - } - - let mut best_score = self.moves[self.current].1; - - for i in (self.current + 1)..self.moves.len() { - if self.moves[i].1 > best_score { - best_score = self.moves[i].1; - self.moves.swap(i, self.current); - } - } - - self.current += 1; - Some(self.moves[self.current - 1].0) - } -} - -#[cfg(test)] -mod tests { - use super::MovePicker; - use crate::{ - moves::{Move, MoveFlag}, - position::{MoveStage, Position, fen::START_POSITION, square::Square}, - search::see::static_exchange, - }; - use std::str::FromStr; - - fn mocks() -> (Position, MovePicker, [Option; 2]) { - let position = Position::from_str("3rk1nr/1p3pbp/p1npb1pP/4p1q1/P1B1P3/8/1PP2PP1/RNBQNRK1 w k - 2 15").unwrap(); - let killers = [ - Some(Move::new(Square::E1, Square::F3, MoveFlag::Quiet)), - Some(Move::new(Square::C1, Square::E3, MoveFlag::Quiet)), - ]; - let picker = MovePicker::new(&position, false, None, killers); - (position, picker, killers) - } - - #[test] - fn no_moves() { - let position = Position::from_str("8/k5K1/8/8/8/8/1Q6/Q7 b - - 16 65").unwrap(); - - let picker = MovePicker::new(&position, false, None, [None, None]); - assert_eq!(picker.len(), 0); - assert!(picker.is_empty()); - - let mut picker = picker.peekable(); - assert!(picker.peek().is_none()); - assert!(picker.next().is_none()); - } - - #[test] - fn hash_first() { - let position = Position::from_str(START_POSITION).unwrap(); - let hash_move = Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush); - let mut picker = MovePicker::new(&position, false, Some(hash_move), [None, None]); - assert_eq!(picker.len(), 20); - assert_eq!(picker.next(), Some(hash_move)); - assert_eq!(picker.len(), 19); - } - - #[test] - fn discard_illegal_hash_move() { - let position = Position::from_str("r1b2rk1/1p1n1ppp/4p3/p1P1bq2/3P4/1Q2BN2/2N2PPP/R3R1K1 b - -").unwrap(); - let hash_move = Move::new(Square::E5, Square::F6, MoveFlag::EnpassantCapture); - let mut picker = MovePicker::new(&position, false, Some(hash_move), [None, None]); - assert_ne!(picker.next(), Some(hash_move)); - } - - #[test] - fn no_repeated_hash() { - let position = Position::from_str(START_POSITION).unwrap(); - let moves = position.moves(MoveStage::All); - let picker = MovePicker::new(&position, false, Some(moves[0]), [None, None]); - assert_eq!(picker.collect::>().len(), moves.len()); - } - - #[test] - fn queen_promotion_first() { - let position = Position::from_str("8/5P2/8/7p/8/1Kp5/2N3kP/8 w - - 1 51").unwrap(); - let mut picker = MovePicker::new(&position, false, None, [None, None]); - assert_eq!(picker.next(), Some(Move::new(Square::F7, Square::F8, MoveFlag::QueenPromotion))); - assert!(picker.next().unwrap().promotion_piece().is_none()); - } - - #[test] - fn underpromotions_last() { - let position = Position::from_str("8/5P2/8/7p/8/1Kp5/2N3kP/8 w - - 1 51").unwrap(); - let mut picker = MovePicker::new(&position, false, None, [None, None]); - let number_of_moves = position.moves(MoveStage::All).len(); - for _ in 0..(number_of_moves - 3) { - picker.next(); - } - assert!(picker.next().unwrap().promotion_piece().is_some()); - assert!(picker.next().unwrap().promotion_piece().is_some()); - assert!(picker.next().unwrap().promotion_piece().is_some()); - assert!(picker.next().is_none()); - } - - #[test] - fn winning_captures_first() { - let (_, mut picker, _) = mocks(); - assert_eq!(picker.next(), Some(Move::new(Square::C1, Square::G5, MoveFlag::Capture))); - assert_eq!(picker.next(), Some(Move::new(Square::H6, Square::G7, MoveFlag::Capture))); - assert_eq!(picker.next(), Some(Move::new(Square::C4, Square::E6, MoveFlag::Capture))); - } - - #[test] - fn good_captures_then_killers_then_bad_captures() { - let (position, picker, killers) = mocks(); - let moves = picker.collect::>(); - - let first_killer_idx = moves.iter().position(|mov| killers.contains(&Some(*mov))).unwrap(); - let last_good_capture_idx = moves - .iter() - .rposition(|mov| mov.is_capture() && static_exchange(&position, *mov) >= 0) - .unwrap(); - assert!(last_good_capture_idx < first_killer_idx); - - let first_bad_capture_idx = moves - .iter() - .position(|mov| mov.is_capture() && static_exchange(&position, *mov) < 0) - .unwrap(); - assert!(first_killer_idx < first_bad_capture_idx); - } - - #[test] - fn quiet_center_heuristic() { - let (_, picker, _) = mocks(); - let moves = picker.collect::>(); - - let knight_to_corner_idx = moves - .iter() - .position(|mov| mov.from() == Square::B1 && mov.to() == Square::A3) - .unwrap(); - let knight_to_center_idx = moves - .iter() - .position(|mov| mov.from() == Square::B1 && mov.to() == Square::C3) - .unwrap(); - assert!(knight_to_center_idx < knight_to_corner_idx); - - let bishop_retreat_idx = moves - .iter() - .position(|mov| mov.from() == Square::C4 && mov.to() == Square::E2) - .unwrap(); - let bishop_to_center_idx = moves - .iter() - .position(|mov| mov.from() == Square::C4 && mov.to() == Square::D5) - .unwrap(); - assert!(bishop_to_center_idx < bishop_retreat_idx); - } - - #[test] - fn only_captures() { - let position = Position::from_str("3rrkR1/1p6/p5p1/2p5/1qb1B3/2N3P1/PP2PP2/2KR4 b - - 1 26").unwrap(); - let mut picker = MovePicker::new(&position, true, None, [None, None]); - assert_eq!(picker.next(), Some(Move::new(Square::C4, Square::G8, MoveFlag::Capture))); - assert_eq!(picker.next(), Some(Move::new(Square::F8, Square::G8, MoveFlag::Capture))); - assert_eq!(picker.next(), None); - } -} diff --git a/src/search/see.rs b/src/search/see.rs index 71fa9bc3..265e0a34 100644 --- a/src/search/see.rs +++ b/src/search/see.rs @@ -116,9 +116,9 @@ mod tests { #[case("Q7/1P5k/1n6/8/8/4K3/8/r7 b - - 0 1", "b6a8", 7)] fn see_sequence(#[case] fen: Fen, #[case] mov: &str, #[case] value: i8) { let position = Position::try_from(fen).unwrap(); - let mov = *position + let mov = position .moves(MoveStage::CapturesAndPromotions) - .iter() + .into_iter() .find(|m| m.to_string() == mov) .unwrap(); assert_eq!(static_exchange(&position, mov), value);