From db1d9cbd15fc083ba8d2a0a826a0462119ae8c60 Mon Sep 17 00:00:00 2001 From: dayfine Date: Mon, 9 Nov 2026 10:15:36 -0800 Subject: [PATCH] FRLG battle sim: a setup and heal plan for the hard battles The sim's lead only knew Potions, Awakenings and Antidotes, and never set up, so it lost most E4 and Champion battles even with a full bag. Adds a per-trainer plan as data (BattleData/strategies.json: X Special, X Speed and X Accuracy goals from PokemonSimulation's measured Pokeguy setup goals) and planned_turn(): C2-style setup turns with an HP reserve and a heal before them, no setup into Self-Destruct, a one-hit KO or a likely status move, and Super Potion / Hyper Potion / Full Restore where a Potion would not pay. The win-chance sim plays it; the battle loop does not use the new items yet, so the story's chances are unchanged. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013VUubhHeYSMGSSDXZkKZss --- .../AutoStory/BattleData/strategies.json | 14 + .../PokemonFRLG_AutoStoryBattleData_Tests.cpp | 2 + .../PokemonFRLG_AutoStoryBattleSim.cpp | 4 +- .../PokemonFRLG_AutoStoryBattleSim.h | 12 +- .../PokemonFRLG_AutoStoryBattleStrategy.cpp | 313 ++++++++++++++++ .../PokemonFRLG_AutoStoryBattleStrategy.h | 120 ++++++ ...emonFRLG_AutoStoryBattleStrategy_Tests.cpp | 353 ++++++++++++++++++ .../PokemonFRLG_AutoStoryWinChance.cpp | 60 ++- .../PokemonFRLG_AutoStoryWinChance.h | 10 +- SerialPrograms/cmake/SourceFiles.cmake | 3 + 10 files changed, 878 insertions(+), 13 deletions(-) create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/BattleData/strategies.json create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.cpp create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy.h create mode 100644 SerialPrograms/Source/VideoGames/PokemonFRLG/Programs/AutoStory/PokemonFRLG_AutoStoryBattleStrategy_Tests.cpp 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