From a127815cad12c3dcb4dd90f123c8a4ff1c79f7f3 Mon Sep 17 00:00:00 2001 From: Moritz Date: Thu, 20 Nov 2025 00:19:35 +0100 Subject: [PATCH] lazy move generation --- benches/eval.rs | 2 - benches/perft.rs | 18 ++++---- progress_tracking/progress.xlsx | Bin 6153 -> 6172 bytes src/bin/suite.rs | 8 ++-- src/main.rs | 2 - src/move.rs | 10 +++++ src/movegen/mod.rs | 16 +------- src/movegen/picker.rs | 70 ++++++++++++++++++++++++++++++++ src/search.rs | 54 +++++++++++++----------- src/zobrist.rs | 16 +++----- tests/perft.rs | 11 ++--- 11 files changed, 132 insertions(+), 75 deletions(-) create mode 100644 src/movegen/picker.rs diff --git a/benches/eval.rs b/benches/eval.rs index c689183..8760931 100644 --- a/benches/eval.rs +++ b/benches/eval.rs @@ -1,10 +1,8 @@ use chess_engine::board::Board; use criterion::{criterion_group, criterion_main, Criterion}; use chess_engine::eval::evaluate_board; -use chess_engine::zobrist::init_zobrist; fn run_eval_benchmark(c: &mut Criterion) { - init_zobrist(); let board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); c.bench_function("standard_board_evaluation", |b| { b.iter(|| { diff --git a/benches/perft.rs b/benches/perft.rs index 673b898..ce09f5c 100644 --- a/benches/perft.rs +++ b/benches/perft.rs @@ -1,19 +1,18 @@ use chess_engine::board::Board; -use chess_engine::movegen::generate_pseudo_legal_moves; +use chess_engine::movegen::picker::MoveGenerator; use chess_engine::movegen::legal_check::is_other_king_attacked; -use chess_engine::r#move::MoveList; use criterion::{criterion_group, criterion_main, Criterion}; -use chess_engine::zobrist::init_zobrist; fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 { if depth == 0 { return 1_u64; } - let mut list = MoveList::new(); - generate_pseudo_legal_moves(&board, &mut list); + + let mut generator = MoveGenerator::new(); let mut leaf_nodes = 0_u64; - for mv in list.iter() { - let undo_info = board.make_move(*mv); + + while let Some(mv) = generator.next(board) { + let undo_info = board.make_move(mv); if !is_other_king_attacked(board) { leaf_nodes += count_legal_moves_recursive(board, depth - 1); } @@ -22,9 +21,8 @@ fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 { leaf_nodes } - fn run_perft_benchmark(c: &mut Criterion) { - init_zobrist(); + // init_zobrist() is no longer needed let mut board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1"); c.bench_function("standard_perft5", |b| { @@ -35,4 +33,4 @@ fn run_perft_benchmark(c: &mut Criterion) { } criterion_group!(benches, run_perft_benchmark); -criterion_main!(benches); +criterion_main!(benches); \ No newline at end of file diff --git a/progress_tracking/progress.xlsx b/progress_tracking/progress.xlsx index 2ad473c2ad78a78061e9347505968752aa42e5b9..1533e567b228daa19254988b39679b7c029584fd 100644 GIT binary patch delta 2185 zcmZ9OdpHyNAICSETM1#yZHSO;?sti~=bBtYGZHaTW4Rl6xVRS)3f# zAI%xdOge^=L?k(@-*e9Mob&zX^L*dW`*V3d-#=d8OpOW+K^J@O17ZLGfDa%9#1sgU z`8dJnof-K>!ITnSfMvp$gk>CijY1OT9VI7f7*))P5Jk((lm5L4WR>tX8SL*X19>ur z;^~>M`m)0hdEI7qJ5A9wQSCpV;~I#H=@b@Rn(vetF4tZ9Cb1!7o<;C9W2tm13FMbZ z8%m@ldFT8fsZpzRczv9cU4S}Hq@0?x;7g1L0MA>03nFusv419)V-bY2!g7ccWhK2ka zyStl?^nzVKOWkQ6x*k0DuPwC*aWO%Mf9@$m274LC0|2mLggst4e0T)iR(PN{D-Gki zmiuCKt4h(;w$hVbm5rcZe^Ck|eXHFn)Y{dlmQ%fGJ z$oN)F1?p3&-tBr<@wb=D`%cj8s(`g0WFIwfx@ zH&gNNoitCQTcf<5rG2xew7lBJ@>n-IV};1;6soAPP^P-cd9Hn_;bDP$0vqKkY3B#t z$GUWu%A3bTt>|6fwvQ0`^bLgqmu&8jP^>M;XP?0xi%nwF`R@#P-4{wuB3NENRrpxl zSq#@&ifL<0MvFgcU=|an1UlWvMzp7)pc&Qvdhr|e%tnt4$ml1%Ps@#q4O+`A(jc^= zN{bgaMGeA()^0F`q~)O1?$?|gfjEV+vh1#}v;{U}adWsVnCz#!l;*<@QJ`Lb4; za3>iSK0T4poW>$rIrt^ehONKGZ6pllb) zc&kSm_REVbTAk4gbcnZpT&XTX+MbS>(LFBxxHGBW^$7gA7_1cxiayRVMPb{!QA1%T z<_fgn@$4)n8yRMvt0&OT%P*kGn(f4Y+f6HzRc%fy@b8K!6k&dmFT3Vq$=f< zy{}i|4!jTM@2B1O$nuFI%pYvwHHE#9Kp9%|^DsVn=YjlUyEtPFrKFhVh##_ULLRNQ zR!>rRbj&TkGeb^hr6N_Tj8JkvSFkSP5B+%BpM~=eKyLz64Qs9l%K^7CI&gG5Phe^$ zDPgrVg(qq(&)YPjBGK?E>f=|kZDLw;z(A8sjF5e9KEOW3gUjLB84J+#b|&uubuG`ZLaO z@?VmJK+^_6U)+uJ$*YXPS031;vs~qnh=#2us*9_{v!qdB!dOJ}*7&5m%OM%a1~=L! zdps&IF-}2o>3Idh;^OdJ#IuYR7wQ|HO5|60!4}0#hoav3Gu8Hf`14($$wg}F!E)}k z$yz<16N*>)=MPO3i_qzhl)-kJZJL8o(-ZzR^I2-H(Y)vVDvL35Kv*H^vt-C>P z@4S2kdB<#Hdot`2KJA=ZurS+lYwzgK*N*wUWmqXqL6X{ZhV2aWitvj}4cWOUBhAnIG)!&GeP>Ce#Pt(5OYUyoLf zQ!s})CQMjCisHNMJD=Tkm(&UQHUb}ne@-z&GjvneLDmOIR9B0xqydAUjPg1)#V+DD zUS70}-RoRb(^j%y9xC#P@^j;f#1{nB>@amB>>4*1UyA_N9;FB?o)}etc5csdU1WnY zqni=GF8gn|APz5ZadY58P!W0mGq#8vi3k1?3Pf4I1gx0-|Al^W<9~2X;05dda+!lo z|I4Oau)+DCxB~7YguA!^fH#K$06`A(|2~O8TaFTgR*%BOqJyH~G2v(zdmxt>;Gd6y zPlwG)7MbG*bJ7p~oyUoZKsZ4bBKfPc2}BAAA$UQczizP*DXtPx!UIuhq5l*kyyO^f O5Eu|8od==@E%iiJsx$nz)CAzoyX8pGgqN|oPHPpcS0Ok zyD*Nr1UXbte+11qZw*eOeaZGdZOsS8LpQyU;ndpuDMN?+bom7=o`3~)4NOQ}=la!S zm@5Mb<+b-8*+7`}euFj2{_MnzjinzU4imUKQ)|qZi!NuC;Cw`)!4Bh_Grh}<@oDrF z2wLe5?wDxnH?*4=*%1Cg&Oy6Fw=y?%`afs;>8X!JrPfsLP%oh3$0~OZlJk|+Qx9S_ zQk9Ln?@;Y%?s~kTk(*`b%H-%GOMPmDDw?KMl&Q^)$klJKe>9FN44&L4^s{x$w*^Vk zJOF?@M&9O?ClLb1&O#G*ljC1p+(^mW+IKYFMBr7r-k3`9`+?5=Zp@csrbFLYtA898 zLdHIG&1Bo*D4@wl3dBPs<6O8v3}w;DFa z10~_v_5Sm&`5)V9mZ{tKV}Xfv*eR+C(&wOOIWG4{*s7>-rxY_|Y}NxW+fz*#;2BOz z!g}{rtvj2c`fATJ`v~49OnE|V_f>!-+nbd%!dP@11)UxjjDBfCeiT z@RX1myQfKodCn%!5G{Fmyp`-09bY-^BYXk=?>*B<4+Oz3p%Px#F7K|(vZTTHXm&o- z*-`ENgD2^8%}>K!L`y<2q7h-G4JC9u&hZ!nZ9FEhGGBdZ@NUe?xAt`vHu~^H)g){m ziE7fkP}G`$BdM=)!JP8NM~iT_bCWeq4Wz2cM;rJ7u4!urY=07b(x2Pl)-_V3_JeWP ziSdFpctfT1))d;n3G-FhZ}(EjAm3>R8N~M?6cS(_I&*8)`QpyX;Jv20 zqO=@R*lN(z>*Mf85e+SJVXILDi>=d5?O5sY;`O8J-)ZA-*vg54!j&KojRk@*o|MK5 zx0L^+ExGPLbz<|1mx~KNg_c=zHAVh!XbFV%=J|~?1as!fdf;sl&T(?$B6X)jZ_U{z z2KpzX?xOw0_Ogg-DZ_k>D`SCa-*N>5tAFvZn#fkreZrt;-%!ws$vxPiQU9JM-=A|E zrK<82Zh7MG276WP>(yo%g5SPui1DC+KpiCUiaD~ z<%i6<D7cuM|?F;yvwz7Ug!4k2thbTiCc+pklZuK6@E0T!c>az&d(|V)=P1T88Wi zG+kC<{0QYq3))F^&Z`Gj-<YBaD-Nw1wQ1#B*nD+6#1x65LJ5)Sl_@Qag3jFo!vk ztmJ*Vkzf{jD^~#PJ{&}{UXnnU>p~{^Y(%uq+MGeiEFR}aX=!Sna+I|F%tn=HHhS6D zwHa9IzWEC$!2jI5`StLuc2!NKH-x|IHtY}o z1B)odPOjPB(k7$C`!Fn6a@yZLGvNXxTik6i>n(1ko!g7F-WmP0W?DhFn|rU5TO`8t__NgDA#m#71J?Yq;DJYIya& zuT567_Y(GeR98^Z6-0w-||Dt8YIIO6jyF6T)N?5VYxIB_Y0>#u%ID5B&k|rYIEK=lN5KG zXuYDvXNuuA_$9m|Ss6AO&e881od2AlWQEd+tfhzqHBr6}3t}G;y}w-#PLH)b8#hil zJS!wPM#|>@!DX^&$e9l)W+`e(MdHG-RwxRkwLfk5UH(p;ldC2RS-w-gEz`4xIKFx5 zdB6kGkRxK3^C>D9x-FG1qhxvtjsK1?T(UMXv`mP~926o`

#+a`$hmP-Ny7X+?22 za#tZ~BGN{d;gOkxRw&8ijJSv_n-*2xZ6BpsVLm_CaA%{F;bPDf75Hm*FI=BjNpBSF86VzKF8MiLbBKZ7N1$oCu9WN8W1 zuT!=XC;ttiCldO io::Result>> { let file = File::open(path)?; @@ -23,15 +22,14 @@ fn load_csv(path: &str) -> io::Result>> { } fn main() { - init_zobrist(); let time_limit_ms = 1000_u64; let time_limit = Duration::from_millis(time_limit_ms); - + let mut total_tests: f32 = 0.0; let mut correct_tests: f32 = 0.0; let sts = load_csv("src/bin/stockfish_testsuite.csv").unwrap(); let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string()); - + for test in &sts { let fen = &test[0]; let bm = &test[1]; @@ -40,7 +38,7 @@ fn main() { let start_time = Instant::now(); - let result = engine.search(time_limit_ms-1); + let result = engine.search(time_limit_ms-5); let duration = start_time.elapsed(); if duration > time_limit { diff --git a/src/main.rs b/src/main.rs index 341e205..bd43db4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,9 +1,7 @@ use chess_engine::engine::Engine; use chess_engine::uci::uci_mainloop; -use chess_engine::zobrist::init_zobrist; fn main() { - init_zobrist(); let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string()); uci_mainloop(&mut engine); } diff --git a/src/move.rs b/src/move.rs index e430bce..13f89f8 100644 --- a/src/move.rs +++ b/src/move.rs @@ -87,6 +87,14 @@ impl MoveList { self.count += 1; } + pub fn pull(&mut self) -> Option { + if self.count > 0 { + self.count -= 1; + return Some(self.moves[self.count]); + } + None + } + pub fn swap(&mut self, a: usize, b: usize) { self.moves[..self.count].swap(a, b); } @@ -106,6 +114,8 @@ impl MoveList { pub fn contains(&self, mv: &Move) -> bool { self.moves.contains(mv) } + + pub fn clear(&mut self) { self.count = 0 } } impl Index for MoveList { diff --git a/src/movegen/mod.rs b/src/movegen/mod.rs index 8c77d95..b1a11a4 100644 --- a/src/movegen/mod.rs +++ b/src/movegen/mod.rs @@ -1,20 +1,6 @@ -use crate::board::Board; -use crate::movegen::non_sliders::{generate_king_moves, generate_knight_moves}; -use crate::movegen::pawns::generate_pawn_moves; -use crate::movegen::sliders::{generate_bishop_moves, generate_queen_moves, generate_rook_moves}; -use crate::r#move::MoveList; - pub mod non_sliders; pub mod sliders; pub mod pawns; pub mod tables; pub mod legal_check; - -pub fn generate_pseudo_legal_moves(board: &Board, list: &mut MoveList) { - generate_pawn_moves(board, list); - generate_knight_moves(board, list); - generate_bishop_moves(board, list); - generate_rook_moves(board, list); - generate_queen_moves(board, list); - generate_king_moves(board, list); -} \ No newline at end of file +pub mod picker; \ No newline at end of file diff --git a/src/movegen/picker.rs b/src/movegen/picker.rs new file mode 100644 index 0000000..834132e --- /dev/null +++ b/src/movegen/picker.rs @@ -0,0 +1,70 @@ +use crate::board::Board; +use crate::movegen::non_sliders::{generate_king_moves, generate_knight_moves}; +use crate::movegen::pawns::generate_pawn_moves; +use crate::movegen::sliders::*; +use crate::r#move::{Move, MoveList}; + +#[derive(Debug, Clone, Copy, PartialEq)] +#[repr(u8)] +enum GenStage { + Pawns = 1, + Knights = 2, + Bishops = 3, + Rooks = 4, + Queens = 5, + King = 6, + Done = 7, +} + +impl GenStage { + pub fn next(&self) -> Option { + match self { + Self::Pawns => Some(Self::Knights), + Self::Knights => Some(Self::Bishops), + Self::Bishops => Some(Self::Rooks), + Self::Rooks => Some(Self::Queens), + Self::Queens => Some(Self::King), + Self::King => Some(Self::Done), + Self::Done => None, + } + } +} + +pub struct MoveGenerator { + buffer: MoveList, + stage: GenStage, +} + +impl MoveGenerator { + pub fn new() -> Self { + Self { + buffer: MoveList::new(), + stage: GenStage::Pawns, + } + } + + fn generate_next_batch(&mut self, board: &Board) { + self.buffer.clear(); + + match self.stage { + GenStage::Pawns => { generate_pawn_moves(board, &mut self.buffer) } + GenStage::Knights => { generate_knight_moves(board, &mut self.buffer) } + GenStage::Bishops => { generate_bishop_moves(board, &mut self.buffer) } + GenStage::Rooks => { generate_rook_moves(board, &mut self.buffer) } + GenStage::Queens => { generate_queen_moves(board, &mut self.buffer) } + GenStage::King => { generate_king_moves(board, &mut self.buffer) } + GenStage::Done => {} + } + if let Some(next_stage) = self.stage.next() { + self.stage = next_stage; + } + } + + pub fn next(&mut self, board: &Board) -> Option { + loop { + if let Some(mv) = self.buffer.pull() { return Some(mv) } + if self.stage == GenStage::Done { return None } + self.generate_next_batch(board); + } + } +} \ No newline at end of file diff --git a/src/search.rs b/src/search.rs index 506b7b5..ff3e06f 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,12 +1,11 @@ use crate::board::{Board, Color}; use crate::eval::evaluate_board; -use crate::movegen::generate_pseudo_legal_moves; use crate::movegen::legal_check::*; -use crate::r#move::{Move, MoveList}; -use crate::tt::{TranspositionTable, NodeType, TTEntry}; // Import TT types +use crate::r#move::Move; +use crate::movegen::picker::MoveGenerator; +use crate::tt::{TranspositionTable, NodeType}; use std::time::{Instant, Duration}; -// A score high enough to be > any material eval, but low enough to not overflow when adding ply const MATE_SCORE: i32 = 1_000_000; fn evaluate_board_relative(board: &Board) -> i32 { @@ -17,9 +16,6 @@ fn evaluate_board_relative(board: &Board) -> i32 { } } -// Helper to adjust mate scores for the TT. -// TT stores "pure" scores, independent of ply. -// Search uses "mated in X" relative to current ply. fn score_to_tt(score: i32, ply: u8) -> i32 { if score > MATE_SCORE - 1000 { score + (ply as i32) @@ -49,14 +45,14 @@ pub fn alpha_beta( start_time: Instant, time_limit: Duration, nodes: &mut u64, - tt: &mut TranspositionTable, // Added TT parameter + tt: &mut TranspositionTable, ) -> (Option, i32) { if (*nodes).is_multiple_of(4096) && start_time.elapsed() > time_limit { return (None, 0); } *nodes += 1; - + let tt_key = board.hash; let mut tt_move: Option = None; @@ -89,25 +85,35 @@ pub fn alpha_beta( return (None, evaluate_board_relative(board)); } - let mut list = MoveList::new(); - generate_pseudo_legal_moves(board, &mut list); - - if let Some(tm) = tt_move { - for i in 0..list.len() { - if list[i] == tm { - list.swap(0, i); - break; - } - } - } - let mut best_move: Option = None; let mut best_score: i32 = -i32::MAX; let mut legal_moves_found = false; let alpha_orig = alpha; - for i in 0..list.len() { - let mv = list[i]; + let mut picker = MoveGenerator::new(); + let mut moves_tried = 0; + + loop { + // Move selection logic: + // 1. Try TT move first (if exists) + // 2. Then use the picker for the rest + let mv = if moves_tried == 0 && tt_move.is_some() { + tt_move.unwrap() + } else { + match picker.next(board) { + Some(m) => { + // Important: skip the TT move if we see it again in the generator + if Some(m) == tt_move { + continue; + } + m + } + None => break, // No more moves + } + }; + + moves_tried += 1; + let undo_mv = board.make_move(mv); let is_illegal = is_other_king_attacked(board); @@ -149,7 +155,7 @@ pub fn alpha_beta( return (None, 0); } } - + let node_type = if best_score <= alpha_orig { NodeType::Alpha } else if best_score >= beta { diff --git a/src/zobrist.rs b/src/zobrist.rs index 7376467..83641d3 100644 --- a/src/zobrist.rs +++ b/src/zobrist.rs @@ -30,11 +30,8 @@ pub struct ZobristKeys { static KEYS: OnceLock = OnceLock::new(); -pub fn init_zobrist() { - if KEYS.get().is_some() { - return; - } - +// Helper function to generate keys, used by the lazy initializer +fn generate_keys() -> ZobristKeys { let mut rng = Xorshift::new(1070372); // Fixed seed for reproducibility let mut pieces = [[0; 64]; 12]; @@ -56,18 +53,17 @@ pub fn init_zobrist() { let side_to_move = rng.next(); - let keys = ZobristKeys { + ZobristKeys { pieces, castling, en_passant, side_to_move, - }; - - KEYS.set(keys).expect("Zobrist keys already initialized"); + } } + pub fn zobrist_keys() -> &'static ZobristKeys { - KEYS.get().expect("Zobrist keys not initialized! Call init_zobrist() in main.") + KEYS.get_or_init(generate_keys) } pub fn piece_index(pt: PieceType, c: Color) -> usize { diff --git a/tests/perft.rs b/tests/perft.rs index 9c05816..f2bb9b5 100644 --- a/tests/perft.rs +++ b/tests/perft.rs @@ -1,20 +1,17 @@ use chess_engine::board::Board; -use chess_engine::movegen::generate_pseudo_legal_moves; use chess_engine::movegen::legal_check::is_other_king_attacked; -use chess_engine::r#move::MoveList; +use chess_engine::movegen::picker::MoveGenerator; fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 { if depth == 0 { return 1_u64; } - let mut list = MoveList::new(); - generate_pseudo_legal_moves(&board, &mut list); - + let mut generator = MoveGenerator::new(); let mut leaf_nodes = 0_u64; - for mv in list.iter() { - let undo_info = board.make_move(*mv); + while let Some(mv) = generator.next(board) { + let undo_info = board.make_move(mv); if !is_other_king_attacked(board) { leaf_nodes += count_legal_moves_recursive(board, depth - 1); }