Add AI_FLAG_PREDICT_SWITCH (#6028)

This commit is contained in:
Pawkkie 2025-01-16 15:43:16 -05:00 committed by GitHub
parent 75d5edad08
commit 465d30fbd0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 379 additions and 58 deletions

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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,
};

View File

@ -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);

View File

@ -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;

View File

@ -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)

View File

@ -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();

View File

@ -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");