implemented transposition table

This commit is contained in:
Moritz 2025-11-19 10:40:41 +01:00
parent ea9419d7e0
commit 42816a6939
13 changed files with 464 additions and 91 deletions

View file

@ -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(|| {

View file

@ -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.

View file

@ -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];

View file

@ -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);
} }

View file

@ -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()
} }
} }
} }

View file

@ -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;

View file

@ -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);
} }

View file

@ -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
} }

View file

@ -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.

View file

@ -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
View 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
View 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
}