Skip to main content

sf_api/simulate/
mod.rs

1//! This simulator is based on Ernest Koguc's simulator (<https://github.com/ernest-koguc/sf-simulator>).
2//! Minor changes have been done to bridge the gap of the original garbage
3//! collected and object oriented design of the original into rust, but
4//! otherwise, this is a direct port. As such, all credit for this
5//! implementation, goes directly to him.
6//! Apart from this, `HafisCZ`'s sf-tools (<https://github.com/HafisCZ/sf-tools>)
7//! must also be credited. Sf-tools predates Ernest Koguc's sim and was/is used
8//! as a reference, both for results and code. The dungeon data used in the
9//! simulations has also been imported and converted directly from that source
10
11#![allow(
12    clippy::cast_possible_wrap,
13    clippy::cast_sign_loss,
14    clippy::cast_precision_loss,
15    clippy::cast_possible_truncation
16)]
17use std::sync::Arc;
18
19use enum_map::{Enum, EnumMap};
20use fastrand::Rng;
21use fighter::InBattleFighter;
22use strum::EnumIter;
23
24pub use crate::simulate::{
25    damage::DamageRange,
26    fighter::Fighter,
27    upgradeable::{PlayerFighterSquad, UpgradeableFighter},
28};
29use crate::{
30    command::AttributeType, gamestate::character::Class,
31    simulate::fighter::FighterIdent,
32};
33
34pub(crate) mod constants;
35mod damage;
36mod fighter;
37mod upgradeable;
38
39/// All the information about a weapon, that is relevant for battle simulations
40#[derive(Debug, Clone)]
41pub struct Weapon {
42    /// The effect amount of this rune
43    pub rune_value: i32,
44    /// The (battle relevant) type this rune has
45    pub rune_type: Option<Element>,
46    /// The amount of damage this weapon does
47    pub damage: DamageRange,
48}
49
50#[derive(Debug, Clone, Default)]
51pub struct FightSimulationResult {
52    /// The amount percentage of fights, that were won (0.0-1.0)
53    pub win_ratio: f64,
54    /// The amount of fights, that were won, out of the total amount of
55    /// iterations
56    pub won_fights: u32,
57}
58
59/// Simulates `iterations` many fights between both sides. The returned result
60/// will be from the perspective of the left side. A win ratio of 1.0 will mean,
61/// the left side win all fights.
62///
63/// Both sides are `Fighter`'s. These can be derived from `UpgradeableFighter`
64/// and `Monster`.
65///
66/// To obtain an `UpgradeableFighter`, we create a `PlayerFighterSquad`, which
67/// can then be turned into a fighter and be used in simulations.
68///
69/// ```rust
70/// use sf_api::{simulate::{Fighter, PlayerFighterSquad, UpgradeableFighter}, gamestate::GameState};
71/// let gs: GameState = GameState::default();
72/// let squad = PlayerFighterSquad::new(&gs);
73/// let player: UpgradeableFighter = squad.character;
74/// let fighter: Fighter = Fighter::from(&player);
75/// ```
76///
77/// We go through the `PlayerFighterSquad`, because calculating the stats for
78/// player + companion is pretty much as fast, as computing the stats for just
79/// the player. Similarely, we use `Fighter`, not `UpgradeableFighter`, because
80/// calculating the final stats of any fighter (attributes, rune values, etc)
81/// is work, that we would not want to do each time this function is invoked.
82///
83/// To obtain monsters, we use `current_enemy()` on Dungeons.
84///
85/// ```rust
86/// use sf_api::{simulate::{Fighter, UpgradeableFighter}, gamestate::{dungeons::LightDungeon, GameState}};
87/// let gs: GameState = GameState::default();
88/// let Some(monster) = gs.dungeons.current_enemy(LightDungeon::MinesOfGloria) else { return };
89/// let fighter: Fighter = Fighter::from(monster);
90/// ```
91#[must_use]
92pub fn simulate_battle(
93    left: &[Fighter],
94    right: &[Fighter],
95    iterations: u32,
96    is_arena_battle: bool,
97) -> FightSimulationResult {
98    if left.is_empty() || right.is_empty() {
99        return FightSimulationResult::default();
100    }
101
102    simulate_fight(left, right, iterations, is_arena_battle)
103}
104
105fn simulate_fight(
106    left: &[Fighter],
107    right: &[Fighter],
108    iterations: u32,
109    is_arena_battle: bool,
110) -> FightSimulationResult {
111    let mut cache = InBattleCache(Vec::new());
112    let mut won_fights = 0;
113    for _ in 0..iterations {
114        let fight_result =
115            perform_single_fight(left, right, is_arena_battle, &mut cache);
116        if fight_result == FightOutcome::SimulationBroken {
117            break;
118        }
119        if fight_result == FightOutcome::LeftSideWin {
120            won_fights += 1;
121        }
122    }
123
124    let win_ratio = f64::from(won_fights) / f64::from(iterations);
125    FightSimulationResult {
126        win_ratio,
127        won_fights,
128    }
129}
130
131struct InBattleCache(Vec<((FighterIdent, FighterIdent), InBattleFighter)>);
132
133impl InBattleCache {
134    pub fn get_or_insert(
135        &mut self,
136        this: &Fighter,
137        other: &Fighter,
138        is_arena_battle: bool,
139    ) -> InBattleFighter {
140        if self.0.len() > 10 {
141            return InBattleFighter::new(this, other, is_arena_battle);
142        }
143        let ident = (this.ident, other.ident);
144        if let Some(existing) = self.0.iter().find(|a| a.0 == ident) {
145            return existing.1.clone();
146        }
147        let new = InBattleFighter::new(this, other, is_arena_battle);
148        self.0.push((ident, new.clone()));
149        new
150    }
151}
152
153fn perform_single_fight(
154    left: &[Fighter],
155    right: &[Fighter],
156    is_arena_battle: bool,
157    cache: &mut InBattleCache,
158) -> FightOutcome {
159    let mut rng = Rng::new();
160
161    let mut left_side = left.iter().peekable();
162    let mut left_in_battle: Option<InBattleFighter> = None;
163
164    let mut right_side = right.iter().peekable();
165    let mut right_in_battle: Option<InBattleFighter> = None;
166
167    for _ in 0..500 {
168        let Some(left) = left_side.peek_mut() else {
169            return FightOutcome::RightSideWin;
170        };
171        let Some(right) = right_side.peek_mut() else {
172            return FightOutcome::LeftSideWin;
173        };
174
175        let (left_fighter, right_fighter) =
176            match (&mut left_in_battle, &mut right_in_battle) {
177                (Some(left), Some(right)) => {
178                    // Battle still ongoing between the same two opponents
179                    (left, right)
180                }
181                (None, None) => {
182                    // Battle just started
183                    (
184                        left_in_battle.insert(cache.get_or_insert(
185                            left,
186                            right,
187                            is_arena_battle,
188                        )),
189                        right_in_battle.insert(cache.get_or_insert(
190                            right,
191                            left,
192                            is_arena_battle,
193                        )),
194                    )
195                }
196                (None, Some(r)) => {
197                    r.update_opponent(right, left, is_arena_battle);
198                    (
199                        left_in_battle.insert(cache.get_or_insert(
200                            left,
201                            right,
202                            is_arena_battle,
203                        )),
204                        r,
205                    )
206                }
207                (Some(l), None) => {
208                    l.update_opponent(left, right, is_arena_battle);
209                    (
210                        l,
211                        right_in_battle.insert(cache.get_or_insert(
212                            right,
213                            left,
214                            is_arena_battle,
215                        )),
216                    )
217                }
218            };
219
220        // println!("{left_fighter:#?}");
221        // println!("{right_fighter:#?}");
222
223        let res = perform_fight(left_fighter, right_fighter, &mut rng);
224
225        match res {
226            FightOutcome::LeftSideWin => {
227                right_side.next();
228                right_in_battle = None;
229            }
230            FightOutcome::RightSideWin => {
231                left_side.next();
232                left_in_battle = None;
233            }
234            FightOutcome::SimulationBroken => {
235                return FightOutcome::SimulationBroken;
236            }
237        }
238    }
239
240    FightOutcome::SimulationBroken
241}
242
243fn perform_fight<'a>(
244    char_side: &'a mut InBattleFighter,
245    dungeon_side: &'a mut InBattleFighter,
246    rng: &mut Rng,
247) -> FightOutcome {
248    let char_side_starts =
249        char_side.reaction > dungeon_side.reaction || rng.bool();
250
251    let (attacker, defender) = if char_side_starts {
252        (char_side, dungeon_side)
253    } else {
254        (dungeon_side, char_side)
255    };
256
257    let round = &mut 0u32;
258
259    if attacker.attack_before_fight(defender, round, rng) {
260        return outcome_from_bool(char_side_starts);
261    }
262
263    if defender.attack_before_fight(attacker, round, rng) {
264        return outcome_from_bool(!char_side_starts);
265    }
266
267    // for sanity we limit max iters to a somewhat reasonable limit, that
268    // should never be hit
269    for _ in 0..1_000_000 {
270        let skip_round =
271            defender.will_skips_opponent_round(attacker, round, rng);
272        if !skip_round && attacker.attack(defender, round, rng) {
273            return outcome_from_bool(char_side_starts);
274        }
275
276        let skip_round =
277            attacker.will_skips_opponent_round(defender, round, rng);
278        if !skip_round && defender.attack(attacker, round, rng) {
279            return outcome_from_bool(!char_side_starts);
280        }
281    }
282    // TODO: Log
283    FightOutcome::SimulationBroken
284}
285
286fn outcome_from_bool(result: bool) -> FightOutcome {
287    if result {
288        FightOutcome::LeftSideWin
289    } else {
290        FightOutcome::RightSideWin
291    }
292}
293
294#[derive(Debug, Clone, Copy, PartialEq, Eq)]
295enum FightOutcome {
296    LeftSideWin,
297    RightSideWin,
298    SimulationBroken,
299}
300
301#[derive(Debug, Clone, Copy, Enum, EnumIter, Hash, PartialEq, Eq)]
302pub enum Element {
303    Fire,
304    Cold,
305    Lightning,
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct Monster {
310    pub name: &'static str,
311    pub level: u16,
312    pub class: Class,
313    pub attributes: EnumMap<AttributeType, u32>,
314    pub hp: u64,
315    pub min_dmg: u32,
316    pub max_dmg: u32,
317    pub armor: u32,
318    pub runes: Option<MonsterRunes>,
319}
320
321#[derive(Debug, Clone, PartialEq, Eq)]
322pub struct MonsterRunes {
323    pub damage_type: Element,
324    pub damage: i32,
325    pub resistances: EnumMap<Element, i32>,
326}