Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Switch AI uses trapping abilities aggressively #4669

Merged
merged 4 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
124 changes: 104 additions & 20 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
Pawkkie marked this conversation as resolved.
Show resolved Hide resolved
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,26 @@ 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);
// Good grief clean up the 1v1 check and outspeed check
Pawkkie marked this conversation as resolved.
Show resolved Hide resolved
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)))))
Pawkkie marked this conversation as resolved.
Show resolved Hide resolved
trapperId = i;
}
}
}
}
Expand All @@ -1867,8 +1945,11 @@ 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;
Pawkkie marked this conversation as resolved.
Show resolved Hide resolved

else if (slowRevengeKillerId != PARTY_SIZE)
Expand All @@ -1894,7 +1975,10 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
}
else
{
// Return GetBestMonTypeMatchup > GetBestMonDefensive > GetBestMonBatonPass
// Return Trapper > GetBestMonTypeMatchup > GetBestMonDefensive > GetBestMonBatonPass
if (trapperId != PARTY_SIZE)
return trapperId;

if (typeMatchupEffectiveId != PARTY_SIZE)
return typeMatchupEffectiveId;

Expand Down
59 changes: 59 additions & 0 deletions test/battle/ai.c
Original file line number Diff line number Diff line change
Expand Up @@ -955,3 +955,62 @@ AI_DOUBLE_BATTLE_TEST("AI will the see a corresponding absorbing ability on part
TURN { EXPECT_MOVE(opponentLeft, MOVE_TACKLE); }
}
}

AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will switch in trapping mon mid battle")
{
u32 aiSmartSwitchingFlag = 0;
PARAMETRIZE { aiSmartSwitchingFlag = 0; }
PARAMETRIZE { aiSmartSwitchingFlag = AI_FLAG_SMART_SWITCHING; }

GIVEN {
ASSUME(gSpeciesInfo[SPECIES_GOLURK].types[0] == TYPE_GROUND);
ASSUME(gSpeciesInfo[SPECIES_GOLURK].types[1] == TYPE_GHOST);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchingFlag);
PLAYER(SPECIES_ELECTRODE) { Speed(4); Moves(MOVE_THUNDERBOLT, MOVE_AURA_SPHERE, MOVE_PROTECT); }
PLAYER(SPECIES_WOBBUFFET) { Speed(1); };
OPPONENT(SPECIES_SNORLAX) { Speed(1); Moves(MOVE_HEADBUTT); }
OPPONENT(SPECIES_DUGTRIO) { Speed(3); Ability(ABILITY_ARENA_TRAP); Moves(MOVE_EARTHQUAKE); }
OPPONENT(SPECIES_GOLURK) { Speed(5); Moves(MOVE_EARTHQUAKE); }
} WHEN {
if (aiSmartSwitchingFlag == AI_FLAG_SMART_SWITCHING)
TURN { MOVE(player, MOVE_AURA_SPHERE) ; EXPECT_SWITCH(opponent, 1); }
else
TURN { MOVE(player, MOVE_AURA_SPHERE) ; EXPECT_MOVE(opponent, MOVE_HEADBUTT); }
}
}

AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will switch in trapping mon after KO")
{
u32 aiSmartMonChoicesFlag = 0; // Enables trapping behaviour after KOs
PARAMETRIZE { aiSmartMonChoicesFlag = 0; } // No trapping behaviour
PARAMETRIZE { aiSmartMonChoicesFlag = AI_FLAG_SMART_MON_CHOICES; } // Traps with mid battle switches
GIVEN{
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartMonChoicesFlag);
PLAYER(SPECIES_MAWILE) { Speed(2); Moves(MOVE_PROTECT, MOVE_TACKLE); }
Pawkkie marked this conversation as resolved.
Show resolved Hide resolved
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
OPPONENT(SPECIES_SNORLAX) { Speed(3); Moves(MOVE_SELF_DESTRUCT); }
OPPONENT(SPECIES_MAGNEZONE) { Speed(1); Ability(ABILITY_MAGNET_PULL); Moves(MOVE_SHOCK_WAVE); }
OPPONENT(SPECIES_MEGANIUM) { Speed(3); Moves(MOVE_EARTH_POWER); }
} WHEN {
if (aiSmartMonChoicesFlag == AI_FLAG_SMART_MON_CHOICES)
TURN{ MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_SELF_DESTRUCT); EXPECT_SEND_OUT(opponent, 1); }
else
TURN{ MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_SELF_DESTRUCT); EXPECT_SEND_OUT(opponent, 2); }
}
}

AI_SINGLE_BATTLE_TEST("AI won't use trapping behaviour if player only has 1 mon left")
{
u32 aiSmartMonChoicesFlag = 0; // Enables trapping behaviour after KOs
PARAMETRIZE { aiSmartMonChoicesFlag = 0; } // No trapping behaviour
PARAMETRIZE { aiSmartMonChoicesFlag = AI_FLAG_SMART_MON_CHOICES; } // Traps with mid battle switches
GIVEN{
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartMonChoicesFlag);
PLAYER(SPECIES_MAWILE) { Speed(2); Moves(MOVE_PROTECT, MOVE_TACKLE); }
Pawkkie marked this conversation as resolved.
Show resolved Hide resolved
OPPONENT(SPECIES_SNORLAX) { Speed(3); Moves(MOVE_SELF_DESTRUCT); }
OPPONENT(SPECIES_MAGNEZONE) { Speed(1); Ability(ABILITY_MAGNET_PULL); Moves(MOVE_SHOCK_WAVE); }
OPPONENT(SPECIES_MEGANIUM) { Speed(3); Moves(MOVE_EARTH_POWER); }
} WHEN {
TURN{ MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_SELF_DESTRUCT); EXPECT_SEND_OUT(opponent, 2); }
}
}
Loading