Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add AI_FLAG_PREDICT_SWITCH #6028

Merged
merged 17 commits into from
Jan 16, 2025
Merged
5 changes: 4 additions & 1 deletion include/battle.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions include/battle_ai_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
3 changes: 2 additions & 1 deletion include/constants/battle_ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions include/random.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down
163 changes: 154 additions & 9 deletions src/battle_ai_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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) =
{
Expand All @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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)))
Expand Down Expand Up @@ -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);
Expand Down
Loading
Loading