Adjust switch AI based on move scoring (#6615)

This commit is contained in:
Pawkkie 2025-05-05 18:39:44 -04:00 committed by GitHub
parent 3f5335c4ba
commit c8fa4442d9
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 149 additions and 28 deletions

View File

@ -32,6 +32,7 @@ enum ShouldSwitchScenario
SHOULD_SWITCH_CHOICE_LOCKED,
SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO,
SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS,
SHOULD_SWITCH_ALL_SCORES_BAD,
};
enum SwitchType
@ -45,5 +46,6 @@ void AI_TrySwitchOrUseItem(u32 battler);
u32 GetMostSuitableMonToSwitchInto(u32 battler, enum SwitchType switchType);
bool32 ShouldSwitch(u32 battler);
bool32 IsMonGrounded(u16 heldItemEffect, u32 ability, u8 type1, u8 type2);
void ModifySwitchAfterMoveScoring(u32 battler);
#endif // GUARD_BATTLE_AI_SWITCH_ITEMS_H

View File

@ -22,6 +22,7 @@
#define SHOULD_SWITCH_CHOICE_LOCKED_PERCENTAGE 100 // Only if locked into status move
#define SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO_PERCENTAGE 50
#define SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS_PERCENTAGE 100
#define SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE 100
// AI smart switching chances for bad statuses
#define SHOULD_SWITCH_PERISH_SONG_PERCENTAGE 100
@ -46,6 +47,8 @@
// AI switchin considerations
#define ALL_MOVES_BAD_STATUS_MOVES_BAD FALSE // If the AI has no moves that affect the target, ShouldSwitchIfAllMovesBad can prompt a switch. Enabling this config will ignore status moves that can affect the target when making this decision.
#define AI_BAD_SCORE_THRESHOLD 90 // Move scores beneath this threshold are considered "bad" when deciding switching
#define AI_GOOD_SCORE_THRESHOLD 100 // Move scores above this threshold are considered "good" when deciding switching
// AI held item-based move scoring
#define LOW_ACCURACY_THRESHOLD 75 // Moves with accuracy equal OR below this value are considered low accuracy

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_SWITCH_ALL_SCORES_BAD,
RNG_AI_PP_STALL_DISREGARD_MOVE,
RNG_SHELL_SIDE_ARM,
RNG_RANDOM_TARGET,

View File

@ -283,11 +283,11 @@ bool32 BattlerChoseNonMoveAction(void)
return FALSE;
}
void SetupAISwitchingData(u32 battler, enum SwitchType switchType)
void SetupAIPredictionData(u32 battler, enum SwitchType switchType)
{
s32 opposingBattler = GetOppositeBattler(battler);
// AI's predicting data
// Switch prediction
if ((AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_PREDICT_SWITCH))
{
AI_DATA->aiSwitchPredictionInProgress = TRUE;
@ -302,11 +302,8 @@ void SetupAISwitchingData(u32 battler, enum SwitchType switchType)
AI_DATA->predictingSwitch = RandomPercentage(RNG_AI_PREDICT_SWITCH, PREDICT_SWITCH_CHANCE);
}
// AI's data
AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, switchType);
if (ShouldSwitch(battler))
AI_DATA->shouldSwitch |= (1u << battler);
gBattleStruct->prevTurnSpecies[battler] = gBattleMons[battler].species;
// TODO Move prediction
// ModifySwitchAfterMoveScoring(opposingBattler);
}
void ComputeBattlerDecisions(u32 battler)
@ -323,9 +320,21 @@ void ComputeBattlerDecisions(u32 battler)
enum SwitchType switchType = (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_RISKY) ? SWITCH_AFTER_KO : SWITCH_MID_BATTLE;
AI_DATA->aiCalcInProgress = TRUE;
// Setup battler and prediction data
BattleAI_SetupAIData(0xF, battler);
SetupAISwitchingData(battler, switchType);
SetupAIPredictionData(battler, switchType);
// AI's own switching data
AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, switchType);
if (ShouldSwitch(battler))
AI_DATA->shouldSwitch |= (1u << battler);
gBattleStruct->prevTurnSpecies[battler] = gBattleMons[battler].species;
// AI's move scoring
gAiBattleData->chosenMoveIndex[battler] = BattleAI_ChooseMoveIndex(battler); // Calculate score and chose move index
ModifySwitchAfterMoveScoring(battler);
AI_DATA->aiCalcInProgress = FALSE;
}
}

