wowlab_engine_rng/stochastic/
accumulated.rs1use super::proc_chance;
2
3#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum AccumulatedOutcome {
7 Failed,
8 Succeeded,
9 Guaranteed,
10}
11
12#[derive(Clone, Copy, Debug)]
14pub struct AccumulatedRng {
15 base_chance: f64,
16 cap: u32,
17 initial_count: u32,
18 attempt_count: u32,
19}
20
21impl AccumulatedRng {
22 #[must_use]
23 pub const fn new(base_chance: f64, cap: u32, initial_count: u32) -> Self {
24 Self {
25 base_chance,
26 cap,
27 initial_count,
28 attempt_count: initial_count,
29 }
30 }
31
32 pub const fn reset(&mut self) {
33 self.attempt_count = self.initial_count;
34 }
35
36 pub fn trigger(&mut self, rng: &mut dyn FnMut() -> f64) -> AccumulatedOutcome {
37 if self.base_chance <= 0.0 {
38 return AccumulatedOutcome::Failed;
39 }
40
41 self.attempt_count = self.attempt_count.saturating_add(1);
42 let guaranteed = self.cap > 0 && self.attempt_count >= self.cap;
43 let chance = if guaranteed {
44 1.0
45 } else {
46 (self.base_chance * f64::from(self.attempt_count)).clamp(0.0, 1.0)
47 };
48
49 if !proc_chance(rng, chance) {
50 return AccumulatedOutcome::Failed;
51 }
52
53 self.attempt_count = 0;
54
55 if guaranteed || chance >= 1.0 {
56 AccumulatedOutcome::Guaranteed
57 } else {
58 AccumulatedOutcome::Succeeded
59 }
60 }
61
62 #[must_use]
63 pub(crate) const fn attempt_count(&self) -> u32 {
64 self.attempt_count
65 }
66}
67
68#[inline]
70pub fn accumulated_proc_chance(
71 rng: &mut dyn FnMut() -> f64,
72 base_chance: f64,
73 failures: &mut u32,
74 cap: u32,
75) -> bool {
76 let mut tracker = AccumulatedRng::new(base_chance, cap, *failures);
77 let outcome = tracker.trigger(rng);
78
79 *failures = if matches!(outcome, AccumulatedOutcome::Failed) {
80 tracker.attempt_count()
81 } else {
82 0
83 };
84
85 !matches!(outcome, AccumulatedOutcome::Failed)
86}
87
88#[cfg(test)]
89mod tests {
90 use googletest::prelude::*;
91
92 use super::*;
93
94 #[gtest]
95 fn chance_increases_by_attempt_and_resets_after_success() -> Result<()> {
96 let rolls = [0.2, 0.2, 0.1, 0.05];
97 let mut index = 0;
98 let mut rng = || {
99 let roll = rolls[index];
100
101 index += 1;
102
103 roll
104 };
105 let mut tracker = AccumulatedRng::new(0.06, 0, 0);
106
107 verify_that!(tracker.trigger(&mut rng), eq(AccumulatedOutcome::Failed))?;
108 verify_that!(tracker.attempt_count(), eq(1))?;
109 verify_that!(tracker.trigger(&mut rng), eq(AccumulatedOutcome::Failed))?;
110 verify_that!(tracker.attempt_count(), eq(2))?;
111 verify_that!(tracker.trigger(&mut rng), eq(AccumulatedOutcome::Succeeded))?;
112 verify_that!(tracker.attempt_count(), eq(0))?;
113
114 verify_that!(tracker.trigger(&mut rng), eq(AccumulatedOutcome::Succeeded))
115 }
116
117 #[gtest]
118 fn cap_guarantees_attempt_and_reports_distinct_outcome() -> Result<()> {
119 let mut rng = || 0.999;
120 let mut tracker = AccumulatedRng::new(0.01, 3, 2);
121
122 verify_that!(
123 tracker.trigger(&mut rng),
124 eq(AccumulatedOutcome::Guaranteed)
125 )?;
126
127 verify_that!(tracker.attempt_count(), eq(0))
128 }
129
130 #[gtest]
131 fn long_run_has_no_attempt_past_guaranteed_cap() -> Result<()> {
132 let mut rng = || 0.999;
133 let mut tracker = AccumulatedRng::new(0.01, 7, 0);
134
135 for _ in 0..100 {
136 let outcome = tracker.trigger(&mut rng);
137
138 verify_that!(tracker.attempt_count(), le(6))?;
139
140 if outcome == AccumulatedOutcome::Guaranteed {
141 verify_that!(tracker.attempt_count(), eq(0))?;
142 }
143 }
144
145 Ok(())
146 }
147}