From 5892e86d46362f9bb3c3969b7e69f20bfeea15f3 Mon Sep 17 00:00:00 2001 From: Carsten Wenderdel Date: Thu, 5 Oct 2023 00:54:57 +0200 Subject: [PATCH] Return different best move for 1 pointer than for money game --- crates/coach/src/position_finder.rs | 5 +- crates/engine/src/evaluator.rs | 44 ++++++++++--- crates/engine/src/position.rs | 6 ++ crates/engine/src/probabilities.rs | 17 +++++ crates/logic/src/bg_move.rs | 7 +- crates/logic/src/lib.rs | 99 +++++++++++++++++++++++++++++ 6 files changed, 165 insertions(+), 13 deletions(-) diff --git a/crates/coach/src/position_finder.rs b/crates/coach/src/position_finder.rs index af10a35..00fd221 100644 --- a/crates/coach/src/position_finder.rs +++ b/crates/coach/src/position_finder.rs @@ -42,7 +42,10 @@ impl PositionFinder { let dice = self.dice_gen.roll(); let new_positions = pos.all_positions_after_moving(&dice); // Todo: remove cloning by implementing the Copy trait -> maybe better performance - pos = self.evaluator.worst_position(&new_positions).clone(); + pos = self + .evaluator + .worst_position(&new_positions, |probabilities| probabilities.equity()) + .clone(); let mut ongoing_games: Vec = new_positions .into_iter() .filter(|p| p.game_state() == Ongoing) diff --git a/crates/engine/src/evaluator.rs b/crates/engine/src/evaluator.rs index 6ba5cc9..1f1289e 100644 --- a/crates/engine/src/evaluator.rs +++ b/crates/engine/src/evaluator.rs @@ -12,15 +12,28 @@ pub trait Evaluator { /// The returned `Position` has already switches sides. /// This means the returned position will have the *lowest* equity of possible positions. fn best_position_by_equity(&self, pos: &Position, dice: &Dice) -> Position { - self.worst_position(&pos.all_positions_after_moving(dice)) + self.best_position(pos, dice, |probabilities| probabilities.equity()) + } + + /// Returns the position after applying the *best* move to `pos`. + /// The returned `Position` has already switches sides. + /// This means the returned position will have the *lowest* value of possible positions. + fn best_position(&self, pos: &Position, dice: &Dice, value: F) -> Position + where + F: Fn(&Probabilities) -> f32, + { + self.worst_position(&pos.all_positions_after_moving(dice), value) .clone() } /// Worst position might be interesting, because when you switch sides, it's suddenly the best. - fn worst_position<'a>(&'a self, positions: &'a [Position]) -> &Position { + fn worst_position<'a, F>(&'a self, positions: &'a [Position], value: F) -> &Position + where + F: Fn(&Probabilities) -> f32, + { positions .iter() - .map(|pos| (pos, self.eval(pos).equity())) + .map(|pos| (pos, value(&self.eval(pos)))) .min_by(|a, b| a.1.partial_cmp(&b.1).unwrap()) .unwrap() .0 @@ -83,7 +96,7 @@ mod evaluator_trait_tests { use crate::position::Position; use std::collections::HashMap; - fn expected_pos() -> Position { + fn position_with_lowest_equity() -> Position { pos!(x 5:1, 3:1; o 20:2).switch_sides() } @@ -91,7 +104,7 @@ mod evaluator_trait_tests { struct EvaluatorFake {} impl Evaluator for EvaluatorFake { fn eval(&self, pos: &Position) -> Probabilities { - if pos == &expected_pos() { + if pos == &position_with_lowest_equity() { Probabilities { win_normal: 0.5, win_gammon: 0.1, @@ -102,10 +115,10 @@ mod evaluator_trait_tests { } } else { Probabilities { - win_normal: 0.4, + win_normal: 0.38, win_gammon: 0.2, win_bg: 0.1, - lose_normal: 0.1, + lose_normal: 0.12, lose_gammon: 0.1, lose_bg: 0.1, } @@ -114,14 +127,27 @@ mod evaluator_trait_tests { } #[test] - fn best_position() { + fn best_position_by_equity() { // Given let given_pos = pos!(x 7:2; o 20:2); let evaluator = EvaluatorFake {}; // When let best_pos = evaluator.best_position_by_equity(&given_pos, &Dice::new(4, 2)); // Then - assert_eq!(best_pos, expected_pos()); + assert_eq!(best_pos, position_with_lowest_equity()); + } + + #[test] + /// This is basically the same test as the one above (best_position_by_equity), but with different outcome for 1 ptrs. + fn best_position_for_1ptr() { + // Given + let given_pos = pos!(x 7:2; o 20:2); + let evaluator = EvaluatorFake {}; + // When + let best_pos = evaluator.best_position(&given_pos, &Dice::new(4, 2), |p| p.win()); + // Then + let expected = pos!(x 7:1, 1:1; o 20: 2); + assert_eq!(best_pos, expected.switch_sides()); } #[test] diff --git a/crates/engine/src/position.rs b/crates/engine/src/position.rs index 6518192..35dff44 100644 --- a/crates/engine/src/position.rs +++ b/crates/engine/src/position.rs @@ -194,6 +194,12 @@ impl Position { } } +impl From for [i8; 26] { + fn from(value: Position) -> Self { + value.pips + } +} + impl TryFrom<[i8; 26]> for Position { type Error = &'static str; diff --git a/crates/engine/src/probabilities.rs b/crates/engine/src/probabilities.rs index 56e95e8..87bdf24 100644 --- a/crates/engine/src/probabilities.rs +++ b/crates/engine/src/probabilities.rs @@ -64,6 +64,10 @@ impl Probabilities { } } + pub fn win(&self) -> f32 { + self.win_normal + self.win_gammon + self.win_bg + } + pub(crate) fn switch_sides(&self) -> Self { Self { win_normal: self.lose_normal, @@ -206,4 +210,17 @@ mod tests { }; assert_eq!(probabilities.equity(), 0.0); } + + #[test] + fn win() { + let probabilities = Probabilities { + win_normal: 0.5, + win_gammon: 0.2, + win_bg: 0.12, + lose_normal: 0.1, + lose_gammon: 0.07, + lose_bg: 0.1, + }; + assert_eq!(probabilities.win(), 0.82); + } } diff --git a/crates/logic/src/bg_move.rs b/crates/logic/src/bg_move.rs index 9f1d0d8..e5c0f97 100644 --- a/crates/logic/src/bg_move.rs +++ b/crates/logic/src/bg_move.rs @@ -9,8 +9,9 @@ mod regular; /// `BgMove` is not used during rollouts or evaluation but only when returning moves via an API /// This is why a new `BgMove` is always calculated based on a `old` and a resulting `new` position. +#[derive(Debug, PartialEq)] pub struct BgMove { - details: Vec, + pub(crate) details: Vec, } #[derive(Debug, PartialEq, Serialize, ToSchema)] @@ -20,10 +21,10 @@ pub struct BgMove { pub struct MoveDetail { /// The bar is represented by `25`. #[schema(minimum = 1, maximum = 25)] - from: usize, + pub(crate) from: usize, /// bear off is represented by `0`. #[schema(minimum = 0, maximum = 24)] - to: usize, + pub(crate) to: usize, } impl BgMove { diff --git a/crates/logic/src/lib.rs b/crates/logic/src/lib.rs index 0349866..ba5f278 100644 --- a/crates/logic/src/lib.rs +++ b/crates/logic/src/lib.rs @@ -1,2 +1,101 @@ +use crate::bg_move::BgMove; +use engine::dice::Dice; +use engine::evaluator::Evaluator; +use engine::onnx::OnnxEvaluator; +use engine::position::Position; + pub mod bg_move; pub mod cube; + +type Error = &'static str; + +pub fn best_move_1ptr(pips: [i8; 26], die1: u8, die2: u8) -> Result { + match OnnxEvaluator::with_default_model() { + None => Err("Could not find neural networks."), + Some(evaluator) => best_move_1ptr_with_evaluator(pips, die1, die2, &evaluator), + } +} + +fn best_move_1ptr_with_evaluator( + pips: [i8; 26], + die1: u8, + die2: u8, + evaluator: &T, +) -> Result { + let position = Position::try_from(pips)?; + let dice = Dice::try_from((die1 as usize, die2 as usize))?; + // gammon and backgammon counts the same as normal wins, so we use p.win() + let new_position = evaluator.best_position(&position, &dice, |p| p.win()); + let bg_move = BgMove::new(&position, &new_position.switch_sides(), &dice); + Ok(bg_move) +} + +#[cfg(test)] +mod tests { + use crate::bg_move::{BgMove, MoveDetail}; + use engine::evaluator::Evaluator; + use engine::pos; + use engine::position::Position; + use engine::probabilities::Probabilities; + use std::collections::HashMap; + + fn position_with_lowest_equity() -> Position { + pos!(x 5:1, 3:1; o 20:2).switch_sides() + } + + /// Test double. Returns not so good probabilities for `expected_pos`, better for everything else. + struct EvaluatorFake {} + impl Evaluator for EvaluatorFake { + fn eval(&self, pos: &Position) -> Probabilities { + if pos == &position_with_lowest_equity() { + // This would be position for money game. + // Remember that this equity is already from the point of the opponent. + Probabilities { + win_normal: 0.5, + win_gammon: 0.1, + win_bg: 0.1, + lose_normal: 0.1, + lose_gammon: 0.1, + lose_bg: 0.1, + } + } else { + // This would be position for 1 ptrs. + Probabilities { + win_normal: 0.38, + win_gammon: 0.2, + win_bg: 0.1, + lose_normal: 0.12, + lose_gammon: 0.1, + lose_bg: 0.1, + } + } + } + } + + #[test] + fn best_move_1ptr_with_evaluator() { + // Given + let given_pos = pos!(x 7:2; o 20:2); + let evaluator = EvaluatorFake {}; + // When + let bg_move = + crate::best_move_1ptr_with_evaluator(given_pos.clone().into(), 4, 2, &evaluator) + .unwrap(); + // Then + let expected_move = BgMove { + details: vec![MoveDetail { from: 7, to: 5 }, MoveDetail { from: 5, to: 1 }], + }; + assert_eq!(bg_move, expected_move); + } + + #[test] + fn best_move_1ptr_error() { + let given_pos = pos!(x 7:2; o 20:2); + assert_eq!( + crate::best_move_1ptr(given_pos.into(), 4, 2).expect_err( + "During tests folders are handled differently than when using a binary crate." + ), + "Could not find neural networks." + ); + } +}