AI: Add AI_SMART_TERA flag to make smarter decisions about when to terastalize. (#6705)

Co-authored-by: jfb1337 <>
Co-authored-by: Alex <93446519+AlexOn1ine@users.noreply.github.com>
This commit is contained in:
jfb1337 2025-05-31 08:47:25 +01:00 committed by GitHub
parent eb81820ae3
commit 096493aa35
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 402 additions and 24 deletions

View File

@ -33,7 +33,7 @@ Expansion has a few "composite" AI flags. This means that these flags have no un
`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_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), `AI_FLAG_OMNISCIENT` (awareness of what moves, items, and abilities the player's mons have to better inform decisions), and `AI_FLAG_SMART_TERA` (make smart decisions about when to terastalize). 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.
@ -180,6 +180,9 @@ AI will determine whether it would switch out in the player's situation or not,
## `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.
## `AI_FLAG_SMART_TERA`
AI will make smarter decisions about when to terastalize (over the default behaviour to always tera when available). This considers factors such as whether tera allows the AI to KO the opponent, whether it can save itself from a KO or a big hit, and how many remaining pokemon could terastalize. This behavior is not currently supported in double battles.
## `AI_FLAG_PREDICT_MOVE`
AI will predict what move the player is going to use based on what move it would use in the same situation. Generally works best if also using `AI_FLAG_OMNISCIENT`.

View File

@ -791,6 +791,7 @@ struct AiBattleData
u8 playerStallMons[PARTY_SIZE];
u8 chosenMoveIndex[MAX_BATTLERS_COUNT];
u8 chosenTarget[MAX_BATTLERS_COUNT];
u16 aiUsingGimmick:6;
u8 actionFlee:1;
u8 choiceWatch:1;
u8 padding:6;

View File

@ -38,6 +38,12 @@ enum WeatherState
WEATHER_INACTIVE_AND_BLOCKED,
};
enum AIConsiderGimmick
{
NO_GIMMICK,
USE_GIMMICK,
};
static inline bool32 IsMoveUnusable(u32 moveIndex, u32 move, u32 moveLimitations)
{
return move == MOVE_NONE
@ -108,6 +114,9 @@ bool32 IsAbilityOfRating(u32 ability, s8 rating);
bool32 AI_IsAbilityOnSide(u32 battlerId, u32 ability);
bool32 AI_MoveMakesContact(u32 ability, enum ItemHoldEffect holdEffect, u32 move);
bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove);
void SetAIUsingGimmick(u32 battler, enum AIConsiderGimmick use);
bool32 IsAIUsingGimmick(u32 battler);
void DecideTerastal(u32 battler);
u32 AI_GetBattlerAbility(u32 battler);
// stat stage checks
@ -129,8 +138,8 @@ bool32 ShouldLowerEvasion(u32 battlerAtk, u32 battlerDef, u32 defAbility);
bool32 IsAffectedByPowder(u32 battler, u32 ability, enum ItemHoldEffect holdEffect);
bool32 MovesWithCategoryUnusable(u32 attacker, u32 target, u32 category);
s32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef, s32 noOfHitsToKo);
struct SimulatedDamage AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, bool32 considerZPower);
struct SimulatedDamage AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, bool32 considerZPower, u32 weather);
struct SimulatedDamage AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, enum AIConsiderGimmick considerGimmickAtk, enum AIConsiderGimmick considerGimmickDef);
struct SimulatedDamage AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, enum AIConsiderGimmick considerGimmickAtk, enum AIConsiderGimmick considerGimmickDef, u32 weather);
bool32 AI_IsDamagedByRecoil(u32 battler);
u32 GetNoOfHitsToKO(u32 dmg, s32 hp);
u32 GetNoOfHitsToKOBattlerDmg(u32 dmg, u32 battlerDef);

View File

@ -64,6 +64,10 @@
#define PREDICT_SWITCH_CHANCE 50
#define PREDICT_MOVE_CHANCE 100
// AI Terastalization chances
#define AI_CONSERVE_TERA_CHANCE_PER_MON 10 // Chance for AI with smart tera flag to decide not to tera before considering defensive benefit is this*(X-1), where X is the number of alive pokemon that could tera
#define AI_TERA_PREDICT_CHANCE 40 // Chance for AI with smart tera flag to tera in the situation where tera would save it from a KO, but could be punished by a KO from a different move.
// AI PP Stall detection chance per roll
#define PP_STALL_DISREGARD_MOVE_PERCENTAGE 50
// Score reduction if any roll for PP stall detection passes

