lazy move generation
This commit is contained in:
parent
a93bfe258c
commit
a127815cad
11 changed files with 132 additions and 75 deletions
|
|
@ -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(|| {
|
||||||
|
|
|
||||||
|
|
@ -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.
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
10
src/move.rs
10
src/move.rs
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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
70
src/movegen/picker.rs
Normal 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue