parthia_lib/
simple_calc.rs

1//! This is a simple calculator that avoids the full complexity of the FE games
2//! (lifesteal, abilities, held items, personal weapons, etc.) to focus on the
3//! stats as they appear in all FE games, providing basic survival
4//! probabilities.
5
6use crate::fegame::FEGame;
7
8use serde::{Deserialize, Serialize};
9
10
11/// The stats needed for one side of combat: damage, hit, brave effect, and
12/// crit.
13#[derive(Default, Debug, Copy, Clone, Eq, Hash, PartialEq, Serialize, Deserialize)]
14pub struct CombatStats {
15    /// The damage dealt.
16    pub dmg: u32,
17
18    /// The hit probability (0-100).
19    pub hit: u32,
20
21    /// The critical probability (0-100).
22    pub crit: u32,
23
24    /// Whether the weapon strikes twice per normal strike. Although usually
25    /// called brave weapons, other weapons like gauntlets or the Amiti do this
26    /// as well.
27    pub is_brave: bool
28}
29
30impl CombatStats {
31    /// Computes possible outcomes for a single round of combat using the given
32    /// statistics. Doesn't deal with FE4 or FE5 crit damage correctly.
33    pub fn possible_outcomes(&self, game: FEGame, outcomes: Vec<Outcome>) -> Vec<Outcome> {
34        let after_one = self.after_single_strike(game, outcomes);
35        if self.is_brave {
36            // strike again
37            self.after_single_strike(game, after_one)
38        } else {
39            after_one
40        }
41    }
42
43    /// Returns the possible states after a single strike given the previous
44    /// possible states. Critical damage is not handled correctly in FE4 and
45    /// FE5.
46    fn after_single_strike(&self, game: FEGame, states: Vec<Outcome>) -> Vec<Outcome> {
47        let mut new_states = vec!();
48        for state in states {
49            if state.atk_hp == 0 {
50                // dead attackers can't do anything
51                new_states.push(state);
52            } else {
53                // three possibilities: miss, non-crit hit, and crit
54                let prob_hit = game.true_hit(self.hit);
55                let prob_miss = 1.0 - prob_hit;
56                let prob_crit = prob_hit * self.crit as f64 / 100.0;
57                let prob_reg_hit = prob_hit - prob_crit;
58
59                // if miss, nothing happens
60                new_states.push(Outcome{
61                    prob: state.prob * prob_miss,
62                    atk_hp: state.atk_hp,
63                    def_hp: state.def_hp
64                });
65
66                // if hit, normal damage: subtract damage, cannot go negative
67                new_states.push(Outcome{
68                    prob: state.prob * prob_reg_hit,
69                    atk_hp: state.atk_hp,
70                    def_hp: state.def_hp.saturating_sub(self.dmg)
71                });
72
73                // if crit, critical damage: FE4 and FE5 critical damage
74                // requires knowing Def, which we don't have, so we just do
75                // triple damage like normal
76                new_states.push(Outcome{
77                    prob: state.prob * prob_crit,
78                    atk_hp: state.atk_hp,
79                    def_hp: state.def_hp.saturating_sub(3 * self.dmg)
80                });
81            }
82        }
83        Outcome::collect(new_states)
84    }
85}
86
87#[derive(Eq, PartialEq, Hash, Clone, Copy, Debug, Serialize, Deserialize)]
88/// The results of different speed differentials between attacker (A) and
89/// defender (B), resulting in different attack patterns.
90pub enum SpeedDiff {
91    /// No one doubles: AB
92    Even,
93    /// Attacker doubles: ABA
94    AtkDoubles,
95    /// Defender doubles: ABB
96    DefDoubles,
97}
98
99#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
100/// The outcome of combat, with associated probability.
101pub struct Outcome {
102    pub prob: f64,
103    pub atk_hp: u32,
104    pub def_hp: u32,
105}
106
107impl Outcome {
108    /// Combines the probabilities of identical outcomes in the list of outcomes
109    /// and removes impossible outcomes, returning a new list with the same
110    /// total probabilities.
111    pub fn collect(outcomes: Vec<Outcome>) -> Vec<Outcome> {
112        outcomes.into_iter().filter(|x| x.prob != 0.0).fold(vec![], |acc, outcome| outcome.add_into(acc))
113    }
114
115    /// Adds the outcome to the list of outcomes, adding it to the probabliity
116    /// of an existing outcome if it's identical.
117    pub fn add_into(&self, outcomes: Vec<Outcome>) -> Vec<Outcome> {
118        let mut new_outcomes = vec!();
119        let mut has_added = false;
120        for outcome in outcomes {
121            if (self.atk_hp == outcome.atk_hp) && (self.def_hp == outcome.def_hp) {
122                new_outcomes.push(Outcome{
123                    prob: self.prob + outcome.prob,
124                    atk_hp: self.atk_hp,
125                    def_hp: self.def_hp,
126                });
127                has_added = true;
128            } else {
129                new_outcomes.push(outcome.clone());
130            }
131        }
132        if !has_added {
133            new_outcomes.push(self.clone());
134        }
135        new_outcomes
136    }
137
138    /// Switches attacker and defender.
139    pub fn switch(&self) -> Outcome {
140        Outcome{
141            prob: self.prob,
142            atk_hp: self.def_hp,
143            def_hp: self.atk_hp,
144        }
145    }
146}
147
148
149/// Returns a list of all of the possible outcomes of combat with associated
150/// probability, using the given game's rules.
151pub fn possible_outcomes(game: FEGame, atk: CombatStats, atk_hp: u32,
152                         def: CombatStats, def_hp: u32,
153                         speed: SpeedDiff) -> Vec<Outcome> {
154    let initial = vec!(Outcome{
155        prob: 1.0,
156        atk_hp,
157        def_hp,
158    });
159
160    let after_atk = atk.possible_outcomes(game, initial);
161    let after_def = def.possible_outcomes(
162        game,
163        after_atk.into_iter().map(|x| x.switch()).collect()
164    ).into_iter().map(|x| x.switch()).collect();
165
166    match speed {
167        SpeedDiff::Even => {
168            // AB attack pattern
169            after_def
170        },
171        SpeedDiff::AtkDoubles => {
172            // ABA attack pattern
173            atk.possible_outcomes(game, after_def)
174        },
175        SpeedDiff::DefDoubles => {
176            // ABB attack pattern
177            def.possible_outcomes(
178                game,
179                after_def.into_iter().map(|x| x.switch()).collect()
180            ).into_iter().map(|x| x.switch()).collect()
181        },
182    }
183}
184
185#[cfg(test)]
186mod tests {
187    use super::*;
188
189    #[test]
190    fn test_outcomes() {
191        dbg!(Outcome{prob: 1.0, atk_hp: 20, def_hp: 30}.add_into(vec!()));
192        dbg!(CombatStats{
193            dmg: 10, hit: 90, crit: 0, is_brave: false,
194        }.possible_outcomes(FEGame::FE15,
195                            vec![Outcome{prob: 1.0, atk_hp: 1, def_hp: 40}]));
196        dbg!(possible_outcomes(FEGame::FE15, CombatStats{
197            dmg: 10, hit: 50, crit: 0, is_brave: false,
198        }, 30, CombatStats{
199            dmg: 10, hit: 100, crit: 0, is_brave: false
200        }, 20, SpeedDiff::AtkDoubles));
201    }
202}