Skip to main content

wowlab_engine_rng/stochastic/
accumulated.rs

1use super::proc_chance;
2
3/// Result of one accumulated-RNG attempt.
4#[derive(Clone, Copy, Debug, Eq, PartialEq)]
5#[non_exhaustive]
6pub enum AccumulatedOutcome {
7    Failed,
8    Succeeded,
9    Guaranteed,
10}
11
12/// State for chance-times-attempt-count RNG with an optional guaranteed cap.
13#[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/// Rolls linearly accumulated RNG and resets the failure count after success.
69#[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}