diff --git a/docs/tutorials/ai_flags.md b/docs/tutorials/ai_flags.md index 0d685f3e0a77..d0ff706143f7 100644 --- a/docs/tutorials/ai_flags.md +++ b/docs/tutorials/ai_flags.md @@ -29,12 +29,14 @@ This section lists all of expansion’s AI Flags and briefly describes the effec ## Composite AI Flags -Expansion has two "composite" AI flags, `AI_FLAG_BASIC_TRAINER` and `AI_FLAG_SMART_TRAINER`. This means that these flags have no unique functionality themselves, and can instead be thought of as groups of other flags that are all enabled when this flag is enabled. The idea behind these flags is that if you don't care to manage the detailed behaviour of a particular trainer, you can use these as a baseline instead, and expansion will keep them updated for you. +Expansion has a few "composite" AI flags. This means that these flags have no unique functionality themselves, and can instead be thought of as groups of other flags that are all enabled when this flag is enabled. The idea behind these flags is that if you don't care to manage the detailed behaviour of a particular trainer, you can use these as a baseline instead, and expansion will keep them updated for you. `AI_FLAG_BASIC_TRAINER` is expansion's version of generic, normal AI behaviour. It includes `AI_FLAG_CHECK_BAD_MOVE` (don't use bad moves), `AI_FLAG_TRY_TO_FAINT` (faint the player where possible), and `AI_FLAG_CHECK_VIABILITY` (choose the most effective move to use in the current context). Trainers with this flag will still be smarter than they are in vanilla as there have been dramatic improvements made to move selection, but not incredibly so. Trainers with this flag should feel like normal trainers. In general we recommend these three flags be used in all cases, unless you specifically want a trainer who makes obvious mistakes in battle. `AI_FLAG_SMART_TRAINER` is expansion's version of a "smart AI". It includes everything in `AI_FLAG_BASIC_TRAINER` along with `AI_FLAG_SMART_SWITCHING` (make smart decisions about when to switch), `AI_FLAG_SMART_MON_CHOICES` (make smart decisions about what mon to send in after a switch / KO), and `AI_FLAG_OMNISCIENT` (awareness of what moves, items, and abilities the player's mons have to better inform decisions). Expansion will keep this updated to represent the most objectively intelligent behaviour our flags are capable of producing. +`AI_FLAG_PREDICTION` will enable all of the prediction flags at once, so the AI can perform as well as possible. It is best paired with the flags in `AI_FLAG_SMART_TRAINER` for optimal behaviour. This currently includes `AI_FLAG_PREDICT_SWITCH` and `AI_FLAG_PREDICT_INCOMING_MON`, but will likely be expanded in the future. + Expansion has LOADS of flags, which will be covered in the rest of this guide. If you don't want to engage with detailed trainer AI tuning though, you can just use these two composite flags, and trust that expansion will keep their contents updated to always represent the most standard and the smartest behaviour we can. ## `AI_FLAG_CHECK_BAD_MOVE` @@ -171,3 +173,9 @@ AI will predict the player's ability based to its aiRating. Without this flag th ## `AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE` AI will add score to its highest damaging move, regardless of accuracy or secondary effects. Replaces deprecated `AI_FLAG_PREFER_STRONGEST_MOVE`. + +## `AI_FLAG_PREDICT_SWITCH` +AI will determine whether it would switch out in the player's situation or not, and predict the player to switch accordingly. In any case where the AI would consider switching, it will assume the player will switch. This is modulated by a 50% failure rate, so the behaviour is non-deterministic and can change from turn to turn to emulate the inconsistency in human predictions. This behaviour is improved significantly by using `AI_FLAG_SMART_SWITCHING` and `AI_FLAG_SMART_MON_CHOICES` as they improve the AI's ability to determine good situations to switch, and also by `AI_FLAG_OMNISCIENT` so the AI can use all its knowledge of the player's team to make the decision. + +## `AI_FLAG_PREDICT_INCOMING_MON` +This flag requires `AI_FLAG_PREDICT_SWITCH` to function. If the AI predicts that the player will switch, this flag allows the AI to run its move scoring calculation against the Pokémon it expects the player to switch into, instead of the Pokémon that it expects to switch out. diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index f561182fca67..f0adcd8af02d 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -146,6 +146,7 @@ bool32 IsStatRaisingEffect(u32 effect); bool32 IsStatLoweringEffect(u32 effect); bool32 IsSelfStatLoweringEffect(u32 effect); bool32 IsSwitchOutEffect(u32 effect); +bool32 IsChaseEffect(u32 effect); bool32 IsAttackBoostMoveEffect(u32 effect); bool32 IsUngroundingEffect(u32 effect); bool32 IsSemiInvulnerable(u32 battlerDef, u32 move); diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index 0e52c10128b9..1dcb62ee0729 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -52,12 +52,14 @@ #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_PREDICT_INCOMING_MON (1 << 24) // AI will score against the predicting incoming mon if it predicts the player to switch. Requires AI_FLAG_PREDICT_SWITCH -#define AI_FLAG_COUNT 24 +#define AI_FLAG_COUNT 25 // 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) #define AI_FLAG_SMART_TRAINER (AI_FLAG_BASIC_TRAINER | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_WEIGH_ABILITY_PREDICTION) +#define AI_FLAG_PREDICTION (AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON) // 'other' ai logic flags #define AI_FLAG_DYNAMIC_FUNC (1 << 28) // Create custom AI functions for specific battles via "setdynamicaifunc" cmd diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index e699cb977fbe..c939b8e1b3fa 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -35,6 +35,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi); static u32 ChooseMoveOrAction_Doubles(u32 battlerAi); static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battlerAi, u32 battlerDef); +static inline void BattleAI_DoAIProcessing_PredictedSwitchin(struct AI_ThinkingStruct *aiThink, struct AiLogicData *aiData, u32 battlerAi, u32 battlerDef); static bool32 IsPinchBerryItemEffect(u32 holdEffect); // ewram @@ -183,6 +184,10 @@ static u32 GetAiFlags(u16 trainerId) if (flags & AI_FLAG_SMART_SWITCHING) flags |= AI_FLAG_SMART_MON_CHOICES; + // Automatically includes AI_FLAG_PREDICT_SWITCH if AI_FLAG_PREDICT_INCOMING_MON is being used + if (flags & AI_FLAG_PREDICT_INCOMING_MON) + flags |= AI_FLAG_PREDICT_SWITCH; + if (sDynamicAiFunc != NULL) flags |= AI_FLAG_DYNAMIC_FUNC; @@ -407,14 +412,35 @@ static u32 Ai_SetMoveAccuracy(struct AiLogicData *aiData, u32 battlerAtk, u32 ba return accuracy; } -static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u32 battlersCount, u32 weather) +static void CalcBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u32 battlerDef, u32 weather) { - u16 *moves; - u32 battlerDef, moveIndex, move; + u32 moveIndex, move; u32 rollType = GetDmgRollType(battlerAtk); - SaveBattlerData(battlerAtk); - moves = GetMovesArray(battlerAtk); + u16 *moves = GetMovesArray(battlerAtk); + for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) + { + struct SimulatedDamage dmg = {0}; + u8 effectiveness = AI_EFFECTIVENESS_x0; + move = moves[moveIndex]; + + if (move != MOVE_NONE + && move != MOVE_UNAVAILABLE + //&& !IsBattleMoveStatus(move) /* we want to get effectiveness and accuracy of status moves */ + && !(aiData->moveLimitations[battlerAtk] & (1u << moveIndex))) + { + dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, TRUE, weather, rollType); + aiData->moveAccuracy[battlerAtk][battlerDef][moveIndex] = Ai_SetMoveAccuracy(aiData, battlerAtk, battlerDef, move); + } + aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex] = dmg; + aiData->effectiveness[battlerAtk][battlerDef][moveIndex] = effectiveness; + } +} + +static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u32 battlersCount, u32 weather) +{ + u32 battlerDef; + SaveBattlerData(battlerAtk); SetBattlerData(battlerAtk); // Simulate dmg for both ai controlled mons and for player controlled mons. @@ -425,23 +451,7 @@ static void SetBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u3 SaveBattlerData(battlerDef); SetBattlerData(battlerDef); - for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) - { - struct SimulatedDamage dmg = {0}; - u8 effectiveness = AI_EFFECTIVENESS_x0; - move = moves[moveIndex]; - - if (move != MOVE_NONE - && move != MOVE_UNAVAILABLE - //&& !IsBattleMoveStatus(move) /* we want to get effectiveness and accuracy of status moves */ - && !(aiData->moveLimitations[battlerAtk] & (1u << moveIndex))) - { - dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, TRUE, weather, rollType); - aiData->moveAccuracy[battlerAtk][battlerDef][moveIndex] = Ai_SetMoveAccuracy(aiData, battlerAtk, battlerDef, move); - } - aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex] = dmg; - aiData->effectiveness[battlerAtk][battlerDef][moveIndex] = effectiveness; - } + CalcBattlerAiMovesData(aiData, battlerAtk, battlerDef, weather); RestoreBattlerData(battlerDef); } RestoreBattlerData(battlerAtk); @@ -496,7 +506,10 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi) { if (flags & 1) { - BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget); + if (IsBattlerPredictedToSwitch(gBattlerTarget) && (AI_THINKING_STRUCT->aiFlags[battlerAi] & AI_FLAG_PREDICT_INCOMING_MON)) + BattleAI_DoAIProcessing_PredictedSwitchin(AI_THINKING_STRUCT, AI_DATA, battlerAi, gBattlerTarget); + else + BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget); } flags >>= 1; AI_THINKING_STRUCT->aiLogicId++; @@ -576,7 +589,10 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi) { if (flags & 1) { - BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget); + if (IsBattlerPredictedToSwitch(gBattlerTarget) && (AI_THINKING_STRUCT->aiFlags[battlerAi] & AI_FLAG_PREDICT_INCOMING_MON)) + BattleAI_DoAIProcessing_PredictedSwitchin(AI_THINKING_STRUCT, AI_DATA, battlerAi, gBattlerTarget); + else + BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget); } flags >>= 1; AI_THINKING_STRUCT->aiLogicId++; @@ -703,6 +719,116 @@ static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u3 aiThink->movesetIndex = 0; } +void BattleAI_DoAIProcessing_PredictedSwitchin(struct AI_ThinkingStruct *aiThink, struct AiLogicData *aiData, u32 battlerAtk, u32 battlerDef) +{ + struct BattlePokemon switchoutCandidate = gBattleMons[battlerDef]; + struct SimulatedDamage simulatedDamageSwitchout[4]; + u8 effectivenessSwitchout[4]; + u8 moveAccuracySwitchout[4]; + + struct BattlePokemon switchinCandidate; + struct SimulatedDamage simulatedDamageSwitchin[4]; + u8 effectivenessSwitchin[4]; + u8 moveAccuracySwitchin[4]; + + struct Pokemon *party = GetBattlerParty(battlerDef); + struct BattlePokemon *savedBattleMons = AllocSaveBattleMons(); + u32 moveIndex; + + // Store battler moves data to save time over recalculating it + for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) + { + simulatedDamageSwitchout[moveIndex] = aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex]; + effectivenessSwitchout[moveIndex] = aiData->effectiveness[battlerAtk][battlerDef][moveIndex]; + moveAccuracySwitchout[moveIndex] = aiData->moveAccuracy[battlerAtk][battlerDef][moveIndex]; + } + + // Get battler and move data for predicted switchin + PokemonToBattleMon(&party[aiData->mostSuitableMonId[battlerDef]], &switchinCandidate); + gBattleMons[battlerDef] = switchinCandidate; + SetBattlerAiData(battlerDef, aiData); + CalcBattlerAiMovesData(aiData, battlerAtk, battlerDef, AI_GetWeather(aiData)); + + // Regular processing with new battler + do + { + if (gBattleMons[battlerAtk].pp[aiThink->movesetIndex] == 0) + aiThink->moveConsidered = MOVE_NONE; + else + aiThink->moveConsidered = gBattleMons[battlerAtk].moves[aiThink->movesetIndex]; + + // There is no point in calculating scores for all 3 battlers(2 opponents + 1 ally) with certain moves. + if (aiThink->moveConsidered != MOVE_NONE + && aiThink->score[aiThink->movesetIndex] > 0 + && ShouldConsiderMoveForBattler(battlerAtk, battlerDef, aiThink->moveConsidered)) + { + if (IsChaseEffect(gMovesInfo[aiThink->moveConsidered].effect)) + { + // Save new switchin data + simulatedDamageSwitchin[aiThink->movesetIndex] = aiData->simulatedDmg[battlerAtk][battlerDef][aiThink->movesetIndex]; + effectivenessSwitchin[aiThink->movesetIndex] = aiData->effectiveness[battlerAtk][battlerDef][aiThink->movesetIndex]; + moveAccuracySwitchin[aiThink->movesetIndex] = aiData->moveAccuracy[battlerAtk][battlerDef][aiThink->movesetIndex]; + + // Restore old switchout data + gBattleMons[battlerDef] = switchoutCandidate; + SetBattlerAiData(battlerDef, aiData); + aiData->simulatedDmg[battlerAtk][battlerDef][aiThink->movesetIndex] = simulatedDamageSwitchout[aiThink->movesetIndex]; + aiData->effectiveness[battlerAtk][battlerDef][aiThink->movesetIndex] = effectivenessSwitchout[aiThink->movesetIndex]; + aiData->moveAccuracy[battlerAtk][battlerDef][aiThink->movesetIndex] = moveAccuracySwitchout[aiThink->movesetIndex]; + + if (aiThink->aiLogicId < ARRAY_COUNT(sBattleAiFuncTable) + && sBattleAiFuncTable[aiThink->aiLogicId] != NULL) + { + // Call AI function + aiThink->score[aiThink->movesetIndex] = + sBattleAiFuncTable[aiThink->aiLogicId](battlerAtk, + battlerDef, + aiThink->moveConsidered, + aiThink->score[aiThink->movesetIndex]); + } + + // Restore new switchin data + gBattleMons[battlerDef] = switchinCandidate; + SetBattlerAiData(battlerDef, aiData); + aiData->simulatedDmg[battlerAtk][battlerDef][aiThink->movesetIndex] = simulatedDamageSwitchin[aiThink->movesetIndex]; + aiData->effectiveness[battlerAtk][battlerDef][aiThink->movesetIndex] = effectivenessSwitchin[aiThink->movesetIndex]; + aiData->moveAccuracy[battlerAtk][battlerDef][aiThink->movesetIndex] = moveAccuracySwitchin[aiThink->movesetIndex]; + } + + else + { + if (aiThink->aiLogicId < ARRAY_COUNT(sBattleAiFuncTable) + && sBattleAiFuncTable[aiThink->aiLogicId] != NULL) + { + // Call AI function + aiThink->score[aiThink->movesetIndex] = + sBattleAiFuncTable[aiThink->aiLogicId](battlerAtk, + battlerDef, + aiThink->moveConsidered, + aiThink->score[aiThink->movesetIndex]); + } + } + } + else + { + aiThink->score[aiThink->movesetIndex] = 0; + } + aiThink->movesetIndex++; + } while (aiThink->movesetIndex < MAX_MON_MOVES && !(aiThink->aiAction & AI_ACTION_DO_NOT_ATTACK)); + + aiThink->movesetIndex = 0; + + // Restore original battler data and moves + FreeRestoreBattleMons(savedBattleMons); + SetBattlerAiData(battlerDef, aiData); + for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++) + { + aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex] = simulatedDamageSwitchout[moveIndex]; + aiData->effectiveness[battlerAtk][battlerDef][moveIndex] = effectivenessSwitchout[moveIndex]; + aiData->moveAccuracy[battlerAtk][battlerDef][moveIndex] = moveAccuracySwitchout[moveIndex]; + } +} + // AI Score Functions // AI_FLAG_CHECK_BAD_MOVE - decreases move scores static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) @@ -5284,7 +5410,11 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) switch (moveEffect) { case EFFECT_PURSUIT: - ADJUST_SCORE(GOOD_EFFECT); + u32 hitsToKO = GetNoOfHitsToKOBattler(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex); + if (hitsToKO == 2) + ADJUST_SCORE(GOOD_EFFECT); + else if (hitsToKO == 1) + ADJUST_SCORE(BEST_EFFECT); // else if (IsPredictedToUsePursuitableMove(battlerDef, battlerAtk) && !MoveWouldHitFirst(move, battlerAtk, battlerDef)) //Pursuit against fast U-Turn // ADJUST_SCORE(GOOD_EFFECT); break; diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 6e7288f95c82..6af068ac66cb 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) || AI_DATA->aiSwitchPredictionInProgress) + 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) || AI_DATA->aiSwitchPredictionInProgress) + if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, 50) && !AI_DATA->aiSwitchPredictionInProgress) return FALSE; // Switch mon out diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 6f6b5c718818..9732176997fe 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -2396,6 +2396,18 @@ bool32 IsSwitchOutEffect(u32 effect) } } +bool32 IsChaseEffect(u32 effect) +{ + // Effects that hit switching out mons like Pursuit + switch (effect) + { + case EFFECT_PURSUIT: + return TRUE; + default: + return FALSE; + } +} + static inline bool32 IsMoveSleepClauseTrigger(u32 move) { u32 i, effect = GetMoveEffect(move); diff --git a/src/battle_main.c b/src/battle_main.c index b47df2fb2f1c..3335944bf6d9 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4204,12 +4204,7 @@ enum 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)) { @@ -4223,6 +4218,11 @@ void SetupAISwitchingData(u32 battler, bool32 isAiRisky) // Determine whether AI will use predictions this turn AI_DATA->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, 50); } + + // AI's data + AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, isAiRisky); + if (ShouldSwitch(battler)) + AI_DATA->shouldSwitch |= (1u << battler); } static void HandleTurnActionSelectionState(void) diff --git a/test/battle/ai/ai_flag_predict_switch.c b/test/battle/ai/ai_flag_predict_switch.c index afed790a4175..a5e74ff2d721 100644 --- a/test/battle/ai/ai_flag_predict_switch.c +++ b/test/battle/ai/ai_flag_predict_switch.c @@ -2,16 +2,16 @@ #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") +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI will 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); } + 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 | AI_FLAG_PREDICT_INCOMING_MON); + PLAYER(SPECIES_GENGAR) { 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); } + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_PURSUIT); SEND_OUT(player, 1); } } } @@ -20,32 +20,19 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Pursuit sc 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_GENGAR) { 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_GENGAR) { Moves(MOVE_PSYCHIC); } OPPONENT(SPECIES_SWELLOW) { Moves(MOVE_PECK); } } WHEN { TURN { MOVE(player, MOVE_PURSUIT); EXPECT_SWITCH(opponent, 1); } @@ -84,14 +71,15 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: Considers ShouldSwitch and GetMos // 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); } + ASSUME(B_POWDER_GRASS >= GEN_6); + 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 | AI_FLAG_PREDICT_INCOMING_MON); + PLAYER(SPECIES_SKARMORY) { Moves(MOVE_TACKLE); } 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) { Ability(ABILITY_EFFECT_SPORE); Moves(MOVE_HEADBUTT, MOVE_THUNDERPUNCH); } 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); } + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_HEADBUTT); } + TURN { MOVE(player, MOVE_ACROBATICS); EXPECT_MOVE(opponent, MOVE_HEADBUTT); EXPECT_SEND_OUT(opponent, 1); } } } @@ -134,6 +122,42 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in Focus Punc } } -// 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"); +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON: AI will score against predicted incoming mon when switch predicted") +{ + 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 | AI_FLAG_PREDICT_INCOMING_MON); + PLAYER(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); } + PLAYER(SPECIES_TYRANITAR) { Moves(MOVE_EARTHQUAKE, MOVE_CRUNCH); } + OPPONENT(SPECIES_TYRANITAR) { Moves(MOVE_SPORE, MOVE_CRUNCH); } + } WHEN { + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_SPORE); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would switch out in predicted-incoming-mon scenario") +{ + PASSES_RANDOMLY(5, 10, RNG_AI_SWITCH_HASBADODDS); + 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_CRUNCH, MOVE_SPORE); } + OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); } + OPPONENT(SPECIES_TYRANITAR) { Moves(MOVE_EARTHQUAKE, MOVE_CRUNCH); } + } WHEN { + TURN { MOVE(player, MOVE_CRUNCH); EXPECT_SWITCH(opponent, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_SWITCH: AI would normally choose prediction-informed move against mon in predicted-incoming-mon scenario") +{ + // The test "AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON: AI will score against predicted incoming mon when switch predicted" is evaluating whether the AI targets the incoming mon. + // This test makes sure the move that we are using to evaluate that, MOVE_SPORE, is actually what the AI would use against the incoming mon under normal circumstances. + // If both of these tests fail, prediction is still working, it just means the move scoring no longer has Breloom using Spore against the target in a vaccuum, so that test needs to be adjusted. + 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_TYRANITAR) { Moves(MOVE_EARTHQUAKE, MOVE_CRUNCH); } + OPPONENT(SPECIES_TYRANITAR) { Moves(MOVE_SPORE, MOVE_CRUNCH); } + } WHEN { + TURN { MOVE(player, MOVE_CRUNCH); EXPECT_MOVE(opponent, MOVE_SPORE); } + } +} diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index a4cd20257469..26d111ba502e 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -669,7 +669,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch out if player's m GIVEN { ASSUME(GetMoveType(MOVE_SOLAR_BEAM) == TYPE_GRASS); AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); - PLAYER(SPECIES_BELLOSSOM) { Moves(MOVE_SOLAR_BEAM); } + PLAYER(SPECIES_BELLOSSOM) { Moves(MOVE_SOLAR_BEAM, MOVE_THUNDERBOLT); } OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_TACKLE); } OPPONENT(SPECIES_AZUMARILL) { Moves(MOVE_PLAY_ROUGH); Ability(ABILITY_SAP_SIPPER); } } WHEN {