restructure

This commit is contained in:
Moritz 2025-11-19 22:05:37 +01:00
parent 2e5f6e3d7d
commit a93bfe258c
23 changed files with 83 additions and 307 deletions

View file

@ -1,6 +1,6 @@
use chess_engine::board::Board;
use criterion::{criterion_group, criterion_main, Criterion};
use chess_engine::eval::basic::evaluate_board;
use chess_engine::eval::evaluate_board;
use chess_engine::zobrist::init_zobrist;
fn run_eval_benchmark(c: &mut Criterion) {

Binary file not shown.

View file

@ -3,7 +3,6 @@ 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<Vec<Vec<String>>> {
let file = File::open(path)?;
@ -25,30 +24,25 @@ fn load_csv(path: &str) -> io::Result<Vec<Vec<String>>> {
fn main() {
init_zobrist();
let time_limit_ms = 1000_u64;
let time_limit = Duration::from_millis(time_limit_ms);
let mut total_tests: f32 = 0.0;
let mut correct_tests: f32 = 0.0;
let sts = load_csv("src/bin/stockfish_testsuite.csv").unwrap();
let mut engine = Engine::new("Yakari".to_string(), "EiSiMo".to_string());
// 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];
engine.setpos_fen(fen);
// Record start time
let start_time = Instant::now();
let result = engine.search(990_u64);
let result = engine.search(time_limit_ms-1);
// Calculate duration
let duration = start_time.elapsed();
// Check if the test exceeded the time limit
if duration > time_limit {
panic!(
"Test exceeded 1 second limit: {:?} for FEN: {}",

View file

@ -1,6 +1,6 @@
use crate::r#move::*;
use crate::square::Square;
use crate::zobrist::{self, zobrist_keys}; // Import Zobrist
use crate::zobrist::{self, zobrist_keys};
use std::ops::Not;
pub const CASTLING_WK_FLAG: u8 = 1;
@ -78,12 +78,10 @@ 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<Square>) -> usize {
match ep {
Some(sq) => (sq as usize) % 8,
@ -91,12 +89,10 @@ impl Board {
}
}
// 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 {
@ -107,14 +103,9 @@ impl Board {
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;
}
@ -122,8 +113,6 @@ impl Board {
self.hash = hash;
}
// --- Original methods with added hashing ---
fn rm_piece(
&mut self,
target_square: Square,
@ -132,7 +121,6 @@ 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];
@ -149,7 +137,6 @@ 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];
@ -169,12 +156,9 @@ 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();
@ -189,7 +173,6 @@ impl Board {
let mut opt_captured_piece: Option<PieceType> = None;
let mut opt_en_passant_target: Option<Square> = None;
// 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);
@ -262,7 +245,6 @@ impl Board {
_ => { panic!("unable to make_move: invalid flags: {}", flags); }
}
// 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];
@ -275,16 +257,12 @@ impl Board {
castling_right_wk | castling_right_wq | castling_right_bk | castling_right_bq;
self.castling_rights = self.castling_rights & new_castling_rights;
// 6. Update en passant target
self.en_passant_target = opt_en_passant_target;
// 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;
@ -292,13 +270,10 @@ 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 fullmove
self.fullmove_number += self.side_to_move as u16;
// 9. Flip side
self.side_to_move = !self.side_to_move;
// 10. Return Undo
UndoMove::new(
mv,
opt_captured_piece,
@ -311,35 +286,26 @@ impl Board {
pub fn undo_move(&mut self, undo_info: UndoMove) {
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;
// 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
self.fullmove_number -= self.side_to_move as u16;
// 4. Extract move data
let mv = undo_info.mv;
let from = mv.get_from();
let to = mv.get_to();
let flags = mv.get_flags();
// 5. Reverse pieces (helpers will update hash automatically)
match flags {
MOVE_FLAG_QUIET => {
self.move_piece(to, from, self.side_to_move);

View file

@ -1,6 +1,6 @@
use crate::board::Board;
use crate::r#move::Move;
use crate::search::alpha_beta::alpha_beta;
use crate::search::alpha_beta;
use crate::tt::TranspositionTable; // Import TT
use std::time::{Instant, Duration};
@ -42,14 +42,8 @@ impl Engine {
let start_time = Instant::now();
let time_limit = Duration::from_millis(time_limit_ms);
// 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,
@ -64,7 +58,6 @@ impl Engine {
let mut depth = 2;
// Iterative Deepening
while start_time.elapsed() < time_limit {
let (new_move, new_score) = alpha_beta(
&mut self.board,

View file

@ -1,5 +1,5 @@
use crate::board::*;
use crate::eval::piece_square_tables::PSQT;
use crate::psqt::PSQT;
pub fn evaluate_board(board: &Board) -> i32 {
let mut mg_score = 0_i32;
@ -7,28 +7,21 @@ pub fn evaluate_board(board: &Board) -> i32 {
let mut phase = 0_i32;
// We use a macro to force loop unrolling.
// This enables the compiler to use constant offsets for PSQT access
// instead of calculating addresses at runtime based on a loop variable.
macro_rules! score_piece {
($pt:expr, $phase_weight:expr) => {
// --- WHITE ---
let mut pieces = board.pieces[$pt][Color::White as usize];
if pieces > 0 {
// Phase calculation uses count_ones (POPPCNT) which is very fast
phase += (pieces.count_ones() as i32) * $phase_weight;
while pieces > 0 {
let sq = pieces.trailing_zeros() as usize;
pieces &= pieces - 1; // Clear LS1B
// Material is already baked into PSQT, so we just add the table value
// Since $pt is a const literal here, this compiles to a direct memory access
mg_score += PSQT[$pt][Color::White as usize][0][sq];
eg_score += PSQT[$pt][Color::White as usize][1][sq];
}
}
// --- BLACK ---
let mut pieces = board.pieces[$pt][Color::Black as usize];
if pieces > 0 {
phase += (pieces.count_ones() as i32) * $phase_weight;
@ -44,21 +37,13 @@ pub fn evaluate_board(board: &Board) -> i32 {
};
}
// Explicitly unrolled execution order
// Pawn (0), Weight 0
score_piece!(0, 0);
// Knight (1), Weight 1
score_piece!(1, 1);
// Bishop (2), Weight 1
score_piece!(2, 1);
// Rook (3), Weight 2
score_piece!(3, 2);
// Queen (4), Weight 4
score_piece!(4, 4);
// King (5), Weight 0 (Phase doesn't change)
score_piece!(5, 0);
// Tapered Evaluation
let phase = phase.min(24);
let mg_phase = phase;
let eg_phase = 24 - phase;

View file

@ -1,2 +0,0 @@
pub mod basic;
pub mod piece_square_tables;

View file

@ -5,8 +5,9 @@ pub mod movegen;
pub mod eval;
pub mod display;
pub mod parsing;
pub mod search;
pub mod engine;
pub mod uci;
pub mod tt;
pub mod zobrist;
pub mod search;
pub mod psqt;

View file

@ -87,7 +87,6 @@ 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);
}

View file

@ -7,15 +7,12 @@ pub fn is_current_king_attacked(board: &Board) -> bool {
is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move)
}
/// Checks if the king of the side that is NOT to move is in check
pub fn is_other_king_attacked(board: &Board) -> bool {
let king = board.pieces[PieceType::King as usize][!board.side_to_move as usize];
is_square_attacked(board, SQUARES[king.trailing_zeros() as usize], board.side_to_move)
}
// TODO check if castle is legal (squares in between)
/// calculate if a square on the board is attacked by a color
pub fn is_square_attacked(board: &Board, square: Square, color: Color) -> bool {
// 1. Non sliding
// 1.1 Pawn

View file

@ -1,17 +1,18 @@
// FILENAME: non_sliders.rs
use crate::board::*;
use crate::movegen::legal_check::is_square_attacked;
use crate::r#move::*;
use crate::square::*;
use super::tables::{KING_ATTACKS, KNIGHT_ATTACKS};
use crate::board::*;
use crate::r#move::*;
use crate::movegen::legal_check::is_square_attacked;
use crate::square::*;
pub fn generate_knight_moves(board: &Board, list: &mut MoveList) {
let enemy_occupied = board.occupied[!board.side_to_move as usize];
let mut friendly_knights = board.pieces[PieceType::Knight as usize][board.side_to_move as usize];
let mut friendly_knights =
board.pieces[PieceType::Knight as usize][board.side_to_move as usize];
while friendly_knights != 0 {
let square = SQUARES[friendly_knights.trailing_zeros() as usize];
let mut attacks = KNIGHT_ATTACKS[square as usize] & !board.occupied[board.side_to_move as usize];
let mut attacks =
KNIGHT_ATTACKS[square as usize] & !board.occupied[board.side_to_move as usize];
while attacks != 0 {
let attack = SQUARES[attacks.trailing_zeros() as usize];
@ -58,7 +59,6 @@ pub fn generate_king_moves(board: &Board, list: &mut MoveList) {
attacks &= attacks - 1;
}
// TODO Optimize the is attacked testing on castling
// 2. Generate castling king moves
if board.side_to_move == Color::White {
// King must not be in check to castle
@ -67,46 +67,39 @@ pub fn generate_king_moves(board: &Board, list: &mut MoveList) {
}
// Kingside (OO)
if (board.castling_rights & CASTLING_WK_FLAG) != 0 {
if (board.all_occupied & CASTLING_WK_MASK) == 0 {
// Check F1 (path). G1 (landing) is checked by perft function.
if !is_square_attacked(board, Square::F1, Color::Black) {
if (board.castling_rights & CASTLING_WK_FLAG) != 0
&& (board.all_occupied & CASTLING_WK_MASK) == 0
&& !is_square_attacked(board, Square::F1, Color::Black) {
list.push(Move::new(Square::E1, Square::G1, MOVE_FLAG_WK_CASTLE));
}
}
}
// Queenside (OOO)
if (board.castling_rights & CASTLING_WQ_FLAG) != 0 {
if (board.all_occupied & CASTLING_WQ_MASK) == 0 {
// Check D1 (path). C1 (landing) is checked by perft function. B1 is irrelevant.
if !is_square_attacked(board, Square::D1, Color::Black) {
list.push(Move::new(Square::E1, Square::C1, MOVE_FLAG_WQ_CASTLE));
}
}
if (board.castling_rights & CASTLING_WQ_FLAG) != 0
&& (board.all_occupied & CASTLING_WQ_MASK) == 0
&& !is_square_attacked(board, Square::D1, Color::Black)
{
list.push(Move::new(Square::E1, Square::C1, MOVE_FLAG_WQ_CASTLE));
}
} else { // Black
} else {
// Black
// King must not be in check to castle
if is_square_attacked(board, Square::E8, Color::White) {
return;
}
// Kingside (OO)
if (board.castling_rights & CASTLING_BK_FLAG) != 0 {
if (board.all_occupied & CASTLING_BK_MASK) == 0 {
// Check F8 (path). G8 (landing) is checked by perft function.
if !is_square_attacked(board, Square::F8, Color::White) {
list.push(Move::new(Square::E8, Square::G8, MOVE_FLAG_BK_CASTLE));
}
}
if (board.castling_rights & CASTLING_BK_FLAG) != 0
&& (board.all_occupied & CASTLING_BK_MASK) == 0
&& !is_square_attacked(board, Square::F8, Color::White)
{
list.push(Move::new(Square::E8, Square::G8, MOVE_FLAG_BK_CASTLE));
}
// Queenside (OOO)
if (board.castling_rights & CASTLING_BQ_FLAG) != 0 {
if (board.all_occupied & CASTLING_BQ_MASK) == 0 {
// Check D8 (path). C8 (landing) is checked by perft function. B8 is irrelevant.
if !is_square_attacked(board, Square::D8, Color::White) {
list.push(Move::new(Square::E8, Square::C8, MOVE_FLAG_BQ_CASTLE));
}
}
if (board.castling_rights & CASTLING_BQ_FLAG) != 0
&& (board.all_occupied & CASTLING_BQ_MASK) == 0
&& !is_square_attacked(board, Square::D8, Color::White)
{
list.push(Move::new(Square::E8, Square::C8, MOVE_FLAG_BQ_CASTLE));
}
}
}

View file

@ -93,7 +93,6 @@ pub fn generate_pawn_moves(board: &Board, list: &mut MoveList) {
// 1.4.2 Capturing Promotion
// 1.4.2.1 A-side capturing promotion
// CORRECTED: Use PAWN_A_SIDE_CAPTURE_PROMOTION_MASK_WITHE (excludes A-file)
let mut promotion_targets_a_side_capture = ((friendly_pawns & PAWN_A_SIDE_CAPTURE_PROMOTION_MASK_WITHE) << 7) & board.occupied[1];
while promotion_targets_a_side_capture > 0 {
let to = SQUARES[promotion_targets_a_side_capture.trailing_zeros() as usize];

View file

@ -1,5 +1,5 @@
use crate::board::*;
use crate::r#move::{Move, MoveList, MOVE_FLAG_CAPTURE, MOVE_FLAG_QUIET};
use crate::r#move::*;
use crate::square::SQUARES;
use super::tables::*;

View file

@ -563,15 +563,11 @@ pub const RELEVANT_BITS_BISHOP: [u8; 64] = [
static ROOK_ATTACKS: OnceLock<Box<[[u64; 4096]; 64]>> = OnceLock::new();
static BISHOP_ATTACKS: OnceLock<Box<[[u64; 512]; 64]>> = OnceLock::new();
// HILFSFUNKTION: Berechnet Turmzüge "langsam"
// Diese Funktion wird nur beim "Backen" der Tabellen verwendet.
fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
let mut attacks = 0_u64;
let rank = square / 8;
let file = square % 8;
// 1. Nach Norden (rank +)
for r in (rank + 1)..=7 {
let target_sq = r * 8 + file;
attacks |= 1_u64 << target_sq;
@ -580,7 +576,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
}
}
// 2. Nach Süden (rank -)
for r in (0..rank).rev() {
let target_sq = r * 8 + file;
attacks |= 1_u64 << target_sq;
@ -589,7 +584,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
}
}
// 3. Nach Osten (file +)
for f in (file + 1)..=7 {
let target_sq = rank * 8 + f;
attacks |= 1_u64 << target_sq;
@ -598,7 +592,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
}
}
// 4. Nach Westen (file -)
for f in (0..file).rev() {
let target_sq = rank * 8 + f;
attacks |= 1_u64 << target_sq;
@ -611,7 +604,6 @@ fn calculate_rook_attacks_slowly(square: usize, blockers: u64) -> u64 {
}
fn generate_final_rook_attacks() -> Box<[[u64; 4096]; 64]> {
// Heap-Allokation (Dein Code war hier korrekt)
let mut v: Vec<[u64; 4096]> = Vec::with_capacity(64);
for _ in 0..64 {
v.push([0_u64; 4096]);
@ -620,32 +612,18 @@ fn generate_final_rook_attacks() -> Box<[[u64; 4096]; 64]> {
panic!("Vec to Box conversion failed.");
});
// Haupt-Back-Schleife
for square_index in 0_usize..=63_usize {
let premask = PREMASKS_ROOK[square_index]; // [deine Quelle]
let magic = MAGICS_ROOK[square_index]; // [deine Quelle]
// ACHTUNG: Hier war ein Fehler in deinem Code, du hattest BISHOP statt ROOK
let relevant_bits = RELEVANT_BITS_ROOK[square_index]; // [deine Quelle]
let premask = PREMASKS_ROOK[square_index];
let magic = MAGICS_ROOK[square_index];
let relevant_bits = RELEVANT_BITS_ROOK[square_index];
let shift = 64 - relevant_bits;
// Schleife durch alle 2^n Blocker-Kombinationen
let mut blocker_combination = 0_u64;
loop {
// ---- HIER IST DIE KORREKTE LOGIK ----
// 1. Berechne die "echten" Züge für diese Blocker-Kombination (der langsame Weg)
let attack_squares = calculate_rook_attacks_slowly(square_index, blocker_combination);
// 2. Berechne den "magischen" Speicherort
let magic_index = (blocker_combination.wrapping_mul(magic)) >> shift;
let magic_index_as_usize = magic_index as usize;
// 3. Speichere die echten Züge an diesem magischen Ort
final_rook_attacks[square_index][magic_index_as_usize] = attack_squares;
// ---- ENDE DER LOGIK ----
// Gehe zur nächsten Blocker-Kombination (Dein Code war hier korrekt)
if blocker_combination == premask {
break;
}
@ -657,39 +635,31 @@ fn generate_final_rook_attacks() -> Box<[[u64; 4096]; 64]> {
}
pub fn get_rook_attacks() -> &'static Box<[[u64; 4096]; 64]> {
// get_or_init stellt sicher, dass generate_final_rook_attacks()
// nur beim allerersten Aufruf ausgeführt wird.
// Alle anderen Aufrufe geben sofort die fertige Tabelle zurück.
ROOK_ATTACKS.get_or_init(generate_final_rook_attacks)
}
// HILFSFUNKTION: Berechnet Läuferzüge "langsam"
// Diese Funktion wird nur beim "Backen" der Tabellen verwendet.
fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
let mut attacks = 0_u64;
let rank = square / 8;
let file = square % 8;
// Temporäre Rank/File-Iteratoren
let (mut r, mut f);
// 1. Nach Nord-Osten (rank+, file+)
r = rank + 1;
f = file + 1;
while r <= 7 && f <= 7 {
let target_sq = r * 8 + f;
attacks |= 1_u64 << target_sq;
if (blockers >> target_sq) & 1 == 1 {
break; // Wir treffen einen Blocker, stopp
break;
}
r += 1;
f += 1;
}
// 2. Nach Süd-Osten (rank-, file+)
r = rank; // Start bei rank, da 0..rank fehlschlägt wenn rank = 0
r = rank;
f = file + 1;
while r > 0 && f <= 7 { // r > 0 (da r = rank-1 in der 1. Iteration)
while r > 0 && f <= 7 {
r -= 1;
let target_sq = r * 8 + f;
attacks |= 1_u64 << target_sq;
@ -699,7 +669,6 @@ fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
f += 1;
}
// 3. Nach Süd-Westen (rank-, file-)
r = rank;
f = file;
while r > 0 && f > 0 { // r > 0 und f > 0
@ -712,7 +681,6 @@ fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
}
}
// 4. Nach Nord-Westen (rank+, file-)
r = rank + 1;
f = file;
while r <= 7 && f > 0 { // f > 0
@ -729,9 +697,6 @@ fn calculate_bishop_attacks_slowly(square: usize, blockers: u64) -> u64 {
}
fn generate_final_bishop_attacks() -> Box<[[u64; 512]; 64]> {
// Heap-Allokation
// (Array ist kleiner: 512 statt 4096. Stack Overflow wäre hier unwahrscheinlich,
// aber wir bleiben konsistent mit der Turm-Logik.)
let mut v: Vec<[u64; 512]> = Vec::with_capacity(64);
for _ in 0..64 {
v.push([0_u64; 512]);
@ -740,30 +705,21 @@ fn generate_final_bishop_attacks() -> Box<[[u64; 512]; 64]> {
panic!("Vec to Box conversion failed for bishops.");
});
// Haupt-Back-Schleife
for square_index in 0_usize..=63_usize {
// Verwende die BISHOP-Konstanten aus deiner tables.rs
let premask = PREMASKS_BISHOP[square_index];
let magic = MAGICS_BISHOP[square_index];
let relevant_bits = RELEVANT_BITS_BISHOP[square_index];
let shift = 64 - relevant_bits;
// Schleife durch alle 2^n Blocker-Kombinationen
let mut blocker_combination = 0_u64;
loop {
// 1. Berechne die "echten" Züge für diese Blocker-Kombination (der langsame Weg)
let attack_squares = calculate_bishop_attacks_slowly(square_index, blocker_combination);
// 2. Berechne den "magischen" Speicherort
let magic_index = (blocker_combination.wrapping_mul(magic)) >> shift;
let magic_index_as_usize = magic_index as usize;
// 3. Speichere die echten Züge an diesem magischen Ort
// (Stelle sicher, dass magic_index_as_usize < 512 ist,
// was durch korrekte Magics garantiert wird)
final_bishop_attacks[square_index][magic_index_as_usize] = attack_squares;
// Gehe zur nächsten Blocker-Kombination
if blocker_combination == premask {
break;
}
@ -775,8 +731,5 @@ fn generate_final_bishop_attacks() -> Box<[[u64; 512]; 64]> {
}
pub fn get_bishop_attacks() -> &'static Box<[[u64; 512]; 64]> {
// get_or_init stellt sicher, dass generate_final_bishop_attacks()
// nur beim allerersten Aufruf ausgeführt wird.
// Alle anderen Aufrufe geben sofort die fertige Tabelle zurück.
BISHOP_ATTACKS.get_or_init(generate_final_bishop_attacks)
}

View file

@ -4,12 +4,9 @@ use crate::r#move::*;
use crate::square::Square;
impl Board {
/// Creates a new Board instance from a FEN string.
/// Assumes the FEN string is valid.
pub fn from_fen(fen: &str) -> Self {
let mut parts = fen.split_whitespace();
// Initialisiere die Arrays
let mut pieces = [[0u64; 2]; 6];
let mut occupied = [0u64; 2];
let mut pieces_on_squares = [None; 64];
@ -20,7 +17,7 @@ impl Board {
let mut file = 0;
for c in placement.chars() {
if c.is_digit(10) {
if c.is_ascii_digit() {
file += c.to_digit(10).unwrap_or(0) as usize;
} else if c == '/' {
rank -= 1;
@ -122,8 +119,8 @@ impl Board {
"-" => None,
sq_str => {
let chars: Vec<char> = sq_str.chars().collect();
let file = (chars[0] as u8 - b'a') as u8;
let rank = (chars[1] as u8 - b'1') as u8;
let file = chars[0] as u8 - b'a';
let rank = chars[1] as u8 - b'1';
let sq_index = rank * 8 + file;
Some(unsafe { mem::transmute::<u8, Square>(sq_index) })
}
@ -223,8 +220,8 @@ impl Board {
fen.push(' ');
if let Some(sq) = self.en_passant_target {
let sq_index = sq as u8;
let file = (sq_index % 8) as u8;
let rank = (sq_index / 8) as u8;
let file = sq_index % 8;
let rank = sq_index / 8;
fen.push((b'a' + file) as char);
fen.push((b'1' + rank) as char);
} else {
@ -254,8 +251,8 @@ impl Move {
/// Converts algebraic notation (e.g., "a1") to a Square.
/// Assumes valid input.
fn alg_to_square(alg: &str) -> Square {
let file = (alg.as_bytes()[0] - b'a') as u8;
let rank = (alg.as_bytes()[1] - b'1') as u8;
let file = alg.as_bytes()[0] - b'a';
let rank = alg.as_bytes()[1] - b'1';
let sq_index = rank * 8 + file;
// This is unsafe, but we assume valid algebraic notation
unsafe { mem::transmute::<u8, Square>(sq_index) }

View file

@ -1,4 +1,3 @@
// --- Material Values (PeSTO Standard) ---
const MG_PAWN_VAL: i32 = 82;
const EG_PAWN_VAL: i32 = 94;
const MG_KNIGHT_VAL: i32 = 337;
@ -9,7 +8,7 @@ const MG_ROOK_VAL: i32 = 477;
const EG_ROOK_VAL: i32 = 512;
const MG_QUEEN_VAL: i32 = 1025;
const EG_QUEEN_VAL: i32 = 936;
const MG_KING_VAL: i32 = 0; // King usually has no material value in eval summation (captured = game over)
const MG_KING_VAL: i32 = 0;
const EG_KING_VAL: i32 = 0;
pub const MG_PAWN_TABLE: [i32; 64] = [

View file

@ -1,5 +1,5 @@
use crate::board::{Board, Color};
use crate::eval::basic::evaluate_board;
use crate::eval::evaluate_board;
use crate::movegen::generate_pseudo_legal_moves;
use crate::movegen::legal_check::*;
use crate::r#move::{Move, MoveList};
@ -51,28 +51,20 @@ pub fn alpha_beta(
nodes: &mut u64,
tt: &mut TranspositionTable, // Added TT parameter
) -> (Option<Move>, i32) {
// Check for time usage
if *nodes % 4096 == 0 {
if start_time.elapsed() > time_limit {
return (None, 0);
}
if (*nodes).is_multiple_of(4096)
&& start_time.elapsed() > time_limit {
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)
if entry.bm.0 != 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);
@ -100,12 +92,7 @@ pub fn alpha_beta(
let mut list = MoveList::new();
generate_pseudo_legal_moves(board, &mut list);
// -----------------------
// 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);
@ -117,13 +104,12 @@ pub fn alpha_beta(
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
let alpha_orig = alpha;
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);
@ -133,7 +119,7 @@ pub fn alpha_beta(
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 {
if (*nodes).is_multiple_of(4096) && start_time.elapsed() > time_limit {
board.undo_move(undo_mv);
return (None, 0);
}
@ -152,7 +138,7 @@ pub fn alpha_beta(
}
if alpha >= beta {
break; // Beta cutoff
break;
}
}
@ -164,18 +150,15 @@ pub fn alpha_beta(
}
}
// -----------------------
// 2. TT STORE
// -----------------------
let node_type = if best_score <= alpha_orig {
NodeType::Alpha // We didn't improve alpha (Fail Low) -> Upper Bound
NodeType::Alpha
} else if best_score >= beta {
NodeType::Beta // We caused a cutoff (Fail High) -> Lower Bound
NodeType::Beta
} else {
NodeType::Exact // We found a score between alpha and beta
NodeType::Exact
};
let save_move = best_move.unwrap_or(Move(0)); // Use dummy 0 if no best move
let save_move = best_move.unwrap_or(Move(0));
let save_score = score_to_tt(best_score, ply);
tt.store(tt_key, save_score, depth, node_type, save_move);

View file

@ -1,63 +0,0 @@
use crate::board::{Board, Color}; // <-- Assuming you have a Color enum (e.g., Color::White, Color::Black)
use crate::eval::basic::evaluate_board;
use crate::movegen::generate_pseudo_legal_moves;
use crate::movegen::legal_check::is_other_king_attacked;
use crate::r#move::{Move, MoveList};
// A score high enough to be > any material eval, but low enough to not overflow when adding ply
const MATE_SCORE: i32 = 1_000_000;
fn evaluate_board_relative(board: &Board) -> i32 {
let static_eval = evaluate_board(board);
match board.side_to_move {
Color::White => static_eval,
Color::Black => -static_eval,
}
}
pub fn minimax(board: &mut Board, depth: u8, ply: u8) -> (Option<Move>, i32) {
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; // Start with the worst possible score
let mut legal_moves_found = false;
for mv in list.iter() {
let undo_mv = board.make_move(*mv);
let is_illegal = is_other_king_attacked(board);
if is_illegal {
board.undo_move(undo_mv);
continue;
}
legal_moves_found = true;
// Recursive call, incrementing ply
let (_, score) = minimax(board, depth - 1, ply + 1);
let current_score = -score;
if current_score > best_score {
best_score = current_score;
best_move = Some(*mv);
}
board.undo_move(undo_mv);
}
if !legal_moves_found {
if is_other_king_attacked(board) {
// Checkmate
// The score is *less* negative the *longer* it takes to be mated (higher ply)
// This translates to a *higher* score for the winner for a *faster* mate
return (None, -MATE_SCORE + (ply as i32));
} else {
// Stalemate
return (None, 0);
}
}
(best_move, best_score)
}

View file

@ -1,2 +0,0 @@
pub mod minimax;
pub mod alpha_beta;

View file

@ -1,6 +1,4 @@
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)]

View file

@ -1,11 +1,8 @@
// uci.rs
use std::io::{self, BufRead};
use crate::engine::Engine;
pub fn uci_mainloop(engine: &mut Engine) {
loop {
// Start the main UCI loop
for line in io::stdin().lock().lines() {
let input = line.unwrap_or_else(|_| "quit".to_string());
let tokens: Vec<&str> = input.split_whitespace().collect();
@ -41,7 +38,6 @@ pub fn uci_mainloop(engine: &mut Engine) {
}
}
"go" => {
// TODO add a lot functionality
println!("bestmove {}", engine.search(1000_u64));
}
"stop" => {

View file

@ -1,8 +1,6 @@
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,
}
@ -24,17 +22,15 @@ impl Xorshift {
#[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 pieces: [[u64; 64]; 12],
pub castling: [u64; 16],
pub en_passant: [u64; 9],
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;
}
@ -42,20 +38,20 @@ pub fn init_zobrist() {
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();
for piece_squares in pieces.iter_mut() {
for square_key in piece_squares.iter_mut() {
*square_key = rng.next();
}
}
let mut castling = [0; 16];
for i in 0..16 {
castling[i] = rng.next();
for c in castling.iter_mut() {
*c = rng.next();
}
let mut en_passant = [0; 9];
for i in 0..9 {
en_passant[i] = rng.next();
for ep in en_passant.iter_mut() {
*ep = rng.next();
}
let side_to_move = rng.next();
@ -67,16 +63,13 @@ pub fn init_zobrist() {
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,

View file

@ -13,14 +13,11 @@ fn count_legal_moves_recursive(board: &mut Board, depth: u8) -> u64 {
let mut leaf_nodes = 0_u64;
for mv in list.iter() {
// Store the undo info when making the move
let undo_info = board.make_move(*mv);
if !is_other_king_attacked(board) {
leaf_nodes += count_legal_moves_recursive(board, depth - 1);
}
// Undo the move to restore the board state for the next iteration
board.undo_move(undo_info);
}
leaf_nodes