implemented transposition table
This commit is contained in:
parent
ea9419d7e0
commit
42816a6939
13 changed files with 464 additions and 91 deletions
|
|
@ -1,8 +1,10 @@
|
|||
use chess_engine::board::Board;
|
||||
use criterion::{criterion_group, criterion_main, Criterion};
|
||||
use chess_engine::eval::basic::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(|| {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ 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 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 {
|
||||
|
|
@ -23,6 +24,7 @@ fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
|
|||
|
||||
|
||||
fn run_perft_benchmark(c: &mut Criterion) {
|
||||
init_zobrist();
|
||||
let mut board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
||||
|
||||
c.bench_function("standard_perft5", |b| {
|
||||
|
|
|
|||
Binary file not shown.
|
|
@ -2,6 +2,7 @@ use std::fs::File;
|
|||
use std::io::{self, BufRead};
|
||||
use chess_engine::engine::Engine;
|
||||
use std::time::{Instant, Duration};
|
||||
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>>> {
|
||||
|
|
@ -23,6 +24,7 @@ fn load_csv(path: &str) -> io::Result<Vec<Vec<String>>> {
|
|||
}
|
||||
|
||||
fn main() {
|
||||
init_zobrist();
|
||||
let mut total_tests: f32 = 0.0;
|
||||
let mut correct_tests: f32 = 0.0;
|
||||
let sts = load_csv("src/bin/stockfish_testsuite.csv").unwrap();
|
||||
|
|
@ -31,6 +33,7 @@ fn main() {
|
|||
// Set the time limit to 1 second
|
||||
let time_limit = Duration::from_millis(1000);
|
||||
|
||||
|
||||
for test in &sts {
|
||||
let fen = &test[0];
|
||||
let bm = &test[1];
|
||||
|
|
|
|||
135
src/board.rs
135
src/board.rs
|
|
@ -1,26 +1,27 @@
|
|||
use crate::r#move::*;
|
||||
use crate::square::Square;
|
||||
use crate::zobrist::{self, zobrist_keys}; // Import Zobrist
|
||||
use std::ops::Not;
|
||||
|
||||
pub const CASTLING_WK_FLAG: u8 = 1;
|
||||
pub const CASTLING_WK_MASK: u64 = 96; // F1 G1
|
||||
pub const CASTLING_WK_K_POS_MASK: u64 = 16; // E1
|
||||
pub const CASTLING_WK_R_POS_MASK: u64 = 128; // H1
|
||||
pub const CASTLING_WK_MASK: u64 = 96;
|
||||
pub const CASTLING_WK_K_POS_MASK: u64 = 16;
|
||||
pub const CASTLING_WK_R_POS_MASK: u64 = 128;
|
||||
|
||||
pub const CASTLING_WQ_FLAG: u8 = 2;
|
||||
pub const CASTLING_WQ_MASK: u64 = 14; // B1 C1 D1
|
||||
pub const CASTLING_WQ_K_POS_MASK: u64 = 16; // E1
|
||||
pub const CASTLING_WQ_R_POS_MASK: u64 = 1; // A1
|
||||
pub const CASTLING_WQ_MASK: u64 = 14;
|
||||
pub const CASTLING_WQ_K_POS_MASK: u64 = 16;
|
||||
pub const CASTLING_WQ_R_POS_MASK: u64 = 1;
|
||||
|
||||
pub const CASTLING_BK_FLAG: u8 = 4;
|
||||
pub const CASTLING_BK_MASK: u64 = 6917529027641081856; // F8 G8
|
||||
pub const CASTLING_BK_K_POS_MASK: u64 = 1152921504606846976; // E8
|
||||
pub const CASTLING_BK_R_POS_MASK: u64 = 9223372036854775808; // H8
|
||||
pub const CASTLING_BK_MASK: u64 = 6917529027641081856;
|
||||
pub const CASTLING_BK_K_POS_MASK: u64 = 1152921504606846976;
|
||||
pub const CASTLING_BK_R_POS_MASK: u64 = 9223372036854775808;
|
||||
|
||||
pub const CASTLING_BQ_FLAG: u8 = 8;
|
||||
pub const CASTLING_BQ_MASK: u64 = 1008806316530991104; // B8 C8 D8
|
||||
pub const CASTLING_BQ_K_POS_MASK: u64 = 1152921504606846976; // E8
|
||||
pub const CASTLING_BQ_R_POS_MASK: u64 = 72057594037927936; // A8
|
||||
pub const CASTLING_BQ_MASK: u64 = 1008806316530991104;
|
||||
pub const CASTLING_BQ_K_POS_MASK: u64 = 1152921504606846976;
|
||||
pub const CASTLING_BQ_R_POS_MASK: u64 = 72057594037927936;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
|
|
@ -65,7 +66,7 @@ pub struct Board {
|
|||
pub side_to_move: Color,
|
||||
|
||||
pub pieces: [[u64; 2]; 6],
|
||||
pub pieces_on_squares: [Option<PieceType>; 64], // <-- ADDED
|
||||
pub pieces_on_squares: [Option<PieceType>; 64],
|
||||
|
||||
pub occupied: [u64; 2],
|
||||
pub all_occupied: u64,
|
||||
|
|
@ -76,9 +77,53 @@ pub struct Board {
|
|||
|
||||
pub halfmove_clock: u8,
|
||||
pub fullmove_number: u16,
|
||||
|
||||
// Added Zobrist Hash
|
||||
pub hash: u64,
|
||||
}
|
||||
|
||||
impl Board {
|
||||
// Helper to get the EP file index (0-7) or 8 if None
|
||||
fn ep_file_index(ep: Option<Square>) -> usize {
|
||||
match ep {
|
||||
Some(sq) => (sq as usize) % 8,
|
||||
None => 8,
|
||||
}
|
||||
}
|
||||
|
||||
// Should be called after loading FEN or creating board
|
||||
pub fn recalculate_hash(&mut self) {
|
||||
let keys = zobrist_keys();
|
||||
let mut hash = 0;
|
||||
|
||||
// 1. Pieces
|
||||
for sq in 0..64 {
|
||||
if let Some(pt) = self.pieces_on_squares[sq] {
|
||||
let color = if (self.pieces[pt as usize][Color::White as usize] & (1 << sq)) != 0 {
|
||||
Color::White
|
||||
} else {
|
||||
Color::Black
|
||||
};
|
||||
hash ^= keys.pieces[zobrist::piece_index(pt, color)][sq];
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Castling
|
||||
hash ^= keys.castling[self.castling_rights as usize];
|
||||
|
||||
// 3. En Passant
|
||||
hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
||||
|
||||
// 4. Side to move
|
||||
if self.side_to_move == Color::Black {
|
||||
hash ^= keys.side_to_move;
|
||||
}
|
||||
|
||||
self.hash = hash;
|
||||
}
|
||||
|
||||
// --- Original methods with added hashing ---
|
||||
|
||||
fn rm_piece(
|
||||
&mut self,
|
||||
target_square: Square,
|
||||
|
|
@ -87,10 +132,13 @@ impl Board {
|
|||
let target_square_bitboard = target_square.to_bitboard();
|
||||
let piece_type = self.pieces_on_squares[target_square as usize].unwrap();
|
||||
|
||||
// UPDATE HASH: Remove piece
|
||||
let keys = zobrist_keys();
|
||||
self.hash ^= keys.pieces[zobrist::piece_index(piece_type, color)][target_square as usize];
|
||||
|
||||
self.pieces[piece_type as usize][color as usize] ^= target_square_bitboard;
|
||||
self.pieces_on_squares[target_square as usize] = None;
|
||||
|
||||
// update occupancy helper bitboards
|
||||
self.occupied[color as usize] ^= target_square_bitboard;
|
||||
self.all_occupied ^= target_square_bitboard;
|
||||
self.empty_squares |= target_square_bitboard;
|
||||
|
|
@ -100,10 +148,14 @@ impl Board {
|
|||
|
||||
fn put_piece(&mut self, target_square: Square, color: Color, piece_type: PieceType) {
|
||||
let target_square_bitboard = target_square.to_bitboard();
|
||||
|
||||
// UPDATE HASH: Add piece
|
||||
let keys = zobrist_keys();
|
||||
self.hash ^= keys.pieces[zobrist::piece_index(piece_type, color)][target_square as usize];
|
||||
|
||||
self.pieces[piece_type as usize][color as usize] |= target_square_bitboard;
|
||||
self.pieces_on_squares[target_square as usize] = Some(piece_type);
|
||||
|
||||
// update occupancy helper bitboards
|
||||
self.occupied[color as usize] |= target_square_bitboard;
|
||||
self.all_occupied |= target_square_bitboard;
|
||||
self.empty_squares ^= target_square_bitboard;
|
||||
|
|
@ -115,24 +167,29 @@ impl Board {
|
|||
}
|
||||
|
||||
pub fn make_move(&mut self, mv: Move) -> UndoMove {
|
||||
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.castling[self.castling_rights as usize];
|
||||
|
||||
// 1. Extract parts from move
|
||||
let from = mv.get_from();
|
||||
let to = mv.get_to();
|
||||
let flags = mv.get_flags();
|
||||
|
||||
// 2. Save old state for UndoMove object
|
||||
let old_en_passant_target: Option<Square> = self.en_passant_target;
|
||||
let old_castling_rights: u8 = self.castling_rights;
|
||||
let old_halfmove_clock: u8 = self.halfmove_clock;
|
||||
|
||||
// 3. Save pawns and total pieces for half move tracking
|
||||
let old_friendly_pawns = self.pieces[PieceType::Pawn as usize][self.side_to_move as usize];
|
||||
let old_total_pieces = self.all_occupied.count_ones();
|
||||
|
||||
let mut opt_captured_piece: Option<PieceType> = None;
|
||||
let mut opt_en_passant_target: Option<Square> = None;
|
||||
|
||||
// 4. Make the actual moves on the bitboard based on flag type
|
||||
// 4. Make the actual moves (rm_piece/put_piece update piece hashes automatically)
|
||||
match flags {
|
||||
MOVE_FLAG_QUIET => {
|
||||
self.move_piece(from, to, self.side_to_move);
|
||||
|
|
@ -205,7 +262,7 @@ impl Board {
|
|||
_ => { panic!("unable to make_move: invalid flags: {}", flags); }
|
||||
}
|
||||
|
||||
// 5. Update the castling rights
|
||||
// 5. Update castling rights
|
||||
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 bk = self.pieces[PieceType::King as usize][Color::Black as usize];
|
||||
|
|
@ -216,12 +273,18 @@ impl Board {
|
|||
let castling_right_bq = (((bk & CASTLING_BQ_K_POS_MASK) > 0 && (br & CASTLING_BQ_R_POS_MASK) > 0) as u8) << 3;
|
||||
let new_castling_rights =
|
||||
castling_right_wk | castling_right_wq | castling_right_bk | castling_right_bq;
|
||||
self.castling_rights = self.castling_rights & new_castling_rights; // & operator makes sure castling rights can not be gained back
|
||||
self.castling_rights = self.castling_rights & new_castling_rights;
|
||||
|
||||
// 6. Update the en passant target square
|
||||
// 6. Update en passant target
|
||||
self.en_passant_target = opt_en_passant_target;
|
||||
|
||||
// 7. Update the halfmove clock
|
||||
// HASH UPDATE: Add new state
|
||||
self.hash ^= keys.en_passant[Self::ep_file_index(self.en_passant_target)];
|
||||
self.hash ^= keys.castling[self.castling_rights as usize];
|
||||
// HASH UPDATE: Side to move (always changes)
|
||||
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_total_pieces = self.all_occupied.count_ones();
|
||||
let pawns_changed = old_friendly_pawns ^ new_friendly_pawns;
|
||||
|
|
@ -229,13 +292,13 @@ impl Board {
|
|||
let increase_halfmove_clock = ((pawns_changed + piece_captured) == 0) as u8;
|
||||
self.halfmove_clock = increase_halfmove_clock * (self.halfmove_clock + 1);
|
||||
|
||||
// 8. Increase the fullmove clock
|
||||
// 8. Increase fullmove
|
||||
self.fullmove_number += self.side_to_move as u16;
|
||||
|
||||
// 9. Flip the side to move
|
||||
// 9. Flip side
|
||||
self.side_to_move = !self.side_to_move;
|
||||
|
||||
// 10. Create and return UndoMove object
|
||||
// 10. Return Undo
|
||||
UndoMove::new(
|
||||
mv,
|
||||
opt_captured_piece,
|
||||
|
|
@ -246,15 +309,28 @@ impl Board {
|
|||
}
|
||||
|
||||
pub fn undo_move(&mut self, undo_info: UndoMove) {
|
||||
// 1. Restore all simple state from the UndoMove object
|
||||
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;
|
||||
// 2. Remove current castling and EP hash
|
||||
self.hash ^= keys.castling[self.castling_rights as usize];
|
||||
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.en_passant_target = undo_info.old_en_passant_square;
|
||||
self.halfmove_clock = undo_info.old_halfmove_clock;
|
||||
|
||||
// 2. Flip side_to_move *before* doing piece ops.
|
||||
// HASH UPDATE: Restore OLD state hash
|
||||
self.hash ^= keys.castling[self.castling_rights as usize];
|
||||
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;
|
||||
|
||||
// 3. Decrement fullmove number if it was Black's turn (which is now self.side_to_move)
|
||||
// 3. Decrement fullmove
|
||||
self.fullmove_number -= self.side_to_move as u16;
|
||||
|
||||
// 4. Extract move data
|
||||
|
|
@ -263,7 +339,7 @@ impl Board {
|
|||
let to = mv.get_to();
|
||||
let flags = mv.get_flags();
|
||||
|
||||
// 5. Reverse the piece movements based on the flag
|
||||
// 5. Reverse pieces (helpers will update hash automatically)
|
||||
match flags {
|
||||
MOVE_FLAG_QUIET => {
|
||||
self.move_piece(to, from, self.side_to_move);
|
||||
|
|
@ -302,7 +378,6 @@ impl Board {
|
|||
}
|
||||
MOVE_FLAG_EN_PASSANT => {
|
||||
self.move_piece(to, from, self.side_to_move);
|
||||
// Determine where the captured pawn was
|
||||
let captured_pawn_square = to + (self.side_to_move as i8 * 16 - 8);
|
||||
self.put_piece(captured_pawn_square, !self.side_to_move, PieceType::Pawn);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,22 +1,29 @@
|
|||
use crate::board::Board;
|
||||
use crate::r#move::Move;
|
||||
use crate::search::alpha_beta::alpha_beta;
|
||||
use crate::tt::TranspositionTable; // Import TT
|
||||
use std::time::{Instant, Duration};
|
||||
|
||||
pub struct Engine {
|
||||
pub name: String,
|
||||
pub author: String,
|
||||
pub board: Board,
|
||||
pub tt: TranspositionTable, // Engine owns the TT
|
||||
}
|
||||
|
||||
impl Engine {
|
||||
pub fn new(name: String, author: String) -> Engine {
|
||||
// Use the standard starting position
|
||||
let board = Board::from_fen("rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1");
|
||||
|
||||
// Create TT with 64 MB
|
||||
let tt = TranspositionTable::new(64);
|
||||
|
||||
Engine {
|
||||
name,
|
||||
author,
|
||||
board
|
||||
board,
|
||||
tt,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -36,10 +43,14 @@ impl Engine {
|
|||
let start_time = Instant::now();
|
||||
let time_limit = Duration::from_millis(time_limit_ms);
|
||||
|
||||
// We track nodes to limit how often we check the clock inside alpha_beta
|
||||
// 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;
|
||||
|
||||
// Initial search at depth 1
|
||||
// Note: We pass &mut self.tt to alpha_beta
|
||||
let (mut opt_move, mut _score) = alpha_beta(
|
||||
&mut self.board,
|
||||
1,
|
||||
|
|
@ -48,7 +59,8 @@ impl Engine {
|
|||
i32::MAX,
|
||||
start_time,
|
||||
time_limit,
|
||||
&mut nodes
|
||||
&mut nodes,
|
||||
&mut self.tt
|
||||
);
|
||||
|
||||
let mut depth = 2;
|
||||
|
|
@ -63,13 +75,12 @@ impl Engine {
|
|||
i32::MAX,
|
||||
start_time,
|
||||
time_limit,
|
||||
&mut nodes
|
||||
&mut nodes,
|
||||
&mut self.tt
|
||||
);
|
||||
|
||||
// If time ran out during the search, alpha_beta returns garbage (None, 0).
|
||||
// We must verify we still have time before accepting the new result.
|
||||
if start_time.elapsed() > time_limit {
|
||||
break; // Discard new_move, keep the one from the previous depth
|
||||
break;
|
||||
}
|
||||
|
||||
opt_move = new_move;
|
||||
|
|
@ -81,7 +92,6 @@ impl Engine {
|
|||
if let Some(mv) = opt_move {
|
||||
mv.to_algebraic()
|
||||
} else {
|
||||
// UCI format for no legal moves (checkmate/stalemate)
|
||||
"null".to_string()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,3 +8,5 @@ pub mod parsing;
|
|||
pub mod search;
|
||||
pub mod engine;
|
||||
pub mod uci;
|
||||
pub mod tt;
|
||||
pub mod zobrist;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -87,6 +87,11 @@ impl MoveList {
|
|||
self.count += 1;
|
||||
}
|
||||
|
||||
// Added swap method as requested
|
||||
pub fn swap(&mut self, a: usize, b: usize) {
|
||||
self.moves[..self.count].swap(a, b);
|
||||
}
|
||||
|
||||
pub fn len(&self) -> usize {
|
||||
self.count
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ impl Board {
|
|||
// Initialisiere die Arrays
|
||||
let mut pieces = [[0u64; 2]; 6];
|
||||
let mut occupied = [0u64; 2];
|
||||
let mut pieces_on_squares = [None; 64]; // <-- ADDED
|
||||
let mut pieces_on_squares = [None; 64];
|
||||
|
||||
// Part 1: Piece placement
|
||||
let placement = parts.next().unwrap_or("");
|
||||
|
|
@ -65,27 +65,27 @@ impl Board {
|
|||
match c {
|
||||
'p' => {
|
||||
pieces[PieceType::Pawn as usize][color_idx] |= mask;
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Pawn); // <-- ADDED
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Pawn);
|
||||
}
|
||||
'n' => {
|
||||
pieces[PieceType::Knight as usize][color_idx] |= mask;
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Knight); // <-- ADDED
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Knight);
|
||||
}
|
||||
'b' => {
|
||||
pieces[PieceType::Bishop as usize][color_idx] |= mask;
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Bishop); // <-- ADDED
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Bishop);
|
||||
}
|
||||
'r' => {
|
||||
pieces[PieceType::Rook as usize][color_idx] |= mask;
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Rook); // <-- ADDED
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Rook);
|
||||
}
|
||||
'q' => {
|
||||
pieces[PieceType::Queen as usize][color_idx] |= mask;
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Queen); // <-- ADDED
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::Queen);
|
||||
}
|
||||
'k' => {
|
||||
pieces[PieceType::King as usize][color_idx] |= mask;
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::King); // <-- ADDED
|
||||
pieces_on_squares[sq as usize] = Some(PieceType::King);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
|
@ -125,7 +125,6 @@ impl Board {
|
|||
let file = (chars[0] as u8 - b'a') as u8;
|
||||
let rank = (chars[1] as u8 - b'1') as u8;
|
||||
let sq_index = rank * 8 + file;
|
||||
// This is unsafe, but assumes the FEN is valid
|
||||
Some(unsafe { mem::transmute::<u8, Square>(sq_index) })
|
||||
}
|
||||
};
|
||||
|
|
@ -139,10 +138,11 @@ impl Board {
|
|||
let all_occupied = occupied[Color::White as usize] | occupied[Color::Black as usize];
|
||||
let empty_squares = !all_occupied;
|
||||
|
||||
Board {
|
||||
// Create mutable board with empty hash
|
||||
let mut board = Board {
|
||||
side_to_move,
|
||||
pieces,
|
||||
pieces_on_squares, // <-- ADDED
|
||||
pieces_on_squares,
|
||||
occupied,
|
||||
all_occupied,
|
||||
empty_squares,
|
||||
|
|
@ -150,7 +150,13 @@ impl Board {
|
|||
en_passant_target,
|
||||
halfmove_clock,
|
||||
fullmove_number,
|
||||
}
|
||||
hash: 0, // Initialize hash to 0
|
||||
};
|
||||
|
||||
// Calculate the correct initial Zobrist hash based on the parsed FEN
|
||||
board.recalculate_hash();
|
||||
|
||||
board
|
||||
}
|
||||
|
||||
/// Converts the current board state into a FEN string.
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ use crate::eval::basic::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 std::time::{Instant, Duration};
|
||||
|
||||
// A score high enough to be > any material eval, but low enough to not overflow when adding ply
|
||||
|
|
@ -16,6 +17,29 @@ 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)
|
||||
} else if score < -MATE_SCORE + 1000 {
|
||||
score - (ply as i32)
|
||||
} else {
|
||||
score
|
||||
}
|
||||
}
|
||||
|
||||
fn score_from_tt(score: i32, ply: u8) -> i32 {
|
||||
if score > MATE_SCORE - 1000 {
|
||||
score - (ply as i32)
|
||||
} else if score < -MATE_SCORE + 1000 {
|
||||
score + (ply as i32)
|
||||
} else {
|
||||
score
|
||||
}
|
||||
}
|
||||
|
||||
pub fn alpha_beta(
|
||||
board: &mut Board,
|
||||
depth: u8,
|
||||
|
|
@ -25,29 +49,81 @@ pub fn alpha_beta(
|
|||
start_time: Instant,
|
||||
time_limit: Duration,
|
||||
nodes: &mut u64,
|
||||
tt: &mut TranspositionTable, // Added TT parameter
|
||||
) -> (Option<Move>, i32) {
|
||||
// Check for time usage every 4096 nodes to reduce system call overhead
|
||||
// Check for time usage
|
||||
if *nodes % 4096 == 0 {
|
||||
if start_time.elapsed() > time_limit {
|
||||
// Return immediately. The return value here effectively signals an abort,
|
||||
// but the engine must discard this result.
|
||||
return (None, 0);
|
||||
}
|
||||
}
|
||||
*nodes += 1;
|
||||
|
||||
// -----------------------
|
||||
// 1. TT PROBE
|
||||
// -----------------------
|
||||
// We assume board.hash holds the current Zobrist key (u64)
|
||||
let tt_key = board.hash;
|
||||
let mut tt_move: Option<Move> = None;
|
||||
|
||||
if let Some(entry) = tt.probe(tt_key) {
|
||||
// We remember the move from TT to sort it first later
|
||||
if entry.bm.0 != 0 { // Check if move is valid (not 0)
|
||||
tt_move = Some(entry.bm);
|
||||
}
|
||||
|
||||
// Can we use the score for a cutoff?
|
||||
if entry.depth >= depth {
|
||||
let tt_score = score_from_tt(entry.score, ply);
|
||||
|
||||
match entry.node_type {
|
||||
NodeType::Exact => return (Some(entry.bm), tt_score),
|
||||
NodeType::Alpha => {
|
||||
if tt_score <= alpha {
|
||||
return (Some(entry.bm), tt_score);
|
||||
}
|
||||
}
|
||||
NodeType::Beta => {
|
||||
if tt_score >= beta {
|
||||
return (Some(entry.bm), tt_score);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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; // This is our local "worst case"
|
||||
let mut legal_moves_found = false;
|
||||
|
||||
for mv in list.iter() {
|
||||
let undo_mv = board.make_move(*mv);
|
||||
// -----------------------
|
||||
// MOVE ORDERING (TT Move First)
|
||||
// -----------------------
|
||||
// If we have a move from TT, we want to search it first!
|
||||
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() {
|
||||
if list[i] == tm {
|
||||
list.swap(0, i);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut best_move: Option<Move> = None;
|
||||
let mut best_score: i32 = -i32::MAX;
|
||||
let mut legal_moves_found = false;
|
||||
let alpha_orig = alpha; // Save original alpha to determine NodeType later
|
||||
|
||||
for i in 0..list.len() {
|
||||
let mv = list[i];
|
||||
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);
|
||||
if is_illegal {
|
||||
board.undo_move(undo_mv);
|
||||
|
|
@ -55,35 +131,28 @@ pub fn alpha_beta(
|
|||
}
|
||||
legal_moves_found = true;
|
||||
|
||||
// Recursive call with negated and swapped alpha/beta
|
||||
// Pass time parameters and node counter down
|
||||
let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes);
|
||||
let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes, tt);
|
||||
|
||||
// If we aborted deeper in the tree (returned 0 due to timeout),
|
||||
// we should technically propagate that up, but checking elapsed()
|
||||
// at the loop start (via recursion) handles it eventually.
|
||||
// For a strict abort, we check here too:
|
||||
if *nodes % 4096 == 0 && start_time.elapsed() > time_limit {
|
||||
board.undo_move(undo_mv);
|
||||
return (None, 0);
|
||||
board.undo_move(undo_mv);
|
||||
return (None, 0);
|
||||
}
|
||||
|
||||
let current_score = -score;
|
||||
|
||||
if current_score > best_score {
|
||||
best_score = current_score;
|
||||
best_move = Some(*mv);
|
||||
best_move = Some(mv);
|
||||
}
|
||||
|
||||
board.undo_move(undo_mv);
|
||||
|
||||
// Alpha-Beta Pruning logic
|
||||
if best_score > alpha {
|
||||
alpha = best_score;
|
||||
}
|
||||
|
||||
if alpha >= beta {
|
||||
break; // Beta cutoff (Pruning)
|
||||
break; // Beta cutoff
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -91,10 +160,25 @@ pub fn alpha_beta(
|
|||
if is_current_king_attacked(board) {
|
||||
return (None, -MATE_SCORE + (ply as i32));
|
||||
} else {
|
||||
// Stalemate
|
||||
return (None, 0);
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------
|
||||
// 2. TT STORE
|
||||
// -----------------------
|
||||
let node_type = if best_score <= alpha_orig {
|
||||
NodeType::Alpha // We didn't improve alpha (Fail Low) -> Upper Bound
|
||||
} else if best_score >= beta {
|
||||
NodeType::Beta // We caused a cutoff (Fail High) -> Lower Bound
|
||||
} else {
|
||||
NodeType::Exact // We found a score between alpha and beta
|
||||
};
|
||||
|
||||
let save_move = best_move.unwrap_or(Move(0)); // Use dummy 0 if no best move
|
||||
let save_score = score_to_tt(best_score, ply);
|
||||
|
||||
tt.store(tt_key, save_score, depth, node_type, save_move);
|
||||
|
||||
(best_move, best_score)
|
||||
}
|
||||
96
src/tt.rs
Normal file
96
src/tt.rs
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
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;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u8)]
|
||||
pub enum NodeType {
|
||||
Empty = 0,
|
||||
Exact = 1,
|
||||
Alpha = 2,
|
||||
Beta = 3,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct TTEntry {
|
||||
pub key: u64,
|
||||
pub bm: Move, // u16
|
||||
pub score: i32,
|
||||
pub depth: u8,
|
||||
pub node_type: NodeType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct TranspositionTable {
|
||||
pub entries: Vec<TTEntry>,
|
||||
pub size: usize,
|
||||
}
|
||||
|
||||
impl Default for TTEntry {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
key: 0,
|
||||
score: 0,
|
||||
bm: Move(0_u16),
|
||||
depth: 0,
|
||||
node_type: NodeType::Empty,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TranspositionTable {
|
||||
pub fn new(mb_size: usize) -> Self {
|
||||
let entry_size = size_of::<TTEntry>();
|
||||
// Calculate how many entries fit into the given MB size
|
||||
let target_count = (mb_size * 1024 * 1024) / entry_size;
|
||||
|
||||
// Round down to nearest power of 2 for fast indexing (using & instead of %)
|
||||
let size = if target_count == 0 {
|
||||
1
|
||||
} else {
|
||||
target_count.next_power_of_two() >> 1
|
||||
};
|
||||
|
||||
Self {
|
||||
entries: vec![TTEntry::default(); size],
|
||||
size,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
for entry in self.entries.iter_mut() {
|
||||
*entry = TTEntry::default();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn probe(&self, key: u64) -> Option<TTEntry> {
|
||||
// Fast modulo using bitwise AND (works because size is power of 2)
|
||||
let index = (key as usize) & (self.size - 1);
|
||||
let entry = self.entries[index];
|
||||
|
||||
// Return entry only if keys match and it's not empty
|
||||
if entry.key == key && entry.node_type != NodeType::Empty {
|
||||
Some(entry)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
pub fn store(&mut self, key: u64, score: i32, depth: u8, flag: NodeType, best_move: Move) {
|
||||
let index = (key as usize) & (self.size - 1);
|
||||
let entry = &mut self.entries[index];
|
||||
|
||||
// Replacement Strategy:
|
||||
// 1. Slot is empty
|
||||
// 2. Collision (different position) -> Always replace (new position is likely more relevant)
|
||||
// 3. Same position -> Replace only if new depth is better or equal
|
||||
if entry.node_type == NodeType::Empty || entry.key != key || depth >= entry.depth {
|
||||
entry.key = key;
|
||||
entry.score = score;
|
||||
entry.depth = depth;
|
||||
entry.node_type = flag;
|
||||
entry.bm = best_move;
|
||||
}
|
||||
}
|
||||
}
|
||||
86
src/zobrist.rs
Normal file
86
src/zobrist.rs
Normal file
|
|
@ -0,0 +1,86 @@
|
|||
use crate::board::{Color, PieceType};
|
||||
use crate::square::Square;
|
||||
use std::sync::OnceLock;
|
||||
|
||||
// We use a simple Xorshift generator to avoid external dependencies like 'rand'
|
||||
struct Xorshift {
|
||||
state: u64,
|
||||
}
|
||||
|
||||
impl Xorshift {
|
||||
fn new(seed: u64) -> Self {
|
||||
Self { state: seed }
|
||||
}
|
||||
|
||||
fn next(&mut self) -> u64 {
|
||||
let mut x = self.state;
|
||||
x ^= x << 13;
|
||||
x ^= x >> 7;
|
||||
x ^= x << 17;
|
||||
self.state = x;
|
||||
x
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ZobristKeys {
|
||||
pub pieces: [[u64; 64]; 12], // [PieceType 0-5 + Color offset][Square]
|
||||
pub castling: [u64; 16], // 16 combinations of castling rights
|
||||
pub en_passant: [u64; 9], // 8 files + 1 for "no ep"
|
||||
pub side_to_move: u64,
|
||||
}
|
||||
|
||||
// Thread-safe, write-once global storage
|
||||
static KEYS: OnceLock<ZobristKeys> = OnceLock::new();
|
||||
|
||||
pub fn init_zobrist() {
|
||||
// If already initialized, do nothing
|
||||
if KEYS.get().is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let mut rng = Xorshift::new(1070372); // Fixed seed for reproducibility
|
||||
|
||||
let mut pieces = [[0; 64]; 12];
|
||||
for i in 0..12 {
|
||||
for j in 0..64 {
|
||||
pieces[i][j] = rng.next();
|
||||
}
|
||||
}
|
||||
|
||||
let mut castling = [0; 16];
|
||||
for i in 0..16 {
|
||||
castling[i] = rng.next();
|
||||
}
|
||||
|
||||
let mut en_passant = [0; 9];
|
||||
for i in 0..9 {
|
||||
en_passant[i] = rng.next();
|
||||
}
|
||||
|
||||
let side_to_move = rng.next();
|
||||
|
||||
let keys = ZobristKeys {
|
||||
pieces,
|
||||
castling,
|
||||
en_passant,
|
||||
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");
|
||||
}
|
||||
|
||||
// Safe accessor without unsafe block
|
||||
pub fn zobrist_keys() -> &'static ZobristKeys {
|
||||
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 {
|
||||
let offset = match c {
|
||||
Color::White => 0,
|
||||
Color::Black => 6,
|
||||
};
|
||||
(pt as usize) + offset
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue