restructure
This commit is contained in:
parent
2e5f6e3d7d
commit
a93bfe258c
23 changed files with 83 additions and 307 deletions
|
|
@ -1,6 +1,6 @@
|
||||||
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::basic::evaluate_board;
|
use chess_engine::eval::evaluate_board;
|
||||||
use chess_engine::zobrist::init_zobrist;
|
use chess_engine::zobrist::init_zobrist;
|
||||||
|
|
||||||
fn run_eval_benchmark(c: &mut Criterion) {
|
fn run_eval_benchmark(c: &mut Criterion) {
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -3,7 +3,6 @@ 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;
|
use chess_engine::zobrist::init_zobrist;
|
||||||
// EACH TEST CAN ONLY TAKE ONE SECOND MAX TO KEEP RESULTS COMPARABLE
|
|
||||||
|
|
||||||
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)?;
|
||||||
|
|
@ -25,30 +24,25 @@ fn load_csv(path: &str) -> io::Result<Vec<Vec<String>>> {
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
init_zobrist();
|
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 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());
|
||||||
|
|
||||||
// Set the time limit to 1 second
|
|
||||||
let time_limit = Duration::from_millis(1000);
|
|
||||||
|
|
||||||
|
|
||||||
for test in &sts {
|
for test in &sts {
|
||||||
let fen = &test[0];
|
let fen = &test[0];
|
||||||
let bm = &test[1];
|
let bm = &test[1];
|
||||||
|
|
||||||
engine.setpos_fen(fen);
|
engine.setpos_fen(fen);
|
||||||
|
|
||||||
// Record start time
|
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
|
|
||||||
let result = engine.search(990_u64);
|
let result = engine.search(time_limit_ms-1);
|
||||||
|
|
||||||
// Calculate duration
|
|
||||||
let duration = start_time.elapsed();
|
let duration = start_time.elapsed();
|
||||||
|
|
||||||
// Check if the test exceeded the time limit
|
|
||||||
if duration > time_limit {
|
if duration > time_limit {
|
||||||
panic!(
|
panic!(
|
||||||
"Test exceeded 1 second limit: {:?} for FEN: {}",
|
"Test exceeded 1 second limit: {:?} for FEN: {}",
|
||||||
|
|
|
||||||
38
src/board.rs
38
src/board.rs
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::r#move::*;
|
use crate::r#move::*;
|
||||||
use crate::square::Square;
|
use crate::square::Square;
|
||||||
use crate::zobrist::{self, zobrist_keys}; // Import Zobrist
|
use crate::zobrist::{self, zobrist_keys};
|
||||||
use std::ops::Not;
|
use std::ops::Not;
|
||||||
|
|
||||||
pub const CASTLING_WK_FLAG: u8 = 1;
|
pub const CASTLING_WK_FLAG: u8 = 1;
|
||||||
|
|
@ -78,12 +78,10 @@ pub struct Board {
|
||||||
pub halfmove_clock: u8,
|
pub halfmove_clock: u8,
|
||||||
pub fullmove_number: u16,
|
pub fullmove_number: u16,
|
||||||
|
|
||||||
// Added Zobrist Hash
|
|
||||||
pub hash: u64,
|
pub hash: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
// Helper to get the EP file index (0-7) or 8 if None
|
|
||||||
fn ep_file_index(ep: Option<Square>) -> usize {
|
fn ep_file_index(ep: Option<Square>) -> usize {
|
||||||
match ep {
|
match ep {
|
||||||
Some(sq) => (sq as usize) % 8,
|
Some(sq) => (sq as usize) % 8,
|
||||||
|
|
@ -91,12 +89,10 @@ impl Board {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Should be called after loading FEN or creating board
|
|
||||||
pub fn recalculate_hash(&mut self) {
|
pub fn recalculate_hash(&mut self) {
|
||||||
let keys = zobrist_keys();
|
let keys = zobrist_keys();
|
||||||
let mut hash = 0;
|
let mut hash = 0;
|
||||||
|
|
||||||
// 1. Pieces
|
|
||||||
for sq in 0..64 {
|
for sq in 0..64 {
|
||||||
if let Some(pt) = self.pieces_on_squares[sq] {
|
if let Some(pt) = self.pieces_on_squares[sq] {
|
||||||
let color = if (self.pieces[pt as usize][Color::White as usize] & (1 << sq)) != 0 {
|
let color = if (self.pieces[pt as usize][Color::White as usize] & (1 << sq)) != 0 {
|
||||||
|
|
@ -107,23 +103,16 @@ impl Board {
|
||||||
hash ^= keys.pieces[zobrist::piece_index(pt, color)][sq];
|
hash ^= keys.pieces[zobrist::piece_index(pt, color)][sq];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Castling
|
|
||||||
hash ^= keys.castling[self.castling_rights as usize];
|
hash ^= keys.castling[self.castling_rights as usize];
|
||||||
|
|
||||||
// 3. En Passant
|
|
||||||
hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
||||||
|
|
||||||
// 4. Side to move
|
|
||||||
if self.side_to_move == Color::Black {
|
if self.side_to_move == Color::Black {
|
||||||
hash ^= keys.side_to_move;
|
hash ^= keys.side_to_move;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.hash = hash;
|
self.hash = hash;
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- Original methods with added hashing ---
|
|
||||||
|
|
||||||
fn rm_piece(
|
fn rm_piece(
|
||||||
&mut self,
|
&mut self,
|
||||||
target_square: Square,
|
target_square: Square,
|
||||||
|
|
@ -132,7 +121,6 @@ impl Board {
|
||||||
let target_square_bitboard = target_square.to_bitboard();
|
let target_square_bitboard = target_square.to_bitboard();
|
||||||
let piece_type = self.pieces_on_squares[target_square as usize].unwrap();
|
let piece_type = self.pieces_on_squares[target_square as usize].unwrap();
|
||||||
|
|
||||||
// UPDATE HASH: Remove piece
|
|
||||||
let keys = zobrist_keys();
|
let keys = zobrist_keys();
|
||||||
self.hash ^= keys.pieces[zobrist::piece_index(piece_type, color)][target_square as usize];
|
self.hash ^= keys.pieces[zobrist::piece_index(piece_type, color)][target_square as usize];
|
||||||
|
|
||||||
|
|
@ -149,7 +137,6 @@ impl Board {
|
||||||
fn put_piece(&mut self, target_square: Square, color: Color, piece_type: PieceType) {
|
fn put_piece(&mut self, target_square: Square, color: Color, piece_type: PieceType) {
|
||||||
let target_square_bitboard = target_square.to_bitboard();
|
let target_square_bitboard = target_square.to_bitboard();
|
||||||
|
|
||||||
// UPDATE HASH: Add piece
|
|
||||||
let keys = zobrist_keys();
|
let keys = zobrist_keys();
|
||||||
self.hash ^= keys.pieces[zobrist::piece_index(piece_type, color)][target_square as usize];
|
self.hash ^= keys.pieces[zobrist::piece_index(piece_type, color)][target_square as usize];
|
||||||
|
|
||||||
|
|
@ -169,12 +156,9 @@ impl Board {
|
||||||
pub fn make_move(&mut self, mv: Move) -> UndoMove {
|
pub fn make_move(&mut self, mv: Move) -> UndoMove {
|
||||||
let keys = zobrist_keys();
|
let keys = zobrist_keys();
|
||||||
|
|
||||||
// HASH UPDATE: Remove old state (EP and Castling)
|
|
||||||
// XORing removes the old value from the hash
|
|
||||||
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
||||||
self.hash ^= keys.castling[self.castling_rights as usize];
|
self.hash ^= keys.castling[self.castling_rights as usize];
|
||||||
|
|
||||||
// 1. Extract parts from move
|
|
||||||
let from = mv.get_from();
|
let from = mv.get_from();
|
||||||
let to = mv.get_to();
|
let to = mv.get_to();
|
||||||
let flags = mv.get_flags();
|
let flags = mv.get_flags();
|
||||||
|
|
@ -189,7 +173,6 @@ impl Board {
|
||||||
let mut opt_captured_piece: Option<PieceType> = None;
|
let mut opt_captured_piece: Option<PieceType> = None;
|
||||||
let mut opt_en_passant_target: Option<Square> = None;
|
let mut opt_en_passant_target: Option<Square> = None;
|
||||||
|
|
||||||
// 4. Make the actual moves (rm_piece/put_piece update piece hashes automatically)
|
|
||||||
match flags {
|
match flags {
|
||||||
MOVE_FLAG_QUIET => {
|
MOVE_FLAG_QUIET => {
|
||||||
self.move_piece(from, to, self.side_to_move);
|
self.move_piece(from, to, self.side_to_move);
|
||||||
|
|
@ -262,7 +245,6 @@ impl Board {
|
||||||
_ => { panic!("unable to make_move: invalid flags: {}", flags); }
|
_ => { panic!("unable to make_move: invalid flags: {}", flags); }
|
||||||
}
|
}
|
||||||
|
|
||||||
// 5. Update castling rights
|
|
||||||
let wk = self.pieces[PieceType::King as usize][Color::White as usize];
|
let wk = self.pieces[PieceType::King as usize][Color::White as usize];
|
||||||
let wr = self.pieces[PieceType::Rook as usize][Color::White as usize];
|
let wr = self.pieces[PieceType::Rook as usize][Color::White as usize];
|
||||||
let bk = self.pieces[PieceType::King as usize][Color::Black as usize];
|
let bk = self.pieces[PieceType::King as usize][Color::Black as usize];
|
||||||
|
|
@ -275,16 +257,12 @@ impl Board {
|
||||||
castling_right_wk | castling_right_wq | castling_right_bk | castling_right_bq;
|
castling_right_wk | castling_right_wq | castling_right_bk | castling_right_bq;
|
||||||
self.castling_rights = self.castling_rights & new_castling_rights;
|
self.castling_rights = self.castling_rights & new_castling_rights;
|
||||||
|
|
||||||
// 6. Update en passant target
|
|
||||||
self.en_passant_target = opt_en_passant_target;
|
self.en_passant_target = opt_en_passant_target;
|
||||||
|
|
||||||
// HASH UPDATE: Add new state
|
|
||||||
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
||||||
self.hash ^= keys.castling[self.castling_rights as usize];
|
self.hash ^= keys.castling[self.castling_rights as usize];
|
||||||
// HASH UPDATE: Side to move (always changes)
|
|
||||||
self.hash ^= keys.side_to_move;
|
self.hash ^= keys.side_to_move;
|
||||||
|
|
||||||
// 7. Update halfmove clock
|
|
||||||
let new_friendly_pawns = self.pieces[PieceType::Pawn as usize][self.side_to_move as usize];
|
let new_friendly_pawns = self.pieces[PieceType::Pawn as usize][self.side_to_move as usize];
|
||||||
let new_total_pieces = self.all_occupied.count_ones();
|
let new_total_pieces = self.all_occupied.count_ones();
|
||||||
let pawns_changed = old_friendly_pawns ^ new_friendly_pawns;
|
let pawns_changed = old_friendly_pawns ^ new_friendly_pawns;
|
||||||
|
|
@ -292,13 +270,10 @@ impl Board {
|
||||||
let increase_halfmove_clock = ((pawns_changed + piece_captured) == 0) as u8;
|
let increase_halfmove_clock = ((pawns_changed + piece_captured) == 0) as u8;
|
||||||
self.halfmove_clock = increase_halfmove_clock * (self.halfmove_clock + 1);
|
self.halfmove_clock = increase_halfmove_clock * (self.halfmove_clock + 1);
|
||||||
|
|
||||||
// 8. Increase fullmove
|
|
||||||
self.fullmove_number += self.side_to_move as u16;
|
self.fullmove_number += self.side_to_move as u16;
|
||||||
|
|
||||||
// 9. Flip side
|
|
||||||
self.side_to_move = !self.side_to_move;
|
self.side_to_move = !self.side_to_move;
|
||||||
|
|
||||||
// 10. Return Undo
|
|
||||||
UndoMove::new(
|
UndoMove::new(
|
||||||
mv,
|
mv,
|
||||||
opt_captured_piece,
|
opt_captured_piece,
|
||||||
|
|
@ -311,35 +286,26 @@ impl Board {
|
||||||
pub fn undo_move(&mut self, undo_info: UndoMove) {
|
pub fn undo_move(&mut self, undo_info: UndoMove) {
|
||||||
let keys = zobrist_keys();
|
let keys = zobrist_keys();
|
||||||
|
|
||||||
// HASH UPDATE: We must remove the CURRENT state hash before overwriting state variables.
|
|
||||||
// 1. Remove current side hash (effectively flipping it back)
|
|
||||||
self.hash ^= keys.side_to_move;
|
self.hash ^= keys.side_to_move;
|
||||||
// 2. Remove current castling and EP hash
|
|
||||||
self.hash ^= keys.castling[self.castling_rights as usize];
|
self.hash ^= keys.castling[self.castling_rights as usize];
|
||||||
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
||||||
|
|
||||||
// 1. Restore simple state
|
|
||||||
self.castling_rights = undo_info.old_castling_rights;
|
self.castling_rights = undo_info.old_castling_rights;
|
||||||
self.en_passant_target = undo_info.old_en_passant_square;
|
self.en_passant_target = undo_info.old_en_passant_square;
|
||||||
self.halfmove_clock = undo_info.old_halfmove_clock;
|
self.halfmove_clock = undo_info.old_halfmove_clock;
|
||||||
|
|
||||||
// HASH UPDATE: Restore OLD state hash
|
|
||||||
self.hash ^= keys.castling[self.castling_rights as usize];
|
self.hash ^= keys.castling[self.castling_rights as usize];
|
||||||
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
||||||
|
|
||||||
// 2. Flip side *before* piece ops
|
|
||||||
self.side_to_move = !self.side_to_move;
|
self.side_to_move = !self.side_to_move;
|
||||||
|
|
||||||
// 3. Decrement fullmove
|
|
||||||
self.fullmove_number -= self.side_to_move as u16;
|
self.fullmove_number -= self.side_to_move as u16;
|
||||||
|
|
||||||
// 4. Extract move data
|
|
||||||
let mv = undo_info.mv;
|
let mv = undo_info.mv;
|
||||||
let from = mv.get_from();
|
let from = mv.get_from();
|
||||||
let to = mv.get_to();
|
let to = mv.get_to();
|
||||||
let flags = mv.get_flags();
|
let flags = mv.get_flags();
|
||||||
|
|
||||||
// 5. Reverse pieces (helpers will update hash automatically)
|
|
||||||
match flags {
|
match flags {
|
||||||
MOVE_FLAG_QUIET => {
|
MOVE_FLAG_QUIET => {
|
||||||
self.move_piece(to, from, self.side_to_move);
|
self.move_piece(to, from, self.side_to_move);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::board::Board;
|
use crate::board::Board;
|
||||||
use crate::r#move::Move;
|
use crate::r#move::Move;
|
||||||
use crate::search::alpha_beta::alpha_beta;
|
use crate::search::alpha_beta;
|
||||||
use crate::tt::TranspositionTable; // Import TT
|
use crate::tt::TranspositionTable; // Import TT
|
||||||
use std::time::{Instant, Duration};
|
use std::time::{Instant, Duration};
|
||||||
|
|
||||||
|
|
@ -42,14 +42,8 @@ impl Engine {
|
||||||
let start_time = Instant::now();
|
let start_time = Instant::now();
|
||||||
let time_limit = Duration::from_millis(time_limit_ms);
|
let time_limit = Duration::from_millis(time_limit_ms);
|
||||||
|
|
||||||
// We usually clear the TT or age it before a new search,
|
|
||||||
// but for now we keep it to learn from previous moves.
|
|
||||||
// self.tt.clear();
|
|
||||||
|
|
||||||
let mut nodes = 0;
|
let mut nodes = 0;
|
||||||
|
|
||||||
// Initial search at depth 1
|
|
||||||
// Note: We pass &mut self.tt to alpha_beta
|
|
||||||
let (mut opt_move, mut _score) = alpha_beta(
|
let (mut opt_move, mut _score) = alpha_beta(
|
||||||
&mut self.board,
|
&mut self.board,
|
||||||
1,
|
1,
|
||||||
|
|
@ -64,7 +58,6 @@ impl Engine {
|
||||||
|
|
||||||
let mut depth = 2;
|
let mut depth = 2;
|
||||||
|
|
||||||
// Iterative Deepening
|
|
||||||
while start_time.elapsed() < time_limit {
|
while start_time.elapsed() < time_limit {
|
||||||
let (new_move, new_score) = alpha_beta(
|
let (new_move, new_score) = alpha_beta(
|
||||||
&mut self.board,
|
&mut self.board,
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::board::*;
|
use crate::board::*;
|
||||||
use crate::eval::piece_square_tables::PSQT;
|
use crate::psqt::PSQT;
|
||||||
|
|
||||||
pub fn evaluate_board(board: &Board) -> i32 {
|
pub fn evaluate_board(board: &Board) -> i32 {
|
||||||
let mut mg_score = 0_i32;
|
let mut mg_score = 0_i32;
|
||||||
|
|
@ -7,28 +7,21 @@ pub fn evaluate_board(board: &Board) -> i32 {
|
||||||
let mut phase = 0_i32;
|
let mut phase = 0_i32;
|
||||||
|
|
||||||
// We use a macro to force loop unrolling.
|
// We use a macro to force loop unrolling.
|
||||||
// This enables the compiler to use constant offsets for PSQT access
|
|
||||||
// instead of calculating addresses at runtime based on a loop variable.
|
|
||||||
macro_rules! score_piece {
|
macro_rules! score_piece {
|
||||||
($pt:expr, $phase_weight:expr) => {
|
($pt:expr, $phase_weight:expr) => {
|
||||||
// --- WHITE ---
|
|
||||||
let mut pieces = board.pieces[$pt][Color::White as usize];
|
let mut pieces = board.pieces[$pt][Color::White as usize];
|
||||||
if pieces > 0 {
|
if pieces > 0 {
|
||||||
// Phase calculation uses count_ones (POPPCNT) which is very fast
|
|
||||||
phase += (pieces.count_ones() as i32) * $phase_weight;
|
phase += (pieces.count_ones() as i32) * $phase_weight;
|
||||||
|
|
||||||
while pieces > 0 {
|
while pieces > 0 {
|
||||||
let sq = pieces.trailing_zeros() as usize;
|
let sq = pieces.trailing_zeros() as usize;
|
||||||
pieces &= pieces - 1; // Clear LS1B
|
pieces &= pieces - 1; // Clear LS1B
|
||||||
|
|
||||||
// Material is already baked into PSQT, so we just add the table value
|
|
||||||
// Since $pt is a const literal here, this compiles to a direct memory access
|
|
||||||
mg_score += PSQT[$pt][Color::White as usize][0][sq];
|
mg_score += PSQT[$pt][Color::White as usize][0][sq];
|
||||||
eg_score += PSQT[$pt][Color::White as usize][1][sq];
|
eg_score += PSQT[$pt][Color::White as usize][1][sq];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// --- BLACK ---
|
|
||||||
let mut pieces = board.pieces[$pt][Color::Black as usize];
|
let mut pieces = board.pieces[$pt][Color::Black as usize];
|
||||||
if pieces > 0 {
|
if pieces > 0 {
|
||||||
phase += (pieces.count_ones() as i32) * $phase_weight;
|
phase += (pieces.count_ones() as i32) * $phase_weight;
|
||||||
|
|
@ -44,21 +37,13 @@ pub fn evaluate_board(board: &Board) -> i32 {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly unrolled execution order
|
|
||||||
// Pawn (0), Weight 0
|
|
||||||
score_piece!(0, 0);
|
score_piece!(0, 0);
|
||||||
// Knight (1), Weight 1
|
|
||||||
score_piece!(1, 1);
|
score_piece!(1, 1);
|
||||||
// Bishop (2), Weight 1
|
|
||||||
score_piece!(2, 1);
|
score_piece!(2, 1);
|
||||||
// Rook (3), Weight 2
|
|
||||||
score_piece!(3, 2);
|
score_piece!(3, 2);
|
||||||
// Queen (4), Weight 4
|
|
||||||
score_piece!(4, 4);
|
score_piece!(4, 4);
|
||||||
// King (5), Weight 0 (Phase doesn't change)
|
|
||||||
score_piece!(5, 0);
|
score_piece!(5, 0);
|
||||||
|
|
||||||
// Tapered Evaluation
|
|
||||||
let phase = phase.min(24);
|
let phase = phase.min(24);
|
||||||
let mg_phase = phase;
|
let mg_phase = phase;
|
||||||
let eg_phase = 24 - phase;
|
let eg_phase = 24 - phase;
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod basic;
|
|
||||||
pub mod piece_square_tables;
|
|
||||||
|
|
@ -5,8 +5,9 @@ pub mod movegen;
|
||||||
pub mod eval;
|
pub mod eval;
|
||||||
pub mod display;
|
pub mod display;
|
||||||
pub mod parsing;
|
pub mod parsing;
|
||||||
pub mod search;
|
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod uci;
|
pub mod uci;
|
||||||
pub mod tt;
|
pub mod tt;
|
||||||
pub mod zobrist;
|
pub mod zobrist;
|
||||||
|
pub mod search;
|
||||||
|
pub mod psqt;
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,6 @@ impl MoveList {
|
||||||
self.count += 1;
|
self.count += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Added swap method as requested
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,12 @@ pub fn is_current_king_attacked(board: &Board) -> bool {
|
||||||
is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move)
|
is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Checks if the king of the side that is NOT to move is in check
|
|
||||||
pub fn is_other_king_attacked(board: &Board) -> bool {
|
pub fn is_other_king_attacked(board: &Board) -> bool {
|
||||||
let king = board.pieces[PieceType::King as usize][!board.side_to_move as usize];
|
let king = board.pieces[PieceType::King as usize][!board.side_to_move as usize];
|
||||||
is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move)
|
is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO check if castle is legal (squares in between)
|
|
||||||
|
|
||||||
/// calculate if a square on the board is attacked by a color
|
|
||||||
pub fn is_square_attacked(board: &Board, square: Square, color: Color) -> bool {
|
pub fn is_square_attacked(board: &Board, square: Square, color: Color) -> bool {
|
||||||
// 1. Non sliding
|
// 1. Non sliding
|
||||||
// 1.1 Pawn
|
// 1.1 Pawn
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,18 @@
|
||||||
// FILENAME: non_sliders.rs
|
|
||||||
use crate::board::*;
|
|
||||||
use crate::movegen::legal_check::is_square_attacked;
|
|
||||||
use crate::r#move::*;
|
|
||||||
use crate::square::*;
|
|
||||||
use super::tables::{KING_ATTACKS, KNIGHT_ATTACKS};
|
use super::tables::{KING_ATTACKS, KNIGHT_ATTACKS};
|
||||||
|
use crate::board::*;
|
||||||
|
use crate::r#move::*;
|
||||||
|
use crate::movegen::legal_check::is_square_attacked;
|
||||||
|
use crate::square::*;
|
||||||
|
|
||||||
pub fn generate_knight_moves(board: &Board, list: &mut MoveList) {
|
pub fn generate_knight_moves(board: &Board, list: &mut MoveList) {
|
||||||
let enemy_occupied = board.occupied[!board.side_to_move as usize];
|
let enemy_occupied = board.occupied[!board.side_to_move as usize];
|
||||||
let mut friendly_knights = board.pieces[PieceType::Knight as usize][board.side_to_move as usize];
|
let mut friendly_knights =
|
||||||
|
board.pieces[PieceType::Knight as usize][board.side_to_move as usize];
|
||||||
|
|
||||||
while friendly_knights != 0 {
|
while friendly_knights != 0 {
|
||||||
let square = SQUARES[friendly_knights.trailing_zeros() as usize];
|
let square = SQUARES[friendly_knights.trailing_zeros() as usize];
|
||||||
let mut attacks = KNIGHT_ATTACKS[square as usize] & !board.occupied[board.side_to_move as usize];
|
let mut attacks =
|
||||||
|
KNIGHT_ATTACKS[square as usize] & !board.occupied[board.side_to_move as usize];
|
||||||
|
|
||||||
while attacks != 0 {
|
while attacks != 0 {
|
||||||
let attack = SQUARES[attacks.trailing_zeros() as usize];
|
let attack = SQUARES[attacks.trailing_zeros() as usize];
|
||||||
|
|
@ -58,7 +59,6 @@ pub fn generate_king_moves(board: &Board, list: &mut MoveList) {
|
||||||
attacks &= attacks - 1;
|
attacks &= attacks - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO Optimize the is attacked testing on castling
|
|
||||||
// 2. Generate castling king moves
|
// 2. Generate castling king moves
|
||||||
if board.side_to_move == Color::White {
|
if board.side_to_move == Color::White {
|
||||||
// King must not be in check to castle
|
// King must not be in check to castle
|
||||||
|
|
@ -67,46 +67,39 @@ pub fn generate_king_moves(board: &Board, list: &mut MoveList) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kingside (OO)
|
// Kingside (OO)
|
||||||
if (board.castling_rights & CASTLING_WK_FLAG) != 0 {
|
if (board.castling_rights & CASTLING_WK_FLAG) != 0
|
||||||
if (board.all_occupied & CASTLING_WK_MASK) == 0 {
|
&& (board.all_occupied & CASTLING_WK_MASK) == 0
|
||||||
// Check F1 (path). G1 (landing) is checked by perft function.
|
&& !is_square_attacked(board, Square::F1, Color::Black) {
|
||||||
if !is_square_attacked(board, Square::F1, Color::Black) {
|
|
||||||
list.push(Move::new(Square::E1, Square::G1, MOVE_FLAG_WK_CASTLE));
|
list.push(Move::new(Square::E1, Square::G1, MOVE_FLAG_WK_CASTLE));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
// Queenside (OOO)
|
// Queenside (OOO)
|
||||||
if (board.castling_rights & CASTLING_WQ_FLAG) != 0 {
|
if (board.castling_rights & CASTLING_WQ_FLAG) != 0
|
||||||
if (board.all_occupied & CASTLING_WQ_MASK) == 0 {
|
&& (board.all_occupied & CASTLING_WQ_MASK) == 0
|
||||||
// Check D1 (path). C1 (landing) is checked by perft function. B1 is irrelevant.
|
&& !is_square_attacked(board, Square::D1, Color::Black)
|
||||||
if !is_square_attacked(board, Square::D1, Color::Black) {
|
{
|
||||||
list.push(Move::new(Square::E1, Square::C1, MOVE_FLAG_WQ_CASTLE));
|
list.push(Move::new(Square::E1, Square::C1, MOVE_FLAG_WQ_CASTLE));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else { // Black
|
} else {
|
||||||
|
// Black
|
||||||
// King must not be in check to castle
|
// King must not be in check to castle
|
||||||
if is_square_attacked(board, Square::E8, Color::White) {
|
if is_square_attacked(board, Square::E8, Color::White) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Kingside (OO)
|
// Kingside (OO)
|
||||||
if (board.castling_rights & CASTLING_BK_FLAG) != 0 {
|
if (board.castling_rights & CASTLING_BK_FLAG) != 0
|
||||||
if (board.all_occupied & CASTLING_BK_MASK) == 0 {
|
&& (board.all_occupied & CASTLING_BK_MASK) == 0
|
||||||
// Check F8 (path). G8 (landing) is checked by perft function.
|
&& !is_square_attacked(board, Square::F8, Color::White)
|
||||||
if !is_square_attacked(board, Square::F8, Color::White) {
|
{
|
||||||
list.push(Move::new(Square::E8, Square::G8, MOVE_FLAG_BK_CASTLE));
|
list.push(Move::new(Square::E8, Square::G8, MOVE_FLAG_BK_CASTLE));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
// Queenside (OOO)
|
// Queenside (OOO)
|
||||||
if (board.castling_rights & CASTLING_BQ_FLAG) != 0 {
|
if (board.castling_rights & CASTLING_BQ_FLAG) != 0
|
||||||
if (board.all_occupied & CASTLING_BQ_MASK) == 0 {
|
&& (board.all_occupied & CASTLING_BQ_MASK) == 0
|
||||||
// Check D8 (path). C8 (landing) is checked by perft function. B8 is irrelevant.
|
&& !is_square_attacked(board, Square::D8, Color::White)
|
||||||
if !is_square_attacked(board, Square::D8, Color::White) {
|
{
|
||||||
list.push(Move::new(Square::E8, Square::C8, MOVE_FLAG_BQ_CASTLE));
|
list.push(Move::new(Square::E8, Square::C8, MOVE_FLAG_BQ_CASTLE));
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -93,7 +93,6 @@ pub fn generate_pawn_moves(board: &Board, list: &mut MoveList) {
|
||||||
|
|
||||||
// 1.4.2 Capturing Promotion
|
// 1.4.2 Capturing Promotion
|
||||||
// 1.4.2.1 A-side capturing promotion
|
// 1.4.2.1 A-side capturing promotion
|
||||||
// CORRECTED: Use PAWN_A_SIDE_CAPTURE_PROMOTION_MASK_WITHE (excludes A-file)
|
|
||||||
let mut promotion_targets_a_side_capture = ((friendly_pawns & PAWN_A_SIDE_CAPTURE_PROMOTION_MASK_WITHE) << 7) & board.occupied[1];
|
let mut promotion_targets_a_side_capture = ((friendly_pawns & PAWN_A_SIDE_CAPTURE_PROMOTION_MASK_WITHE) << 7) & board.occupied[1];
|
||||||
while promotion_targets_a_side_capture > 0 {
|
while promotion_targets_a_side_capture > 0 {
|
||||||
let to = SQUARES[promotion_targets_a_side_capture.trailing_zeros() as usize];
|
let to = SQUARES[promotion_targets_a_side_capture.trailing_zeros() as usize];
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::board::*;
|
use crate::board::*;
|
||||||
use crate::r#move::{Move, MoveList, MOVE_FLAG_CAPTURE, MOVE_FLAG_QUIET};
|
use crate::r#move::*;
|
||||||
use crate::square::SQUARES;
|
use crate::square::SQUARES;
|
||||||
use super::tables::*;
|
use super::tables::*;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -563,15 +563,11 @@ pub const RELEVANT_BITS_BISHOP: [u8; 64] = [
|
||||||
static ROOK_ATTACKS: OnceLock<Box<[[u64; 4096]; 64]>> = OnceLock::new();
|
static ROOK_ATTACKS: OnceLock<Box<[[u64; 4096]; 64]>> = OnceLock::new();
|
||||||
static BISHOP_ATTACKS: OnceLock<Box<[[u64; 512]; 64]>> = OnceLock::new();
|
static BISHOP_ATTACKS: OnceLock<Box<[[u64; 512]; 64]>> = OnceLock::new();
|
||||||
|
|
||||||
|
|
||||||
// HILFSFUNKTION: Berechnet Turmzüge "langsam"
|
|
||||||
// Diese Funktion wird nur beim "Backen" der Tabellen verwendet.
|
|
||||||
fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
let mut attacks = 0_u64;
|
let mut attacks = 0_u64;
|
||||||
let rank = square / 8;
|
let rank = square / 8;
|
||||||
let file = square % 8;
|
let file = square % 8;
|
||||||
|
|
||||||
// 1. Nach Norden (rank +)
|
|
||||||
for r in (rank + 1)..=7 {
|
for r in (rank + 1)..=7 {
|
||||||
let target_sq = r * 8 + file;
|
let target_sq = r * 8 + file;
|
||||||
attacks |= 1_u64 << target_sq;
|
attacks |= 1_u64 << target_sq;
|
||||||
|
|
@ -580,7 +576,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Nach Süden (rank -)
|
|
||||||
for r in (0..rank).rev() {
|
for r in (0..rank).rev() {
|
||||||
let target_sq = r * 8 + file;
|
let target_sq = r * 8 + file;
|
||||||
attacks |= 1_u64 << target_sq;
|
attacks |= 1_u64 << target_sq;
|
||||||
|
|
@ -589,7 +584,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Nach Osten (file +)
|
|
||||||
for f in (file + 1)..=7 {
|
for f in (file + 1)..=7 {
|
||||||
let target_sq = rank * 8 + f;
|
let target_sq = rank * 8 + f;
|
||||||
attacks |= 1_u64 << target_sq;
|
attacks |= 1_u64 << target_sq;
|
||||||
|
|
@ -598,7 +592,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Nach Westen (file -)
|
|
||||||
for f in (0..file).rev() {
|
for f in (0..file).rev() {
|
||||||
let target_sq = rank * 8 + f;
|
let target_sq = rank * 8 + f;
|
||||||
attacks |= 1_u64 << target_sq;
|
attacks |= 1_u64 << target_sq;
|
||||||
|
|
@ -611,7 +604,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_final_rook_attacks() -> Box<[[u64; 4096]; 64]> {
|
fn generate_final_rook_attacks() -> Box<[[u64; 4096]; 64]> {
|
||||||
// Heap-Allokation (Dein Code war hier korrekt)
|
|
||||||
let mut v: Vec<[u64; 4096]> = Vec::with_capacity(64);
|
let mut v: Vec<[u64; 4096]> = Vec::with_capacity(64);
|
||||||
for _ in 0..64 {
|
for _ in 0..64 {
|
||||||
v.push([0_u64; 4096]);
|
v.push([0_u64; 4096]);
|
||||||
|
|
@ -620,32 +612,18 @@ fn generate_final_rook_attacks() -> Box<[[u64; 4096]; 64]> {
|
||||||
panic!("Vec to Box conversion failed.");
|
panic!("Vec to Box conversion failed.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Haupt-Back-Schleife
|
|
||||||
for square_index in 0_usize..=63_usize {
|
for square_index in 0_usize..=63_usize {
|
||||||
let premask = PREMASKS_ROOK[square_index]; // [deine Quelle]
|
let premask = PREMASKS_ROOK[square_index];
|
||||||
let magic = MAGICS_ROOK[square_index]; // [deine Quelle]
|
let magic = MAGICS_ROOK[square_index];
|
||||||
// ACHTUNG: Hier war ein Fehler in deinem Code, du hattest BISHOP statt ROOK
|
let relevant_bits = RELEVANT_BITS_ROOK[square_index];
|
||||||
let relevant_bits = RELEVANT_BITS_ROOK[square_index]; // [deine Quelle]
|
|
||||||
let shift = 64 - relevant_bits;
|
let shift = 64 - relevant_bits;
|
||||||
|
|
||||||
// Schleife durch alle 2^n Blocker-Kombinationen
|
|
||||||
let mut blocker_combination = 0_u64;
|
let mut blocker_combination = 0_u64;
|
||||||
loop {
|
loop {
|
||||||
// ---- HIER IST DIE KORREKTE LOGIK ----
|
|
||||||
|
|
||||||
// 1. Berechne die "echten" Züge für diese Blocker-Kombination (der langsame Weg)
|
|
||||||
let attack_squares = calculate_rook_attacks_slowly(square_index, blocker_combination);
|
let attack_squares = calculate_rook_attacks_slowly(square_index, blocker_combination);
|
||||||
|
|
||||||
// 2. Berechne den "magischen" Speicherort
|
|
||||||
let magic_index = (blocker_combination.wrapping_mul(magic)) >> shift;
|
let magic_index = (blocker_combination.wrapping_mul(magic)) >> shift;
|
||||||
let magic_index_as_usize = magic_index as usize;
|
let magic_index_as_usize = magic_index as usize;
|
||||||
|
|
||||||
// 3. Speichere die echten Züge an diesem magischen Ort
|
|
||||||
final_rook_attacks[square_index][magic_index_as_usize] = attack_squares;
|
final_rook_attacks[square_index][magic_index_as_usize] = attack_squares;
|
||||||
|
|
||||||
// ---- ENDE DER LOGIK ----
|
|
||||||
|
|
||||||
// Gehe zur nächsten Blocker-Kombination (Dein Code war hier korrekt)
|
|
||||||
if blocker_combination == premask {
|
if blocker_combination == premask {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -657,39 +635,31 @@ fn generate_final_rook_attacks() -> Box<[[u64; 4096]; 64]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_rook_attacks() -> &'static Box<[[u64; 4096]; 64]> {
|
pub fn get_rook_attacks() -> &'static Box<[[u64; 4096]; 64]> {
|
||||||
// get_or_init stellt sicher, dass generate_final_rook_attacks()
|
|
||||||
// nur beim allerersten Aufruf ausgeführt wird.
|
|
||||||
// Alle anderen Aufrufe geben sofort die fertige Tabelle zurück.
|
|
||||||
ROOK_ATTACKS.get_or_init(generate_final_rook_attacks)
|
ROOK_ATTACKS.get_or_init(generate_final_rook_attacks)
|
||||||
}
|
}
|
||||||
|
|
||||||
// HILFSFUNKTION: Berechnet Läuferzüge "langsam"
|
|
||||||
// Diese Funktion wird nur beim "Backen" der Tabellen verwendet.
|
|
||||||
fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
let mut attacks = 0_u64;
|
let mut attacks = 0_u64;
|
||||||
let rank = square / 8;
|
let rank = square / 8;
|
||||||
let file = square % 8;
|
let file = square % 8;
|
||||||
|
|
||||||
// Temporäre Rank/File-Iteratoren
|
|
||||||
let (mut r, mut f);
|
let (mut r, mut f);
|
||||||
|
|
||||||
// 1. Nach Nord-Osten (rank+, file+)
|
|
||||||
r = rank + 1;
|
r = rank + 1;
|
||||||
f = file + 1;
|
f = file + 1;
|
||||||
while r <= 7 && f <= 7 {
|
while r <= 7 && f <= 7 {
|
||||||
let target_sq = r * 8 + f;
|
let target_sq = r * 8 + f;
|
||||||
attacks |= 1_u64 << target_sq;
|
attacks |= 1_u64 << target_sq;
|
||||||
if (blockers >> target_sq) & 1 == 1 {
|
if (blockers >> target_sq) & 1 == 1 {
|
||||||
break; // Wir treffen einen Blocker, stopp
|
break;
|
||||||
}
|
}
|
||||||
r += 1;
|
r += 1;
|
||||||
f += 1;
|
f += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Nach Süd-Osten (rank-, file+)
|
r = rank;
|
||||||
r = rank; // Start bei rank, da 0..rank fehlschlägt wenn rank = 0
|
|
||||||
f = file + 1;
|
f = file + 1;
|
||||||
while r > 0 && f <= 7 { // r > 0 (da r = rank-1 in der 1. Iteration)
|
while r > 0 && f <= 7 {
|
||||||
r -= 1;
|
r -= 1;
|
||||||
let target_sq = r * 8 + f;
|
let target_sq = r * 8 + f;
|
||||||
attacks |= 1_u64 << target_sq;
|
attacks |= 1_u64 << target_sq;
|
||||||
|
|
@ -699,7 +669,6 @@ fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
f += 1;
|
f += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3. Nach Süd-Westen (rank-, file-)
|
|
||||||
r = rank;
|
r = rank;
|
||||||
f = file;
|
f = file;
|
||||||
while r > 0 && f > 0 { // r > 0 und f > 0
|
while r > 0 && f > 0 { // r > 0 und f > 0
|
||||||
|
|
@ -712,7 +681,6 @@ fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4. Nach Nord-Westen (rank+, file-)
|
|
||||||
r = rank + 1;
|
r = rank + 1;
|
||||||
f = file;
|
f = file;
|
||||||
while r <= 7 && f > 0 { // f > 0
|
while r <= 7 && f > 0 { // f > 0
|
||||||
|
|
@ -729,9 +697,6 @@ fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_final_bishop_attacks() -> Box<[[u64; 512]; 64]> {
|
fn generate_final_bishop_attacks() -> Box<[[u64; 512]; 64]> {
|
||||||
// Heap-Allokation
|
|
||||||
// (Array ist kleiner: 512 statt 4096. Stack Overflow wäre hier unwahrscheinlich,
|
|
||||||
// aber wir bleiben konsistent mit der Turm-Logik.)
|
|
||||||
let mut v: Vec<[u64; 512]> = Vec::with_capacity(64);
|
let mut v: Vec<[u64; 512]> = Vec::with_capacity(64);
|
||||||
for _ in 0..64 {
|
for _ in 0..64 {
|
||||||
v.push([0_u64; 512]);
|
v.push([0_u64; 512]);
|
||||||
|
|
@ -740,30 +705,21 @@ fn generate_final_bishop_attacks() -> Box<[[u64; 512]; 64]> {
|
||||||
panic!("Vec to Box conversion failed for bishops.");
|
panic!("Vec to Box conversion failed for bishops.");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Haupt-Back-Schleife
|
|
||||||
for square_index in 0_usize..=63_usize {
|
for square_index in 0_usize..=63_usize {
|
||||||
// Verwende die BISHOP-Konstanten aus deiner tables.rs
|
|
||||||
let premask = PREMASKS_BISHOP[square_index];
|
let premask = PREMASKS_BISHOP[square_index];
|
||||||
let magic = MAGICS_BISHOP[square_index];
|
let magic = MAGICS_BISHOP[square_index];
|
||||||
let relevant_bits = RELEVANT_BITS_BISHOP[square_index];
|
let relevant_bits = RELEVANT_BITS_BISHOP[square_index];
|
||||||
let shift = 64 - relevant_bits;
|
let shift = 64 - relevant_bits;
|
||||||
|
|
||||||
// Schleife durch alle 2^n Blocker-Kombinationen
|
|
||||||
let mut blocker_combination = 0_u64;
|
let mut blocker_combination = 0_u64;
|
||||||
loop {
|
loop {
|
||||||
// 1. Berechne die "echten" Züge für diese Blocker-Kombination (der langsame Weg)
|
|
||||||
let attack_squares = calculate_bishop_attacks_slowly(square_index, blocker_combination);
|
let attack_squares = calculate_bishop_attacks_slowly(square_index, blocker_combination);
|
||||||
|
|
||||||
// 2. Berechne den "magischen" Speicherort
|
|
||||||
let magic_index = (blocker_combination.wrapping_mul(magic)) >> shift;
|
let magic_index = (blocker_combination.wrapping_mul(magic)) >> shift;
|
||||||
let magic_index_as_usize = magic_index as usize;
|
let magic_index_as_usize = magic_index as usize;
|
||||||
|
|
||||||
// 3. Speichere die echten Züge an diesem magischen Ort
|
|
||||||
// (Stelle sicher, dass magic_index_as_usize < 512 ist,
|
|
||||||
// was durch korrekte Magics garantiert wird)
|
|
||||||
final_bishop_attacks[square_index][magic_index_as_usize] = attack_squares;
|
final_bishop_attacks[square_index][magic_index_as_usize] = attack_squares;
|
||||||
|
|
||||||
// Gehe zur nächsten Blocker-Kombination
|
|
||||||
if blocker_combination == premask {
|
if blocker_combination == premask {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -775,8 +731,5 @@ fn generate_final_bishop_attacks() -> Box<[[u64; 512]; 64]> {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_bishop_attacks() -> &'static Box<[[u64; 512]; 64]> {
|
pub fn get_bishop_attacks() -> &'static Box<[[u64; 512]; 64]> {
|
||||||
// get_or_init stellt sicher, dass generate_final_bishop_attacks()
|
|
||||||
// nur beim allerersten Aufruf ausgeführt wird.
|
|
||||||
// Alle anderen Aufrufe geben sofort die fertige Tabelle zurück.
|
|
||||||
BISHOP_ATTACKS.get_or_init(generate_final_bishop_attacks)
|
BISHOP_ATTACKS.get_or_init(generate_final_bishop_attacks)
|
||||||
}
|
}
|
||||||
|
|
@ -4,12 +4,9 @@ use crate::r#move::*;
|
||||||
use crate::square::Square;
|
use crate::square::Square;
|
||||||
|
|
||||||
impl Board {
|
impl Board {
|
||||||
/// Creates a new Board instance from a FEN string.
|
|
||||||
/// Assumes the FEN string is valid.
|
|
||||||
pub fn from_fen(fen: &str) -> Self {
|
pub fn from_fen(fen: &str) -> Self {
|
||||||
let mut parts = fen.split_whitespace();
|
let mut parts = fen.split_whitespace();
|
||||||
|
|
||||||
// Initialisiere die Arrays
|
|
||||||
let mut pieces = [[0u64; 2]; 6];
|
let mut pieces = [[0u64; 2]; 6];
|
||||||
let mut occupied = [0u64; 2];
|
let mut occupied = [0u64; 2];
|
||||||
let mut pieces_on_squares = [None; 64];
|
let mut pieces_on_squares = [None; 64];
|
||||||
|
|
@ -20,7 +17,7 @@ impl Board {
|
||||||
let mut file = 0;
|
let mut file = 0;
|
||||||
|
|
||||||
for c in placement.chars() {
|
for c in placement.chars() {
|
||||||
if c.is_digit(10) {
|
if c.is_ascii_digit() {
|
||||||
file += c.to_digit(10).unwrap_or(0) as usize;
|
file += c.to_digit(10).unwrap_or(0) as usize;
|
||||||
} else if c == '/' {
|
} else if c == '/' {
|
||||||
rank -= 1;
|
rank -= 1;
|
||||||
|
|
@ -122,8 +119,8 @@ impl Board {
|
||||||
"-" => None,
|
"-" => None,
|
||||||
sq_str => {
|
sq_str => {
|
||||||
let chars: Vec<char> = sq_str.chars().collect();
|
let chars: Vec<char> = sq_str.chars().collect();
|
||||||
let file = (chars[0] as u8 - b'a') as u8;
|
let file = chars[0] as u8 - b'a';
|
||||||
let rank = (chars[1] as u8 - b'1') as u8;
|
let rank = chars[1] as u8 - b'1';
|
||||||
let sq_index = rank * 8 + file;
|
let sq_index = rank * 8 + file;
|
||||||
Some(unsafe { mem::transmute::<u8, Square>(sq_index) })
|
Some(unsafe { mem::transmute::<u8, Square>(sq_index) })
|
||||||
}
|
}
|
||||||
|
|
@ -223,8 +220,8 @@ impl Board {
|
||||||
fen.push(' ');
|
fen.push(' ');
|
||||||
if let Some(sq) = self.en_passant_target {
|
if let Some(sq) = self.en_passant_target {
|
||||||
let sq_index = sq as u8;
|
let sq_index = sq as u8;
|
||||||
let file = (sq_index % 8) as u8;
|
let file = sq_index % 8;
|
||||||
let rank = (sq_index / 8) as u8;
|
let rank = sq_index / 8;
|
||||||
fen.push((b'a' + file) as char);
|
fen.push((b'a' + file) as char);
|
||||||
fen.push((b'1' + rank) as char);
|
fen.push((b'1' + rank) as char);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -254,8 +251,8 @@ impl Move {
|
||||||
/// Converts algebraic notation (e.g., "a1") to a Square.
|
/// Converts algebraic notation (e.g., "a1") to a Square.
|
||||||
/// Assumes valid input.
|
/// Assumes valid input.
|
||||||
fn alg_to_square(alg: &str) -> Square {
|
fn alg_to_square(alg: &str) -> Square {
|
||||||
let file = (alg.as_bytes()[0] - b'a') as u8;
|
let file = alg.as_bytes()[0] - b'a';
|
||||||
let rank = (alg.as_bytes()[1] - b'1') as u8;
|
let rank = alg.as_bytes()[1] - b'1';
|
||||||
let sq_index = rank * 8 + file;
|
let sq_index = rank * 8 + file;
|
||||||
// This is unsafe, but we assume valid algebraic notation
|
// This is unsafe, but we assume valid algebraic notation
|
||||||
unsafe { mem::transmute::<u8, Square>(sq_index) }
|
unsafe { mem::transmute::<u8, Square>(sq_index) }
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,3 @@
|
||||||
// --- Material Values (PeSTO Standard) ---
|
|
||||||
const MG_PAWN_VAL: i32 = 82;
|
const MG_PAWN_VAL: i32 = 82;
|
||||||
const EG_PAWN_VAL: i32 = 94;
|
const EG_PAWN_VAL: i32 = 94;
|
||||||
const MG_KNIGHT_VAL: i32 = 337;
|
const MG_KNIGHT_VAL: i32 = 337;
|
||||||
|
|
@ -9,7 +8,7 @@ const MG_ROOK_VAL: i32 = 477;
|
||||||
const EG_ROOK_VAL: i32 = 512;
|
const EG_ROOK_VAL: i32 = 512;
|
||||||
const MG_QUEEN_VAL: i32 = 1025;
|
const MG_QUEEN_VAL: i32 = 1025;
|
||||||
const EG_QUEEN_VAL: i32 = 936;
|
const EG_QUEEN_VAL: i32 = 936;
|
||||||
const MG_KING_VAL: i32 = 0; // King usually has no material value in eval summation (captured = game over)
|
const MG_KING_VAL: i32 = 0;
|
||||||
const EG_KING_VAL: i32 = 0;
|
const EG_KING_VAL: i32 = 0;
|
||||||
|
|
||||||
pub const MG_PAWN_TABLE: [i32; 64] = [
|
pub const MG_PAWN_TABLE: [i32; 64] = [
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::board::{Board, Color};
|
use crate::board::{Board, Color};
|
||||||
use crate::eval::basic::evaluate_board;
|
use crate::eval::evaluate_board;
|
||||||
use crate::movegen::generate_pseudo_legal_moves;
|
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, MoveList};
|
||||||
|
|
@ -51,28 +51,20 @@ pub fn alpha_beta(
|
||||||
nodes: &mut u64,
|
nodes: &mut u64,
|
||||||
tt: &mut TranspositionTable, // Added TT parameter
|
tt: &mut TranspositionTable, // Added TT parameter
|
||||||
) -> (Option<Move>, i32) {
|
) -> (Option<Move>, i32) {
|
||||||
// Check for time usage
|
if (*nodes).is_multiple_of(4096)
|
||||||
if *nodes % 4096 == 0 {
|
&& start_time.elapsed() > time_limit {
|
||||||
if start_time.elapsed() > time_limit {
|
return (None, 0);
|
||||||
return (None, 0);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
*nodes += 1;
|
*nodes += 1;
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// 1. TT PROBE
|
|
||||||
// -----------------------
|
|
||||||
// We assume board.hash holds the current Zobrist key (u64)
|
|
||||||
let tt_key = board.hash;
|
let tt_key = board.hash;
|
||||||
let mut tt_move: Option<Move> = None;
|
let mut tt_move: Option<Move> = None;
|
||||||
|
|
||||||
if let Some(entry) = tt.probe(tt_key) {
|
if let Some(entry) = tt.probe(tt_key) {
|
||||||
// We remember the move from TT to sort it first later
|
if entry.bm.0 != 0 {
|
||||||
if entry.bm.0 != 0 { // Check if move is valid (not 0)
|
|
||||||
tt_move = Some(entry.bm);
|
tt_move = Some(entry.bm);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Can we use the score for a cutoff?
|
|
||||||
if entry.depth >= depth {
|
if entry.depth >= depth {
|
||||||
let tt_score = score_from_tt(entry.score, ply);
|
let tt_score = score_from_tt(entry.score, ply);
|
||||||
|
|
||||||
|
|
@ -99,13 +91,8 @@ pub fn alpha_beta(
|
||||||
|
|
||||||
let mut list = MoveList::new();
|
let mut list = MoveList::new();
|
||||||
generate_pseudo_legal_moves(board, &mut list);
|
generate_pseudo_legal_moves(board, &mut list);
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// MOVE ORDERING (TT Move First)
|
|
||||||
// -----------------------
|
|
||||||
// If we have a move from TT, we want to search it first!
|
|
||||||
if let Some(tm) = tt_move {
|
if let Some(tm) = tt_move {
|
||||||
// Find the move in the list and swap it to the front (index 0)
|
|
||||||
for i in 0..list.len() {
|
for i in 0..list.len() {
|
||||||
if list[i] == tm {
|
if list[i] == tm {
|
||||||
list.swap(0, i);
|
list.swap(0, i);
|
||||||
|
|
@ -117,13 +104,12 @@ pub fn alpha_beta(
|
||||||
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; // Save original alpha to determine NodeType later
|
let alpha_orig = alpha;
|
||||||
|
|
||||||
for i in 0..list.len() {
|
for i in 0..list.len() {
|
||||||
let mv = list[i];
|
let mv = list[i];
|
||||||
let undo_mv = board.make_move(mv);
|
let undo_mv = board.make_move(mv);
|
||||||
|
|
||||||
// Optimization: Check legality locally if possible, but for now rely on King check
|
|
||||||
let is_illegal = is_other_king_attacked(board);
|
let is_illegal = is_other_king_attacked(board);
|
||||||
if is_illegal {
|
if is_illegal {
|
||||||
board.undo_move(undo_mv);
|
board.undo_move(undo_mv);
|
||||||
|
|
@ -133,7 +119,7 @@ pub fn alpha_beta(
|
||||||
|
|
||||||
let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes, tt);
|
let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes, tt);
|
||||||
|
|
||||||
if *nodes % 4096 == 0 && start_time.elapsed() > time_limit {
|
if (*nodes).is_multiple_of(4096) && start_time.elapsed() > time_limit {
|
||||||
board.undo_move(undo_mv);
|
board.undo_move(undo_mv);
|
||||||
return (None, 0);
|
return (None, 0);
|
||||||
}
|
}
|
||||||
|
|
@ -152,7 +138,7 @@ pub fn alpha_beta(
|
||||||
}
|
}
|
||||||
|
|
||||||
if alpha >= beta {
|
if alpha >= beta {
|
||||||
break; // Beta cutoff
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -163,19 +149,16 @@ pub fn alpha_beta(
|
||||||
return (None, 0);
|
return (None, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// -----------------------
|
|
||||||
// 2. TT STORE
|
|
||||||
// -----------------------
|
|
||||||
let node_type = if best_score <= alpha_orig {
|
let node_type = if best_score <= alpha_orig {
|
||||||
NodeType::Alpha // We didn't improve alpha (Fail Low) -> Upper Bound
|
NodeType::Alpha
|
||||||
} else if best_score >= beta {
|
} else if best_score >= beta {
|
||||||
NodeType::Beta // We caused a cutoff (Fail High) -> Lower Bound
|
NodeType::Beta
|
||||||
} else {
|
} else {
|
||||||
NodeType::Exact // We found a score between alpha and beta
|
NodeType::Exact
|
||||||
};
|
};
|
||||||
|
|
||||||
let save_move = best_move.unwrap_or(Move(0)); // Use dummy 0 if no best move
|
let save_move = best_move.unwrap_or(Move(0));
|
||||||
let save_score = score_to_tt(best_score, ply);
|
let save_score = score_to_tt(best_score, ply);
|
||||||
|
|
||||||
tt.store(tt_key, save_score, depth, node_type, save_move);
|
tt.store(tt_key, save_score, depth, node_type, save_move);
|
||||||
|
|
@ -1,63 +0,0 @@
|
||||||
use crate::board::{Board, Color}; // <-- Assuming you have a Color enum (e.g., Color::White, Color::Black)
|
|
||||||
use crate::eval::basic::evaluate_board;
|
|
||||||
use crate::movegen::generate_pseudo_legal_moves;
|
|
||||||
use crate::movegen::legal_check::is_other_king_attacked;
|
|
||||||
use crate::r#move::{Move, MoveList};
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
let static_eval = evaluate_board(board);
|
|
||||||
match board.side_to_move {
|
|
||||||
Color::White => static_eval,
|
|
||||||
Color::Black => -static_eval,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn minimax(board: &mut Board, depth: u8, ply: u8) -> (Option<Move>, i32) {
|
|
||||||
if depth == 0 {
|
|
||||||
return (None, evaluate_board_relative(board));
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut list = MoveList::new();
|
|
||||||
generate_pseudo_legal_moves(board, &mut list);
|
|
||||||
let mut best_move: Option<Move> = None;
|
|
||||||
let mut best_score: i32 = -i32::MAX; // Start with the worst possible score
|
|
||||||
let mut legal_moves_found = false;
|
|
||||||
|
|
||||||
for mv in list.iter() {
|
|
||||||
let undo_mv = board.make_move(*mv);
|
|
||||||
let is_illegal = is_other_king_attacked(board);
|
|
||||||
if is_illegal {
|
|
||||||
board.undo_move(undo_mv);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
legal_moves_found = true;
|
|
||||||
|
|
||||||
// Recursive call, incrementing ply
|
|
||||||
let (_, score) = minimax(board, depth - 1, ply + 1);
|
|
||||||
let current_score = -score;
|
|
||||||
|
|
||||||
if current_score > best_score {
|
|
||||||
best_score = current_score;
|
|
||||||
best_move = Some(*mv);
|
|
||||||
}
|
|
||||||
|
|
||||||
board.undo_move(undo_mv);
|
|
||||||
}
|
|
||||||
|
|
||||||
if !legal_moves_found {
|
|
||||||
if is_other_king_attacked(board) {
|
|
||||||
// Checkmate
|
|
||||||
// The score is *less* negative the *longer* it takes to be mated (higher ply)
|
|
||||||
// This translates to a *higher* score for the winner for a *faster* mate
|
|
||||||
return (None, -MATE_SCORE + (ply as i32));
|
|
||||||
} else {
|
|
||||||
// Stalemate
|
|
||||||
return (None, 0);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
(best_move, best_score)
|
|
||||||
}
|
|
||||||
|
|
@ -1,2 +0,0 @@
|
||||||
pub mod minimax;
|
|
||||||
pub mod alpha_beta;
|
|
||||||
|
|
@ -1,6 +1,4 @@
|
||||||
use std::mem::size_of;
|
use std::mem::size_of;
|
||||||
// I assume you have a move.rs file.
|
|
||||||
// If you call the file "move.rs", you must import it as r#move because "move" is a keyword.
|
|
||||||
use crate::r#move::Move;
|
use crate::r#move::Move;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,8 @@
|
||||||
// uci.rs
|
|
||||||
|
|
||||||
use std::io::{self, BufRead};
|
use std::io::{self, BufRead};
|
||||||
use crate::engine::Engine;
|
use crate::engine::Engine;
|
||||||
|
|
||||||
pub fn uci_mainloop(engine: &mut Engine) {
|
pub fn uci_mainloop(engine: &mut Engine) {
|
||||||
loop {
|
loop {
|
||||||
// Start the main UCI loop
|
|
||||||
for line in io::stdin().lock().lines() {
|
for line in io::stdin().lock().lines() {
|
||||||
let input = line.unwrap_or_else(|_| "quit".to_string());
|
let input = line.unwrap_or_else(|_| "quit".to_string());
|
||||||
let tokens: Vec<&str> = input.split_whitespace().collect();
|
let tokens: Vec<&str> = input.split_whitespace().collect();
|
||||||
|
|
@ -41,7 +38,6 @@ pub fn uci_mainloop(engine: &mut Engine) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"go" => {
|
"go" => {
|
||||||
// TODO add a lot functionality
|
|
||||||
println!("bestmove {}", engine.search(1000_u64));
|
println!("bestmove {}", engine.search(1000_u64));
|
||||||
}
|
}
|
||||||
"stop" => {
|
"stop" => {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,6 @@
|
||||||
use crate::board::{Color, PieceType};
|
use crate::board::{Color, PieceType};
|
||||||
use crate::square::Square;
|
|
||||||
use std::sync::OnceLock;
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
// We use a simple Xorshift generator to avoid external dependencies like 'rand'
|
|
||||||
struct Xorshift {
|
struct Xorshift {
|
||||||
state: u64,
|
state: u64,
|
||||||
}
|
}
|
||||||
|
|
@ -24,17 +22,15 @@ impl Xorshift {
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct ZobristKeys {
|
pub struct ZobristKeys {
|
||||||
pub pieces: [[u64; 64]; 12], // [PieceType 0-5 + Color offset][Square]
|
pub pieces: [[u64; 64]; 12],
|
||||||
pub castling: [u64; 16], // 16 combinations of castling rights
|
pub castling: [u64; 16],
|
||||||
pub en_passant: [u64; 9], // 8 files + 1 for "no ep"
|
pub en_passant: [u64; 9],
|
||||||
pub side_to_move: u64,
|
pub side_to_move: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Thread-safe, write-once global storage
|
|
||||||
static KEYS: OnceLock<ZobristKeys> = OnceLock::new();
|
static KEYS: OnceLock<ZobristKeys> = OnceLock::new();
|
||||||
|
|
||||||
pub fn init_zobrist() {
|
pub fn init_zobrist() {
|
||||||
// If already initialized, do nothing
|
|
||||||
if KEYS.get().is_some() {
|
if KEYS.get().is_some() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
@ -42,20 +38,20 @@ pub fn init_zobrist() {
|
||||||
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];
|
||||||
for i in 0..12 {
|
for piece_squares in pieces.iter_mut() {
|
||||||
for j in 0..64 {
|
for square_key in piece_squares.iter_mut() {
|
||||||
pieces[i][j] = rng.next();
|
*square_key = rng.next();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut castling = [0; 16];
|
let mut castling = [0; 16];
|
||||||
for i in 0..16 {
|
for c in castling.iter_mut() {
|
||||||
castling[i] = rng.next();
|
*c = rng.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut en_passant = [0; 9];
|
let mut en_passant = [0; 9];
|
||||||
for i in 0..9 {
|
for ep in en_passant.iter_mut() {
|
||||||
en_passant[i] = rng.next();
|
*ep = rng.next();
|
||||||
}
|
}
|
||||||
|
|
||||||
let side_to_move = rng.next();
|
let side_to_move = rng.next();
|
||||||
|
|
@ -67,16 +63,13 @@ pub fn init_zobrist() {
|
||||||
side_to_move,
|
side_to_move,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Set the global keys. Unwrap panics if set is called twice (should not happen).
|
|
||||||
KEYS.set(keys).expect("Zobrist keys already initialized");
|
KEYS.set(keys).expect("Zobrist keys already initialized");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Safe accessor without unsafe block
|
|
||||||
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().expect("Zobrist keys not initialized! Call init_zobrist() in main.")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Helper to map piece+color to index 0-11
|
|
||||||
pub fn piece_index(pt: PieceType, c: Color) -> usize {
|
pub fn piece_index(pt: PieceType, c: Color) -> usize {
|
||||||
let offset = match c {
|
let offset = match c {
|
||||||
Color::White => 0,
|
Color::White => 0,
|
||||||
|
|
|
||||||
|
|
@ -13,14 +13,11 @@ fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
|
||||||
|
|
||||||
let mut leaf_nodes = 0_u64;
|
let mut leaf_nodes = 0_u64;
|
||||||
for mv in list.iter() {
|
for mv in list.iter() {
|
||||||
// Store the undo info when making the move
|
|
||||||
let undo_info = board.make_move(*mv);
|
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Undo the move to restore the board state for the next iteration
|
|
||||||
board.undo_move(undo_info);
|
board.undo_move(undo_info);
|
||||||
}
|
}
|
||||||
leaf_nodes
|
leaf_nodes
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue