From 06c54a2d607dfe0223aca6f0af2773e62bf157e2 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Sun, 13 Jul 2025 11:45:59 -0400 Subject: [PATCH 1/2] Fix switching 1v1 calcs not handling 0 (#7131) --- src/battle_ai_switch_items.c | 34 +++++++++++++++++++++++----------- test/battle/ai/ai_switching.c | 13 +++++++++++++ 2 files changed, 36 insertions(+), 11 deletions(-) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 9138f59d59..2ca97a5720 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -261,13 +261,10 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH) { damageTaken = AI_GetDamage(opposingBattler, battler, i, AI_DEFENDING, gAiLogicData); - if (playerMove == gBattleStruct->choicedMove[opposingBattler]) // If player is choiced, only care about the choice locked move + if (damageTaken > maxDamageTaken && !AI_DoesChoiceItemBlockMove(opposingBattler, playerMove)) { - return maxDamageTaken = damageTaken; - break; - } - if (damageTaken > maxDamageTaken) maxDamageTaken = damageTaken; + } } } @@ -281,7 +278,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) } // Check if current mon can 1v1 in spite of bad matchup, and don't switch out if it can - if (hitsToKoPlayer < hitsToKoAI || (hitsToKoPlayer == hitsToKoAI && AI_IsFaster(battler, opposingBattler, aiBestMove))) + if ((hitsToKoPlayer != 0 && (hitsToKoPlayer < hitsToKoAI || hitsToKoAI == 0)) || (hitsToKoPlayer == hitsToKoAI && AI_IsFaster(battler, opposingBattler, aiBestMove))) return FALSE; // If we don't have any other viable options, don't switch out @@ -1805,7 +1802,7 @@ static u32 GetSwitchinHitsToKO(s32 damageTaken, u32 battler) // No damage being dealt if ((damageTaken + statusDamage + recurringDamage <= recurringHealing) || damageTaken + statusDamage + recurringDamage == 0) - return startingHP; + return hitsToKO; // Mon fainted to hazards if (startingHP == 0) @@ -2012,6 +2009,18 @@ static inline bool32 IsFreeSwitch(enum SwitchType switchType, u32 battlerSwitchi static inline bool32 CanSwitchinWin1v1(u32 hitsToKOAI, u32 hitsToKOPlayer, bool32 isSwitchinFirst, bool32 isFreeSwitch) { + // Player's best move deals 0 damage + if (hitsToKOAI == 0 && hitsToKOPlayer > 0) + return TRUE; + + // AI's best move deals 0 damage + if (hitsToKOPlayer == 0 && hitsToKOAI > 0) + return FALSE; + + // Neither mon can damage the other + if (hitsToKOPlayer == 0 && hitsToKOAI == 0) + return FALSE; + // Free switch, need to outspeed or take 1 extra hit if (isFreeSwitch) { @@ -2028,7 +2037,7 @@ static inline bool32 CanSwitchinWin1v1(u32 hitsToKOAI, u32 hitsToKOPlayer, bool3 // Everything runs in the same loop to minimize computation time. This makes it harder to read, but hopefully the comments can guide you! static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, u32 battler, u32 opposingBattler, u32 battlerIn1, u32 battlerIn2, enum SwitchType switchType) { - int revengeKillerId = PARTY_SIZE, slowRevengeKillerId = PARTY_SIZE, fastThreatenId = PARTY_SIZE, slowThreatenId = PARTY_SIZE, damageMonId = PARTY_SIZE; + int revengeKillerId = PARTY_SIZE, slowRevengeKillerId = PARTY_SIZE, fastThreatenId = PARTY_SIZE, slowThreatenId = PARTY_SIZE, damageMonId = PARTY_SIZE, generic1v1MonId = 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, aceMonCount = 0; s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed @@ -2073,9 +2082,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, for (j = 0; j < MAX_MON_MOVES; j++) { aiMove = gAiLogicData->switchinCandidate.battleMon.moves[j]; - - if (aiMove != MOVE_NONE && !IsBattleMoveStatus(aiMove)) - damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, AI_ATTACKING); + damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, AI_ATTACKING); // Offensive switchin decisions are based on which whether switchin moves first and whether it can win a 1v1 isSwitchinFirst = AI_WhoStrikesFirstPartyMon(battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, aiMove); @@ -2106,6 +2113,9 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, defensiveMonId = i; } + if (canSwitchinWin1v1) + generic1v1MonId = i; + // Check for mon with resistance and super effective move for best type matchup mon with effective move if (aiMove != MOVE_NONE && !IsBattleMoveStatus(aiMove)) { @@ -2184,6 +2194,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, else if (typeMatchupEffectiveId != PARTY_SIZE) return typeMatchupEffectiveId; else if (typeMatchupId != PARTY_SIZE) return typeMatchupId; else if (batonPassId != PARTY_SIZE) return batonPassId; + else if (generic1v1MonId != PARTY_SIZE) return generic1v1MonId; else if (damageMonId != PARTY_SIZE) return damageMonId; } else @@ -2194,6 +2205,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, else if (typeMatchupId != PARTY_SIZE) return typeMatchupId; else if (defensiveMonId != PARTY_SIZE) return defensiveMonId; else if (batonPassId != PARTY_SIZE) return batonPassId; + else if (generic1v1MonId != PARTY_SIZE) return generic1v1MonId; } // If ace mon is the last available Pokemon and U-Turn/Volt Switch or Eject Pack/Button was used - switch to the mon. if (aceMonId != PARTY_SIZE && CountUsablePartyMons(battler) <= aceMonCount diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index e9ad61bdfa..0c9e94695e 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -1255,3 +1255,16 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI won't send out defensive mon TURN { MOVE(player, MOVE_WATER_PULSE); EXPECT_MOVE(opponent, MOVE_BULLDOZE); } } } + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_SWITCHING: AI considers 0 hits to KO as losing a 1v1") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_JOLTEON) { Level(100); Ability(ABILITY_VOLT_ABSORB); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_ZIGZAGOON) { Level(1); HP(1); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_TANGELA) { Level(100); Moves(MOVE_THUNDERBOLT); } + OPPONENT(SPECIES_TANGELA) { Level(100); Moves(MOVE_GIGA_DRAIN); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); EXPECT_SEND_OUT(opponent, 2); } + } +} From 939e168c041dc8499b50671a7b8a1645627b8402 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Sun, 13 Jul 2025 13:24:14 -0400 Subject: [PATCH 2/2] Fix AI_DoesChoiceEffectBlockMove typo (#7335) --- src/battle_ai_switch_items.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 2ca97a5720..c044ff2804 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -261,7 +261,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler) if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH) { damageTaken = AI_GetDamage(opposingBattler, battler, i, AI_DEFENDING, gAiLogicData); - if (damageTaken > maxDamageTaken && !AI_DoesChoiceItemBlockMove(opposingBattler, playerMove)) + if (damageTaken > maxDamageTaken && !AI_DoesChoiceEffectBlockMove(opposingBattler, playerMove)) { maxDamageTaken = damageTaken; }