From e817229d12d26cdd5ce57576c33741217f633055 Mon Sep 17 00:00:00 2001 From: stefiosif Date: Tue, 3 Sep 2024 20:35:08 +0300 Subject: [PATCH] Convert minimax to negamax and implement alpha-beta pruning --- src/evaluation/evaluation.rs | 42 ++++++++-------- src/interface/uci.rs | 19 ++++++-- src/search/minimax.rs | 87 --------------------------------- src/search/mod.rs | 2 +- src/search/negamax.rs | 94 ++++++++++++++++++++++++++++++++++++ 5 files changed, 130 insertions(+), 114 deletions(-) delete mode 100644 src/search/minimax.rs create mode 100644 src/search/negamax.rs diff --git a/src/evaluation/evaluation.rs b/src/evaluation/evaluation.rs index cbe01cf..aca57d2 100644 --- a/src/evaluation/evaluation.rs +++ b/src/evaluation/evaluation.rs @@ -6,6 +6,9 @@ use crate::{ evaluation::psqt::{mirror_index, piece_square_score}, }; +pub const MAX_EVAL: i32 = 100000; +pub const MIN_EVAL: i32 = -100000; + const fn piece_score(piece_type: PieceType) -> i32 { match piece_type { PieceType::Pawn => 100, @@ -17,7 +20,7 @@ const fn piece_score(piece_type: PieceType) -> i32 { } } -fn evaluate_side(board: &Board, color: Color) -> i32 { +fn evaluate_side_for(board: &Board, color: Color) -> i32 { let mut total_score = 0; let pieces = match color { Color::White => &board.white_pieces, @@ -42,23 +45,21 @@ fn evaluate_side(board: &Board, color: Color) -> i32 { total_score } -pub fn evaluate_position(board: &Board, color: Color) -> i32 { - let total_white_score = evaluate_side(board, Color::White); - let total_black_score = evaluate_side(board, Color::Black); - let evaluation = total_white_score - total_black_score; +pub fn evaluate_position(board: &Board) -> i32 { + let total_white_score = evaluate_side_for(board, Color::White); + let total_black_score = evaluate_side_for(board, Color::Black); - match color { - Color::White => evaluation, - Color::Black => -evaluation, - } + -(total_white_score - total_black_score) } #[cfg(test)] mod tests { use crate::{ - board::board::{Color, PieceType}, - board::fen::from_fen, - evaluation::evaluation::{evaluate_position, evaluate_side, piece_score}, + board::{ + board::{Color, PieceType}, + fen::from_fen, + }, + evaluation::evaluation::{evaluate_position, evaluate_side_for, piece_score}, }; const FEN_QUIET: [&str; 2] = [ @@ -79,17 +80,17 @@ mod tests { } #[test] - fn test_evaluate_side() -> Result<(), String> { + fn test_evaluate_side_for() -> Result<(), String> { let game = from_fen(FEN_QUIET[0])?; assert_eq!( - evaluate_side(&game.board, Color::White), - evaluate_side(&game.board, Color::Black) + evaluate_side_for(&game.board, Color::White), + evaluate_side_for(&game.board, Color::Black) ); let game_2 = from_fen(FEN_QUIET[1])?; - let evaluate_white = evaluate_side(&game_2.board, Color::White); - let evaluate_black = evaluate_side(&game_2.board, Color::Black); + let evaluate_white = evaluate_side_for(&game_2.board, Color::White); + let evaluate_black = evaluate_side_for(&game_2.board, Color::Black); assert_eq!(24005, evaluate_white); assert_eq!(23965, evaluate_black); @@ -102,14 +103,13 @@ mod tests { let game = from_fen(FEN_QUIET[0])?; assert_eq!( - evaluate_position(&game.board, Color::White), - evaluate_position(&game.board, Color::Black) + evaluate_position(&game.board), + evaluate_position(&game.board) ); let game_2 = from_fen(FEN_QUIET[1])?; - assert_eq!(40, evaluate_position(&game_2.board, Color::White)); - assert_eq!(-40, evaluate_position(&game_2.board, Color::Black)); + assert_eq!(-40, evaluate_position(&game_2.board)); Ok(()) } diff --git a/src/interface/uci.rs b/src/interface/uci.rs index 01bec10..d0ae626 100644 --- a/src/interface/uci.rs +++ b/src/interface/uci.rs @@ -3,7 +3,12 @@ use std::{ str::SplitWhitespace, }; -use crate::{board::game::Game, movegen::r#move::Move, search}; +use crate::{ + board::game::Game, + evaluation::evaluation::{MAX_EVAL, MIN_EVAL}, + movegen::r#move::Move, + search, +}; #[derive(PartialEq, Eq, Debug)] pub enum Command { @@ -132,11 +137,15 @@ pub fn uci_go(go: &mut SplitWhitespace, game: &mut Game) -> Result _ => (), } } - Ok( - search::minimax::minimax(game, params.depth.unwrap_or(MAX_DEPTH)) - .0 - .expect("No move selected"), + Ok(search::negamax::negamax( + game, + MIN_EVAL, + MAX_EVAL, + params.depth.unwrap_or(MAX_DEPTH), + 0, ) + .0 + .expect("No move selected")) } pub fn uci_loop(input: R, mut output: W) -> Result<(), String> { diff --git a/src/search/minimax.rs b/src/search/minimax.rs deleted file mode 100644 index 438e6d1..0000000 --- a/src/search/minimax.rs +++ /dev/null @@ -1,87 +0,0 @@ -use crate::{ - board::{ - board::Color, - game::Game, - history::{History, MoveParameters}, - }, - evaluation::evaluation::evaluate_position, - movegen::r#move::Move, -}; - -pub fn minimax(game: &mut Game, depth: u8) -> (Option, i32) { - let color = game.current_player(); - - if depth == 0 { - return (None, evaluate_position(&game.board, color)); - } - - let mut history = History::new(); - let mut best_move: Option = None; - let (mut best_score, mate_score) = match color { - Color::White => (-100000, -49000 - depth as i32), - Color::Black => (100000, 49000 + depth as i32), - }; - - let moves = game.board.pseudo_moves_all(color); - let mut legal_moves = 0; - - for mv in moves { - history.push_move_parameters(MoveParameters::build(&game.board, &mv)); - game.board.make_move(&mv); - - if !game.board.king_under_check(color) { - legal_moves += 1; - let (_, node_score) = minimax(game, depth - 1); - match color { - Color::White => { - if best_score < node_score { - best_score = node_score; - best_move = Some(mv); - } - } - Color::Black => { - if best_score > node_score { - best_score = node_score; - best_move = Some(mv); - } - } - } - } - game.board - .unmake_move(history.pop_move_parameters().expect("Empty history stack")); - } - - if legal_moves == 0 { - if game.board.king_under_check(color) { - return (None, mate_score); - } else { - return (None, 0); - } - } - (best_move, best_score) -} - -#[cfg(test)] -mod tests { - use crate::board::{fen::from_fen, square::Square}; - use crate::movegen::attack::init_attacks; - use crate::movegen::r#move::Move; - use crate::search::minimax::minimax; - - const FEN_MATE_IN_1: &str = "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1"; - - #[test] - fn test_minimax() -> Result<(), String> { - init_attacks(); - let mut game = from_fen(FEN_MATE_IN_1)?; - - let e3f2 = Move::new(Square::E3, Square::F2); - - assert_eq!(e3f2, minimax(&mut game, 2).0.unwrap()); - assert_eq!(e3f2, minimax(&mut game, 3).0.unwrap()); - assert_eq!(e3f2, minimax(&mut game, 4).0.unwrap()); - assert_eq!(e3f2, minimax(&mut game, 5).0.unwrap()); - - Ok(()) - } -} diff --git a/src/search/mod.rs b/src/search/mod.rs index 7bf93a6..957be4a 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -1,2 +1,2 @@ -pub mod minimax; +pub mod negamax; pub mod perft; diff --git a/src/search/negamax.rs b/src/search/negamax.rs new file mode 100644 index 0000000..b34f735 --- /dev/null +++ b/src/search/negamax.rs @@ -0,0 +1,94 @@ +use crate::{ + board::{game::Game, history::MoveParameters}, + evaluation::evaluation::evaluate_position, + movegen::r#move::Move, +}; + +pub fn negamax( + game: &mut Game, + mut alpha: i32, + beta: i32, + depth: u8, + plies: u8, +) -> (Option, i32) { + let color = game.current_player(); + + if depth == 0 { + return (None, evaluate_position(&game.board)); + } + + let (mut best_move, mut best_score, mate_score) = (None, -100000, -50000 + plies as i32); + let mut legal_moves = 0; + + for mv in game.board.pseudo_moves_all(color) { + let move_parameters = MoveParameters::build(&game.board, &mv); + game.board.make_move(&mv); + + if !game.board.king_under_check(color) { + legal_moves += 1; + let move_score = -negamax(game, -beta, -alpha, depth - 1, plies + 1).1; + + if move_score > best_score { + best_score = move_score; + best_move = Some(mv) + } + + if move_score >= beta { + game.board.unmake_move(move_parameters); + return (Some(mv), beta); // fail hard beta-cutoff + } + + if move_score > alpha { + alpha = move_score; // alpha acts like max in minimax + } + } + game.board.unmake_move(move_parameters); + } + + if legal_moves == 0 { + if game.board.king_under_check(color) { + return (None, mate_score); + } else { + return (None, 0); + } + } + (best_move, best_score) +} + +#[cfg(test)] +mod tests { + use crate::board::{fen::from_fen, square::Square}; + use crate::evaluation::evaluation::{MAX_EVAL, MIN_EVAL}; + use crate::movegen::attack::init_attacks; + use crate::movegen::r#move::Move; + use crate::search::negamax::negamax; + + const FEN_MATE_IN_1: &str = "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1"; + + #[test] + fn test_negamax() -> Result<(), String> { + init_attacks(); + let mut game = from_fen(FEN_MATE_IN_1)?; + + let e3f2 = Move::new(Square::E3, Square::F2); + + assert_eq!( + e3f2, + negamax(&mut game, MIN_EVAL, MAX_EVAL, 2, 0).0.unwrap() + ); + assert_eq!( + e3f2, + negamax(&mut game, MIN_EVAL, MAX_EVAL, 3, 0).0.unwrap() + ); + assert_eq!( + e3f2, + negamax(&mut game, MIN_EVAL, MAX_EVAL, 4, 0).0.unwrap() + ); + assert_eq!( + e3f2, + negamax(&mut game, MIN_EVAL, MAX_EVAL, 5, 0).0.unwrap() + ); + + Ok(()) + } +}