wowlab_engine_rng/stochastic/
sampling.rs1#[inline]
3pub fn proc_chance(rng: &mut dyn FnMut() -> f64, chance: f64) -> bool {
4 if chance <= 0.0 {
5 false
6 } else if chance >= 1.0 {
7 true
8 } else {
9 rng() < chance
10 }
11}
12
13#[inline]
15#[must_use]
16pub const fn ppm_proc_chance(ppm: f64, swing_period_seconds: f64) -> f64 {
17 ppm * swing_period_seconds / 60.0
19}
20
21#[inline]
23pub fn roll_range(rng: &mut dyn FnMut() -> f64, low: f64, high: f64) -> f64 {
24 low + rng() * (high - low)
25}
26
27#[inline]
29pub fn roll_tier(rng: &mut dyn FnMut() -> f64, thresholds: &[f64]) -> usize {
30 let roll = rng();
31
32 for (index, &threshold) in thresholds.iter().enumerate() {
33 if roll < threshold {
34 return index;
35 }
36 }
37
38 thresholds.len()
39}
40
41pub fn shuffle_pick<T>(rng: &mut dyn FnMut() -> f64, items: &mut [T], n: usize) {
43 let len = items.len();
44 let picks = n.min(len);
45
46 for index in 0..picks {
47 let remaining = len - index;
48 #[expect(
49 clippy::cast_possible_truncation,
50 clippy::cast_precision_loss,
51 clippy::cast_sign_loss,
52 reason = "Fisher-Yates maps a unit-interval float to a bounded slice index"
53 )]
54 let offset = (rng() * remaining as f64) as usize;
55 let swap_index = (index + offset).min(len - 1);
56
57 items.swap(index, swap_index);
58 }
59}
60
61#[cfg(test)]
62mod tests {
63 use googletest::prelude::*;
64 use rstest::rstest;
65
66 use super::*;
67
68 #[gtest]
69 #[rstest]
70 #[case::chance_zero_never(0.0, 0.0, false)]
71 #[case::chance_one_always(0.0, 1.0, true)]
72 #[case::chance_one_high_roll(0.999, 1.0, true)]
73 #[case::below_threshold(0.4, 0.5, true)]
74 #[case::at_threshold_strict(0.5, 0.5, false)]
75 #[case::above_threshold(0.6, 0.5, false)]
76 fn proc_chance_boundary_contract(
77 #[case] roll: f64,
78 #[case] chance: f64,
79 #[case] expected: bool,
80 ) -> Result<()> {
81 let mut rng = || roll;
82
83 verify_that!(proc_chance(&mut rng, chance), eq(expected))
84 }
85
86 #[gtest]
87 #[rstest]
88 #[case::zero(0.0, false)]
89 #[case::negative(-0.5, false)]
90 #[case::one(1.0, true)]
91 #[case::above_one(1.5, true)]
92 fn deterministic_proc_chances_do_not_consume_rng(
93 #[case] chance: f64,
94 #[case] expected: bool,
95 ) -> Result<()> {
96 let mut draws = 0;
97
98 let mut rng = || {
99 draws += 1;
100
101 0.5
102 };
103
104 verify_that!(proc_chance(&mut rng, chance), eq(expected))?;
105
106 verify_that!(draws, eq(0))
107 }
108
109 #[gtest]
110 #[rstest]
111 #[case::one_second(7.0, 1.0, 7.0 / 60.0)]
112 #[case::unusual_slow_swing(7.0, 2.4, 0.28)]
113 #[case::moment_of_clarity_on_unusual_swing(9.1, 2.4, 0.364)]
114 #[case::zero_rate(0.0, 2.4, 0.0)]
115 fn ppm_proc_chance_scales_with_the_unhasted_swing_period(
116 #[case] ppm: f64,
117 #[case] swing_period_seconds: f64,
118 #[case] expected: f64,
119 ) -> Result<()> {
120 verify_that!(
121 ppm_proc_chance(ppm, swing_period_seconds),
122 near(expected, f64::EPSILON)
123 )
124 }
125
126 #[gtest]
127 #[rstest]
128 #[case::first_tier(vec![0.2, 0.5, 1.0], 0.1, 0)]
129 #[case::second_tier(vec![0.2, 0.5, 1.0], 0.3, 1)]
130 #[case::third_tier(vec![0.2, 0.5, 1.0], 0.7, 2)]
131 #[case::fall_through(vec![0.2, 0.5, 0.8], 0.9, 3)]
132 #[case::boundary_not_less(vec![0.2, 0.5, 0.8], 0.2, 1)]
133 #[case::empty_slice(vec![], 0.5, 0)]
134 fn roll_tier_threshold_arms(
135 #[case] thresholds: Vec<f64>,
136 #[case] roll: f64,
137 #[case] expected: usize,
138 ) -> Result<()> {
139 let mut rng = || roll;
140
141 verify_that!(roll_tier(&mut rng, &thresholds), eq(expected))
142 }
143
144 #[gtest]
145 #[rstest]
146 #[case::n_zero(vec![1, 2, 3, 4], 0.0, 0, vec![1, 2, 3, 4])]
147 #[case::identity_self_swap(vec![1, 2, 3, 4], 0.0, 4, vec![1, 2, 3, 4])]
148 #[case::high_roll_permutation(vec![1, 2, 3, 4], 0.999, 2, vec![4, 1, 3, 2])]
149 #[case::n_exceeds_len(vec![1, 2], 0.0, 10, vec![1, 2])]
150 fn shuffle_pick_known_answer(
151 #[case] mut items: Vec<i32>,
152 #[case] rng_val: f64,
153 #[case] n: usize,
154 #[case] expected: Vec<i32>,
155 ) -> Result<()> {
156 let mut rng = || rng_val;
157
158 shuffle_pick(&mut rng, &mut items, n);
159
160 verify_that!(items, container_eq(expected))
161 }
162
163 #[gtest]
164 fn shuffle_pick_consumes_one_draw_per_pick() -> Result<()> {
165 let mut draws = 0;
166 let mut rng = || {
167 draws += 1;
168
169 0.0
170 };
171 let mut items = [1, 2, 3];
172
173 shuffle_pick(&mut rng, &mut items, 10);
174
175 verify_that!(draws, eq(items.len()))
176 }
177}