From ba0cbf4d7d961c4cfb0ae28c7620f051ef497254 Mon Sep 17 00:00:00 2001 From: stefiosif Date: Sun, 1 Sep 2024 14:15:02 +0300 Subject: [PATCH] Add minimax and adapt UCI tests --- src/board/history.rs | 15 ++++++-- src/interface/uci.rs | 33 ++++++++++------ src/movegen/move.rs | 2 +- src/search/minimax.rs | 87 +++++++++++++++++++++++++++++++++++++++++++ src/search/mod.rs | 2 +- src/search/search.rs | 42 --------------------- 6 files changed, 123 insertions(+), 58 deletions(-) create mode 100644 src/search/minimax.rs delete mode 100644 src/search/search.rs diff --git a/src/board/history.rs b/src/board/history.rs index 529b9ce..33c8956 100644 --- a/src/board/history.rs +++ b/src/board/history.rs @@ -46,15 +46,24 @@ impl MoveParameters { } } - pub fn add_move(&mut self, mv: Move) { + pub fn build(board: &Board, mv: &Move) -> Self { + let mut move_parameters = Self::new(); + move_parameters.add_move(*mv); + move_parameters.add_irreversible_parameters(board.state); + move_parameters.add_capture_and_promotion_piece(board, *mv, board.state.current_player()); + + move_parameters + } + + fn add_move(&mut self, mv: Move) { self.mv = Some(mv) } - pub fn add_captured_piece(&mut self, board: &Board, dst: usize, color: Color) { + fn add_captured_piece(&mut self, board: &Board, dst: usize, color: Color) { self.captured_piece = board.piece_type_at(dst, color); } - pub fn add_promoted_piece(&mut self, promote: Promote) { + fn add_promoted_piece(&mut self, promote: Promote) { self.promoted_piece = Some(promote.into_piece_type()) } diff --git a/src/interface/uci.rs b/src/interface/uci.rs index 25b0e5c..01bec10 100644 --- a/src/interface/uci.rs +++ b/src/interface/uci.rs @@ -3,7 +3,7 @@ use std::{ str::SplitWhitespace, }; -use crate::{board::game::Game, board::square::Square, movegen::r#move::Move, search::search}; +use crate::{board::game::Game, movegen::r#move::Move, search}; #[derive(PartialEq, Eq, Debug)] pub enum Command { @@ -91,7 +91,11 @@ pub fn uci_position(position: &mut SplitWhitespace) -> Result { let state = position.next().ok_or("Expected startpos or fen")?; let mut game = match state { "startpos" => Game::new(), - "fen" => Game::new_from_fen(position.next().ok_or("Expected fen string")?)?, + "fen" => { + let fen_parts: Vec<&str> = position.take_while(|&part| part != "moves").collect(); + let fen = fen_parts.join(" "); + Game::new_from_fen(&fen)? + } _ => return Err("Expected startpos or fen".to_string()), }; @@ -128,9 +132,11 @@ pub fn uci_go(go: &mut SplitWhitespace, game: &mut Game) -> Result _ => (), } } - search::search(game, params.depth.unwrap_or(MAX_DEPTH)); - - Ok(Move::new(Square::B8, Square::C6)) + Ok( + search::minimax::minimax(game, params.depth.unwrap_or(MAX_DEPTH)) + .0 + .expect("No move selected"), + ) } pub fn uci_loop(input: R, mut output: W) -> Result<(), String> { @@ -171,9 +177,9 @@ mod tests { use std::io::Cursor; use crate::{ - board::{fen::from_fen, game::Game, square::Square}, + board::{fen::from_fen, square::Square}, interface::uci::{parse_command, Command}, - movegen::r#move::Move, + movegen::{attack::init_attacks, r#move::Move}, }; use super::uci_go; @@ -204,22 +210,27 @@ mod tests { Ok(()) } + const FEN_MATE_IN_1: &str = "8/8/8/8/8/4q1k1/8/5K2 b - - 0 1"; + #[test] fn test_uci_go() -> Result<(), String> { - let mut game = Game::new(); + init_attacks(); + let mut game = from_fen(FEN_MATE_IN_1)?; let command_go = "go depth 4"; let mut parts = command_go.split_whitespace(); let response = uci_go(&mut parts, &mut game)?; - assert_eq!(Move::new(Square::B8, Square::C6), response); + assert_eq!(Move::new(Square::E3, Square::F2), response); + Ok(()) } #[test] fn test_uci_loop() -> Result<(), String> { + init_attacks(); let commands = b"uci\n\ ucinewgame\n\ - position startpos moves e2e4 e7e5\n\ + position fen 8/8/8/8/8/4q1k1/8/5K2 b - - 0 1\n\ go\n\ quit"; let input = Cursor::new(commands); @@ -232,7 +243,7 @@ mod tests { uciok\n\ Clear cache\n\ Initialized position\n\ - bestmove b8c6\n"; + bestmove e3f2\n"; let actual_response = String::from_utf8(output).expect("Invalid UTF-8 in output"); assert_eq!(expected_response, actual_response); diff --git a/src/movegen/move.rs b/src/movegen/move.rs index b8efb30..bd3c2d6 100644 --- a/src/movegen/move.rs +++ b/src/movegen/move.rs @@ -131,7 +131,7 @@ impl Board { Color::Black => ( &mut self.black_pieces, &mut self.white_pieces, - Some(mv.src - 8), + Some(mv.src.saturating_sub(8)), ), }; let pawn_move = Self::is_pawn_move(mv.src, &own_pieces[PieceType::Pawn]); diff --git a/src/search/minimax.rs b/src/search/minimax.rs new file mode 100644 index 0000000..438e6d1 --- /dev/null +++ b/src/search/minimax.rs @@ -0,0 +1,87 @@ +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 0f647b8..7bf93a6 100644 --- a/src/search/mod.rs +++ b/src/search/mod.rs @@ -1,2 +1,2 @@ +pub mod minimax; pub mod perft; -pub mod search; diff --git a/src/search/search.rs b/src/search/search.rs deleted file mode 100644 index eda2ddf..0000000 --- a/src/search/search.rs +++ /dev/null @@ -1,42 +0,0 @@ -use crate::board::game::Game; - -pub fn search(game: &mut Game, depth: u8) { - if depth == 0 { - return; - } - - let color = game.current_player(); - let pseudo_moves = game.board.pseudo_moves_all(color); - - for mv in pseudo_moves { - let original_board = game.board.clone(); - game.board.make_move(&mv); - - if game.board.king_under_check(color) { - game.board = original_board; - continue; - } - - search(game, depth - 1); - game.board = original_board; - } -} - -#[cfg(test)] -mod tests { - use crate::board::fen::from_fen; - - const FEN_QUIET: [&str; 2] = [ - "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR w KQkq - 0 1", - "rnbqkbnr/ppp2ppp/4p3/3pN3/3P4/8/PPP1PPPP/RNBQKB1R w KQkq - 0 1", - ]; - - #[test] - fn test_search() -> Result<(), String> { - let _game = from_fen(FEN_QUIET[0])?; - - //TODO: - - Ok(()) - } -}