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

@ -3,6 +3,7 @@ use crate::eval::basic::evaluate_board;
use crate::movegen::generate_pseudo_legal_moves;
use crate::movegen::legal_check::*;
use crate::r#move::{Move, MoveList};
use crate::tt::{TranspositionTable, NodeType, TTEntry}; // Import TT types
use std::time::{Instant, Duration};
// A score high enough to be > any material eval, but low enough to not overflow when adding ply
@ -16,6 +17,29 @@ fn evaluate_board_relative(board: &Board) -> i32 {
}
}
// Helper to adjust mate scores for the TT.
// TT stores "pure" scores, independent of ply.
// Search uses "mated in X" relative to current ply.
fn score_to_tt(score: i32, ply: u8) -> i32 {
if score > MATE_SCORE - 1000 {
score + (ply as i32)
} else if score < -MATE_SCORE + 1000 {
score - (ply as i32)
} else {
score
}
}
fn score_from_tt(score: i32, ply: u8) -> i32 {
if score > MATE_SCORE - 1000 {
score - (ply as i32)
} else if score < -MATE_SCORE + 1000 {
score + (ply as i32)
} else {
score
}
}
pub fn alpha_beta(
board: &mut Board,
depth: u8,
@ -25,29 +49,81 @@ pub fn alpha_beta(
start_time: Instant,
time_limit: Duration,
nodes: &mut u64,
tt: &mut TranspositionTable, // Added TT parameter
) -> (Option<Move>, i32) {
// Check for time usage every 4096 nodes to reduce system call overhead
// Check for time usage
if *nodes % 4096 == 0 {
if start_time.elapsed() > time_limit {
// Return immediately. The return value here effectively signals an abort,
// but the engine must discard this result.
return (None, 0);
return (None, 0);
}
}
*nodes += 1;
// -----------------------
// 1. TT PROBE
// -----------------------
// We assume board.hash holds the current Zobrist key (u64)
let tt_key = board.hash;
let mut tt_move: Option<Move> = None;
if let Some(entry) = tt.probe(tt_key) {
// We remember the move from TT to sort it first later
if entry.bm.0 != 0 { // Check if move is valid (not 0)
tt_move = Some(entry.bm);
}
// Can we use the score for a cutoff?
if entry.depth >= depth {
let tt_score = score_from_tt(entry.score, ply);
match entry.node_type {
NodeType::Exact => return (Some(entry.bm), tt_score),
NodeType::Alpha => {
if tt_score <= alpha {
return (Some(entry.bm), tt_score);
}
}
NodeType::Beta => {
if tt_score >= beta {
return (Some(entry.bm), tt_score);
}
}
_ => {}
}
}
}
if depth == 0 {
return (None, evaluate_board_relative(board));
}
let mut list = MoveList::new();
generate_pseudo_legal_moves(board, &mut list);
let mut best_move: Option<Move> = None;
let mut best_score: i32 = -i32::MAX; // This is our local "worst case"
let mut legal_moves_found = false;
for mv in list.iter() {
let undo_mv = board.make_move(*mv);
// -----------------------
// MOVE ORDERING (TT Move First)
// -----------------------
// If we have a move from TT, we want to search it first!
if let Some(tm) = tt_move {
// Find the move in the list and swap it to the front (index 0)
for i in 0..list.len() {
if list[i] == tm {
list.swap(0, i);
break;
}
}
}
let mut best_move: Option<Move> = None;
let mut best_score: i32 = -i32::MAX;
let mut legal_moves_found = false;
let alpha_orig = alpha; // Save original alpha to determine NodeType later
for i in 0..list.len() {
let mv = list[i];
let undo_mv = board.make_move(mv);
// Optimization: Check legality locally if possible, but for now rely on King check
let is_illegal = is_other_king_attacked(board);
if is_illegal {
board.undo_move(undo_mv);
@ -55,35 +131,28 @@ pub fn alpha_beta(
}
legal_moves_found = true;
// Recursive call with negated and swapped alpha/beta
// Pass time parameters and node counter down
let (_, score) = alpha_beta(board, depth - 1, ply + 1, -beta, -alpha, start_time, time_limit, nodes);
// 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)
}
}