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_INCOMING_MON: AI will score against predicted switchin if predicting switch #6037

Merged
merged 16 commits into from
Jan 20, 2025
Merged
10 changes: 9 additions & 1 deletion docs/tutorials/ai_flags.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down Expand Up @@ -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.
1 change: 1 addition & 0 deletions include/battle_ai_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 3 additions & 1 deletion include/constants/battle_ai.h
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
180 changes: 155 additions & 25 deletions src/battle_ai_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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.
Expand All @@ -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);
Expand Down Expand Up @@ -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++;
Expand Down Expand Up @@ -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++;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down
4 changes: 2 additions & 2 deletions src/battle_ai_switch_items.c
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
12 changes: 12 additions & 0 deletions src/battle_ai_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
12 changes: 6 additions & 6 deletions src/battle_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -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))
{
Expand All @@ -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)
Expand Down
Loading
Loading