diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/BattleData/strategies.json b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/BattleData/strategies.json new file mode 100644 index 0000000000..7454fa6986 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/BattleData/strategies.json @@ -0,0 +1,14 @@ +{ +"TRAINER_RIVAL_POKEMON_TOWER_BULBASAUR": {"sp_attack": 2, "accuracy": 0, "outspeed": true, "source": "PokemonSimulation pokeguy-conditions.md tower-rival (+4 Sp. Atk while Torrent is off)"}, +"TRAINER_LEADER_KOGA": {"sp_attack": 1, "accuracy": 0, "outspeed": true, "source": "pokeguy-conditions.md koga"}, +"TRAINER_LEADER_ERIKA": {"sp_attack": 1, "accuracy": 2, "outspeed": true, "source": "pokeguy-conditions.md erika"}, +"TRAINER_RIVAL_SILPH_BULBASAUR": {"sp_attack": 2, "accuracy": 2, "outspeed": true, "source": "pokeguy-conditions.md silph-rival"}, +"TRAINER_BOSS_GIOVANNI_2": {"sp_attack": 1, "accuracy": 0, "outspeed": true, "source": "pokeguy-conditions.md silph-giovanni"}, +"TRAINER_LEADER_SABRINA": {"sp_attack": 0, "accuracy": 0, "outspeed": true, "source": "pokeguy-conditions.md sabrina"}, +"TRAINER_RIVAL_ROUTE22_LATE_BULBASAUR": {"sp_attack": 2, "accuracy": 2, "outspeed": true, "source": "pokeguy-conditions.md route22-rival"}, +"TRAINER_ELITE_FOUR_LORELEI": {"sp_attack": 4, "accuracy": 0, "outspeed": true, "source": "pokeguy-conditions.md lorelei"}, +"TRAINER_ELITE_FOUR_BRUNO": {"sp_attack": 2, "accuracy": 0, "outspeed": true, "source": "pokeguy-conditions.md bruno (two X Specials, one X Speed for Rock Tomb)"}, +"TRAINER_ELITE_FOUR_AGATHA": {"sp_attack": 1, "accuracy": 0, "outspeed": true, "source": "pokeguy-conditions.md agatha (more Sp. Atk while Torrent is off)"}, +"TRAINER_ELITE_FOUR_LANCE": {"sp_attack": 3, "accuracy": 2, "outspeed": true, "source": "pokeguy-conditions.md lance"}, +"TRAINER_CHAMPION_FIRST_BULBASAUR": {"sp_attack": 2, "accuracy": 2, "outspeed": true, "source": "pokeguy-conditions.md champion"} +} diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp index d2e1cda312..d99053ddd2 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleData_Tests.cpp @@ -19,6 +19,7 @@ #include "PokemonFRLG_AutoStoryBattleData.h" #include "PokemonFRLG_AutoStoryBattleDecisions.h" #include "PokemonFRLG_AutoStoryBattleSim.h" +#include "PokemonFRLG_AutoStoryBattleStrategy.h" #include "PokemonFRLG_AutoStoryParty.h" #include "PokemonFRLG_AutoStoryTable.h" @@ -230,6 +231,7 @@ void add_tests_AutoStoryBattleData(UnitTestDatabase& database){ database.add(scenario); } add_tests_AutoStoryBattleSim(database); + add_tests_AutoStoryBattleStrategy(database); } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.cpp index b42742a543..33b404c015 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.cpp @@ -730,8 +730,8 @@ PotionCase potion_case(const SimBattler& own, const SimBattler& foe, const SimTr }; } -bool potion_pays(const PotionCase& c){ - const uint16_t heal = static_cast(std::min(POTION_HP, c.max_hp - c.hp)); +bool potion_pays(const PotionCase& c, uint16_t heal_hp){ + const uint16_t heal = static_cast(std::min(heal_hp, c.max_hp - c.hp)); if (heal == 0){ return false; } diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.h index fc5bb46896..cfa8763dbf 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.h @@ -80,6 +80,14 @@ struct SimItems{ int potions = 0; int awakenings = 0; int antidotes = 0; + // The ones only the plan uses (PokemonFRLG_AutoStoryBattleStrategy.h). + int super_potions = 0; + int hyper_potions = 0; + int full_restores = 0; + int x_attacks = 0; + int x_specials = 0; + int x_speeds = 0; + int x_accuracies = 0; }; @@ -251,8 +259,8 @@ PotionCase potion_case(const SimBattler& own, const SimBattler& foe, const SimTr // attacking now wouldn't. Otherwise it attacks: a Potion the next turn // undoes only gives the foe another turn (console B, 2026-11-03, m51: five // Potions at 8/60 HP each undone by Misty's Starmie, then no Potions and -// a faint). -bool potion_pays(const PotionCase& c); +// a faint). `heal`: a bigger heal's HP, weighed the same way. +bool potion_pays(const PotionCase& c, uint16_t heal = POTION_HP); // BattleRisk::HIGH: a Potion now, with `potions` left: the foe's next turn // could faint the lead (PotionCase::max_hit) and potion_pays(). diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.cpp new file mode 100644 index 0000000000..4e67a44947 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.cpp @@ -0,0 +1,313 @@ +/* AutoStory Battle Strategy + * + * From: https://github.com/PokemonAutomation/ + * + */ + +#include +#include +#include +#include +#include "Common/Cpp/Json/JsonObject.h" +#include "Common/Cpp/Json/JsonValue.h" +#include "PokemonFRLG_AutoStoryBattleData.h" +#include "PokemonFRLG_AutoStoryBattleStrategy.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +constexpr int MAX_STAGE = 6; + +// No setup turn while the foe's next turn is a status move this often: the +// turn hands it the status (Erika's Vileplume, Koga's Venomoth). +constexpr double MAX_SETUP_STATUS_CHANCE = 0.5; + +// HP each heal restores; a Full Restore all of it. +constexpr uint16_t SUPER_POTION_HP = 50; +constexpr uint16_t HYPER_POTION_HP = 200; + +// The heals, smallest first. +constexpr std::array HEALS = {PlanItem::SUPER_POTION, PlanItem::HYPER_POTION, PlanItem::FULL_RESTORE}; + +using GoalTable = std::map>; + +int8_t stage_goal(const JsonObject& entry, const std::string& key){ + return static_cast(std::clamp(entry.get_integer_default(key), 0, MAX_STAGE)); +} + +const GoalTable& goal_table(){ + static const GoalTable TABLE = []{ + GoalTable table; + for (const auto& [file, text] : autostory_battle_json()){ + if (file != "strategies"){ + continue; + } + const JsonValue json = parse_json(text); + for (const auto& [trainer, value] : json.to_object_throw()){ + const JsonObject& entry = value.to_object_throw(); + table.emplace(trainer, SetupGoals{ + .attack = stage_goal(entry, "attack"), + .sp_attack = stage_goal(entry, "sp_attack"), + .accuracy = stage_goal(entry, "accuracy"), + .outspeed = entry.get_boolean_default("outspeed"), + }); + } + } + return table; + }(); + return TABLE; +} + +int stage_of(const SimBattler& battler, Stage stat){ + return battler.body.stages[static_cast(stat)]; +} + +uint32_t staged_speed(const SimBattler& battler){ + const uint32_t speed = staged_stat(battler.body.stats.speed, stage_of(battler, Stage::SPEED)); + return battler.status == SimStatus::PARALYSIS ? speed / 4 : speed; +} + +bool outspeeds_all(const SimBattler& own, const SimBattler& foe, const std::vector& to_come){ + const uint32_t speed = staged_speed(own); + if (speed <= staged_speed(foe)){ + return false; + } + return std::ranges::all_of(to_come, [&](const SimBattler& next){ + return next.hp == 0 || speed > staged_speed(next); + }); +} + +uint16_t eighth(const SimBattler& battler){ + return std::max(battler.body.stats.hp / 8, 1); +} + +// D: the foe's highest roll, and the poison and Leech Seed on the lead. +uint32_t worst_turn(const SimBattler& own, const SimBattler& foe){ + uint32_t residual = own.status == SimStatus::POISON ? eighth(own) : 0; + residual += own.seeded ? eighth(own) : 0; + return foe_max_hit(foe, own) + residual; +} + +// The chance the foe's next turn is a move that only puts a status on +// the lead (sleep, poison, paralysis), while it has none. +double status_move_chance(const SimBattler& own, const SimBattler& foe, const SimTrainer& trainer){ + if (own.status != SimStatus::NONE){ + return 0; + } + const std::vector chances = ai_move_chances(trainer, foe, own); + double chance = 0; + for (size_t slot = 0; slot < foe.moves.size(); slot++){ + const MoveData* move = move_data(foe.moves[slot].move); + const bool status = move != nullptr && move->power == 0 && ( + move->effect == "sleep" || move->effect == "poison" || move->effect == "toxic" || move->effect == "paralyze" + ); + chance += status ? chances[slot] : 0; + } + return chance; +} + +bool foe_explodes(const SimBattler& foe){ + return std::ranges::any_of(foe.moves, [](const SimMove& move){ + return move.pp > 0 && (move.move == "self-destruct" || move.move == "explosion"); + }); +} + +// The accuracy stage to set up to: the goal over the foe's evasion, none +// without a goal (a Minimize every turn outpaces X Accuracy). +int accuracy_goal(const SimBattler& foe, const SetupGoals& goals){ + if (goals.accuracy <= 0){ + return 0; + } + return std::min(goals.accuracy + std::max(stage_of(foe, Stage::EVASION), 0), MAX_STAGE); +} + +// N: the setup turns the goals still need, as far as the bag holds items. +int setup_turns_left(const SimBattler& own, const SimItems& items, const SimBattler& foe, const SetupGoals& goals){ + int turns = std::min(std::max(goals.attack - stage_of(own, Stage::ATTACK), 0), items.x_attacks); + turns += std::min(std::max(goals.sp_attack - stage_of(own, Stage::SP_ATTACK), 0), items.x_specials); + turns += std::min(std::max(accuracy_goal(foe, goals) - stage_of(own, Stage::ACCURACY), 0), items.x_accuracies); + return turns; +} + +// The heal before a setup turn: the smallest whose HP after the foe's mean +// turn reaches `reserve`, else the one that leaves the most, each above +// `worst`. None if no heal gets above it. +std::optional setup_heal(const SimBattler& own, const SimItems& items, uint32_t worst, double mean, double reserve){ + std::optional best; + double best_left = own.hp; + for (PlanItem heal : HEALS){ + const uint16_t gain = plan_item_heal(heal, own); + if (plan_item_count(items, heal) <= 0 || gain == 0 || own.hp + gain <= worst){ + continue; + } + const double left = own.hp + gain - mean; + if (left >= reserve){ + return heal; + } + if (left > best_left){ + best = heal; + best_left = left; + } + } + return best; +} + +// The setup turn, or the heal before it; none to leave it to own_turn(). +std::optional setup_turn( + const SimBattler& own, const SimItems& items, const SimBattler& foe, + const SimTrainer& trainer, size_t foes_left, + const std::vector& to_come, const SetupGoals& goals +){ + const std::optional next = next_setup_item(own, items, foe, to_come, goals); + if (!next || own.status == SimStatus::SLEEP || foe_explodes(foe)){ + return std::nullopt; + } + if (status_move_chance(own, foe, trainer) > MAX_SETUP_STATUS_CHANCE){ + return std::nullopt; + } + const uint32_t worst = worst_turn(own, foe); + if (worst >= own.body.stats.hp){ + return std::nullopt; + } + const PotionCase c = potion_case(own, foe, trainer); + if (foes_left <= 1 && c.turns_to_ko <= 1){ + return std::nullopt; + } + const int pending = std::max(setup_turns_left(own, items, foe, goals) + (*next == PlanItem::X_SPEED ? 1 : 0), 1); + const double reserve = std::min(0.75 * own.body.stats.hp, worst + (pending - 1) * c.expected_hit); + if (own.hp > worst && own.hp >= reserve){ + return next; + } + return setup_heal(own, items, worst, c.expected_hit, reserve); +} + +// A heal bigger than a Potion, when the foe's next turn could faint the +// lead and the lead doesn't faint it first: the smallest that pays. +std::optional bigger_heal(const SimBattler& own, const SimItems& items, const SimBattler& foe, const SimTrainer& trainer){ + if (own.hp >= own.body.stats.hp || own.status == SimStatus::SLEEP){ + return std::nullopt; + } + const PotionCase c = potion_case(own, foe, trainer); + if (c.max_hit < c.hp || (c.turns_to_ko <= 1 && staged_speed(own) > staged_speed(foe))){ + return std::nullopt; + } + for (PlanItem heal : HEALS){ + const uint16_t gain = plan_item_heal(heal, own); + if (plan_item_count(items, heal) > 0 && gain > POTION_HP && potion_pays(c, gain)){ + return heal; + } + } + return std::nullopt; +} + +} // namespace + + +SetupGoals setup_goals(std::string_view trainer){ + const GoalTable& table = goal_table(); + const auto it = table.find(trainer); + return it == table.end() ? SetupGoals{} : it->second; +} + +std::string_view plan_item_slug(PlanItem item){ + switch (item){ + case PlanItem::SUPER_POTION: + return "super-potion"; + case PlanItem::HYPER_POTION: + return "hyper-potion"; + case PlanItem::FULL_RESTORE: + return "full-restore"; + case PlanItem::X_ATTACK: + return "x-attack"; + case PlanItem::X_SPECIAL: + return "x-special"; + case PlanItem::X_SPEED: + return "x-speed"; + case PlanItem::X_ACCURACY: + return "x-accuracy"; + } + return ""; +} + +uint16_t plan_item_heal(PlanItem item, const SimBattler& own){ + const uint16_t missing = static_cast(own.body.stats.hp - std::min(own.hp, own.body.stats.hp)); + switch (item){ + case PlanItem::SUPER_POTION: + return std::min(SUPER_POTION_HP, missing); + case PlanItem::HYPER_POTION: + return std::min(HYPER_POTION_HP, missing); + case PlanItem::FULL_RESTORE: + return missing; + default: + return 0; + } +} + +int plan_item_count(const SimItems& items, PlanItem item){ + switch (item){ + case PlanItem::SUPER_POTION: + return items.super_potions; + case PlanItem::HYPER_POTION: + return items.hyper_potions; + case PlanItem::FULL_RESTORE: + return items.full_restores; + case PlanItem::X_ATTACK: + return items.x_attacks; + case PlanItem::X_SPECIAL: + return items.x_specials; + case PlanItem::X_SPEED: + return items.x_speeds; + case PlanItem::X_ACCURACY: + return items.x_accuracies; + } + return 0; +} + +std::optional next_setup_item( + const SimBattler& own, const SimItems& items, const SimBattler& foe, + const std::vector& to_come, const SetupGoals& goals +){ + if (stage_of(own, Stage::ATTACK) < goals.attack && items.x_attacks > 0){ + return PlanItem::X_ATTACK; + } + if (stage_of(own, Stage::SP_ATTACK) < goals.sp_attack && items.x_specials > 0){ + return PlanItem::X_SPECIAL; + } + if (goals.outspeed && items.x_speeds > 0 && stage_of(own, Stage::SPEED) < MAX_STAGE && !outspeeds_all(own, foe, to_come)){ + return PlanItem::X_SPEED; + } + if (stage_of(own, Stage::ACCURACY) < accuracy_goal(foe, goals) && items.x_accuracies > 0){ + return PlanItem::X_ACCURACY; + } + return std::nullopt; +} + +PlannedTurn planned_turn( + const SimBattler& own, const SimItems& items, const SimBattler& foe, + const SimTrainer& trainer, size_t foes_left, + const std::vector& to_come, const SetupGoals& goals +){ + if (goals.any()){ + if (const std::optional setup = setup_turn(own, items, foe, trainer, foes_left, to_come, goals)){ + return PlannedTurn{.item = setup}; + } + } + const OwnTurn turn = own_turn(own, items, foe, trainer, foes_left, to_come); + if (turn.potion || turn.awakening){ + return PlannedTurn{.turn = turn}; + } + if (const std::optional heal = bigger_heal(own, items, foe, trainer)){ + return PlannedTurn{.item = heal}; + } + return PlannedTurn{.turn = turn}; +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.h new file mode 100644 index 0000000000..871b6be4f9 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.h @@ -0,0 +1,120 @@ +/* AutoStory Battle Strategy + * + * From: https://github.com/PokemonAutomation/ + * + * The lead's plan for the hard battles from Lavender to the Champion, as + * data (BattleData/strategies.json): how many X item stages to set up + * before attacking, per trainer, from the setup goals PokemonSimulation + * measured for the Pokeguy Squirtle route (docs/battles/ + * pokeguy-conditions.md, its "C2" rules). A trainer with no entry gets the + * general policy: own_turn() of PokemonFRLG_AutoStoryBattleSim.h, with a + * Super Potion, Hyper Potion or Full Restore where a Potion would not + * cover the foe's next turn. + * + * The setup turn (C2, simplified to the solo lead): + * - No setup while asleep, against a foe with Self-Destruct or Explosion, + * against one whose best hit faints the lead from full, against one + * whose next turn is more likely than not a status move (sleep, poison, + * paralysis: the setup turn hands it the status), or on the last foe + * when the lead's attack faints it in one turn. + * - Setup only while the lead's HP is above D, the foe's highest roll plus + * poison and Leech Seed, and at least R = min(3/4 of its HP, + * D + (N - 1) * M), with N the setup turns still planned and M the + * foe's mean turn. Below that, the smallest heal whose HP after the + * foe's mean turn reaches R, else the one that leaves the most; with no + * heal above D, the lead attacks. + * - X Attack and X Special first, then X Speed until the lead outspeeds + * every foe left, then X Accuracy up to the goal plus the foe's evasion + * (no evasion chase without an accuracy goal: Minimize outpaces it). + * + * The battle loop doesn't use these items yet (Supplies doesn't count + * them), so the story's own win chance is unchanged: these items count + * only in the sim. + * + */ + +#ifndef PokemonAutomation_PokemonFRLG_AutoStoryBattleStrategy_H +#define PokemonAutomation_PokemonFRLG_AutoStoryBattleStrategy_H + +#include +#include +#include +#include +#include "Common/Cpp/TestRunners/UnitTestDatabase.h" +#include "PokemonFRLG_AutoStoryBattleSim.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +// The stat stages the lead sets up before it attacks, one X item a stage. +struct SetupGoals{ + int8_t attack = 0; + int8_t sp_attack = 0; + // Over the active foe's evasion stages; 0 for none at all. + int8_t accuracy = 0; + // Faster than every foe left. + bool outspeed = false; + + bool any() const{ + return attack > 0 || sp_attack > 0 || accuracy > 0 || outspeed; + } +}; + +// `trainer`'s goals in strategies.json; none set for one not there. +SetupGoals setup_goals(std::string_view trainer); + + +// An item the plan uses that own_turn() doesn't. +enum class PlanItem{ + SUPER_POTION, + HYPER_POTION, + FULL_RESTORE, + X_ATTACK, + X_SPECIAL, + X_SPEED, + X_ACCURACY, +}; +// Item slug for logging: "super-potion", "x-special". +std::string_view plan_item_slug(PlanItem item); + +// The HP `item` heals on `own` now; 0 for an X item. +uint16_t plan_item_heal(PlanItem item, const SimBattler& own); + +// What the bag holds of `item` in `items`. +int plan_item_count(const SimItems& items, PlanItem item); + + +// The lead's turn under the plan: own_turn()'s, or one of the plan's items. +struct PlannedTurn{ + OwnTurn turn = {}; + std::optional item = {}; +}; + +// The next X item `goals` still need with `foe` out and `to_come` left, +// that `items` holds; none once they are met. +std::optional next_setup_item( + const SimBattler& own, const SimItems& items, const SimBattler& foe, + const std::vector& to_come, const SetupGoals& goals +); + +// The lead's turn: setup and its heals (see the top), else own_turn(), +// with a bigger heal than a Potion when own_turn() attacks into a turn +// that could faint the lead and the bigger heal pays (potion_pays()). +PlannedTurn planned_turn( + const SimBattler& own, const SimItems& items, const SimBattler& foe, + const SimTrainer& trainer, size_t foes_left, + const std::vector& to_come, const SetupGoals& goals +); + + +// The hard battles' win chances with and without the plan. +void add_tests_AutoStoryBattleStrategy(UnitTestDatabase& database); + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation +#endif diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy_Tests.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy_Tests.cpp new file mode 100644 index 0000000000..b7eaa57ed0 --- /dev/null +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy_Tests.cpp @@ -0,0 +1,353 @@ +/* AutoStory Battle Strategy Tests + * + * From: https://github.com/PokemonAutomation/ + * + * The hard battles from Lavender to the Champion, each from the lead + * Pokeguy's Squirtle route brought to it (PokemonSimulation + * docs/progression/pokeguy-states.md: Blastoise, its raw stats, HP, moves + * and PP, and its bag), played in the sim three ways from the same seed: + * - before: own_turn() with the items it knows (Potions, Awakenings, + * Antidotes); + * - heals: the plan with the whole bag but no setup goals; + * - plan: the plan with strategies.json's goals. + * The sim has no abilities (Torrent, Intimidate) or held items (Black + * Glasses), so its chances are not PokemonSimulation's. + * + */ + +#include +#include +#include +#include +#include +#include "Common/Cpp/Exceptions.h" +#include "Common/Cpp/Logging/AbstractLogger.h" +#include "Common/Cpp/TestRunners/UnitTest.h" +#include "PokemonFRLG_AutoStoryBattleData.h" +#include "PokemonFRLG_AutoStoryBattleStrategy.h" +#include "PokemonFRLG_AutoStoryWinChance.h" + +namespace PokemonAutomation{ +namespace NintendoSwitch{ +namespace PokemonFRLG{ + + +namespace{ + +// Battles per estimate, and the seed every estimate starts from. +constexpr size_t BATTLES = 400; +constexpr uint32_t SEED = 1; + +// A hard battle and the lead Pokeguy brought to it. +struct HardBattle{ + std::string_view name; + std::string_view trainer; + uint8_t level = 0; + uint16_t hp = 0; + Stats stats = {}; + std::array, 4> moves = {}; + SimItems items = {}; + bool soul_badge = false; + bool volcano_badge = false; +}; + +const HardBattle HARD_BATTLES[] = { + { + .name = "hideout-giovanni", .trainer = "TRAINER_BOSS_GIOVANNI", .level = 36, .hp = 38, + .stats = {.hp = 114, .attack = 74, .defense = 76, .sp_attack = 85, .sp_defense = 88, .speed = 74}, + .moves = {{{"water-gun", 20}, {"mega-kick", 1}, {"bite", 12}, {"water-pulse", 8}}}, + .items = {.potions = 5, .awakenings = 3, .antidotes = 1, .super_potions = 5, .full_restores = 0, .x_specials = 0, .x_speeds = 0, .x_accuracies = 0}, + .soul_badge = false, .volcano_badge = false, + }, + { + .name = "tower-rival", .trainer = "TRAINER_RIVAL_POKEMON_TOWER_BULBASAUR", .level = 36, .hp = 38, + .stats = {.hp = 114, .attack = 74, .defense = 76, .sp_attack = 85, .sp_defense = 88, .speed = 74}, + .moves = {{{"water-gun", 18}, {"mega-kick", 1}, {"bite", 12}, {"water-pulse", 6}}}, + .items = {.potions = 5, .awakenings = 3, .antidotes = 1, .super_potions = 5, .full_restores = 0, .x_specials = 21, .x_speeds = 6, .x_accuracies = 5}, + .soul_badge = false, .volcano_badge = false, + }, + { + .name = "koga", .trainer = "TRAINER_LEADER_KOGA", .level = 39, .hp = 34, + .stats = {.hp = 122, .attack = 81, .defense = 82, .sp_attack = 93, .sp_defense = 95, .speed = 81}, + .moves = {{{"mega-kick", 1}, {"bite", 6}, {"water-pulse", 2}, {"surf", 11}}}, + .items = {.potions = 5, .awakenings = 3, .antidotes = 1, .super_potions = 5, .full_restores = 1, .x_specials = 19, .x_speeds = 6, .x_accuracies = 5}, + .soul_badge = false, .volcano_badge = false, + }, + { + .name = "blaine", .trainer = "TRAINER_LEADER_BLAINE", .level = 40, .hp = 24, + .stats = {.hp = 126, .attack = 83, .defense = 85, .sp_attack = 95, .sp_defense = 98, .speed = 83}, + .moves = {{{"mega-kick", 1}, {"bite", 5}, {"water-pulse", 2}, {"surf", 5}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 5, .full_restores = 1, .x_specials = 19, .x_speeds = 6, .x_accuracies = 5}, + .soul_badge = true, .volcano_badge = false, + }, + { + .name = "erika", .trainer = "TRAINER_LEADER_ERIKA", .level = 44, .hp = 35, + .stats = {.hp = 137, .attack = 91, .defense = 93, .sp_attack = 104, .sp_defense = 107, .speed = 92}, + .moves = {{{"bite", 2}, {"water-pulse", 2}, {"surf", 1}, {"blizzard", 5}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 5, .full_restores = 1, .x_specials = 19, .x_speeds = 6, .x_accuracies = 5}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "silph-rival", .trainer = "TRAINER_RIVAL_SILPH_BULBASAUR", .level = 44, .hp = 85, + .stats = {.hp = 137, .attack = 91, .defense = 93, .sp_attack = 104, .sp_defense = 107, .speed = 92}, + .moves = {{{"bite", 25}, {"water-pulse", 20}, {"surf", 15}, {"blizzard", 5}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 1, .x_specials = 19, .x_speeds = 6, .x_accuracies = 5}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "silph-giovanni", .trainer = "TRAINER_BOSS_GIOVANNI_2", .level = 46, .hp = 33, + .stats = {.hp = 143, .attack = 96, .defense = 98, .sp_attack = 110, .sp_defense = 112, .speed = 96}, + .moves = {{{"bite", 23}, {"water-pulse", 17}, {"surf", 14}, {"blizzard", 3}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 1, .x_specials = 17, .x_speeds = 6, .x_accuracies = 4}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "sabrina", .trainer = "TRAINER_LEADER_SABRINA", .level = 46, .hp = 33, + .stats = {.hp = 143, .attack = 96, .defense = 98, .sp_attack = 110, .sp_defense = 112, .speed = 96}, + .moves = {{{"bite", 23}, {"water-pulse", 16}, {"surf", 11}, {"blizzard", 3}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 1, .x_specials = 17, .x_speeds = 6, .x_accuracies = 4}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "giovanni-gym", .trainer = "TRAINER_LEADER_GIOVANNI", .level = 48, .hp = 28, + .stats = {.hp = 149, .attack = 101, .defense = 101, .sp_attack = 115, .sp_defense = 117, .speed = 100}, + .moves = {{{"bite", 23}, {"water-pulse", 11}, {"surf", 4}, {"blizzard", 3}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 1, .x_specials = 17, .x_speeds = 5, .x_accuracies = 4}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "route22-rival", .trainer = "TRAINER_RIVAL_ROUTE22_LATE_BULBASAUR", .level = 49, .hp = 153, + .stats = {.hp = 153, .attack = 103, .defense = 105, .sp_attack = 117, .sp_defense = 119, .speed = 102}, + .moves = {{{"bite", 23}, {"water-pulse", 6}, {"surf", 4}, {"blizzard", 3}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 0, .x_specials = 17, .x_speeds = 5, .x_accuracies = 4}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "lorelei", .trainer = "TRAINER_ELITE_FOUR_LORELEI", .level = 51, .hp = 159, + .stats = {.hp = 159, .attack = 107, .defense = 108, .sp_attack = 123, .sp_defense = 124, .speed = 107}, + .moves = {{{"bite", 25}, {"surf", 15}, {"blizzard", 5}, {"strength", 15}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 13, .x_specials = 15, .x_speeds = 4, .x_accuracies = 3}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "bruno", .trainer = "TRAINER_ELITE_FOUR_BRUNO", .level = 52, .hp = 130, + .stats = {.hp = 162, .attack = 109, .defense = 111, .sp_attack = 126, .sp_defense = 126, .speed = 109}, + .moves = {{{"bite", 19}, {"surf", 14}, {"blizzard", 5}, {"strength", 15}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 12, .x_specials = 11, .x_speeds = 4, .x_accuracies = 3}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "agatha", .trainer = "TRAINER_ELITE_FOUR_AGATHA", .level = 53, .hp = 45, + .stats = {.hp = 165, .attack = 112, .defense = 113, .sp_attack = 128, .sp_defense = 129, .speed = 111}, + .moves = {{{"bite", 19}, {"surf", 9}, {"blizzard", 5}, {"strength", 15}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 12, .x_specials = 9, .x_speeds = 3, .x_accuracies = 3}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "lance", .trainer = "TRAINER_ELITE_FOUR_LANCE", .level = 54, .hp = 168, + .stats = {.hp = 168, .attack = 114, .defense = 116, .sp_attack = 130, .sp_defense = 131, .speed = 113}, + .moves = {{{"bite", 19}, {"surf", 4}, {"blizzard", 5}, {"strength", 15}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 11, .x_specials = 8, .x_speeds = 2, .x_accuracies = 3}, + .soul_badge = true, .volcano_badge = true, + }, + { + .name = "champion", .trainer = "TRAINER_CHAMPION_FIRST_BULBASAUR", .level = 55, .hp = 171, + .stats = {.hp = 171, .attack = 117, .defense = 118, .sp_attack = 134, .sp_defense = 134, .speed = 115}, + .moves = {{{"bite", 25}, {"surf", 13}, {"blizzard", 5}, {"strength", 15}}}, + .items = {.potions = 4, .awakenings = 3, .antidotes = 1, .super_potions = 4, .full_restores = 9, .x_specials = 7, .x_speeds = 2, .x_accuracies = 2}, + .soul_badge = true, .volcano_badge = true, + }, +}; + +SimBattler lead(const HardBattle& battle){ + SimBattler own; + own.species = "blastoise"; + own.body = combatant("blastoise", battle.level, OWN_GENES).value(); + own.body.stats = battle.stats; + own.body.boulder_badge = true; + own.body.soul_badge = battle.soul_badge; + own.body.volcano_badge = battle.volcano_badge; + own.hp = battle.hp; + for (const auto& [move, pp] : battle.moves){ + own.moves.push_back(SimMove{.move = std::string(move), .pp = pp}); + } + return own; +} + +// The items own_turn() uses, the rest left out. +SimItems known_items(const SimItems& items){ + return SimItems{.potions = items.potions, .awakenings = items.awakenings, .antidotes = items.antidotes}; +} + +struct Rates{ + double before = 0; + double heals = 0; + double plan = 0; +}; + +Rates rates(const HardBattle& battle, uint16_t hp){ + SimBattler own = lead(battle); + own.hp = hp; + const SimTrainer trainer = sim_trainer(battle.trainer).value(); + return Rates{ + .before = estimate_win_chance(own, known_items(battle.items), trainer, BATTLES, SEED).chance, + .heals = estimate_win_chance(own, battle.items, trainer, BATTLES, SEED).chance, + .plan = estimate_win_chance(own, battle.items, trainer, BATTLES, SEED, setup_goals(battle.trainer)).chance, + }; +} + +// Each hard battle at its recorded HP and at full HP: the plan wins at +// least as often as before, give or take the sampling (5 points at 400 +// battles), and more often over all of them. +UnitTestResult test_plan_win_rates(Logger& logger){ + double before_sum = 0; + double plan_sum = 0; + std::string worse; + logger.log("battle, HP: before / heals / plan"); + for (const HardBattle& battle : HARD_BATTLES){ + for (uint16_t hp : {battle.hp, battle.stats.hp}){ + const Rates r = rates(battle, hp); + logger.log(std::format( + "{}, {}/{}: {:.1f}% / {:.1f}% / {:.1f}%", + battle.name, hp, battle.stats.hp, 100 * r.before, 100 * r.heals, 100 * r.plan + )); + before_sum += r.before; + plan_sum += r.plan; + if (r.plan < r.before - 0.05){ + worse += std::format(" {} at {} HP ({:.1f}% < {:.1f}%);", battle.name, hp, 100 * r.plan, 100 * r.before); + } + } + } + if (!worse.empty()){ + return "The plan lost more often than before:" + worse; + } + if (plan_sum <= before_sum){ + return std::format("Expected the plan to win more over all, got {:.2f} <= {:.2f}.", plan_sum, before_sum); + } + return true; +} + +const HardBattle& hard_battle(std::string_view name){ + for (const HardBattle& battle : HARD_BATTLES){ + if (battle.name == name){ + return battle; + } + } + throw InternalProgramError(nullptr, PA_CURRENT_FUNCTION, "No hard battle " + std::string(name) + "."); +} + +// strategies.json's goals: Bruno two X Specials and outspeeding, Lance +// three and two X Accuracies; none for a trainer it doesn't name. +UnitTestResult test_goals_from_data(Logger&){ + const SetupGoals bruno = setup_goals("TRAINER_ELITE_FOUR_BRUNO"); + const SetupGoals lance = setup_goals("TRAINER_ELITE_FOUR_LANCE"); + if (bruno.sp_attack != 2 || !bruno.outspeed || bruno.accuracy != 0 || bruno.attack != 0){ + return "Bruno: expected +2 Sp. Atk and outspeeding."; + } + if (lance.sp_attack != 3 || lance.accuracy != 2){ + return "Lance: expected +3 Sp. Atk and +2 accuracy."; + } + if (setup_goals("TRAINER_LEADER_BROCK").any() || setup_goals("TRAINER_BOSS_GIOVANNI").any()){ + return "Expected no goals for Brock or the hideout's Giovanni."; + } + return true; +} + +// Lorelei's Dewgong against the full-HP Blastoise: X Special first (goal +// +4), and after four, X Speed while Dewgong's speed isn't beaten. With +// no X items left, own_turn()'s attack. At 20/159 HP with a Full Restore +// left, the Full Restore before more setup. +UnitTestResult test_setup_order(Logger&){ + const HardBattle& battle = hard_battle("lorelei"); + const SimTrainer trainer = sim_trainer(battle.trainer).value(); + const SetupGoals goals = setup_goals(battle.trainer); + const SimBattler& dewgong = trainer.party[0]; + const std::vector to_come(trainer.party.begin() + 1, trainer.party.end()); + SimBattler own = lead(battle); + PlannedTurn turn = planned_turn(own, battle.items, dewgong, trainer, trainer.party.size(), to_come, goals); + if (turn.item != PlanItem::X_SPECIAL){ + return "At full HP: expected an X Special."; + } + own.body.stages[static_cast(Stage::SP_ATTACK)] = 4; + turn = planned_turn(own, battle.items, dewgong, trainer, trainer.party.size(), to_come, goals); + const std::optional speed = next_setup_item(own, battle.items, dewgong, to_come, goals); + if (turn.item != speed){ + return "At +4 Sp. Atk: expected the next setup item, " + std::string(speed ? plan_item_slug(*speed) : "none") + "."; + } + SimItems no_x = battle.items; + no_x.x_specials = no_x.x_speeds = no_x.x_accuracies = 0; + turn = planned_turn(own, no_x, dewgong, trainer, trainer.party.size(), to_come, goals); + if (turn.item || !turn.turn.slot){ + return "With no X items: expected an attack."; + } + own.body.stages[static_cast(Stage::SP_ATTACK)] = 0; + own.hp = 20; + turn = planned_turn(own, battle.items, dewgong, trainer, trainer.party.size(), to_come, goals); + if (!turn.item || plan_item_heal(*turn.item, own) == 0){ + return std::format("At 20 HP: expected a heal, got {}.", turn.item ? plan_item_slug(*turn.item) : "no plan item"); + } + return true; +} + + +struct AutoStoryBattleStrategyInput{ + std::string name; + UnitTestResult (*body)(Logger&); +}; + +struct AutoStoryBattleStrategyExpectation{ + const char* description; +}; + +struct AutoStoryBattleStrategyTestCase{ + AutoStoryBattleStrategyInput input; + AutoStoryBattleStrategyExpectation expectation; +}; + +class Test_AutoStoryBattleStrategy : public UnitTest{ +public: + explicit Test_AutoStoryBattleStrategy(AutoStoryBattleStrategyTestCase scenario) + : UnitTest("PokemonFRLG::AutoStoryBattleStrategy - " + scenario.input.name) + , m_scenario(std::move(scenario)) + {} + virtual UnitTestResult run(Logger& logger, CancellableScope&) const override{ + try{ + return m_scenario.input.body(logger); + }catch (Exception& e){ + return std::string(e.name()) + ": " + e.message(); + } + } + +private: + AutoStoryBattleStrategyTestCase m_scenario; +}; + +const AutoStoryBattleStrategyTestCase AUTO_STORY_BATTLE_STRATEGY_CASES[] = { + { + .input = {.name = "setup goals from strategies.json", .body = test_goals_from_data}, + .expectation = {.description = "reads each trainer's goals, none for one not listed"}, + }, + { + .input = {.name = "setup order and the heal before it", .body = test_setup_order}, + .expectation = {.description = "X Special, then X Speed, then the attack; a heal first at low HP"}, + }, + { + .input = {.name = "hard battles: the plan against before", .body = test_plan_win_rates}, + .expectation = {.description = "the plan wins at least as often as own_turn() in every hard battle"}, + }, +}; + +} // namespace + + +void add_tests_AutoStoryBattleStrategy(UnitTestDatabase& database){ + for (const auto& scenario : AUTO_STORY_BATTLE_STRATEGY_CASES){ + database.add(scenario); + } +} + + + +} // namespace PokemonFRLG +} // namespace NintendoSwitch +} // namespace PokemonAutomation diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.cpp b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.cpp index 21cd717f1d..38ed1affc4 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.cpp +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.cpp @@ -87,10 +87,11 @@ void change_stage(SimBattler& user, SimBattler& target, std::string_view effect) // One battle, played once. class SimBattle{ public: - SimBattle(const SimBattler& own, const SimItems& items, const SimTrainer& trainer, std::mt19937& rng) + SimBattle(const SimBattler& own, const SimItems& items, const SimTrainer& trainer, const SetupGoals& goals, std::mt19937& rng) : m_own(own) , m_items(items) , m_trainer(trainer) + , m_goals(goals) , m_rng(rng) {} @@ -115,7 +116,7 @@ class SimBattle{ tally.asleep += m_own.status == SimStatus::SLEEP ? 1 : 0; tally.paralyzed += m_own.status == SimStatus::PARALYSIS ? 1 : 0; tally.accuracy_down += stage(m_own, Stage::ACCURACY) < 0 ? 1 : 0; - tally.no_potions += m_items.potions == 0 ? 1 : 0; + tally.no_potions += m_items.potions + m_items.super_potions + m_items.hyper_potions + m_items.full_restores == 0 ? 1 : 0; tally.turn_cap += m_own.hp > 0 ? 1 : 0; } @@ -175,8 +176,9 @@ class SimBattle{ // A charged two-turn move strikes: no choice this turn. const bool own_locked = !m_own.charging.empty(); const bool foe_locked = !foe().charging.empty(); - const OwnTurn own = own_locked ? OwnTurn{} : own_turn(m_own, m_items, foe(), m_trainer, left, to_come()); - const bool antidote = !own_locked && !own.potion && !own.awakening && antidote_now(); + const PlannedTurn planned = own_locked ? PlannedTurn{} : planned_turn(m_own, m_items, foe(), m_trainer, left, to_come(), m_goals); + const OwnTurn& own = planned.turn; + const bool antidote = !own_locked && !own.potion && !own.awakening && !planned.item && antidote_now(); const std::optional item = foe_locked ? std::nullopt : ai_item_choice(foe(), m_trainer.items, left); const std::optional foe_slot = item || foe_locked ? std::nullopt : foe_move(); if (own.potion){ @@ -193,11 +195,14 @@ class SimBattle{ m_items.antidotes--; m_antidote_used = true; } + if (planned.item){ + use_plan_item(*planned.item); + } if (item){ gain_hp(foe(), ai_item_hp(m_trainer.items[*item])); m_trainer.items[*item].clear(); } - const bool item_used = own.potion || own.awakening || antidote; + const bool item_used = own.potion || own.awakening || antidote || planned.item; const std::string own_move = own_locked ? m_own.charging : item_used ? "" : own.slot ? m_own.moves[*own.slot].move : "struggle"; const std::string foe_used = foe_locked ? foe().charging @@ -206,6 +211,43 @@ class SimBattle{ end_of_turn(); } + // A heal raises HP (a Full Restore also cures the status), an X item + // its stage by one. + void use_plan_item(PlanItem item){ + gain_hp(m_own, plan_item_heal(item, m_own)); + switch (item){ + case PlanItem::SUPER_POTION: + m_items.super_potions--; + return; + case PlanItem::HYPER_POTION: + m_items.hyper_potions--; + return; + case PlanItem::FULL_RESTORE: + m_own.status = SimStatus::NONE; + m_own.sleep = 0; + m_items.full_restores--; + return; + case PlanItem::X_ATTACK: + raise(Stage::ATTACK, m_items.x_attacks); + return; + case PlanItem::X_SPECIAL: + raise(Stage::SP_ATTACK, m_items.x_specials); + return; + case PlanItem::X_SPEED: + raise(Stage::SPEED, m_items.x_speeds); + return; + case PlanItem::X_ACCURACY: + raise(Stage::ACCURACY, m_items.x_accuracies); + return; + } + } + + void raise(Stage which, int& count){ + int8_t& now = stage(m_own, which); + now = static_cast(std::min(now + 1, MAX_STAGE)); + count--; + } + // As the battle loop: once a battle, PSN_TURNS_BEFORE_ANTIDOTE turns // poisoned in a row. bool antidote_now() const{ @@ -403,6 +445,7 @@ class SimBattle{ SimBattler m_own; SimItems m_items; SimTrainer m_trainer; + const SetupGoals m_goals; std::mt19937& m_rng; size_t m_out = 0; bool m_flinched = false; @@ -413,11 +456,14 @@ class SimBattle{ } // namespace -WinEstimate estimate_win_chance(const SimBattler& own, const SimItems& items, const SimTrainer& trainer, size_t battles, uint32_t seed){ +WinEstimate estimate_win_chance( + const SimBattler& own, const SimItems& items, const SimTrainer& trainer, + size_t battles, uint32_t seed, const SetupGoals& goals +){ std::mt19937 rng(seed); WinEstimate estimate{.battles = battles, .wins = 0, .chance = 0}; for (size_t i = 0; i < battles; i++){ - SimBattle battle(own, items, trainer, rng); + SimBattle battle(own, items, trainer, goals, rng); if (battle.won()){ estimate.wins++; }else{ diff --git a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.h b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.h index 2807d5b092..5961dcf7d5 100644 --- a/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.h +++ b/SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryWinChance.h @@ -36,6 +36,7 @@ #include #include #include "PokemonFRLG_AutoStoryBattleSim.h" +#include "PokemonFRLG_AutoStoryBattleStrategy.h" namespace PokemonAutomation{ namespace NintendoSwitch{ @@ -74,8 +75,13 @@ struct WinEstimate{ std::string loss_text(const WinEstimate& estimate); // `own` with `items` against `trainer`, `battles` times, from a fixed -// `seed` so the same inputs give the same estimate. -WinEstimate estimate_win_chance(const SimBattler& own, const SimItems& items, const SimTrainer& trainer, size_t battles = 1000, uint32_t seed = 1); +// `seed` so the same inputs give the same estimate. The lead plays +// planned_turn() with `goals`: own_turn() when there are none and the +// bag holds only the items own_turn() uses. +WinEstimate estimate_win_chance( + const SimBattler& own, const SimItems& items, const SimTrainer& trainer, + size_t battles = 1000, uint32_t seed = 1, const SetupGoals& goals = SetupGoals() +); // The story's lead (`hp_share` of its HP) with `items` against // `trainer`. None if the data lacks the lead's species or the trainer. diff --git a/SerialPrograms/cmake/SourceFiles.cmake b/SerialPrograms/cmake/SourceFiles.cmake index 0f9f768a04..da283d2a28 100644 --- a/SerialPrograms/cmake/SourceFiles.cmake +++ b/SerialPrograms/cmake/SourceFiles.cmake @@ -1898,6 +1898,9 @@ file(GLOB LIBRARY_SOURCES Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleSim_Tests.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.cpp + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.h + Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy_Tests.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.cpp Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture.h Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryCapture_Tests.cpp