Add AI_FLAG_PP_STALL_PREVENTION (#6743)

Co-authored-by: Hedara <hedara90@gmail.com>
This commit is contained in:
hedara90 2025-05-02 17:30:09 +02:00 committed by GitHub
parent 1a2cd5645a
commit ccda2308a3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 111 additions and 23 deletions

View File

@ -816,6 +816,7 @@ struct BattleStruct
struct AiBattleData
{
s32 finalScore[MAX_BATTLERS_COUNT][MAX_BATTLERS_COUNT][MAX_MON_MOVES]; // AI, target, moves to make debugging easier
u8 playerStallMons[PARTY_SIZE];
u8 chosenMoveIndex[MAX_BATTLERS_COUNT];
u8 chosenTarget[MAX_BATTLERS_COUNT];
u8 actionFlee:1;

View File

@ -377,5 +377,6 @@ bool32 HasWeatherEffect(void);
u32 RestoreWhiteHerbStats(u32 battler);
bool32 IsFutureSightAttackerInParty(u32 battlerAtk, u32 battlerDef);
bool32 HadMoreThanHalfHpNowDoesnt(u32 battler);
void UpdateStallMons(void);
#endif // GUARD_BATTLE_UTIL_H

View File

@ -60,4 +60,9 @@
// AI prediction chances
#define PREDICT_SWITCH_CHANCE 50
// AI PP Stall detection chance per roll
#define PP_STALL_DISREGARD_MOVE_PERCENTAGE 50
// Score reduction if any roll for PP stall detection passes
#define PP_STALL_SCORE_REDUCTION 20
#endif // GUARD_CONFIG_AI_H

View File

@ -30,12 +30,13 @@
#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_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_COUNT 25
#define AI_FLAG_COUNT 26
// 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_PP_STALL_PREVENTION)
#define AI_FLAG_PREDICTION (AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON)
// 'other' ai logic flags

View File

@ -187,6 +187,7 @@ enum RandomTag
RNG_AI_SWITCH_TRAPPER,
RNG_AI_SWITCH_FREE_TURN,
RNG_AI_SWITCH_ALL_MOVES_BAD,
RNG_AI_PP_STALL_DISREGARD_MOVE,
RNG_SHELL_SIDE_ARM,
RNG_RANDOM_TARGET,
RNG_AI_PREDICT_ABILITY,

View File

@ -58,6 +58,7 @@ 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 AI_CheckPpStall(u32 battlerAtk, u32 battlerDef, u32 move, s32 score);
static s32 (*const sBattleAiFuncTable[])(u32, u32, u32, s32) =
{
@ -86,7 +87,7 @@ static s32 (*const sBattleAiFuncTable[])(u32, u32, u32, s32) =
[22] = NULL, // Unused
[23] = AI_PredictSwitch, // AI_FLAG_PREDICT_SWITCH
[24] = NULL, // Unused
[25] = NULL, // Unused
[25] = AI_CheckPpStall, // AI_FLAG_PP_STALL_PREVENTION
[26] = NULL, // Unused
[27] = NULL, // Unused
[28] = AI_DynamicFunc, // AI_FLAG_DYNAMIC_FUNC
@ -554,6 +555,52 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData)
AI_DATA->aiCalcInProgress = FALSE;
}
u32 GetPartyMonAbility(struct Pokemon *mon)
{
// Doesn't have any special handling yet
u32 species = GetMonData(mon, MON_DATA_SPECIES);
u32 ability = gSpeciesInfo[species].abilities[GetMonData(mon, MON_DATA_ABILITY_NUM)];
return ability;
}
static u32 PpStallReduction(u32 move, u32 battlerAtk)
{
if (move == MOVE_NONE)
return 0;
u32 tempBattleMonIndex = 0;
u32 totalStallValue = 0;
u32 returnValue = 0;
struct BattlePokemon backupBattleMon;
memcpy(&backupBattleMon, &gBattleMons[tempBattleMonIndex], sizeof(struct BattlePokemon));
for (u32 partyIndex = 0; partyIndex < PARTY_SIZE; partyIndex++)
{
u32 currentStallValue = gAiBattleData->playerStallMons[partyIndex];
if (currentStallValue == 0 || GetMonData(&gPlayerParty[partyIndex], MON_DATA_HP) == 0)
continue;
PokemonToBattleMon(&gPlayerParty[partyIndex], &gBattleMons[tempBattleMonIndex]);
u32 species = GetMonData(&gPlayerParty[partyIndex], MON_DATA_SPECIES);
u32 abilityAtk = ABILITY_NONE;
u32 abilityDef = GetPartyMonAbility(&gPlayerParty[partyIndex]);
u32 moveType = GetBattleMoveType(move); // Probably doesn't handle dynamic types right now
if (CanAbilityAbsorbMove(battlerAtk, tempBattleMonIndex, abilityDef, move, moveType, ABILITY_CHECK_TRIGGER)
|| CanAbilityBlockMove(battlerAtk, tempBattleMonIndex, abilityAtk, abilityDef, move, ABILITY_CHECK_TRIGGER)
|| (CalcPartyMonTypeEffectivenessMultiplier(move, species, abilityDef) == 0))
{
totalStallValue += currentStallValue;
}
}
for (u32 i = 0; returnValue == 0 && i < totalStallValue; i++)
{
if (RandomPercentage(RNG_AI_PP_STALL_DISREGARD_MOVE, (100 - PP_STALL_DISREGARD_MOVE_PERCENTAGE)))
returnValue = PP_STALL_SCORE_REDUCTION;
}
memcpy(&gBattleMons[tempBattleMonIndex], &backupBattleMon, sizeof(struct BattlePokemon));
return returnValue;
}
static u32 ChooseMoveOrAction_Singles(u32 battlerAi)
{
u8 currentMoveArray[MAX_MON_MOVES];
@ -5637,6 +5684,13 @@ static s32 AI_PredictSwitch(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
return score;
}
static s32 AI_CheckPpStall(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
{
if (GetBattlerSide(battlerAtk) == B_SIDE_OPPONENT)
score -= PpStallReduction(move, battlerAtk);
return score;
}
static void AI_Flee(void)
{
AI_THINKING_STRUCT->aiAction |= (AI_ACTION_DONE | AI_ACTION_FLEE | AI_ACTION_DO_NOT_ATTACK);

View File

@ -6841,6 +6841,8 @@ static void Cmd_moveend(void)
gBattleScripting.moveendState++;
break;
case MOVEEND_UPDATE_LAST_MOVES:
if (GetBattlerSide(gBattlerAttacker) == B_SIDE_OPPONENT)
UpdateStallMons();
if ((gBattleStruct->moveResultFlags[gBattlerTarget] & (MOVE_RESULT_FAILED | MOVE_RESULT_DOESNT_AFFECT_FOE))
|| (gBattleMons[gBattlerAttacker].status2 & (STATUS2_FLINCHED))
|| gProtectStructs[gBattlerAttacker].nonVolatileStatusImmobility)

View File

@ -11400,3 +11400,28 @@ static bool32 IsAnyTargetAffected(u32 battlerAtk)
}
return FALSE;
}
void UpdateStallMons(void)
{
if (IsBattlerTurnDamaged(gBattlerTarget) || IsBattlerProtected(gBattlerAttacker, gBattlerTarget, gCurrentMove) || gMovesInfo[gCurrentMove].category == DAMAGE_CATEGORY_STATUS)
return;
if (!IsDoubleBattle() || gMovesInfo[gCurrentMove].target == MOVE_TARGET_SELECTED)
{
u32 moveType = GetBattleMoveType(gCurrentMove); // Probably doesn't handle dynamic move types right now
u32 abilityAtk = GetBattlerAbility(gBattlerAttacker);
u32 abilityDef = GetBattlerAbility(gBattlerTarget);
if (CanAbilityAbsorbMove(gBattlerAttacker, gBattlerTarget, abilityDef, gCurrentMove, moveType, ABILITY_CHECK_TRIGGER))
{
gAiBattleData->playerStallMons[gBattlerPartyIndexes[gBattlerTarget]]++;
}
else if (CanAbilityBlockMove(gBattlerAttacker, gBattlerTarget, abilityAtk, abilityDef, gCurrentMove, ABILITY_CHECK_TRIGGER))
{
gAiBattleData->playerStallMons[gBattlerPartyIndexes[gBattlerTarget]]++;
}
else if (AI_GetMoveEffectiveness(gCurrentMove, gBattlerAttacker, gBattlerTarget) == 0)
{
gAiBattleData->playerStallMons[gBattlerPartyIndexes[gBattlerTarget]]++;
}
}
// Handling for moves that target multiple opponents in doubles not handled currently
}

View File

@ -0,0 +1,18 @@
#include "global.h"
#include "test/battle.h"
#include "battle_ai_util.h"
AI_SINGLE_BATTLE_TEST("AI_FLAG_PP_STALL_PREVENTION: AI will stop using moves that has hit into immunities due to switches sometimes")
{
PASSES_RANDOMLY(PP_STALL_DISREGARD_MOVE_PERCENTAGE, 100, RNG_AI_PP_STALL_DISREGARD_MOVE);
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_PP_STALL_PREVENTION);
PLAYER(SPECIES_RATICATE);
PLAYER(SPECIES_GENGAR);
OPPONENT(SPECIES_KARTANA) { Moves(MOVE_SHADOW_CLAW, MOVE_SACRED_SWORD, MOVE_ROCK_SLIDE); }
} WHEN {
TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_SACRED_SWORD); }
TURN { SWITCH(player, 0); EXPECT_MOVE(opponent, MOVE_SHADOW_CLAW); }
TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_SACRED_SWORD); }
}
}

View File

@ -1,26 +1,6 @@
#include "global.h"
#include "test/battle.h"
AI_SINGLE_BATTLE_TEST("AI gets baited by Protect Switch tactics") // This behavior is to be fixed.
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING);
PLAYER(SPECIES_STUNFISK);
PLAYER(SPECIES_PELIPPER);
OPPONENT(SPECIES_DARKRAI) { Moves(MOVE_TACKLE, MOVE_PECK, MOVE_EARTHQUAKE, MOVE_THUNDERBOLT); }
OPPONENT(SPECIES_SCIZOR) { Moves(MOVE_HYPER_BEAM, MOVE_FACADE, MOVE_GIGA_IMPACT, MOVE_EXTREME_SPEED); }
} WHEN {
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake
TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt
TURN { SWITCH(player, 0); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake
TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE);} // E-quake
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt
}
}
// General switching behaviour
AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill")
{