Skip to content

Commit

Permalink
Switch AI uses trapping abilities aggressively (#4669)
Browse files Browse the repository at this point in the history
* Trapping switch AI

* Review feedback, mostly spacing cleanup

* Assume Mawile is Steel type

* Move switching tests into their own file
  • Loading branch information
Pawkkie committed Jun 1, 2024
1 parent 5405e65 commit cb1b4bc
Show file tree
Hide file tree
Showing 3 changed files with 433 additions and 295 deletions.
133 changes: 102 additions & 31 deletions src/battle_ai_switch_items.c
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ static bool32 AiExpectsToFaintPlayer(u32 battler);
static bool32 AI_ShouldHeal(u32 battler, u32 healAmount);
static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount);
static u32 GetSwitchinHazardsDamage(u32 battler, struct BattlePokemon *battleMon);
static bool32 CanAbilityTrapOpponent(u16 ability, u32 opponent);

static void InitializeSwitchinCandidate(struct Pokemon *mon)
{
Expand Down Expand Up @@ -414,6 +415,48 @@ static bool32 FindMonThatAbsorbsOpponentsMove(u32 battler, bool32 emitResult)
return FALSE;
}

static bool32 FindMonThatTrapsOpponent(u32 battler, bool32 emitResult)
{
s32 firstId;
s32 lastId;
struct Pokemon *party;
s32 i;
u16 monAbility;
s32 opposingBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerPosition(battler)));

// Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer
if (!(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SMART_SWITCHING))
return FALSE;

// Check if current mon has an ability that traps opponent
if (CanAbilityTrapOpponent(gBattleMons[battler].ability, opposingBattler))
return FALSE;

// Check party for mon with ability that traps opponent
GetAIPartyIndexes(battler, &firstId, &lastId);

if (GetBattlerSide(battler) == B_SIDE_PLAYER)
party = gPlayerParty;
else
party = gEnemyParty;

for (i = firstId; i < lastId; i++)
{
monAbility = GetMonAbility(&party[i]);
if (CanAbilityTrapOpponent(monAbility, opposingBattler))
{
if (i == AI_DATA->mostSuitableMonId[battler]) // If mon in slot i is the most suitable switchin candidate, then it's a trapper than wins 1v1
{
gBattleStruct->AI_monToSwitchIntoId[battler] = i;
if (emitResult)
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
return TRUE;
}
}
}
return FALSE;
}

static bool32 ShouldSwitchIfGameStatePrompt(u32 battler, bool32 emitResult)
{
bool32 switchMon = FALSE;
Expand Down Expand Up @@ -1017,6 +1060,8 @@ bool32 ShouldSwitch(u32 battler, bool32 emitResult)
return TRUE;
if (ShouldSwitchIfGameStatePrompt(battler, emitResult))
return TRUE;
if (FindMonThatTrapsOpponent(battler, emitResult))
return TRUE;
if (FindMonThatAbsorbsOpponentsMove(battler, emitResult))
return TRUE;

Expand Down Expand Up @@ -1691,6 +1736,25 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattle
return maxDamageTaken;
}

static bool32 CanAbilityTrapOpponent(u16 ability, u32 opponent)
{
if ((B_GHOSTS_ESCAPE >= GEN_6 && IS_BATTLER_OF_TYPE(opponent, TYPE_GHOST)))
return FALSE;
else if (ability == ABILITY_SHADOW_TAG)
{
if (B_SHADOW_TAG_ESCAPE >= GEN_4 && GetBattlerAbility(opponent) == ABILITY_SHADOW_TAG) // Check if ability exists in species
return FALSE;
else
return TRUE;
}
else if (ability == ABILITY_ARENA_TRAP && IsBattlerGrounded(opponent))
return TRUE;
else if (ability == ABILITY_MAGNET_PULL && IS_BATTLER_OF_TYPE(opponent, TYPE_STEEL))
return TRUE;
else
return FALSE;
}

