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 9a26e1e..392253a 100644 Binary files a/progress_tracking/progress.xlsx and b/progress_tracking/progress.xlsx differ 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