Add AI_FLAG_PREDICT_MOVES (#6551)

This commit is contained in:
Alex 2025-05-12 23:10:10 +02:00 committed by GitHub
commit 006abe097e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 132 additions and 78 deletions

View File

@ -180,5 +180,8 @@ 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_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`.
## `AI_FLAG_PP_STALL_PREVENTION`
This flag aims to prevent the player from PP stalling the AI by switching between immunities. The AI mon's move scores will slowly decay for absorbed moves over time, eventually making its moves unpredictable. More detailed control for this behaviour can be customized in the `ai.h` config file.

View File

@ -331,12 +331,14 @@ struct AiLogicData
u8 weatherHasEffect:1; // The same as HasWeatherEffect(). 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 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 predictingSwitch:1; // Determines whether AI will use switch predictions this turn or not
u8 predictingMove:1; // Determines whether AI will use move predictions this turn or not
u8 aiPredictionInProgress:1; // Tracks whether the AI is in the middle of running prediction calculations
u8 padding:2;
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
u16 predictedMove[MAX_BATTLERS_COUNT];
};
struct AI_ThinkingStruct

View File

@ -240,5 +240,6 @@ bool32 IsBattlerItemEnabled(u32 battler);
bool32 IsBattlerPredictedToSwitch(u32 battler);
bool32 HasLowAccuracyMove(u32 battlerAtk, u32 battlerDef);
bool32 HasBattlerSideAbility(u32 battlerDef, u32 ability, struct AiLogicData *aiData);
u32 GetThinkingBattler(u32 battler);
#endif //GUARD_BATTLE_AI_UTIL_H

View File

@ -62,6 +62,7 @@
// AI prediction chances
#define PREDICT_SWITCH_CHANCE 50
#define PREDICT_MOVE_CHANCE 100
// AI PP Stall detection chance per roll
#define PP_STALL_DISREGARD_MOVE_PERCENTAGE 50

View File

@ -31,16 +31,18 @@
#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_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
// Flags at and after 32 need different formatting, as in
// #define AI_FLAG_PLACEHOLDER ((u64)1 << 32)
#define AI_FLAG_COUNT 26
#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 | AI_FLAG_PP_STALL_PREVENTION)
#define AI_FLAG_PREDICTION (AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON)
#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 | AI_FLAG_PREDICT_MOVE)
// 'other' ai logic flags
#define AI_FLAG_DYNAMIC_FUNC ((u64)1 << 60) // Create custom AI functions for specific battles via "setdynamicaifunc" cmd

View File

@ -196,6 +196,7 @@ enum RandomTag
RNG_RANDOM_TARGET,
RNG_AI_PREDICT_ABILITY,
RNG_AI_PREDICT_SWITCH,
RNG_AI_PREDICT_MOVE,
RNG_AI_STATUS_FOCUS_PUNCH,
RNG_HEALER,
RNG_DEXNAV_ENCOUNTER_LEVEL,

View File

@ -32,10 +32,10 @@
#define AI_ACTION_WATCH (1 << 2)
#define AI_ACTION_DO_NOT_ATTACK (1 << 3)
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 u32 ChooseMoveOrAction_Singles(u32 battler);
static u32 ChooseMoveOrAction_Doubles(u32 battler);
static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battlerAtk, u32 battlerDef);
static inline void BattleAI_DoAIProcessing_PredictedSwitchin(struct AI_ThinkingStruct *aiThink, struct AiLogicData *aiData, u32 battlerAtk, u32 battlerDef);
static bool32 IsPinchBerryItemEffect(enum ItemHoldEffect holdEffect);
// ewram
@ -271,8 +271,10 @@ void BattleAI_SetupFlags(void)
void BattleAI_SetupAIData(u8 defaultScoreMoves, u32 battler)
{
u32 moveLimitations;
u32 moveLimitations, moveLimitationsTarget;
u32 defaultScoreMovesTarget = defaultScoreMoves;
u64 flags[MAX_BATTLERS_COUNT];
u32 moveIndex;
// Clear AI data but preserve the flags.
memcpy(&flags[0], &AI_THINKING_STRUCT->aiFlags[0], sizeof(u64) * MAX_BATTLERS_COUNT);
@ -282,7 +284,7 @@ void BattleAI_SetupAIData(u8 defaultScoreMoves, u32 battler)
moveLimitations = AI_DATA->moveLimitations[battler];
// Conditional score reset, unlike Ruby.
for (u32 moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
{
if (moveLimitations & (1u << moveIndex))
SET_SCORE(battler, moveIndex, 0);
@ -296,6 +298,25 @@ void BattleAI_SetupAIData(u8 defaultScoreMoves, u32 battler)
gBattlerTarget = SetRandomTarget(battler);
gAiBattleData->chosenTarget[battler] = gBattlerTarget;
// Initialize move prediction scores
if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_MOVE)
{
u32 opposingBattler = GetOppositeBattler(battler);
moveLimitationsTarget = AI_DATA->moveLimitations[opposingBattler];
for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
{
if (moveLimitationsTarget & (1u << moveIndex))
SET_SCORE(opposingBattler, moveIndex, 0);
if (defaultScoreMovesTarget & 1)
SET_SCORE(opposingBattler, moveIndex, AI_SCORE_DEFAULT);
else
SET_SCORE(opposingBattler, moveIndex, 0);
defaultScoreMovesTarget >>= 1;
}
}
}
bool32 BattlerChoseNonMoveAction(void)
@ -318,24 +339,32 @@ bool32 BattlerChoseNonMoveAction(void)
void SetupAIPredictionData(u32 battler, enum SwitchType switchType)
{
s32 opposingBattler = GetOppositeBattler(battler);
AI_DATA->aiPredictionInProgress = TRUE;
AI_DATA->battlerDoingPrediction = battler;
// Switch prediction
if ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_SWITCH))
{
AI_DATA->aiSwitchPredictionInProgress = TRUE;
AI_DATA->battlerDoingPrediction = battler;
AI_DATA->mostSuitableMonId[opposingBattler] = GetMostSuitableMonToSwitchInto(opposingBattler, switchType);
if (ShouldSwitch(opposingBattler))
AI_DATA->shouldSwitch |= (1u << opposingBattler);
AI_DATA->aiSwitchPredictionInProgress = FALSE;
gBattleStruct->prevTurnSpecies[opposingBattler] = gBattleMons[opposingBattler].species;
// Determine whether AI will use predictions this turn
AI_DATA->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, PREDICT_SWITCH_CHANCE);
}
// TODO Move prediction
// ModifySwitchAfterMoveScoring(opposingBattler);
// Move prediction
if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_MOVE)
{
AI_DATA->predictedMove[opposingBattler] = gBattleMons[opposingBattler].moves[BattleAI_ChooseMoveIndex(opposingBattler)];
DebugPrintf("Predicted move: %d", AI_DATA->predictedMove[opposingBattler]);
ModifySwitchAfterMoveScoring(opposingBattler);
// Determine whether AI will use predictions this turn
AI_DATA->predictingMove = RandomPercentage(RNG_AI_PREDICT_MOVE, PREDICT_MOVE_CHANCE);
}
AI_DATA->aiPredictionInProgress = FALSE;
}
void ComputeBattlerDecisions(u32 battler)
@ -642,23 +671,23 @@ static u32 PpStallReduction(u32 move, u32 battlerAtk)
return returnValue;
}
static u32 ChooseMoveOrAction_Singles(u32 battlerAi)
static u32 ChooseMoveOrAction_Singles(u32 battler)
{
u8 currentMoveArray[MAX_MON_MOVES];
u8 consideredMoveArray[MAX_MON_MOVES];
u32 numOfBestMoves;
s32 i;
u64 flags = AI_THINKING_STRUCT->aiFlags[battlerAi];
u64 flags = AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)];
AI_DATA->partnerMove = 0; // no ally
while (flags != 0)
{
if (flags & 1)
{
if (IsBattlerPredictedToSwitch(gBattlerTarget) && (AI_THINKING_STRUCT->aiFlags[battlerAi] & AI_FLAG_PREDICT_INCOMING_MON))
BattleAI_DoAIProcessing_PredictedSwitchin(AI_THINKING_STRUCT, AI_DATA, battlerAi, gBattlerTarget);
if (IsBattlerPredictedToSwitch(gBattlerTarget) && (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_INCOMING_MON))
BattleAI_DoAIProcessing_PredictedSwitchin(AI_THINKING_STRUCT, AI_DATA, battler, gBattlerTarget);
else
BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget);
BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battler, gBattlerTarget);
}
flags >>= (u64)1;
AI_THINKING_STRUCT->aiLogicId++;
@ -666,7 +695,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi)
for (i = 0; i < MAX_MON_MOVES; i++)
{
gAiBattleData->finalScore[battlerAi][gBattlerTarget][i] = AI_THINKING_STRUCT->score[i];
gAiBattleData->finalScore[battler][gBattlerTarget][i] = AI_THINKING_STRUCT->score[i];
}
numOfBestMoves = 1;
@ -675,7 +704,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi)
for (i = 1; i < MAX_MON_MOVES; i++)
{
if (gBattleMons[battlerAi].moves[i] != MOVE_NONE)
if (gBattleMons[battler].moves[i] != MOVE_NONE)
{
// In ruby, the order of these if statements is reversed.
if (currentMoveArray[0] == AI_THINKING_STRUCT->score[i])
@ -694,7 +723,7 @@ static u32 ChooseMoveOrAction_Singles(u32 battlerAi)
return consideredMoveArray[Random() % numOfBestMoves];
}
static u32 ChooseMoveOrAction_Doubles(u32 battlerAi)
static u32 ChooseMoveOrAction_Doubles(u32 battler)
{
s32 i, j;
u64 flags;
@ -709,7 +738,7 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi)
for (i = 0; i < MAX_BATTLERS_COUNT; i++)
{
if (i == battlerAi || gBattleMons[i].hp == 0)
if (i == battler || gBattleMons[i].hp == 0)
{
actionOrMoveIndex[i] = 0xFF;
bestMovePointsForTarget[i] = -1;
@ -717,25 +746,25 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi)
else
{
if (gBattleTypeFlags & BATTLE_TYPE_PALACE)
BattleAI_SetupAIData(gBattleStruct->palaceFlags >> 4, battlerAi);
BattleAI_SetupAIData(gBattleStruct->palaceFlags >> 4, battler);
else
BattleAI_SetupAIData(0xF, battlerAi);
BattleAI_SetupAIData(0xF, battler);
gBattlerTarget = i;
AI_DATA->partnerMove = GetAllyChosenMove(battlerAi);
AI_DATA->partnerMove = GetAllyChosenMove(battler);
AI_THINKING_STRUCT->aiLogicId = 0;
AI_THINKING_STRUCT->movesetIndex = 0;
flags = AI_THINKING_STRUCT->aiFlags[battlerAi];
flags = AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)];
while (flags != 0)
{
if (flags & 1)
{
if (IsBattlerPredictedToSwitch(gBattlerTarget) && (AI_THINKING_STRUCT->aiFlags[battlerAi] & AI_FLAG_PREDICT_INCOMING_MON))
BattleAI_DoAIProcessing_PredictedSwitchin(AI_THINKING_STRUCT, AI_DATA, battlerAi, gBattlerTarget);
if (IsBattlerPredictedToSwitch(gBattlerTarget) && (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_INCOMING_MON))
BattleAI_DoAIProcessing_PredictedSwitchin(AI_THINKING_STRUCT, AI_DATA, battler, gBattlerTarget);
else
BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battlerAi, gBattlerTarget);
BattleAI_DoAIProcessing(AI_THINKING_STRUCT, battler, gBattlerTarget);
}
flags >>= (u64)1;
AI_THINKING_STRUCT->aiLogicId++;
@ -746,9 +775,9 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi)
mostViableMovesNo = 1;
for (j = 1; j < MAX_MON_MOVES; j++)
{
if (gBattleMons[battlerAi].moves[j] != 0)
if (gBattleMons[battler].moves[j] != 0)
{
if (!CanTargetBattler(battlerAi, i, gBattleMons[battlerAi].moves[j]))
if (!CanTargetBattler(battler, i, gBattleMons[battler].moves[j]))
continue;
if (mostViableMovesScores[0] == AI_THINKING_STRUCT->score[j])
@ -769,14 +798,14 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi)
bestMovePointsForTarget[i] = mostViableMovesScores[0];
// Don't use a move against ally if it has less than 100 points.
if (i == BATTLE_PARTNER(battlerAi) && bestMovePointsForTarget[i] < AI_SCORE_DEFAULT)
if (i == BATTLE_PARTNER(battler) && bestMovePointsForTarget[i] < AI_SCORE_DEFAULT)
{
bestMovePointsForTarget[i] = -1;
}
for (j = 0; j < MAX_MON_MOVES; j++)
{
gAiBattleData->finalScore[battlerAi][gBattlerTarget][j] = AI_THINKING_STRUCT->score[j];
gAiBattleData->finalScore[battler][gBattlerTarget][j] = AI_THINKING_STRUCT->score[j];
}
}
}
@ -801,7 +830,7 @@ static u32 ChooseMoveOrAction_Doubles(u32 battlerAi)
}
gBattlerTarget = mostViableTargetsArray[Random() % mostViableTargetsNo];
gAiBattleData->chosenTarget[battlerAi] = gBattlerTarget;
gAiBattleData->chosenTarget[battler] = gBattlerTarget;
return actionOrMoveIndex[gBattlerTarget];
}
@ -816,26 +845,26 @@ static inline bool32 ShouldConsiderMoveForBattler(u32 battlerAi, u32 battlerDef,
return TRUE;
}
static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battlerAi, u32 battlerDef)
static inline void BattleAI_DoAIProcessing(struct AI_ThinkingStruct *aiThink, u32 battlerAtk, u32 battlerDef)
{
do
{
if (gBattleMons[battlerAi].pp[aiThink->movesetIndex] == 0)
if (gBattleMons[battlerAtk].pp[aiThink->movesetIndex] == 0)
aiThink->moveConsidered = MOVE_NONE;
else
aiThink->moveConsidered = gBattleMons[battlerAi].moves[aiThink->movesetIndex];
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(battlerAi, battlerDef, aiThink->moveConsidered))
&& ShouldConsiderMoveForBattler(battlerAtk, battlerDef, aiThink->moveConsidered))
{
if (aiThink->aiLogicId < ARRAY_COUNT(sBattleAiFuncTable)
&& sBattleAiFuncTable[aiThink->aiLogicId] != NULL)
{
// Call AI function
aiThink->score[aiThink->movesetIndex] =
sBattleAiFuncTable[aiThink->aiLogicId](battlerAi,
sBattleAiFuncTable[aiThink->aiLogicId](battlerAtk,
battlerDef,
aiThink->moveConsidered,
aiThink->score[aiThink->movesetIndex]);
@ -974,7 +1003,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
bool32 isDoubleBattle = IsValidDoubleBattle(battlerAtk);
u32 i;
u32 weather;
u32 predictedMove = aiData->lastUsedMove[battlerDef];
u32 predictedMove = ((AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_PREDICT_MOVE) && aiData->predictingMove) ? AI_DATA->predictedMove[battlerDef] : aiData->lastUsedMove[battlerDef];
u32 abilityAtk = aiData->abilities[battlerAtk];
u32 abilityDef = aiData->abilities[battlerDef];
s32 atkPriority = GetBattleMovePriority(battlerAtk, abilityAtk, move);
@ -997,8 +1026,8 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
if (gBattleStruct->battlerState[battlerDef].commandingDondozo)
RETURN_SCORE_MINUS(20);
// Don't setup into expected Focus Punch. Revisit alongside predictedMove with move prediction
if (GetMoveCategory(move) == DAMAGE_CATEGORY_STATUS && moveEffect != EFFECT_SLEEP
// Don't setup into expected Focus Punch.
if (GetMoveCategory(move) == DAMAGE_CATEGORY_STATUS && moveEffect != EFFECT_SLEEP && GetMoveEffect(predictedMove) != EFFECT_FOCUS_PUNCH
&& GetMoveEffect(GetBestDmgMoveFromBattler(battlerDef, battlerAtk, AI_DEFENDING)) == EFFECT_FOCUS_PUNCH && RandomPercentage(RNG_AI_STATUS_FOCUS_PUNCH, STATUS_MOVE_FOCUS_PUNCH_CHANCE))
{
RETURN_SCORE_MINUS(20);
@ -2903,7 +2932,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
bool32 partnerProtecting = (partnerEffect == EFFECT_PROTECT);
bool32 attackerHasBadAbility = (gAbilitiesInfo[aiData->abilities[battlerAtk]].aiRating < 0);
bool32 partnerHasBadAbility = (gAbilitiesInfo[atkPartnerAbility].aiRating < 0);
u32 predictedMove = aiData->lastUsedMove[battlerDef];
u32 predictedMove = ((AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_PREDICT_MOVE) && aiData->predictingMove) ? AI_DATA->predictedMove[battlerDef] : aiData->lastUsedMove[battlerDef];
SetTypeBeforeUsingMove(move, battlerAtk);
moveType = GetBattleMoveType(move);
@ -3474,7 +3503,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move)
uq4_12_t effectiveness = aiData->effectiveness[battlerAtk][battlerDef][movesetIndex];
s32 score = 0;
u32 predictedMove = aiData->lastUsedMove[battlerDef];
u32 predictedMove = ((AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_PREDICT_MOVE) && aiData->predictingMove) ? AI_DATA->predictedMove[battlerDef] : aiData->lastUsedMove[battlerDef];
u32 predictedType = GetMoveType(predictedMove);
u32 predictedMoveSlot = GetMoveSlot(GetMovesArray(battlerDef), predictedMove);
bool32 isDoubleBattle = IsValidDoubleBattle(battlerAtk);
@ -5582,6 +5611,7 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
enum BattleMoveEffects moveEffect = GetMoveEffect(move);
struct AiLogicData *aiData = AI_DATA;
uq4_12_t effectiveness = aiData->effectiveness[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex];
u32 predictedMove = ((AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_PREDICT_MOVE) && aiData->predictingMove) ? AI_DATA->predictedMove[battlerDef] : aiData->lastUsedMove[battlerDef];
// Switch benefit
switch (moveEffect)
@ -5593,8 +5623,8 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
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);
else if (IsSwitchOutEffect(GetMoveEffect(predictedMove)) && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER) // Pursuit against fast U-Turn
ADJUST_SCORE(DECENT_EFFECT);
break;
}

View File

@ -108,13 +108,6 @@ u32 GetSwitchChance(enum ShouldSwitchScenario shouldSwitchScenario)
}
}
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[GetThinkingBattler(battler)] & AI_FLAG_ACE_POKEMON
@ -295,7 +288,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, (100 - GetSwitchChance(SHOULD_SWITCH_HASBADODDS))) && !AI_DATA->aiSwitchPredictionInProgress)
if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, (100 - GetSwitchChance(SHOULD_SWITCH_HASBADODDS))) && !AI_DATA->aiPredictionInProgress)
return FALSE;
// Switch mon out
@ -315,7 +308,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler)
return FALSE;
// 50% chance to stay in regardless
if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, (100 - GetSwitchChance(SHOULD_SWITCH_HASBADODDS))) && !AI_DATA->aiSwitchPredictionInProgress)
if (RandomPercentage(RNG_AI_SWITCH_HASBADODDS, (100 - GetSwitchChance(SHOULD_SWITCH_HASBADODDS))) && !AI_DATA->aiPredictionInProgress)
return FALSE;
// Switch mon out
@ -456,18 +449,16 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler)
struct Pokemon *party;
u16 monAbility, aiMove;
u32 opposingBattler = GetOppositeBattler(battler);
u32 incomingMove = AI_DATA->lastUsedMove[opposingBattler];
u32 incomingMove = ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_MOVE) && AI_DATA->predictingMove) ? AI_DATA->predictedMove[opposingBattler] : AI_DATA->lastUsedMove[opposingBattler];
u32 incomingType = GetMoveType(incomingMove);
u32 predictedMove = incomingMove; // Update for move prediction
u32 predictedType = GetMoveType(predictedMove);
bool32 isOpposingBattlerChargingOrInvulnerable = (IsSemiInvulnerable(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove));
s32 i, j;
if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING))
return FALSE;
if (gBattleStruct->prevTurnSpecies[battler] != gBattleMons[battler].species) // AI mon has changed, player's behaviour no longer reliable; note to override this if using AI_FLAG_PREDICT_MOVE
if (gBattleStruct->prevTurnSpecies[battler] != gBattleMons[battler].species && !(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_MOVE)) // AI mon has changed, player's behaviour no longer reliable; override this if using AI_FLAG_PREDICT_MOVE
return FALSE;
if (HasSuperEffectiveMoveAgainstOpponents(battler, TRUE) && (RandomPercentage(RNG_AI_SWITCH_ABSORBING_STAY_IN, STAY_IN_ABSORBING_PERCENTAGE) || AI_DATA->aiSwitchPredictionInProgress))
if (HasSuperEffectiveMoveAgainstOpponents(battler, TRUE) && (RandomPercentage(RNG_AI_SWITCH_ABSORBING_STAY_IN, STAY_IN_ABSORBING_PERCENTAGE) || AI_DATA->aiPredictionInProgress))
return FALSE;
if (AreStatsRaised(battler))
return FALSE;
@ -502,38 +493,38 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler)
}
// Create an array of possible absorb abilities so the AI considers all of them
if (predictedType == TYPE_FIRE)
if (incomingType == TYPE_FIRE)
{
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_FLASH_FIRE;
}
else if (predictedType == TYPE_WATER || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_WATER))
else if (incomingType == TYPE_WATER || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_WATER))
{
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_WATER_ABSORB;
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_DRY_SKIN;
if (B_REDIRECT_ABILITY_IMMUNITY >= GEN_5)
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_STORM_DRAIN;
}
else if (predictedType == TYPE_ELECTRIC || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_ELECTRIC))
else if (incomingType == TYPE_ELECTRIC || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_ELECTRIC))
{
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_VOLT_ABSORB;
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_MOTOR_DRIVE;
if (B_REDIRECT_ABILITY_IMMUNITY >= GEN_5)
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_LIGHTNING_ROD;
}
else if (predictedType == TYPE_GRASS || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GRASS))
else if (incomingType == TYPE_GRASS || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GRASS))
{
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_SAP_SIPPER;
}
else if (predictedType == TYPE_GROUND || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GROUND))
else if (incomingType == TYPE_GROUND || (isOpposingBattlerChargingOrInvulnerable && incomingType == TYPE_GROUND))
{
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_EARTH_EATER;
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_LEVITATE;
}
else if (IsSoundMove(predictedMove) || (isOpposingBattlerChargingOrInvulnerable && IsSoundMove(incomingMove)))
else if (IsSoundMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsSoundMove(incomingMove)))
{
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_SOUNDPROOF;
}
else if (IsWindMove(predictedMove) || (isOpposingBattlerChargingOrInvulnerable && IsWindMove(incomingMove)))
else if (IsWindMove(incomingMove) || (isOpposingBattlerChargingOrInvulnerable && IsWindMove(incomingMove)))
{
absorbingTypeAbilities[numAbsorbingAbilities++] = ABILITY_WIND_RIDER;
}
@ -583,7 +574,7 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler)
static bool32 ShouldSwitchIfOpponentChargingOrInvulnerable(u32 battler)
{
u32 opposingBattler = GetOppositeBattler(battler);
u32 incomingMove = AI_DATA->lastUsedMove[opposingBattler];
u32 incomingMove = ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_MOVE) && AI_DATA->predictingMove) ? AI_DATA->predictedMove[opposingBattler] : AI_DATA->lastUsedMove[opposingBattler];
bool32 isOpposingBattlerChargingOrInvulnerable = (IsSemiInvulnerable(opposingBattler, incomingMove) || IsTwoTurnNotSemiInvulnerableMove(opposingBattler, incomingMove));
if (IsDoubleBattle() || !(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING))
@ -905,7 +896,7 @@ static bool32 FindMonWithFlagsAndSuperEffective(u32 battler, u16 flags, u32 perc
if (move == 0)
continue;
if (AI_GetMoveEffectiveness(move, battler, battlerIn1) >= UQ_4_12(2.0) && (RandomPercentage(RNG_AI_SWITCH_SE_DEFENSIVE, percentChance) || AI_DATA->aiSwitchPredictionInProgress))
if (AI_GetMoveEffectiveness(move, battler, battlerIn1) >= UQ_4_12(2.0) && (RandomPercentage(RNG_AI_SWITCH_SE_DEFENSIVE, percentChance) || AI_DATA->aiPredictionInProgress))
return SetSwitchinAndSwitch(battler, i);
}
}
@ -997,7 +988,7 @@ static bool32 ShouldSwitchIfEncored(u32 battler)
return FALSE;
// Switch out 50% of the time otherwise
else if ((RandomPercentage(RNG_AI_SWITCH_ENCORE, GetSwitchChance(SHOULD_SWITCH_ENCORE_DAMAGE)) || AI_DATA->aiSwitchPredictionInProgress) && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE)
else if ((RandomPercentage(RNG_AI_SWITCH_ENCORE, GetSwitchChance(SHOULD_SWITCH_ENCORE_DAMAGE)) || AI_DATA->aiPredictionInProgress) && AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE)
return SetSwitchinAndSwitch(battler, PARTY_SIZE);
return FALSE;
@ -1043,7 +1034,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, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO)) || AI_DATA->aiSwitchPredictionInProgress))
if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO)) || AI_DATA->aiPredictionInProgress))
return SetSwitchinAndSwitch(battler, PARTY_SIZE);
}
// If at -3 or worse, switch out regardless
@ -1060,7 +1051,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, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO)) || AI_DATA->aiSwitchPredictionInProgress))
if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (RandomPercentage(RNG_AI_SWITCH_STATS_LOWERED, GetSwitchChance(SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO)) || AI_DATA->aiPredictionInProgress))
return SetSwitchinAndSwitch(battler, PARTY_SIZE);
}
// If at -3 or worse, switch out regardless

View File

@ -1135,7 +1135,7 @@ s32 AI_WhoStrikesFirst(u32 battlerAI, u32 battler, u32 moveConsidered)
u32 abilityAI = AI_DATA->abilities[battlerAI];
u32 abilityPlayer = AI_DATA->abilities[battler];
u32 predictedMove = AI_DATA->lastUsedMove[battler]; // TODO update for move prediction
u32 predictedMove = ((AI_THINKING_STRUCT->aiFlags[battlerAI] & AI_FLAG_PREDICT_MOVE) && AI_DATA->predictingMove) ? AI_DATA->predictedMove[battler] : AI_DATA->lastUsedMove[battler];
s8 aiPriority = GetBattleMovePriority(battlerAI, abilityAI, moveConsidered);
s8 playerPriority = GetBattleMovePriority(battler, abilityPlayer, predictedMove);
@ -4523,3 +4523,10 @@ bool32 HasBattlerSideAbility(u32 battler, u32 ability, struct AiLogicData *aiDat
return TRUE;
return FALSE;
}
u32 GetThinkingBattler(u32 battler)
{
if (AI_DATA->aiPredictionInProgress)
return AI_DATA->battlerDoingPrediction;
return battler;
}

View File

@ -0,0 +1,16 @@
#include "global.h"
#include "test/battle.h"
#include "battle_ai_util.h"
AI_SINGLE_BATTLE_TEST("AI_FLAG_PREDICT_MOVE: AI will predict player's move")
{
PASSES_RANDOMLY(PREDICT_MOVE_CHANCE, 100, RNG_AI_PREDICT_MOVE);
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_MOVE);
PLAYER(SPECIES_VAPOREON) { Ability(ABILITY_WATER_ABSORB); Moves(MOVE_SURF, MOVE_TACKLE); }
OPPONENT(SPECIES_NUMEL) { Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_VAPOREON) { Ability(ABILITY_WATER_ABSORB); Moves(MOVE_TACKLE); }
} WHEN {
TURN { MOVE(player, MOVE_SURF); EXPECT_SWITCH(opponent, 1); }
}
}