From 465d30fbd05677f5bb38291fbc32f17ae8596123 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Thu, 16 Jan 2025 15:43:16 -0500 Subject: [PATCH] Add AI_FLAG_PREDICT_SWITCH (#6028) --- include/battle.h | 5 +- include/battle_ai_util.h | 1 + include/constants/battle_ai.h | 3 +- include/random.h | 1 + src/battle_ai_main.c | 163 ++++++++++++++++++++++-- src/battle_ai_switch_items.c | 57 +++++---- src/battle_ai_util.c | 38 +++--- src/battle_main.c | 30 ++++- test/battle/ai/ai_flag_predict_switch.c | 139 ++++++++++++++++++++ 9 files changed, 379 insertions(+), 58 deletions(-) create mode 100644 test/battle/ai/ai_flag_predict_switch.c diff --git a/include/battle.h b/include/battle.h index d6b5abb7f9..a61eff5306 100644 --- a/include/battle.h +++ b/include/battle.h @@ -357,9 +357,12 @@ 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 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; + u8 battlerDoingPrediction; // Stores which battler is currently running its prediction calcs }; struct AI_ThinkingStruct diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 4a47b63503..f561182fca 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 acfe020f49..0e52c10128 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 -#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 886094733a..1a735097d3 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 67e06badd6..8482716f5b 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 @@ -1328,6 +1328,14 @@ 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); + if (HasMoveEffect(battlerAtk, EFFECT_SLEEP) && ! (gBattleMons[battlerDef].status1 & STATUS1_SLEEP)) + ADJUST_SCORE(-10); break; case EFFECT_COUNTER: case EFFECT_MIRROR_COAT: @@ -3852,13 +3860,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 +5269,150 @@ 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; + struct AiLogicData *aiData = AI_DATA; + u32 effectiveness = aiData->effectiveness[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex]; + + // 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; + + case EFFECT_FOCUS_PUNCH: + ADJUST_SCORE(DECENT_EFFECT); + if (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_CHECK_BAD_MOVE) + { + 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) + 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_switch_items.c b/src/battle_ai_switch_items.c index 24c8d34699..93148d8b83 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 AI_DATA->battlerDoingPrediction; + 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 @@ -156,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) @@ -190,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 @@ -210,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 @@ -338,9 +345,9 @@ 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)) + if (HasSuperEffectiveMoveAgainstOpponents(battler, TRUE) && (RandomPercentage(RNG_AI_SWITCH_ABSORBING, 66) || AI_DATA->aiSwitchPredictionInProgress)) return FALSE; if (IsDoubleBattle()) @@ -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) @@ -757,7 +764,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); } } @@ -839,7 +846,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 @@ -855,7 +862,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; @@ -881,7 +888,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 @@ -893,7 +900,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 @@ -910,7 +917,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 @@ -939,7 +946,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; @@ -993,7 +1000,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; @@ -1019,7 +1026,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; @@ -1695,7 +1702,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) @@ -1826,7 +1833,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); @@ -2004,14 +2011,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_ai_util.c b/src/battle_ai_util.c index c9a3ae07b2..6f6b5c7188 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[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; + } + return FALSE; +} + void ClearBattlerMoveHistory(u32 battlerId) { memset(BATTLE_HISTORY->usedMoves[battlerId], 0, sizeof(BATTLE_HISTORY->usedMoves[battlerId])); @@ -477,16 +489,6 @@ bool32 IsDamageMoveUnusable(u32 battlerAtk, u32 battlerDef, u32 move, u32 moveTy if (!gDisableStructs[battlerAtk].isFirstTurn) return TRUE; break; - case EFFECT_FOCUS_PUNCH: - 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)) - return TRUE; - break; } return FALSE; @@ -2779,9 +2781,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 +3794,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: @@ -3951,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; @@ -4193,9 +4198,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 5dba22a160..5c1b999ab9 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4201,6 +4201,30 @@ 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->aiSwitchPredictionInProgress = TRUE; + AI_DATA->battlerDoingPrediction = battler; + 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); + } +} + static void HandleTurnActionSelectionState(void) { s32 i, battler; @@ -4225,11 +4249,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 0000000000..afed790a41 --- /dev/null +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -0,0 +1,139 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +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 { + 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 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: 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_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); } + } +} + +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_TYRANITAR) { Moves(MOVE_FOCUS_PUNCH, MOVE_BRICK_BREAK); } + OPPONENT(SPECIES_BRONZONG) { Moves(MOVE_PSYCHIC); } + OPPONENT(SPECIES_CONKELDURR) { Moves(MOVE_HAMMER_ARM); } + } WHEN { + TURN { EXPECT_SWITCH(opponent, 1); MOVE(player, MOVE_FOCUS_PUNCH); } + } +} + +// 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");