Skip to content

Commit

Permalink
Fix AI trying to switch into the same mon 2 times (#4098)
Browse files Browse the repository at this point in the history
  • Loading branch information
DizzyEggg authored Jan 30, 2024
1 parent 8b62828 commit f562359
Show file tree
Hide file tree
Showing 5 changed files with 83 additions and 44 deletions.
2 changes: 1 addition & 1 deletion include/battle.h
Original file line number Diff line number Diff line change
Expand Up @@ -323,7 +323,7 @@ struct AiLogicData
bool8 shouldSwitchMon; // Because all available moves have no/little effect. Each bit per battler.
u8 monToSwitchId[MAX_BATTLERS_COUNT]; // ID of the mon to switch.
bool8 weatherHasEffect; // The same as WEATHER_HAS_EFFECT. Stored here, so it's called only once.
u8 mostSuitableMonId; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId.
u8 mostSuitableMonId[MAX_BATTLERS_COUNT]; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId.
struct SwitchinCandidate switchinCandidate; // Struct used for deciding which mon to switch to in battle_ai_switch_items.c
};

Expand Down
23 changes: 17 additions & 6 deletions src/battle_ai_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
#include "battle_anim.h"
#include "battle_ai_util.h"
#include "battle_ai_main.h"
#include "battle_controllers.h"
#include "battle_factory.h"
#include "battle_setup.h"
#include "battle_z_move.h"
Expand Down Expand Up @@ -414,11 +415,21 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData)
}
}

static bool32 AI_SwitchMonIfSuitable(u32 battler)
static bool32 AI_SwitchMonIfSuitable(u32 battler, bool32 doubleBattle)
{
u32 monToSwitchId = AI_DATA->mostSuitableMonId;
if (monToSwitchId != PARTY_SIZE)
u32 monToSwitchId = AI_DATA->mostSuitableMonId[battler];
if (monToSwitchId != PARTY_SIZE && IsValidForBattle(&GetBattlerParty(battler)[monToSwitchId]))
{
gBattleMoveDamage = monToSwitchId;
// Edge case: See if partner already chose to switch into the same mon
if (doubleBattle)
{
u32 partner = BATTLE_PARTNER(battler);
if (AI_DATA->shouldSwitchMon & gBitTable[partner] && AI_DATA->monToSwitchId[partner] == monToSwitchId)
{
return FALSE;
}
}
AI_DATA->shouldSwitchMon |= gBitTable[battler];
AI_DATA->monToSwitchId[battler] = monToSwitchId;
return TRUE;
Expand Down Expand Up @@ -455,7 +466,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle)
break;
}
}
if (i == MAX_BATTLERS_COUNT && AI_SwitchMonIfSuitable(battler))
if (i == MAX_BATTLERS_COUNT && AI_SwitchMonIfSuitable(battler, doubleBattle))
return TRUE;
}
else
Expand All @@ -466,7 +477,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle)
break;
}

if (i == MAX_MON_MOVES && AI_SwitchMonIfSuitable(battler))
if (i == MAX_MON_MOVES && AI_SwitchMonIfSuitable(battler, doubleBattle))
return TRUE;
}

Expand All @@ -478,7 +489,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle)
&& IsTruantMonVulnerable(battler, gBattlerTarget)
&& gDisableStructs[battler].truantCounter
&& gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2
&& AI_SwitchMonIfSuitable(battler))
&& AI_SwitchMonIfSuitable(battler, doubleBattle))
{
return TRUE;
}
Expand Down
72 changes: 36 additions & 36 deletions src/battle_ai_switch_items.c
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ void GetAIPartyIndexes(u32 battler, s32 *firstId, s32 *lastId)
}

// Note that as many return statements as possible are INTENTIONALLY put after all of the loops;
// the function can take a max of about 0.06s to run, and this prevents the player from identifying
// the function can take a max of about 0.06s to run, and this prevents the player from identifying
// whether the mon will switch or not by seeing how long the delay is before they select a move
static bool8 HasBadOdds(u32 battler)

Expand All @@ -82,12 +82,12 @@ static bool8 HasBadOdds(u32 battler)
return FALSE;

// Won't bother configuring this for double battles
if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE)
if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE)
return FALSE;

opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler));
opposingBattler = GetBattlerAtPosition(opposingPosition);

// Gets types of player (opposingBattler) and computer (battler)
atkType1 = gBattleMons[opposingBattler].type1;
atkType2 = gBattleMons[opposingBattler].type2;
Expand All @@ -102,7 +102,7 @@ static bool8 HasBadOdds(u32 battler)
if (aiMove != MOVE_NONE)
{
// Check if mon has an "important" status move
if (aiMoveEffect == EFFECT_REFLECT || aiMoveEffect == EFFECT_LIGHT_SCREEN
if (aiMoveEffect == EFFECT_REFLECT || aiMoveEffect == EFFECT_LIGHT_SCREEN
|| aiMoveEffect == EFFECT_SPIKES || aiMoveEffect == EFFECT_TOXIC_SPIKES || aiMoveEffect == EFFECT_STEALTH_ROCK || aiMoveEffect == EFFECT_STICKY_WEB || aiMoveEffect == EFFECT_LEECH_SEED
|| aiMoveEffect == EFFECT_EXPLOSION
|| aiMoveEffect == EFFECT_SLEEP || aiMoveEffect == EFFECT_YAWN || aiMoveEffect == EFFECT_TOXIC || aiMoveEffect == EFFECT_WILL_O_WISP || aiMoveEffect == EFFECT_PARALYZE
Expand Down Expand Up @@ -168,23 +168,23 @@ static bool8 HasBadOdds(u32 battler)
}

// If we don't have any other viable options, don't switch out
if (AI_DATA->mostSuitableMonId == PARTY_SIZE)
if (AI_DATA->mostSuitableMonId[battler] == PARTY_SIZE)
return FALSE;

// Start assessing whether or not mon has bad odds
// Jump straight to swtiching out in cases where mon gets OHKO'd
if (((getsOneShot && gBattleMons[opposingBattler].speed > gBattleMons[battler].speed) // If the player OHKOs and outspeeds OR OHKOs, doesn't outspeed but isn't 2HKO'd
|| (getsOneShot && gBattleMons[opposingBattler].speed <= gBattleMons[battler].speed && maxDamageDealt < gBattleMons[opposingBattler].hp / 2))
|| (getsOneShot && gBattleMons[opposingBattler].speed <= gBattleMons[battler].speed && maxDamageDealt < gBattleMons[opposingBattler].hp / 2))
&& (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator
|| (aiAbility == ABILITY_REGENERATOR
&& gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4)))
|| (aiAbility == ABILITY_REGENERATOR
&& gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4)))
{
// 50% chance to stay in regardless
if (Random() % 2 == 0)
if (Random() % 2 == 0)
return FALSE;

// Switch mon out
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
return TRUE;
}
Expand All @@ -194,19 +194,19 @@ static bool8 HasBadOdds(u32 battler)
{
if (!hasSuperEffectiveMove // If the AI doesn't have a super effective move
&& (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator
|| (aiAbility == ABILITY_REGENERATOR
&& gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4)))
|| (aiAbility == ABILITY_REGENERATOR
&& gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4)))
{
// Then check if they have an important status move, which is worth using even in a bad matchup
if(hasStatusMove)
return FALSE;

// 50% chance to stay in regardless
if (Random() % 2 == 0)
if (Random() % 2 == 0)
return FALSE;

// Switch mon out
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
return TRUE;
}
Expand Down Expand Up @@ -593,12 +593,12 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler)
moduloChance = 4; //25%
//Attempt to cure bad ailment
if (gBattleMons[battler].status1 & (STATUS1_SLEEP | STATUS1_FREEZE | STATUS1_TOXIC_POISON)
&& AI_DATA->mostSuitableMonId != PARTY_SIZE)
&& AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE)
break;
//Attempt to cure lesser ailment
if ((gBattleMons[battler].status1 & STATUS1_ANY)
&& (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2)
&& AI_DATA->mostSuitableMonId != PARTY_SIZE
&& AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE
&& Random() % (moduloChance*chanceReducer) == 0)
break;

Expand All @@ -610,7 +610,7 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler)
if (gBattleMons[battler].status1 & STATUS1_ANY)
return FALSE;
if ((gBattleMons[battler].hp <= ((gBattleMons[battler].maxHP * 2) / 3))
&& AI_DATA->mostSuitableMonId != PARTY_SIZE
&& AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE
&& Random() % (moduloChance*chanceReducer) == 0)
break;

Expand Down Expand Up @@ -785,7 +785,7 @@ static bool32 CanMonSurviveHazardSwitchin(u32 battler)

if (ability == ABILITY_REGENERATOR)
battlerHp = (battlerHp * 133) / 100; // Account for Regenerator healing

hazardDamage = GetSwitchinHazardsDamage(battler, &gBattleMons[battler]);

// Battler will faint to hazards, check to see if another mon can clear them
Expand Down Expand Up @@ -840,13 +840,13 @@ static bool32 CanMonSurviveHazardSwitchin(u32 battler)
}

