From 36fb1a5d3bfe0a1fabd04df5d5a377ff13a62dc3 Mon Sep 17 00:00:00 2001 From: Nilstrieb <48135649+Nilstrieb@users.noreply.github.com> Date: Sat, 14 Jan 2023 18:08:44 +0100 Subject: [PATCH] minmax --- minmax-rs/src/connect4/board.rs | 2 +- minmax-rs/src/lib.rs | 8 +++---- minmax-rs/src/minmax.rs | 39 +++++++++++++++++++++++++++------ 3 files changed, 37 insertions(+), 12 deletions(-) diff --git a/minmax-rs/src/connect4/board.rs b/minmax-rs/src/connect4/board.rs index 025a6d2..002cd0e 100644 --- a/minmax-rs/src/connect4/board.rs +++ b/minmax-rs/src/connect4/board.rs @@ -148,7 +148,7 @@ impl IndexMut for Connect4 { impl Game for Connect4 { type Move = usize; - const REASONABLE_SEARCH_DEPTH: Option = Some(7); + const REASONABLE_SEARCH_DEPTH: Option = Some(11); fn empty() -> Self { Self::new() diff --git a/minmax-rs/src/lib.rs b/minmax-rs/src/lib.rs index e4de693..a3aff71 100644 --- a/minmax-rs/src/lib.rs +++ b/minmax-rs/src/lib.rs @@ -78,14 +78,14 @@ pub trait Game: Display { } } -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Score(i32); impl Score { - const MIN: Self = Self(i32::MIN); - const LOST: Self = Self(-100); + // Due to the nature of two's completement, we can't actually negate this properly, so add 1. + const LOST: Self = Self(i32::MIN + 1); const TIE: Self = Self(0); - const WON: Self = Self(100); + const WON: Self = Self(i32::MAX); pub fn new(int: i32) -> Self { Self(int) diff --git a/minmax-rs/src/minmax.rs b/minmax-rs/src/minmax.rs index 84bcac3..806a5ee 100644 --- a/minmax-rs/src/minmax.rs +++ b/minmax-rs/src/minmax.rs @@ -33,14 +33,23 @@ impl PerfectPlayer { self.best_move.expect("no move made yet") } - fn minmax(&mut self, board: &mut G, player: Player, depth: usize) -> Score { + fn minmax( + &mut self, + board: &mut G, + maximizing_player: Player, + alpha: Score, + beta: Score, + depth: usize, + ) -> Score { + // FIXME: Make depth decrease not increase. if let Some(max_depth) = self.max_depth && depth >= max_depth { - return board.rate(player); + // FIXME: Why do we have rate and result? + return board.rate(maximizing_player); } match board.result() { State::Winner(winner) => { - if winner == player { + if winner == maximizing_player { Score::WON } else { Score::LOST @@ -48,11 +57,12 @@ impl PerfectPlayer { } State::Draw => Score::TIE, State::InProgress => { - let mut max_value = Score::MIN; + let mut max_value = alpha; for pos in board.possible_moves() { - board.make_move(pos, player); - let value = -self.minmax(board, player.opponent(), depth + 1); + board.make_move(pos, maximizing_player); + let value = + -self.minmax(board, maximizing_player.opponent(), -beta, -max_value, depth + 1); board.undo_move(pos); @@ -62,6 +72,21 @@ impl PerfectPlayer { self.best_move = Some(pos); } } + // Imagine a game tree like this + // P( ) + // / \ + // A(10) B( ) <- we are here in the loop for the first child that returned 11. + // / \ + // C(11) D( ) + // + // Our beta parameter is 10, because that's the current max_value of our parent. + // If P plays B, we know that B will pick something _at least_ as good as C. This means + // that B will be -11 or worse. -11 is definitly worse than -10, so playing B is definitly + // a very bad idea, no matter the value of D. So don't even bother calculating the value of D + // and just break out. + if max_value >= beta { + break; + } } max_value @@ -74,7 +99,7 @@ impl GamePlayer for PerfectPlayer { fn next_move(&mut self, board: &mut G, this_player: Player) { let start = Instant::now(); self.best_move = None; - self.minmax(board, this_player, 0); + self.minmax(board, this_player, Score::LOST, Score::WON, 0); board.make_move(self.best_move.expect("could not make move"), this_player);