Skip to content

Commit

Permalink
Return different best move for 1 pointer than for money game
Browse files Browse the repository at this point in the history
  • Loading branch information
carsten-wenderdel committed Oct 5, 2023
1 parent 5ac11be commit 5892e86
Show file tree
Hide file tree
Showing 6 changed files with 165 additions and 13 deletions.
5 changes: 4 additions & 1 deletion crates/coach/src/position_finder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,10 @@ impl<T: Evaluator> PositionFinder<T> {
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<Position> = new_positions
.into_iter()
.filter(|p| p.game_state() == Ongoing)
Expand Down
44 changes: 35 additions & 9 deletions crates/engine/src/evaluator.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F>(&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
Expand Down Expand Up @@ -83,15 +96,15 @@ 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()
}

/// 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 == &expected_pos() {
if pos == &position_with_lowest_equity() {
Probabilities {
win_normal: 0.5,
win_gammon: 0.1,
Expand All @@ -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,
}
Expand All @@ -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]
Expand Down
6 changes: 6 additions & 0 deletions crates/engine/src/position.rs
Original file line number Diff line number Diff line change
Expand Up @@ -194,6 +194,12 @@ impl Position {
}
}

impl From<Position> for [i8; 26] {
fn from(value: Position) -> Self {
value.pips
}
}

impl TryFrom<[i8; 26]> for Position {
type Error = &'static str;

Expand Down
17 changes: 17 additions & 0 deletions crates/engine/src/probabilities.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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);
}
}
7 changes: 4 additions & 3 deletions crates/logic/src/bg_move.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<MoveDetail>,
pub(crate) details: Vec<MoveDetail>,
}

#[derive(Debug, PartialEq, Serialize, ToSchema)]
Expand All @@ -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 {
Expand Down
99 changes: 99 additions & 0 deletions crates/logic/src/lib.rs
Original file line number Diff line number Diff line change
@@ -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<BgMove, Error> {
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<T: Evaluator>(
pips: [i8; 26],
die1: u8,
die2: u8,
evaluator: &T,
) -> Result<BgMove, Error> {
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."
);
}
}

0 comments on commit 5892e86

Please sign in to comment.