View File

@ -32,16 +32,16 @@
#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_PP_STALL_PREVENTION (1 << 25) // AI keeps track of the player's switches where the incoming mon is immune to the chosen move
#define AI_FLAG_PREDICT_MOVE (1 << 26) // AI will predict the player's move based on what move it would use in the same situation. Recommend using AI_FLAG_OMNISCIENT
#define AI_FLAG_SMART_TERA (1 << 27) // AI will make smarter decisions when choosing whether to terrastalize (default is to always tera whenever available).
#define AI_FLAG_COUNT 28
// Flags at and after 32 need different formatting, as in
// #define AI_FLAG_PLACEHOLDER ((u64)1 << 32)
#define AI_FLAG_COUNT 27
// 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_SMART_TRAINER (AI_FLAG_BASIC_TRAINER | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_WEIGH_ABILITY_PREDICTION | AI_FLAG_SMART_TERA)
#define AI_FLAG_PREDICTION (AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON | AI_FLAG_PREDICT_MOVE)
// 'other' ai logic flags

View File

@ -190,6 +190,7 @@ enum RandomTag
RNG_AI_SWITCH_TRAPPER,
RNG_AI_SWITCH_FREE_TURN,
RNG_AI_SWITCH_ALL_MOVES_BAD,
RNG_AI_CONSERVE_TERA,
RNG_AI_SWITCH_ALL_SCORES_BAD,
RNG_AI_PP_STALL_DISREGARD_MOVE,
RNG_SHELL_SIDE_ARM,

View File

@ -399,15 +399,36 @@ void ComputeBattlerDecisions(u32 battler)
}
}
void ReconsiderGimmick(u32 battlerAtk, u32 battlerDef, u16 move)
{
// After choosing a move for battlerAtk assuming that a gimmick will be used, reconsider whether the gimmick is necessary.
if (gBattleStruct->gimmick.usableGimmick[battlerAtk] == GIMMICK_Z_MOVE && !ShouldUseZMove(battlerAtk, battlerDef, move))
SetAIUsingGimmick(battlerAtk, NO_GIMMICK);
if (gBattleStruct->gimmick.usableGimmick[battlerAtk] == GIMMICK_TERA && GetMoveEffect(move) == EFFECT_PROTECT)
SetAIUsingGimmick(battlerAtk, NO_GIMMICK);
}
u32 BattleAI_ChooseMoveIndex(u32 battler)
{
u32 chosenMoveIndex;
SetAIUsingGimmick(battler, USE_GIMMICK);
if (gBattleStruct->gimmick.usableGimmick[battler] == GIMMICK_TERA && (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_TERA))
DecideTerastal(battler);
if (!IsDoubleBattle())
chosenMoveIndex = ChooseMoveOrAction_Singles(battler);
else
chosenMoveIndex = ChooseMoveOrAction_Doubles(battler);
if (gBattleStruct->gimmick.usableGimmick[battler] != GIMMICK_NONE)
ReconsiderGimmick(battler, gBattlerTarget, gBattleMons[battler].moves[chosenMoveIndex]);
// Clear protect structures, some flags may be set during AI calcs
// e.g. pranksterElevated from GetBattleMovePriority
memset(&gProtectStructs, 0, MAX_BATTLERS_COUNT * sizeof(struct ProtectStruct));
@ -555,7 +576,7 @@ static void CalcBattlerAiMovesData(struct AiLogicData *aiData, u32 battlerAtk, u
continue;
// Also get effectiveness of status moves
dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, TRUE, weather);
dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, USE_GIMMICK, NO_GIMMICK, weather);
aiData->moveAccuracy[battlerAtk][battlerDef][moveIndex] = Ai_SetMoveAccuracy(aiData, battlerAtk, battlerDef, move);
aiData->simulatedDmg[battlerAtk][battlerDef][moveIndex] = dmg;

View File

