From 7f14f73737683b3703e987b2339524f970cb9ad9 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 18 Jul 2026 15:00:18 +0100 Subject: [PATCH 1/5] Implement movelist --- src/moves/list.rs | 200 ++++++++++++++++++++++++++++++++++++++++++++++ src/moves/mod.rs | 1 + 2 files changed, 201 insertions(+) create mode 100644 src/moves/list.rs diff --git a/src/moves/list.rs b/src/moves/list.rs new file mode 100644 index 00000000..c45a729b --- /dev/null +++ b/src/moves/list.rs @@ -0,0 +1,200 @@ +use crate::moves::Move; + +pub struct MoveList { + data: [(Option, i8); 256], + raw_size: usize, + length: usize, +} + +impl Default for MoveList { + fn default() -> Self { + Self { + data: [(None, i8::MIN); 256], + raw_size: 0, + length: 0, + } + } +} + +pub struct MoveListIterator<'a> { + move_list: &'a mut MoveList, + index: usize, +} + +impl<'a> Iterator for MoveListIterator<'a> { + type Item = Move; + + fn next(&mut self) -> Option { + let mut best_score = self.move_list.data[self.index].1; + let mut curr_idx = self.index + 1; + + // Put the best move at the front without sorting the entire list. + // This laziness fits the minimax model where most moves can be discarded. + while curr_idx < self.move_list.raw_size { + let score = self.move_list.data[curr_idx].1; + if score > best_score { + best_score = score; + self.move_list.data.swap(self.index, curr_idx); + } + curr_idx += 1; + } + + self.index += 1; + self.move_list.data[self.index - 1].0 + } +} + +impl<'a> IntoIterator for &'a mut MoveList { + type Item = Move; + type IntoIter = MoveListIterator<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter_mut() + } +} + +impl MoveList { + 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 push_internal(&mut self, mov: Move, score: Option) { + self.data[self.raw_size] = (Some(mov), score.unwrap_or(i8::MIN)); + self.raw_size += 1; + self.length += 1; + } + + pub fn push(&mut self, mov: Move) { + self.push_internal(mov, None); + } + + pub fn push_scored(&mut self, mov: Move, score: i8) { + self.push_internal(mov, Some(score)); + } + + pub fn iter_mut<'a>(&'a mut self) -> MoveListIterator<'a> { + MoveListIterator { + move_list: self, + index: 0, + } + } + + pub fn retain(&mut self, f: fn(Move) -> bool) { + for i in 0..self.raw_size { + if let Some(mov) = self.data[i].0 + && !f(mov) + { + self.data[i] = (None, i8::MIN); + self.length -= 1; + } + } + } + + pub fn evaluate(&mut self, f: fn(Move) -> i8) { + for i in 0..self.raw_size { + if let Some(mov) = self.data[i].0 { + self.data[i].1 = f(mov); + } + } + } +} + +#[cfg(test)] +mod tests { + use crate::{ + moves::{Move, MoveFlag, list::MoveList}, + position::square::Square, + }; + + #[test] + fn push_len_clear() { + let mut list = MoveList::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 = MoveList::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.data[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.data[0].0, Some(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush))); + + list.data[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 = MoveList::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.data[0].0, Some(Move::new(Square::B8, Square::C6, MoveFlag::Quiet))); + assert_eq!(list.data[1].0, Some(Move::new(Square::G1, Square::F3, MoveFlag::Quiet))); + assert_eq!(list.data[2].0, None); + assert_eq!(list.data[3].0, None); + } + + #[test] + fn evaluate_iter() { + let mut list = MoveList::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); + } +} diff --git a/src/moves/mod.rs b/src/moves/mod.rs index 530b9981..c743d7b7 100644 --- a/src/moves/mod.rs +++ b/src/moves/mod.rs @@ -4,6 +4,7 @@ use primitive_enum::primitive_enum; use std::{fmt::Display, mem}; pub mod generate; +pub mod list; pub mod make; primitive_enum! { MoveFlag u8; From 01179f633109b15843de8b1725cb8311a7033081 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 18 Jul 2026 15:54:53 +0100 Subject: [PATCH 2/5] Use MoveList --- Cargo.lock | 7 - Cargo.toml | 1 - src/engine/mod.rs | 10 +- src/main.rs | 12 +- src/moves/generate/castle.rs | 7 +- src/moves/generate/leapers.rs | 4 +- src/moves/generate/mod.rs | 35 +++-- src/moves/generate/pawns.rs | 7 +- src/moves/mod.rs | 9 +- src/moves/{list.rs => vec.rs} | 74 ++++++++--- src/position/mod.rs | 31 +++-- src/search/mod.rs | 30 +++-- src/search/move_evaluator.rs | 186 ++++++++++++++++++++++++++ src/search/perft.rs | 17 +-- src/search/picker.rs | 243 ---------------------------------- src/search/see.rs | 4 +- 16 files changed, 324 insertions(+), 353 deletions(-) rename src/moves/{list.rs => vec.rs} (78%) create mode 100644 src/search/move_evaluator.rs delete mode 100644 src/search/picker.rs diff --git a/Cargo.lock b/Cargo.lock index da8d577f..f26dbd99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,7 +107,6 @@ dependencies = [ "rstest", "serde", "serde_json", - "smallvec", "tempfile", ] @@ -730,12 +729,6 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" -[[package]] -name = "smallvec" -version = "1.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" - [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 7c30827e..15f44a31 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,6 @@ primitive_enum = "1.2.0" rand = "0.10.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.148" -smallvec = "1.15.1" [dev-dependencies] rstest = "0.26.1" 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..f92018d4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -13,7 +13,7 @@ use crate::{ }, moves::{Move, 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 c743d7b7..95f30228 100644 --- a/src/moves/mod.rs +++ b/src/moves/mod.rs @@ -4,8 +4,8 @@ use primitive_enum::primitive_enum; use std::{fmt::Display, mem}; pub mod generate; -pub mod list; pub mod make; +pub mod vec; primitive_enum! { MoveFlag u8; Quiet, @@ -105,6 +105,7 @@ impl Display for Move { mod tests { use super::{Move, MoveFlag}; use crate::moves::MoveFlag::*; + use crate::moves::vec::MoveVec; use crate::position::fen::{Fen, KIWIPETE_POSITION, START_POSITION}; use crate::position::piece::Piece::*; use crate::position::square::Square::*; @@ -206,9 +207,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/list.rs b/src/moves/vec.rs similarity index 78% rename from src/moves/list.rs rename to src/moves/vec.rs index c45a729b..63344aff 100644 --- a/src/moves/list.rs +++ b/src/moves/vec.rs @@ -1,34 +1,34 @@ use crate::moves::Move; -pub struct MoveList { - data: [(Option, i8); 256], +pub struct MoveVec { + data: [(Option, i8); 128], raw_size: usize, length: usize, } -impl Default for MoveList { +impl Default for MoveVec { fn default() -> Self { Self { - data: [(None, i8::MIN); 256], + data: [(None, i8::MIN); 128], raw_size: 0, length: 0, } } } -pub struct MoveListIterator<'a> { - move_list: &'a mut MoveList, +pub struct MoveVecIterator<'a> { + move_list: &'a mut MoveVec, index: usize, } -impl<'a> Iterator for MoveListIterator<'a> { +impl<'a> Iterator for MoveVecIterator<'a> { type Item = Move; fn next(&mut self) -> Option { let mut best_score = self.move_list.data[self.index].1; let mut curr_idx = self.index + 1; - // Put the best move at the front without sorting the entire list. + // 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 score = self.move_list.data[curr_idx].1; @@ -44,16 +44,20 @@ impl<'a> Iterator for MoveListIterator<'a> { } } -impl<'a> IntoIterator for &'a mut MoveList { +impl<'a> IntoIterator for &'a mut MoveVec { type Item = Move; - type IntoIter = MoveListIterator<'a>; + type IntoIter = MoveVecIterator<'a>; fn into_iter(self) -> Self::IntoIter { self.iter_mut() } } -impl MoveList { +impl MoveVec { + pub fn new() -> Self { + Self::default() + } + pub fn len(&self) -> usize { self.length } @@ -81,14 +85,17 @@ impl MoveList { self.push_internal(mov, Some(score)); } - pub fn iter_mut<'a>(&'a mut self) -> MoveListIterator<'a> { - MoveListIterator { + pub fn iter_mut<'a>(&'a mut self) -> MoveVecIterator<'a> { + MoveVecIterator { move_list: self, index: 0, } } - pub fn retain(&mut self, f: fn(Move) -> bool) { + pub fn retain(&mut self, f: F) + where + F: Fn(Move) -> bool, + { for i in 0..self.raw_size { if let Some(mov) = self.data[i].0 && !f(mov) @@ -99,25 +106,39 @@ impl MoveList { } } - pub fn evaluate(&mut self, f: fn(Move) -> i8) { + pub fn evaluate(&mut self, f: F) + where + F: Fn(Move) -> i8, + { for i in 0..self.raw_size { if let Some(mov) = self.data[i].0 { self.data[i].1 = f(mov); } } } + + pub fn contains(&self, mov: Move) -> bool { + for i in 0..self.raw_size { + if let Some(m) = self.data[i].0 + && m == mov + { + return true; + } + } + false + } } #[cfg(test)] mod tests { use crate::{ - moves::{Move, MoveFlag, list::MoveList}, + moves::{Move, MoveFlag, vec::MoveVec}, position::square::Square, }; #[test] fn push_len_clear() { - let mut list = MoveList::default(); + 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)); @@ -130,7 +151,7 @@ mod tests { #[test] fn iter() { - let mut list = MoveList::default(); + 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.data[0].0, Some(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush))); @@ -149,7 +170,7 @@ mod tests { #[test] pub fn retain_iter() { - let mut list = MoveList::default(); + 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); @@ -177,7 +198,7 @@ mod tests { #[test] fn evaluate_iter() { - let mut list = MoveList::default(); + 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)); @@ -197,4 +218,17 @@ mod tests { 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..29b42b52 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; @@ -15,7 +17,6 @@ use color::Color; use fen::Fen; use hash::ZobristHash; use piece::Piece; -use smallvec::SmallVec; use square::Square; pub mod bitboard; @@ -39,8 +40,6 @@ pub enum PositionDiffEntry { Clear(Square, Piece, Color), } -pub type PositionDiffVec = SmallVec<[PositionDiffEntry; 16]>; - #[derive(Debug, Clone, Copy)] pub struct Position { hash: ZobristHash, @@ -272,7 +271,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 +285,9 @@ 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 move_vec = MoveVec::new(); + generate_moves(self, MoveStage::All, &mut move_vec); + move_vec.into_iter().find(|&m| m.to_string().as_str() == mov) } pub fn is_check(&self) -> bool { @@ -305,8 +307,9 @@ impl Position { self.material } - pub fn diff(&self, other: &Self) -> PositionDiffVec { - let mut diff = PositionDiffVec::new(); + pub fn diff(&self, other: &Self) -> Vec { + // TODO: Force single instance. + let mut diff = Vec::with_capacity(16); let occ_new_white = self.occupancy_bb(Color::White); let occ_old_white = other.occupancy_bb(Color::White); @@ -362,8 +365,8 @@ mod tests { use crate::{ moves::{Move, MoveFlag}, position::{ - Piece, PositionDiffEntry, PositionDiffVec, bitboard::Bitboard, castling_rights::CastlingSide, color::Color, - fen::Fen, square::Square, + Piece, PositionDiffEntry, bitboard::Bitboard, castling_rights::CastlingSide, color::Color, fen::Fen, + square::Square, }, }; @@ -659,7 +662,7 @@ mod tests { let position2 = position.make_move_str("e2e4").unwrap().make_move_str("d7d5").unwrap(); assert_eq!( position2.diff(&position), - PositionDiffVec::from_iter([ + Vec::from_iter([ PositionDiffEntry::Clear(Square::E2, Piece::Pawn, Color::White), PositionDiffEntry::Clear(Square::D7, Piece::Pawn, Color::Black), PositionDiffEntry::Set(Square::E4, Piece::Pawn, Color::White), @@ -670,7 +673,7 @@ mod tests { let position3 = position2.make_move_str("e4d5").unwrap(); assert_eq!( position3.diff(&position2), - PositionDiffVec::from_iter([ + Vec::from_iter([ PositionDiffEntry::Clear(Square::E4, Piece::Pawn, Color::White), PositionDiffEntry::Clear(Square::D5, Piece::Pawn, Color::Black), PositionDiffEntry::Set(Square::D5, Piece::Pawn, Color::White), @@ -680,7 +683,7 @@ mod tests { let position4 = position3.make_move_str("g8f6").unwrap(); assert_eq!( position4.diff(&position2), - PositionDiffVec::from_iter([ + Vec::from_iter([ PositionDiffEntry::Clear(Square::E4, Piece::Pawn, Color::White), PositionDiffEntry::Clear(Square::D5, Piece::Pawn, Color::Black), PositionDiffEntry::Set(Square::D5, Piece::Pawn, Color::White), diff --git a/src/search/mod.rs b/src/search/mod.rs index 63aeabff..36f9cae1 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; @@ -17,13 +17,13 @@ use crate::{ nnue::NeuralNetwork, score::{MATE_SCORE, ValueScore}, }, - moves::Move, + moves::{Move, vec::MoveVec}, position::{MoveStage, Position, piece::Piece}, search::{ game_history::GameHistory, heuristics::maybe_zug, killer_table::KillerTable, - picker::MovePicker, + move_evaluator::MoveEvaluator, score_table::ScoreTable, see::static_exchange, status::{SearchStatus, SearchStatusValue}, @@ -71,14 +71,14 @@ impl<'a> Searcher<'a> { } pub fn pv(&self, position: &Position) -> Vec { - let mut moves = Vec::with_capacity(16); + let mut moves = MoveVec::new(); let mut history = self.history.clone(); let mut position = *position; 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); @@ -88,7 +88,7 @@ impl<'a> Searcher<'a> { break; } } - moves + moves.into_iter().collect() } pub fn pv_str(&self, position: &Position) -> Vec { @@ -130,13 +130,16 @@ impl<'a> Searcher<'a> { } } - let picker = MovePicker::new(position, !is_check, None, [None, None]); + let mut moves = position.moves(MoveStage::CapturesAndPromotions); - 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..96d17ae5 --- /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, vec::MoveVec}, + 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..d7fe4870 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, generate::generate_moves, make::make_move, vec::MoveVec}, 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 vec = MoveVec::new(); + generate_moves(position, MoveStage::All, &mut vec); if depth == 1 { - moves.len() + vec.len() } else { - moves - .iter() - .map(|&m| go(&make_move::(position, m), depth - 1, status)) + vec.into_iter() + .map(|m| go(&make_move::(position, m), depth - 1, status)) .sum() } } @@ -27,9 +27,10 @@ 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 move_vec = MoveVec::new(); + generate_moves(position, MoveStage::All, &mut move_vec); let mut count = 0; - for m in moves { + for m in &mut move_vec { 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); From 5585f4abdbaab942d151897fdb9e3c2fb4a8e5e1 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 18 Jul 2026 16:22:24 +0100 Subject: [PATCH 3/5] Fix swaps --- Cargo.lock | 7 +++++++ Cargo.toml | 1 + src/main.rs | 2 +- src/moves/mod.rs | 1 - src/moves/vec.rs | 26 ++++++++++++++------------ src/position/mod.rs | 6 +++--- src/search/mod.rs | 8 ++++---- src/search/move_evaluator.rs | 2 +- src/search/perft.rs | 15 +++++++-------- 9 files changed, 38 insertions(+), 30 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f26dbd99..455107a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,7 @@ dependencies = [ "rstest", "serde", "serde_json", + "smallvec", "tempfile", ] @@ -729,6 +730,12 @@ version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + [[package]] name = "strsim" version = "0.11.1" diff --git a/Cargo.toml b/Cargo.toml index 15f44a31..7c30827e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ primitive_enum = "1.2.0" rand = "0.10.0" serde = { version = "1.0.228", features = ["derive"] } serde_json = "1.0.148" +smallvec = "1.15.1" [dev-dependencies] rstest = "0.26.1" diff --git a/src/main.rs b/src/main.rs index f92018d4..0b8458bd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use crate::{ repl::{Command, DumpCommand, PositionCommand, repl}, time::get_duration, }, - moves::{Move, generate::magics::save_magics}, + moves::generate::magics::save_magics, position::{ MoveStage, Position, color::Color, diff --git a/src/moves/mod.rs b/src/moves/mod.rs index 95f30228..32cad707 100644 --- a/src/moves/mod.rs +++ b/src/moves/mod.rs @@ -105,7 +105,6 @@ impl Display for Move { mod tests { use super::{Move, MoveFlag}; use crate::moves::MoveFlag::*; - use crate::moves::vec::MoveVec; use crate::position::fen::{Fen, KIWIPETE_POSITION, START_POSITION}; use crate::position::piece::Piece::*; use crate::position::square::Square::*; diff --git a/src/moves/vec.rs b/src/moves/vec.rs index 63344aff..e9a34795 100644 --- a/src/moves/vec.rs +++ b/src/moves/vec.rs @@ -1,7 +1,9 @@ use crate::moves::Move; +const MAX_SIZE: usize = 128; + pub struct MoveVec { - data: [(Option, i8); 128], + data: [(Option, i8); MAX_SIZE], raw_size: usize, length: usize, } @@ -9,7 +11,7 @@ pub struct MoveVec { impl Default for MoveVec { fn default() -> Self { Self { - data: [(None, i8::MIN); 128], + data: [(None, i8::MIN); MAX_SIZE], raw_size: 0, length: 0, } @@ -26,15 +28,17 @@ impl<'a> Iterator for MoveVecIterator<'a> { fn next(&mut self) -> Option { let mut best_score = self.move_list.data[self.index].1; + let mut currently_empty = self.move_list.data[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 score = self.move_list.data[curr_idx].1; - if score > best_score { + let (mov, score) = self.move_list.data[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; } @@ -71,18 +75,16 @@ impl MoveVec { self.length = 0; } - fn push_internal(&mut self, mov: Move, score: Option) { - self.data[self.raw_size] = (Some(mov), score.unwrap_or(i8::MIN)); - self.raw_size += 1; - self.length += 1; - } - pub fn push(&mut self, mov: Move) { - self.push_internal(mov, None); + self.push_scored(mov, i8::MIN); } pub fn push_scored(&mut self, mov: Move, score: i8) { - self.push_internal(mov, Some(score)); + if self.raw_size < MAX_SIZE { + self.data[self.raw_size] = (Some(mov), score); + self.raw_size += 1; + self.length += 1; + } } pub fn iter_mut<'a>(&'a mut self) -> MoveVecIterator<'a> { diff --git a/src/position/mod.rs b/src/position/mod.rs index 29b42b52..f4789a0f 100644 --- a/src/position/mod.rs +++ b/src/position/mod.rs @@ -285,9 +285,9 @@ impl Position { } pub fn get_move_str(&self, mov: &str) -> Option { - let mut move_vec = MoveVec::new(); - generate_moves(self, MoveStage::All, &mut move_vec); - move_vec.into_iter().find(|&m| m.to_string().as_str() == mov) + self.moves(MoveStage::All) + .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 36f9cae1..b1eb382f 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -17,7 +17,7 @@ use crate::{ nnue::NeuralNetwork, score::{MATE_SCORE, ValueScore}, }, - moves::{Move, vec::MoveVec}, + moves::Move, position::{MoveStage, Position, piece::Piece}, search::{ game_history::GameHistory, @@ -71,7 +71,7 @@ impl<'a> Searcher<'a> { } pub fn pv(&self, position: &Position) -> Vec { - let mut moves = MoveVec::new(); + let mut moves = Vec::with_capacity(16); let mut history = self.history.clone(); let mut position = *position; @@ -88,7 +88,7 @@ impl<'a> Searcher<'a> { break; } } - moves.into_iter().collect() + moves } pub fn pv_str(&self, position: &Position) -> Vec { @@ -130,7 +130,7 @@ impl<'a> Searcher<'a> { } } - let mut moves = position.moves(MoveStage::CapturesAndPromotions); + let mut moves = position.moves(if !is_check { MoveStage::CapturesAndPromotions } else { MoveStage::All }); if moves.is_empty() { return if is_check { MATE_SCORE + ply as ValueScore } else { window.best() }; diff --git a/src/search/move_evaluator.rs b/src/search/move_evaluator.rs index 96d17ae5..3d4f6440 100644 --- a/src/search/move_evaluator.rs +++ b/src/search/move_evaluator.rs @@ -60,7 +60,7 @@ impl<'a> MoveEvaluator<'a> { #[cfg(test)] mod tests { use crate::{ - moves::{Move, MoveFlag, vec::MoveVec}, + moves::{Move, MoveFlag}, position::{MoveStage, Position, fen::START_POSITION, square::Square}, search::{move_evaluator::MoveEvaluator, see::static_exchange}, }; diff --git a/src/search/perft.rs b/src/search/perft.rs index d7fe4870..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, vec::MoveVec}, + moves::{Move, make::make_move}, position::{MoveStage, Position}, search::{ Depth, @@ -12,12 +12,12 @@ pub fn perft(position: &Position, depth: Depth, status: &SearchStatus) -> (usize if depth == 0 || status.get() == SearchStatusValue::Stopped { return 1; } - let mut vec = MoveVec::new(); - generate_moves(position, MoveStage::All, &mut vec); + let mut moves = position.moves(MoveStage::All); if depth == 1 { - vec.len() + moves.len() } else { - vec.into_iter() + moves + .into_iter() .map(|m| go(&make_move::(position, m), depth - 1, status)) .sum() } @@ -27,10 +27,9 @@ pub fn perft(position: &Position, depth: Depth, status: &SearchStatus) -> (usize return (1, vec![]); } let mut divided = Vec::with_capacity(64); - let mut move_vec = MoveVec::new(); - generate_moves(position, MoveStage::All, &mut move_vec); + let mut moves = position.moves(MoveStage::All); let mut count = 0; - for m in &mut move_vec { + for m in &mut moves { let branch = go(&make_move::(position, m), depth - 1, status); println!("{}: {}", m, branch); divided.push((m, branch)); From fd451e54529a026d4d7fc796be69f566d0c0a2d2 Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 18 Jul 2026 16:53:40 +0100 Subject: [PATCH 4/5] Use MaybeUninit --- src/moves/vec.rs | 54 +++++++++++++++++++++++++++++++----------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/src/moves/vec.rs b/src/moves/vec.rs index e9a34795..01daf265 100644 --- a/src/moves/vec.rs +++ b/src/moves/vec.rs @@ -1,9 +1,11 @@ +use std::mem::MaybeUninit; + use crate::moves::Move; const MAX_SIZE: usize = 128; pub struct MoveVec { - data: [(Option, i8); MAX_SIZE], + data: [MaybeUninit<(Option, i8)>; MAX_SIZE], raw_size: usize, length: usize, } @@ -11,7 +13,7 @@ pub struct MoveVec { impl Default for MoveVec { fn default() -> Self { Self { - data: [(None, i8::MIN); MAX_SIZE], + data: [MaybeUninit::uninit(); MAX_SIZE], raw_size: 0, length: 0, } @@ -27,14 +29,18 @@ impl<'a> Iterator for MoveVecIterator<'a> { type Item = Move; fn next(&mut self) -> Option { - let mut best_score = self.move_list.data[self.index].1; - let mut currently_empty = self.move_list.data[self.index].0.is_none(); + 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.data[curr_idx]; + 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); @@ -44,7 +50,7 @@ impl<'a> Iterator for MoveVecIterator<'a> { } self.index += 1; - self.move_list.data[self.index - 1].0 + self.move_list.entry(self.index - 1).0 } } @@ -75,13 +81,23 @@ impl MoveVec { 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] = (Some(mov), score); + self.data[self.raw_size].write((Some(mov), score)); self.raw_size += 1; self.length += 1; } @@ -99,10 +115,10 @@ impl MoveVec { F: Fn(Move) -> bool, { for i in 0..self.raw_size { - if let Some(mov) = self.data[i].0 + if let Some(mov) = self.entry(i).0 && !f(mov) { - self.data[i] = (None, i8::MIN); + *self.entry_mut(i) = (None, i8::MIN); self.length -= 1; } } @@ -113,15 +129,15 @@ impl MoveVec { F: Fn(Move) -> i8, { for i in 0..self.raw_size { - if let Some(mov) = self.data[i].0 { - self.data[i].1 = f(mov); + 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.data[i].0 + if let Some(m) = self.entry(i).0 && m == mov { return true; @@ -156,15 +172,15 @@ mod tests { 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.data[0].0, Some(Move::new(Square::E2, Square::E4, MoveFlag::DoublePawnPush))); + 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.data[0].0, Some(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush))); + assert_eq!(list.entry(0).0, Some(Move::new(Square::E7, Square::E5, MoveFlag::DoublePawnPush))); - list.data[0] = (None, i8::MIN); + *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); @@ -192,10 +208,10 @@ mod tests { assert_eq!(list.len(), 2); assert_eq!(list.raw_size, 4); - assert_eq!(list.data[0].0, Some(Move::new(Square::B8, Square::C6, MoveFlag::Quiet))); - assert_eq!(list.data[1].0, Some(Move::new(Square::G1, Square::F3, MoveFlag::Quiet))); - assert_eq!(list.data[2].0, None); - assert_eq!(list.data[3].0, None); + 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] From 9365584f68446c5669a72382d91229386dc74ecf Mon Sep 17 00:00:00 2001 From: Bruno Mendes Date: Sat, 18 Jul 2026 16:56:10 +0100 Subject: [PATCH 5/5] Bring back positiondiffvec --- src/position/mod.rs | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/src/position/mod.rs b/src/position/mod.rs index f4789a0f..824d7f93 100644 --- a/src/position/mod.rs +++ b/src/position/mod.rs @@ -17,6 +17,7 @@ use color::Color; use fen::Fen; use hash::ZobristHash; use piece::Piece; +use smallvec::SmallVec; use square::Square; pub mod bitboard; @@ -40,6 +41,8 @@ pub enum PositionDiffEntry { Clear(Square, Piece, Color), } +pub type PositionDiffVec = SmallVec<[PositionDiffEntry; 16]>; + #[derive(Debug, Clone, Copy)] pub struct Position { hash: ZobristHash, @@ -285,9 +288,8 @@ impl Position { } pub fn get_move_str(&self, mov: &str) -> Option { - self.moves(MoveStage::All) - .into_iter() - .find(|&m| m.to_string().as_str() == mov) + let mut moves = self.moves(MoveStage::All); + moves.into_iter().find(|m| m.to_string().as_str() == mov) } pub fn is_check(&self) -> bool { @@ -307,9 +309,8 @@ impl Position { self.material } - pub fn diff(&self, other: &Self) -> Vec { - // TODO: Force single instance. - let mut diff = Vec::with_capacity(16); + pub fn diff(&self, other: &Self) -> PositionDiffVec { + let mut diff = PositionDiffVec::new(); let occ_new_white = self.occupancy_bb(Color::White); let occ_old_white = other.occupancy_bb(Color::White); @@ -365,8 +366,8 @@ mod tests { use crate::{ moves::{Move, MoveFlag}, position::{ - Piece, PositionDiffEntry, bitboard::Bitboard, castling_rights::CastlingSide, color::Color, fen::Fen, - square::Square, + Piece, PositionDiffEntry, PositionDiffVec, bitboard::Bitboard, castling_rights::CastlingSide, color::Color, + fen::Fen, square::Square, }, }; @@ -662,7 +663,7 @@ mod tests { let position2 = position.make_move_str("e2e4").unwrap().make_move_str("d7d5").unwrap(); assert_eq!( position2.diff(&position), - Vec::from_iter([ + PositionDiffVec::from_iter([ PositionDiffEntry::Clear(Square::E2, Piece::Pawn, Color::White), PositionDiffEntry::Clear(Square::D7, Piece::Pawn, Color::Black), PositionDiffEntry::Set(Square::E4, Piece::Pawn, Color::White), @@ -673,7 +674,7 @@ mod tests { let position3 = position2.make_move_str("e4d5").unwrap(); assert_eq!( position3.diff(&position2), - Vec::from_iter([ + PositionDiffVec::from_iter([ PositionDiffEntry::Clear(Square::E4, Piece::Pawn, Color::White), PositionDiffEntry::Clear(Square::D5, Piece::Pawn, Color::Black), PositionDiffEntry::Set(Square::D5, Piece::Pawn, Color::White), @@ -683,7 +684,7 @@ mod tests { let position4 = position3.make_move_str("g8f6").unwrap(); assert_eq!( position4.diff(&position2), - Vec::from_iter([ + PositionDiffVec::from_iter([ PositionDiffEntry::Clear(Square::E4, Piece::Pawn, Color::White), PositionDiffEntry::Clear(Square::D5, Piece::Pawn, Color::Black), PositionDiffEntry::Set(Square::D5, Piece::Pawn, Color::White),