1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
//! The data types required to work with randomness in Fire Emblem.
//!
//! The most important thing to know about randomness in Fire Emblem is that the
//! majority of the series does not display the true values underpinning the hit
//! rate or level up systems. Most games use a system that attempts to match
//! human psychology better by making unlikely events even less likely and
//! making likely events even more so, so a 90% hit rate may actually represent
//! a 99% chance to hit.
//!
//! The other thing to know is that Fire Emblem is fundamentally deterministic:
//! the same actions will result in the same outcomes. You can think of it as
//! playing Monopoly where, instead of rolling new dice for each turn, you roll
//! 1000 dice at the start of the game, and then every time you want to do
//! something with a die roll you simply read off the next values from the list.
//! You can see this with tools that let you save the game state during battles
//! or rewind time (at least the *Three Houses* version that preserves RNG): if
//! the next attack's critical number comes up as 1, then *any* attack you
//! choose to do on that turn with a critical rate of 1 or higher will crit.
//!
//! Different Fire Emblem games have different approaches to dealing with
//! randomness, and so a unified approach is difficult. This file tries to make
//! that easier.

/// One of the different RN systems used to compute hits and misses.
pub enum RNSystem {
    /// The honest approach: a 95% hit rate means a 95% chance of hitting, using
    /// a single random number for the calculation.
    OneRN,

    /// The hybrid approach used in *Fates* games: below 50%, one number is
    /// used, and above 50% one RNs is used but manipulated in a way that tries to
    /// split the difference between the 1RN and 2RN hit rates.
    FatesRN,

    /// The approach used in most Fire Emblem games: two numbers from 0-100 are
    /// used, and the average of those numbers is compared to the hit rate. This
    /// means that 90% listed hit rate corresponds to 99% hit rate (the chance
    /// two numbers 0-100 average to above 90 is much smaller than a single
    /// number being above 90).
    TwoRN,
}

impl RNSystem {
    /// Returns the true hit rate, as a number between 0 and 1, for a listed hit
    /// rate as described in the enum declaration.
    pub fn true_hit(&self, listed_hit: u32) -> f64 {
        let lh = listed_hit as f64;
        match self {
            RNSystem::OneRN => lh / 100.0,
            // there's no formula for this that's easier than just enumerating
            // the possibilities
            // if this is a performance bottleneck, just store the values,
            // there's only 101 of them
            RNSystem::TwoRN => {
                let mut num_hits = 0;
                for i in 0..100 {
                    for j in 0..100 {
                        if i + j < listed_hit * 2 {
                            num_hits += 1;
                        }
                    }
                }
                (num_hits as f64) / (100.0 * 100.0)
            }

            RNSystem::FatesRN => if listed_hit < 50 {
                lh / 100.0
            } else {
                // this is a weird formula!
                // https://www.reddit.com/r/fireemblem/comments/ae5666/echoes_absolutely_uses_fates_rn_bonus_explanation/
                (lh + ((4.0 / 30.0) * lh * ((0.02 * lh - 1.0) * 180.0).to_radians().sin())) / 100.0
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_onern_rng() {
        assert!((RNSystem::OneRN.true_hit(70) - 0.7).abs() <= 0.01);
    }

    #[test]
    fn test_fates_rng() {
        assert!((RNSystem::FatesRN.true_hit(70) - 0.7887).abs() <= 0.01);
    }

    #[test]
    fn test_tworn_rng() {
        assert!((RNSystem::TwoRN.true_hit(70) - 0.823).abs() <= 0.01);
    }
}