parthia_lib/
rng.rs

1//! The data types required to work with randomness in Fire Emblem.
2//!
3//! The most important thing to know about randomness in Fire Emblem is that the
4//! majority of the series does not display the true values underpinning the hit
5//! rate or level up systems. Most games use a system that attempts to match
6//! human psychology better by making unlikely events even less likely and
7//! making likely events even more so, so a 90% hit rate may actually represent
8//! a 99% chance to hit.
9//!
10//! The other thing to know is that Fire Emblem is fundamentally deterministic:
11//! the same actions will result in the same outcomes. You can think of it as
12//! playing Monopoly where, instead of rolling new dice for each turn, you roll
13//! 1000 dice at the start of the game, and then every time you want to do
14//! something with a die roll you simply read off the next values from the list.
15//! You can see this with tools that let you save the game state during battles
16//! or rewind time (at least the *Three Houses* version that preserves RNG): if
17//! the next attack's critical number comes up as 1, then *any* attack you
18//! choose to do on that turn with a critical rate of 1 or higher will crit.
19//!
20//! Different Fire Emblem games have different approaches to dealing with
21//! randomness, and so a unified approach is difficult. This file tries to make
22//! that easier.
23
24/// One of the different RN systems used to compute hits and misses.
25pub enum RNSystem {
26    /// The honest approach: a 95% hit rate means a 95% chance of hitting, using
27    /// a single random number for the calculation.
28    OneRN,
29
30    /// The hybrid approach used in *Fates* games: below 50%, one number is
31    /// used, and above 50% one RNs is used but manipulated in a way that tries to
32    /// split the difference between the 1RN and 2RN hit rates.
33    FatesRN,
34
35    /// The approach used in most Fire Emblem games: two numbers from 0-100 are
36    /// used, and the average of those numbers is compared to the hit rate. This
37    /// means that 90% listed hit rate corresponds to 99% hit rate (the chance
38    /// two numbers 0-100 average to above 90 is much smaller than a single
39    /// number being above 90).
40    TwoRN,
41}
42
43impl RNSystem {
44    /// Returns the true hit rate, as a number between 0 and 1, for a listed hit
45    /// rate as described in the enum declaration.
46    pub fn true_hit(&self, listed_hit: u32) -> f64 {
47        let lh = listed_hit as f64;
48        match self {
49            RNSystem::OneRN => lh / 100.0,
50            // there's no formula for this that's easier than just enumerating
51            // the possibilities
52            // if this is a performance bottleneck, just store the values,
53            // there's only 101 of them
54            RNSystem::TwoRN => {
55                let mut num_hits = 0;
56                for i in 0..100 {
57                    for j in 0..100 {
58                        if i + j < listed_hit * 2 {
59                            num_hits += 1;
60                        }
61                    }
62                }
63                (num_hits as f64) / (100.0 * 100.0)
64            }
65
66            RNSystem::FatesRN => if listed_hit < 50 {
67                lh / 100.0
68            } else {
69                // this is a weird formula!
70                // https://www.reddit.com/r/fireemblem/comments/ae5666/echoes_absolutely_uses_fates_rn_bonus_explanation/
71                (lh + ((4.0 / 30.0) * lh * ((0.02 * lh - 1.0) * 180.0).to_radians().sin())) / 100.0
72            }
73        }
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_onern_rng() {
83        assert!((RNSystem::OneRN.true_hit(70) - 0.7).abs() <= 0.01);
84    }
85
86    #[test]
87    fn test_fates_rng() {
88        assert!((RNSystem::FatesRN.true_hit(70) - 0.7887).abs() <= 0.01);
89    }
90
91    #[test]
92    fn test_tworn_rng() {
93        assert!((RNSystem::TwoRN.true_hit(70) - 0.823).abs() <= 0.01);
94    }
95}