static bool32 ShouldSwitchIfEncored(u32 battler)
{
{
// Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer
if (!(AI_THINKING_STRUCT->aiFlags & AI_FLAG_SMART_SWITCHING))
return FALSE;

// If not Encored or if no good switchin, don't switch
if (gDisableStructs[battler].encoredMove == MOVE_NONE || AI_DATA->mostSuitableMonId == PARTY_SIZE)
if (gDisableStructs[battler].encoredMove == MOVE_NONE || AI_DATA->mostSuitableMonId[battler] == PARTY_SIZE)
return FALSE;

// Otherwise 50% chance to switch out
Expand Down Expand Up @@ -879,7 +879,7 @@ static bool8 AreAttackingStatsLowered(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 != PARTY_SIZE && (Random() & 1))
if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (Random() & 1))
{
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
Expand All @@ -904,7 +904,7 @@ static bool8 AreAttackingStatsLowered(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 != PARTY_SIZE && (Random() & 1))
if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (Random() & 1))
{
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
Expand Down Expand Up @@ -1053,7 +1053,7 @@ void AI_TrySwitchOrUseItem(u32 battler)
{
if (*(gBattleStruct->AI_monToSwitchIntoId + battler) == PARTY_SIZE)
{
s32 monToSwitchId = AI_DATA->mostSuitableMonId;
s32 monToSwitchId = AI_DATA->mostSuitableMonId[battler];
if (monToSwitchId == PARTY_SIZE)
{
if (!(gBattleTypeFlags & BATTLE_TYPE_DOUBLE))
Expand Down Expand Up @@ -1367,7 +1367,7 @@ static s32 GetSwitchinWeatherImpact(void)

// Gets one turn of recurring healing
static u32 GetSwitchinRecurringHealing(void)
{
{
u32 recurringHealing = 0, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability;
u16 item = AI_DATA->switchinCandidate.battleMon.item;

Expand Down Expand Up @@ -1439,9 +1439,9 @@ static u32 GetSwitchinStatusDamage(u32 battler)
u32 statusDamage = 0;

// Status condition damage
if ((status != 0) && AI_DATA->switchinCandidate.battleMon.ability != ABILITY_MAGIC_GUARD)
if ((status != 0) && AI_DATA->switchinCandidate.battleMon.ability != ABILITY_MAGIC_GUARD)
{
if (status & STATUS1_BURN)
if (status & STATUS1_BURN)
{
#if B_BURN_DAMAGE >= GEN_7
statusDamage = maxHP / 16;
Expand Down Expand Up @@ -1484,7 +1484,7 @@ static u32 GetSwitchinStatusDamage(u32 battler)
if (tSpikesLayers != 0 && (defType1 != TYPE_POISON && defType2 != TYPE_POISON
&& ability != ABILITY_IMMUNITY && ability != ABILITY_POISON_HEAL
&& status == 0
&& !(heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS
&& !(heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS
&& (((gFieldStatuses & STATUS_FIELD_MAGIC_ROOM) || ability == ABILITY_KLUTZ)))
&& heldItemEffect != HOLD_EFFECT_CURE_PSN && heldItemEffect != HOLD_EFFECT_CURE_STATUS
&& IsMonGrounded(heldItemEffect, ability, defType1, defType2)))
Expand Down Expand Up @@ -1559,7 +1559,7 @@ static u32 GetSwitchinHitsToKO(s32 damageTaken, u32 battler)
singleUseItemHeal = 1;
}
}
else if (currentHP < maxHP / CONFUSE_BERRY_HP_FRACTION
else if (currentHP < maxHP / CONFUSE_BERRY_HP_FRACTION
&& opposingAbility != ABILITY_UNNERVE
&& (item == ITEM_AGUAV_BERRY || item == ITEM_FIGY_BERRY || item == ITEM_IAPAPA_BERRY || item == ITEM_MAGO_BERRY || item == ITEM_WIKI_BERRY))
{
Expand Down Expand Up @@ -1608,7 +1608,7 @@ static u16 GetSwitchinTypeMatchup(u32 opposingBattler, struct BattlePokemon batt

// Check type matchup
u16 typeEffectiveness = UQ_4_12(1.0);
u8 atkType1 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[0], atkType2 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[1],
u8 atkType1 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[0], atkType2 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[1],
defType1 = battleMon.type1, defType2 = battleMon.type2;

// Multiply type effectiveness by a factor depending on type matchup
Expand Down Expand Up @@ -1669,7 +1669,7 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattle
// the Type Matchup code will prioritize switching into a mon with the best type matchup and also a super effective move, or just best type matchup if no super effective move is found
// the Most Defensive code will prioritize switching into the mon that takes the most hits to KO, with a minimum of 4 hits required to be considered a valid option
// the Baton Pass code will prioritize switching into a mon with Baton Pass if it can get in, boost, and BP out without being KO'd, and randomizes between multiple valid options
// the Revenge Killer code will prioritize, in order, OHKO and outspeeds / OHKO, slower but not 2HKO'd / 2HKO, outspeeds and not OHKO'd / 2HKO, slower but not 3HKO'd
// the Revenge Killer code will prioritize, in order, OHKO and outspeeds / OHKO, slower but not 2HKO'd / 2HKO, outspeeds and not OHKO'd / 2HKO, slower but not 3HKO'd
// the Most Damage code will prioritize switching into whatever mon deals the most damage, which is generally not as good as having a good Type Matchup
// Everything runs in the same loop to minimize computation time. This makes it harder to read, but hopefully the comments can guide you!

Expand Down Expand Up @@ -1712,7 +1712,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
InitializeSwitchinCandidate(&party[i]);

// While not really invalid per say, not really wise to switch into this mon
if (AI_DATA->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))
if (AI_DATA->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))
continue;

// Get max number of hits for player to KO AI mon
Expand Down Expand Up @@ -1748,7 +1748,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
// Only do damage calc if switching after KO, don't need it otherwise and saves ~0.02s per turn
if (isSwitchAfterKO && aiMove != MOVE_NONE && gBattleMoves[aiMove].power != 0)
damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE);

// Check for Baton Pass; hitsToKO requirements mean mon can boost and BP without dying whether it's slower or not
if (aiMove == MOVE_BATON_PASS && ((hitsToKO > hitsToKOThreshold + 1 && AI_DATA->switchinCandidate.battleMon.speed < playerMonSpeed) || (hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed)))
bits |= gBitTable[i];
Expand Down Expand Up @@ -1857,7 +1857,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,

else if (typeMatchupId != PARTY_SIZE)
return typeMatchupId;

else if (batonPassId != PARTY_SIZE)
return batonPassId;

Expand Down Expand Up @@ -1932,7 +1932,7 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd)
if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_SMART_MON_CHOICES)
{
bestMonId = GetBestMonIntegrated(party, firstId, lastId, battler, opposingBattler, battlerIn1, battlerIn2, switchAfterMonKOd);
return bestMonId;
return bestMonId;
}

// This all handled by the GetBestMonIntegrated function if the AI_FLAG_SMART_MON_CHOICES flag is set
Expand Down Expand Up @@ -2138,4 +2138,4 @@ static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount)
}
}
return FALSE;
}
}
2 changes: 1 addition & 1 deletion src/battle_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -4037,7 +4037,7 @@ static void HandleTurnActionSelectionState(void)
if ((gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart())
&& (BattlerHasAi(battler) && !(gBattleTypeFlags & BATTLE_TYPE_PALACE)))
{
AI_DATA->mostSuitableMonId = GetMostSuitableMonToSwitchInto(battler, FALSE);
AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, FALSE);
gBattleStruct->aiMoveOrAction[battler] = ComputeBattleAiScores(battler);
}
// fallthrough
Expand Down
28 changes: 28 additions & 0 deletions test/battle/ai.c
Original file line number Diff line number Diff line change
Expand Up @@ -660,3 +660,31 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will not switch out if Pokemo
}
}
}

AI_DOUBLE_BATTLE_TEST("AI will not try to switch for the same pokemon for 2 spots in a double battle")
{
u32 flags;

PARAMETRIZE {flags = AI_FLAG_SMART_SWITCHING; }
PARAMETRIZE {flags = 0; }

GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | flags);
PLAYER(SPECIES_RATTATA);
PLAYER(SPECIES_RATTATA);
// No moves to damage player.
OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_HAUNTER) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_RATICATE) { Moves(MOVE_HEADBUTT); }
} WHEN {
TURN { EXPECT_SWITCH(opponentLeft, 3); };
} SCENE {
MESSAGE("{PKMN} TRAINER LEAF withdrew Gengar!");
MESSAGE("{PKMN} TRAINER LEAF sent out Raticate!");
NONE_OF {
MESSAGE("{PKMN} TRAINER LEAF withdrew Haunter!");
MESSAGE("{PKMN} TRAINER LEAF sent out Raticate!");
}
}
}

0 comments on commit f562359

Please sign in to comment.