1use crate::{Game, Prng};
4
5pub fn sample_chance<G: Game>(game: &G, state: &G::State, rng: &mut Prng) -> Option<G::Action> {
49 let outcomes = game.chance_outcomes(state);
50 if outcomes.is_empty() {
51 return None;
52 }
53 let total: f64 = outcomes.iter().map(|(_, weight)| *weight).sum();
54
55 #[allow(clippy::cast_precision_loss)]
58 let unit = rng.next_u64() as f64 / (u64::MAX as f64 + 1.0);
59 let mut remaining = unit * total;
60
61 let mut chosen = None;
62 for (action, weight) in outcomes {
63 chosen = Some(action);
64 remaining -= weight;
65 if remaining < 0.0 {
66 break;
67 }
68 }
69 chosen
70}
71
72#[cfg(test)]
73mod tests {
74 use super::sample_chance;
75 use crate::{ActivePlayers, Game, PlayerId, Prng};
76
77 struct Weighted;
79
80 impl Game for Weighted {
81 type State = ();
82 type Action = u8;
83 type View = ();
84
85 fn new_initial_state(&self, _seed: u64) -> Self::State {}
86 fn num_players(&self) -> usize {
87 0
88 }
89 fn active_players(&self, _state: &Self::State) -> ActivePlayers {
90 ActivePlayers::one(PlayerId::CHANCE)
91 }
92 fn legal_actions(&self, _state: &Self::State, player: PlayerId) -> Vec<Self::Action> {
93 if player.is_chance() {
94 vec![0, 1, 2]
95 } else {
96 Vec::new()
97 }
98 }
99 fn chance_outcomes(&self, _state: &Self::State) -> Vec<(Self::Action, f64)> {
100 vec![(0, 0.1), (1, 0.3), (2, 0.6)]
101 }
102 fn apply(&self, _state: &mut Self::State, _player: PlayerId, _action: Self::Action) {}
103 fn is_terminal(&self, _state: &Self::State) -> bool {
104 false
105 }
106 fn reward(&self, _state: &Self::State, _player: PlayerId) -> f64 {
107 0.0
108 }
109 fn view(&self, _state: &Self::State, _viewer: Option<PlayerId>) -> Self::View {}
110 }
111
112 #[test]
113 fn only_returns_offered_outcomes() {
114 let mut rng = Prng::new(1);
115 for _ in 0..1000 {
116 let outcome = sample_chance(&Weighted, &(), &mut rng).unwrap();
117 assert!(outcome <= 2);
118 }
119 }
120
121 #[test]
122 fn empty_distribution_yields_none() {
123 struct NoChance;
124 impl Game for NoChance {
125 type State = ();
126 type Action = u8;
127 type View = ();
128 fn new_initial_state(&self, _seed: u64) -> Self::State {}
129 fn num_players(&self) -> usize {
130 0
131 }
132 fn active_players(&self, _s: &Self::State) -> ActivePlayers {
133 ActivePlayers::none()
134 }
135 fn legal_actions(&self, _s: &Self::State, _p: PlayerId) -> Vec<Self::Action> {
136 Vec::new()
137 }
138 fn apply(&self, _s: &mut Self::State, _p: PlayerId, _a: Self::Action) {}
139 fn is_terminal(&self, _s: &Self::State) -> bool {
140 true
141 }
142 fn reward(&self, _s: &Self::State, _p: PlayerId) -> f64 {
143 0.0
144 }
145 fn view(&self, _s: &Self::State, _v: Option<PlayerId>) -> Self::View {}
146 }
147 let mut rng = Prng::new(1);
148 assert!(sample_chance(&NoChance, &(), &mut rng).is_none());
149 }
150
151 #[test]
152 fn empirical_frequencies_track_the_weights() {
153 let mut rng = Prng::new(12345);
154 let mut counts = [0u32; 3];
155 let trials = 100_000;
156 for _ in 0..trials {
157 let outcome = sample_chance(&Weighted, &(), &mut rng).unwrap();
158 counts[outcome as usize] += 1;
159 }
160 let freq = counts.map(|c| f64::from(c) / f64::from(trials));
161 assert!((freq[0] - 0.1).abs() < 0.01, "freq {freq:?}");
163 assert!((freq[1] - 0.3).abs() < 0.01, "freq {freq:?}");
164 assert!((freq[2] - 0.6).abs() < 0.01, "freq {freq:?}");
165 }
166
167 #[test]
168 fn same_seed_reproduces_the_draw() {
169 let mut a = Prng::new(99);
170 let mut b = Prng::new(99);
171 for _ in 0..100 {
172 assert_eq!(
173 sample_chance(&Weighted, &(), &mut a),
174 sample_chance(&Weighted, &(), &mut b)
175 );
176 }
177 }
178}