camel_up/oracle/
mod.rs

1//! An oracle is
2//!
3//! > a person or agency considered to provide wise and insightful counsel or prophetic predictions or precognition of the future, inspired by the gods. As such it is a form of divination.
4//!
5//! We divine by way of mathematics.
6use crate::{
7    camel::{Camel, Dice, Race},
8    fraction::Fraction,
9    tree::{LeafVisitor, Tree},
10};
11use std::{collections::HashMap, iter::Iterator, ops::Index};
12
13/// Determines the win chances for each camel.
14///
15/// The `Distribution` returns for each camel present in the race, the chance of winning.
16pub fn project(race: &Race, dice: &Dice) -> Chances {
17    let mut tree = Tree::singleton(race.clone());
18    tree.expand(dice);
19
20    let mut counter: LeafCounter = Default::default();
21    tree.visit_leaves(&mut counter);
22
23    counter.chances()
24}
25
26/// All the relevant chances for each camel.
27///
28/// I.e. which camel is winning, which is losing, which is the runner up.
29pub struct Chances {
30    /// Distribution of the chance to win.
31    pub winner: Distribution,
32    /// Distribution of the chance to be runner up.
33    pub runner_up: Distribution,
34    /// Distribution of the chance to lose.
35    pub loser: Distribution,
36}
37
38/// The chances for a specific situation for each camel.
39pub struct Distribution {
40    distribution: HashMap<Camel, Fraction>,
41    default: Fraction,
42}
43
44impl Distribution {
45    /// Returns an iterator that iterates over the chances.
46    ///
47    /// I.e. iterates over `(&Camel, &Fraction)` values.
48    pub fn values(&self) -> impl Iterator<Item = (&Camel, &Fraction)> + '_ {
49        self.distribution.iter()
50    }
51}
52
53impl From<HashMap<Camel, Fraction>> for Distribution {
54    fn from(distribution: HashMap<Camel, Fraction>) -> Self {
55        Self {
56            distribution,
57            default: Fraction::default(),
58        }
59    }
60}
61
62impl Index<&Camel> for Distribution {
63    type Output = Fraction;
64
65    fn index(&self, camel: &Camel) -> &Self::Output {
66        self.distribution.get(camel).unwrap_or(&self.default)
67    }
68}
69
70struct LeafCounter {
71    total: usize,
72    winner: HashMap<Camel, usize>,
73    runner_up: HashMap<Camel, usize>,
74    loser: HashMap<Camel, usize>,
75}
76
77impl LeafCounter {
78    fn chances(&self) -> Chances {
79        let winner: HashMap<Camel, Fraction> = self
80            .winner
81            .iter()
82            .map(|(camel, count)| (*camel, Fraction::new(*count as i64, self.total as u64)))
83            .collect();
84        let runner_up: HashMap<Camel, Fraction> = self
85            .runner_up
86            .iter()
87            .map(|(camel, count)| (*camel, Fraction::new(*count as i64, self.total as u64)))
88            .collect();
89        let loser: HashMap<Camel, Fraction> = self
90            .loser
91            .iter()
92            .map(|(camel, count)| (*camel, Fraction::new(*count as i64, self.total as u64)))
93            .collect();
94        Chances {
95            winner: Distribution::from(winner),
96            runner_up: Distribution::from(runner_up),
97            loser: Distribution::from(loser),
98        }
99    }
100}
101
102impl Default for LeafCounter {
103    fn default() -> Self {
104        Self {
105            total: 0,
106            winner: HashMap::new(),
107            runner_up: HashMap::new(),
108            loser: HashMap::new(),
109        }
110    }
111}
112
113impl LeafVisitor for LeafCounter {
114    fn visit(&mut self, race: &Race) {
115        if let Some(winner) = race.winner() {
116            *self.winner.entry(winner).or_insert(0) += 1;
117        };
118        if let Some(runner_up) = race.runner_up() {
119            *self.runner_up.entry(runner_up).or_insert(0) += 1;
120        };
121        if let Some(loser) = race.loser() {
122            *self.loser.entry(loser).or_insert(0) += 1;
123        };
124        self.total += 1;
125    }
126}
127
128#[cfg(test)]
129mod test {
130    use super::*;
131
132    #[test]
133    fn should_have_a_clear_winner() {
134        let race = "r,y".parse::<Race>().expect("to parse");
135        let dice = "r".parse::<Dice>().expect("to parse");
136        let chances = project(&race, &dice);
137
138        assert_eq!(chances.winner[&Camel::Red], Fraction::one());
139    }
140
141    #[test]
142    fn should_determine_chances() {
143        let race = "r,,y".parse::<Race>().expect("to parse");
144        let dice = "r".parse::<Dice>().expect("to parse");
145        let chances = project(&race, &dice);
146
147        assert_eq!(chances.winner[&Camel::Red], Fraction::new(2, 3));
148        assert_eq!(chances.winner[&Camel::Yellow], Fraction::new(1, 3));
149    }
150}