From 034776535a60afbcfe64bf264d0b59e8a9f0f4d1 Mon Sep 17 00:00:00 2001 From: KonaeAkira Date: Tue, 19 Nov 2024 18:52:19 +0100 Subject: [PATCH 1/2] Add verified simulator test --- simulator/tests/simulator_tests.rs | 301 +++++++++++++++++------------ 1 file changed, 174 insertions(+), 127 deletions(-) diff --git a/simulator/tests/simulator_tests.rs b/simulator/tests/simulator_tests.rs index 59c9e37..e59e02e 100644 --- a/simulator/tests/simulator_tests.rs +++ b/simulator/tests/simulator_tests.rs @@ -3,14 +3,23 @@ use simulator::{Action, ActionMask, Condition, Settings, SimulationState}; fn simulate( settings: &Settings, steps: impl Iterator, -) -> Result, &'static str> { - let mut state = SimulationState::new(&settings); - let mut result = Vec::new(); +) -> Vec { + let mut current_state = SimulationState::new(&settings); + let mut states = Vec::new(); for (action, condition) in steps { - state = state.use_action(action, condition, &settings)?; - result.push(state); + current_state = current_state + .use_action(action, condition, &settings) + .unwrap(); + states.push(current_state); } - Ok(result) + states +} + +fn simulate_normal( + settings: &Settings, + actions: impl Iterator, +) -> Vec { + simulate(settings, actions.zip(std::iter::repeat(Condition::Normal))) } #[test] @@ -33,13 +42,7 @@ fn test_random_926ae85b() { Action::BasicTouch, Action::BasicTouch, ]; - let simulation = simulate( - &settings, - actions - .into_iter() - .zip(std::iter::repeat(Condition::Normal)), - ); - let state = simulation.unwrap().last().copied().unwrap(); + let state = SimulationState::from_macro(&settings, &actions).unwrap(); assert_eq!(state.cp, 14); assert_eq!(state.durability, 30); assert_eq!(state.progress, 4); @@ -72,13 +75,7 @@ fn test_random_3c721e47() { Action::PreparatoryTouch, Action::PrudentTouch, ]; - let simulation = simulate( - &settings, - actions - .into_iter() - .zip(std::iter::repeat(Condition::Normal)), - ); - let state = simulation.unwrap().last().copied().unwrap(); + let state = SimulationState::from_macro(&settings, &actions).unwrap(); assert_eq!(state.cp, 223); assert_eq!(state.durability, 60); assert_eq!(state.progress, 2520); @@ -111,13 +108,7 @@ fn test_random_3ba90d3a() { Action::BasicTouch, Action::ComboStandardTouch, ]; - let simulation = simulate( - &settings, - actions - .into_iter() - .zip(std::iter::repeat(Condition::Normal)), - ); - let state = simulation.unwrap().last().copied().unwrap(); + let state = SimulationState::from_macro(&settings, &actions).unwrap(); assert_eq!(state.cp, 188); assert_eq!(state.durability, 25); assert_eq!(state.progress, 918); @@ -169,13 +160,7 @@ fn test_random_bce2650c() { Action::GreatStrides, Action::ByregotsBlessing, ]; - let simulation = simulate( - &settings, - actions - .into_iter() - .zip(std::iter::repeat(Condition::Normal)), - ); - let state = simulation.unwrap().last().copied().unwrap(); + let state = SimulationState::from_macro(&settings, &actions).unwrap(); assert_eq!(state.cp, 1); assert_eq!(state.durability, 5); assert_eq!(state.progress, 6323); @@ -197,7 +182,7 @@ fn test_ingame_be9fc5c2() { allowed_actions: ActionMask::all(), adversarial: false, }; - let states: Vec<(u16, u16)> = simulate( + let states = simulate( &settings, [ (Action::Reflect, Condition::Normal), // 0, 795 @@ -221,33 +206,35 @@ fn test_ingame_be9fc5c2() { (Action::DelicateSynthesis, Condition::Normal), // 1703, 10456 ] .into_iter(), - ) - .unwrap() - .into_iter() - .map(|state| (state.progress, state.quality)) - .collect(); - let expected = [ - (0, 795), - (0, 795), - (0, 2703), - (0, 2703), - (0, 3445), - (0, 3445), - (0, 4293), - (0, 4293), - (0, 5008), - (0, 5952), - (0, 7144), - (0, 7939), - (0, 7939), - (0, 7939), - (0, 7939), - (0, 9926), - (0, 9926), - (1333, 9926), - (1703, 10456), - ]; - assert_eq!(states, expected); + ); + let primary_stats: Vec<_> = states + .into_iter() + .map(|state| (state.progress, state.quality)) + .collect(); + assert_eq!( + primary_stats, + [ + (0, 795), + (0, 795), + (0, 2703), + (0, 2703), + (0, 3445), + (0, 3445), + (0, 4293), + (0, 4293), + (0, 5008), + (0, 5952), + (0, 7144), + (0, 7939), + (0, 7939), + (0, 7939), + (0, 7939), + (0, 9926), + (0, 9926), + (1333, 9926), + (1703, 10456), + ] + ); } #[test] @@ -280,31 +267,28 @@ fn test_ingame_d11d9c68() { Action::GreatStrides, Action::ByregotsBlessing, ]; - let states: Vec<(u16, u16)> = simulate( - &settings, - actions - .into_iter() - .zip(std::iter::repeat(Condition::Normal)), - ) - .unwrap() - .into_iter() - .map(|state| (state.progress, state.quality)) - .collect(); - let expected = [ - (0, 900), - (0, 900), - (0, 1980), - (0, 2610), - (0, 2610), - (0, 4860), - (0, 4860), - (0, 4860), - (0, 7410), - (0, 7410), - (0, 7410), - (0, 11400), - ]; - assert_eq!(states, expected); + let states = simulate_normal(&settings, actions.into_iter()); + let primary_stats: Vec<_> = states + .into_iter() + .map(|state| (state.progress, state.quality)) + .collect(); + assert_eq!( + primary_stats, + [ + (0, 900), + (0, 900), + (0, 1980), + (0, 2610), + (0, 2610), + (0, 4860), + (0, 4860), + (0, 4860), + (0, 7410), + (0, 7410), + (0, 7410), + (0, 11400), + ] + ); } #[test] @@ -354,45 +338,108 @@ fn test_ingame_f9f0dac7() { Action::Veneration, Action::Groundwork, ]; - let states: Vec<(u16, u16)> = simulate( - &settings, - actions - .into_iter() - .zip(std::iter::repeat(Condition::Normal)), - ) - .unwrap() - .into_iter() - .map(|state| (state.progress, state.quality)) - .collect(); - let expected = [ - (0, 720), - (0, 720), - (0, 936), - (0, 936), - (0, 1386), - (0, 2016), - (0, 2826), - (0, 3978), - (0, 3978), - (0, 3978), - (0, 4302), - (0, 4986), - (0, 5886), - (0, 6966), - (0, 6966), - (0, 7326), - (0, 8226), - (0, 9306), - (0, 11466), - (0, 11466), - (0, 11466), - (0, 11466), - (1409, 11466), - (2818, 11466), - (4227, 11466), - (5636, 11466), - (5636, 11466), - (7045, 11466), + let states = simulate_normal(&settings, actions.into_iter()); + let primary_stats: Vec<_> = states + .into_iter() + .map(|state| (state.progress, state.quality)) + .collect(); + assert_eq!( + primary_stats, + [ + (0, 720), + (0, 720), + (0, 936), + (0, 936), + (0, 1386), + (0, 2016), + (0, 2826), + (0, 3978), + (0, 3978), + (0, 3978), + (0, 4302), + (0, 4986), + (0, 5886), + (0, 6966), + (0, 6966), + (0, 7326), + (0, 8226), + (0, 9306), + (0, 11466), + (0, 11466), + (0, 11466), + (0, 11466), + (1409, 11466), + (2818, 11466), + (4227, 11466), + (5636, 11466), + (5636, 11466), + (7045, 11466), + ] + ); +} + +#[test] +fn test_ingame_4866545e() { + // Maraging Steel Ingot + // Lv. 100, 5300 Craftsmanship, 4601 Control (7.1 TC High Tier melds + Specialist with no hat or earring equipped) + // Verfied in-game (patch 7.1) + let settings = Settings { + max_cp: 540, + max_durability: 35, + max_progress: 4125, + max_quality: 12000, + base_progress: 282, + base_quality: 256, + job_level: 100, + allowed_actions: ActionMask::all(), + adversarial: false, + }; + let actions = [ + Action::Reflect, + Action::Manipulation, + Action::Innovation, + Action::BasicTouch, + Action::ComboRefinedTouch, + Action::PrudentTouch, + Action::PrudentTouch, + Action::Innovation, + Action::BasicTouch, + Action::ComboStandardTouch, + Action::ComboAdvancedTouch, + Action::Manipulation, + Action::TrainedPerfection, + Action::GreatStrides, + Action::Innovation, + Action::PreparatoryTouch, + Action::GreatStrides, + Action::ByregotsBlessing, ]; - assert_eq!(states, expected); + let states = simulate_normal(&settings, actions.into_iter()); + let primary_stats: Vec<_> = states + .into_iter() + .map(|state| (state.progress, state.quality)) + .collect(); + assert_eq!( + primary_stats, + [ + (0, 768), + (0, 768), + (0, 768), + (0, 1228), + (0, 1727), + (0, 2303), + (0, 2917), + (0, 2917), + (0, 3569), + (0, 4433), + (0, 5527), + (0, 5527), + (0, 5527), + (0, 5527), + (0, 5527), + (0, 8087), + (0, 8087), + (0, 11927) + ] + ); } From 11d1320b33b726cbb34ca8d7b97263de8b0a13df Mon Sep 17 00:00:00 2001 From: KonaeAkira Date: Tue, 19 Nov 2024 18:55:51 +0100 Subject: [PATCH 2/2] Action system rework to enable custom actions --- .github/workflows/rust.yml | 2 +- Cargo.toml | 6 +- game_data/src/lib.rs | 2 +- game_data/src/locales.rs | 24 +- game_data/tests/test_get_game_settings.rs | 10 +- simulator/src/actions.rs | 1160 ++++++++++++----- simulator/src/lib.rs | 2 +- simulator/src/settings.rs | 27 +- simulator/src/state.rs | 263 ++-- simulator/tests/action_tests.rs | 652 ++++++--- simulator/tests/adversarial_tests.rs | 6 +- simulator/tests/effect_tests.rs | 46 + simulator/tests/simulator_tests.rs | 54 +- solvers/examples/macro_solver_example.rs | 2 +- solvers/src/actions.rs | 4 +- solvers/src/branch_pruning.rs | 2 +- solvers/src/lib.rs | 20 +- solvers/src/macro_solver/fast_lower_bound.rs | 13 +- solvers/src/macro_solver/solver.rs | 8 +- .../src/quality_upper_bound_solver/solver.rs | 190 ++- .../src/quality_upper_bound_solver/state.rs | 88 +- solvers/src/step_lower_bound_solver/solver.rs | 80 +- solvers/src/step_lower_bound_solver/state.rs | 32 +- solvers/tests/01_normal_solver_tests.rs | 68 +- .../02_progress_backload_solver_tests.rs | 72 +- ..._unsound_progress_backload_solver_tests.rs | 72 +- solvers/tests/04_adversarial_solver_tests.rs | 28 +- src/app.rs | 12 +- 28 files changed, 1874 insertions(+), 1071 deletions(-) create mode 100644 simulator/tests/effect_tests.rs diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 983c67f..b511154 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -16,4 +16,4 @@ jobs: steps: - uses: actions/checkout@v3 - name: Run tests - run: cargo test --verbose --release --package simulator --package solvers --package game-data + run: cargo test --verbose --package simulator --package solvers --package game-data diff --git a/Cargo.toml b/Cargo.toml index 360657a..e7054cd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ members = ["simulator", "solvers", "game_data", "raphael-cli"] [package] name = "raphael-xiv" -version = "0.14.3" +version = "0.15.0" edition = "2021" default-run = "raphael-xiv" @@ -37,3 +37,7 @@ path = "src/webworker.rs" [profile.release] lto = "fat" + +[profile.test] +opt-level = 3 +overflow-checks = true diff --git a/game_data/src/lib.rs b/game_data/src/lib.rs index 3041b65..4661b2d 100644 --- a/game_data/src/lib.rs +++ b/game_data/src/lib.rs @@ -77,7 +77,7 @@ pub fn get_game_settings( base_quality = base_quality * rlvl.quality_mod as f32 / 100.0; } - let mut allowed_actions = ActionMask::from_level(crafter_stats.level as _); + let mut allowed_actions = ActionMask::all(); if !crafter_stats.manipulation { allowed_actions = allowed_actions.remove(Action::Manipulation); } diff --git a/game_data/src/locales.rs b/game_data/src/locales.rs index 129d171..6edd2d0 100644 --- a/game_data/src/locales.rs +++ b/game_data/src/locales.rs @@ -94,7 +94,7 @@ const fn action_name_en(action: Action) -> &'static str { Action::Observe => "Observe", Action::WasteNot => "Waste Not", Action::Veneration => "Veneration", - Action::StandardTouch | Action::ComboStandardTouch => "Standard Touch", + Action::StandardTouch => "Standard Touch", Action::GreatStrides => "Great Strides", Action::Innovation => "Innovation", Action::WasteNot2 => "Waste Not II", @@ -104,7 +104,7 @@ const fn action_name_en(action: Action) -> &'static str { Action::CarefulSynthesis => "Careful Synthesis", Action::Manipulation => "Manipulation", Action::PrudentTouch => "Prudent Touch", - Action::AdvancedTouch | Action::ComboAdvancedTouch => "Advanced Touch", + Action::AdvancedTouch => "Advanced Touch", Action::Reflect => "Reflect", Action::PreparatoryTouch => "Preparatory Touch", Action::Groundwork => "Groundwork", @@ -113,7 +113,7 @@ const fn action_name_en(action: Action) -> &'static str { Action::HeartAndSoul => "Heart and Soul", Action::PrudentSynthesis => "Prudent Synthesis", Action::TrainedFinesse => "Trained Finesse", - Action::ComboRefinedTouch => "Refined Touch", + Action::RefinedTouch => "Refined Touch", Action::ImmaculateMend => "Immaculate Mend", Action::TrainedPerfection => "Trained Perfection", Action::TrainedEye => "Trained Eye", @@ -129,7 +129,7 @@ const fn action_name_de(action: Action) -> &'static str { Action::Observe => "Beobachten", Action::WasteNot => "Nachhaltigkeit", Action::Veneration => "Ehrfurcht", - Action::StandardTouch | Action::ComboStandardTouch => "Solide Veredelung", + Action::StandardTouch => "Solide Veredelung", Action::GreatStrides => "Große Schritte", Action::Innovation => "Innovation", Action::WasteNot2 => "Nachhaltigkeit II", @@ -139,7 +139,7 @@ const fn action_name_de(action: Action) -> &'static str { Action::CarefulSynthesis => "Sorgfältige Bearbeitung", Action::Manipulation => "Manipulation", Action::PrudentTouch => "Nachhaltige Veredelung", - Action::AdvancedTouch | Action::ComboAdvancedTouch => "Höhere Veredelung", + Action::AdvancedTouch => "Höhere Veredelung", Action::Reflect => "Einkehr", Action::PreparatoryTouch => "Basisveredelung", Action::Groundwork => "Vorarbeit", @@ -148,7 +148,7 @@ const fn action_name_de(action: Action) -> &'static str { Action::HeartAndSoul => "Mit Leib und Seele", Action::PrudentSynthesis => "Rationelle Bearbeitung", Action::TrainedFinesse => "Götter Werk", - Action::ComboRefinedTouch => "Raffinierte Veredelung", + Action::RefinedTouch => "Raffinierte Veredelung", Action::ImmaculateMend => "Winkelzug", Action::TrainedPerfection => "Meisters Beitrag", Action::TrainedEye => "Flinke Hand", @@ -164,7 +164,7 @@ const fn action_name_fr(action: Action) -> &'static str { Action::Observe => "Observation", Action::WasteNot => "Parcimonie", Action::Veneration => "Vénération", - Action::StandardTouch | Action::ComboStandardTouch => "Ouvrage standard", + Action::StandardTouch => "Ouvrage standard", Action::GreatStrides => "Grands progrès", Action::Innovation => "Innovation", Action::WasteNot2 => "Parcimonie pérenne", @@ -174,7 +174,7 @@ const fn action_name_fr(action: Action) -> &'static str { Action::CarefulSynthesis => "Travail prudent", Action::Manipulation => "Manipulation", Action::PrudentTouch => "Ouvrage parcimonieux", - Action::AdvancedTouch | Action::ComboAdvancedTouch => "Ouvrage avancé", + Action::AdvancedTouch => "Ouvrage avancé", Action::Reflect => "Véritable valeur", Action::PreparatoryTouch => "Ouvrage préparatoire", Action::Groundwork => "Travail préparatoire", @@ -183,7 +183,7 @@ const fn action_name_fr(action: Action) -> &'static str { Action::HeartAndSoul => "Attention totale", Action::PrudentSynthesis => "Travail économe", Action::TrainedFinesse => "Main divine", - Action::ComboRefinedTouch => "Ouvrage raffiné", + Action::RefinedTouch => "Ouvrage raffiné", Action::ImmaculateMend => "Réparation totale", Action::TrainedPerfection => "Main suprême", Action::TrainedEye => "Main preste", @@ -199,7 +199,7 @@ const fn action_name_jp(action: Action) -> &'static str { Action::Observe => "経過観察", Action::WasteNot => "倹約", Action::Veneration => "ヴェネレーション", - Action::StandardTouch | Action::ComboStandardTouch => "中級加工", + Action::StandardTouch => "中級加工", Action::GreatStrides => "グレートストライド", Action::Innovation => "イノベーション", Action::WasteNot2 => "長期倹約", @@ -209,7 +209,7 @@ const fn action_name_jp(action: Action) -> &'static str { Action::CarefulSynthesis => "模範作業", Action::Manipulation => "マニピュレーション", Action::PrudentTouch => "倹約加工", - Action::AdvancedTouch | Action::ComboAdvancedTouch => "上級加工", + Action::AdvancedTouch => "上級加工", Action::Reflect => "真価", Action::PreparatoryTouch => "下地加工", Action::Groundwork => "下地作業", @@ -218,7 +218,7 @@ const fn action_name_jp(action: Action) -> &'static str { Action::HeartAndSoul => "一心不乱", Action::PrudentSynthesis => "倹約作業", Action::TrainedFinesse => "匠の神業", - Action::ComboRefinedTouch => "洗練加工", + Action::RefinedTouch => "洗練加工", Action::ImmaculateMend => "パーフェクトメンド", Action::TrainedPerfection => "匠の絶技", Action::TrainedEye => "匠の早業", diff --git a/game_data/tests/test_get_game_settings.rs b/game_data/tests/test_get_game_settings.rs index 661a25a..b42de86 100644 --- a/game_data/tests/test_get_game_settings.rs +++ b/game_data/tests/test_get_game_settings.rs @@ -56,7 +56,7 @@ fn test_roast_chicken() { base_progress: 264, base_quality: 274, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -98,7 +98,7 @@ fn test_turali_pineapple_ponzecake() { base_progress: 280, base_quality: 355, job_level: 94, - allowed_actions: ActionMask::from_level(94) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::QuickInnovation), adversarial: false, @@ -140,7 +140,7 @@ fn test_smaller_water_otter_hardware() { base_quality: 260, job_level: 100, // Trained Eye is not available for expert recipes - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -183,7 +183,7 @@ fn test_grade_8_tincture() { base_quality: 387, job_level: 100, // Trained Eye is available - allowed_actions: ActionMask::from_level(100).remove(Action::QuickInnovation), + allowed_actions: ActionMask::all().remove(Action::QuickInnovation), adversarial: false, } ); @@ -218,7 +218,7 @@ fn test_claro_walnut_spinning_wheel() { base_progress: 241, base_quality: 304, job_level: 99, - allowed_actions: ActionMask::from_level(99) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul), adversarial: false, diff --git a/simulator/src/actions.rs b/simulator/src/actions.rs index 47d82a2..f3e2531 100755 --- a/simulator/src/actions.rs +++ b/simulator/src/actions.rs @@ -1,351 +1,809 @@ -use crate::{Condition, Effects, SingleUse}; - -use super::Settings; - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] -pub enum Action { - BasicSynthesis, - BasicTouch, - MasterMend, - Observe, - WasteNot, - Veneration, - StandardTouch, // out-of-combo version - ComboStandardTouch, - GreatStrides, - Innovation, - WasteNot2, - ByregotsBlessing, - PreciseTouch, - MuscleMemory, - CarefulSynthesis, - Manipulation, - PrudentTouch, - AdvancedTouch, // out-of-combo version - ComboAdvancedTouch, - Reflect, - PreparatoryTouch, - Groundwork, - DelicateSynthesis, - IntensiveSynthesis, - TrainedEye, - HeartAndSoul, - PrudentSynthesis, - TrainedFinesse, - ComboRefinedTouch, - QuickInnovation, - ImmaculateMend, - TrainedPerfection, -} - -#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] -pub enum Combo { - None, - SynthesisBegin, - BasicTouch, - StandardTouch, -} - -impl Combo { - pub const fn into_bits(self) -> u8 { - match self { - Self::None => 0, - Self::SynthesisBegin => 1, - Self::BasicTouch => 2, - Self::StandardTouch => 3, - } - } - - pub const fn from_bits(value: u8) -> Self { - match value { - 1 => Self::SynthesisBegin, - 2 => Self::BasicTouch, - 3 => Self::StandardTouch, - _ => Self::None, - } - } -} - -impl Action { - pub const fn level_requirement(self) -> u8 { - match self { - Action::BasicSynthesis => 1, - Action::BasicTouch => 5, - Action::MasterMend => 7, - Action::Observe => 13, - Action::WasteNot => 15, - Action::Veneration => 15, - Action::StandardTouch => 18, - Action::ComboStandardTouch => 18, - Action::GreatStrides => 21, - Action::Innovation => 26, - Action::WasteNot2 => 47, - Action::ByregotsBlessing => 50, - Action::PreciseTouch => 53, - Action::MuscleMemory => 54, - Action::CarefulSynthesis => 62, - Action::Manipulation => 65, - Action::PrudentTouch => 66, - Action::AdvancedTouch => 68, - Action::ComboAdvancedTouch => 68, - Action::Reflect => 69, - Action::PreparatoryTouch => 71, - Action::Groundwork => 72, - Action::DelicateSynthesis => 76, - Action::IntensiveSynthesis => 78, - Action::TrainedEye => 80, - Action::HeartAndSoul => 86, - Action::PrudentSynthesis => 88, - Action::TrainedFinesse => 90, - Action::ComboRefinedTouch => 92, - Action::QuickInnovation => 96, - Action::ImmaculateMend => 98, - Action::TrainedPerfection => 100, - } - } - - pub const fn time_cost(self) -> i16 { - match self { - Action::BasicSynthesis => 3, - Action::BasicTouch => 3, - Action::MasterMend => 3, - Action::Observe => 3, - Action::WasteNot => 2, - Action::Veneration => 2, - Action::StandardTouch => 3, - Action::ComboStandardTouch => 3, - Action::GreatStrides => 2, - Action::Innovation => 2, - Action::WasteNot2 => 2, - Action::ByregotsBlessing => 3, - Action::PreciseTouch => 3, - Action::MuscleMemory => 3, - Action::CarefulSynthesis => 3, - Action::Manipulation => 2, - Action::PrudentTouch => 3, - Action::Reflect => 3, - Action::PreparatoryTouch => 3, - Action::Groundwork => 3, - Action::DelicateSynthesis => 3, - Action::IntensiveSynthesis => 3, - Action::AdvancedTouch => 3, - Action::ComboAdvancedTouch => 3, - Action::HeartAndSoul => 3, - Action::PrudentSynthesis => 3, - Action::TrainedFinesse => 3, - Action::ComboRefinedTouch => 3, - Action::ImmaculateMend => 3, - Action::TrainedPerfection => 3, - Action::TrainedEye => 3, - Action::QuickInnovation => 3, - } - } - - pub const fn cp_cost(self) -> i16 { - match self { - Action::BasicSynthesis => 0, - Action::BasicTouch => 18, - Action::MasterMend => 88, - Action::Observe => 7, - Action::WasteNot => 56, - Action::Veneration => 18, - Action::StandardTouch => 32, - Action::ComboStandardTouch => 18, - Action::GreatStrides => 32, - Action::Innovation => 18, - Action::WasteNot2 => 98, - Action::ByregotsBlessing => 24, - Action::PreciseTouch => 18, - Action::MuscleMemory => 6, - Action::CarefulSynthesis => 7, - Action::Manipulation => 96, - Action::PrudentTouch => 25, - Action::Reflect => 6, - Action::PreparatoryTouch => 40, - Action::Groundwork => 18, - Action::DelicateSynthesis => 32, - Action::IntensiveSynthesis => 6, - Action::AdvancedTouch => 46, - Action::ComboAdvancedTouch => 18, - Action::HeartAndSoul => 0, - Action::PrudentSynthesis => 18, - Action::TrainedFinesse => 32, - Action::ComboRefinedTouch => 24, - Action::ImmaculateMend => 112, - Action::TrainedPerfection => 0, - Action::TrainedEye => 250, - Action::QuickInnovation => 0, - } - } - - pub const fn base_durability_cost(self) -> i8 { - match self { - Action::BasicSynthesis => 10, - Action::BasicTouch => 10, - Action::MasterMend => 0, - Action::Observe => 0, - Action::WasteNot => 0, - Action::Veneration => 0, - Action::StandardTouch => 10, - Action::ComboStandardTouch => 10, - Action::GreatStrides => 0, - Action::Innovation => 0, - Action::WasteNot2 => 0, - Action::ByregotsBlessing => 10, - Action::PreciseTouch => 10, - Action::MuscleMemory => 10, - Action::CarefulSynthesis => 10, - Action::Manipulation => 0, - Action::PrudentTouch => 5, - Action::Reflect => 10, - Action::PreparatoryTouch => 20, - Action::Groundwork => 20, - Action::DelicateSynthesis => 10, - Action::IntensiveSynthesis => 10, - Action::AdvancedTouch => 10, - Action::ComboAdvancedTouch => 10, - Action::HeartAndSoul => 0, - Action::PrudentSynthesis => 5, - Action::TrainedFinesse => 0, - Action::ComboRefinedTouch => 10, - Action::ImmaculateMend => 0, - Action::TrainedPerfection => 0, - Action::TrainedEye => 0, - Action::QuickInnovation => 0, - } - } - - pub const fn durability_cost(self, effects: &Effects) -> i8 { - if matches!(effects.trained_perfection(), SingleUse::Active) { - return 0; - } - match effects.waste_not() { - 0 => self.base_durability_cost(), - _ => (self.base_durability_cost() + 1) / 2, - } - } - - pub const fn progress_efficiency(self, job_level: u8) -> u64 { - match self { - Action::BasicSynthesis => { - if job_level < 31 { - 100 - } else { - 120 - } - } - Action::MuscleMemory => 300, - Action::CarefulSynthesis => { - if job_level < 82 { - 150 - } else { - 180 - } - } - Action::Groundwork => { - if job_level < 86 { - 300 - } else { - 360 - } - } - Action::DelicateSynthesis => { - if job_level < 94 { - 100 - } else { - 150 - } - } - Action::IntensiveSynthesis => 400, - Action::PrudentSynthesis => 180, - _ => 0, - } - } - - pub const fn progress_increase(self, settings: &Settings, effects: &Effects) -> u16 { - let efficiency_mod = self.progress_efficiency(settings.job_level); - let mut effect_mod = 100; - if effects.muscle_memory() > 0 { - effect_mod += 100; - } - if effects.veneration() > 0 { - effect_mod += 50; - } - (settings.base_progress as u64 * efficiency_mod * effect_mod / 10000) as u16 - } - - pub const fn quality_efficiency(self, inner_quiet: u8) -> u64 { - match self { - Action::BasicTouch => 100, - Action::StandardTouch => 125, - Action::ComboStandardTouch => 125, - Action::PreciseTouch => 150, - Action::PrudentTouch => 100, - Action::Reflect => 300, - Action::PreparatoryTouch => 200, - Action::DelicateSynthesis => 100, - Action::AdvancedTouch => 150, - Action::ComboAdvancedTouch => 150, - Action::TrainedFinesse => 100, - Action::ComboRefinedTouch => 100, - Action::ByregotsBlessing => 100 + 20 * inner_quiet as u64, - _ => 0, - } - } - - pub const fn quality_increase( - self, - settings: &Settings, - effects: &Effects, - condition: Condition, - ) -> u16 { - if matches!(self, Action::TrainedEye) { - return settings.max_quality; - } - let efficieny_mod = self.quality_efficiency(effects.inner_quiet()); - let condition_mod = match condition { - Condition::Good => 150, - Condition::Excellent => 400, - Condition::Poor => 50, - _ => 100, - }; - let mut effect_mod = 100; - if effects.innovation() != 0 { - effect_mod += 50; - } - if effects.great_strides() != 0 { - effect_mod += 100; - } - let inner_quiet_mod = 100 + 10 * effects.inner_quiet() as u64; - (settings.base_quality as u64 - * efficieny_mod - * condition_mod - * effect_mod - * inner_quiet_mod - / 100000000) as u16 - } - - pub const fn combo_fulfilled(self, combo: Combo) -> bool { - match self { - Action::Reflect | Action::MuscleMemory | Action::TrainedEye => { - matches!(combo, Combo::SynthesisBegin) - } - Action::ComboStandardTouch => matches!(combo, Combo::BasicTouch), - Action::ComboAdvancedTouch => { - matches!(combo, Combo::StandardTouch) - } - Action::ComboRefinedTouch => matches!(combo, Combo::BasicTouch), - _ => true, - } - } - - pub const fn to_combo(self) -> Combo { - match self { - Action::BasicTouch => Combo::BasicTouch, - Action::ComboStandardTouch => Combo::StandardTouch, - // Observe and StandardTouch unlock the same action (ComboAdvancedTouch) - Action::Observe => Combo::StandardTouch, - _ => Combo::None, - } - } -} +use crate::{ActionMask, Condition, Settings, SimulationState, SingleUse}; + +pub trait ActionImpl { + const LEVEL_REQUIREMENT: u8; + /// All bits of this mask must be present in the settings' action mask for the action to be enabled. + const ACTION_MASK: ActionMask; + /// Does this action trigger ticking effects (e.g. Manipulation)? + const TICK_EFFECTS: bool = true; + + fn precondition( + _state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + Ok(()) + } + + fn progress_increase( + state: &SimulationState, + settings: &Settings, + _condition: Condition, + ) -> u16 { + let efficiency_mod = Self::base_progress_increase(state, settings) as u64; + let mut effect_mod = 100; + if state.effects.muscle_memory() != 0 { + effect_mod += 100; + } + if state.effects.veneration() != 0 { + effect_mod += 50; + } + (settings.base_progress as u64 * efficiency_mod * effect_mod / 10000) as u16 + } + + fn quality_increase(state: &SimulationState, settings: &Settings, condition: Condition) -> u16 { + let efficieny_mod = Self::base_quality_increase(state, settings) as u64; + let condition_mod = match condition { + Condition::Good => 150, + Condition::Excellent => 400, + Condition::Poor => 50, + _ => 100, + }; + let mut effect_mod = 100; + if state.effects.innovation() != 0 { + effect_mod += 50; + } + if state.effects.great_strides() != 0 { + effect_mod += 100; + } + let inner_quiet_mod = 100 + 10 * state.effects.inner_quiet() as u64; + (settings.base_quality as u64 + * efficieny_mod + * condition_mod + * effect_mod + * inner_quiet_mod + / 100000000) as u16 + } + + fn durability_cost(state: &SimulationState, settings: &Settings, _condition: Condition) -> i8 { + if matches!(state.effects.trained_perfection(), SingleUse::Active) { + return 0; + } + match state.effects.waste_not() { + 0 => Self::base_durability_cost(state, settings), + _ => (Self::base_durability_cost(state, settings) + 1) / 2, + } + } + + fn cp_cost(state: &SimulationState, settings: &Settings, _condition: Condition) -> i16 { + Self::base_cp_cost(state, settings) + } + + fn base_progress_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 0 + } + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 0 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 0 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 0 + } + + fn transform_pre(_state: &mut SimulationState, _settings: &Settings, _condition: Condition) {} + fn transform_post(_state: &mut SimulationState, _settings: &Settings, _condition: Condition) {} + + fn combo(_state: &SimulationState, _settings: &Settings, _condition: Condition) -> Combo { + Combo::None + } +} + +pub struct BasicSynthesis {} +impl ActionImpl for BasicSynthesis { + const LEVEL_REQUIREMENT: u8 = 1; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::BasicSynthesis); + fn base_progress_increase(_state: &SimulationState, settings: &Settings) -> u16 { + if settings.job_level < 31 { + 100 + } else { + 120 + } + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } +} + +pub struct BasicTouch {} +impl ActionImpl for BasicTouch { + const LEVEL_REQUIREMENT: u8 = 5; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::BasicTouch); + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 100 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 18 + } + fn combo(_state: &SimulationState, _settings: &Settings, _condition: Condition) -> Combo { + Combo::BasicTouch + } +} + +pub struct MasterMend {} +impl ActionImpl for MasterMend { + const LEVEL_REQUIREMENT: u8 = 7; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::MasterMend); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 88 + } + fn transform_post(state: &mut SimulationState, settings: &Settings, _condition: Condition) { + state.durability = + std::cmp::min(settings.max_durability, state.durability.saturating_add(30)); + } +} + +pub struct Observe {} +impl ActionImpl for Observe { + const LEVEL_REQUIREMENT: u8 = 13; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::Observe); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 7 + } + fn combo(_state: &SimulationState, _settings: &Settings, _condition: Condition) -> Combo { + Combo::StandardTouch + } +} + +pub struct WasteNot {} +impl ActionImpl for WasteNot { + const LEVEL_REQUIREMENT: u8 = 15; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::WasteNot); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 56 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_waste_not(4); + } +} + +pub struct Veneration {} +impl ActionImpl for Veneration { + const LEVEL_REQUIREMENT: u8 = 15; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::Veneration); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 18 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_veneration(4); + } +} + +pub struct StandardTouch {} +impl ActionImpl for StandardTouch { + const LEVEL_REQUIREMENT: u8 = 18; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::StandardTouch); + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 125 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(state: &SimulationState, _settings: &Settings) -> i16 { + match state.combo { + Combo::BasicTouch => 18, + _ => 32, + } + } + fn combo(state: &SimulationState, _settings: &Settings, _condition: Condition) -> Combo { + match state.combo { + Combo::BasicTouch => Combo::StandardTouch, + _ => Combo::None, + } + } +} + +pub struct GreatStrides {} +impl ActionImpl for GreatStrides { + const LEVEL_REQUIREMENT: u8 = 21; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::GreatStrides); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 32 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_great_strides(3); + } +} + +pub struct Innovation {} +impl ActionImpl for Innovation { + const LEVEL_REQUIREMENT: u8 = 26; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::Innovation); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 18 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_innovation(4); + } +} + +pub struct WasteNot2 {} +impl ActionImpl for WasteNot2 { + const LEVEL_REQUIREMENT: u8 = 47; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::WasteNot2); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 98 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_waste_not(8); + } +} + +pub struct ByregotsBlessing {} +impl ActionImpl for ByregotsBlessing { + const LEVEL_REQUIREMENT: u8 = 50; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::ByregotsBlessing); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + match state.effects.inner_quiet() { + 0 => Err("Cannot use Byregot's Blessing when Inner Quiet is 0."), + _ => Ok(()), + } + } + fn base_quality_increase(state: &SimulationState, _settings: &Settings) -> u16 { + 100 + 20 * state.effects.inner_quiet() as u16 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 24 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_inner_quiet(0); + } +} + +pub struct PreciseTouch {} +impl ActionImpl for PreciseTouch { + const LEVEL_REQUIREMENT: u8 = 53; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::PreciseTouch); + fn precondition( + state: &SimulationState, + _settings: &Settings, + condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.heart_and_soul() != SingleUse::Active + && condition != Condition::Good + && condition != Condition::Excellent + { + return Err("Precise Touch can only be used when the condition is Good or Excellent."); + } + Ok(()) + } + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 150 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 18 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, condition: Condition) { + let iq = state.effects.inner_quiet(); + state.effects.set_inner_quiet(std::cmp::min(10, iq + 1)); + if condition != Condition::Good && condition != Condition::Excellent { + state.effects.set_heart_and_soul(SingleUse::Unavailable); + } + } +} + +pub struct MuscleMemory {} +impl ActionImpl for MuscleMemory { + const LEVEL_REQUIREMENT: u8 = 54; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::MuscleMemory); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.combo != Combo::SynthesisBegin { + return Err("Muscle Memory can only be used at synthesis begin."); + } + Ok(()) + } + fn base_progress_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 300 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 6 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_muscle_memory(5); + } +} + +pub struct CarefulSynthesis {} +impl ActionImpl for CarefulSynthesis { + const LEVEL_REQUIREMENT: u8 = 62; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::CarefulSynthesis); + fn base_progress_increase(_state: &SimulationState, settings: &Settings) -> u16 { + match settings.job_level { + 0..82 => 150, + 82.. => 180, + } + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 7 + } +} + +pub struct Manipulation {} +impl ActionImpl for Manipulation { + const LEVEL_REQUIREMENT: u8 = 65; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::Manipulation); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 96 + } + fn transform_pre(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_manipulation(0); + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_manipulation(8); + } +} + +pub struct PrudentTouch {} +impl ActionImpl for PrudentTouch { + const LEVEL_REQUIREMENT: u8 = 66; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::PrudentTouch); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.waste_not() != 0 { + return Err("Prudent Touch cannot be used while Waste Not is active."); + } + Ok(()) + } + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 100 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 5 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 25 + } +} + +pub struct AdvancedTouch {} +impl ActionImpl for AdvancedTouch { + const LEVEL_REQUIREMENT: u8 = 68; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::AdvancedTouch); + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 150 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(state: &SimulationState, _settings: &Settings) -> i16 { + match state.combo { + Combo::StandardTouch => 18, + _ => 46, + } + } +} + +pub struct Reflect {} +impl ActionImpl for Reflect { + const LEVEL_REQUIREMENT: u8 = 69; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::Reflect); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.combo != Combo::SynthesisBegin { + return Err("Reflect can only be used at synthesis begin."); + } + Ok(()) + } + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 300 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 6 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + let iq = state.effects.inner_quiet(); + state.effects.set_inner_quiet(std::cmp::min(10, iq + 1)); + } +} + +pub struct PreparatoryTouch {} +impl ActionImpl for PreparatoryTouch { + const LEVEL_REQUIREMENT: u8 = 71; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::PreparatoryTouch); + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 200 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 20 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 40 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + let iq = state.effects.inner_quiet(); + state.effects.set_inner_quiet(std::cmp::min(10, iq + 1)); + } +} + +pub struct Groundwork {} +impl ActionImpl for Groundwork { + const LEVEL_REQUIREMENT: u8 = 72; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::Groundwork); + fn base_progress_increase(state: &SimulationState, settings: &Settings) -> u16 { + let base = match settings.job_level { + 0..86 => 300, + 86.. => 360, + }; + if Self::durability_cost(state, settings, Condition::Normal) > state.durability { + return base / 2; + } + base + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 20 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 18 + } +} + +pub struct DelicateSynthesis {} +impl ActionImpl for DelicateSynthesis { + const LEVEL_REQUIREMENT: u8 = 76; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::DelicateSynthesis); + fn base_progress_increase(_state: &SimulationState, settings: &Settings) -> u16 { + match settings.job_level { + 0..94 => 100, + 94.. => 150, + } + } + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 100 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 32 + } +} + +pub struct IntensiveSynthesis {} +impl ActionImpl for IntensiveSynthesis { + const LEVEL_REQUIREMENT: u8 = 78; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::IntensiveSynthesis); + fn precondition( + state: &SimulationState, + _settings: &Settings, + condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.heart_and_soul() != SingleUse::Active + && condition != Condition::Good + && condition != Condition::Excellent + { + return Err( + "Intensive Synthesis can only be used when the condition is Good or Excellent.", + ); + } + Ok(()) + } + fn base_progress_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 400 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 6 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, condition: Condition) { + if condition != Condition::Good && condition != Condition::Excellent { + state.effects.set_heart_and_soul(SingleUse::Unavailable); + } + } +} + +pub struct TrainedEye {} +impl ActionImpl for TrainedEye { + const LEVEL_REQUIREMENT: u8 = 80; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::TrainedEye); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.combo != Combo::SynthesisBegin { + return Err("Trained Eye can only be used at synthesis begin."); + } + Ok(()) + } + fn quality_increase( + _state: &SimulationState, + settings: &Settings, + _condition: Condition, + ) -> u16 { + settings.max_quality + } + fn base_quality_increase(_state: &SimulationState, settings: &Settings) -> u16 { + settings.max_quality + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 250 + } +} + +pub struct HeartAndSoul {} +impl ActionImpl for HeartAndSoul { + const LEVEL_REQUIREMENT: u8 = 86; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::HeartAndSoul); + const TICK_EFFECTS: bool = false; + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.heart_and_soul() != SingleUse::Available { + return Err("Heart and Sould can only be used once per synthesis."); + } + Ok(()) + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_heart_and_soul(SingleUse::Active); + } +} + +pub struct PrudentSynthesis {} +impl ActionImpl for PrudentSynthesis { + const LEVEL_REQUIREMENT: u8 = 88; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::PrudentSynthesis); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.waste_not() != 0 { + return Err("Prudent Synthesis cannot be used while Waste Not is active."); + } + Ok(()) + } + fn base_progress_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 180 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 5 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 18 + } +} + +pub struct TrainedFinesse {} +impl ActionImpl for TrainedFinesse { + const LEVEL_REQUIREMENT: u8 = 90; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::TrainedFinesse); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.inner_quiet() < 10 { + return Err("Trained Finesse can only be used when Inner Quiet is 10."); + } + Ok(()) + } + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 100 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 32 + } +} + +pub struct RefinedTouch {} +impl ActionImpl for RefinedTouch { + const LEVEL_REQUIREMENT: u8 = 92; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::RefinedTouch); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.combo != Combo::BasicTouch { + return Err("Refined Touch can only be used after Observe or Standard Touch."); + } + Ok(()) + } + fn base_quality_increase(_state: &SimulationState, _settings: &Settings) -> u16 { + 100 + } + fn base_durability_cost(_state: &SimulationState, _settings: &Settings) -> i8 { + 10 + } + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 24 + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + let iq = state.effects.inner_quiet(); + state.effects.set_inner_quiet(std::cmp::min(10, iq + 1)); + } +} + +pub struct QuickInnovation {} +impl ActionImpl for QuickInnovation { + const LEVEL_REQUIREMENT: u8 = 96; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::QuickInnovation); + const TICK_EFFECTS: bool = false; + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.innovation() != 0 { + return Err("Quick Innovation cannot be used while Innovation is active."); + } + if state.effects.quick_innovation_used() { + return Err("Quick Innovation can only be used once per synthesis."); + } + Ok(()) + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_innovation(1); + state.effects.set_quick_innovation_used(true); + } +} + +pub struct ImmaculateMend {} +impl ActionImpl for ImmaculateMend { + const LEVEL_REQUIREMENT: u8 = 98; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::ImmaculateMend); + fn base_cp_cost(_state: &SimulationState, _settings: &Settings) -> i16 { + 112 + } + fn transform_post(state: &mut SimulationState, settings: &Settings, _condition: Condition) { + state.durability = settings.max_durability; + } +} + +pub struct TrainedPerfection {} +impl ActionImpl for TrainedPerfection { + const LEVEL_REQUIREMENT: u8 = 100; + const ACTION_MASK: ActionMask = ActionMask::none().add(Action::TrainedPerfection); + fn precondition( + state: &SimulationState, + _settings: &Settings, + _condition: Condition, + ) -> Result<(), &'static str> { + if state.effects.trained_perfection() != SingleUse::Available { + return Err("Trained Perfection can only be used once per synthesis."); + } + Ok(()) + } + fn transform_post(state: &mut SimulationState, _settings: &Settings, _condition: Condition) { + state.effects.set_trained_perfection(SingleUse::Active); + } +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, serde::Serialize, serde::Deserialize)] +pub enum Action { + BasicSynthesis, + BasicTouch, + MasterMend, + Observe, + WasteNot, + Veneration, + StandardTouch, + GreatStrides, + Innovation, + WasteNot2, + ByregotsBlessing, + PreciseTouch, + MuscleMemory, + CarefulSynthesis, + Manipulation, + PrudentTouch, + AdvancedTouch, + Reflect, + PreparatoryTouch, + Groundwork, + DelicateSynthesis, + IntensiveSynthesis, + TrainedEye, + HeartAndSoul, + PrudentSynthesis, + TrainedFinesse, + RefinedTouch, + QuickInnovation, + ImmaculateMend, + TrainedPerfection, +} + +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Combo { + None, + SynthesisBegin, + BasicTouch, + StandardTouch, +} + +impl Combo { + pub const fn into_bits(self) -> u8 { + match self { + Self::None => 0, + Self::SynthesisBegin => 1, + Self::BasicTouch => 2, + Self::StandardTouch => 3, + } + } + + pub const fn from_bits(value: u8) -> Self { + match value { + 1 => Self::SynthesisBegin, + 2 => Self::BasicTouch, + 3 => Self::StandardTouch, + _ => Self::None, + } + } +} + +impl Action { + pub const fn time_cost(self) -> i16 { + match self { + Action::BasicSynthesis => 3, + Action::BasicTouch => 3, + Action::MasterMend => 3, + Action::Observe => 3, + Action::WasteNot => 2, + Action::Veneration => 2, + Action::StandardTouch => 3, + Action::GreatStrides => 2, + Action::Innovation => 2, + Action::WasteNot2 => 2, + Action::ByregotsBlessing => 3, + Action::PreciseTouch => 3, + Action::MuscleMemory => 3, + Action::CarefulSynthesis => 3, + Action::Manipulation => 2, + Action::PrudentTouch => 3, + Action::Reflect => 3, + Action::PreparatoryTouch => 3, + Action::Groundwork => 3, + Action::DelicateSynthesis => 3, + Action::IntensiveSynthesis => 3, + Action::AdvancedTouch => 3, + Action::HeartAndSoul => 3, + Action::PrudentSynthesis => 3, + Action::TrainedFinesse => 3, + Action::RefinedTouch => 3, + Action::ImmaculateMend => 3, + Action::TrainedPerfection => 3, + Action::TrainedEye => 3, + Action::QuickInnovation => 3, + } + } +} diff --git a/simulator/src/lib.rs b/simulator/src/lib.rs index d7616e8..41815ec 100644 --- a/simulator/src/lib.rs +++ b/simulator/src/lib.rs @@ -1,5 +1,5 @@ mod actions; -pub use actions::{Action, Combo}; +pub use actions::*; mod conditions; pub use conditions::Condition; diff --git a/simulator/src/settings.rs b/simulator/src/settings.rs index 2d74784..a8ddc28 100644 --- a/simulator/src/settings.rs +++ b/simulator/src/settings.rs @@ -11,7 +11,14 @@ pub struct Settings { pub adversarial: bool, } -use crate::Action; +impl Settings { + pub fn is_action_allowed(&self) -> bool { + self.job_level >= ACTION::LEVEL_REQUIREMENT + && self.allowed_actions.has_mask(ACTION::ACTION_MASK) + } +} + +use crate::{Action, ActionImpl}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)] pub struct ActionMask { @@ -27,20 +34,14 @@ impl ActionMask { Self { mask: u64::MAX } } - pub fn from_level(level: u8) -> Self { - let mut result = Self::none(); - for action in ALL_ACTIONS { - if action.level_requirement() <= level { - result = result.add(*action); - } - } - result - } - pub const fn has(self, action: Action) -> bool { (self.mask & (1 << action as u64)) != 0 } + pub const fn has_mask(self, other: Self) -> bool { + (self.mask & other.mask) == other.mask + } + pub const fn add(self, action: Action) -> Self { let bit = 1 << (action as u64); Self { @@ -110,7 +111,6 @@ const ALL_ACTIONS: &[Action] = &[ Action::WasteNot, Action::Veneration, Action::StandardTouch, - Action::ComboStandardTouch, Action::GreatStrides, Action::Innovation, Action::WasteNot2, @@ -127,11 +127,10 @@ const ALL_ACTIONS: &[Action] = &[ Action::DelicateSynthesis, Action::IntensiveSynthesis, Action::AdvancedTouch, - Action::ComboAdvancedTouch, Action::HeartAndSoul, Action::PrudentSynthesis, Action::TrainedFinesse, - Action::ComboRefinedTouch, + Action::RefinedTouch, Action::ImmaculateMend, Action::TrainedPerfection, Action::QuickInnovation, diff --git a/simulator/src/state.rs b/simulator/src/state.rs index 012f0ef..6657cb2 100755 --- a/simulator/src/state.rs +++ b/simulator/src/state.rs @@ -1,4 +1,15 @@ -use crate::{effects::SingleUse, Action, Combo, Condition, Effects, Settings}; +use crate::{ + actions::{ + ActionImpl, AdvancedTouch, BasicSynthesis, BasicTouch, ByregotsBlessing, CarefulSynthesis, + DelicateSynthesis, GreatStrides, Groundwork, HeartAndSoul, ImmaculateMend, Innovation, + IntensiveSynthesis, Manipulation, MasterMend, MuscleMemory, Observe, PreciseTouch, + PreparatoryTouch, PrudentSynthesis, PrudentTouch, QuickInnovation, RefinedTouch, Reflect, + StandardTouch, TrainedEye, TrainedFinesse, TrainedPerfection, Veneration, WasteNot, + WasteNot2, + }, + effects::SingleUse, + Action, Combo, Condition, Effects, Settings, +}; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct SimulationState { @@ -57,146 +68,87 @@ impl SimulationState { self.durability <= 0 || self.progress >= settings.max_progress } - pub fn can_use_action( + fn check_common_preconditions( &self, - action: Action, - condition: Condition, settings: &Settings, + condition: Condition, ) -> Result<(), &'static str> { - if self.is_final(settings) { - return Err("State is final"); - } - if !settings.allowed_actions.has(action) { - return Err("Action not enabled"); - } - if action.cp_cost() > self.cp { - return Err("Not enough CP"); - } - if !action.combo_fulfilled(self.combo) { - return Err("Combo requirement not fulfilled"); - } - match action { - Action::ByregotsBlessing if self.effects.inner_quiet() == 0 => { - Err("Need Inner Quiet to use Byregot's Blessing") - } - Action::PrudentSynthesis | Action::PrudentTouch if self.effects.waste_not() != 0 => { - Err("Action cannot be used during Waste Not") - } - Action::IntensiveSynthesis | Action::PreciseTouch - if self.effects.heart_and_soul() != SingleUse::Active - && condition != Condition::Good - && condition != Condition::Excellent => - { - Err("Requires condition to be Good or Excellent") - } - Action::Groundwork if self.durability < action.durability_cost(&self.effects) => { - Err("Not enough durability") - } - Action::TrainedFinesse if self.effects.inner_quiet() < 10 => { - Err("Requires 10 Inner Quiet") - } - Action::TrainedPerfection - if !matches!(self.effects.trained_perfection(), SingleUse::Available) => - { - Err("Action can only be used once per synthesis") - } - Action::HeartAndSoul if self.effects.heart_and_soul() != SingleUse::Available => { - Err("Action can only be used once per synthesis") - } - Action::QuickInnovation if self.effects.quick_innovation_used() => { - Err("Action can only be used once per synthesis") - } - Action::QuickInnovation if self.effects.innovation() != 0 => { - Err("Action cannot be used when Innovation is active") - } - _ => Ok(()), + if settings.job_level < A::LEVEL_REQUIREMENT { + Err("Level not high enough") + } else if !settings.allowed_actions.has_mask(A::ACTION_MASK) { + Err("Action disabled by action mask") + } else if self.is_final(settings) { + Err("State is final") + } else if A::cp_cost(self, settings, condition) > self.cp { + Err("Not enough CP") + } else { + Ok(()) } } - pub fn use_action( - self, - action: Action, - condition: Condition, + pub fn use_action_impl( + &self, settings: &Settings, + condition: Condition, ) -> Result { - self.can_use_action(action, condition, settings)?; - let mut state = self; - - let cp_cost = action.cp_cost(); - let durability_cost = action.durability_cost(&state.effects); - let progress_increase = action.progress_increase(settings, &state.effects); - let quality_increase = if settings.adversarial && state.effects.guard() == 0 { - action.quality_increase(settings, &state.effects, Condition::Poor) - } else { - action.quality_increase(settings, &state.effects, condition) - }; - let quality_delta = if settings.adversarial && state.effects.guard() == 0 { - action.quality_increase(settings, &state.effects, condition) - - action.quality_increase(settings, &state.effects, Condition::Poor) - } else { - 0 - }; + self.check_common_preconditions::(settings, condition)?; + A::precondition(&self, settings, condition)?; - state.cp -= cp_cost; - state.durability -= durability_cost; + let mut state = self.clone(); - if action.base_durability_cost() != 0 - && state.effects.trained_perfection() == SingleUse::Active - { - state.effects.set_trained_perfection(SingleUse::Unavailable); + A::transform_pre(&mut state, settings, condition); + + if A::base_durability_cost(&state, settings) != 0 { + state.durability -= A::durability_cost(self, settings, condition); + if state.effects.trained_perfection() == SingleUse::Active { + state.effects.set_trained_perfection(SingleUse::Unavailable); + } } - // reset muscle memory if progress increased + state.cp -= A::cp_cost(self, settings, condition); + + let progress_increase = A::progress_increase(self, settings, condition); + state.progress += progress_increase; if progress_increase != 0 { - state.progress += progress_increase; state.effects.set_muscle_memory(0); } - if state.effects.guard() == 0 && quality_increase == 0 { - state.unreliable_quality = 0; - } else if state.effects.guard() != 0 && quality_increase != 0 { + let quality_increase = A::quality_increase(self, settings, condition); + if settings.adversarial { + let adversarial_quality_increase = if state.effects.guard() != 0 { + quality_increase + } else { + A::quality_increase(self, settings, Condition::Poor) + }; + if state.effects.guard() == 0 && adversarial_quality_increase == 0 { + state.unreliable_quality = 0; + } else if state.effects.guard() != 0 && adversarial_quality_increase != 0 { + state.quality += adversarial_quality_increase; + state.unreliable_quality = 0; + } else if adversarial_quality_increase != 0 { + let quality_diff = quality_increase - adversarial_quality_increase; + state.quality += adversarial_quality_increase + + std::cmp::min(state.unreliable_quality, quality_diff); + state.unreliable_quality = quality_diff.saturating_sub(state.unreliable_quality); + } + } else { state.quality += quality_increase; - state.unreliable_quality = 0; - } else if quality_increase != 0 { - state.quality += - quality_increase + std::cmp::min(state.unreliable_quality, quality_delta); - state.unreliable_quality = quality_delta.saturating_sub(state.unreliable_quality); } - - #[cfg(test)] - assert!(settings.adversarial || state.unreliable_quality == 0); - - // reset great strides and increase inner quiet if quality increased - if quality_increase != 0 { + if quality_increase != 0 && settings.job_level >= 11 { state.effects.set_great_strides(0); - if settings.job_level >= 11 { - let inner_quiet_bonus = match action { - Action::Reflect => 2, - Action::PreciseTouch => 2, - Action::PreparatoryTouch => 2, - Action::ComboRefinedTouch => 2, - _ => 1, - }; - state.effects.set_inner_quiet(std::cmp::min( - 10, - state.effects.inner_quiet() + inner_quiet_bonus, - )); - } + state + .effects + .set_inner_quiet(std::cmp::min(10, state.effects.inner_quiet() + 1)); } if state.is_final(settings) { return Ok(state); } - state.combo = action.to_combo(); - - // skip processing effects for actions that do not increase turn count - if !matches!(action, Action::HeartAndSoul | Action::QuickInnovation) { - if action == Action::Manipulation { - state.effects.set_manipulation(0); - } - if state.effects.manipulation() > 0 { - state.durability = std::cmp::min(state.durability + 5, settings.max_durability); + if A::TICK_EFFECTS { + if state.effects.manipulation() != 0 { + state.durability = + std::cmp::min(settings.max_durability, state.durability.saturating_add(5)); } state.effects.tick_down(); } @@ -205,35 +157,64 @@ impl SimulationState { state.effects.set_guard(1); } - // trigger special action effects + A::transform_post(&mut state, settings, condition); + + state.combo = A::combo(&state, settings, condition); + + Ok(state) + } + + pub fn use_action( + &self, + action: Action, + condition: Condition, + settings: &Settings, + ) -> Result { match action { - Action::MuscleMemory => state.effects.set_muscle_memory(5), - Action::GreatStrides => state.effects.set_great_strides(3), - Action::Veneration => state.effects.set_veneration(4), - Action::Innovation => state.effects.set_innovation(4), - Action::WasteNot => state.effects.set_waste_not(4), - Action::WasteNot2 => state.effects.set_waste_not(8), - Action::Manipulation => state.effects.set_manipulation(8), - Action::MasterMend => { - state.durability = - std::cmp::min(settings.max_durability, state.durability.saturating_add(30)) + Action::BasicSynthesis => self.use_action_impl::(settings, condition), + Action::BasicTouch => self.use_action_impl::(settings, condition), + Action::MasterMend => self.use_action_impl::(settings, condition), + Action::Observe => self.use_action_impl::(settings, condition), + Action::WasteNot => self.use_action_impl::(settings, condition), + Action::Veneration => self.use_action_impl::(settings, condition), + Action::StandardTouch => self.use_action_impl::(settings, condition), + Action::GreatStrides => self.use_action_impl::(settings, condition), + Action::Innovation => self.use_action_impl::(settings, condition), + Action::WasteNot2 => self.use_action_impl::(settings, condition), + Action::ByregotsBlessing => { + self.use_action_impl::(settings, condition) } - Action::ByregotsBlessing => state.effects.set_inner_quiet(0), - Action::ImmaculateMend => state.durability = settings.max_durability, - Action::TrainedPerfection => state.effects.set_trained_perfection(SingleUse::Active), - Action::HeartAndSoul => state.effects.set_heart_and_soul(SingleUse::Active), - Action::QuickInnovation => { - state.effects.set_innovation(1); - state.effects.set_quick_innovation_used(true); + Action::PreciseTouch => self.use_action_impl::(settings, condition), + Action::MuscleMemory => self.use_action_impl::(settings, condition), + Action::CarefulSynthesis => { + self.use_action_impl::(settings, condition) } - Action::IntensiveSynthesis | Action::PreciseTouch - if condition != Condition::Good && condition != Condition::Excellent => - { - state.effects.set_heart_and_soul(SingleUse::Unavailable) + Action::Manipulation => self.use_action_impl::(settings, condition), + Action::PrudentTouch => self.use_action_impl::(settings, condition), + Action::AdvancedTouch => self.use_action_impl::(settings, condition), + Action::Reflect => self.use_action_impl::(settings, condition), + Action::PreparatoryTouch => { + self.use_action_impl::(settings, condition) + } + Action::Groundwork => self.use_action_impl::(settings, condition), + Action::DelicateSynthesis => { + self.use_action_impl::(settings, condition) + } + Action::IntensiveSynthesis => { + self.use_action_impl::(settings, condition) + } + Action::TrainedEye => self.use_action_impl::(settings, condition), + Action::HeartAndSoul => self.use_action_impl::(settings, condition), + Action::PrudentSynthesis => { + self.use_action_impl::(settings, condition) + } + Action::TrainedFinesse => self.use_action_impl::(settings, condition), + Action::RefinedTouch => self.use_action_impl::(settings, condition), + Action::QuickInnovation => self.use_action_impl::(settings, condition), + Action::ImmaculateMend => self.use_action_impl::(settings, condition), + Action::TrainedPerfection => { + self.use_action_impl::(settings, condition) } - _ => (), } - - Ok(state) } } diff --git a/simulator/tests/action_tests.rs b/simulator/tests/action_tests.rs index 0800d3c..1aa6c30 100644 --- a/simulator/tests/action_tests.rs +++ b/simulator/tests/action_tests.rs @@ -1,4 +1,4 @@ -use simulator::{Action, ActionMask, Combo, Settings, SimulationState, SingleUse}; +use simulator::*; const SETTINGS: Settings = Settings { max_cp: 250, @@ -12,140 +12,496 @@ const SETTINGS: Settings = Settings { adversarial: false, }; +/// Returns the 4 primary stats of a state: +/// - Progress +/// - Quality +/// - Durability (used) +/// - CP (used) +fn primary_stats(state: &SimulationState, settings: &Settings) -> (u16, u16, i8, i16) { + ( + state.progress, + state.quality, + settings.max_durability - state.durability, + settings.max_cp - state.cp, + ) +} + #[test] -fn test_redundant_half_efficiency_groundwork() { - // Groundwork's efficiency is halved when the remaining durability is lower than the durability cost - // Careful Synthesis is a strictly better action in case Groundwork's efficiency is halved - // This enables the simulator to simplify things by simply not allowing Groundwork when there isn't enough durability - for level in 1..=100 { - let settings = Settings { - job_level: level, - allowed_actions: ActionMask::from_level(level as _), - ..SETTINGS - }; - match ( - settings.allowed_actions.has(Action::CarefulSynthesis), - settings.allowed_actions.has(Action::Groundwork), - ) { - (false, true) => panic!("Cannot replace half-efficiency Groundwork because CarefulSynthesis is not available at level {}", level), - (true, true) => { - let state_1 = SimulationState::from_macro(&settings, &[Action::CarefulSynthesis]).unwrap(); - let state_2 = SimulationState::from_macro(&settings, &[Action::Groundwork]).unwrap(); - assert!(state_1.progress * 2 >= state_2.progress); - assert!(state_1.durability >= state_2.durability); - assert!(state_1.cp >= state_2.cp); - } - _ => () - } - } +fn test_basic_synthesis() { + // Low level, potency increase trait not unlocked + let settings = Settings { + job_level: 30, + ..SETTINGS + }; + let state = SimulationState::new(&settings) + .use_action(Action::BasicSynthesis, Condition::Normal, &settings) + .unwrap(); + assert_eq!(primary_stats(&state, &settings), (100, 0, 10, 0)); + // Potency-increase trait unlocked + let settings = Settings { + job_level: 31, + ..SETTINGS + }; + let state = SimulationState::new(&settings) + .use_action(Action::BasicSynthesis, Condition::Normal, &settings) + .unwrap(); + assert_eq!(primary_stats(&state, &settings), (120, 0, 10, 0)); } #[test] -fn test_standard_touch_combo() { - let state = - SimulationState::from_macro(&SETTINGS, &[Action::BasicTouch, Action::ComboStandardTouch]); - assert!(matches!(state, Ok(_))); - let state = SimulationState::from_macro(&SETTINGS, &[Action::ComboStandardTouch]); - assert!(matches!(state, Err("Combo requirement not fulfilled"))); +fn test_basic_touch() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::BasicTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 100, 10, 18)); + assert_eq!(state.effects.inner_quiet(), 1); + assert_eq!(state.combo, Combo::BasicTouch); } #[test] -fn test_advanced_touch_combo() { - let state = SimulationState::from_macro( - &SETTINGS, - &[ - Action::BasicTouch, - Action::ComboStandardTouch, - Action::ComboAdvancedTouch, - ], - ); - assert!(matches!(state, Ok(_))); - let state = - SimulationState::from_macro(&SETTINGS, &[Action::Observe, Action::ComboAdvancedTouch]); - assert!(matches!(state, Ok(_))); - let state = SimulationState::from_macro(&SETTINGS, &[Action::ComboAdvancedTouch]); - assert!(matches!(state, Err("Combo requirement not fulfilled"))); +fn test_master_mend() { + // Durability-restore fully utilized + let initial_state = SimulationState { + durability: SETTINGS.max_durability - 40, + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::MasterMend, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 10, 88)); + // Durability-restore partially utilized + let initial_state = SimulationState { + durability: SETTINGS.max_durability - 10, + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::MasterMend, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 88)); } #[test] -fn test_reflect_opener() { - let state = SimulationState::from_macro(&SETTINGS, &[Action::Reflect]); - assert!(matches!(state, Ok(_))); - let state = SimulationState::from_macro(&SETTINGS, &[Action::BasicTouch, Action::Reflect]); - assert!(matches!(state, Err("Combo requirement not fulfilled"))); +fn test_observe() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::Observe, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 7)); + assert_eq!(state.combo, Combo::StandardTouch); } #[test] -fn test_muscle_memory_opener() { - let state = SimulationState::from_macro(&SETTINGS, &[Action::MuscleMemory]); - assert!(matches!(state, Ok(_))); - let state = SimulationState::from_macro(&SETTINGS, &[Action::BasicTouch, Action::MuscleMemory]); - assert!(matches!(state, Err("Combo requirement not fulfilled"))); +fn test_waste_not() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::WasteNot, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 56)); + assert_eq!(state.effects.waste_not(), 4); } #[test] -fn test_trained_eye_opener() { - let state = SimulationState::from_macro(&SETTINGS, &[Action::TrainedEye]); - assert!(matches!(state, Ok(_))); - let state = state.unwrap(); - assert_eq!(state.quality, SETTINGS.max_quality); +fn test_veneration() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::Veneration, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 18)); + assert_eq!(state.effects.veneration(), 4); +} + +#[test] +fn test_standard_touch() { + // Combo requirement not fulfilled + let state = SimulationState::new(&SETTINGS) + .use_action(Action::StandardTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 125, 10, 32)); assert_eq!(state.effects.inner_quiet(), 1); - let state = - SimulationState::from_macro(&SETTINGS, &[Action::BasicSynthesis, Action::TrainedEye]); - assert!(matches!(state, Err("Combo requirement not fulfilled"))); + assert_eq!(state.combo, Combo::None); + // Combo requirement fulfilled + let initial_state = SimulationState { + combo: Combo::BasicTouch, + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::StandardTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 125, 10, 18)); + assert_eq!(state.effects.inner_quiet(), 1); + assert_eq!(state.combo, Combo::StandardTouch); +} + +#[test] +fn test_great_strides() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::GreatStrides, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 32)); + assert_eq!(state.effects.great_strides(), 3); +} + +#[test] +fn test_innovation() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::Innovation, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 18)); + assert_eq!(state.effects.innovation(), 4); +} + +#[test] +fn test_waste_not_2() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::WasteNot2, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 98)); + assert_eq!(state.effects.waste_not(), 8); +} + +#[test] +fn test_byregots_blessing() { + // Cannot use without inner quiet + let error = SimulationState::new(&SETTINGS) + .use_action(Action::ByregotsBlessing, Condition::Normal, &SETTINGS) + .unwrap_err(); + assert_eq!( + error, + "Cannot use Byregot's Blessing when Inner Quiet is 0." + ); + // Quality efficiency scales with inner quiet + let initial_state = SimulationState { + effects: Effects::new().with_inner_quiet(5), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::ByregotsBlessing, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 300, 10, 24)); + assert_eq!(state.effects.inner_quiet(), 0); + let initial_state = SimulationState { + effects: Effects::new().with_inner_quiet(10), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::ByregotsBlessing, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 600, 10, 24)); + assert_eq!(state.effects.inner_quiet(), 0); +} + +#[test] +fn test_precise_touch() { + // Precondition not fulfilled + let error = SimulationState::new(&SETTINGS) + .use_action(Action::PreciseTouch, Condition::Normal, &SETTINGS) + .unwrap_err(); + assert_eq!( + error, + "Precise Touch can only be used when the condition is Good or Excellent." + ); + // Can use when condition is Good or Excellent + let state = SimulationState::new(&SETTINGS) + .use_action(Action::PreciseTouch, Condition::Good, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 225, 10, 18)); + assert_eq!(state.effects.inner_quiet(), 2); + // Can use when Heart and Soul is active + let initial_state = SimulationState { + effects: Effects::new().with_heart_and_soul(SingleUse::Active), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::PreciseTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 150, 10, 18)); + assert_eq!(state.effects.inner_quiet(), 2); + assert_eq!(state.effects.heart_and_soul(), SingleUse::Unavailable); + // Heart and Soul effect isn't consumed when condition is Good or Excellent + let initial_state = SimulationState { + effects: Effects::new().with_heart_and_soul(SingleUse::Active), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::PreciseTouch, Condition::Good, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 225, 10, 18)); + assert_eq!(state.effects.inner_quiet(), 2); + assert_eq!(state.effects.heart_and_soul(), SingleUse::Active); +} + +#[test] +fn test_muscle_memory() { + // Precondition unfulfilled + let initial_state = SimulationState { + combo: Combo::None, + ..SimulationState::new(&SETTINGS) + }; + let error = initial_state + .use_action(Action::MuscleMemory, Condition::Normal, &SETTINGS) + .unwrap_err(); + assert_eq!(error, "Muscle Memory can only be used at synthesis begin."); + // Precondition fulfilled + let state = SimulationState::new(&SETTINGS) + .use_action(Action::MuscleMemory, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (300, 0, 10, 6)); + assert_eq!(state.effects.muscle_memory(), 5); +} + +#[test] +fn test_careful_synthesis() { + // Low level, potency-increase trait not unlocked + let settings = Settings { + job_level: 81, + ..SETTINGS + }; + let state = SimulationState::new(&settings) + .use_action(Action::CarefulSynthesis, Condition::Normal, &settings) + .unwrap(); + assert_eq!(primary_stats(&state, &settings), (150, 0, 10, 7)); + // Potency-increase trait unlocked + let settings = Settings { + job_level: 82, + ..SETTINGS + }; + let state = SimulationState::new(&settings) + .use_action(Action::CarefulSynthesis, Condition::Normal, &settings) + .unwrap(); + assert_eq!(primary_stats(&state, &settings), (180, 0, 10, 7)); } #[test] fn test_manipulation() { - let state = SimulationState::from_macro( - &SETTINGS, - &[ - Action::BasicSynthesis, - Action::Manipulation, - Action::Manipulation, - ], - ) - .unwrap(); - assert_eq!(state.durability, SETTINGS.max_durability - 10); + let state = SimulationState::new(&SETTINGS) + .use_action(Action::Manipulation, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 0, 96)); + assert_eq!(state.effects.manipulation(), 8); + // Using Manipulation while Manipulation is already active doesn't restore durability + let initial_state = SimulationState { + durability: SETTINGS.max_durability - 5, + effects: Effects::new().with_manipulation(2), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::Manipulation, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 0, 5, 96)); + assert_eq!(state.effects.manipulation(), 8); } #[test] fn test_prudent_touch() { - let state = SimulationState::from_macro(&SETTINGS, &[Action::WasteNot, Action::PrudentTouch]); - assert!(matches!( - state, - Err("Action cannot be used during Waste Not") - )); + let state = SimulationState::new(&SETTINGS) + .use_action(Action::PrudentTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 100, 5, 25)); + assert_eq!(state.effects.inner_quiet(), 1); + // Cannot use while Waste Not is active + let initial_state = SimulationState { + effects: Effects::new().with_waste_not(2), + ..SimulationState::new(&SETTINGS) + }; + let error = initial_state + .use_action(Action::PrudentTouch, Condition::Normal, &SETTINGS) + .unwrap_err(); + assert_eq!( + error, + "Prudent Touch cannot be used while Waste Not is active." + ); +} + +#[test] +fn test_advanced_touch() { + // Combo requirement unfulfilled + let state = SimulationState::new(&SETTINGS) + .use_action(Action::AdvancedTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 150, 10, 46)); + assert_eq!(state.effects.inner_quiet(), 1); + // Combo requirement fulfilled + let initial_state = SimulationState { + combo: Combo::StandardTouch, + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::AdvancedTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 150, 10, 18)); + assert_eq!(state.effects.inner_quiet(), 1); +} + +#[test] +fn test_reflect() { + // Precondition unfulfilled + let initial_state = SimulationState { + combo: Combo::None, + ..SimulationState::new(&SETTINGS) + }; + let error = initial_state + .use_action(Action::Reflect, Condition::Normal, &SETTINGS) + .unwrap_err(); + assert_eq!(error, "Reflect can only be used at synthesis begin."); + // Precondition fulfilled + let state = SimulationState::new(&SETTINGS) + .use_action(Action::Reflect, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 300, 10, 6)); + assert_eq!(state.effects.inner_quiet(), 2); +} + +#[test] +fn test_preparatory_touch() { + let state = SimulationState::new(&SETTINGS) + .use_action(Action::PreparatoryTouch, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (0, 200, 20, 40)); + assert_eq!(state.effects.inner_quiet(), 2); } #[test] fn test_groundwork() { + // Low level, potency-increasing trait not unlocked let settings = Settings { - job_level: 100, - max_durability: 10, + job_level: 85, ..SETTINGS }; - let state = SimulationState::from_macro(&settings, &[Action::Groundwork]); - assert!(matches!(state, Err("Not enough durability"))); - let state = - SimulationState::from_macro(&settings, &[Action::TrainedPerfection, Action::Groundwork]); - match state { - Ok(state) => { - assert_eq!(state.progress, 360); - assert_eq!(state.durability, 10); - } - Err(e) => panic!("Unexpected error: {}", e), - } + let state = SimulationState::new(&settings) + .use_action(Action::Groundwork, Condition::Normal, &settings) + .unwrap(); + assert_eq!(primary_stats(&state, &settings), (300, 0, 20, 18)); + // Potency-increasing trait unlocked + let state = SimulationState::new(&SETTINGS) + .use_action(Action::Groundwork, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (360, 0, 20, 18)); + // Potency is halved when durability isn't enough + let initial_state = SimulationState { + durability: 10, + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::Groundwork, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!( + primary_stats(&state, &SETTINGS), + (180, 0, SETTINGS.max_durability + 10, 18) + ); + // Potency isn't halved when Waste Not causes durability cost to fit into remaining durability + let initial_state = SimulationState { + durability: 10, + effects: Effects::new().with_waste_not(1), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::Groundwork, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!( + primary_stats(&state, &SETTINGS), + (360, 0, SETTINGS.max_durability, 18) + ); + // Potency isn't halved when Trained Perfection is active + let initial_state = SimulationState { + durability: 10, + effects: Effects::new().with_trained_perfection(SingleUse::Active), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::Groundwork, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!( + primary_stats(&state, &SETTINGS), + (360, 0, SETTINGS.max_durability - 10, 18) + ); +} + +#[test] +fn test_delicate_synthesis() { + // Low level, potency-increasing trait not unlocked + let settings = Settings { + job_level: 93, + ..SETTINGS + }; + let state = SimulationState::new(&settings) + .use_action(Action::DelicateSynthesis, Condition::Normal, &settings) + .unwrap(); + assert_eq!(primary_stats(&state, &settings), (100, 100, 10, 32)); + // Potency-increasing trait unlocked + let settings = Settings { + job_level: 94, + ..SETTINGS + }; + let state = SimulationState::new(&settings) + .use_action(Action::DelicateSynthesis, Condition::Normal, &settings) + .unwrap(); + assert_eq!(primary_stats(&state, &settings), (150, 100, 10, 32)); +} + +#[test] +fn test_intensive_synthesis() { + // Precondition not fulfilled + let error = SimulationState::new(&SETTINGS) + .use_action(Action::IntensiveSynthesis, Condition::Normal, &SETTINGS) + .unwrap_err(); + assert_eq!( + error, + "Intensive Synthesis can only be used when the condition is Good or Excellent." + ); + // Can use when condition is Good or Excellent + let state = SimulationState::new(&SETTINGS) + .use_action(Action::IntensiveSynthesis, Condition::Good, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (400, 0, 10, 6)); + // Can use when Heart and Soul is active + let initial_state = SimulationState { + effects: Effects::new().with_heart_and_soul(SingleUse::Active), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::IntensiveSynthesis, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (400, 0, 10, 6)); + assert_eq!(state.effects.heart_and_soul(), SingleUse::Unavailable); + // Heart and Soul effect isn't consumed when condition is Good or Excellent + let initial_state = SimulationState { + effects: Effects::new().with_heart_and_soul(SingleUse::Active), + ..SimulationState::new(&SETTINGS) + }; + let state = initial_state + .use_action(Action::IntensiveSynthesis, Condition::Good, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (400, 0, 10, 6)); + assert_eq!(state.effects.heart_and_soul(), SingleUse::Active); +} + +#[test] +fn test_trained_eye() { + // Precondition unfulfilled + let initial_state = SimulationState { + combo: Combo::None, + ..SimulationState::new(&SETTINGS) + }; + let error = initial_state + .use_action(Action::TrainedEye, Condition::Normal, &SETTINGS) + .unwrap_err(); + assert_eq!(error, "Trained Eye can only be used at synthesis begin."); + // Precondition fulfilled + let state = SimulationState::new(&SETTINGS) + .use_action(Action::TrainedEye, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!( + primary_stats(&state, &SETTINGS), + (0, SETTINGS.max_quality, 10, 250) + ); + assert_eq!(state.effects.inner_quiet(), 1); } #[test] fn test_prudent_synthesis() { let state = SimulationState::from_macro(&SETTINGS, &[Action::WasteNot, Action::PrudentSynthesis]); - assert!(matches!( + assert_eq!( state, - Err("Action cannot be used during Waste Not") - )); + Err("Prudent Synthesis cannot be used while Waste Not is active.") + ); } #[test] @@ -158,13 +514,15 @@ fn test_trained_finesse() { Action::TrainedFinesse, ], ); - assert!(matches!(state, Err("Requires 10 Inner Quiet"))); + assert_eq!( + state, + Err("Trained Finesse can only be used when Inner Quiet is 10.") + ); } #[test] fn test_refined_touch() { - let state = - SimulationState::from_macro(&SETTINGS, &[Action::BasicTouch, Action::ComboRefinedTouch]); + let state = SimulationState::from_macro(&SETTINGS, &[Action::BasicTouch, Action::RefinedTouch]); match state { Ok(state) => { assert_eq!(state.effects.inner_quiet(), 3); @@ -172,8 +530,11 @@ fn test_refined_touch() { Err(e) => panic!("Unexpected error: {}", e), } assert!(matches!(state, Ok(_))); - let state = SimulationState::from_macro(&SETTINGS, &[Action::ComboRefinedTouch]); - assert!(matches!(state, Err("Combo requirement not fulfilled"))); + let state = SimulationState::from_macro(&SETTINGS, &[Action::RefinedTouch]); + assert_eq!( + state, + Err("Refined Touch can only be used after Observe or Standard Touch.") + ); } #[test] @@ -215,77 +576,10 @@ fn test_trained_perfection() { &SETTINGS, &[Action::TrainedPerfection, Action::TrainedPerfection], ); - assert!(matches!( + assert_eq!( state, - Err("Action can only be used once per synthesis") - )); -} - -#[test] -fn test_delicate_synthesis() { - let settings = Settings { - job_level: 93, - ..SETTINGS - }; - let state = SimulationState::from_macro(&settings, &[Action::DelicateSynthesis]); - match state { - Ok(state) => { - assert_eq!(state.progress, 100); - assert_eq!(state.quality, 100); - } - Err(e) => panic!("Unexpected error: {}", e), - } - let settings = Settings { - job_level: 94, - ..SETTINGS - }; - let state = SimulationState::from_macro(&settings, &[Action::DelicateSynthesis]); - match state { - Ok(state) => { - assert_eq!(state.progress, 150); - assert_eq!(state.quality, 100); - } - Err(e) => panic!("Unexpected error: {}", e), - } -} - -#[test] -fn test_intensive_synthesis() { - let state = SimulationState::from_macro( - &SETTINGS, - &[Action::HeartAndSoul, Action::IntensiveSynthesis], + Err("Trained Perfection can only be used once per synthesis.") ); - match state { - Ok(state) => { - assert_eq!(state.progress, 400); - assert_eq!(state.effects.heart_and_soul(), SingleUse::Unavailable); - } - Err(e) => panic!("Unexpected error: {}", e), - } - let state = SimulationState::from_macro(&SETTINGS, &[Action::IntensiveSynthesis]); - assert!(matches!( - state, - Err("Requires condition to be Good or Excellent") - )); -} - -#[test] -fn test_precise_touch() { - let state = - SimulationState::from_macro(&SETTINGS, &[Action::HeartAndSoul, Action::PreciseTouch]); - match state { - Ok(state) => { - assert_eq!(state.quality, 150); - assert_eq!(state.effects.inner_quiet(), 2); - assert_eq!(state.effects.heart_and_soul(), SingleUse::Unavailable); - } - Err(e) => panic!("Unexpected error: {}", e), - } - let state = SimulationState::from_macro(&SETTINGS, &[Action::IntensiveSynthesis]); - assert!(matches!( - state, - Err("Requires condition to be Good or Excellent") - )); } #[test] @@ -351,10 +645,10 @@ fn test_heart_and_soul() { Action::HeartAndSoul, ], ); - assert!(matches!( + assert_eq!( state, - Err("Action can only be used once per synthesis") - )); + Err("Heart and Sould can only be used once per synthesis.") + ); } #[test] @@ -388,14 +682,14 @@ fn test_quick_innovation() { Action::QuickInnovation, ], ); - assert!(matches!( + assert_eq!( state, - Err("Action can only be used once per synthesis") - )); + Err("Quick Innovation can only be used once per synthesis.") + ); let state = SimulationState::from_macro(&setings, &[Action::Innovation, Action::QuickInnovation]); - assert!(matches!( + assert_eq!( state, - Err("Action cannot be used when Innovation is active") - )); + Err("Quick Innovation cannot be used while Innovation is active.") + ); } diff --git a/simulator/tests/adversarial_tests.rs b/simulator/tests/adversarial_tests.rs index aca6d74..4b5ce5d 100644 --- a/simulator/tests/adversarial_tests.rs +++ b/simulator/tests/adversarial_tests.rs @@ -192,7 +192,7 @@ fn test_long_sequence() { Action::Innovation, Action::WasteNot2, Action::BasicTouch, - Action::ComboStandardTouch, + Action::StandardTouch, Action::PreparatoryTouch, Action::Veneration, Action::DelicateSynthesis, @@ -201,8 +201,8 @@ fn test_long_sequence() { Action::Groundwork, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, - Action::ComboAdvancedTouch, + Action::StandardTouch, + Action::AdvancedTouch, Action::ByregotsBlessing, Action::CarefulSynthesis, ]; diff --git a/simulator/tests/effect_tests.rs b/simulator/tests/effect_tests.rs new file mode 100644 index 0000000..626ac1d --- /dev/null +++ b/simulator/tests/effect_tests.rs @@ -0,0 +1,46 @@ +use simulator::*; + +const SETTINGS: Settings = Settings { + max_cp: 250, + max_durability: 60, + max_progress: 2000, + max_quality: 40000, + base_progress: 100, + base_quality: 100, + job_level: 100, + allowed_actions: ActionMask::all(), + adversarial: false, +}; + +/// Returns the 4 primary stats of a state: +/// - Progress +/// - Quality +/// - Durability (used) +/// - CP (used) +fn primary_stats(state: &SimulationState, settings: &Settings) -> (u16, u16, i8, i16) { + ( + state.progress, + state.quality, + settings.max_durability - state.durability, + settings.max_cp - state.cp, + ) +} + +#[test] +fn test_trained_perfection() { + let initial_state = SimulationState { + effects: Effects::new().with_trained_perfection(SingleUse::Active), + ..SimulationState::new(&SETTINGS) + }; + // No durability cost when trained perfection is active + let state = initial_state + .use_action(Action::BasicSynthesis, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(primary_stats(&state, &SETTINGS), (120, 0, 0, 0)); + assert_eq!(state.effects.trained_perfection(), SingleUse::Unavailable); + // Trained Perfection effect doesn't wear off if durability cost is zero + let state = initial_state + .use_action(Action::Observe, Condition::Normal, &SETTINGS) + .unwrap(); + assert_eq!(state.effects.trained_perfection(), SingleUse::Active); +} diff --git a/simulator/tests/simulator_tests.rs b/simulator/tests/simulator_tests.rs index e59e02e..0575d33 100644 --- a/simulator/tests/simulator_tests.rs +++ b/simulator/tests/simulator_tests.rs @@ -22,6 +22,26 @@ fn simulate_normal( simulate(settings, actions.zip(std::iter::repeat(Condition::Normal))) } +#[test] +/// The simulator should return an error in case the job level isn't high enough to use an action +fn test_level_requirement() { + let settings = Settings { + max_cp: 50, + max_durability: 60, + max_progress: 33, + max_quality: 150, + base_progress: 4, + base_quality: 38, + job_level: 50, + allowed_actions: ActionMask::all(), + adversarial: false, + }; + let error = SimulationState::new(&settings) + .use_action(Action::ImmaculateMend, Condition::Normal, &settings) + .unwrap_err(); + assert_eq!(error, "Level not high enough"); +} + #[test] fn test_random_926ae85b() { // Copper Gorget @@ -106,7 +126,7 @@ fn test_random_3ba90d3a() { Action::Innovation, Action::PrudentTouch, Action::BasicTouch, - Action::ComboStandardTouch, + Action::StandardTouch, ]; let state = SimulationState::from_macro(&settings, &actions).unwrap(); assert_eq!(state.cp, 188); @@ -151,12 +171,12 @@ fn test_random_bce2650c() { Action::Innovation, Action::PrudentTouch, Action::BasicTouch, - Action::ComboStandardTouch, - Action::ComboAdvancedTouch, + Action::StandardTouch, + Action::AdvancedTouch, Action::GreatStrides, Action::Innovation, Action::Observe, - Action::ComboAdvancedTouch, + Action::AdvancedTouch, Action::GreatStrides, Action::ByregotsBlessing, ]; @@ -194,13 +214,13 @@ fn test_ingame_be9fc5c2() { (Action::PreparatoryTouch, Condition::Normal), // 0, 4293 (Action::Innovation, Condition::Normal), (Action::BasicTouch, Condition::Normal), // 0, 5008 - (Action::ComboStandardTouch, Condition::Normal), // 0, 5952 - (Action::ComboAdvancedTouch, Condition::Normal), // 0, 7144 + (Action::StandardTouch, Condition::Normal), // 0, 5952 + (Action::AdvancedTouch, Condition::Normal), // 0, 7144 (Action::PrudentTouch, Condition::Normal), // 0, 7939 (Action::GreatStrides, Condition::Normal), (Action::Innovation, Condition::Normal), (Action::Observe, Condition::Normal), - (Action::ComboAdvancedTouch, Condition::Normal), // 0, 9926 + (Action::AdvancedTouch, Condition::Normal), // 0, 9926 (Action::Veneration, Condition::Normal), (Action::Groundwork, Condition::Normal), // 1333, 9926 (Action::DelicateSynthesis, Condition::Normal), // 1703, 10456 @@ -311,22 +331,22 @@ fn test_ingame_f9f0dac7() { let actions = [ Action::Reflect, Action::Observe, - Action::ComboAdvancedTouch, + Action::AdvancedTouch, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, - Action::ComboAdvancedTouch, + Action::StandardTouch, + Action::AdvancedTouch, Action::PreparatoryTouch, Action::ImmaculateMend, Action::Innovation, Action::PrudentTouch, Action::BasicTouch, - Action::ComboStandardTouch, - Action::ComboAdvancedTouch, + Action::StandardTouch, + Action::AdvancedTouch, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, - Action::ComboAdvancedTouch, + Action::StandardTouch, + Action::AdvancedTouch, Action::ByregotsBlessing, Action::TrainedPerfection, Action::ImmaculateMend, @@ -399,13 +419,13 @@ fn test_ingame_4866545e() { Action::Manipulation, Action::Innovation, Action::BasicTouch, - Action::ComboRefinedTouch, + Action::RefinedTouch, Action::PrudentTouch, Action::PrudentTouch, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, - Action::ComboAdvancedTouch, + Action::StandardTouch, + Action::AdvancedTouch, Action::Manipulation, Action::TrainedPerfection, Action::GreatStrides, diff --git a/solvers/examples/macro_solver_example.rs b/solvers/examples/macro_solver_example.rs index e6d95b5..d703e71 100644 --- a/solvers/examples/macro_solver_example.rs +++ b/solvers/examples/macro_solver_example.rs @@ -15,7 +15,7 @@ fn main() { base_progress: 237, base_quality: 245, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), diff --git a/solvers/src/actions.rs b/solvers/src/actions.rs index c41184f..9c67a3b 100644 --- a/solvers/src/actions.rs +++ b/solvers/src/actions.rs @@ -16,7 +16,6 @@ pub const QUALITY_ACTIONS: ActionMask = action_mask!( Action::BasicTouch, Action::Observe, Action::StandardTouch, - Action::ComboStandardTouch, Action::GreatStrides, Action::Innovation, Action::ByregotsBlessing, @@ -26,9 +25,8 @@ pub const QUALITY_ACTIONS: ActionMask = action_mask!( Action::PreparatoryTouch, Action::DelicateSynthesis, Action::AdvancedTouch, - Action::ComboAdvancedTouch, Action::TrainedFinesse, - Action::ComboRefinedTouch, + Action::RefinedTouch, Action::TrainedEye, Action::HeartAndSoul, Action::QuickInnovation diff --git a/solvers/src/branch_pruning.rs b/solvers/src/branch_pruning.rs index 4b01f7f..fbdcd95 100644 --- a/solvers/src/branch_pruning.rs +++ b/solvers/src/branch_pruning.rs @@ -1,7 +1,7 @@ use simulator::{Combo, SimulationState}; pub fn is_progress_only_state( - state: SimulationState, + state: &SimulationState, backload_progress: bool, allow_unsound: bool, ) -> bool { diff --git a/solvers/src/lib.rs b/solvers/src/lib.rs index 58895bb..6d23396 100644 --- a/solvers/src/lib.rs +++ b/solvers/src/lib.rs @@ -15,9 +15,8 @@ mod macro_solver; pub use macro_solver::MacroSolver; pub mod test_utils { - use simulator::{Action, Condition, Settings, SimulationState}; - use crate::MacroSolver; + use simulator::*; pub fn solve( settings: &Settings, @@ -51,4 +50,21 @@ pub mod test_utils { assert!(state.progress >= settings.max_progress); state.quality } + + pub fn is_progress_backloaded(actions: &[Action], settings: &Settings) -> bool { + let mut state = SimulationState::new(settings); + let mut quality_lock = None; + for action in actions { + state = state + .use_action(*action, Condition::Normal, settings) + .unwrap(); + if state.progress != 0 && quality_lock.is_none() { + quality_lock = Some(state.quality); + } + } + match quality_lock { + Some(quality) => state.quality == quality, + None => true, + } + } } diff --git a/solvers/src/macro_solver/fast_lower_bound.rs b/solvers/src/macro_solver/fast_lower_bound.rs index 15cd298..f927f11 100644 --- a/solvers/src/macro_solver/fast_lower_bound.rs +++ b/solvers/src/macro_solver/fast_lower_bound.rs @@ -70,17 +70,14 @@ fn should_use_action(action: Action, state: &SimulationState, allowed_actions: A match state.combo { Combo::None => (), Combo::BasicTouch => { - let combo_available = allowed_actions.has(Action::ComboStandardTouch) - || allowed_actions.has(Action::ComboRefinedTouch); + let combo_available = allowed_actions.has(Action::StandardTouch) + || allowed_actions.has(Action::RefinedTouch); return !combo_available - || matches!( - action, - Action::ComboStandardTouch | Action::ComboRefinedTouch - ); + || matches!(action, Action::StandardTouch | Action::RefinedTouch); } Combo::StandardTouch => { - let combo_available = allowed_actions.has(Action::ComboAdvancedTouch); - return !combo_available || matches!(action, Action::ComboAdvancedTouch); + let combo_available = allowed_actions.has(Action::AdvancedTouch); + return !combo_available || matches!(action, Action::AdvancedTouch); } Combo::SynthesisBegin => { let combo_available = allowed_actions.has(Action::Reflect) diff --git a/solvers/src/macro_solver/solver.rs b/solvers/src/macro_solver/solver.rs index 2ba55ec..f889896 100644 --- a/solvers/src/macro_solver/solver.rs +++ b/solvers/src/macro_solver/solver.rs @@ -109,10 +109,10 @@ impl<'a> MacroSolver<'a> { } let progress_only = - is_progress_only_state(state, self.backload_progress, self.unsound_branch_pruning); + is_progress_only_state(&state, self.backload_progress, self.unsound_branch_pruning); let search_actions = match progress_only { - true => PROGRESS_SEARCH_ACTIONS.intersection(self.settings.allowed_actions), - false => FULL_SEARCH_ACTIONS.intersection(self.settings.allowed_actions), + true => PROGRESS_SEARCH_ACTIONS, + false => FULL_SEARCH_ACTIONS, }; let current_steps = search_queue.steps(backtrack_id); @@ -150,7 +150,7 @@ impl<'a> MacroSolver<'a> { }; let progress_only = is_progress_only_state( - state, + &state, self.backload_progress, self.unsound_branch_pruning, ); diff --git a/solvers/src/quality_upper_bound_solver/solver.rs b/solvers/src/quality_upper_bound_solver/solver.rs index cd54efa..73bf6f6 100644 --- a/solvers/src/quality_upper_bound_solver/solver.rs +++ b/solvers/src/quality_upper_bound_solver/solver.rs @@ -1,9 +1,8 @@ use crate::{ actions::{PROGRESS_ACTIONS, QUALITY_ACTIONS}, - branch_pruning::is_progress_only_state, utils::{ParetoFrontBuilder, ParetoFrontId, ParetoValue}, }; -use simulator::{Action, ActionMask, Combo, Condition, Settings, SimulationState, SingleUse}; +use simulator::*; use rustc_hash::FxHashMap as HashMap; @@ -19,11 +18,15 @@ const PROGRESS_SEARCH_ACTIONS: ActionMask = PROGRESS_ACTIONS .add(Action::WasteNot) .add(Action::WasteNot2); +pub struct SolverSettings { + pub durability_cost: i16, // how much CP does it cost to restore 5 durability? + pub backload_progress: bool, + pub unsound_branch_pruning: bool, +} + pub struct QualityUpperBoundSolver { - settings: Settings, // simulator settings - backload_progress: bool, - unsound_branch_pruning: bool, - durability_cost: i16, + simulator_settings: Settings, + solver_settings: SolverSettings, solved_states: HashMap, pareto_front_builder: ParetoFrontBuilder, // pre-computed branch pruning values @@ -35,59 +38,53 @@ impl QualityUpperBoundSolver { pub fn new(settings: Settings, backload_progress: bool, unsound_branch_pruning: bool) -> Self { dbg!(std::mem::size_of::()); dbg!(std::mem::align_of::()); - let mut durability_cost = Action::MasterMend.cp_cost() / 6; - if settings.allowed_actions.has(Action::Manipulation) { - durability_cost = std::cmp::min(durability_cost, Action::Manipulation.cp_cost() / 8); + + let initial_state = SimulationState::new(&settings); + let mut durability_cost = 100; + if settings.is_action_allowed::() { + let master_mend_cost = MasterMend::base_cp_cost(&initial_state, &settings); + durability_cost = std::cmp::min(durability_cost, master_mend_cost / 6); } - if settings.allowed_actions.has(Action::ImmaculateMend) { - durability_cost = std::cmp::min( - durability_cost, - Action::ImmaculateMend.cp_cost() / (settings.max_durability as i16 / 5 - 1), - ); + if settings.is_action_allowed::() { + let manipulation_cost = Manipulation::base_cp_cost(&initial_state, &settings); + durability_cost = std::cmp::min(durability_cost, manipulation_cost / 8); } + if settings.is_action_allowed::() { + let immaculate_mend_cost = ImmaculateMend::base_cp_cost(&initial_state, &settings); + let max_restored = settings.max_durability as i16 / 5 - 1; + durability_cost = std::cmp::min(durability_cost, immaculate_mend_cost / max_restored); + } + Self { - settings, - backload_progress, - unsound_branch_pruning, - durability_cost, + simulator_settings: settings, + solver_settings: SolverSettings { + durability_cost, + backload_progress, + unsound_branch_pruning, + }, solved_states: HashMap::default(), pareto_front_builder: ParetoFrontBuilder::new( settings.max_progress, settings.max_quality, ), - waste_not_1_min_cp: waste_not_min_cp(Action::WasteNot.cp_cost(), 4, durability_cost), - waste_not_2_min_cp: waste_not_min_cp(Action::WasteNot2.cp_cost(), 8, durability_cost), + waste_not_1_min_cp: waste_not_min_cp(56, 4, durability_cost), + waste_not_2_min_cp: waste_not_min_cp(98, 8, durability_cost), } } /// Returns an upper-bound on the maximum Quality achievable from this state while also maxing out Progress. /// There is no guarantee on the tightness of the upper-bound. - pub fn quality_upper_bound(&mut self, mut state: SimulationState) -> u16 { + pub fn quality_upper_bound(&mut self, state: SimulationState) -> u16 { let current_quality = state.quality; - let missing_progress = self.settings.max_progress.saturating_sub(state.progress); - - // refund effects and durability - state.cp += state.effects.manipulation() as i16 * (Action::Manipulation.cp_cost() / 8); - state.cp += state.durability as i16 / 5 * self.durability_cost; - if state.effects.trained_perfection() != SingleUse::Unavailable - && self.settings.allowed_actions.has(Action::TrainedPerfection) - { - state.cp += self.durability_cost * 4; - } - - if state.effects.heart_and_soul() == SingleUse::Available { - state.effects.set_heart_and_soul(SingleUse::Active); - } - - state.durability = i8::MAX; + let missing_progress = self + .simulator_settings + .max_progress + .saturating_sub(state.progress); - let progress_only = - is_progress_only_state(state, self.backload_progress, self.unsound_branch_pruning); - let reduced_state = ReducedState::from_state( + let reduced_state = ReducedState::from_simulation_state( state, - progress_only, - self.durability_cost, - self.settings.base_quality, + &self.simulator_settings, + &self.solver_settings, ); let pareto_front = match self.solved_states.get(&reduced_state) { Some(id) => self.pareto_front_builder.retrieve(*id), @@ -114,7 +111,7 @@ impl QualityUpperBoundSolver { }; std::cmp::min( - self.settings.max_quality, + self.simulator_settings.max_quality, pareto_front[index].second.saturating_add(current_quality), ) } @@ -130,9 +127,9 @@ impl QualityUpperBoundSolver { fn solve_normal_state(&mut self, state: ReducedState) { self.pareto_front_builder.push_empty(); let search_actions = if state.data.progress_only() { - PROGRESS_SEARCH_ACTIONS.intersection(self.settings.allowed_actions) + PROGRESS_SEARCH_ACTIONS.intersection(self.simulator_settings.allowed_actions) } else { - FULL_SEARCH_ACTIONS.intersection(self.settings.allowed_actions) + FULL_SEARCH_ACTIONS.intersection(self.simulator_settings.allowed_actions) }; for action in search_actions.actions_iter() { if !self.should_use_action(state, action) { @@ -151,59 +148,32 @@ impl QualityUpperBoundSolver { } fn solve_combo_state(&mut self, state: ReducedState) { - match self.solved_states.get(&state.to_non_combo()) { + match self.solved_states.get(&state.drop_combo()) { Some(id) => self.pareto_front_builder.push_from_id(*id), - None => self.solve_normal_state(state.to_non_combo()), + None => self.solve_normal_state(state.drop_combo()), } match state.data.combo() { Combo::None => unreachable!(), Combo::SynthesisBegin => { - for action in [Action::MuscleMemory, Action::Reflect, Action::TrainedEye] { - if self.settings.allowed_actions.has(action) { - self.build_child_front(state, action); - } - } + self.build_child_front(state, Action::MuscleMemory); + self.build_child_front(state, Action::Reflect); + self.build_child_front(state, Action::TrainedEye); } Combo::BasicTouch => { - for action in [Action::ComboRefinedTouch, Action::ComboStandardTouch] { - if self.settings.allowed_actions.has(action) { - self.build_child_front(state, action); - } - } + self.build_child_front(state, Action::RefinedTouch); + self.build_child_front(state, Action::StandardTouch); } Combo::StandardTouch => { - if self - .settings - .allowed_actions - .has(Action::ComboAdvancedTouch) - { - self.build_child_front(state, Action::ComboAdvancedTouch); - } + self.build_child_front(state, Action::AdvancedTouch); } } } fn build_child_front(&mut self, state: ReducedState, action: Action) { - if let Ok(new_state) = state.to_state(self.settings.base_quality).use_action( - action, - Condition::Normal, - &self.settings, - ) { - let action_progress = new_state.progress; - let action_quality = new_state.quality; - let progress_only = state.data.progress_only() - || is_progress_only_state( - new_state, - self.backload_progress, - self.unsound_branch_pruning, - ); - let new_state = ReducedState::from_state( - new_state, - progress_only, - self.durability_cost, - self.settings.base_quality, - ); - if new_state.data.cp() >= self.durability_cost { + if let Ok((new_state, action_progress, action_quality)) = + state.use_action(action, &self.simulator_settings, &self.solver_settings) + { + if new_state.data.cp() >= self.solver_settings.durability_cost { match self.solved_states.get(&new_state) { Some(id) => self.pareto_front_builder.push_from_id(*id), None => self.solve_state(new_state), @@ -213,7 +183,9 @@ impl QualityUpperBoundSolver { value.second += action_quality; }); self.pareto_front_builder.merge(); - } else if new_state.data.cp() >= -self.durability_cost && action_progress != 0 { + } else if new_state.data.cp() >= -self.solver_settings.durability_cost + && action_progress != 0 + { // "durability" must not go lower than -5 // last action must be a progress increase self.pareto_front_builder @@ -238,6 +210,8 @@ fn waste_not_min_cp( effect_duration: i16, durability_cost: i16, ) -> i16 { + const BASIC_SYNTH_CP: i16 = 0; + const GROUNDWORK_CP: i16 = 18; // how many units of 5-durability does WasteNot have to save to be worth using over magically restoring durability? let min_durability_save = (waste_not_action_cp_cost - 1) / durability_cost + 1; if min_durability_save > effect_duration * 2 { @@ -247,8 +221,8 @@ fn waste_not_min_cp( let double_dur_count = min_durability_save.saturating_sub(effect_duration); let single_dur_count = min_durability_save.abs_diff(effect_duration) as i16; // minimum CP required to execute those actions - let double_dur_cost = double_dur_count * (Action::Groundwork.cp_cost() + durability_cost * 2); - let single_dur_cost = single_dur_count * (Action::BasicSynthesis.cp_cost() + durability_cost); + let double_dur_cost = double_dur_count * (GROUNDWORK_CP + durability_cost * 2); + let single_dur_cost = single_dur_count * (BASIC_SYNTH_CP + durability_cost); waste_not_action_cp_cost + double_dur_cost + single_dur_cost - durability_cost } @@ -277,7 +251,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -310,7 +284,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -343,7 +317,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -373,7 +347,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -403,7 +377,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -422,7 +396,7 @@ mod tests { Action::PreparatoryTouch, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, + Action::StandardTouch, ], ); assert_eq!(result, 4053); @@ -438,7 +412,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -457,7 +431,7 @@ mod tests { Action::PreparatoryTouch, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, + Action::StandardTouch, ], ); assert_eq!(result, 3406); @@ -473,7 +447,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -493,7 +467,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -513,7 +487,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -533,7 +507,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -553,7 +527,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -573,7 +547,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -593,7 +567,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -613,7 +587,7 @@ mod tests { base_progress: 10000, base_quality: 10000, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -633,7 +607,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) @@ -654,7 +628,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) @@ -675,7 +649,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) @@ -696,7 +670,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), diff --git a/solvers/src/quality_upper_bound_solver/state.rs b/solvers/src/quality_upper_bound_solver/state.rs index 1b54afd..17fdf4d 100644 --- a/solvers/src/quality_upper_bound_solver/state.rs +++ b/solvers/src/quality_upper_bound_solver/state.rs @@ -1,4 +1,7 @@ -use simulator::{Combo, Effects, SimulationState, SingleUse}; +use crate::branch_pruning::is_progress_only_state; + +use super::solver::SolverSettings; +use simulator::*; #[bitfield_struct::bitfield(u32)] #[derive(PartialEq, Eq, Hash)] @@ -20,25 +23,44 @@ pub struct ReducedState { } impl ReducedState { - pub fn to_non_combo(self) -> Self { - Self { - data: self.data.with_combo(Combo::None), - effects: self.effects, + pub fn from_simulation_state( + mut state: SimulationState, + simulator_settings: &Settings, + solver_settings: &SolverSettings, + ) -> Self { + state.cp += state.effects.manipulation() as i16 + * (Manipulation::base_cp_cost(&state, &simulator_settings) / 8); + if state.effects.trained_perfection() != SingleUse::Unavailable + && simulator_settings.is_action_allowed::() + { + state.cp += solver_settings.durability_cost * 4; } + if state.effects.heart_and_soul() == SingleUse::Available { + state.effects.set_heart_and_soul(SingleUse::Active); + } + state.cp += state.durability as i16 / 5 * solver_settings.durability_cost; + state.durability = simulator_settings.max_durability; + Self::from_simulation_state_inner(&state, simulator_settings, solver_settings) } - pub fn from_state( - state: SimulationState, - progress_only: bool, - durability_cost: i16, - base_quality: u16, + fn from_simulation_state_inner( + state: &SimulationState, + simulator_settings: &Settings, + solver_settings: &SolverSettings, ) -> Self { - let used_durability = (i8::MAX - state.durability) / 5; - let cp = state.cp - used_durability as i16 * durability_cost; + let progress_only = is_progress_only_state( + state, + solver_settings.backload_progress, + solver_settings.unsound_branch_pruning, + ); + let used_durability = (simulator_settings.max_durability - state.durability) / 5; + let cp = state.cp - used_durability as i16 * solver_settings.durability_cost; let unreliable_quality = if progress_only { 0 } else { - ((state.unreliable_quality + 2 * base_quality - 1) / (2 * base_quality)) as u8 + state + .unreliable_quality + .div_ceil(2 * simulator_settings.base_quality) as u8 }; let effects = if progress_only { state @@ -68,15 +90,49 @@ impl ReducedState { } } - pub fn to_state(self, base_quality: u16) -> SimulationState { + pub fn drop_combo(self) -> Self { + Self { + data: self.data.with_combo(Combo::None), + effects: self.effects, + } + } + + fn to_simulation_state(&self, settings: &Settings) -> SimulationState { SimulationState { - durability: i8::MAX, + durability: settings.max_durability, cp: self.data.cp(), progress: 0, quality: 0, - unreliable_quality: self.data.unreliable_quality() as u16 * base_quality * 2, + unreliable_quality: self.data.unreliable_quality() as u16 * settings.base_quality * 2, effects: self.effects, combo: self.data.combo(), } } + + pub fn use_action( + &self, + action: Action, + simulator_settings: &Settings, + solver_settings: &SolverSettings, + ) -> Result<(Self, u16, u16), &'static str> { + if matches!( + action, + Action::MasterMend | Action::ImmaculateMend | Action::Manipulation + ) { + panic!("Action not supported.") + } + let progress_only = self.data.progress_only(); + let state = self.to_simulation_state(simulator_settings); + match state.use_action(action, Condition::Normal, simulator_settings) { + Ok(state) => { + let mut solver_state = + Self::from_simulation_state_inner(&state, simulator_settings, solver_settings); + if progress_only { + solver_state.data.set_progress_only(true); + } + Ok((solver_state, state.progress, state.quality)) + } + Err(err) => Err(err), + } + } } diff --git a/solvers/src/step_lower_bound_solver/solver.rs b/solvers/src/step_lower_bound_solver/solver.rs index c79608b..918af05 100644 --- a/solvers/src/step_lower_bound_solver/solver.rs +++ b/solvers/src/step_lower_bound_solver/solver.rs @@ -3,7 +3,7 @@ use crate::{ branch_pruning::is_progress_only_state, utils::{ParetoFrontBuilder, ParetoFrontId, ParetoValue}, }; -use simulator::{Action, ActionMask, Combo, Condition, Settings, SimulationState}; +use simulator::*; use rustc_hash::FxHashMap as HashMap; @@ -35,23 +35,17 @@ impl StepLowerBoundSolver { dbg!(std::mem::size_of::()); dbg!(std::mem::align_of::()); let mut bonus_durability_restore = 0; - if settings.allowed_actions.has(Action::Manipulation) { - bonus_durability_restore = std::cmp::max(bonus_durability_restore, 10); - } - if settings.allowed_actions.has(Action::ImmaculateMend) { + if settings.is_action_allowed::() { bonus_durability_restore = std::cmp::max(bonus_durability_restore, settings.max_durability - 35); } - if settings.allowed_actions.has(Action::Manipulation) { + if settings.is_action_allowed::() { + bonus_durability_restore = std::cmp::max(bonus_durability_restore, 10); settings.max_durability += 40; } + ReducedState::optimize_action_mask(&mut settings); Self { - settings: Settings { - allowed_actions: ReducedState::optimize_action_mask(settings.allowed_actions) - .remove(Action::Manipulation) - .remove(Action::ImmaculateMend), - ..settings - }, + settings, backload_progress, unsound_branch_pruning, bonus_durability_restore, @@ -87,7 +81,7 @@ impl StepLowerBoundSolver { let missing_progress = self.settings.max_progress.saturating_sub(state.progress); let progress_only = - is_progress_only_state(state, self.backload_progress, self.unsound_branch_pruning); + is_progress_only_state(&state, self.backload_progress, self.unsound_branch_pruning); let reduced_state = ReducedState::from_state(state, step_budget, progress_only); let pareto_front = match self.solved_states.get(&reduced_state) { @@ -130,8 +124,8 @@ impl StepLowerBoundSolver { fn solve_normal_state(&mut self, reduced_state: ReducedState) { self.pareto_front_builder.push_empty(); let search_actions = match reduced_state.progress_only { - false => FULL_SEARCH_ACTIONS.intersection(self.settings.allowed_actions), - true => PROGRESS_SEARCH_ACTIONS.intersection(self.settings.allowed_actions), + false => FULL_SEARCH_ACTIONS, + true => PROGRESS_SEARCH_ACTIONS, }; for action in search_actions.actions_iter() { self.build_child_front(reduced_state, action); @@ -154,17 +148,13 @@ impl StepLowerBoundSolver { match reduced_state.combo { Combo::None => unreachable!(), Combo::SynthesisBegin => { - for action in [Action::MuscleMemory, Action::Reflect, Action::TrainedEye] { - if self.settings.allowed_actions.has(action) { - self.build_child_front(reduced_state, action); - } - } + self.build_child_front(reduced_state, Action::MuscleMemory); + self.build_child_front(reduced_state, Action::Reflect); + self.build_child_front(reduced_state, Action::TrainedEye); } Combo::BasicTouch => { - if !reduced_state.progress_only - && self.settings.allowed_actions.has(Action::ComboRefinedTouch) - { - self.build_child_front(reduced_state, Action::ComboRefinedTouch); + if !reduced_state.progress_only { + self.build_child_front(reduced_state, Action::RefinedTouch); } } Combo::StandardTouch => unreachable!(), @@ -181,7 +171,7 @@ impl StepLowerBoundSolver { let action_quality = new_full_state.quality; let progress_only = reduced_state.progress_only || is_progress_only_state( - new_full_state, + &new_full_state, self.backload_progress, self.unsound_branch_pruning, ); @@ -241,7 +231,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -274,7 +264,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -307,7 +297,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -337,7 +327,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -367,7 +357,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -386,7 +376,7 @@ mod tests { Action::PreparatoryTouch, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, + Action::StandardTouch, ], ); assert_eq!(result, 13); @@ -402,7 +392,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -421,7 +411,7 @@ mod tests { Action::PreparatoryTouch, Action::Innovation, Action::BasicTouch, - Action::ComboStandardTouch, + Action::StandardTouch, ], ); assert_eq!(result, 13); @@ -437,7 +427,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -457,7 +447,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -477,7 +467,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -497,7 +487,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -517,7 +507,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -537,7 +527,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -557,7 +547,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -577,7 +567,7 @@ mod tests { base_progress: 10000, base_quality: 10000, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -597,7 +587,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) @@ -618,7 +608,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) @@ -639,7 +629,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) @@ -660,7 +650,7 @@ mod tests { base_progress: 100, base_quality: 100, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), diff --git a/solvers/src/step_lower_bound_solver/state.rs b/solvers/src/step_lower_bound_solver/state.rs index a0e00a5..d1402e2 100644 --- a/solvers/src/step_lower_bound_solver/state.rs +++ b/solvers/src/step_lower_bound_solver/state.rs @@ -1,4 +1,4 @@ -use simulator::{Action, ActionMask, Combo, Effects, SimulationState, SingleUse}; +use simulator::*; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct ReducedState { @@ -17,27 +17,25 @@ impl ReducedState { } } - pub fn optimize_action_mask(mut action_mask: ActionMask) -> ActionMask { - action_mask = action_mask.remove(Action::TrainedPerfection); - // No CP cost so Observe is useless - action_mask = action_mask.remove(Action::Observe); - // Non-combo version is just as good as the combo version because there is no CP cost - action_mask = action_mask - .remove(Action::ComboStandardTouch) - .remove(Action::ComboAdvancedTouch); + pub fn optimize_action_mask(settings: &mut Settings) { + settings.allowed_actions = settings + .allowed_actions + .remove(Action::Observe) + .remove(Action::Manipulation) + .remove(Action::TrainedPerfection) + .remove(Action::ImmaculateMend); // WasteNot2 is always better than WasteNot because there is no CP cost - if action_mask.has(Action::WasteNot2) { - action_mask = action_mask.remove(Action::WasteNot); + if settings.is_action_allowed::() { + settings.allowed_actions = settings.allowed_actions.remove(Action::WasteNot); } // CarefulSynthesis is always better than BasicSynthesis because there is no CP cost - if action_mask.has(Action::CarefulSynthesis) { - action_mask = action_mask.remove(Action::BasicSynthesis); + if settings.is_action_allowed::() { + settings.allowed_actions = settings.allowed_actions.remove(Action::BasicSynthesis); } - // AdvancedTouch (non-combo) is always better than StandardTouch (non-combo) because there is no CP cost - if action_mask.has(Action::AdvancedTouch) { - action_mask = action_mask.remove(Action::StandardTouch); + // AdvancedTouch is always better than StandardTouch because there is no CP cost + if settings.is_action_allowed::() { + settings.allowed_actions = settings.allowed_actions.remove(Action::StandardTouch); } - action_mask } pub fn from_state(state: SimulationState, steps_budget: u8, progress_only: bool) -> Self { diff --git a/solvers/tests/01_normal_solver_tests.rs b/solvers/tests/01_normal_solver_tests.rs index cb470c2..87f0805 100644 --- a/solvers/tests/01_normal_solver_tests.rs +++ b/solvers/tests/01_normal_solver_tests.rs @@ -1,4 +1,4 @@ -use simulator::{Action, ActionMask, Settings}; +use simulator::*; use solvers::test_utils::*; #[test] @@ -11,7 +11,7 @@ fn unsolvable() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -31,7 +31,7 @@ fn zero_quality() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -52,7 +52,7 @@ fn max_quality() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -73,7 +73,7 @@ fn random_0f93c79f() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -94,7 +94,7 @@ fn random_1e281667() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -115,7 +115,7 @@ fn random_d0bf2aef() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -136,7 +136,7 @@ fn random_e413e05d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -157,7 +157,7 @@ fn random_bb38a037() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -178,7 +178,7 @@ fn random_a300ca2b() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -199,7 +199,7 @@ fn random_0f9d7781() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -220,7 +220,7 @@ fn random_e451d981() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -241,7 +241,7 @@ fn random_6799bb1d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -262,7 +262,7 @@ fn random_940b4755() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -283,7 +283,7 @@ fn rinascita_3700_3280() { base_progress: 229, base_quality: 224, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -304,7 +304,7 @@ fn pactmaker_3240_3130() { base_progress: 200, base_quality: 215, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -325,7 +325,7 @@ fn pactmaker_3240_3130_heart_and_soul() { base_progress: 200, base_quality: 215, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::QuickInnovation), adversarial: false, @@ -345,7 +345,7 @@ fn diadochos_4021_3660() { base_progress: 249, base_quality: 247, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -366,7 +366,7 @@ fn indagator_3858_4057() { base_progress: 239, base_quality: 271, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -387,7 +387,7 @@ fn random_2ea6c001() { base_progress: 241, base_quality: 322, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -408,7 +408,7 @@ fn random_48ae7c9f() { base_progress: 295, base_quality: 310, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -428,8 +428,8 @@ fn random_4ecd54c4() { max_quality: 40000, base_progress: 100, base_quality: 100, - job_level: 100, - allowed_actions: ActionMask::from_level(90) + job_level: 90, + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -437,7 +437,7 @@ fn random_4ecd54c4() { }; let actions = solve(&settings, false, false).unwrap(); let score = get_score_triple(&settings, &actions); - assert_eq!(score, (3080, 19, 51)); + assert_eq!(score, (3002, 19, 51)); } #[test] @@ -450,7 +450,7 @@ fn rarefied_tacos_de_carne_asada_4785_4758() { base_progress: 256, base_quality: 265, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -473,7 +473,7 @@ fn stuffed_peppers_2() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -496,7 +496,7 @@ fn stuffed_peppers_2_heart_and_soul() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::QuickInnovation), adversarial: false, @@ -518,7 +518,7 @@ fn stuffed_peppers_2_quick_innovation() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul), adversarial: false, @@ -538,7 +538,7 @@ fn rakaznar_lapidary_hammer_4462_4391() { base_progress: 237, base_quality: 245, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -559,7 +559,7 @@ fn black_star_4048_3997() { base_progress: 250, base_quality: 312, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -580,7 +580,7 @@ fn claro_walnut_lumber_4900_4800() { base_progress: 300, base_quality: 368, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -601,7 +601,7 @@ fn rakaznar_lapidary_hammer_4900_4800() { base_progress: 261, base_quality: 266, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -622,7 +622,7 @@ fn rarefied_tacos_de_carne_asada_4966_4817() { base_progress: 264, base_quality: 267, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -643,7 +643,7 @@ fn archeo_kingdom_broadsword_4966_4914() { base_progress: 264, base_quality: 271, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), diff --git a/solvers/tests/02_progress_backload_solver_tests.rs b/solvers/tests/02_progress_backload_solver_tests.rs index 297175d..40d7ef1 100644 --- a/solvers/tests/02_progress_backload_solver_tests.rs +++ b/solvers/tests/02_progress_backload_solver_tests.rs @@ -1,18 +1,6 @@ -use simulator::{Action, ActionMask, Settings}; +use simulator::*; use solvers::test_utils::*; -fn is_progress_backloaded(actions: &[Action], settings: &Settings) -> bool { - let first_progress_action = actions - .iter() - .position(|action| action.progress_efficiency(settings.job_level) != 0) - .unwrap(); - // there musn't be any Quality-increasing actions after the first Progress-increasing action - !actions - .into_iter() - .skip(first_progress_action + 1) - .any(|action| action.quality_efficiency(settings.job_level) != 0) -} - #[test] fn random_0f93c79f() { let settings = Settings { @@ -23,7 +11,7 @@ fn random_0f93c79f() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -45,7 +33,7 @@ fn random_1e281667() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -67,7 +55,7 @@ fn random_d0bf2aef() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -89,7 +77,7 @@ fn random_e413e05d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -111,7 +99,7 @@ fn random_bb38a037() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -133,7 +121,7 @@ fn random_a300ca2b() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -155,7 +143,7 @@ fn random_0f9d7781() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -177,7 +165,7 @@ fn random_e451d981() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -199,7 +187,7 @@ fn random_6799bb1d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -221,7 +209,7 @@ fn random_940b4755() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -243,7 +231,7 @@ fn rinascita_3700_3280() { base_progress: 229, base_quality: 224, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -265,7 +253,7 @@ fn pactmaker_3240_3130() { base_progress: 200, base_quality: 215, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -287,7 +275,7 @@ fn pactmaker_3240_3130_heart_and_soul() { base_progress: 200, base_quality: 215, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::QuickInnovation), adversarial: false, @@ -308,7 +296,7 @@ fn diadochos_4021_3660() { base_progress: 249, base_quality: 247, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -330,7 +318,7 @@ fn indagator_3858_4057() { base_progress: 239, base_quality: 271, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -352,7 +340,7 @@ fn random_2ea6c001() { base_progress: 241, base_quality: 322, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -374,7 +362,7 @@ fn random_48ae7c9f() { base_progress: 295, base_quality: 310, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -395,8 +383,8 @@ fn random_4ecd54c4() { max_quality: 40000, base_progress: 100, base_quality: 100, - job_level: 100, - allowed_actions: ActionMask::from_level(90) + job_level: 90, + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -418,7 +406,7 @@ fn rarefied_tacos_de_carne_asada_4785_4758() { base_progress: 256, base_quality: 265, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -442,7 +430,7 @@ fn stuffed_peppers_2() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -466,7 +454,7 @@ fn stuffed_peppers_2_heart_and_soul() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::QuickInnovation), adversarial: false, @@ -489,7 +477,7 @@ fn stuffed_peppers_2_quick_innovation() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul), adversarial: false, @@ -510,7 +498,7 @@ fn rakaznar_lapidary_hammer_4462_4391() { base_progress: 237, base_quality: 245, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -532,7 +520,7 @@ fn black_star_4048_3997() { base_progress: 250, base_quality: 312, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -554,7 +542,7 @@ fn claro_walnut_lumber_4900_4800() { base_progress: 300, base_quality: 368, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -576,7 +564,7 @@ fn rakaznar_lapidary_hammer_4900_4800() { base_progress: 261, base_quality: 266, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -598,7 +586,7 @@ fn rarefied_tacos_de_carne_asada_4966_4817() { base_progress: 264, base_quality: 267, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -620,7 +608,7 @@ fn archeo_kingdom_broadsword_4966_4914() { base_progress: 264, base_quality: 271, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), diff --git a/solvers/tests/03_unsound_progress_backload_solver_tests.rs b/solvers/tests/03_unsound_progress_backload_solver_tests.rs index 44a37f3..2357431 100644 --- a/solvers/tests/03_unsound_progress_backload_solver_tests.rs +++ b/solvers/tests/03_unsound_progress_backload_solver_tests.rs @@ -1,18 +1,6 @@ -use simulator::{Action, ActionMask, Settings}; +use simulator::*; use solvers::test_utils::*; -fn is_progress_backloaded(actions: &[Action], settings: &Settings) -> bool { - let first_progress_action = actions - .iter() - .position(|action| action.progress_efficiency(settings.job_level) != 0) - .unwrap(); - // there musn't be any Quality-increasing actions after the first Progress-increasing action - !actions - .into_iter() - .skip(first_progress_action + 1) - .any(|action| action.quality_efficiency(settings.job_level) != 0) -} - #[test] fn random_0f93c79f() { let settings = Settings { @@ -23,7 +11,7 @@ fn random_0f93c79f() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -45,7 +33,7 @@ fn random_1e281667() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -67,7 +55,7 @@ fn random_d0bf2aef() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -89,7 +77,7 @@ fn random_e413e05d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -111,7 +99,7 @@ fn random_bb38a037() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -133,7 +121,7 @@ fn random_a300ca2b() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -155,7 +143,7 @@ fn random_0f9d7781() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -177,7 +165,7 @@ fn random_e451d981() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -199,7 +187,7 @@ fn random_6799bb1d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -221,7 +209,7 @@ fn random_940b4755() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -243,7 +231,7 @@ fn rinascita_3700_3280() { base_progress: 229, base_quality: 224, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -265,7 +253,7 @@ fn pactmaker_3240_3130() { base_progress: 200, base_quality: 215, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -287,7 +275,7 @@ fn pactmaker_3240_3130_heart_and_soul() { base_progress: 200, base_quality: 215, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::QuickInnovation), adversarial: false, @@ -308,7 +296,7 @@ fn diadochos_4021_3660() { base_progress: 249, base_quality: 247, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -330,7 +318,7 @@ fn indagator_3858_4057() { base_progress: 239, base_quality: 271, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -352,7 +340,7 @@ fn random_2ea6c001() { base_progress: 241, base_quality: 322, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -374,7 +362,7 @@ fn random_48ae7c9f() { base_progress: 295, base_quality: 310, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -395,8 +383,8 @@ fn random_4ecd54c4() { max_quality: 40000, base_progress: 100, base_quality: 100, - job_level: 100, - allowed_actions: ActionMask::from_level(90) + job_level: 90, + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -418,7 +406,7 @@ fn rarefied_tacos_de_carne_asada_4785_4758() { base_progress: 256, base_quality: 265, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -442,7 +430,7 @@ fn stuffed_peppers_2() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -466,7 +454,7 @@ fn stuffed_peppers_2_heart_and_soul() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::QuickInnovation), adversarial: false, @@ -489,7 +477,7 @@ fn stuffed_peppers_2_quick_innovation() { base_progress: 289, base_quality: 360, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul), adversarial: false, @@ -510,7 +498,7 @@ fn rakaznar_lapidary_hammer_4462_4391() { base_progress: 237, base_quality: 245, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -532,7 +520,7 @@ fn black_star_4048_3997() { base_progress: 250, base_quality: 312, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -554,7 +542,7 @@ fn claro_walnut_lumber_4900_4800() { base_progress: 300, base_quality: 368, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -576,7 +564,7 @@ fn rakaznar_lapidary_hammer_4900_4800() { base_progress: 261, base_quality: 266, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -598,7 +586,7 @@ fn rarefied_tacos_de_carne_asada_4966_4817() { base_progress: 264, base_quality: 267, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -620,7 +608,7 @@ fn archeo_kingdom_broadsword_4966_4914() { base_progress: 264, base_quality: 271, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), diff --git a/solvers/tests/04_adversarial_solver_tests.rs b/solvers/tests/04_adversarial_solver_tests.rs index ae392e8..2cc2737 100644 --- a/solvers/tests/04_adversarial_solver_tests.rs +++ b/solvers/tests/04_adversarial_solver_tests.rs @@ -1,4 +1,4 @@ -use simulator::{Action, ActionMask, Settings}; +use simulator::*; use solvers::test_utils::*; const SETTINGS: Settings = Settings { @@ -39,7 +39,7 @@ fn random_1e281667() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -60,7 +60,7 @@ fn random_d0bf2aef() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -81,7 +81,7 @@ fn random_e413e05d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -102,7 +102,7 @@ fn random_bb38a037() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -123,7 +123,7 @@ fn random_0f9d7781() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -144,7 +144,7 @@ fn random_6799bb1d() { base_progress: 100, base_quality: 100, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -165,7 +165,7 @@ fn random_2ea6c001() { base_progress: 241, base_quality: 322, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -185,8 +185,8 @@ fn random_4ecd54c4() { max_quality: 40000, base_progress: 100, base_quality: 100, - job_level: 100, - allowed_actions: ActionMask::from_level(90) + job_level: 90, + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -194,7 +194,7 @@ fn random_4ecd54c4() { }; let actions = solve(&settings, false, false).unwrap(); let score = get_score_triple(&settings, &actions); - assert_eq!(score, (2759, 17, 47)); + assert_eq!(score, (2717, 19, 52)); } #[test] @@ -250,7 +250,7 @@ fn test_mountain_chromite_ingot_no_manipulation() { base_progress: 217, base_quality: 293, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) @@ -272,7 +272,7 @@ fn test_indagator_3858_4057() { base_progress: 239, base_quality: 271, job_level: 90, - allowed_actions: ActionMask::from_level(90) + allowed_actions: ActionMask::all() .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) .remove(Action::QuickInnovation), @@ -293,7 +293,7 @@ fn test_rare_tacos_4628_4410() { base_progress: 246, base_quality: 246, job_level: 100, - allowed_actions: ActionMask::from_level(100) + allowed_actions: ActionMask::all() .remove(Action::Manipulation) .remove(Action::TrainedEye) .remove(Action::HeartAndSoul) diff --git a/src/app.rs b/src/app.rs index a2a8b4d..e9b0a91 100644 --- a/src/app.rs +++ b/src/app.rs @@ -15,7 +15,7 @@ use egui::{ }; use game_data::{action_name, get_initial_quality, get_job_name, Consumable, Locale}; -use simulator::Action; +use simulator::{Action, ActionImpl, HeartAndSoul, Manipulation, QuickInnovation}; use crate::config::{CrafterConfig, QualitySource, QualityTarget, RecipeConfiguration}; use crate::widgets::*; @@ -524,8 +524,7 @@ impl MacroSolverApp { ui.separator(); ui.label(egui::RichText::new(t!("label.actions")).strong()); - if self.crafter_config.active_stats().level >= Action::Manipulation.level_requirement() - { + if self.crafter_config.active_stats().level >= Manipulation::LEVEL_REQUIREMENT { ui.add(egui::Checkbox::new( &mut self.crafter_config.active_stats_mut().manipulation, format!("{}", action_name(Action::Manipulation, self.locale)), @@ -539,8 +538,7 @@ impl MacroSolverApp { ), ); } - if self.crafter_config.active_stats().level >= Action::HeartAndSoul.level_requirement() - { + if self.crafter_config.active_stats().level >= HeartAndSoul::LEVEL_REQUIREMENT { ui.add(egui::Checkbox::new( &mut self.crafter_config.active_stats_mut().heart_and_soul, format!("{}", action_name(Action::HeartAndSoul, self.locale)), @@ -554,9 +552,7 @@ impl MacroSolverApp { ), ); } - if self.crafter_config.active_stats().level - >= Action::QuickInnovation.level_requirement() - { + if self.crafter_config.active_stats().level >= QuickInnovation::LEVEL_REQUIREMENT { ui.add(egui::Checkbox::new( &mut self.crafter_config.active_stats_mut().quick_innovation, format!("{}", action_name(Action::QuickInnovation, self.locale)),