@ -446,7 +446,7 @@ bool32 MovesWithCategoryUnusable(u32 attacker, u32 target, u32 category)
}
// To save computation time this function has 2 variants. One saves, sets and restores battlers, while the other doesn't.
struct SimulatedDamage AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, bool32 considerZPower)
struct SimulatedDamage AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, enum AIConsiderGimmick considerGimmickAtk, enum AIConsiderGimmick considerGimmickDef)
{
struct SimulatedDamage dmg;
@ -454,7 +454,7 @@ struct SimulatedDamage AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 b
SaveBattlerData(battlerDef);
SetBattlerData(battlerAtk);
SetBattlerData(battlerDef);
dmg = AI_CalcDamage(move, battlerAtk, battlerDef, typeEffectiveness, considerZPower, AI_GetWeather());
dmg = AI_CalcDamage(move, battlerAtk, battlerDef, typeEffectiveness, considerGimmickAtk, considerGimmickDef, AI_GetWeather());
RestoreBattlerData(battlerAtk);
RestoreBattlerData(battlerDef);
return dmg;
@ -732,14 +732,15 @@ static inline bool32 ShouldCalcCritDamage(u32 battlerAtk, u32 battlerDef, u32 mo
return FALSE;
}
struct SimulatedDamage AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, bool32 considerZPower, u32 weather)
struct SimulatedDamage AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, uq4_12_t *typeEffectiveness, enum AIConsiderGimmick considerGimmickAtk, enum AIConsiderGimmick considerGimmickDef, u32 weather)
{
struct SimulatedDamage simDamage;
s32 moveType;
enum BattleMoveEffects moveEffect = GetMoveEffect(move);
uq4_12_t effectivenessMultiplier;
bool32 isDamageMoveUnusable = FALSE;
bool32 toggledGimmick = FALSE;
bool32 toggledGimmickAtk = FALSE;
bool32 toggledGimmickDef = FALSE;
struct AiLogicData *aiData = gAiLogicData;
gAiLogicData->aiCalcInProgress = TRUE;
@ -748,16 +749,23 @@ struct SimulatedDamage AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u
// Temporarily enable gimmicks for damage calcs if planned
if (gBattleStruct->gimmick.usableGimmick[battlerAtk] && GetActiveGimmick(battlerAtk) == GIMMICK_NONE
&& !(gBattleStruct->gimmick.usableGimmick[battlerAtk] == GIMMICK_Z_MOVE && !considerZPower))
&& gBattleStruct->gimmick.usableGimmick[battlerAtk] != GIMMICK_NONE && considerGimmickAtk == USE_GIMMICK)
{
// Set Z-Move variables if needed
if (gBattleStruct->gimmick.usableGimmick[battlerAtk] == GIMMICK_Z_MOVE && IsViableZMove(battlerAtk, move))
gBattleStruct->zmove.baseMoves[battlerAtk] = move;
toggledGimmick = TRUE;
toggledGimmickAtk = TRUE;
SetActiveGimmick(battlerAtk, gBattleStruct->gimmick.usableGimmick[battlerAtk]);
}
if (gBattleStruct->gimmick.usableGimmick[battlerDef] && GetActiveGimmick(battlerDef) == GIMMICK_NONE
&& gBattleStruct->gimmick.usableGimmick[battlerDef] != GIMMICK_NONE && considerGimmickDef == USE_GIMMICK)
{
toggledGimmickDef = TRUE;
SetActiveGimmick(battlerDef, gBattleStruct->gimmick.usableGimmick[battlerDef]);
}
SetDynamicMoveCategory(battlerAtk, battlerDef, move);
SetTypeBeforeUsingMove(move, battlerAtk);
moveType = GetBattleMoveType(move);
@ -860,8 +868,10 @@ struct SimulatedDamage AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u
gBattleStruct->dynamicMoveType = 0;
gBattleStruct->swapDamageCategory = FALSE;
gBattleStruct->zmove.baseMoves[battlerAtk] = MOVE_NONE;
if (toggledGimmick)
if (toggledGimmickAtk)
SetActiveGimmick(battlerAtk, GIMMICK_NONE);
if (toggledGimmickDef)
SetActiveGimmick(battlerDef, GIMMICK_NONE);
gAiLogicData->aiCalcInProgress = FALSE;
return simDamage;
}
@ -3921,8 +3931,7 @@ s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct Battl
gAiThinkingStruct->saved[battlerAtk].saved = FALSE;
}
dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, FALSE, AI_GetWeather());
dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, NO_GIMMICK, NO_GIMMICK, AI_GetWeather());
// restores original gBattleMon struct
FreeRestoreBattleMons(savedBattleMons);
@ -4461,7 +4470,7 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove)
else if (!IsBattleMoveStatus(chosenMove) && IsBattleMoveStatus(zMove))
return FALSE;
dmg = AI_CalcDamageSaveBattlers(chosenMove, battlerAtk, battlerDef, &effectiveness, FALSE);
dmg = AI_CalcDamageSaveBattlers(chosenMove, battlerAtk, battlerDef, &effectiveness, NO_GIMMICK, NO_GIMMICK);
if (!IsBattleMoveStatus(chosenMove) && dmg.minimum >= gBattleMons[battlerDef].hp)
return FALSE; // don't waste damaging z move if can otherwise faint target
@ -4472,6 +4481,267 @@ bool32 ShouldUseZMove(u32 battlerAtk, u32 battlerDef, u32 chosenMove)
return FALSE;
}
void SetAIUsingGimmick(u32 battler, enum AIConsiderGimmick use)
{
if (use == USE_GIMMICK)
gAiBattleData->aiUsingGimmick |= (1<<battler);
else
gAiBattleData->aiUsingGimmick &= ~(1<<battler);
}
bool32 IsAIUsingGimmick(u32 battler)
{
return (gAiBattleData->aiUsingGimmick & (1<<battler)) != 0;
}
struct AltTeraCalcs {
struct SimulatedDamage takenWithTera[MAX_MON_MOVES];
struct SimulatedDamage dealtWithoutTera[MAX_MON_MOVES];
};
enum AIConsiderGimmick ShouldTeraFromCalcs(u32 battler, u32 opposingBattler, struct AltTeraCalcs *altCalcs);
void DecideTerastal(u32 battler)
{
if (gBattleStruct->gimmick.usableGimmick[battler] != GIMMICK_TERA)
return;
if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_SMART_TERA))
return;
// TODO: Currently only single battles are considered.
if (IsDoubleBattle())
return;
// TODO: A lot of these checks are most effective for an omnicient ai.
// If we don't have enough information about the opponent's moves, consider simpler checks based on type effectivness.
u32 opposingBattler = GetOppositeBattler(battler);
// Default calculations automatically assume gimmicks for the attacker, but not the defender.
// Consider calcs for the other possibilities.
struct AltTeraCalcs altCalcs;
struct SimulatedDamage noDmg = {0};
uq4_12_t effectivenessTakenWithTera[MAX_MON_MOVES];
u16* aiMoves = GetMovesArray(battler);
u16* oppMoves = GetMovesArray(opposingBattler);
uq4_12_t effectiveness;
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (!IsMoveUnusable(i, aiMoves[i], gAiLogicData->moveLimitations[battler]) && !IsBattleMoveStatus(aiMoves[i]))
altCalcs.dealtWithoutTera[i] = AI_CalcDamage(aiMoves[i], battler, opposingBattler, &effectiveness, NO_GIMMICK, NO_GIMMICK, AI_GetWeather());
else
altCalcs.dealtWithoutTera[i] = noDmg;
if (!IsMoveUnusable(i, oppMoves[i], gAiLogicData->moveLimitations[opposingBattler]) && !IsBattleMoveStatus(oppMoves[i]))
{
altCalcs.takenWithTera[i] = AI_CalcDamage(oppMoves[i], opposingBattler, battler, &effectiveness, USE_GIMMICK, USE_GIMMICK, AI_GetWeather());
effectivenessTakenWithTera[i] = effectiveness;
}
else
{
altCalcs.takenWithTera[i] = noDmg;
effectivenessTakenWithTera[i] = Q_4_12(0.0);
}
}
enum AIConsiderGimmick res = ShouldTeraFromCalcs(battler, opposingBattler, &altCalcs);
if (res == USE_GIMMICK)
{
// Damage calcs for damage received assumed we wouldn't tera. Adjust that so that further AI decisions are more accurate.
for (int i = 0; i < MAX_MON_MOVES; i++)
{
gAiLogicData->simulatedDmg[opposingBattler][battler][i] = altCalcs.takenWithTera[i];
gAiLogicData->effectiveness[opposingBattler][battler][i] = effectivenessTakenWithTera[i];
}
}
else
{
// Damage calcs for damage dealt assumed we would tera. Adjust that so that further AI decisions are more accurate.
for (int i = 0; i < MAX_MON_MOVES; i++)
gAiLogicData->simulatedDmg[battler][opposingBattler][i] = altCalcs.dealtWithoutTera[i];
}
SetAIUsingGimmick(battler, res);
return;
}
// macros are not expanded recursively
#define dealtWithTera gAiLogicData->simulatedDmg[battler][opposingBattler]
#define dealtWithoutTera altCalcs->dealtWithoutTera
#define takenWithTera altCalcs->takenWithTera
#define takenWithoutTera gAiLogicData->simulatedDmg[opposingBattler][battler]
enum AIConsiderGimmick ShouldTeraFromCalcs(u32 battler, u32 opposingBattler, struct AltTeraCalcs *altCalcs) {
struct Pokemon* party = GetBattlerParty(battler);
// Check how many pokemon we have that could tera
int numPossibleTera = 0;
for (int i = 0; i < PARTY_SIZE; i++)
{
if (GetMonData(&party[i], MON_DATA_HP) != 0
&& GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_NONE
&& GetMonData(&party[i], MON_DATA_SPECIES_OR_EGG) != SPECIES_EGG
&& GetMonData(&party[i], MON_DATA_TERA_TYPE) > 0)
numPossibleTera++;
}
u16 aiHp = gBattleMons[battler].hp;
u16 oppHp = gBattleMons[opposingBattler].hp;
u16* aiMoves = GetMovesArray(battler);
u16* oppMoves = GetMovesArray(opposingBattler);
// Check whether tera enables a KO
bool32 hasKoWithout = FALSE;
u16 killingMove = MOVE_NONE;
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (dealtWithTera[i].median >= oppHp)
{
u16 move = aiMoves[i];
if (killingMove == MOVE_NONE || GetBattleMovePriority(battler, gAiLogicData->abilities[battler], move) > GetBattleMovePriority(battler, gAiLogicData->abilities[battler], killingMove))
killingMove = move;
}
if (dealtWithoutTera[i].median >= oppHp)
hasKoWithout = TRUE;
}
bool32 enablesKo = (killingMove != MOVE_NONE) && !hasKoWithout;
// Check whether tera saves us from a KO
bool32 savedFromKo = FALSE;
bool32 getsKodRegardlessBySingleMove = FALSE;
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (takenWithoutTera[i].maximum >= aiHp && takenWithTera[i].maximum >= aiHp)
getsKodRegardlessBySingleMove = TRUE;
if (takenWithoutTera[i].maximum >= aiHp && takenWithTera[i].maximum < aiHp)
savedFromKo = TRUE;
}
if (getsKodRegardlessBySingleMove)
savedFromKo = FALSE;
// Check whether opponent can punish tera by ko'ing
u16 hardPunishingMove = MOVE_NONE;
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (takenWithTera[i].maximum >= aiHp)
{
u16 move = oppMoves[i];
if (hardPunishingMove == MOVE_NONE || GetBattleMovePriority(opposingBattler, gAiLogicData->abilities[opposingBattler], move) > GetBattleMovePriority(opposingBattler, gAiLogicData->abilities[opposingBattler], hardPunishingMove))
hardPunishingMove = move;
}
}
// Check whether there is a move that deals over half hp, and all such moves are reduced to under 1/4 hp by tera
// (e.g. a weakness becomes a resistance, a 4x weakness becomes neutral, etc)
bool32 takesBigHit = FALSE;
bool32 savedFromAllBigHits = TRUE;
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (takenWithoutTera[i].median > aiHp/2)
{
takesBigHit = TRUE;
if (takenWithTera[i].median > aiHp/4)
savedFromAllBigHits = FALSE;
}
}
// Check for any benefit whatsoever. Only used for the last possible mon that could tera.
bool32 anyOffensiveBenefit = FALSE;
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (dealtWithTera[i].median > dealtWithoutTera[i].median)
anyOffensiveBenefit = TRUE;
}
bool32 anyDefensiveBenefit = FALSE;
bool32 anyDefensiveDrawback = FALSE;
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (takenWithTera[i].median < takenWithoutTera[i].median)
anyDefensiveBenefit = TRUE;
if (takenWithTera[i].median > takenWithoutTera[i].median)
anyDefensiveDrawback = TRUE;
}
// Make decisions
// This is done after all loops to minimize the possibility of a timing attack in which the player could
// determine whether the AI will tera based on the time taken to select a move.
if (enablesKo)
{
if (hardPunishingMove == MOVE_NONE)
{
return USE_GIMMICK;
}
else
{
// will we go first?
if (AI_WhoStrikesFirst(battler, opposingBattler, killingMove) == AI_IS_FASTER && GetBattleMovePriority(battler, gAiLogicData->abilities[battler], killingMove) >= GetBattleMovePriority(opposingBattler, gAiLogicData->abilities[opposingBattler], hardPunishingMove))
return USE_GIMMICK;
}
}
// Decide to conserve tera based on number of possible later oppotunities
u16 conserveTeraChance = AI_CONSERVE_TERA_CHANCE_PER_MON * (numPossibleTera-1);
if (RandomPercentage(RNG_AI_CONSERVE_TERA, conserveTeraChance))
return NO_GIMMICK;
if (savedFromKo)
{
if (hardPunishingMove == MOVE_NONE)
{
return USE_GIMMICK;
}
else
{
// If tera saves us from a ko from one move, but enables a ko otherwise, randomly predict
// savesFromKo being true ensures opponent doesn't have a ko if we don't tera
if (Random() % 100 < AI_TERA_PREDICT_CHANCE)
return USE_GIMMICK;
}
}
if (hardPunishingMove != MOVE_NONE)
return NO_GIMMICK;
if (takesBigHit && savedFromAllBigHits)
return USE_GIMMICK;
// No strongly compelling reason to tera. Conserve it if possible.
if (numPossibleTera > 1)
return NO_GIMMICK;
if (anyOffensiveBenefit || (anyDefensiveBenefit && !anyDefensiveDrawback))
return USE_GIMMICK;
// TODO: Effects other than direct damage are not yet considered. For example, may want to tera poison to avoid a Toxic.
return NO_GIMMICK;
}
#undef dealtWithTera
#undef dealtWithoutTera
#undef takenWithTera
#undef takenWithoutTera
bool32 AI_IsBattlerAsleepOrComatose(u32 battlerId)
{
return (gBattleMons[battlerId].status1 & STATUS1_SLEEP) || gAiLogicData->abilities[battlerId] == ABILITY_COMATOSE;

View File

@ -522,9 +522,7 @@ static void OpponentHandleChooseMove(u32 battler)
gBattlerTarget = GetBattlerAtPosition(B_POSITION_PLAYER_RIGHT);
}
// If opponent can and should use a gimmick (considering trainer data), do it
if (gBattleStruct->gimmick.usableGimmick[battler] != GIMMICK_NONE
&& !(gBattleStruct->gimmick.usableGimmick[battler] == GIMMICK_Z_MOVE
&& !ShouldUseZMove(battler, gBattlerTarget, moveInfo->moves[chosenMoveIndex])))
if (gBattleStruct->gimmick.usableGimmick[battler] != GIMMICK_NONE && IsAIUsingGimmick(battler))
{
BtlController_EmitTwoReturnValues(battler, B_COMM_TO_ENGINE, 10, (chosenMoveIndex) | (RET_GIMMICK) | (gBattlerTarget << 8));
}

