From 3a7f3269dbb72c0acec655979b5e34b6e1b627a9 Mon Sep 17 00:00:00 2001 From: Pawkkie Date: Thu, 30 May 2024 18:01:47 -0400 Subject: [PATCH 1/4] Trapping switch AI --- src/battle_ai_switch_items.c | 124 +++++++++++++++++++++++++++++------ test/battle/ai.c | 59 +++++++++++++++++ 2 files changed, 163 insertions(+), 20 deletions(-) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 2dac845c47bd..74e31b4e70d5 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -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) { @@ -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; @@ -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; @@ -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) @@ -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++) @@ -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; } @@ -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; @@ -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 @@ -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; @@ -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; @@ -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; @@ -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; @@ -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; @@ -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 + 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; + } } } } @@ -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; else if (slowRevengeKillerId != PARTY_SIZE) @@ -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; diff --git a/test/battle/ai.c b/test/battle/ai.c index e673be4e6a3a..3862b221cfc6 100644 --- a/test/battle/ai.c +++ b/test/battle/ai.c @@ -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); } + 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); } + 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); } + } +} From 4303bcdafa457b86097e2bac163ca56e0b66acfd Mon Sep 17 00:00:00 2001 From: Pawkkie Date: Fri, 31 May 2024 11:36:57 -0400 Subject: [PATCH 2/4] Review feedback, mostly spacing cleanup --- src/battle_ai_switch_items.c | 23 +++++------------------ 1 file changed, 5 insertions(+), 18 deletions(-) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 74e31b4e70d5..c58888da05d5 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -1928,12 +1928,11 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, if (CanAbilityTrapOpponent(AI_DATA->switchinCandidate.battleMon.ability, opposingBattler)) { hitsToKOPlayer = GetNoOfHitsToKOBattlerDmg(damageDealt, opposingBattler); - // Good grief clean up the 1v1 check and outspeed check 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))))) + && (((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; } } @@ -1948,28 +1947,20 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, // 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; } @@ -1978,16 +1969,12 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, // Return Trapper > GetBestMonTypeMatchup > GetBestMonDefensive > GetBestMonBatonPass if (trapperId != PARTY_SIZE) return trapperId; - - if (typeMatchupEffectiveId != PARTY_SIZE) + 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; From b30b2416ef498890c611eb06d28b0b49e71726ca Mon Sep 17 00:00:00 2001 From: Pawkkie Date: Fri, 31 May 2024 17:26:00 -0400 Subject: [PATCH 3/4] Assume Mawile is Steel type --- test/battle/ai.c | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/battle/ai.c b/test/battle/ai.c index 3862b221cfc6..3b6474aafa07 100644 --- a/test/battle/ai.c +++ b/test/battle/ai.c @@ -985,6 +985,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will switch in trapping mon PARAMETRIZE { aiSmartMonChoicesFlag = 0; } // No trapping behaviour PARAMETRIZE { aiSmartMonChoicesFlag = AI_FLAG_SMART_MON_CHOICES; } // Traps with mid battle switches GIVEN{ + ASSUME(gSpeciesInfo[SPECIES_MAWILE].types[0] == TYPE_STEEL); 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); } PLAYER(SPECIES_WOBBUFFET) { Speed(1); } @@ -1005,6 +1006,7 @@ AI_SINGLE_BATTLE_TEST("AI won't use trapping behaviour if player only has 1 mon PARAMETRIZE { aiSmartMonChoicesFlag = 0; } // No trapping behaviour PARAMETRIZE { aiSmartMonChoicesFlag = AI_FLAG_SMART_MON_CHOICES; } // Traps with mid battle switches GIVEN{ + ASSUME(gSpeciesInfo[SPECIES_MAWILE].types[0] == TYPE_STEEL); 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); } OPPONENT(SPECIES_SNORLAX) { Speed(3); Moves(MOVE_SELF_DESTRUCT); } From 20fcdc232ca3160504deef64f3bb899e9e042936 Mon Sep 17 00:00:00 2001 From: Pawkkie Date: Fri, 31 May 2024 17:37:11 -0400 Subject: [PATCH 4/4] Move switching tests into their own file --- test/battle/ai.c | 325 ------------------------------------ test/battle/ai_switching.c | 331 +++++++++++++++++++++++++++++++++++++ 2 files changed, 331 insertions(+), 325 deletions(-) create mode 100644 test/battle/ai_switching.c diff --git a/test/battle/ai.c b/test/battle/ai.c index 3b6474aafa07..2d804446d505 100644 --- a/test/battle/ai.c +++ b/test/battle/ai.c @@ -2,26 +2,6 @@ #include "test/battle.h" #include "battle_ai_util.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 - } -} - AI_SINGLE_BATTLE_TEST("AI prefers Bubble over Water Gun if it's slower") { u32 speedPlayer, speedAi; @@ -526,222 +506,6 @@ AI_SINGLE_BATTLE_TEST("AI will choose either Rock Tomb or Bulldoze if Stat drop } } -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Number of hits to KO calculation checks whether incoming damage is less than recurring healing to avoid an infinite loop") -{ - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); - PLAYER(SPECIES_VENUSAUR) { Level(30); Moves(MOVE_TACKLE); } - // Opponent party courtesy of Skolgrahd, who triggered the bug in the first place - OPPONENT(SPECIES_PIKACHU) { Level(100); Moves(MOVE_ZIPPY_ZAP, MOVE_EXTREME_SPEED, MOVE_IRON_TAIL, MOVE_KNOCK_OFF); } - OPPONENT(SPECIES_NINETALES_ALOLAN) { Level(100); Moves(MOVE_AURORA_VEIL, MOVE_BLIZZARD, MOVE_MOONBLAST, MOVE_DISABLE); } - OPPONENT(SPECIES_WEAVILE) { Level(100); Moves(MOVE_NIGHT_SLASH, MOVE_TRIPLE_AXEL, MOVE_ICE_SHARD, MOVE_FAKE_OUT); } - OPPONENT(SPECIES_DITTO) { Level(100); Moves(MOVE_TRANSFORM); } - OPPONENT(SPECIES_TYPHLOSION) { Level(100); Moves(MOVE_ERUPTION, MOVE_HEAT_WAVE, MOVE_FOCUS_BLAST, MOVE_EXTRASENSORY); } - OPPONENT(SPECIES_UMBREON) { Level(100); Item(ITEM_LEFTOVERS); Moves(MOVE_FOUL_PLAY, MOVE_SNARL, MOVE_HELPING_HAND, MOVE_THUNDER_WAVE); } - } WHEN { - TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVES(opponent, MOVE_ZIPPY_ZAP, MOVE_EXTREME_SPEED, MOVE_IRON_TAIL, MOVE_KNOCK_OFF); } - } SCENE { - MESSAGE("Venusaur fainted!"); - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Number of hits to KO calculation checks whether incoming damage is zero to avoid an infinite loop") -{ - GIVEN { - ASSUME(gItemsInfo[ITEM_LEFTOVERS].holdEffect == HOLD_EFFECT_LEFTOVERS); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); - PLAYER(SPECIES_BULBASAUR) { Level(5); Moves(MOVE_SWORDS_DANCE, MOVE_WHIRLWIND, MOVE_SAND_ATTACK, MOVE_TAIL_WHIP); } - // Scenario courtesy of Duke, who triggered the bug in the first place - OPPONENT(SPECIES_GEODUDE) { Level(100); Moves(MOVE_TACKLE); } - OPPONENT(SPECIES_GEODUDE) { Level(100); Moves(MOVE_TACKLE); } - OPPONENT(SPECIES_NOSEPASS) { Level(100); Moves(MOVE_TACKLE); } - } WHEN { - TURN { MOVE(player, MOVE_SWORDS_DANCE); EXPECT_MOVES(opponent, MOVE_TACKLE); } - } SCENE { - MESSAGE("Bulbasaur fainted!"); - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Avoid infinite loop if damage taken is equal to recurring healing") -{ - GIVEN { - ASSUME(gItemsInfo[ITEM_LEFTOVERS].holdEffect == HOLD_EFFECT_LEFTOVERS); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); - PLAYER(SPECIES_MEOWTH_GALARIAN) { Level(100); Moves(MOVE_GROWL, MOVE_FAKE_OUT, MOVE_HONE_CLAWS); } - // Scenario courtesy of Duke, who triggered the bug in the first place - OPPONENT(SPECIES_MEOWTH_GALARIAN) { Level(5); Moves(MOVE_GROWL, MOVE_FAKE_OUT, MOVE_HONE_CLAWS); } - OPPONENT(SPECIES_GEODUDE) { Level(5); Moves(MOVE_DOUBLE_EDGE); } - OPPONENT(SPECIES_GEODUDE) { Level(5); Moves(MOVE_DOUBLE_EDGE); } - OPPONENT(SPECIES_NOSEPASS) { Level(5); Moves(MOVE_DOUBLE_EDGE); } - OPPONENT(SPECIES_HOUNDSTONE) { Level(5); Moves(MOVE_NIGHT_SHADE, MOVE_BODY_PRESS, MOVE_WILL_O_WISP, MOVE_PROTECT); Item(ITEM_LEFTOVERS); } - } WHEN { - TURN { MOVE(player, MOVE_FAKE_OUT); EXPECT_MOVES(opponent, MOVE_FAKE_OUT); } - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will not switch in a Pokemon which is slower and gets 1HKOed after fainting") -{ - bool32 alakazamFirst; - u32 speedAlakazm; - u32 aiSmartSwitchFlags = 0; - - PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = TRUE; } // AI will always send out Alakazan as it sees a KO with Focus Blast, even if Alakazam dies before it can get it off - PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = FALSE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES lets AI see that Alakazam would be KO'd before it can KO, and won't switch it in - PARAMETRIZE{ speedAlakazm = 400; alakazamFirst = TRUE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES recognizes that Alakazam is faster and can KO, and will switch it in - - GIVEN { - ASSUME(gMovesInfo[MOVE_PSYCHIC].category == DAMAGE_CATEGORY_SPECIAL); - ASSUME(gMovesInfo[MOVE_FOCUS_BLAST].category == DAMAGE_CATEGORY_SPECIAL); - ASSUME(gMovesInfo[MOVE_BUBBLE_BEAM].category == DAMAGE_CATEGORY_SPECIAL); - ASSUME(gMovesInfo[MOVE_WATER_GUN].category == DAMAGE_CATEGORY_SPECIAL); - ASSUME(gMovesInfo[MOVE_STRENGTH].category == DAMAGE_CATEGORY_PHYSICAL); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); - PLAYER(SPECIES_WEAVILE) { Speed(300); Ability(ABILITY_SHADOW_TAG); } // Weavile has Shadow Tag, so AI can't switch on the first turn, but has to do it after fainting. - OPPONENT(SPECIES_KADABRA) { Speed(200); Moves(MOVE_PSYCHIC, MOVE_DISABLE, MOVE_TAUNT, MOVE_CALM_MIND); } - OPPONENT(SPECIES_ALAKAZAM) { Speed(speedAlakazm); Moves(MOVE_FOCUS_BLAST, MOVE_PSYCHIC); } // Alakazam has a move which OHKOes Weavile, but it doesn't matter if he's getting KO-ed first. - OPPONENT(SPECIES_BLASTOISE) { Speed(200); Moves(MOVE_BUBBLE_BEAM, MOVE_WATER_GUN, MOVE_LEER, MOVE_STRENGTH); } // Can't OHKO, but survives a hit from Weavile's Night Slash. - } WHEN { - TURN { MOVE(player, MOVE_NIGHT_SLASH) ; EXPECT_SEND_OUT(opponent, alakazamFirst ? 1 : 2); } // AI doesn't send out Alakazam if it gets outsped - } SCENE { - MESSAGE("Foe Kadabra fainted!"); - if (alakazamFirst) { - MESSAGE("{PKMN} TRAINER LEAF sent out Alakazam!"); - } else { - MESSAGE("{PKMN} TRAINER LEAF sent out Blastoise!"); - } - } -} - -AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill") -{ - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_WOBBUFFET) {Moves(MOVE_TACKLE); } - OPPONENT(SPECIES_CROBAT) {Moves(MOVE_TACKLE); } - } WHEN { - TURN { MOVE(player, MOVE_PERISH_SONG); } - TURN { ; } - TURN { ; } - TURN { EXPECT_SWITCH(opponent, 1); } - } SCENE { - MESSAGE("{PKMN} TRAINER LEAF sent out Crobat!"); - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI considers hazard damage when choosing which Pokemon to switch in") -{ - u32 aiIsSmart = 0; - u32 aiSmartSwitchFlags = 0; - - PARAMETRIZE{ aiIsSmart = 0; aiSmartSwitchFlags = 0; } // AI doesn't care about hazard damage resulting in Pokemon being KO'd - PARAMETRIZE{ aiIsSmart = 1; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES avoids being KO'd as a result of hazards damage - - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); - PLAYER(SPECIES_MEGANIUM) { Speed(100); SpDefense(328); SpAttack(265); Moves(MOVE_STEALTH_ROCK, MOVE_SURF); } // Meganium does ~56% minimum ~66% maximum, enough to KO Charizard after rocks and never KO Typhlosion after rocks - OPPONENT(SPECIES_PONYTA) { Level(5); Speed(5); Moves(MOVE_TACKLE); } - OPPONENT(SPECIES_CHARIZARD) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium - OPPONENT(SPECIES_TYPHLOSION) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium - } WHEN { - TURN { MOVE(player, MOVE_STEALTH_ROCK) ;} - TURN { MOVE(player, MOVE_SURF); EXPECT_SEND_OUT(opponent, aiIsSmart ? 2 : 1); } // AI sends out Typhlosion to get the KO with the flag rather than Charizard - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize type matchup + SE move, then type matchup") -{ - u32 aiSmartSwitchFlags = 0; - u32 move1; - u32 move2; - u32 expectedIndex; - - PARAMETRIZE{ expectedIndex = 3; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; } // When not smart, AI will only switch in a defensive mon if it has a SE move, otherwise will just default to damage - PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; } - PARAMETRIZE{ expectedIndex = 2; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // When smart, AI will prioritize SE move, but still switch in good type matchup without SE move - PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } - - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); - PLAYER(SPECIES_MARSHTOMP) { Level(30); Moves(MOVE_MUD_BOMB, MOVE_WATER_GUN, MOVE_GROWL, MOVE_MUD_SHOT); Speed(5); } - OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(6); } // Forces switchout - OPPONENT(SPECIES_TANGELA) { Level(30); Moves(move1); Speed(4); } - OPPONENT(SPECIES_LOMBRE) { Level(30); Moves(move2); Speed(4); } - OPPONENT(SPECIES_HARIYAMA) { Level(30); Moves(MOVE_VITAL_THROW); Speed(4); } - } WHEN { - TURN { MOVE(player, MOVE_GROWL); EXPECT_SWITCH(opponent, expectedIndex); } - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize defensive options") -{ - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES); - PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } - OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout - OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); SpDefense(41); } // Mid battle, AI sends out Aron - OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } - } WHEN { - TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SWITCH(opponent, 1); } - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Post-KO switches prioritize offensive options") -{ - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES); - PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } - OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_TACKLE); Speed(4); } - OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } // Mid battle, AI sends out Aron - OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } - } WHEN { - TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SEND_OUT(opponent, 2); } - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI switches out after sufficient stat drops") -{ - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); - PLAYER(SPECIES_HITMONTOP) { Level(30); Moves(MOVE_CHARM, MOVE_TACKLE); Ability(ABILITY_INTIMIDATE); Speed(5); } - OPPONENT(SPECIES_GRIMER) { Level(30); Moves(MOVE_TACKLE); Speed(4); } - OPPONENT(SPECIES_PONYTA) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } - } WHEN { - TURN { MOVE(player, MOVE_CHARM); } - TURN { MOVE(player, MOVE_TACKLE); EXPECT_SWITCH(opponent, 1); } - } -} - -AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will not switch out if Pokemon would faint to hazards unless party member can clear them") -{ - u32 move1; - - PARAMETRIZE{ move1 = MOVE_TACKLE; } - PARAMETRIZE{ move1 = MOVE_RAPID_SPIN; } - - GIVEN { - ASSUME(gMovesInfo[MOVE_TACKLE].category == DAMAGE_CATEGORY_PHYSICAL); - ASSUME(gMovesInfo[MOVE_RAPID_SPIN].category == DAMAGE_CATEGORY_PHYSICAL); - ASSUME(gMovesInfo[MOVE_EARTHQUAKE].category == DAMAGE_CATEGORY_PHYSICAL); - ASSUME(gMovesInfo[MOVE_HEADBUTT].category == DAMAGE_CATEGORY_PHYSICAL); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); - PLAYER(SPECIES_HITMONTOP) { Level(30); Moves(MOVE_CHARM, MOVE_TACKLE, MOVE_STEALTH_ROCK, MOVE_EARTHQUAKE); Ability(ABILITY_INTIMIDATE); Speed(5); } - OPPONENT(SPECIES_GRIMER) { Level(30); Moves(MOVE_TACKLE); Item(ITEM_FOCUS_SASH); Speed(4); } - OPPONENT(SPECIES_PONYTA) { Level(30); Moves(MOVE_HEADBUTT, move1); Speed(4); } - } WHEN { - TURN { MOVE(player, MOVE_STEALTH_ROCK); } - TURN { MOVE(player, MOVE_EARTHQUAKE); } - TURN { MOVE(player, MOVE_CHARM); } - TURN { // If the AI has a mon that can remove hazards, don't prevent them switching out - MOVE(player, MOVE_CHARM); - if (move1 == MOVE_RAPID_SPIN) - EXPECT_SWITCH(opponent, 1); - else if (move1 == MOVE_TACKLE) - EXPECT_MOVE(opponent, MOVE_TACKLE); - } - } -} - AI_SINGLE_BATTLE_TEST("First Impression is preferred on the first turn of the species if it's the best dmg move") { GIVEN { @@ -778,34 +542,6 @@ AI_SINGLE_BATTLE_TEST("First Impression is not chosen if it's blocked by certain } } -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!"); - } - } -} - AI_SINGLE_BATTLE_TEST("AI will not choose Burn Up if the user lost the Fire typing") { GIVEN { @@ -955,64 +691,3 @@ 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{ - ASSUME(gSpeciesInfo[SPECIES_MAWILE].types[0] == TYPE_STEEL); - 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); } - 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{ - ASSUME(gSpeciesInfo[SPECIES_MAWILE].types[0] == TYPE_STEEL); - 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); } - 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); } - } -} diff --git a/test/battle/ai_switching.c b/test/battle/ai_switching.c new file mode 100644 index 000000000000..fb5085a7911f --- /dev/null +++ b/test/battle/ai_switching.c @@ -0,0 +1,331 @@ +#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") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) {Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_CROBAT) {Moves(MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_PERISH_SONG); } + TURN { ; } + TURN { ; } + TURN { EXPECT_SWITCH(opponent, 1); } + } SCENE { + MESSAGE("{PKMN} TRAINER LEAF sent out Crobat!"); + } +} + +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!"); + } + } +} + +// General AI_FLAG_SMART_MON_CHOICES behaviour +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Number of hits to KO calculation checks whether incoming damage is less than recurring healing to avoid an infinite loop") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_VENUSAUR) { Level(30); Moves(MOVE_TACKLE); } + // Opponent party courtesy of Skolgrahd, who triggered the bug in the first place + OPPONENT(SPECIES_PIKACHU) { Level(100); Moves(MOVE_ZIPPY_ZAP, MOVE_EXTREME_SPEED, MOVE_IRON_TAIL, MOVE_KNOCK_OFF); } + OPPONENT(SPECIES_NINETALES_ALOLAN) { Level(100); Moves(MOVE_AURORA_VEIL, MOVE_BLIZZARD, MOVE_MOONBLAST, MOVE_DISABLE); } + OPPONENT(SPECIES_WEAVILE) { Level(100); Moves(MOVE_NIGHT_SLASH, MOVE_TRIPLE_AXEL, MOVE_ICE_SHARD, MOVE_FAKE_OUT); } + OPPONENT(SPECIES_DITTO) { Level(100); Moves(MOVE_TRANSFORM); } + OPPONENT(SPECIES_TYPHLOSION) { Level(100); Moves(MOVE_ERUPTION, MOVE_HEAT_WAVE, MOVE_FOCUS_BLAST, MOVE_EXTRASENSORY); } + OPPONENT(SPECIES_UMBREON) { Level(100); Item(ITEM_LEFTOVERS); Moves(MOVE_FOUL_PLAY, MOVE_SNARL, MOVE_HELPING_HAND, MOVE_THUNDER_WAVE); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVES(opponent, MOVE_ZIPPY_ZAP, MOVE_EXTREME_SPEED, MOVE_IRON_TAIL, MOVE_KNOCK_OFF); } + } SCENE { + MESSAGE("Venusaur fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Number of hits to KO calculation checks whether incoming damage is zero to avoid an infinite loop") +{ + GIVEN { + ASSUME(gItemsInfo[ITEM_LEFTOVERS].holdEffect == HOLD_EFFECT_LEFTOVERS); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_BULBASAUR) { Level(5); Moves(MOVE_SWORDS_DANCE, MOVE_WHIRLWIND, MOVE_SAND_ATTACK, MOVE_TAIL_WHIP); } + // Scenario courtesy of Duke, who triggered the bug in the first place + OPPONENT(SPECIES_GEODUDE) { Level(100); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_GEODUDE) { Level(100); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_NOSEPASS) { Level(100); Moves(MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_SWORDS_DANCE); EXPECT_MOVES(opponent, MOVE_TACKLE); } + } SCENE { + MESSAGE("Bulbasaur fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Avoid infinite loop if damage taken is equal to recurring healing") +{ + GIVEN { + ASSUME(gItemsInfo[ITEM_LEFTOVERS].holdEffect == HOLD_EFFECT_LEFTOVERS); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_MEOWTH_GALARIAN) { Level(100); Moves(MOVE_GROWL, MOVE_FAKE_OUT, MOVE_HONE_CLAWS); } + // Scenario courtesy of Duke, who triggered the bug in the first place + OPPONENT(SPECIES_MEOWTH_GALARIAN) { Level(5); Moves(MOVE_GROWL, MOVE_FAKE_OUT, MOVE_HONE_CLAWS); } + OPPONENT(SPECIES_GEODUDE) { Level(5); Moves(MOVE_DOUBLE_EDGE); } + OPPONENT(SPECIES_GEODUDE) { Level(5); Moves(MOVE_DOUBLE_EDGE); } + OPPONENT(SPECIES_NOSEPASS) { Level(5); Moves(MOVE_DOUBLE_EDGE); } + OPPONENT(SPECIES_HOUNDSTONE) { Level(5); Moves(MOVE_NIGHT_SHADE, MOVE_BODY_PRESS, MOVE_WILL_O_WISP, MOVE_PROTECT); Item(ITEM_LEFTOVERS); } + } WHEN { + TURN { MOVE(player, MOVE_FAKE_OUT); EXPECT_MOVES(opponent, MOVE_FAKE_OUT); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will not switch in a Pokemon which is slower and gets 1HKOed after fainting") +{ + bool32 alakazamFirst; + u32 speedAlakazm; + u32 aiSmartSwitchFlags = 0; + + PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = TRUE; } // AI will always send out Alakazan as it sees a KO with Focus Blast, even if Alakazam dies before it can get it off + PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = FALSE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES lets AI see that Alakazam would be KO'd before it can KO, and won't switch it in + PARAMETRIZE{ speedAlakazm = 400; alakazamFirst = TRUE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES recognizes that Alakazam is faster and can KO, and will switch it in + + GIVEN { + ASSUME(gMovesInfo[MOVE_PSYCHIC].category == DAMAGE_CATEGORY_SPECIAL); + ASSUME(gMovesInfo[MOVE_FOCUS_BLAST].category == DAMAGE_CATEGORY_SPECIAL); + ASSUME(gMovesInfo[MOVE_BUBBLE_BEAM].category == DAMAGE_CATEGORY_SPECIAL); + ASSUME(gMovesInfo[MOVE_WATER_GUN].category == DAMAGE_CATEGORY_SPECIAL); + ASSUME(gMovesInfo[MOVE_STRENGTH].category == DAMAGE_CATEGORY_PHYSICAL); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); + PLAYER(SPECIES_WEAVILE) { Speed(300); Ability(ABILITY_SHADOW_TAG); } // Weavile has Shadow Tag, so AI can't switch on the first turn, but has to do it after fainting. + OPPONENT(SPECIES_KADABRA) { Speed(200); Moves(MOVE_PSYCHIC, MOVE_DISABLE, MOVE_TAUNT, MOVE_CALM_MIND); } + OPPONENT(SPECIES_ALAKAZAM) { Speed(speedAlakazm); Moves(MOVE_FOCUS_BLAST, MOVE_PSYCHIC); } // Alakazam has a move which OHKOes Weavile, but it doesn't matter if he's getting KO-ed first. + OPPONENT(SPECIES_BLASTOISE) { Speed(200); Moves(MOVE_BUBBLE_BEAM, MOVE_WATER_GUN, MOVE_LEER, MOVE_STRENGTH); } // Can't OHKO, but survives a hit from Weavile's Night Slash. + } WHEN { + TURN { MOVE(player, MOVE_NIGHT_SLASH) ; EXPECT_SEND_OUT(opponent, alakazamFirst ? 1 : 2); } // AI doesn't send out Alakazam if it gets outsped + } SCENE { + MESSAGE("Foe Kadabra fainted!"); + if (alakazamFirst) { + MESSAGE("{PKMN} TRAINER LEAF sent out Alakazam!"); + } else { + MESSAGE("{PKMN} TRAINER LEAF sent out Blastoise!"); + } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI considers hazard damage when choosing which Pokemon to switch in") +{ + u32 aiIsSmart = 0; + u32 aiSmartSwitchFlags = 0; + + PARAMETRIZE{ aiIsSmart = 0; aiSmartSwitchFlags = 0; } // AI doesn't care about hazard damage resulting in Pokemon being KO'd + PARAMETRIZE{ aiIsSmart = 1; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES avoids being KO'd as a result of hazards damage + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); + PLAYER(SPECIES_MEGANIUM) { Speed(100); SpDefense(328); SpAttack(265); Moves(MOVE_STEALTH_ROCK, MOVE_SURF); } // Meganium does ~56% minimum ~66% maximum, enough to KO Charizard after rocks and never KO Typhlosion after rocks + OPPONENT(SPECIES_PONYTA) { Level(5); Speed(5); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_CHARIZARD) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium + OPPONENT(SPECIES_TYPHLOSION) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium + } WHEN { + TURN { MOVE(player, MOVE_STEALTH_ROCK) ;} + TURN { MOVE(player, MOVE_SURF); EXPECT_SEND_OUT(opponent, aiIsSmart ? 2 : 1); } // AI sends out Typhlosion to get the KO with the flag rather than Charizard + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize type matchup + SE move, then type matchup") +{ + u32 aiSmartSwitchFlags = 0; + u32 move1; + u32 move2; + u32 expectedIndex; + + PARAMETRIZE{ expectedIndex = 3; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; } // When not smart, AI will only switch in a defensive mon if it has a SE move, otherwise will just default to damage + PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; } + PARAMETRIZE{ expectedIndex = 2; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // When smart, AI will prioritize SE move, but still switch in good type matchup without SE move + PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); + PLAYER(SPECIES_MARSHTOMP) { Level(30); Moves(MOVE_MUD_BOMB, MOVE_WATER_GUN, MOVE_GROWL, MOVE_MUD_SHOT); Speed(5); } + OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(6); } // Forces switchout + OPPONENT(SPECIES_TANGELA) { Level(30); Moves(move1); Speed(4); } + OPPONENT(SPECIES_LOMBRE) { Level(30); Moves(move2); Speed(4); } + OPPONENT(SPECIES_HARIYAMA) { Level(30); Moves(MOVE_VITAL_THROW); Speed(4); } + } WHEN { + TURN { MOVE(player, MOVE_GROWL); EXPECT_SWITCH(opponent, expectedIndex); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize defensive options") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } + OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout + OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); SpDefense(41); } // Mid battle, AI sends out Aron + OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } + } WHEN { + TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SWITCH(opponent, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Post-KO switches prioritize offensive options") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } + OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_TACKLE); Speed(4); } + OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } // Mid battle, AI sends out Aron + OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } + } WHEN { + TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SEND_OUT(opponent, 2); } + } +} + +// General AI_FLAG_SMART_SWITCHING behaviour +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI switches out after sufficient stat drops") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); + PLAYER(SPECIES_HITMONTOP) { Level(30); Moves(MOVE_CHARM, MOVE_TACKLE); Ability(ABILITY_INTIMIDATE); Speed(5); } + OPPONENT(SPECIES_GRIMER) { Level(30); Moves(MOVE_TACKLE); Speed(4); } + OPPONENT(SPECIES_PONYTA) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } + } WHEN { + TURN { MOVE(player, MOVE_CHARM); } + TURN { MOVE(player, MOVE_TACKLE); EXPECT_SWITCH(opponent, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI will not switch out if Pokemon would faint to hazards unless party member can clear them") +{ + u32 move1; + + PARAMETRIZE{ move1 = MOVE_TACKLE; } + PARAMETRIZE{ move1 = MOVE_RAPID_SPIN; } + + GIVEN { + ASSUME(gMovesInfo[MOVE_TACKLE].category == DAMAGE_CATEGORY_PHYSICAL); + ASSUME(gMovesInfo[MOVE_RAPID_SPIN].category == DAMAGE_CATEGORY_PHYSICAL); + ASSUME(gMovesInfo[MOVE_EARTHQUAKE].category == DAMAGE_CATEGORY_PHYSICAL); + ASSUME(gMovesInfo[MOVE_HEADBUTT].category == DAMAGE_CATEGORY_PHYSICAL); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); + PLAYER(SPECIES_HITMONTOP) { Level(30); Moves(MOVE_CHARM, MOVE_TACKLE, MOVE_STEALTH_ROCK, MOVE_EARTHQUAKE); Ability(ABILITY_INTIMIDATE); Speed(5); } + OPPONENT(SPECIES_GRIMER) { Level(30); Moves(MOVE_TACKLE); Item(ITEM_FOCUS_SASH); Speed(4); } + OPPONENT(SPECIES_PONYTA) { Level(30); Moves(MOVE_HEADBUTT, move1); Speed(4); } + } WHEN { + TURN { MOVE(player, MOVE_STEALTH_ROCK); } + TURN { MOVE(player, MOVE_EARTHQUAKE); } + TURN { MOVE(player, MOVE_CHARM); } + TURN { // If the AI has a mon that can remove hazards, don't prevent them switching out + MOVE(player, MOVE_CHARM); + if (move1 == MOVE_RAPID_SPIN) + EXPECT_SWITCH(opponent, 1); + else if (move1 == MOVE_TACKLE) + EXPECT_MOVE(opponent, MOVE_TACKLE); + } + } +} + +// Trapping behaviour +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{ + ASSUME(gSpeciesInfo[SPECIES_MAWILE].types[0] == TYPE_STEEL); + 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); } + 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{ + ASSUME(gSpeciesInfo[SPECIES_MAWILE].types[0] == TYPE_STEEL); + 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); } + 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); } + } +}