From 42816a6939466fc5b68d356933e4ffa511d959d2 Mon Sep 17 00:00:00 2001 From: Moritz Date: Wed, 19 Nov 2025 10:40:41 +0100 Subject: [PATCH] implemented transposition table --- benches/eval.rs | 2 + benches/perft.rs | 2 + progress_tracking/progress.xlsx | Bin 9504 -> 5995 bytes src/bin/suite.rs | 3 + src/board.rs | 135 +++++++++++++++++++++++++------- src/engine.rs | 62 +++++++++------ src/lib.rs | 2 + src/main.rs | 2 + src/move.rs | 5 ++ src/parsing.rs | 28 ++++--- src/search/alpha_beta.rs | 132 +++++++++++++++++++++++++------ src/tt.rs | 96 +++++++++++++++++++++++ src/zobrist.rs | 86 ++++++++++++++++++++ 13 files changed, 464 insertions(+), 91 deletions(-) create mode 100644 src/tt.rs create mode 100644 src/zobrist.rs diff --git a/benches/eval.rs b/benches/eval.rs index 8664abd..1e977f4 100644 --- a/benches/eval.rs +++ b/benches/eval.rs @@ -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(|| { diff --git a/benches/perft.rs b/benches/perft.rs index 016f730..673b898 100644 --- a/benches/perft.rs +++ b/benches/perft.rs @@ -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| { diff --git a/progress_tracking/progress.xlsx b/progress_tracking/progress.xlsx index 9a26e1e989a11f5feab6f54d44779ff462670688..392253a0caa3d7ae0288da356dab9f2833f600c2 100644 GIT binary patch literal 5995 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 NUHD;&hzNdP{U5RbovHu; literal 9504 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 diff --git a/src/bin/suite.rs b/src/bin/suite.rs index 997acab..d7ab2ad 100644 --- a/src/bin/suite.rs +++ b/src/bin/suite.rs @@ -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>> { @@ -23,6 +24,7 @@ fn load_csv(path: &str) -> io::Result>> { } 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]; diff --git a/src/board.rs b/src/board.rs index e0af250..a94242c 100644 --- a/src/board.rs +++ b/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; 64], // <-- ADDED + pub pieces_on_squares: [Option; 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) -> 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 = 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 = None; let mut opt_en_passant_target: Option = 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); } diff --git a/src/engine.rs b/src/engine.rs index f51268c..00e0e98 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -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, } } @@ -35,54 +42,57 @@ impl Engine { pub fn search(&mut self, time_limit_ms: u64) -> String { 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, - 0, - -i32::MAX, - i32::MAX, - start_time, - time_limit, - &mut nodes + &mut self.board, + 1, + 0, + -i32::MAX, + i32::MAX, + start_time, + time_limit, + &mut nodes, + &mut self.tt ); - + let mut depth = 2; // Iterative Deepening while start_time.elapsed() < time_limit { let (new_move, new_score) = alpha_beta( - &mut self.board, - depth, - 0, - -i32::MAX, - i32::MAX, - start_time, - time_limit, - &mut nodes + &mut self.board, + depth, + 0, + -i32::MAX, + i32::MAX, + start_time, + time_limit, + &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; _score = new_score; - + depth += 1; } if let Some(mv) = opt_move { mv.to_algebraic() } else { - // UCI format for no legal moves (checkmate/stalemate) "null".to_string() } } -} +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index bb08255..1e6d087 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -8,3 +8,5 @@ pub mod parsing; pub mod search; pub mod engine; pub mod uci; +pub mod tt; +pub mod zobrist; diff --git a/src/main.rs b/src/main.rs index bd43db4..341e205 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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); } diff --git a/src/move.rs b/src/move.rs index ed72ac5..be737fd 100644 --- a/src/move.rs +++ b/src/move.rs @@ -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 } diff --git a/src/parsing.rs b/src/parsing.rs index 6f20a09..1d40200 100644 --- a/src/parsing.rs +++ b/src/parsing.rs @@ -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::(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. diff --git a/src/search/alpha_beta.rs b/src/search/alpha_beta.rs index 7562d3a..1f2305c 100644 --- a/src/search/alpha_beta.rs +++ b/src/search/alpha_beta.rs @@ -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, 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); + 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 = 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 = 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 = 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); - - // 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: + let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes, tt); + if *nodes % 4096 == 0 && start_time.elapsed() > time_limit { - 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) -} +} \ No newline at end of file diff --git a/src/tt.rs b/src/tt.rs new file mode 100644 index 0000000..fe71214 --- /dev/null +++ b/src/tt.rs @@ -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, + 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::(); + // 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 { + // 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; + } + } +} \ No newline at end of file diff --git a/src/zobrist.rs b/src/zobrist.rs new file mode 100644 index 0000000..a33f807 --- /dev/null +++ b/src/zobrist.rs @@ -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 = 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 +} \ No newline at end of file