// This function splits switching behaviour mid-battle from after a KO.
// Mid battle, it integrates GetBestMonTypeMatchup (vanilla with modifications), GetBestMonDefensive (custom), and GetBestMonBatonPass (vanilla with modifications)
// After a KO, integrates GetBestMonRevengeKiller (custom), GetBestMonTypeMatchup (vanilla with modifications), GetBestMonBatonPass (vanilla with modifications), and GetBestMonDmg (vanilla)
Expand All @@ -1704,17 +1768,17 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattle
static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, u32 battler, u32 opposingBattler, u8 battlerIn1, u8 battlerIn2, bool32 isSwitchAfterKO)
{
int revengeKillerId = PARTY_SIZE, slowRevengeKillerId = PARTY_SIZE, fastThreatenId = PARTY_SIZE, slowThreatenId = PARTY_SIZE, damageMonId = PARTY_SIZE;
int batonPassId = PARTY_SIZE, typeMatchupId = PARTY_SIZE, typeMatchupEffectiveId = PARTY_SIZE, defensiveMonId = PARTY_SIZE, aceMonId = PARTY_SIZE;
int batonPassId = PARTY_SIZE, typeMatchupId = PARTY_SIZE, typeMatchupEffectiveId = PARTY_SIZE, defensiveMonId = PARTY_SIZE, aceMonId = PARTY_SIZE, trapperId = PARTY_SIZE;
int i, j, aliveCount = 0, bits = 0;
s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed
u32 aiMove, hitsToKO, hitsToKOThreshold, maxHitsToKO = 0;
s32 playerMonSpeed = gBattleMons[opposingBattler].speed, playerMonHP = gBattleMons[opposingBattler].hp, aiMonSpeed, maxDamageDealt = 0, damageDealt = 0;
u32 aiMove, hitsToKOAI, hitsToKOPlayer, hitsToKOAIThreshold, maxHitsToKO = 0;
s32 playerMonSpeed = gBattleMons[opposingBattler].speed, playerMonHP = gBattleMons[opposingBattler].hp, aiMonSpeed, aiMovePriority = 0, maxDamageDealt = 0, damageDealt = 0;
u16 bestResist = UQ_4_12(1.0), bestResistEffective = UQ_4_12(1.0), typeMatchup;

if (isSwitchAfterKO)
hitsToKOThreshold = 1; // After a KO, mons at minimum need to not be 1-shot, as they switch in for free
hitsToKOAIThreshold = 1; // After a KO, mons at minimum need to not be 1-shot, as they switch in for free
else
hitsToKOThreshold = 2; // When switching in otherwise need to not be 2-shot, as they do not switch in for free
hitsToKOAIThreshold = 2; // When switching in otherwise need to not be 2-shot, as they do not switch in for free

// Iterate through mons
for (i = firstId; i < lastId; i++)
Expand Down Expand Up @@ -1744,12 +1808,12 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
continue;

// Get max number of hits for player to KO AI mon
hitsToKO = GetSwitchinHitsToKO(GetMaxDamagePlayerCouldDealToSwitchin(battler, opposingBattler, AI_DATA->switchinCandidate.battleMon), battler);
hitsToKOAI = GetSwitchinHitsToKO(GetMaxDamagePlayerCouldDealToSwitchin(battler, opposingBattler, AI_DATA->switchinCandidate.battleMon), battler);

// Track max hits to KO and set GetBestMonDefensive if applicable
if(hitsToKO > maxHitsToKO)
if(hitsToKOAI > maxHitsToKO)
{
maxHitsToKO = hitsToKO;
maxHitsToKO = hitsToKOAI;
if(maxHitsToKO > defensiveMonHitKOThreshold)
defensiveMonId = i;
}
Expand All @@ -1759,7 +1823,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
// Check that good type matchups gets at least two turns and set GetBestMonTypeMatchup if applicable
if (typeMatchup < bestResist)
{
if ((hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed) || hitsToKO > hitsToKOThreshold + 1) // Need to take an extra hit if slower
if ((hitsToKOAI > hitsToKOAIThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed) || hitsToKOAI > hitsToKOAIThreshold + 1) // Need to take an extra hit if slower
{
bestResist = typeMatchup;
typeMatchupId = i;
Expand All @@ -1772,13 +1836,14 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
for (j = 0; j < MAX_MON_MOVES; j++)
{
aiMove = AI_DATA->switchinCandidate.battleMon.moves[j];
aiMovePriority = gMovesInfo[aiMove].priority;

// Only do damage calc if switching after KO, don't need it otherwise and saves ~0.02s per turn
if (isSwitchAfterKO && aiMove != MOVE_NONE && gMovesInfo[aiMove].power != 0)
damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE, DMG_ROLL_AVERAGE);

// 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)))
if (aiMove == MOVE_BATON_PASS && ((hitsToKOAI > hitsToKOAIThreshold + 1 && AI_DATA->switchinCandidate.battleMon.speed < playerMonSpeed) || (hitsToKOAI > hitsToKOAIThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed)))
bits |= gBitTable[i];

// Check for mon with resistance and super effective move for GetBestMonTypeMatchup
Expand All @@ -1789,7 +1854,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (AI_GetTypeEffectiveness(aiMove, battler, opposingBattler) >= UQ_4_12(2.0))
{
// Assuming a super effective move would do significant damage or scare the player out, so not being as conservative here
if (hitsToKO > hitsToKOThreshold)
if (hitsToKOAI > hitsToKOAIThreshold)
{
bestResistEffective = typeMatchup;
typeMatchupEffectiveId = i;
Expand All @@ -1804,7 +1869,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
// Check that mon isn't one shot and set GetBestMonDmg if applicable
if (damageDealt > maxDamageDealt)
{
if(hitsToKO > hitsToKOThreshold)
if(hitsToKOAI > hitsToKOAIThreshold)
{
maxDamageDealt = damageDealt;
damageMonId = i;
Expand All @@ -1816,7 +1881,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (damageDealt > playerMonHP)
{
// If AI mon is faster and doesn't die to hazards
if ((aiMonSpeed > playerMonSpeed || gMovesInfo[aiMove].priority > 0) && AI_DATA->switchinCandidate.battleMon.hp > GetSwitchinHazardsDamage(battler, &AI_DATA->switchinCandidate.battleMon))
if ((aiMonSpeed > playerMonSpeed || aiMovePriority > 0) && AI_DATA->switchinCandidate.battleMon.hp > GetSwitchinHazardsDamage(battler, &AI_DATA->switchinCandidate.battleMon))
{
// We have a revenge killer
revengeKillerId = i;
Expand All @@ -1826,7 +1891,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
else
{
// If AI mon can't be OHKO'd
if (hitsToKO > hitsToKOThreshold)
if (hitsToKOAI > hitsToKOAIThreshold)
{
// We have a slow revenge killer
slowRevengeKillerId = i;
Expand All @@ -1838,10 +1903,10 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (damageDealt > playerMonHP / 2)
{
// If AI mon is faster
if (aiMonSpeed > playerMonSpeed || gMovesInfo[aiMove].priority > 0)
if (aiMonSpeed > playerMonSpeed || aiMovePriority > 0)
{
// If AI mon can't be OHKO'd
if (hitsToKO > hitsToKOThreshold)
if (hitsToKOAI > hitsToKOAIThreshold)
{
// We have a fast threaten
fastThreatenId = i;
Expand All @@ -1851,13 +1916,25 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
else
{
// If AI mon can't be 2HKO'd
if (hitsToKO > hitsToKOThreshold + 1)
if (hitsToKOAI > hitsToKOAIThreshold + 1)
{
// We have a slow threaten
slowThreatenId = i;
}
}
}

// If mon can trap
if (CanAbilityTrapOpponent(AI_DATA->switchinCandidate.battleMon.ability, opposingBattler))
{
hitsToKOPlayer = GetNoOfHitsToKOBattlerDmg(damageDealt, opposingBattler);
if (CountUsablePartyMons(opposingBattler) > 0
&& (((hitsToKOAI > hitsToKOPlayer && isSwitchAfterKO) // If can 1v1 after a KO
|| (hitsToKOAI == hitsToKOPlayer && isSwitchAfterKO && (aiMonSpeed > playerMonSpeed || aiMovePriority > 0)))
|| ((hitsToKOAI > hitsToKOPlayer + 1 && !isSwitchAfterKO) // If can 1v1 after mid battle
|| (hitsToKOAI == hitsToKOPlayer + 1 && !isSwitchAfterKO && (aiMonSpeed > playerMonSpeed || aiMovePriority > 0)))))
trapperId = i;
}
}
}
}
Expand All @@ -1867,43 +1944,37 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
// Different switching priorities depending on switching mid battle vs switching after a KO
if (isSwitchAfterKO)
{
// Return GetBestMonRevengeKiller > GetBestMonTypeMatchup > GetBestMonBatonPass > GetBestMonDmg
if (revengeKillerId != PARTY_SIZE)
// Return Trapper > GetBestMonRevengeKiller > GetBestMonTypeMatchup > GetBestMonBatonPass > GetBestMonDmg
if (trapperId != PARTY_SIZE)
return trapperId;
else if (revengeKillerId != PARTY_SIZE)
return revengeKillerId;

else if (slowRevengeKillerId != PARTY_SIZE)
return slowRevengeKillerId;

else if (fastThreatenId != PARTY_SIZE)
return fastThreatenId;

else if (slowThreatenId != PARTY_SIZE)
return slowThreatenId;

else if (typeMatchupEffectiveId != PARTY_SIZE)
return typeMatchupEffectiveId;

else if (typeMatchupId != PARTY_SIZE)
return typeMatchupId;

else if (batonPassId != PARTY_SIZE)
return batonPassId;

else if (damageMonId != PARTY_SIZE)
return damageMonId;
}
else
{
// Return GetBestMonTypeMatchup > GetBestMonDefensive > GetBestMonBatonPass
if (typeMatchupEffectiveId != PARTY_SIZE)
// Return Trapper > GetBestMonTypeMatchup > GetBestMonDefensive > GetBestMonBatonPass
if (trapperId != PARTY_SIZE)
return trapperId;
else if (typeMatchupEffectiveId != PARTY_SIZE)
return typeMatchupEffectiveId;

else if (typeMatchupId != PARTY_SIZE)
return typeMatchupId;

else if (defensiveMonId != PARTY_SIZE)
return defensiveMonId;

else if (batonPassId != PARTY_SIZE)
return batonPassId;

Expand Down
Loading

0 comments on commit cb1b4bc

Please sign in to comment.