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 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::basic::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(|| {
|
||||||
|
|
|
||||||
|
|
@ -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::movegen::legal_check::is_other_king_attacked;
|
||||||
use chess_engine::r#move::MoveList;
|
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 {
|
||||||
|
|
@ -23,6 +24,7 @@ fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
|
||||||
|
|
||||||
|
|
||||||
fn run_perft_benchmark(c: &mut Criterion) {
|
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");
|
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| {
|
||||||
|
|
|
||||||
Binary file not shown.
|
|
@ -2,6 +2,7 @@ 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;
|
||||||
// EACH TEST CAN ONLY TAKE ONE SECOND MAX TO KEEP RESULTS COMPARABLE
|
// 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>>> {
|
||||||
|
|
@ -23,6 +24,7 @@ fn load_csv(path: &str) -> io::Result<Vec<Vec<String>>> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
|
init_zobrist();
|
||||||
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();
|
||||||
|
|
@ -31,6 +33,7 @@ fn main() {
|
||||||
// Set the time limit to 1 second
|
// Set the time limit to 1 second
|
||||||
let time_limit = Duration::from_millis(1000);
|
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];
|
||||||
|
|
|
||||||
135
src/board.rs
135
src/board.rs
|
|
@ -1,26 +1,27 @@
|
||||||
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 std::ops::Not;
|
use std::ops::Not;
|
||||||
|
|
||||||
pub const CASTLING_WK_FLAG: u8 = 1;
|
pub const CASTLING_WK_FLAG: u8 = 1;
|
||||||
pub const CASTLING_WK_MASK: u64 = 96; // F1 G1
|
pub const CASTLING_WK_MASK: u64 = 96;
|
||||||
pub const CASTLING_WK_K_POS_MASK: u64 = 16; // E1
|
pub const CASTLING_WK_K_POS_MASK: u64 = 16;
|
||||||
pub const CASTLING_WK_R_POS_MASK: u64 = 128; // H1
|
pub const CASTLING_WK_R_POS_MASK: u64 = 128;
|
||||||
|
|
||||||
pub const CASTLING_WQ_FLAG: u8 = 2;
|
pub const CASTLING_WQ_FLAG: u8 = 2;
|
||||||
pub const CASTLING_WQ_MASK: u64 = 14; // B1 C1 D1
|
pub const CASTLING_WQ_MASK: u64 = 14;
|
||||||
pub const CASTLING_WQ_K_POS_MASK: u64 = 16; // E1
|
pub const CASTLING_WQ_K_POS_MASK: u64 = 16;
|
||||||
pub const CASTLING_WQ_R_POS_MASK: u64 = 1; // A1
|
pub const CASTLING_WQ_R_POS_MASK: u64 = 1;
|
||||||
|
|
||||||
pub const CASTLING_BK_FLAG: u8 = 4;
|
pub const CASTLING_BK_FLAG: u8 = 4;
|
||||||
pub const CASTLING_BK_MASK: u64 = 6917529027641081856; // F8 G8
|
pub const CASTLING_BK_MASK: u64 = 6917529027641081856;
|
||||||
pub const CASTLING_BK_K_POS_MASK: u64 = 1152921504606846976; // E8
|
pub const CASTLING_BK_K_POS_MASK: u64 = 1152921504606846976;
|
||||||
pub const CASTLING_BK_R_POS_MASK: u64 = 9223372036854775808; // H8
|
pub const CASTLING_BK_R_POS_MASK: u64 = 9223372036854775808;
|
||||||
|
|
||||||
pub const CASTLING_BQ_FLAG: u8 = 8;
|
pub const CASTLING_BQ_FLAG: u8 = 8;
|
||||||
pub const CASTLING_BQ_MASK: u64 = 1008806316530991104; // B8 C8 D8
|
pub const CASTLING_BQ_MASK: u64 = 1008806316530991104;
|
||||||
pub const CASTLING_BQ_K_POS_MASK: u64 = 1152921504606846976; // E8
|
pub const CASTLING_BQ_K_POS_MASK: u64 = 1152921504606846976;
|
||||||
pub const CASTLING_BQ_R_POS_MASK: u64 = 72057594037927936; // A8
|
pub const CASTLING_BQ_R_POS_MASK: u64 = 72057594037927936;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
#[repr(u8)]
|
#[repr(u8)]
|
||||||
|
|
@ -65,7 +66,7 @@ pub struct Board {
|
||||||
pub side_to_move: Color,
|
pub side_to_move: Color,
|
||||||
|
|
||||||
pub pieces: [[u64; 2]; 6],
|
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 occupied: [u64; 2],
|
||||||
pub all_occupied: u64,
|
pub all_occupied: u64,
|
||||||
|
|
@ -76,9 +77,53 @@ 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,
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
||||||
|
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(
|
fn rm_piece(
|
||||||
&mut self,
|
&mut self,
|
||||||
target_square: Square,
|
target_square: Square,
|
||||||
|
|
@ -87,10 +132,13 @@ 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();
|
||||||
|
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[piece_type as usize][color as usize] ^= target_square_bitboard;
|
||||||
self.pieces_on_squares[target_square as usize] = None;
|
self.pieces_on_squares[target_square as usize] = None;
|
||||||
|
|
||||||
// update occupancy helper bitboards
|
|
||||||
self.occupied[color as usize] ^= target_square_bitboard;
|
self.occupied[color as usize] ^= target_square_bitboard;
|
||||||
self.all_occupied ^= target_square_bitboard;
|
self.all_occupied ^= target_square_bitboard;
|
||||||
self.empty_squares |= 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) {
|
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();
|
||||||
|
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[piece_type as usize][color as usize] |= target_square_bitboard;
|
||||||
self.pieces_on_squares[target_square as usize] = Some(piece_type);
|
self.pieces_on_squares[target_square as usize] = Some(piece_type);
|
||||||
|
|
||||||
// update occupancy helper bitboards
|
|
||||||
self.occupied[color as usize] |= target_square_bitboard;
|
self.occupied[color as usize] |= target_square_bitboard;
|
||||||
self.all_occupied |= target_square_bitboard;
|
self.all_occupied |= target_square_bitboard;
|
||||||
self.empty_squares ^= target_square_bitboard;
|
self.empty_squares ^= target_square_bitboard;
|
||||||
|
|
@ -115,24 +167,29 @@ impl Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn make_move(&mut self, mv: Move) -> UndoMove {
|
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
|
// 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();
|
||||||
|
|
||||||
// 2. Save old state for UndoMove object
|
|
||||||
let old_en_passant_target: Option<Square> = self.en_passant_target;
|
let old_en_passant_target: Option<Square> = self.en_passant_target;
|
||||||
let old_castling_rights: u8 = self.castling_rights;
|
let old_castling_rights: u8 = self.castling_rights;
|
||||||
let old_halfmove_clock: u8 = self.halfmove_clock;
|
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_friendly_pawns = self.pieces[PieceType::Pawn as usize][self.side_to_move as usize];
|
||||||
let old_total_pieces = self.all_occupied.count_ones();
|
let old_total_pieces = self.all_occupied.count_ones();
|
||||||
|
|
||||||
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 on the bitboard based on flag type
|
// 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);
|
||||||
|
|
@ -205,7 +262,7 @@ impl Board {
|
||||||
_ => { panic!("unable to make_move: invalid flags: {}", flags); }
|
_ => { 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 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];
|
||||||
|
|
@ -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 castling_right_bq = (((bk & CASTLING_BQ_K_POS_MASK) > 0 && (br & CASTLING_BQ_R_POS_MASK) > 0) as u8) << 3;
|
||||||
let new_castling_rights =
|
let new_castling_rights =
|
||||||
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; // & 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;
|
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_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;
|
||||||
|
|
@ -229,13 +292,13 @@ 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 the fullmove clock
|
// 8. Increase fullmove
|
||||||
self.fullmove_number += self.side_to_move as u16;
|
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;
|
self.side_to_move = !self.side_to_move;
|
||||||
|
|
||||||
// 10. Create and return UndoMove object
|
// 10. Return Undo
|
||||||
UndoMove::new(
|
UndoMove::new(
|
||||||
mv,
|
mv,
|
||||||
opt_captured_piece,
|
opt_captured_piece,
|
||||||
|
|
@ -246,15 +309,28 @@ impl Board {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn undo_move(&mut self, undo_info: UndoMove) {
|
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.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;
|
||||||
|
|
||||||
// 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;
|
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;
|
self.fullmove_number -= self.side_to_move as u16;
|
||||||
|
|
||||||
// 4. Extract move data
|
// 4. Extract move data
|
||||||
|
|
@ -263,7 +339,7 @@ impl Board {
|
||||||
let to = mv.get_to();
|
let to = mv.get_to();
|
||||||
let flags = mv.get_flags();
|
let flags = mv.get_flags();
|
||||||
|
|
||||||
// 5. Reverse the piece movements based on the flag
|
// 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);
|
||||||
|
|
@ -302,7 +378,6 @@ impl Board {
|
||||||
}
|
}
|
||||||
MOVE_FLAG_EN_PASSANT => {
|
MOVE_FLAG_EN_PASSANT => {
|
||||||
self.move_piece(to, from, self.side_to_move);
|
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);
|
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);
|
self.put_piece(captured_pawn_square, !self.side_to_move, PieceType::Pawn);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,29 @@
|
||||||
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::alpha_beta;
|
||||||
|
use crate::tt::TranspositionTable; // Import TT
|
||||||
use std::time::{Instant, Duration};
|
use std::time::{Instant, Duration};
|
||||||
|
|
||||||
pub struct Engine {
|
pub struct Engine {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub author: String,
|
pub author: String,
|
||||||
pub board: Board,
|
pub board: Board,
|
||||||
|
pub tt: TranspositionTable, // Engine owns the TT
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Engine {
|
impl Engine {
|
||||||
pub fn new(name: String, author: String) -> Engine {
|
pub fn new(name: String, author: String) -> Engine {
|
||||||
// Use the standard starting position
|
// Use the standard starting position
|
||||||
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");
|
||||||
|
|
||||||
|
// Create TT with 64 MB
|
||||||
|
let tt = TranspositionTable::new(64);
|
||||||
|
|
||||||
Engine {
|
Engine {
|
||||||
name,
|
name,
|
||||||
author,
|
author,
|
||||||
board
|
board,
|
||||||
|
tt,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -35,54 +42,57 @@ impl Engine {
|
||||||
pub fn search(&mut self, time_limit_ms: u64) -> String {
|
pub fn search(&mut self, time_limit_ms: u64) -> String {
|
||||||
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 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;
|
let mut nodes = 0;
|
||||||
|
|
||||||
// Initial search at depth 1
|
// 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,
|
||||||
0,
|
0,
|
||||||
-i32::MAX,
|
-i32::MAX,
|
||||||
i32::MAX,
|
i32::MAX,
|
||||||
start_time,
|
start_time,
|
||||||
time_limit,
|
time_limit,
|
||||||
&mut nodes
|
&mut nodes,
|
||||||
|
&mut self.tt
|
||||||
);
|
);
|
||||||
|
|
||||||
let mut depth = 2;
|
let mut depth = 2;
|
||||||
|
|
||||||
// Iterative Deepening
|
// 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,
|
||||||
depth,
|
depth,
|
||||||
0,
|
0,
|
||||||
-i32::MAX,
|
-i32::MAX,
|
||||||
i32::MAX,
|
i32::MAX,
|
||||||
start_time,
|
start_time,
|
||||||
time_limit,
|
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 {
|
if start_time.elapsed() > time_limit {
|
||||||
break; // Discard new_move, keep the one from the previous depth
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
opt_move = new_move;
|
opt_move = new_move;
|
||||||
_score = new_score;
|
_score = new_score;
|
||||||
|
|
||||||
depth += 1;
|
depth += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(mv) = opt_move {
|
if let Some(mv) = opt_move {
|
||||||
mv.to_algebraic()
|
mv.to_algebraic()
|
||||||
} else {
|
} else {
|
||||||
// UCI format for no legal moves (checkmate/stalemate)
|
|
||||||
"null".to_string()
|
"null".to_string()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -8,3 +8,5 @@ pub mod parsing;
|
||||||
pub mod search;
|
pub mod search;
|
||||||
pub mod engine;
|
pub mod engine;
|
||||||
pub mod uci;
|
pub mod uci;
|
||||||
|
pub mod tt;
|
||||||
|
pub mod zobrist;
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,9 @@
|
||||||
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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -87,6 +87,11 @@ impl MoveList {
|
||||||
self.count += 1;
|
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 {
|
pub fn len(&self) -> usize {
|
||||||
self.count
|
self.count
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ impl Board {
|
||||||
// Initialisiere die Arrays
|
// 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]; // <-- ADDED
|
let mut pieces_on_squares = [None; 64];
|
||||||
|
|
||||||
// Part 1: Piece placement
|
// Part 1: Piece placement
|
||||||
let placement = parts.next().unwrap_or("");
|
let placement = parts.next().unwrap_or("");
|
||||||
|
|
@ -65,27 +65,27 @@ impl Board {
|
||||||
match c {
|
match c {
|
||||||
'p' => {
|
'p' => {
|
||||||
pieces[PieceType::Pawn as usize][color_idx] |= mask;
|
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' => {
|
'n' => {
|
||||||
pieces[PieceType::Knight as usize][color_idx] |= mask;
|
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' => {
|
'b' => {
|
||||||
pieces[PieceType::Bishop as usize][color_idx] |= mask;
|
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' => {
|
'r' => {
|
||||||
pieces[PieceType::Rook as usize][color_idx] |= mask;
|
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' => {
|
'q' => {
|
||||||
pieces[PieceType::Queen as usize][color_idx] |= mask;
|
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' => {
|
'k' => {
|
||||||
pieces[PieceType::King as usize][color_idx] |= mask;
|
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 file = (chars[0] as u8 - b'a') as u8;
|
||||||
let rank = (chars[1] as u8 - b'1') as u8;
|
let rank = (chars[1] as u8 - b'1') as u8;
|
||||||
let sq_index = rank * 8 + file;
|
let sq_index = rank * 8 + file;
|
||||||
// This is unsafe, but assumes the FEN is valid
|
|
||||||
Some(unsafe { mem::transmute::<u8, Square>(sq_index) })
|
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 all_occupied = occupied[Color::White as usize] | occupied[Color::Black as usize];
|
||||||
let empty_squares = !all_occupied;
|
let empty_squares = !all_occupied;
|
||||||
|
|
||||||
Board {
|
// Create mutable board with empty hash
|
||||||
|
let mut board = Board {
|
||||||
side_to_move,
|
side_to_move,
|
||||||
pieces,
|
pieces,
|
||||||
pieces_on_squares, // <-- ADDED
|
pieces_on_squares,
|
||||||
occupied,
|
occupied,
|
||||||
all_occupied,
|
all_occupied,
|
||||||
empty_squares,
|
empty_squares,
|
||||||
|
|
@ -150,7 +150,13 @@ impl Board {
|
||||||
en_passant_target,
|
en_passant_target,
|
||||||
halfmove_clock,
|
halfmove_clock,
|
||||||
fullmove_number,
|
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.
|
/// 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::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};
|
||||||
|
use crate::tt::{TranspositionTable, NodeType, TTEntry}; // Import TT types
|
||||||
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
|
// 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(
|
pub fn alpha_beta(
|
||||||
board: &mut Board,
|
board: &mut Board,
|
||||||
depth: u8,
|
depth: u8,
|
||||||
|
|
@ -25,29 +49,81 @@ 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
|
||||||
) -> (Option<Move>, i32) {
|
) -> (Option<Move>, i32) {
|
||||||
// Check for time usage every 4096 nodes to reduce system call overhead
|
// Check for time usage
|
||||||
if *nodes % 4096 == 0 {
|
if *nodes % 4096 == 0 {
|
||||||
if start_time.elapsed() > time_limit {
|
if start_time.elapsed() > time_limit {
|
||||||
// Return immediately. The return value here effectively signals an abort,
|
return (None, 0);
|
||||||
// but the engine must discard this result.
|
|
||||||
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 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 {
|
if depth == 0 {
|
||||||
return (None, evaluate_board_relative(board));
|
return (None, evaluate_board_relative(board));
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut list = MoveList::new();
|
let mut list = MoveList::new();
|
||||||
generate_pseudo_legal_moves(board, &mut list);
|
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);
|
let is_illegal = is_other_king_attacked(board);
|
||||||
if is_illegal {
|
if is_illegal {
|
||||||
board.undo_move(undo_mv);
|
board.undo_move(undo_mv);
|
||||||
|
|
@ -55,35 +131,28 @@ pub fn alpha_beta(
|
||||||
}
|
}
|
||||||
legal_moves_found = true;
|
legal_moves_found = true;
|
||||||
|
|
||||||
// Recursive call with negated and swapped alpha/beta
|
let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes, tt);
|
||||||
// Pass time parameters and node counter down
|
|
||||||
let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes);
|
|
||||||
|
|
||||||
// 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 {
|
if *nodes % 4096 == 0 && start_time.elapsed() > time_limit {
|
||||||
board.undo_move(undo_mv);
|
board.undo_move(undo_mv);
|
||||||
return (None, 0);
|
return (None, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
let current_score = -score;
|
let current_score = -score;
|
||||||
|
|
||||||
if current_score > best_score {
|
if current_score > best_score {
|
||||||
best_score = current_score;
|
best_score = current_score;
|
||||||
best_move = Some(*mv);
|
best_move = Some(mv);
|
||||||
}
|
}
|
||||||
|
|
||||||
board.undo_move(undo_mv);
|
board.undo_move(undo_mv);
|
||||||
|
|
||||||
// Alpha-Beta Pruning logic
|
|
||||||
if best_score > alpha {
|
if best_score > alpha {
|
||||||
alpha = best_score;
|
alpha = best_score;
|
||||||
}
|
}
|
||||||
|
|
||||||
if alpha >= beta {
|
if alpha >= beta {
|
||||||
break; // Beta cutoff (Pruning)
|
break; // Beta cutoff
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -91,10 +160,25 @@ pub fn alpha_beta(
|
||||||
if is_current_king_attacked(board) {
|
if is_current_king_attacked(board) {
|
||||||
return (None, -MATE_SCORE + (ply as i32));
|
return (None, -MATE_SCORE + (ply as i32));
|
||||||
} else {
|
} else {
|
||||||
// Stalemate
|
|
||||||
return (None, 0);
|
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)
|
(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