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}