diff --git a/include/battle_ai_switch_items.h b/include/battle_ai_switch_items.h index f6cabcc684..b91d452097 100644 --- a/include/battle_ai_switch_items.h +++ b/include/battle_ai_switch_items.h @@ -32,6 +32,7 @@ enum ShouldSwitchScenario SHOULD_SWITCH_CHOICE_LOCKED, SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO, SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS, + SHOULD_SWITCH_ALL_SCORES_BAD, }; enum SwitchType @@ -45,5 +46,6 @@ void AI_TrySwitchOrUseItem(u32 battler); u32 GetMostSuitableMonToSwitchInto(u32 battler, enum SwitchType switchType); bool32 ShouldSwitch(u32 battler); bool32 IsMonGrounded(u16 heldItemEffect, u32 ability, u8 type1, u8 type2); +void ModifySwitchAfterMoveScoring(u32 battler); #endif // GUARD_BATTLE_AI_SWITCH_ITEMS_H diff --git a/include/config/ai.h b/include/config/ai.h index de49a2248f..31fca355dd 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -22,6 +22,7 @@ #define SHOULD_SWITCH_CHOICE_LOCKED_PERCENTAGE 100 // Only if locked into status move #define SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO_PERCENTAGE 50 #define SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS_PERCENTAGE 100 +#define SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE 100 // AI smart switching chances for bad statuses #define SHOULD_SWITCH_PERISH_SONG_PERCENTAGE 100 @@ -46,6 +47,8 @@ // AI switchin considerations #define ALL_MOVES_BAD_STATUS_MOVES_BAD FALSE // If the AI has no moves that affect the target, ShouldSwitchIfAllMovesBad can prompt a switch. Enabling this config will ignore status moves that can affect the target when making this decision. +#define AI_BAD_SCORE_THRESHOLD 90 // Move scores beneath this threshold are considered "bad" when deciding switching +#define AI_GOOD_SCORE_THRESHOLD 100 // Move scores above this threshold are considered "good" when deciding switching // AI held item-based move scoring #define LOW_ACCURACY_THRESHOLD 75 // Moves with accuracy equal OR below this value are considered low accuracy diff --git a/include/random.h b/include/random.h index 14d0408ea8..d47260c7f0 100644 --- a/include/random.h +++ b/include/random.h @@ -190,6 +190,7 @@ enum RandomTag RNG_AI_SWITCH_TRAPPER, RNG_AI_SWITCH_FREE_TURN, RNG_AI_SWITCH_ALL_MOVES_BAD, + RNG_AI_SWITCH_ALL_SCORES_BAD, RNG_AI_PP_STALL_DISREGARD_MOVE, RNG_SHELL_SIDE_ARM, RNG_RANDOM_TARGET, diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 4bd1be1b4b..e506dbc742 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -283,11 +283,11 @@ bool32 BattlerChoseNonMoveAction(void) return FALSE; } -void SetupAISwitchingData(u32 battler, enum SwitchType switchType) +void SetupAIPredictionData(u32 battler, enum SwitchType switchType) { s32 opposingBattler = GetOppositeBattler(battler); - // AI's predicting data + // Switch prediction if ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_SWITCH)) { AI_DATA->aiSwitchPredictionInProgress = TRUE; @@ -302,11 +302,8 @@ void SetupAISwitchingData(u32 battler, enum SwitchType switchType) AI_DATA->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, PREDICT_SWITCH_CHANCE); } - // AI's data - AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, switchType); - if (ShouldSwitch(battler)) - AI_DATA->shouldSwitch |= (1u << battler); - gBattleStruct->prevTurnSpecies[battler] = gBattleMons[battler].species; + // TODO Move prediction + // ModifySwitchAfterMoveScoring(opposingBattler); } void ComputeBattlerDecisions(u32 battler) @@ -323,9 +320,21 @@ void ComputeBattlerDecisions(u32 battler) enum SwitchType switchType = (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_RISKY) ? SWITCH_AFTER_KO : SWITCH_MID_BATTLE; AI_DATA->aiCalcInProgress = TRUE; + + // Setup battler and prediction data BattleAI_SetupAIData(0xF, battler); - SetupAISwitchingData(battler, switchType); + SetupAIPredictionData(battler, switchType); + + // AI's own switching data + AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, switchType); + if (ShouldSwitch(battler)) + AI_DATA->shouldSwitch |= (1u << battler); + gBattleStruct->prevTurnSpecies[battler] = gBattleMons[battler].species; + + // AI's move scoring gAiBattleData->chosenMoveIndex[battler] = BattleAI_ChooseMoveIndex(battler); // Calculate score and chose move index + ModifySwitchAfterMoveScoring(battler); + AI_DATA->aiCalcInProgress = FALSE; } } diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index c1ff6d9c4a..6c6a54ded7 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -101,6 +101,8 @@ u32 GetSwitchChance(enum ShouldSwitchScenario shouldSwitchScenario) return SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO_PERCENTAGE; case SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS: return SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS_PERCENTAGE; + case SHOULD_SWITCH_ALL_SCORES_BAD: + return SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE; default: return 100; } @@ -1063,24 +1065,6 @@ static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler) return FALSE; } -static bool32 HasGoodSubstituteMove(u32 battler) -{ - int i; - u32 aiMove, opposingBattler = GetOppositeBattler(battler); - enum BattleMoveEffects aiMoveEffect; - for (i = 0; i < MAX_MON_MOVES; i++) - { - aiMove = gBattleMons[battler].moves[i]; - aiMoveEffect = GetMoveEffect(aiMove); - if (IsSubstituteEffect(aiMoveEffect)) - { - if (IncreaseSubstituteMoveScore(battler, opposingBattler, aiMove) > 0) - return TRUE; - } - } - return FALSE; -} - bool32 ShouldSwitch(u32 battler) { u32 battlerIn1, battlerIn2; @@ -1154,8 +1138,6 @@ bool32 ShouldSwitch(u32 battler) return TRUE; if ((AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) && (CanMonSurviveHazardSwitchin(battler) == FALSE)) return FALSE; - if (HasGoodSubstituteMove(battler)) - return FALSE; if (ShouldSwitchIfTrapperInParty(battler)) return TRUE; if (FindMonThatAbsorbsOpponentsMove(battler)) @@ -1198,6 +1180,109 @@ bool32 ShouldSwitch(u32 battler) return FALSE; } +bool32 ShouldSwitchIfAllScoresBad(u32 battler) +{ + u32 i, score, opposingBattler = GetOppositeBattler(battler); + if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING)) + return FALSE; + + for (i = 0; i < MAX_MON_MOVES; i++) + { + score = gAiBattleData->finalScore[battler][opposingBattler][i]; + if (score > AI_BAD_SCORE_THRESHOLD) + return FALSE; + } + if (RandomPercentage(RNG_AI_SWITCH_ALL_SCORES_BAD, GetSwitchChance(SHOULD_SWITCH_ALL_SCORES_BAD))) + return TRUE; + return FALSE; +} + +bool32 ShouldStayInToUseMove(u32 battler) +{ + u32 i, aiMove, opposingBattler = GetOppositeBattler(battler); + enum BattleMoveEffects aiMoveEffect; + for (i = 0; i < MAX_MON_MOVES; i++) + { + aiMove = gBattleMons[battler].moves[i]; + aiMoveEffect = GetMoveEffect(aiMove); + if (aiMoveEffect == EFFECT_REVIVAL_BLESSING || IsSwitchOutEffect(aiMoveEffect)) + { + if (gAiBattleData->finalScore[battler][opposingBattler][i] > AI_GOOD_SCORE_THRESHOLD) + return TRUE; + } + } + return FALSE; +} + +void ModifySwitchAfterMoveScoring(u32 battler) +{ + u32 battlerIn1, battlerIn2; + s32 firstId; + s32 lastId; // + 1 + struct Pokemon *party; + s32 i; + s32 availableToSwitch; + + if (gBattleMons[battler].status2 & (STATUS2_WRAPPED | STATUS2_ESCAPE_PREVENTION)) + return; + if (gStatuses3[battler] & STATUS3_ROOTED) + return; + if (IsAbilityPreventingEscape(battler)) + return; + if (gBattleTypeFlags & BATTLE_TYPE_ARENA) + return; + + // Sequence Switching AI never switches mid-battle + if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SEQUENCE_SWITCHING) + return; + + availableToSwitch = 0; + + if (IsDoubleBattle()) + { + u32 partner = GetBattlerAtPosition(BATTLE_PARTNER(GetBattlerAtPosition(battler))); + battlerIn1 = battler; + if (gAbsentBattlerFlags & (1u << partner)) + battlerIn2 = battler; + else + battlerIn2 = partner; + } + else + { + battlerIn1 = battler; + battlerIn2 = battler; + } + + GetAIPartyIndexes(battler, &firstId, &lastId); + party = GetBattlerParty(battler); + + for (i = firstId; i < lastId; i++) + { + if (!IsValidForBattle(&party[i])) + continue; + if (i == gBattlerPartyIndexes[battlerIn1]) + continue; + if (i == gBattlerPartyIndexes[battlerIn2]) + continue; + if (i == gBattleStruct->monToSwitchIntoId[battlerIn1]) + continue; + if (i == gBattleStruct->monToSwitchIntoId[battlerIn2]) + continue; + if (IsAceMon(battler, i)) + continue; + + availableToSwitch++; + } + + if (availableToSwitch == 0) + return; + + if (ShouldSwitchIfAllScoresBad(battler)) + AI_DATA->shouldSwitch |= (1u << battler); + else if (ShouldStayInToUseMove(battler)) + AI_DATA->shouldSwitch &= ~(1u << battler); +} + bool32 IsSwitchinValid(u32 battler) { // Edge case: See if partner already chose to switch into the same mon diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index e39ab1ab3e..85461966da 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -1132,3 +1132,24 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will consider choice-locked TURN { MOVE(player, MOVE_EARTHQUAKE); EXPECT_MOVE(opponent, MOVE_TACKLE); item == ITEM_NONE ? EXPECT_SEND_OUT(opponent, 1) : EXPECT_SEND_OUT(opponent, 2); } } } + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch out if all moves deal zero damage") +{ + PASSES_RANDOMLY(SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE, 100, RNG_AI_SWITCH_ALL_SCORES_BAD); + GIVEN { + ASSUME(GetMoveEffect(MOVE_WILL_O_WISP) == EFFECT_WILL_O_WISP); + ASSUME(GetMoveEffect(MOVE_POLTERGEIST) == EFFECT_POLTERGEIST); + ASSUME(GetMoveType(MOVE_SCALD) == TYPE_WATER); + ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND); + ASSUME(gSpeciesInfo[SPECIES_MANTINE].types[1] == TYPE_FLYING); + ASSUME(ItemId_GetHoldEffect(ITEM_WATER_GEM) == HOLD_EFFECT_GEMS); + ASSUME(ItemId_GetSecondaryId(ITEM_WATER_GEM) == TYPE_WATER); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_MANTINE) { Speed(5); Moves(MOVE_ROOST, MOVE_SCALD); Ability(ABILITY_WATER_VEIL); Item(ITEM_WATER_GEM); } + OPPONENT(SPECIES_DUSKNOIR) { Speed(6); Moves(MOVE_WILL_O_WISP, MOVE_POLTERGEIST, MOVE_EARTHQUAKE); } + OPPONENT(SPECIES_ZIGZAGOON) { Speed(6); Moves(MOVE_TACKLE); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_POLTERGEIST); MOVE(player, MOVE_SCALD); } + TURN { EXPECT_SWITCH(opponent, 1); MOVE(player, MOVE_ROOST); } + } +}