From fb4e7f947d32e8c3b164c38cdb3d4e4b688d0911 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Tue, 14 Jan 2025 22:45:18 -0500 Subject: [PATCH 01/17] Initial setup --- include/battle.h | 4 +- include/battle_ai_util.h | 1 + include/constants/battle_ai.h | 3 +- include/random.h | 1 + src/battle_ai_main.c | 147 +++++++++++++++++++++--- src/battle_ai_util.c | 29 +++-- src/battle_main.c | 27 ++++- test/battle/ai/ai_flag_predict_switch.c | 88 ++++++++++++++ 8 files changed, 270 insertions(+), 30 deletions(-) create mode 100644 test/battle/ai/ai_flag_predict_switch.c diff --git a/include/battle.h b/include/battle.h index d6b5abb7f9c5..a523bb5e7243 100644 --- a/include/battle.h +++ b/include/battle.h @@ -357,7 +357,9 @@ struct AiLogicData u8 weatherHasEffect:1; // The same as WEATHER_HAS_EFFECT. Stored here, so it's called only once. u8 ejectButtonSwitch:1; // Tracks whether current switch out was from Eject Button u8 ejectPackSwitch:1; // Tracks whether current switch out was from Eject Pack - u8 padding:5; + u8 predictingSwitch:1; // Determines whether AI will use predictions this turn or not + u8 allowFocusPunch:1; // Allow use of Focus Punch if AI predicting switch + u8 padding:3; u8 shouldSwitch; // Stores result of ShouldSwitch, which decides whether a mon should be switched out u8 aiCalcInProgress:1; }; diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 4a47b63503ef..f561182fca67 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -215,5 +215,6 @@ bool32 AI_ShouldSetUpHazards(u32 battlerAtk, u32 battlerDef, struct AiLogicData void IncreaseTidyUpScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); bool32 AI_ShouldSpicyExtract(u32 battlerAtk, u32 battlerAtkPartner, u32 move, struct AiLogicData *aiData); void IncreaseSubstituteMoveScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); +bool32 IsBattlerPredictedToSwitch(u32 battler); #endif //GUARD_BATTLE_AI_UTIL_H diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index acfe020f49ee..5067e1e16667 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -51,8 +51,9 @@ #define AI_FLAG_DOUBLE_ACE_POKEMON (1 << 20) // AI has *two* Ace Pokémon. The last two Pokémons in the party won't be used unless they're the last ones remaining. Goes well in battles where the trainer ID equals to twins, couples, etc. #define AI_FLAG_WEIGH_ABILITY_PREDICTION (1 << 21) // AI will predict player's ability based on aiRating #define AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE (1 << 22) // AI adds score to highest damage move regardless of accuracy or secondary effect +#define AI_FLAG_PREDICT_SWITCH (1 << 23) // AI will predict the player's switches and switchins based on how it would handle the situation. Recommend using AI_FLAG_OMNISCIENT. Only works for singles. -#define AI_FLAG_COUNT 23 +#define AI_FLAG_COUNT 24 // The following options are enough to have a basic/smart trainer. Any other addtion could make the trainer worse/better depending on the flag #define AI_FLAG_BASIC_TRAINER (AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY) diff --git a/include/random.h b/include/random.h index 886094733a9f..1a735097d32d 100644 --- a/include/random.h +++ b/include/random.h @@ -175,6 +175,7 @@ enum RandomTag RNG_SHELL_SIDE_ARM, RNG_RANDOM_TARGET, RNG_AI_PREDICT_ABILITY, + RNG_AI_PREDICT_SWITCH, RNG_HEALER, RNG_DEXNAV_ENCOUNTER_LEVEL, }; diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 67e06badd656..978415957d04 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -57,7 +57,7 @@ static s32 AI_FirstBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 AI_PowerfulStatus(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 AI_DynamicFunc(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); - +static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score); static s32 (*const sBattleAiFuncTable[])(u32, u32, u32, s32) = { @@ -84,7 +84,7 @@ static s32 (*const sBattleAiFuncTable[])(u32, u32, u32, s32) = [20] = NULL, // Unused [21] = NULL, // Unused [22] = NULL, // Unused - [23] = NULL, // Unused + [23] = AI_PredictSwitch, // AI_FLAG_PREDICT_SWITCH [24] = NULL, // Unused [25] = NULL, // Unused [26] = NULL, // Unused @@ -412,19 +412,14 @@ static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u3 u16 *moves; u32 battlerDef, moveIndex, move; u32 rollType = GetDmgRollType(battlerAtk); - SaveBattlerData(battlerAtk); moves = GetMovesArray(battlerAtk); - SetBattlerData(battlerAtk); - // Simulate dmg for both ai controlled mons and for player controlled mons. for (battlerDef = 0; battlerDef < battlersCount; battlerDef++) { if (battlerAtk == battlerDef || !IsBattlerAlive(battlerDef)) continue; - SaveBattlerData(battlerDef); - SetBattlerData(battlerDef); for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) { struct SimulatedDamage dmg = {0}; @@ -442,9 +437,7 @@ static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u3 aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex] = dmg; aiData->effectiveness[battlerAtk][battlerDef][moveIndex] = effectiveness; } - RestoreBattlerData(battlerDef); } - RestoreBattlerData(battlerAtk); } void SetAiLogicDataForTurn(struct AiLogicData *aiData) @@ -3852,13 +3845,6 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) //if (CountUsablePartyMons(battlerDef) != 0) //ADJUST_SCORE(8); break; - case EFFECT_PURSUIT: - // TODO - // if (IsPredictedToSwitch(battlerDef, battlerAtk)) - // ADJUST_SCORE(GOOD_EFFECT); - // else if (IsPredictedToUsePursuitableMove(battlerDef, battlerAtk) && !MoveWouldHitFirst(move, battlerAtk, battlerDef)) //Pursuit against fast U-Turn - // ADJUST_SCORE(GOOD_EFFECT); - // break; case EFFECT_DEFOG: if ((gSideStatuses[GetBattlerSide(battlerAtk)] & SIDE_STATUS_HAZARDS_ANY && CountUsablePartyMons(battlerAtk) != 0) || (gSideStatuses[GetBattlerSide(battlerDef)] & (SIDE_STATUS_SCREEN_ANY | SIDE_STATUS_SAFEGUARD | SIDE_STATUS_MIST))) @@ -5268,6 +5254,135 @@ static s32 AI_PowerfulStatus(u32 battlerAtk, u32 battlerDef, u32 move, s32 score return score; } +static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) +{ + u32 i; + u32 unmodifiedScore = score; + u32 ability = gBattleMons[battlerAtk].ability; + u32 opposingHazardFlags = gSideStatuses[GetBattlerSide(battlerDef)] & (SIDE_STATUS_SPIKES | SIDE_STATUS_STEALTH_ROCK | SIDE_STATUS_TOXIC_SPIKES); + u32 aiHazardFlags = gSideStatuses[GetBattlerSide(battlerAtk)] & (SIDE_STATUS_HAZARDS_ANY); + u32 moveEffect = gMovesInfo[move].effect; + + // Switch benefit + switch (moveEffect) + { + case EFFECT_PURSUIT: + ADJUST_SCORE(GOOD_EFFECT); + // else if (IsPredictedToUsePursuitableMove(battlerDef, battlerAtk) && !MoveWouldHitFirst(move, battlerAtk, battlerDef)) //Pursuit against fast U-Turn + // ADJUST_SCORE(GOOD_EFFECT); + break; + + // Free setup (U-Turn etc. handled in Check Viability by ShouldPivot) + case EFFECT_BOLT_BEAK: + case EFFECT_LIGHT_SCREEN: + case EFFECT_REFLECT: + case EFFECT_MAGNET_RISE: + case EFFECT_TRICK_ROOM: + case EFFECT_STEALTH_ROCK: + case EFFECT_SPIKES: + case EFFECT_TOXIC_SPIKES: + ADJUST_SCORE(BEST_EFFECT); + break; + case EFFECT_FUTURE_SIGHT: + case EFFECT_TELEKINESIS: + case EFFECT_GRAVITY: + case EFFECT_RAIN_DANCE: + case EFFECT_SANDSTORM: + case EFFECT_SNOWSCAPE: + case EFFECT_HAIL: + case EFFECT_SUNNY_DAY: + case EFFECT_AQUA_RING: + case EFFECT_ELECTRIC_TERRAIN: + case EFFECT_PSYCHIC_TERRAIN: + case EFFECT_GRASSY_TERRAIN: + case EFFECT_MISTY_TERRAIN: + ADJUST_SCORE(GOOD_EFFECT); + break; + case EFFECT_HIT_SWITCH_TARGET: + if (opposingHazardFlags != 0) + ADJUST_SCORE(BEST_EFFECT); + else + ADJUST_SCORE(GOOD_EFFECT); + break; + case EFFECT_ROAR: + if (opposingHazardFlags != 0) + ADJUST_SCORE(GOOD_EFFECT); + break; + case EFFECT_DEFOG: + if (aiHazardFlags != 0) + ADJUST_SCORE(GOOD_EFFECT); + break; + case EFFECT_WISH: + case EFFECT_HEAL_BELL: + if (ShouldUseWishAromatherapy(battlerAtk, battlerDef, move)) + ADJUST_SCORE(DECENT_EFFECT); + break; + case EFFECT_RESTORE_HP: + if (AI_DATA->hpPercents[battlerAtk] < 60) + ADJUST_SCORE(GOOD_EFFECT); + break; + + // Fails if opponent switches + case EFFECT_PROTECT: + case EFFECT_COUNTER: + case EFFECT_MIRROR_COAT: + case EFFECT_SHELL_TRAP: + case EFFECT_METAL_BURST: + case EFFECT_SUCKER_PUNCH: + case EFFECT_UPPER_HAND: + case EFFECT_ENCORE: + case EFFECT_FOLLOW_ME: + case EFFECT_ME_FIRST: + case EFFECT_DISABLE: + case EFFECT_ELECTRIFY: + case EFFECT_ENDURE: + case EFFECT_HAZE: + case EFFECT_TOPSY_TURVY: + case EFFECT_ION_DELUGE: + case EFFECT_MAGIC_COAT: + case EFFECT_SNATCH: + ADJUST_SCORE(-BEST_EFFECT); + break; + + // Get stuck in bad matchup + case EFFECT_IMPRISON: + case EFFECT_EMBARGO: + case EFFECT_TAUNT: + case EFFECT_INGRAIN: + case EFFECT_NO_RETREAT: + case EFFECT_MEAN_LOOK: + ADJUST_SCORE(-GOOD_EFFECT); + break; + } + + // Additional effects + for (i = 0; i < gMovesInfo[move].numAdditionalEffects; i++) + { + switch (gMovesInfo[move].additionalEffects[i].moveEffect) + { + case MOVE_EFFECT_WRAP: + ADJUST_SCORE(-GOOD_EFFECT); + break; + case MOVE_EFFECT_RAPID_SPIN: + if (aiHazardFlags != 0) + ADJUST_SCORE(BEST_EFFECT); + break; + case MOVE_EFFECT_FEINT: + ADJUST_SCORE(-BEST_EFFECT); + break; + } + } + + // Take advantage of ability damage bonus + if ((ability == ABILITY_STAKEOUT || ability == ABILITY_ANALYTIC) && IsBattleMoveStatus(move)) + ADJUST_SCORE(-WEAK_EFFECT); + + // This must be last or the player can gauge whether the AI is predicting based on how long it thinks + if (!IsBattlerPredictedToSwitch(battlerDef)) + return unmodifiedScore; + return score; +} + static void AI_Flee(void) { AI_THINKING_STRUCT->aiAction |= (AI_ACTION_DONE | AI_ACTION_FLEE | AI_ACTION_DO_NOT_ATTACK); diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index c9a3ae07b2bf..3a437b99b28f 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -102,6 +102,18 @@ bool32 IsAiBattlerPredictingAbility(u32 battlerId) return BattlerHasAi(battlerId); } +bool32 IsBattlerPredictedToSwitch(u32 battler) +{ + // Check for prediction flag on AI, whether they're using those predictions this turn, and whether the AI thinks the player should switch + if (AI_THINKING_STRUCT->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_PREDICT_SWITCH + || AI_THINKING_STRUCT->aiFlags[B_POSITION_OPPONENT_RIGHT] & AI_FLAG_PREDICT_SWITCH) + { + if (AI_DATA->predictingSwitch && AI_DATA->shouldSwitch & (1u << battler)) + return TRUE; + } + return FALSE; +} + void ClearBattlerMoveHistory(u32 battlerId) { memset(BATTLE_HISTORY->usedMoves[battlerId], 0, sizeof(BATTLE_HISTORY->usedMoves[battlerId])); @@ -478,10 +490,11 @@ bool32 IsDamageMoveUnusable(u32 battlerAtk, u32 battlerDef, u32 move, u32 moveTy return TRUE; break; case EFFECT_FOCUS_PUNCH: + if (IsBattlerPredictedToSwitch(battlerDef)) + return FALSE; if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) - // TODO: || IsPredictedToSwitch(battlerDef, battlerAtk) return TRUE; // If AI could Sub and doesn't have a Sub, don't Punch yet if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) @@ -2779,9 +2792,8 @@ enum AIPivot ShouldPivot(u32 battlerAtk, u32 battlerDef, u32 defAbility, u32 mov if (CountUsablePartyMons(battlerAtk) == 0) return CAN_TRY_PIVOT; // can't switch, but attack might still be useful - //TODO - predict opponent switching - /*if (IsPredictedToSwitch(battlerDef, battlerAtk) && !hasStatBoost) - return SHOULD_PIVOT; // Try pivoting so you can switch to a better matchup to counter your new opponent*/ + if (IsBattlerPredictedToSwitch(battlerDef)) + return SHOULD_PIVOT; // Try pivoting so you can switch to a better matchup to counter your new opponent if (AI_IsFaster(battlerAtk, battlerDef, move)) // Attacker goes first { @@ -3793,6 +3805,10 @@ static u32 IncreaseStatUpScoreInternal(u32 battlerAtk, u32 battlerDef, u32 statI if (AI_DATA->abilities[battlerDef] == ABILITY_OPPORTUNIST) return NO_INCREASE; + // If predicting switch, stat increases are great momentum + if (IsBattlerPredictedToSwitch(battlerDef)) + tempScore += WEAK_EFFECT; + switch (statId) { case STAT_CHANGE_ATK: @@ -4193,9 +4209,8 @@ void IncreaseSubstituteMoveScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 * else if (gBattleMons[battlerDef].status1 & (STATUS1_BURN | STATUS1_PSN_ANY | STATUS1_FROSTBITE)) ADJUST_SCORE_PTR(DECENT_EFFECT); - // TODO: - // if (IsPredictedToSwitch(battlerDef, battlerAtk) - // ADJUST_SCORE_PTR(DECENT_EFFECT); + if (IsBattlerPredictedToSwitch(battlerDef)) + ADJUST_SCORE(DECENT_EFFECT); if (HasMoveEffect(battlerDef, EFFECT_SLEEP) || HasMoveEffect(battlerDef, EFFECT_TOXIC) diff --git a/src/battle_main.c b/src/battle_main.c index 5dba22a160c5..9d3851a4ec22 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4201,6 +4201,27 @@ enum STATE_SELECTION_SCRIPT_MAY_RUN }; +void SetupAISwitchingData(u32 battler, bool32 isAiRisky) +{ + s32 opposingBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerPosition(battler))); + + // AI's data + AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, isAiRisky); + if (ShouldSwitch(battler)) + AI_DATA->shouldSwitch |= (1u << battler); + + // AI's predicting data + if ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_SWITCH)) + { + AI_DATA->mostSuitableMonId[opposingBattler] = GetMostSuitableMonToSwitchInto(opposingBattler, isAiRisky); + if (ShouldSwitch(opposingBattler)) + AI_DATA->shouldSwitch |= (1u << opposingBattler); + + // Determine whether AI will use predictions this turn + AI_DATA->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, 50); + } +} + static void HandleTurnActionSelectionState(void) { s32 i, battler; @@ -4225,11 +4246,7 @@ static void HandleTurnActionSelectionState(void) // Setup battler data sBattler_AI = battler; BattleAI_SetupAIData(0xF, sBattler_AI); - - // Setup switching data - AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, isAiRisky); - if (ShouldSwitch(battler)) - AI_DATA->shouldSwitch |= (1u << battler); + SetupAISwitchingData(battler, isAiRisky); // Do scoring gBattleStruct->aiMoveOrAction[battler] = BattleAI_ChooseMoveOrAction(); diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c new file mode 100644 index 000000000000..e42a596e2bd2 --- /dev/null +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -0,0 +1,88 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will predict switches with Pursuit") +{ + PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PREDICT_SWITCH); + PLAYER(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + PLAYER(SPECIES_CONKELDURR) { Moves(MOVE_HAMMER_ARM); } + OPPONENT(SPECIES_TYRANITAR) { Moves(MOVE_PURSUIT, MOVE_CRUNCH); } + } WHEN { + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_PURSUIT); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Pursuit scenario") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_TYRANITAR) { Moves(MOVE_PURSUIT, MOVE_CRUNCH); } + OPPONENT(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + OPPONENT(SPECIES_CONKELDURR) { Moves(MOVE_HAMMER_ARM); } + } WHEN { + TURN { MOVE(player, MOVE_PURSUIT); EXPECT_SWITCH(opponent, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will predict switches with Wonder Guard") +{ + PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PREDICT_SWITCH); + PLAYER(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + PLAYER(SPECIES_SWELLOW) { Moves(MOVE_PECK); } + OPPONENT(SPECIES_SHEDINJA) { Moves(MOVE_PURSUIT, MOVE_CRUNCH); } + } WHEN { + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_PURSUIT); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Wonder Guard scenario") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_SHEDINJA) { Moves(MOVE_PURSUIT, MOVE_CRUNCH); } + OPPONENT(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + OPPONENT(SPECIES_SWELLOW) { Moves(MOVE_PECK); } + } WHEN { + TURN { MOVE(player, MOVE_PURSUIT); EXPECT_SWITCH(opponent, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI can use Focus Punch on predicted switches") +{ + PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PREDICT_SWITCH); + PLAYER(SPECIES_CACNEA) { Moves(MOVE_ABSORB); } + PLAYER(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_AERIAL_ACE); } + OPPONENT(SPECIES_BRELOOM) { Moves(MOVE_FOCUS_PUNCH, MOVE_TACKLE); } + OPPONENT(SPECIES_BRELOOM); + } WHEN { + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_FOCUS_PUNCH); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Focus Punch scenario") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_BRELOOM) { Moves(MOVE_FOCUS_PUNCH, MOVE_TACKLE); } + PLAYER(SPECIES_BRELOOM); + OPPONENT(SPECIES_CACNEA) { Moves(MOVE_ABSORB); } + OPPONENT(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_AERIAL_ACE); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); EXPECT_SWITCH(opponent, 1); } + } +} + +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use hit escape moves on predicted switches"); +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in hit escape scenario") +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use Substitute on predicted switches"); +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Substitute scenario"); + +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will score against predicted incoming mon when switch predicted") +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in predicted-incoming-mon scenario"); From fbbd471c7bd734086ec03e23a4f4dcbc82f017fa Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Tue, 14 Jan 2025 23:54:17 -0500 Subject: [PATCH 02/17] Focus Punch doesn't work --- include/battle.h | 2 +- src/battle_ai_switch_items.c | 39 +++++++++++++++---------- src/battle_main.c | 4 ++- test/battle/ai/ai_flag_predict_switch.c | 37 +++++++++++++++++++++-- 4 files changed, 61 insertions(+), 21 deletions(-) diff --git a/include/battle.h b/include/battle.h index a523bb5e7243..0313b562cbf0 100644 --- a/include/battle.h +++ b/include/battle.h @@ -358,7 +358,7 @@ struct AiLogicData u8 ejectButtonSwitch:1; // Tracks whether current switch out was from Eject Button u8 ejectPackSwitch:1; // Tracks whether current switch out was from Eject Pack u8 predictingSwitch:1; // Determines whether AI will use predictions this turn or not - u8 allowFocusPunch:1; // Allow use of Focus Punch if AI predicting switch + u8 aiSwitchPredictionInProgress:1; // Tracks whether the AI is in the middle of running prediction calculations u8 padding:3; u8 shouldSwitch; // Stores result of ShouldSwitch, which decides whether a mon should be switched out u8 aiCalcInProgress:1; diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 7522c7ec05a5..a4af21c87bf8 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -37,13 +37,20 @@ static void InitializeSwitchinCandidate(struct Pokemon *mon) AI_DATA->switchinCandidate.hypotheticalStatus = FALSE; } +u32 GetThinkingBattler(u32 battler) +{ + if (AI_DATA->aiSwitchPredictionInProgress) + return GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerAtPosition(battler))); + return battler; +} + static bool32 IsAceMon(u32 battler, u32 monPartyId) { - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_ACE_POKEMON + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_ACE_POKEMON && !gBattleStruct->battlerState[battler].forcedSwitch && monPartyId == CalculateEnemyPartyCountInSide(battler)-1) return TRUE; - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_DOUBLE_ACE_POKEMON + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_DOUBLE_ACE_POKEMON && !gBattleStruct->battlerState[battler].forcedSwitch && (monPartyId == CalculateEnemyPartyCount()-1 || monPartyId == CalculateEnemyPartyCount()-2)) return TRUE; @@ -88,7 +95,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) u16 typeEffectiveness = UQ_4_12(1.0), aiMoveEffect; //baseline typing damage // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) + if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic @@ -338,7 +345,7 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler) bool32 isOpposingBattlerChargingOrInvulnerable = (IsSemiInvulnerable(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove)); s32 i, j; - if (!(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) + if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) return FALSE; if (HasSuperEffectiveMoveAgainstOpponents(battler, TRUE) && RandomPercentage(RNG_AI_SWITCH_ABSORBING, 66)) return FALSE; @@ -438,7 +445,7 @@ static bool32 ShouldSwitchIfOpponentChargingOrInvulnerable(u32 battler) u32 incomingMove = AI_DATA->lastUsedMove[opposingBattler]; bool32 isOpposingBattlerChargingOrInvulnerable = (IsSemiInvulnerable(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove)); - if (IsDoubleBattle() || !(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) + if (IsDoubleBattle() || !(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) return FALSE; if (isOpposingBattlerChargingOrInvulnerable && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE) @@ -457,7 +464,7 @@ static bool32 ShouldSwitchIfTrapperInParty(u32 battler) s32 opposingBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerPosition(battler))); // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) + if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Check if current mon has an ability that traps opponent @@ -500,7 +507,7 @@ static bool32 ShouldSwitchIfBadlyStatused(u32 battler) && monAbility != ABILITY_SOUNDPROOF) switchMon = TRUE; - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) { //Yawn if (gStatuses3[battler] & STATUS3_YAWN @@ -695,7 +702,7 @@ static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 perc u16 move; // Similar functionality handled more thoroughly by ShouldSwitchIfHasBadOdds - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) return FALSE; if (gLastLandedMoves[battler] == MOVE_NONE) @@ -836,7 +843,7 @@ static bool32 ShouldSwitchIfEncored(u32 battler) u32 opposingBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerPosition(battler))); // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) + if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) return FALSE; // If not Encore'd don't switch @@ -878,7 +885,7 @@ static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler) s8 spAttackingStage = gBattleMons[battler].statStages[STAT_SPATK]; // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer - if (!(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING)) + if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) return FALSE; // Physical attacker @@ -936,7 +943,7 @@ bool32 ShouldSwitch(u32 battler) return FALSE; // Sequence Switching AI never switches mid-battle - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SEQUENCE_SWITCHING) return FALSE; availableToSwitch = 0; @@ -990,7 +997,7 @@ bool32 ShouldSwitch(u32 battler) return TRUE; // These Functions can prompt switch to party member returned by GetMostSuitableMonToSwitchInto - if ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) && (CanMonSurviveHazardSwitchin(battler) == FALSE)) + if ((AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) && (CanMonSurviveHazardSwitchin(battler) == FALSE)) return FALSE; if (ShouldSwitchIfTrapperInParty(battler)) return TRUE; @@ -1016,7 +1023,7 @@ bool32 ShouldSwitch(u32 battler) // Removing switch capabilites under specific conditions // These Functions prevent the "FindMonWithFlagsAndSuperEffective" from getting out of hand. // We don't use FindMonWithFlagsAndSuperEffective with AI_FLAG_SMART_SWITCHING, so we can bail early. - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING) + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) return FALSE; if (HasSuperEffectiveMoveAgainstOpponents(battler, FALSE)) return FALSE; @@ -1823,7 +1830,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, if (aiMove != MOVE_NONE && !IsBattleMoveStatus(aiMove)) { - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_CONSERVATIVE) + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_CONSERVATIVE) damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE, DMG_ROLL_LOWEST); else damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE, DMG_ROLL_DEFAULT); @@ -2001,14 +2008,14 @@ u32 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd) GetAIPartyIndexes(battler, &firstId, &lastId); party = GetBattlerParty(battler); - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SEQUENCE_SWITCHING) { bestMonId = GetNextMonInParty(party, firstId, lastId, battlerIn1, battlerIn2); return bestMonId; } // Only use better mon selection if AI_FLAG_SMART_MON_CHOICES is set for the trainer. - if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_MON_CHOICES && !IsDoubleBattle()) // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_MON_CHOICES && !IsDoubleBattle()) // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic { bestMonId = GetBestMonIntegrated(party, firstId, lastId, battler, opposingBattler, battlerIn1, battlerIn2, switchAfterMonKOd); return bestMonId; diff --git a/src/battle_main.c b/src/battle_main.c index 9d3851a4ec22..19a0a232a9bb 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4213,10 +4213,12 @@ void SetupAISwitchingData(u32 battler, bool32 isAiRisky) // AI's predicting data if ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_SWITCH)) { + AI_DATA->aiSwitchPredictionInProgress = TRUE; AI_DATA->mostSuitableMonId[opposingBattler] = GetMostSuitableMonToSwitchInto(opposingBattler, isAiRisky); if (ShouldSwitch(opposingBattler)) AI_DATA->shouldSwitch |= (1u << opposingBattler); - + AI_DATA->aiSwitchPredictionInProgress = FALSE; + // Determine whether AI will use predictions this turn AI_DATA->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, 50); } diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index e42a596e2bd2..b16dfe6db412 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -52,6 +52,33 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Wonder Gua } } +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use hit escape moves on predicted switches") +{ + PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PREDICT_SWITCH); + PLAYER(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + PLAYER(SPECIES_CONKELDURR) { Moves(MOVE_HAMMER_ARM); } + OPPONENT(SPECIES_TYRANITAR) { Moves(MOVE_U_TURN, MOVE_CRUNCH); } + OPPONENT(SPECIES_TYRANITAR) { Moves(MOVE_U_TURN, MOVE_CRUNCH); } + } WHEN { + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_U_TURN); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in hit escape scenario") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_TYRANITAR) { Moves(MOVE_U_TURN, MOVE_CRUNCH); } + PLAYER(SPECIES_TYRANITAR) { Moves(MOVE_U_TURN, MOVE_CRUNCH); } + OPPONENT(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + OPPONENT(SPECIES_CONKELDURR) { Moves(MOVE_HAMMER_ARM); } + } WHEN { + TURN { EXPECT_SWITCH(opponent, 1); MOVE(player, MOVE_U_TURN); SEND_OUT(player, 1); } + } +} + AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI can use Focus Punch on predicted switches") { PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); @@ -79,10 +106,14 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Focus Punc } } -TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use hit escape moves on predicted switches"); -TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in hit escape scenario") -TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use Substitute on predicted switches"); +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use stat boosting moves on predicted switch"); +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in boosting move scenario"); + +TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use Subsitute on predicted switch"); TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Substitute scenario"); + + +// This might be for a follow-up PR TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will score against predicted incoming mon when switch predicted") TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in predicted-incoming-mon scenario"); From 2197f45cb00db9b0f870d58e9d01356444aa7bcc Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 00:34:10 -0500 Subject: [PATCH 03/17] unnecessary tests --- test/battle/ai/ai_flag_predict_switch.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index b16dfe6db412..6f389fa5affb 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -106,14 +106,6 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Focus Punc } } -TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use stat boosting moves on predicted switch"); -TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in boosting move scenario"); - -TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will use Subsitute on predicted switch"); -TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Substitute scenario"); - - - // This might be for a follow-up PR TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will score against predicted incoming mon when switch predicted") TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in predicted-incoming-mon scenario"); From a096a4f8fb6dd5aad74f9bfb9ada7a49808eb47a Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 11:36:57 -0500 Subject: [PATCH 04/17] Use battlerDoingPrediction for doubles support --- include/battle.h | 1 + include/constants/battle_ai.h | 2 +- src/battle_ai_switch_items.c | 2 +- src/battle_ai_util.c | 4 ++-- src/battle_main.c | 1 + 5 files changed, 6 insertions(+), 4 deletions(-) diff --git a/include/battle.h b/include/battle.h index 0313b562cbf0..a61eff530610 100644 --- a/include/battle.h +++ b/include/battle.h @@ -362,6 +362,7 @@ struct AiLogicData u8 padding:3; u8 shouldSwitch; // Stores result of ShouldSwitch, which decides whether a mon should be switched out u8 aiCalcInProgress:1; + u8 battlerDoingPrediction; // Stores which battler is currently running its prediction calcs }; struct AI_ThinkingStruct diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index 5067e1e16667..0e52c10128b9 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -51,7 +51,7 @@ #define AI_FLAG_DOUBLE_ACE_POKEMON (1 << 20) // AI has *two* Ace Pokémon. The last two Pokémons in the party won't be used unless they're the last ones remaining. Goes well in battles where the trainer ID equals to twins, couples, etc. #define AI_FLAG_WEIGH_ABILITY_PREDICTION (1 << 21) // AI will predict player's ability based on aiRating #define AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE (1 << 22) // AI adds score to highest damage move regardless of accuracy or secondary effect -#define AI_FLAG_PREDICT_SWITCH (1 << 23) // AI will predict the player's switches and switchins based on how it would handle the situation. Recommend using AI_FLAG_OMNISCIENT. Only works for singles. +#define AI_FLAG_PREDICT_SWITCH (1 << 23) // AI will predict the player's switches and switchins based on how it would handle the situation. Recommend using AI_FLAG_OMNISCIENT #define AI_FLAG_COUNT 24 diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index a4af21c87bf8..94506e639e98 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -40,7 +40,7 @@ static void InitializeSwitchinCandidate(struct Pokemon *mon) u32 GetThinkingBattler(u32 battler) { if (AI_DATA->aiSwitchPredictionInProgress) - return GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerAtPosition(battler))); + return AI_DATA->battlerDoingPrediction; return battler; } diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 3a437b99b28f..9467e1a9db4b 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -105,8 +105,8 @@ bool32 IsAiBattlerPredictingAbility(u32 battlerId) bool32 IsBattlerPredictedToSwitch(u32 battler) { // Check for prediction flag on AI, whether they're using those predictions this turn, and whether the AI thinks the player should switch - if (AI_THINKING_STRUCT->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_PREDICT_SWITCH - || AI_THINKING_STRUCT->aiFlags[B_POSITION_OPPONENT_RIGHT] & AI_FLAG_PREDICT_SWITCH) + if (AI_THINKING_STRUCT->aiFlags[AI_DATA->battlerDoingPrediction] & AI_FLAG_PREDICT_SWITCH + || AI_THINKING_STRUCT->aiFlags[AI_DATA->battlerDoingPrediction] & AI_FLAG_PREDICT_SWITCH) { if (AI_DATA->predictingSwitch && AI_DATA->shouldSwitch & (1u << battler)) return TRUE; diff --git a/src/battle_main.c b/src/battle_main.c index 19a0a232a9bb..5c1b999ab95b 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4214,6 +4214,7 @@ void SetupAISwitchingData(u32 battler, bool32 isAiRisky) if ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_SWITCH)) { AI_DATA->aiSwitchPredictionInProgress = TRUE; + AI_DATA->battlerDoingPrediction = battler; AI_DATA->mostSuitableMonId[opposingBattler] = GetMostSuitableMonToSwitchInto(opposingBattler, isAiRisky); if (ShouldSwitch(opposingBattler)) AI_DATA->shouldSwitch |= (1u << opposingBattler); From fc9be87d303bf7d29943aa31ce66333b7dc2be8c Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 11:42:42 -0500 Subject: [PATCH 05/17] When predicting, only use RNG_AI_PREDICT_SWITCH --- src/battle_ai_switch_items.c | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 94506e639e98..6e21078c8fa0 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -197,7 +197,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) { // 50% chance to stay in regardless - if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, 50)) + if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, 50) || AI_DATA->aiSwitchPredictionInProgress) return FALSE; // Switch mon out @@ -217,7 +217,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) return FALSE; // 50% chance to stay in regardless - if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, 50)) + if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, 50) || AI_DATA->aiSwitchPredictionInProgress) return FALSE; // Switch mon out @@ -347,7 +347,7 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler) if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) return FALSE; - if (HasSuperEffectiveMoveAgainstOpponents(battler, TRUE) && RandomPercentage(RNG_AI_SWITCH_ABSORBING, 66)) + if (HasSuperEffectiveMoveAgainstOpponents(battler, TRUE) && (RandomPercentage(RNG_AI_SWITCH_ABSORBING, 66) || AI_DATA->aiSwitchPredictionInProgress)) return FALSE; if (IsDoubleBattle()) @@ -761,7 +761,7 @@ static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 perc if (move == 0) continue; - if (AI_GetMoveEffectiveness(move, battler, battlerIn1) >= AI_EFFECTIVENESS_x2 && RandomPercentage(RNG_AI_SWITCH_SE_DEFENSIVE, percentChance)) + if (AI_GetMoveEffectiveness(move, battler, battlerIn1) >= AI_EFFECTIVENESS_x2 && (RandomPercentage(RNG_AI_SWITCH_SE_DEFENSIVE, percentChance) || AI_DATA->aiSwitchPredictionInProgress)) return SetSwitchinAndSwitch(battler, i); } } @@ -859,7 +859,7 @@ static bool32 ShouldSwitchIfEncored(u32 battler) return FALSE; // Switch out 50% of the time otherwise - else if (RandomPercentage(RNG_AI_SWITCH_ENCORE, 50) && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE) + else if ((RandomPercentage(RNG_AI_SWITCH_ENCORE, 50) || AI_DATA->aiSwitchPredictionInProgress) && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE) return SetSwitchinAndSwitch(battler, PARTY_SIZE); return FALSE; @@ -897,7 +897,7 @@ static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler) // 50% chance if attack at -2 and have a good candidate mon else if (attackingStage == DEFAULT_STAT_STAGE - 2) { - if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, 50)) + if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, 50) || AI_DATA->aiSwitchPredictionInProgress)) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // If at -3 or worse, switch out regardless @@ -914,7 +914,7 @@ static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler) // 50% chance if attack at -2 and have a good candidate mon else if (spAttackingStage == DEFAULT_STAT_STAGE - 2) { - if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, 50)) + if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, 50) || AI_DATA->aiSwitchPredictionInProgress)) return SetSwitchinAndSwitch(battler, PARTY_SIZE); } // If at -3 or worse, switch out regardless From dbeb0e3c0032001545c80158fa1ae0028680e673 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 12:33:44 -0500 Subject: [PATCH 06/17] Fix test --- test/battle/ai/ai_flag_predict_switch.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index 6f389fa5affb..b24a647fff18 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -89,7 +89,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI can use Focus Punch on predict OPPONENT(SPECIES_BRELOOM) { Moves(MOVE_FOCUS_PUNCH, MOVE_TACKLE); } OPPONENT(SPECIES_BRELOOM); } WHEN { - TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_FOCUS_PUNCH); } + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_FOCUS_PUNCH); SEND_OUT(player, 0); } } } From eed32517e0f3ec9280aa03e4075a5dc43f2da9a2 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 14:41:32 -0500 Subject: [PATCH 07/17] Tests for shouldswitch and getmostsuitablemontoswitchinto --- src/battle_ai_main.c | 4 +++ test/battle/ai/ai_flag_predict_switch.c | 35 +++++++++++++++++++++++-- 2 files changed, 37 insertions(+), 2 deletions(-) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 978415957d04..74613e107bce 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -5272,6 +5272,10 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // ADJUST_SCORE(GOOD_EFFECT); break; + case EFFECT_FOCUS_PUNCH: + ADJUST_SCORE(DECENT_EFFECT); + break; + // Free setup (U-Turn etc. handled in Check Viability by ShouldPivot) case EFFECT_BOLT_BEAK: case EFFECT_LIGHT_SCREEN: diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index b24a647fff18..c915e0d73346 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -79,17 +79,48 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in hit escape } } +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: Considers ShouldSwitch and GetMostSuitableMonToSwitchInto from player's perspective") +{ + // Switching in trapper is an advanced feature of ShouldSwitch that requires GetMostSuitableMonToSwitchInto to also return a specific mon; this passing means the AI can use both in prediction + PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PREDICT_SWITCH); + PLAYER(SPECIES_CACNEA) { Moves(MOVE_ABSORB); } + PLAYER(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_ACROBATICS); } + OPPONENT(SPECIES_BRELOOM) { Ability(ABILITY_EFFECT_SPORE); Moves(MOVE_PURSUIT, MOVE_BITE); } + OPPONENT(SPECIES_BRELOOM); + } WHEN { + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_PURSUIT); } + TURN { MOVE(player, MOVE_ACROBATICS); EXPECT_MOVE(opponent, MOVE_BITE); EXPECT_SEND_OUT(opponent, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in trapper-from-player's-perspective case") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_BRELOOM) { Speed(5); Ability(ABILITY_EFFECT_SPORE); Moves(MOVE_PURSUIT, MOVE_BITE); } + PLAYER(SPECIES_BRELOOM) { Speed(5); } + OPPONENT(SPECIES_CACNEA) { Speed(6); Moves(MOVE_ABSORB); } + OPPONENT(SPECIES_DUGTRIO) { Speed(6); Ability(ABILITY_ARENA_TRAP); Moves(MOVE_ACROBATICS); } + } WHEN { + TURN { MOVE(player, MOVE_PURSUIT); EXPECT_SWITCH(opponent, 1); } + TURN { EXPECT_MOVE(opponent, MOVE_ACROBATICS); MOVE(player, MOVE_BITE); SEND_OUT(player, 1); } + } +} + AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI can use Focus Punch on predicted switches") { PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); GIVEN { AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PREDICT_SWITCH); PLAYER(SPECIES_CACNEA) { Moves(MOVE_ABSORB); } - PLAYER(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_AERIAL_ACE); } + PLAYER(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_ACROBATICS); } OPPONENT(SPECIES_BRELOOM) { Moves(MOVE_FOCUS_PUNCH, MOVE_TACKLE); } OPPONENT(SPECIES_BRELOOM); } WHEN { TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_FOCUS_PUNCH); SEND_OUT(player, 0); } + TURN { MOVE(player, MOVE_ABSORB); EXPECT_MOVE(opponent, MOVE_TACKLE); } } } @@ -100,7 +131,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Focus Punc PLAYER(SPECIES_BRELOOM) { Moves(MOVE_FOCUS_PUNCH, MOVE_TACKLE); } PLAYER(SPECIES_BRELOOM); OPPONENT(SPECIES_CACNEA) { Moves(MOVE_ABSORB); } - OPPONENT(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_AERIAL_ACE); } + OPPONENT(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_ACROBATICS); } } WHEN { TURN { MOVE(player, MOVE_TACKLE); EXPECT_SWITCH(opponent, 1); } } From b1fb6b1d31dffc1caee3ffe66934bed8413f72a4 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 14:44:37 -0500 Subject: [PATCH 08/17] Fix test name --- test/battle/ai/ai_flag_predict_switch.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index c915e0d73346..88c5a549ded6 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -2,7 +2,7 @@ #include "test/battle.h" #include "battle_ai_util.h" -AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will predict switches with Pursuit") +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will predict use Pursuit on predicted switches") { PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); GIVEN { From 287cc25f7c1eee2033f64553c513837d177f85e2 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 15:02:37 -0500 Subject: [PATCH 09/17] PR scope --- test/battle/ai/ai_flag_predict_switch.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index 88c5a549ded6..cedff9d34461 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -137,6 +137,6 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Focus Punc } } -// This might be for a follow-up PR +// This will be for a follow-up PR TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will score against predicted incoming mon when switch predicted") TO_DO_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in predicted-incoming-mon scenario"); From 8cce92ed55e625e52a22e40c456b64fda26b0ecd Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 15:30:25 -0500 Subject: [PATCH 10/17] Move Focus Punch logic to CheckBadMove --- src/battle_ai_main.c | 6 ++++++ src/battle_ai_util.c | 11 ----------- 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 74613e107bce..5de28b8c282d 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1321,6 +1321,12 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // AI_CBM_HighRiskForDamage if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) ADJUST_SCORE(-10); + if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) + || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) + || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) + ADJUST_SCORE(-10); + if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) + ADJUST_SCORE(-10); break; case EFFECT_COUNTER: case EFFECT_MIRROR_COAT: diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 9467e1a9db4b..a17339d9b770 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -489,17 +489,6 @@ bool32 IsDamageMoveUnusable(u32 battlerAtk, u32 battlerDef, u32 move, u32 moveTy if (!gDisableStructs[battlerAtk].isFirstTurn) return TRUE; break; - case EFFECT_FOCUS_PUNCH: - if (IsBattlerPredictedToSwitch(battlerDef)) - return FALSE; - if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) - || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) - || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) - return TRUE; - // If AI could Sub and doesn't have a Sub, don't Punch yet - if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) - return TRUE; - break; } return FALSE; From 7ed9666351dccd5d4327b525de04d06b16400456 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 15:48:29 -0500 Subject: [PATCH 11/17] Fix Focus Punch scoring --- src/battle_ai_main.c | 6 ++++-- test/battle/ai/ai_flag_predict_switch.c | 19 ++++++++----------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 5de28b8c282d..980d470e19d8 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1321,11 +1321,11 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // AI_CBM_HighRiskForDamage if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) ADJUST_SCORE(-10); - if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) + else if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) ADJUST_SCORE(-10); - if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) + else if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) ADJUST_SCORE(-10); break; case EFFECT_COUNTER: @@ -5280,6 +5280,8 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_FOCUS_PUNCH: ADJUST_SCORE(DECENT_EFFECT); + if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_CHECK_BAD_MOVE) + ADJUST_SCORE(10); break; // Free setup (U-Turn etc. handled in Check Viability by ShouldPivot) diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index cedff9d34461..afed790a4175 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -114,13 +114,11 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI can use Focus Punch on predict PASSES_RANDOMLY(5, 10, RNG_AI_PREDICT_SWITCH); GIVEN { AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PREDICT_SWITCH); - PLAYER(SPECIES_CACNEA) { Moves(MOVE_ABSORB); } - PLAYER(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_ACROBATICS); } - OPPONENT(SPECIES_BRELOOM) { Moves(MOVE_FOCUS_PUNCH, MOVE_TACKLE); } - OPPONENT(SPECIES_BRELOOM); + PLAYER(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + PLAYER(SPECIES_CONKELDURR) { Moves(MOVE_HAMMER_ARM); } + OPPONENT(SPECIES_TYRANITAR) { Moves(MOVE_FOCUS_PUNCH, MOVE_BRICK_BREAK); } } WHEN { - TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_FOCUS_PUNCH); SEND_OUT(player, 0); } - TURN { MOVE(player, MOVE_ABSORB); EXPECT_MOVE(opponent, MOVE_TACKLE); } + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_FOCUS_PUNCH); } } } @@ -128,12 +126,11 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Focus Punc { GIVEN { AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); - PLAYER(SPECIES_BRELOOM) { Moves(MOVE_FOCUS_PUNCH, MOVE_TACKLE); } - PLAYER(SPECIES_BRELOOM); - OPPONENT(SPECIES_CACNEA) { Moves(MOVE_ABSORB); } - OPPONENT(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); Moves(MOVE_ACROBATICS); } + PLAYER(SPECIES_TYRANITAR) { Moves(MOVE_FOCUS_PUNCH, MOVE_BRICK_BREAK); } + OPPONENT(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + OPPONENT(SPECIES_CONKELDURR) { Moves(MOVE_HAMMER_ARM); } } WHEN { - TURN { MOVE(player, MOVE_TACKLE); EXPECT_SWITCH(opponent, 1); } + TURN { EXPECT_SWITCH(opponent, 1); MOVE(player, MOVE_FOCUS_PUNCH); } } } From d586627294a2e84167d7ab1f3a1b2f6f5155f1dc Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 15:58:41 -0500 Subject: [PATCH 12/17] Focus Punch score handling --- include/battle_ai_util.h | 1 + src/battle_ai_main.c | 14 +++----------- src/battle_ai_util.c | 16 ++++++++++++++++ 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index f561182fca67..58ebf94d232f 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -216,5 +216,6 @@ void IncreaseTidyUpScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); bool32 AI_ShouldSpicyExtract(u32 battlerAtk, u32 battlerAtkPartner, u32 move, struct AiLogicData *aiData); void IncreaseSubstituteMoveScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); bool32 IsBattlerPredictedToSwitch(u32 battler); +s32 AdjustFocusPunchScore(u32 battlerAtk, u32 battlerDef, s32 score, s32 adjustment); #endif //GUARD_BATTLE_AI_UTIL_H diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 980d470e19d8..bfec98b59cb0 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1318,15 +1318,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_PRESENT: case EFFECT_FIXED_DAMAGE_ARG: case EFFECT_FOCUS_PUNCH: - // AI_CBM_HighRiskForDamage - if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) - ADJUST_SCORE(-10); - else if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) - || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) - || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) - ADJUST_SCORE(-10); - else if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) - ADJUST_SCORE(-10); + score = AdjustFocusPunchScore(battlerAtk, battlerDef, score, -10); break; case EFFECT_COUNTER: case EFFECT_MIRROR_COAT: @@ -5279,9 +5271,9 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; case EFFECT_FOCUS_PUNCH: - ADJUST_SCORE(DECENT_EFFECT); if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_CHECK_BAD_MOVE) - ADJUST_SCORE(10); + score = AdjustFocusPunchScore(battlerAtk, battlerDef, score, 10); + ADJUST_SCORE(DECENT_EFFECT); break; // Free setup (U-Turn etc. handled in Check Viability by ShouldPivot) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index a17339d9b770..c2a20d8be9c9 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -4213,3 +4213,19 @@ void IncreaseSubstituteMoveScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 * if (AI_DATA->hpPercents[battlerAtk] > 70) ADJUST_SCORE_PTR(WEAK_EFFECT); } + +s32 AdjustFocusPunchScore(u32 battlerAtk, u32 battlerDef, s32 score, s32 adjustment) +{ + struct AiLogicData *aiData = AI_DATA; + u32 effectiveness = aiData->effectiveness[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex]; + + if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) + ADJUST_SCORE(adjustment); + else if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) + || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) + || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) + ADJUST_SCORE(adjustment); + else if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) + ADJUST_SCORE(adjustment); + return score; +} From bc0e5d0fe5dd31826bba62bc8ad8ba3e8128887f Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 16:04:57 -0500 Subject: [PATCH 13/17] Revert "Focus Punch score handling" This reverts commit d586627294a2e84167d7ab1f3a1b2f6f5155f1dc. --- include/battle_ai_util.h | 1 - src/battle_ai_main.c | 14 +++++++++++--- src/battle_ai_util.c | 16 ---------------- 3 files changed, 11 insertions(+), 20 deletions(-) diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 58ebf94d232f..f561182fca67 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -216,6 +216,5 @@ void IncreaseTidyUpScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); bool32 AI_ShouldSpicyExtract(u32 battlerAtk, u32 battlerAtkPartner, u32 move, struct AiLogicData *aiData); void IncreaseSubstituteMoveScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); bool32 IsBattlerPredictedToSwitch(u32 battler); -s32 AdjustFocusPunchScore(u32 battlerAtk, u32 battlerDef, s32 score, s32 adjustment); #endif //GUARD_BATTLE_AI_UTIL_H diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index bfec98b59cb0..980d470e19d8 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1318,7 +1318,15 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_PRESENT: case EFFECT_FIXED_DAMAGE_ARG: case EFFECT_FOCUS_PUNCH: - score = AdjustFocusPunchScore(battlerAtk, battlerDef, score, -10); + // AI_CBM_HighRiskForDamage + if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) + ADJUST_SCORE(-10); + else if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) + || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) + || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) + ADJUST_SCORE(-10); + else if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) + ADJUST_SCORE(-10); break; case EFFECT_COUNTER: case EFFECT_MIRROR_COAT: @@ -5271,9 +5279,9 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; case EFFECT_FOCUS_PUNCH: - if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_CHECK_BAD_MOVE) - score = AdjustFocusPunchScore(battlerAtk, battlerDef, score, 10); ADJUST_SCORE(DECENT_EFFECT); + if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_CHECK_BAD_MOVE) + ADJUST_SCORE(10); break; // Free setup (U-Turn etc. handled in Check Viability by ShouldPivot) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index c2a20d8be9c9..a17339d9b770 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -4213,19 +4213,3 @@ void IncreaseSubstituteMoveScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 * if (AI_DATA->hpPercents[battlerAtk] > 70) ADJUST_SCORE_PTR(WEAK_EFFECT); } - -s32 AdjustFocusPunchScore(u32 battlerAtk, u32 battlerDef, s32 score, s32 adjustment) -{ - struct AiLogicData *aiData = AI_DATA; - u32 effectiveness = aiData->effectiveness[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex]; - - if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) - ADJUST_SCORE(adjustment); - else if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) - || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) - || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) - ADJUST_SCORE(adjustment); - else if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) - ADJUST_SCORE(adjustment); - return score; -} From c5dec7ec29eed70ae4105c47d92fc2b569a21a42 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 16:11:58 -0500 Subject: [PATCH 14/17] Change FPunch handling --- src/battle_ai_main.c | 15 ++++++++++++--- src/battle_ai_util.c | 2 +- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 980d470e19d8..ebec1edac4e8 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1321,11 +1321,11 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // AI_CBM_HighRiskForDamage if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) ADJUST_SCORE(-10); - else if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) + if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) ADJUST_SCORE(-10); - else if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) + if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) ADJUST_SCORE(-10); break; case EFFECT_COUNTER: @@ -5268,6 +5268,8 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) u32 opposingHazardFlags = gSideStatuses[GetBattlerSide(battlerDef)] & (SIDE_STATUS_SPIKES | SIDE_STATUS_STEALTH_ROCK | SIDE_STATUS_TOXIC_SPIKES); u32 aiHazardFlags = gSideStatuses[GetBattlerSide(battlerAtk)] & (SIDE_STATUS_HAZARDS_ANY); u32 moveEffect = gMovesInfo[move].effect; + struct AiLogicData *aiData = AI_DATA; + u32 effectiveness = aiData->effectiveness[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex]; // Switch benefit switch (moveEffect) @@ -5281,7 +5283,14 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_FOCUS_PUNCH: ADJUST_SCORE(DECENT_EFFECT); if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_CHECK_BAD_MOVE) - ADJUST_SCORE(10); + { + if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2) + ADJUST_SCORE(10); + if (HasDamagingMove(battlerDef) && !((gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE) + || IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) + || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION))) + ADJUST_SCORE(10); + } break; // Free setup (U-Turn etc. handled in Check Viability by ShouldPivot) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index a17339d9b770..005e035e1943 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1253,7 +1253,7 @@ bool32 CanAIFaintTarget(u32 battlerAtk, u32 battlerDef, u32 numHits) for (i = 0; i < MAX_MON_MOVES; i++) { - if (moves[i] != MOVE_NONE && moves[i] != MOVE_UNAVAILABLE && !(moveLimitations & (1u << i))) + if (moves[i] != MOVE_NONE && moves[i] != MOVE_UNAVAILABLE && !(moveLimitations & (1u << i)) && moves[i] != MOVE_FOCUS_PUNCH) { // Use the pre-calculated value in simulatedDmg instead of re-calculating it dmg = AI_DATA->simulatedDmg[battlerAtk][battlerDef][i].expected; From 2a0367a7dd467b752381c1b8457e0a44c43e7836 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 17:18:34 -0500 Subject: [PATCH 15/17] Put back set/restore --- src/battle_ai_main.c | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index ebec1edac4e8..cb8b49173181 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -412,14 +412,19 @@ static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u3 u16 *moves; u32 battlerDef, moveIndex, move; u32 rollType = GetDmgRollType(battlerAtk); + SaveBattlerData(battlerAtk); moves = GetMovesArray(battlerAtk); + SetBattlerData(battlerAtk); + // Simulate dmg for both ai controlled mons and for player controlled mons. for (battlerDef = 0; battlerDef < battlersCount; battlerDef++) { if (battlerAtk == battlerDef || !IsBattlerAlive(battlerDef)) continue; + SaveBattlerData(battlerDef); + SetBattlerData(battlerDef); for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) { struct SimulatedDamage dmg = {0}; @@ -437,7 +442,9 @@ static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u3 aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex] = dmg; aiData->effectiveness[battlerAtk][battlerDef][moveIndex] = effectiveness; } + RestoreBattlerData(battlerDef); } + RestoreBattlerData(battlerAtk); } void SetAiLogicDataForTurn(struct AiLogicData *aiData) From 34e581e89921b01962f792f2cce79186c289d034 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 17:32:10 -0500 Subject: [PATCH 16/17] Cleaner FP refactor imo --- src/battle_ai_main.c | 2 ++ src/battle_ai_util.c | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index cb8b49173181..8482716f5b55 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1334,6 +1334,8 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) ADJUST_SCORE(-10); if (HasMoveEffect(battlerAtk, EFFECT_SUBSTITUTE) && !(gBattleMons[battlerAtk].status2 & STATUS2_SUBSTITUTE)) ADJUST_SCORE(-10); + if (HasMoveEffect(battlerAtk, EFFECT_SLEEP) && ! (gBattleMons[battlerDef].status1 & STATUS1_SLEEP)) + ADJUST_SCORE(-10); break; case EFFECT_COUNTER: case EFFECT_MIRROR_COAT: diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 005e035e1943..6f6b5c718818 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1253,7 +1253,7 @@ bool32 CanAIFaintTarget(u32 battlerAtk, u32 battlerDef, u32 numHits) for (i = 0; i < MAX_MON_MOVES; i++) { - if (moves[i] != MOVE_NONE && moves[i] != MOVE_UNAVAILABLE && !(moveLimitations & (1u << i)) && moves[i] != MOVE_FOCUS_PUNCH) + if (moves[i] != MOVE_NONE && moves[i] != MOVE_UNAVAILABLE && !(moveLimitations & (1u << i))) { // Use the pre-calculated value in simulatedDmg instead of re-calculating it dmg = AI_DATA->simulatedDmg[battlerAtk][battlerDef][i].expected; @@ -3956,7 +3956,7 @@ void IncreaseParalyzeScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score) void IncreaseSleepScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score) { - if (((AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_TRY_TO_FAINT) && CanAIFaintTarget(battlerAtk, battlerDef, 0)) + if (((AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_TRY_TO_FAINT) && CanAIFaintTarget(battlerAtk, battlerDef, 0) && gMovesInfo[GetBestDmgMoveFromBattler(battlerAtk, battlerDef)].effect != EFFECT_FOCUS_PUNCH) || AI_DATA->holdEffects[battlerDef] == HOLD_EFFECT_CURE_SLP || AI_DATA->holdEffects[battlerDef] == HOLD_EFFECT_CURE_STATUS) return; From 9a1d1a2a21ca8f09132a5449c1d55c8005830910 Mon Sep 17 00:00:00 2001 From: KyleLaporte Date: Wed, 15 Jan 2025 17:51:23 -0500 Subject: [PATCH 17/17] Switch logic ignores focus punch as damage move --- src/battle_ai_switch_items.c | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 6e21078c8fa0..31f28a48be85 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -163,7 +163,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) for (i = 0; i < MAX_MON_MOVES; i++) { playerMove = gBattleMons[opposingBattler].moves[i]; - if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove)) + if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && gMovesInfo[playerMove].effect != EFFECT_FOCUS_PUNCH) { damageTaken = AI_CalcDamage(playerMove, opposingBattler, battler, &effectiveness, FALSE, weather, DMG_ROLL_HIGHEST).expected; if (damageTaken > maxDamageTaken) @@ -1699,7 +1699,7 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattle for (i = 0; i < MAX_MON_MOVES; i++) { playerMove = gBattleMons[opposingBattler].moves[i]; - if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove)) + if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && gMovesInfo[playerMove].effect != EFFECT_FOCUS_PUNCH) { damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, FALSE, DMG_ROLL_HIGHEST); if (damageTaken > maxDamageTaken)