Convert minimax to negamax and implement alpha-beta pruning
This commit is contained in:
@@ -6,6 +6,9 @@ use crate::{
|
|||||||
evaluation::psqt::{mirror_index, piece_square_score},
|
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 {
|
const fn piece_score(piece_type: PieceType) -> i32 {
|
||||||
match piece_type {
|
match piece_type {
|
||||||
PieceType::Pawn => 100,
|
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 mut total_score = 0;
|
||||||
let pieces = match color {
|
let pieces = match color {
|
||||||
Color::White => &board.white_pieces,
|
Color::White => &board.white_pieces,
|
||||||
@@ -42,23 +45,21 @@ fn evaluate_side(board: &Board, color: Color) -> i32 {
|
|||||||
total_score
|
total_score
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn evaluate_position(board: &Board, color: Color) -> i32 {
|
pub fn evaluate_position(board: &Board) -> i32 {
|
||||||
let total_white_score = evaluate_side(board, Color::White);
|
let total_white_score = evaluate_side_for(board, Color::White);
|
||||||
let total_black_score = evaluate_side(board, Color::Black);
|
let total_black_score = evaluate_side_for(board, Color::Black);
|
||||||
let evaluation = total_white_score - total_black_score;
|
|
||||||
|
|
||||||
match color {
|
-(total_white_score - total_black_score)
|
||||||
Color::White => evaluation,
|
|
||||||
Color::Black => -evaluation,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use crate::{
|
use crate::{
|
||||||
board::board::{Color, PieceType},
|
board::{
|
||||||
board::fen::from_fen,
|
board::{Color, PieceType},
|
||||||
evaluation::evaluation::{evaluate_position, evaluate_side, piece_score},
|
fen::from_fen,
|
||||||
|
},
|
||||||
|
evaluation::evaluation::{evaluate_position, evaluate_side_for, piece_score},
|
||||||
};
|
};
|
||||||
|
|
||||||
const FEN_QUIET: [&str; 2] = [
|
const FEN_QUIET: [&str; 2] = [
|
||||||
@@ -79,17 +80,17 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_evaluate_side() -> Result<(), String> {
|
fn test_evaluate_side_for() -> Result<(), String> {
|
||||||
let game = from_fen(FEN_QUIET[0])?;
|
let game = from_fen(FEN_QUIET[0])?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
evaluate_side(&game.board, Color::White),
|
evaluate_side_for(&game.board, Color::White),
|
||||||
evaluate_side(&game.board, Color::Black)
|
evaluate_side_for(&game.board, Color::Black)
|
||||||
);
|
);
|
||||||
|
|
||||||
let game_2 = from_fen(FEN_QUIET[1])?;
|
let game_2 = from_fen(FEN_QUIET[1])?;
|
||||||
let evaluate_white = evaluate_side(&game_2.board, Color::White);
|
let evaluate_white = evaluate_side_for(&game_2.board, Color::White);
|
||||||
let evaluate_black = evaluate_side(&game_2.board, Color::Black);
|
let evaluate_black = evaluate_side_for(&game_2.board, Color::Black);
|
||||||
|
|
||||||
assert_eq!(24005, evaluate_white);
|
assert_eq!(24005, evaluate_white);
|
||||||
assert_eq!(23965, evaluate_black);
|
assert_eq!(23965, evaluate_black);
|
||||||
@@ -102,14 +103,13 @@ mod tests {
|
|||||||
let game = from_fen(FEN_QUIET[0])?;
|
let game = from_fen(FEN_QUIET[0])?;
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
evaluate_position(&game.board, Color::White),
|
evaluate_position(&game.board),
|
||||||
evaluate_position(&game.board, Color::Black)
|
evaluate_position(&game.board)
|
||||||
);
|
);
|
||||||
|
|
||||||
let game_2 = from_fen(FEN_QUIET[1])?;
|
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));
|
||||||
assert_eq!(-40, evaluate_position(&game_2.board, Color::Black));
|
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,12 @@ use std::{
|
|||||||
str::SplitWhitespace,
|
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)]
|
#[derive(PartialEq, Eq, Debug)]
|
||||||
pub enum Command {
|
pub enum Command {
|
||||||
@@ -132,11 +137,15 @@ pub fn uci_go(go: &mut SplitWhitespace, game: &mut Game) -> Result<Move, String>
|
|||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Ok(
|
Ok(search::negamax::negamax(
|
||||||
search::minimax::minimax(game, params.depth.unwrap_or(MAX_DEPTH))
|
game,
|
||||||
.0
|
MIN_EVAL,
|
||||||
.expect("No move selected"),
|
MAX_EVAL,
|
||||||
|
params.depth.unwrap_or(MAX_DEPTH),
|
||||||
|
0,
|
||||||
)
|
)
|
||||||
|
.0
|
||||||
|
.expect("No move selected"))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> Result<(), String> {
|
pub fn uci_loop<R: BufRead, W: Write>(input: R, mut output: W) -> Result<(), String> {
|
||||||
|
|||||||
@@ -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<Move>, 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<Move> = 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(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,2 +1,2 @@
|
|||||||
pub mod minimax;
|
pub mod negamax;
|
||||||
pub mod perft;
|
pub mod perft;
|
||||||
|
|||||||
94
src/search/negamax.rs
Normal file
94
src/search/negamax.rs
Normal file
@@ -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<Move>, 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(())
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user