View File

@ -328,9 +328,7 @@ static void PlayerPartnerHandleChooseMove(u32 battler)
gBattlerTarget = GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT);
}
// If partner can and should use a gimmick (considering trainer data), do it
if (gBattleStruct->gimmick.usableGimmick[battler] != GIMMICK_NONE
&& !(gBattleStruct->gimmick.usableGimmick[battler] == GIMMICK_Z_MOVE
&& !ShouldUseZMove(battler, gBattlerTarget, moveInfo->moves[chosenMoveIndex])))
if (gBattleStruct->gimmick.usableGimmick[battler] != GIMMICK_NONE && IsAIUsingGimmick(battler))
{
BtlController_EmitTwoReturnValues(battler, B_COMM_TO_ENGINE, 10, (chosenMoveIndex) | (RET_GIMMICK) | (gBattlerTarget << 8));
}

View File

@ -0,0 +1,73 @@
#include "global.h"
#include "test/battle.h"
#include "battle_ai_util.h"
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will tera if it enables a ko")
{
KNOWN_FAILING; // Tests don't currently give the AI the capability to tera, even with a tera type set.
GIVEN {
ASSUME(GetMovePower(MOVE_SEED_BOMB) == 80);
ASSUME(GetMovePower(MOVE_AQUA_TAIL) == 90);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_TERA);
PLAYER(SPECIES_WOBBUFFET) { HP(47); Speed(100); }
PLAYER(SPECIES_WOBBUFFET) { Speed(100); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(1); Moves(MOVE_AQUA_TAIL, MOVE_SEED_BOMB); TeraType(TYPE_GRASS); }
OPPONENT(SPECIES_WOBBUFFET) { HP(1); Speed(100); TeraType(TYPE_FIRE); }
} WHEN {
TURN { EXPECT_MOVE(opponent, MOVE_SEED_BOMB); }
} SCENE {
MESSAGE("The opposing Wobbuffet terastilized into the Grass type!");
MESSAGE("Wobbuffet fainted!");
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will not tera if it gets outsped and ko'd")
{
GIVEN {
ASSUME(GetMovePower(MOVE_SEED_BOMB) == 80);
ASSUME(GetMovePower(MOVE_FLAMETHROWER) == 90);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_TERA | AI_FLAG_OMNISCIENT );
PLAYER(SPECIES_WOBBUFFET) { HP(47); Speed(100); Moves(MOVE_FLAMETHROWER, MOVE_CELEBRATE); }
PLAYER(SPECIES_WOBBUFFET) { Speed(100); }
OPPONENT(SPECIES_WOBBUFFET) { HP(60); Speed(1); Moves(MOVE_SEED_BOMB); TeraType(TYPE_GRASS); }
OPPONENT(SPECIES_WOBBUFFET) { HP(1); Speed(100); TeraType(TYPE_FIRE); }
} WHEN {
TURN { }
} SCENE {
NOT MESSAGE("The opposing Wobbuffet terastilized into the Grass type!");
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will not tera if it gets ko'd by priority")
{
GIVEN {
ASSUME(GetMovePower(MOVE_SEED_BOMB) == 80);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_TERA | AI_FLAG_OMNISCIENT );
PLAYER(SPECIES_WOBBUFFET) { HP(47); Speed(1); Moves(MOVE_QUICK_ATTACK, MOVE_CELEBRATE); }
PLAYER(SPECIES_WOBBUFFET) { Speed(100); }
OPPONENT(SPECIES_WOBBUFFET) { HP(1); Speed(100); Moves(MOVE_SEED_BOMB, MOVE_AQUA_TAIL); TeraType(TYPE_GRASS); }
OPPONENT(SPECIES_WOBBUFFET) { HP(1); Speed(100); TeraType(TYPE_FIRE); }
} WHEN {
TURN { }
} SCENE {
NOT MESSAGE("The opposing Wobbuffet terastilized into the Grass type!");
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI might tera if it gets saved from a ko")
{
KNOWN_FAILING; // Tests don't currently give the AI the capability to tera, even with a tera type set.
PASSES_RANDOMLY(90, 100, RNG_AI_CONSERVE_TERA);
GIVEN {
ASSUME(GetMovePower(MOVE_SEED_BOMB) == 80);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_TERA | AI_FLAG_OMNISCIENT );
PLAYER(SPECIES_WOBBUFFET) { HP(47); Speed(100); Moves(MOVE_SEED_BOMB); }
PLAYER(SPECIES_WOBBUFFET) { Speed(100); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(100); HP(30); Moves(MOVE_SEED_BOMB); TeraType(TYPE_FIRE); }
OPPONENT(SPECIES_WOBBUFFET) { Speed(100); TeraType(TYPE_FIRE); }
} WHEN {
TURN { MOVE(player, MOVE_SEED_BOMB); }
} SCENE {
MESSAGE("The opposing Wobbuffet terastilized into the Fire type!");
}
}