lazy move generation

This commit is contained in:
Moritz 2025-11-20 00:19:35 +01:00
parent a93bfe258c
commit a127815cad
11 changed files with 132 additions and 75 deletions

View file

@ -1,10 +1,8 @@
use chess_engine::board::Board; use chess_engine::board::Board;
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use chess_engine::eval::evaluate_board; use chess_engine::eval::evaluate_board;
use chess_engine::zobrist::init_zobrist;
fn run_eval_benchmark(c: &mut Criterion) { 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"); let board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
c.bench_function("standard_board_evaluation", |b| { c.bench_function("standard_board_evaluation", |b| {
b.iter(|| { b.iter(|| {

View file

@ -1,19 +1,18 @@
use chess_engine::board::Board; 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::movegen::legal_check::is_other_king_attacked;
use chess_engine::r#move::MoveList;
use criterion::{criterion_group, criterion_main, Criterion}; use criterion::{criterion_group, criterion_main, Criterion};
use chess_engine::zobrist::init_zobrist;
fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 { fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
if depth == 0 { if depth == 0 {
return 1_u64; 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; 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) { if !is_other_king_attacked(board) {
leaf_nodes += count_legal_moves_recursive(board, depth - 1); 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 leaf_nodes
} }
fn run_perft_benchmark(c: &mut Criterion) { 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"); let mut board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
c.bench_function("standard_perft5", |b| { c.bench_function("standard_perft5", |b| {
@ -35,4 +33,4 @@ fn run_perft_benchmark(c: &mut Criterion) {
} }
criterion_group!(benches, run_perft_benchmark); criterion_group!(benches, run_perft_benchmark);
criterion_main!(benches); criterion_main!(benches);

Binary file not shown.

View file

@ -2,7 +2,6 @@ use std::fs::File;
use std::io::{self, BufRead}; use std::io::{self, BufRead};
use chess_engine::engine::Engine; use chess_engine::engine::Engine;
use std::time::{Instant, Duration}; use std::time::{Instant, Duration};
use chess_engine::zobrist::init_zobrist;
fn load_csv(path: &str) -> io::Result<Vec<Vec<String>>> { fn load_csv(path: &str) -> io::Result<Vec<Vec<String>>> {
let file = File::open(path)?; let file = File::open(path)?;
@ -23,15 +22,14 @@ fn load_csv(path: &str) -> io::Result<Vec<Vec<String>>> {
} }
fn main() { fn main() {
init_zobrist();
let time_limit_ms = 1000_u64; let time_limit_ms = 1000_u64;
let time_limit = Duration::from_millis(time_limit_ms); let time_limit = Duration::from_millis(time_limit_ms);
let mut total_tests: f32 = 0.0; let mut total_tests: f32 = 0.0;
let mut correct_tests: f32 = 0.0; let mut correct_tests: f32 = 0.0;
let sts = load_csv("src/bin/stockfish_testsuite.csv").unwrap(); let sts = load_csv("src/bin/stockfish_testsuite.csv").unwrap();
let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string()); let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string());
for test in &sts { for test in &sts {
let fen = &test[0]; let fen = &test[0];
let bm = &test[1]; let bm = &test[1];
@ -40,7 +38,7 @@ fn main() {
let start_time = Instant::now(); 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(); let duration = start_time.elapsed();
if duration > time_limit { if duration > time_limit {

View file

@ -1,9 +1,7 @@
use chess_engine::engine::Engine; use chess_engine::engine::Engine;
use chess_engine::uci::uci_mainloop; use chess_engine::uci::uci_mainloop;
use chess_engine::zobrist::init_zobrist;
fn main() { fn main() {
init_zobrist();
let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string()); let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string());
uci_mainloop(&mut engine); uci_mainloop(&mut engine);
} }

View file

@ -87,6 +87,14 @@ impl MoveList {
self.count += 1; self.count += 1;
} }
pub fn pull(&mut self) -> Option<Move> {
if self.count > 0 {
self.count -= 1;
return Some(self.moves[self.count]);
}
None
}
pub fn swap(&mut self, a: usize, b: usize) { pub fn swap(&mut self, a: usize, b: usize) {
self.moves[..self.count].swap(a, b); self.moves[..self.count].swap(a, b);
} }
@ -106,6 +114,8 @@ impl MoveList {
pub fn contains(&self, mv: &Move) -> bool { pub fn contains(&self, mv: &Move) -> bool {
self.moves.contains(mv) self.moves.contains(mv)
} }
pub fn clear(&mut self) { self.count = 0 }
} }
impl Index<usize> for MoveList { impl Index<usize> for MoveList {

View file

@ -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 non_sliders;
pub mod sliders; pub mod sliders;
pub mod pawns; pub mod pawns;
pub mod tables; pub mod tables;
pub mod legal_check; pub mod legal_check;
pub mod picker;
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);
}

70
src/movegen/picker.rs Normal file
View file

@ -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<Self> {
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<Move> {
loop {
if let Some(mv) = self.buffer.pull() { return Some(mv) }
if self.stage == GenStage::Done { return None }
self.generate_next_batch(board);
}
}
}

View file

@ -1,12 +1,11 @@
use crate::board::{Board, Color}; use crate::board::{Board, Color};
use crate::eval::evaluate_board; use crate::eval::evaluate_board;
use crate::movegen::generate_pseudo_legal_moves;
use crate::movegen::legal_check::*; use crate::movegen::legal_check::*;
use crate::r#move::{Move, MoveList}; use crate::r#move::Move;
use crate::tt::{TranspositionTable, NodeType, TTEntry}; // Import TT types use crate::movegen::picker::MoveGenerator;
use crate::tt::{TranspositionTable, NodeType};
use std::time::{Instant, Duration}; 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; const MATE_SCORE: i32 = 1_000_000;
fn evaluate_board_relative(board: &Board) -> i32 { 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 { fn score_to_tt(score: i32, ply: u8) -> i32 {
if score > MATE_SCORE - 1000 { if score > MATE_SCORE - 1000 {
score + (ply as i32) score + (ply as i32)
@ -49,14 +45,14 @@ pub fn alpha_beta(
start_time: Instant, start_time: Instant,
time_limit: Duration, time_limit: Duration,
nodes: &mut u64, nodes: &mut u64,
tt: &mut TranspositionTable, // Added TT parameter tt: &mut TranspositionTable,
) -> (Option<Move>, i32) { ) -> (Option<Move>, i32) {
if (*nodes).is_multiple_of(4096) if (*nodes).is_multiple_of(4096)
&& start_time.elapsed() > time_limit { && start_time.elapsed() > time_limit {
return (None, 0); return (None, 0);
} }
*nodes += 1; *nodes += 1;
let tt_key = board.hash; let tt_key = board.hash;
let mut tt_move: Option<Move> = None; let mut tt_move: Option<Move> = None;
@ -89,25 +85,35 @@ pub fn alpha_beta(
return (None, evaluate_board_relative(board)); 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<Move> = None; let mut best_move: Option<Move> = None;
let mut best_score: i32 = -i32::MAX; let mut best_score: i32 = -i32::MAX;
let mut legal_moves_found = false; let mut legal_moves_found = false;
let alpha_orig = alpha; let alpha_orig = alpha;
for i in 0..list.len() { let mut picker = MoveGenerator::new();
let mv = list[i]; 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 undo_mv = board.make_move(mv);
let is_illegal = is_other_king_attacked(board); let is_illegal = is_other_king_attacked(board);
@ -149,7 +155,7 @@ pub fn alpha_beta(
return (None, 0); return (None, 0);
} }
} }
let node_type = if best_score <= alpha_orig { let node_type = if best_score <= alpha_orig {
NodeType::Alpha NodeType::Alpha
} else if best_score >= beta { } else if best_score >= beta {

View file

@ -30,11 +30,8 @@ pub struct ZobristKeys {
static KEYS: OnceLock<ZobristKeys> = OnceLock::new(); static KEYS: OnceLock<ZobristKeys> = OnceLock::new();
pub fn init_zobrist() { // Helper function to generate keys, used by the lazy initializer
if KEYS.get().is_some() { fn generate_keys() -> ZobristKeys {
return;
}
let mut rng = Xorshift::new(1070372); // Fixed seed for reproducibility let mut rng = Xorshift::new(1070372); // Fixed seed for reproducibility
let mut pieces = [[0; 64]; 12]; let mut pieces = [[0; 64]; 12];
@ -56,18 +53,17 @@ pub fn init_zobrist() {
let side_to_move = rng.next(); let side_to_move = rng.next();
let keys = ZobristKeys { ZobristKeys {
pieces, pieces,
castling, castling,
en_passant, en_passant,
side_to_move, side_to_move,
}; }
KEYS.set(keys).expect("Zobrist keys already initialized");
} }
pub fn zobrist_keys() -> &'static ZobristKeys { 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 { pub fn piece_index(pt: PieceType, c: Color) -> usize {

View file

@ -1,20 +1,17 @@
use chess_engine::board::Board; 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::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 { fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
if depth == 0 { if depth == 0 {
return 1_u64; return 1_u64;
} }
let mut list = MoveList::new(); let mut generator = MoveGenerator::new();
generate_pseudo_legal_moves(&board, &mut list);
let mut leaf_nodes = 0_u64; 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) { if !is_other_king_attacked(board) {
leaf_nodes += count_legal_moves_recursive(board, depth - 1); leaf_nodes += count_legal_moves_recursive(board, depth - 1);
} }