View File

@ -101,6 +101,8 @@ u32 GetSwitchChance(enum ShouldSwitchScenario shouldSwitchScenario)
return SHOULD_SWITCH_ATTACKING_STAT_MINUS_TWO_PERCENTAGE;
case SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS:
return SHOULD_SWITCH_ATTACKING_STAT_MINUS_THREE_PLUS_PERCENTAGE;
case SHOULD_SWITCH_ALL_SCORES_BAD:
return SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE;
default:
return 100;
}
@ -1063,24 +1065,6 @@ static bool32 ShouldSwitchIfAttackingStatsLowered(u32 battler)
return FALSE;
}
static bool32 HasGoodSubstituteMove(u32 battler)
{
int i;
u32 aiMove, opposingBattler = GetOppositeBattler(battler);
enum BattleMoveEffects aiMoveEffect;
for (i = 0; i < MAX_MON_MOVES; i++)
{
aiMove = gBattleMons[battler].moves[i];
aiMoveEffect = GetMoveEffect(aiMove);
if (IsSubstituteEffect(aiMoveEffect))
{
if (IncreaseSubstituteMoveScore(battler, opposingBattler, aiMove) > 0)
return TRUE;
}
}
return FALSE;
}
bool32 ShouldSwitch(u32 battler)
{
u32 battlerIn1, battlerIn2;
@ -1154,8 +1138,6 @@ bool32 ShouldSwitch(u32 battler)
return TRUE;
if ((AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING) && (CanMonSurviveHazardSwitchin(battler) == FALSE))
return FALSE;
if (HasGoodSubstituteMove(battler))
return FALSE;
if (ShouldSwitchIfTrapperInParty(battler))
return TRUE;
if (FindMonThatAbsorbsOpponentsMove(battler))
@ -1198,6 +1180,109 @@ bool32 ShouldSwitch(u32 battler)
return FALSE;
}
bool32 ShouldSwitchIfAllScoresBad(u32 battler)
{
u32 i, score, opposingBattler = GetOppositeBattler(battler);
if (!(AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SMART_SWITCHING))
return FALSE;
for (i = 0; i < MAX_MON_MOVES; i++)
{
score = gAiBattleData->finalScore[battler][opposingBattler][i];
if (score > AI_BAD_SCORE_THRESHOLD)
return FALSE;
}
if (RandomPercentage(RNG_AI_SWITCH_ALL_SCORES_BAD, GetSwitchChance(SHOULD_SWITCH_ALL_SCORES_BAD)))
return TRUE;
return FALSE;
}
bool32 ShouldStayInToUseMove(u32 battler)
{
u32 i, aiMove, opposingBattler = GetOppositeBattler(battler);
enum BattleMoveEffects aiMoveEffect;
for (i = 0; i < MAX_MON_MOVES; i++)
{
aiMove = gBattleMons[battler].moves[i];
aiMoveEffect = GetMoveEffect(aiMove);
if (aiMoveEffect == EFFECT_REVIVAL_BLESSING || IsSwitchOutEffect(aiMoveEffect))
{
if (gAiBattleData->finalScore[battler][opposingBattler][i] > AI_GOOD_SCORE_THRESHOLD)
return TRUE;
}
}
return FALSE;
}
void ModifySwitchAfterMoveScoring(u32 battler)
{
u32 battlerIn1, battlerIn2;
s32 firstId;
s32 lastId; // + 1
struct Pokemon *party;
s32 i;
s32 availableToSwitch;
if (gBattleMons[battler].status2 & (STATUS2_WRAPPED | STATUS2_ESCAPE_PREVENTION))
return;
if (gStatuses3[battler] & STATUS3_ROOTED)
return;
if (IsAbilityPreventingEscape(battler))
return;
if (gBattleTypeFlags & BATTLE_TYPE_ARENA)
return;
// Sequence Switching AI never switches mid-battle
if (AI_THINKING_STRUCT->aiFlags[GetThinkingBattler(battler)] & AI_FLAG_SEQUENCE_SWITCHING)
return;
availableToSwitch = 0;
if (IsDoubleBattle())
{
u32 partner = GetBattlerAtPosition(BATTLE_PARTNER(GetBattlerAtPosition(battler)));
battlerIn1 = battler;
if (gAbsentBattlerFlags & (1u << partner))
battlerIn2 = battler;
else
battlerIn2 = partner;
}
else
{
battlerIn1 = battler;
battlerIn2 = battler;
}
GetAIPartyIndexes(battler, &firstId, &lastId);
party = GetBattlerParty(battler);
for (i = firstId; i < lastId; i++)
{
if (!IsValidForBattle(&party[i]))
continue;
if (i == gBattlerPartyIndexes[battlerIn1])
continue;
if (i == gBattlerPartyIndexes[battlerIn2])
continue;
if (i == gBattleStruct->monToSwitchIntoId[battlerIn1])
continue;
if (i == gBattleStruct->monToSwitchIntoId[battlerIn2])
continue;
if (IsAceMon(battler, i))
continue;
availableToSwitch++;
}
if (availableToSwitch == 0)
return;
if (ShouldSwitchIfAllScoresBad(battler))
AI_DATA->shouldSwitch |= (1u << battler);
else if (ShouldStayInToUseMove(battler))
AI_DATA->shouldSwitch &= ~(1u << battler);
}
bool32 IsSwitchinValid(u32 battler)
{
// Edge case: See if partner already chose to switch into the same mon

View File

@ -1132,3 +1132,24 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will consider choice-locked
TURN { MOVE(player, MOVE_EARTHQUAKE); EXPECT_MOVE(opponent, MOVE_TACKLE); item == ITEM_NONE ? EXPECT_SEND_OUT(opponent, 1) : EXPECT_SEND_OUT(opponent, 2); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch out if all moves deal zero damage")
{
PASSES_RANDOMLY(SHOULD_SWITCH_ALL_SCORES_BAD_PERCENTAGE, 100, RNG_AI_SWITCH_ALL_SCORES_BAD);
GIVEN {
ASSUME(GetMoveEffect(MOVE_WILL_O_WISP) == EFFECT_WILL_O_WISP);
ASSUME(GetMoveEffect(MOVE_POLTERGEIST) == EFFECT_POLTERGEIST);
ASSUME(GetMoveType(MOVE_SCALD) == TYPE_WATER);
ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND);
ASSUME(gSpeciesInfo[SPECIES_MANTINE].types[1] == TYPE_FLYING);
ASSUME(ItemId_GetHoldEffect(ITEM_WATER_GEM) == HOLD_EFFECT_GEMS);
ASSUME(ItemId_GetSecondaryId(ITEM_WATER_GEM) == TYPE_WATER);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_OMNISCIENT);
PLAYER(SPECIES_MANTINE) { Speed(5); Moves(MOVE_ROOST, MOVE_SCALD); Ability(ABILITY_WATER_VEIL); Item(ITEM_WATER_GEM); }
OPPONENT(SPECIES_DUSKNOIR) { Speed(6); Moves(MOVE_WILL_O_WISP, MOVE_POLTERGEIST, MOVE_EARTHQUAKE); }
OPPONENT(SPECIES_ZIGZAGOON) { Speed(6); Moves(MOVE_TACKLE); }
} WHEN {
TURN { EXPECT_MOVE(opponent, MOVE_POLTERGEIST); MOVE(player, MOVE_SCALD); }
TURN { EXPECT_SWITCH(opponent, 1); MOVE(player, MOVE_ROOST); }
}
}