From e44d0326be7ffcad364aaec3d56a69c14611f9d4 Mon Sep 17 00:00:00 2001 From: surskitty Date: Fri, 16 May 2025 11:41:03 -0400 Subject: [PATCH] AI double battle friendly fire logic improvements (#6766) --- include/battle_ai_util.h | 6 + include/config/ai.h | 5 + src/battle_ai_main.c | 373 +++++++++++++++++++++---- src/battle_ai_util.c | 117 ++++++++ test/battle/ai/ai.c | 171 ------------ test/battle/ai/ai_check_viability.c | 15 - test/battle/ai/ai_doubles.c | 412 ++++++++++++++++++++++++++++ 7 files changed, 857 insertions(+), 242 deletions(-) create mode 100644 test/battle/ai/ai_doubles.c diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 03e6b9e21f..70ff0e976c 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -197,6 +197,10 @@ bool32 ShouldTrap(u32 battlerAtk, u32 battlerDef, u32 move); bool32 IsWakeupTurn(u32 battler); bool32 AI_IsBattlerAsleepOrComatose(u32 battlerId); +// ability logic +bool32 IsMoxieTypeAbility(u32 ability); +bool32 ShouldTriggerAbility(u32 battler, u32 ability); + // partner logic #define IS_TARGETING_PARTNER(battlerAtk, battlerDef)((battlerAtk) == (battlerDef ^ BIT_FLANK)) u32 GetAllyChosenMove(u32 battlerId); @@ -212,6 +216,8 @@ bool32 PartnerMoveIsSameAsAttacker(u32 battlerAtkPartner, u32 battlerDef, u32 mo bool32 PartnerMoveIsSameNoTarget(u32 battlerAtkPartner, u32 move, u32 partnerMove); bool32 PartnerMoveActivatesSleepClause(u32 move); bool32 ShouldUseWishAromatherapy(u32 battlerAtk, u32 battlerDef, u32 move); +u32 GetFriendlyFireKOThreshold(u32 battler); +bool32 IsAllyProtectingFromMove(u32 battlerAtk, u32 attackerMove, u32 allyMove); // party logic struct BattlePokemon *AllocSaveBattleMons(void); diff --git a/include/config/ai.h b/include/config/ai.h index c734eb2c1d..db98f72ee9 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -69,4 +69,9 @@ // Score reduction if any roll for PP stall detection passes #define PP_STALL_SCORE_REDUCTION 20 +// AI's acceptable number of hits to KO the partner via friendly fire in a double battle. +#define FRIENDLY_FIRE_RISKY_THRESHOLD 2 +#define FRIENDLY_FIRE_NORMAL_THRESHOLD 3 +#define FRIENDLY_FIRE_CONSERVATIVE_THRESHOLD 4 + #endif // GUARD_CONFIG_AI_H diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 9fea906cff..7ac5065643 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1110,7 +1110,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) RETURN_SCORE_MINUS(20); break; case ABILITY_JUSTIFIED: - if (moveType == TYPE_DARK && !IsBattleMoveStatus(move)) + if (moveType == TYPE_DARK && !IsBattleMoveStatus(move) && !IS_TARGETING_PARTNER(battlerAtk, battlerDef)) RETURN_SCORE_MINUS(10); break; case ABILITY_RATTLED: @@ -2221,8 +2221,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) && move != MOVE_CRAFTY_SHIELD) //These moves have infinite usage { if (GetBattlerSecondaryDamage(battlerAtk) >= gBattleMons[battlerAtk].hp - && aiData->abilities[battlerDef] != ABILITY_MOXIE - && aiData->abilities[battlerDef] != ABILITY_BEAST_BOOST) + && !(IsMoxieTypeAbility(aiData->abilities[battlerDef]))) { ADJUST_SCORE(-10); //Don't protect if you're going to faint after protecting } @@ -2929,7 +2928,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) u32 atkPartnerAbility = aiData->abilities[BATTLE_PARTNER(battlerAtk)]; u32 atkPartnerHoldEffect = aiData->holdEffects[BATTLE_PARTNER(battlerAtk)]; enum BattleMoveEffects partnerEffect = GetMoveEffect(aiData->partnerMove); - bool32 partnerProtecting = (partnerEffect == EFFECT_PROTECT); + bool32 partnerProtecting = IsAllyProtectingFromMove(battlerAtk, move, aiData->partnerMove) && !MoveIgnoresProtect(move); bool32 attackerHasBadAbility = (gAbilitiesInfo[aiData->abilities[battlerAtk]].aiRating < 0); bool32 partnerHasBadAbility = (gAbilitiesInfo[atkPartnerAbility].aiRating < 0); u32 predictedMove = GetIncomingMove(battlerAtk, battlerDef, gAiLogicData); @@ -2937,6 +2936,12 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) SetTypeBeforeUsingMove(move, battlerAtk); moveType = GetBattleMoveType(move); + u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk); + u32 noOfHitsToKOPartner = GetNoOfHitsToKOBattler(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING); + bool32 wouldPartnerFaint = CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING) + && !partnerProtecting && IsBattlerAlive(battlerAtkPartner); + bool32 isFriendlyFireOK = !wouldPartnerFaint && (noOfHitsToKOPartner == 0 || noOfHitsToKOPartner > friendlyFireThreshold); + // check what effect partner is using if (aiData->partnerMove != 0) { @@ -2987,7 +2992,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { case EFFECT_HELPING_HAND: if (!IsBattlerAlive(battlerAtkPartner) || !HasDamagingMove(battlerAtkPartner)) - ADJUST_SCORE(-5); + ADJUST_SCORE(-20); break; case EFFECT_PERISH_SONG: if (aiData->partnerMove != 0 && HasTrappingMoveEffect(battlerAtkPartner)) @@ -3054,9 +3059,103 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) break; } // global move effect check + // Specific logic for spread moves. + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + // Don't kill your partner for no reason. + if (wouldPartnerFaint) + { + // If it kills both opponents, that's a good reason. + if (CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING) + && CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), gAiThinkingStruct->movesetIndex, AI_ATTACKING)) + { + switch (effect) + { + case EFFECT_EXPLOSION: + if (gAiThinkingStruct->aiFlags[battlerAtk] & (AI_FLAG_RISKY | AI_FLAG_WILL_SUICIDE)) + { + RETURN_SCORE_PLUS(10); + } + break; + default: + RETURN_SCORE_PLUS(10); + break; + } + } + // Both opponents can kill the partner + else if (CanTargetFaintAi(BATTLE_OPPOSITE(battlerAtk), battlerAtkPartner) + && (CanTargetFaintAi(BATTLE_OPPOSITE(battlerAtkPartner), battlerAtkPartner))) + { + // The spread move should kill an opponent. + if (CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING) + || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), gAiThinkingStruct->movesetIndex, AI_ATTACKING)) + { + ADJUST_SCORE(WEAK_EFFECT); + } + + // Alternatively, it benefits from the ally's death, and it will probably die anyway. + if (IsMoxieTypeAbility(aiData->abilities[battlerAtk])) + { + ADJUST_SCORE(GOOD_EFFECT); + } + if ((aiData->abilities[battlerAtk] == ABILITY_RECEIVER) && !partnerHasBadAbility) + { + ADJUST_SCORE(GOOD_EFFECT); + } + if (HasMove(battlerAtk, MOVE_LAST_RESPECTS)) + { + ADJUST_SCORE(GOOD_EFFECT); + } + } + // It can kill one opponent when killing its ally, and its ally is not in extreme danger. + // This is easy for the player to cheese. + else if (CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING) + || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), gAiThinkingStruct->movesetIndex, AI_ATTACKING)) + { + if (gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_WILL_SUICIDE) + { + RETURN_SCORE_PLUS(10); + } + else + { + ADJUST_SCORE(-DECENT_EFFECT); + } + } + // No reason to kill partner has been found. + else + { + RETURN_SCORE_MINUS(10); + } + } + + // Partner will not faint. + else { + + // Triggering your ally's hold item should only be done deliberately with a spread move. + switch (atkPartnerHoldEffect) + { + case HOLD_EFFECT_WEAKNESS_POLICY: + if (aiData->effectiveness[battlerAtk][battlerAtkPartner][gAiThinkingStruct->movesetIndex] >= UQ_4_12(2.0) && isFriendlyFireOK) + { + ADJUST_SCORE(GOOD_EFFECT); + } + break; + default: + break; + } + } + } + // check specific target if (IS_TARGETING_PARTNER(battlerAtk, battlerDef)) { + bool32 isMoveAffectedByPartnerAbility = TRUE; + + if (wouldPartnerFaint) + { + RETURN_SCORE_MINUS(30); + } + // partner ability checks if (!partnerProtecting && moveTarget != MOVE_TARGET_BOTH && !DoesBattlerIgnoreAbilityChecks(battlerAtk, aiData->abilities[battlerAtk], move)) { @@ -3066,95 +3165,241 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (MoveAlwaysCrits(move) && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK) && AI_IsFaster(battlerAtk, battlerAtkPartner, move) - && !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING)) + && isFriendlyFireOK) { - RETURN_SCORE_PLUS(GOOD_EFFECT); + if (MoveAlwaysCrits(move)) + { + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + RETURN_SCORE_PLUS(GOOD_EFFECT); + } + // todo: figuring out a non-guaranteed crit roll that's useful } - break; - case ABILITY_VOLT_ABSORB: - if (!(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_HP_AWARE)) + else { - RETURN_SCORE_MINUS(10); - } - break; // handled in AI_HPAware - case ABILITY_MOTOR_DRIVE: - if (moveType == TYPE_ELECTRIC && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPEED)) - { - RETURN_SCORE_PLUS(WEAK_EFFECT); + isMoveAffectedByPartnerAbility = FALSE; } break; case ABILITY_LIGHTNING_ROD: - if (B_REDIRECT_ABILITY_IMMUNITY >= GEN_5 - && moveType == TYPE_ELECTRIC - && HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_SPECIAL) - && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPATK)) + case ABILITY_MOTOR_DRIVE: + case ABILITY_VOLT_ABSORB: + if (moveType == TYPE_ELECTRIC) { - RETURN_SCORE_PLUS(WEAK_EFFECT); + if (B_REDIRECT_ABILITY_IMMUNITY < GEN_5 && atkPartnerAbility == ABILITY_LIGHTNING_ROD) + { + RETURN_SCORE_MINUS(10); + } + + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + else if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) + { + RETURN_SCORE_PLUS(WEAK_EFFECT); + } + else + { + RETURN_SCORE_MINUS(10); + } + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } break; - case ABILITY_WATER_ABSORB: - case ABILITY_DRY_SKIN: case ABILITY_EARTH_EATER: - if (!(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_HP_AWARE)) + case ABILITY_LEVITATE: + if (moveType == TYPE_GROUND) { - RETURN_SCORE_MINUS(10); + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + else if (atkPartnerAbility == ABILITY_EARTH_EATER && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_HP_AWARE)) + { + RETURN_SCORE_MINUS(10); + } + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } break; // handled in AI_HPAware + case ABILITY_DRY_SKIN: + case ABILITY_WATER_ABSORB: case ABILITY_STORM_DRAIN: - if (B_REDIRECT_ABILITY_IMMUNITY >= GEN_5 - && moveType == TYPE_WATER - && HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_SPECIAL) - && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPATK)) + if (moveType == TYPE_WATER) { - RETURN_SCORE_PLUS(WEAK_EFFECT); + if (B_REDIRECT_ABILITY_IMMUNITY < GEN_5 && atkPartnerAbility == ABILITY_STORM_DRAIN) + { + RETURN_SCORE_MINUS(10); + } + + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + else if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) + { + RETURN_SCORE_PLUS(WEAK_EFFECT); + } + else + { + RETURN_SCORE_MINUS(10); + } + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } break; case ABILITY_WATER_COMPACTION: - if (moveType == TYPE_WATER && GetNoOfHitsToKOBattler(battlerAtk, battlerDef, gAiThinkingStruct->movesetIndex, AI_ATTACKING) >= 4) + if (moveType == TYPE_WATER && isFriendlyFireOK + && ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) { - RETURN_SCORE_PLUS(WEAK_EFFECT); // only mon with this ability is weak to water so only make it okay if we do very little damage + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + + if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS) + { + ADJUST_SCORE(DECENT_EFFECT); + } + } + else + { + isMoveAffectedByPartnerAbility = FALSE; + } + break; + case ABILITY_STEAM_ENGINE: + if (isFriendlyFireOK && (moveType == TYPE_WATER || moveType == TYPE_FIRE) + && ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) + { + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + RETURN_SCORE_PLUS(WEAK_EFFECT); + } + else + { + isMoveAffectedByPartnerAbility = FALSE; + } + break; + case ABILITY_THERMAL_EXCHANGE: + if (moveType == TYPE_FIRE && isFriendlyFireOK + && !IsBattleMoveStatus(move) + && ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) + { + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + + if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS) + { + ADJUST_SCORE(WEAK_EFFECT); + } + + RETURN_SCORE_PLUS(WEAK_EFFECT); + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } - RETURN_SCORE_MINUS(10); break; case ABILITY_FLASH_FIRE: - if (moveType == TYPE_FIRE - && HasMoveWithType(battlerAtkPartner, TYPE_FIRE) - && !gDisableStructs[battlerAtkPartner].flashFireBoosted) + case ABILITY_WELL_BAKED_BODY: + if (moveType == TYPE_FIRE) { - RETURN_SCORE_PLUS(WEAK_EFFECT); + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) + { + RETURN_SCORE_PLUS(WEAK_EFFECT); + } + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } break; case ABILITY_SAP_SIPPER: - if (moveType == TYPE_GRASS - && HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_PHYSICAL) - && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)) + if (moveType == TYPE_GRASS) { - RETURN_SCORE_PLUS(WEAK_EFFECT); + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } + + if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) + { + RETURN_SCORE_PLUS(WEAK_EFFECT); + } + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } break; case ABILITY_JUSTIFIED: - if (moveType == TYPE_DARK + if (moveType == TYPE_DARK && isFriendlyFireOK && !IsBattleMoveStatus(move) - && HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_PHYSICAL) - && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK) - && !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING)) + && ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) { - RETURN_SCORE_PLUS(WEAK_EFFECT); + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(GOOD_EFFECT); + } + + if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS) + { + ADJUST_SCORE(WEAK_EFFECT); + } + + ADJUST_SCORE(WEAK_EFFECT); // Beat Up is handled later + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } break; case ABILITY_RATTLED: - if (!IsBattleMoveStatus(move) + if (!IsBattleMoveStatus(move) && isFriendlyFireOK && (moveType == TYPE_DARK || moveType == TYPE_GHOST || moveType == TYPE_BUG) - && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPEED) - && !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING)) + && ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) { + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(DECENT_EFFECT); + } RETURN_SCORE_PLUS(WEAK_EFFECT); } + else + { + isMoveAffectedByPartnerAbility = FALSE; + } break; case ABILITY_CONTRARY: - if (IsStatLoweringEffect(effect)) + case ABILITY_DEFIANT: + case ABILITY_COMPETITIVE: + if (IsStatLoweringEffect(effect) && isFriendlyFireOK && ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility)) { - RETURN_SCORE_PLUS(DECENT_EFFECT); + if (moveTarget == MOVE_TARGET_FOES_AND_ALLY) + { + ADJUST_SCORE(GOOD_EFFECT); + } + RETURN_SCORE_PLUS(WEAK_EFFECT); + } + else + { + isMoveAffectedByPartnerAbility = FALSE; } break; } @@ -3163,6 +3408,11 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) // attacker move effects specifically targeting partner if (!partnerProtecting) { + if (wouldPartnerFaint) + { + RETURN_SCORE_MINUS(30); + } + switch (effect) { case EFFECT_SPICY_EXTRACT: @@ -3200,11 +3450,16 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_BEAT_UP: if (atkPartnerAbility == ABILITY_JUSTIFIED && moveType == TYPE_DARK + && !DoesBattlerIgnoreAbilityChecks(battlerAtk, aiData->abilities[battlerAtk], move) && !IsBattleMoveStatus(move) && HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_PHYSICAL) && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK) - && !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING)) + && !wouldPartnerFaint) { + if (isFriendlyFireOK) + { + ADJUST_SCORE(DECENT_EFFECT); + } RETURN_SCORE_PLUS(WEAK_EFFECT); } break; @@ -3298,7 +3553,12 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) } // attacker move effects } // check partner protecting - ADJUST_SCORE(-30); // otherwise, don't target partner + if ((isMoveAffectedByPartnerAbility && (score <= AI_SCORE_DEFAULT)) || !isMoveAffectedByPartnerAbility) + { + RETURN_SCORE_MINUS(10); + } + + } else // checking opponent { @@ -3356,12 +3616,13 @@ static inline bool32 ShouldUseSpreadDamageMove(u32 battlerAtk, u32 move, u32 mov { u32 partnerBattler = BATTLE_PARTNER(battlerAtk); u32 noOfHitsToFaintPartner = GetNoOfHitsToKOBattler(battlerAtk, partnerBattler, moveIndex, AI_ATTACKING); + u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk); return (IsDoubleBattle() && noOfHitsToFaintPartner != 0 // Immunity check && IsBattlerAlive(partnerBattler) && GetBattlerMoveTargetType(battlerAtk, move) == MOVE_TARGET_FOES_AND_ALLY - && !(noOfHitsToFaintPartner < 4 && hitsToFaintOpposingBattler == 1) - && noOfHitsToFaintPartner < 7); + && !(noOfHitsToFaintPartner < friendlyFireThreshold && hitsToFaintOpposingBattler == 1) + && noOfHitsToFaintPartner < (friendlyFireThreshold * 2)); } static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId) diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 3c08f108dc..6a1e6ea681 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -1588,6 +1588,51 @@ bool32 IsHazardClearingMove(u32 move) return FALSE; } +bool32 IsAllyProtectingFromMove(u32 battlerAtk, u32 attackerMove, u32 allyMove) +{ + enum BattleMoveEffects effect = GetMoveEffect(allyMove); + + if (effect != EFFECT_PROTECT) + { + return FALSE; + } + else + { + enum ProtectMethod protectMethod = GetMoveProtectMethod(allyMove); + + if (protectMethod == PROTECT_QUICK_GUARD) + { + u32 priority = GetBattleMovePriority(battlerAtk, gAiLogicData->abilities[battlerAtk], attackerMove); + return (priority > 0); + } + + if (IsBattleMoveStatus(attackerMove)) + { + switch (protectMethod) + { + case PROTECT_NORMAL: + case PROTECT_CRAFTY_SHIELD: + case PROTECT_MAX_GUARD: + case PROTECT_WIDE_GUARD: + return TRUE; + + default: + return FALSE; + } + } + else + { + switch (protectMethod) + { + case PROTECT_CRAFTY_SHIELD: + return FALSE; + default: + return TRUE; + } + } + } +} + bool32 IsMoveRedirectionPrevented(u32 battlerAtk, u32 move, u32 atkAbility) { if (gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_NEGATE_UNAWARE) @@ -4566,6 +4611,78 @@ bool32 HasBattlerSideAbility(u32 battler, u32 ability, struct AiLogicData *aiDat return FALSE; } +u32 GetFriendlyFireKOThreshold(u32 battler) +{ + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_RISKY) + return FRIENDLY_FIRE_RISKY_THRESHOLD; + if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_CONSERVATIVE) + return FRIENDLY_FIRE_CONSERVATIVE_THRESHOLD; + + return FRIENDLY_FIRE_NORMAL_THRESHOLD; +} + +bool32 IsMoxieTypeAbility(u32 ability) +{ + switch (ability) + { + case ABILITY_MOXIE: + case ABILITY_BEAST_BOOST: + case ABILITY_CHILLING_NEIGH: + case ABILITY_AS_ONE_ICE_RIDER: + case ABILITY_GRIM_NEIGH: + case ABILITY_AS_ONE_SHADOW_RIDER: + return TRUE; + default: + return FALSE; + } +} + +// Should the AI use a spread move to deliberately activate its partner's ability? +bool32 ShouldTriggerAbility(u32 battler, u32 ability) +{ + switch (ability) + { + case ABILITY_LIGHTNING_ROD: + case ABILITY_STORM_DRAIN: + if (B_REDIRECT_ABILITY_IMMUNITY < GEN_5) + return FALSE; + else + return (BattlerStatCanRise(battler, ability, STAT_SPATK) && HasMoveWithCategory(battler, DAMAGE_CATEGORY_SPECIAL)); + + case ABILITY_DEFIANT: + case ABILITY_JUSTIFIED: + case ABILITY_MOXIE: + case ABILITY_SAP_SIPPER: + case ABILITY_THERMAL_EXCHANGE: + return (BattlerStatCanRise(battler, ability, STAT_ATK) && HasMoveWithCategory(battler, DAMAGE_CATEGORY_PHYSICAL)); + + case ABILITY_COMPETITIVE: + return (BattlerStatCanRise(battler, ability, STAT_SPATK) && HasMoveWithCategory(battler, DAMAGE_CATEGORY_SPECIAL)); + + case ABILITY_CONTRARY: + return TRUE; + + case ABILITY_DRY_SKIN: + case ABILITY_VOLT_ABSORB: + case ABILITY_WATER_ABSORB: + return (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_HP_AWARE); + + case ABILITY_RATTLED: + case ABILITY_STEAM_ENGINE: + return BattlerStatCanRise(battler, ability, STAT_SPEED); + + case ABILITY_FLASH_FIRE: + return (HasMoveWithType(battler, TYPE_FIRE) && !gDisableStructs[battler].flashFireBoosted); + + case ABILITY_WATER_COMPACTION: + case ABILITY_WELL_BAKED_BODY: + return (BattlerStatCanRise(battler, ability, STAT_DEF)); + + default: + return FALSE; + } +} + u32 GetThinkingBattler(u32 battler) { if (gAiLogicData->aiPredictionInProgress) diff --git a/test/battle/ai/ai.c b/test/battle/ai/ai.c index 3f799d09ab..55942c2834 100644 --- a/test/battle/ai/ai.c +++ b/test/battle/ai/ai.c @@ -369,88 +369,6 @@ AI_SINGLE_BATTLE_TEST("AI won't use ground type attacks against flying type Poke } } -AI_DOUBLE_BATTLE_TEST("AI won't use a Weather changing move if partner already chose such move") -{ - u32 j, k; - static const u16 weatherMoves[] = {MOVE_SUNNY_DAY, MOVE_HAIL, MOVE_RAIN_DANCE, MOVE_SANDSTORM, MOVE_SNOWSCAPE}; - u16 weatherMoveLeft = MOVE_NONE, weatherMoveRight = MOVE_NONE; - - for (j = 0; j < ARRAY_COUNT(weatherMoves); j++) - { - for (k = 0; k < ARRAY_COUNT(weatherMoves); k++) - { - PARAMETRIZE { weatherMoveLeft = weatherMoves[j]; weatherMoveRight = weatherMoves[k]; } - } - } - - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_WOBBUFFET) { Moves(weatherMoveLeft); } - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SCRATCH, weatherMoveRight); } - } WHEN { - TURN { NOT_EXPECT_MOVE(opponentRight, weatherMoveRight); - SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerLeft); - SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerRight); - SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:opponentLeft); - } - } -} - -AI_DOUBLE_BATTLE_TEST("AI will not use Helping Hand if partner does not have any damage moves") -{ - u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; - - PARAMETRIZE { move1 = MOVE_LEER; move2 = MOVE_TOXIC; } - PARAMETRIZE { move1 = MOVE_HELPING_HAND; move2 = MOVE_PROTECT; } - PARAMETRIZE { move1 = MOVE_ACUPRESSURE; move2 = MOVE_DOUBLE_TEAM; move3 = MOVE_TOXIC; move4 = MOVE_PROTECT; } - - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND, MOVE_SCRATCH); } - OPPONENT(SPECIES_WOBBUFFET) { Moves(move1, move2, move3, move4); } - } WHEN { - TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_HELPING_HAND); - SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft); - SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerRight); - SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:opponentLeft); - } - } SCENE { - NOT MESSAGE("The opposing Wobbuffet used Helping Hand!"); - } -} - -AI_DOUBLE_BATTLE_TEST("AI will not use a status move if partner already chose Helping Hand") -{ - s32 j; - u32 statusMove = MOVE_NONE; - - for (j = MOVE_NONE + 1; j < MOVES_COUNT; j++) - { - if (GetMoveCategory(j) == DAMAGE_CATEGORY_STATUS) { - PARAMETRIZE { statusMove = j; } - } - } - - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND); } - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SCRATCH, statusMove); } - } WHEN { - TURN { NOT_EXPECT_MOVE(opponentRight, statusMove); - SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerLeft); - SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerRight); - SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:opponentLeft); - } - } SCENE { - MESSAGE("The opposing Wobbuffet used Helping Hand!"); - } -} AI_SINGLE_BATTLE_TEST("AI without any flags chooses moves at random - singles") { @@ -641,79 +559,6 @@ AI_SINGLE_BATTLE_TEST("AI will choose Superpower over Outrage with Contrary") } } -AI_DOUBLE_BATTLE_TEST("AI will not choose Earthquake if it damages the partner") -{ - u32 species; - - PARAMETRIZE { species = SPECIES_CHARIZARD; } - PARAMETRIZE { species = SPECIES_CHARMANDER; } - PARAMETRIZE { species = SPECIES_CHIKORITA; } - - GIVEN { - ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_PHANPY) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } - OPPONENT(species) { Moves(MOVE_CELEBRATE); } - } WHEN { - if (species == SPECIES_CHARIZARD) - TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } - else - TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH, target: playerLeft); } - } -} - -AI_DOUBLE_BATTLE_TEST("AI will choose Earthquake if partner is not alive") -{ - GIVEN { - ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } - OPPONENT(SPECIES_PIKACHU) { HP(1); Moves(MOVE_CELEBRATE); } - } WHEN { - TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentRight); } - TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } - } -} - -AI_DOUBLE_BATTLE_TEST("AI will choose Earthquake if it kill an opposing mon and does 1/3 of damage to AI") -{ - GIVEN { - ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - PLAYER(SPECIES_WOBBUFFET) { HP(1); } - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } - OPPONENT(SPECIES_PARAS) { Moves(MOVE_CELEBRATE); } - } WHEN { - TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } - } -} - -AI_DOUBLE_BATTLE_TEST("AI will the see a corresponding absorbing ability on partner to one of its moves") -{ - u32 ability; - PARAMETRIZE { ability = ABILITY_LIGHTNING_ROD; } - PARAMETRIZE { ability = ABILITY_STATIC; } - - GIVEN { - ASSUME(GetMoveTarget(MOVE_DISCHARGE) == MOVE_TARGET_FOES_AND_ALLY); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_WOBBUFFET); - PLAYER(SPECIES_WOBBUFFET); - OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_DISCHARGE, MOVE_SCRATCH); } - OPPONENT(SPECIES_PIKACHU) { HP(1); Ability(ability); Moves(MOVE_CELEBRATE); } - } WHEN { - if (ability == ABILITY_LIGHTNING_ROD) - TURN { EXPECT_MOVE(opponentLeft, MOVE_DISCHARGE); } - else - TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH); } - } -} - AI_SINGLE_BATTLE_TEST("AI calculates guaranteed criticals and detects critical immunity") { u32 ability; @@ -737,22 +582,6 @@ AI_SINGLE_BATTLE_TEST("AI calculates guaranteed criticals and detects critical i } } -AI_DOUBLE_BATTLE_TEST("AI recognizes Volt Absorb received from Trace") -{ - KNOWN_FAILING; // MGriffin's PR that switched two turn charging moves in AI tests broke this test, waiting on a fix - GIVEN { - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); - PLAYER(SPECIES_MAGNETON); - PLAYER(SPECIES_GARDEVOIR) { Ability(ABILITY_TRACE); } - OPPONENT(SPECIES_JOLTEON) { Ability(ABILITY_VOLT_ABSORB); Moves(MOVE_THUNDER_WAVE, MOVE_THUNDERSHOCK, MOVE_WATER_GUN); } - OPPONENT(SPECIES_JOLTEON) { Ability(ABILITY_VOLT_ABSORB); Moves(MOVE_THUNDER_WAVE, MOVE_THUNDERSHOCK, MOVE_WATER_GUN); } - } WHEN { - TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_THUNDERSHOCK); NOT_EXPECT_MOVE(opponentLeft, MOVE_THUNDER_WAVE); NOT_EXPECT_MOVE(opponentRight, MOVE_THUNDER_WAVE); } - } THEN { - EXPECT(gAiLogicData->abilities[B_POSITION_PLAYER_RIGHT] == ABILITY_VOLT_ABSORB); - } -} - AI_SINGLE_BATTLE_TEST("AI avoids contact moves against rocky helmet") { u32 item; diff --git a/test/battle/ai/ai_check_viability.c b/test/battle/ai/ai_check_viability.c index 2f30c3df3a..f80597f476 100644 --- a/test/battle/ai/ai_check_viability.c +++ b/test/battle/ai/ai_check_viability.c @@ -241,21 +241,6 @@ AI_SINGLE_BATTLE_TEST("AI chooses moves that cure inactive party members") } } -AI_DOUBLE_BATTLE_TEST("AI prioritizes Skill Swapping Contrary to allied mons that would benefit from it") -{ - GIVEN { - ASSUME(GetMoveEffect(MOVE_SKILL_SWAP) == EFFECT_SKILL_SWAP); - ASSUME(GetMoveAdditionalEffectById(MOVE_OVERHEAT, 0)->moveEffect == MOVE_EFFECT_SP_ATK_MINUS_2); - AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_DOUBLE_BATTLE); - PLAYER(SPECIES_WOBBUFFET) { Speed(3); } - PLAYER(SPECIES_WOBBUFFET) { Speed(3); } - OPPONENT(SPECIES_SPINDA) { Ability(ABILITY_CONTRARY); Speed(5); Moves(MOVE_SKILL_SWAP, MOVE_ENCORE, MOVE_FAKE_TEARS, MOVE_SWAGGER); } - OPPONENT(SPECIES_ARCANINE) { Ability(ABILITY_INTIMIDATE); Speed(4); Moves (MOVE_OVERHEAT); } - } WHEN { - TURN { EXPECT_MOVE(opponentLeft, MOVE_SKILL_SWAP, target:opponentRight); EXPECT_MOVE(opponentRight, MOVE_OVERHEAT); } - } -} - AI_SINGLE_BATTLE_TEST("AI prioritizes Pursuit if it would KO opponent") { GIVEN { diff --git a/test/battle/ai/ai_doubles.c b/test/battle/ai/ai_doubles.c new file mode 100644 index 0000000000..b162c8c67f --- /dev/null +++ b/test/battle/ai/ai_doubles.c @@ -0,0 +1,412 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +AI_DOUBLE_BATTLE_TEST("AI won't use a Weather changing move if partner already chose such move") +{ + u32 j, k; + static const u16 weatherMoves[] = {MOVE_SUNNY_DAY, MOVE_HAIL, MOVE_RAIN_DANCE, MOVE_SANDSTORM, MOVE_SNOWSCAPE}; + u16 weatherMoveLeft = MOVE_NONE, weatherMoveRight = MOVE_NONE; + + for (j = 0; j < ARRAY_COUNT(weatherMoves); j++) + { + for (k = 0; k < ARRAY_COUNT(weatherMoves); k++) + { + PARAMETRIZE { weatherMoveLeft = weatherMoves[j]; weatherMoveRight = weatherMoves[k]; } + } + } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(weatherMoveLeft); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SCRATCH, weatherMoveRight); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponentRight, weatherMoveRight); + SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerRight); + SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:opponentLeft); + } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will not use Helping Hand if partner does not have any damage moves") +{ + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + + PARAMETRIZE { move1 = MOVE_LEER; move2 = MOVE_TOXIC; } + PARAMETRIZE { move1 = MOVE_HELPING_HAND; move2 = MOVE_PROTECT; } + PARAMETRIZE { move1 = MOVE_ACUPRESSURE; move2 = MOVE_DOUBLE_TEAM; move3 = MOVE_TOXIC; move4 = MOVE_PROTECT; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND, MOVE_SCRATCH); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(move1, move2, move3, move4); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_HELPING_HAND); + SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerRight); + SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:opponentLeft); + } + } SCENE { + NOT MESSAGE("The opposing Wobbuffet used Helping Hand!"); + } +} + +AI_DOUBLE_BATTLE_TEST("AI will not use a status move if partner already chose Helping Hand") +{ + s32 j; + u32 statusMove = MOVE_NONE; + + for (j = MOVE_NONE + 1; j < MOVES_COUNT; j++) + { + if (GetMoveCategory(j) == DAMAGE_CATEGORY_STATUS) { + PARAMETRIZE { statusMove = j; } + } + } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SCRATCH, statusMove); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponentRight, statusMove); + SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerRight); + SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:opponentLeft); + } + } SCENE { + MESSAGE("The opposing Wobbuffet used Helping Hand!"); + } +} + +AI_DOUBLE_BATTLE_TEST("AI will not choose Earthquake if it damages the partner without a positive effect") +{ + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + + u32 species; + + PARAMETRIZE { species = SPECIES_CHARIZARD; } + PARAMETRIZE { species = SPECIES_CHARMANDER; } + PARAMETRIZE { species = SPECIES_CHIKORITA; } + + GIVEN { + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PHANPY) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } + OPPONENT(species) { Moves(MOVE_CELEBRATE); } + } WHEN { + if (species == SPECIES_CHARIZARD) + TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH, target: playerLeft); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI recognizes its ally's Telepathy") +{ + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + + GIVEN { + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PHANPY) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } + OPPONENT(SPECIES_ELGYEM) { Level(1); Ability(ABILITY_TELEPATHY); } + } WHEN { + TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will choose Bulldoze if it triggers its ally's ability but will not KO the ally needlessly") +{ + ASSUME(GetMoveTarget(MOVE_BULLDOZE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_BULLDOZE) == TYPE_GROUND); + ASSUME(MoveHasAdditionalEffect(MOVE_BULLDOZE, MOVE_EFFECT_SPD_MINUS_1)); + + u32 species, ability, currentHP; + + PARAMETRIZE { species = SPECIES_KINGAMBIT; ability = ABILITY_DEFIANT; currentHP = 400; } + PARAMETRIZE { species = SPECIES_SHUCKLE; ability = ABILITY_CONTRARY; currentHP = 400; } + PARAMETRIZE { species = SPECIES_PAWNIARD; ability = ABILITY_PRESSURE; currentHP = 1; } + PARAMETRIZE { species = SPECIES_PAWNIARD; ability = ABILITY_DEFIANT; currentHP = 1; } + PARAMETRIZE { species = SPECIES_SHUCKLE; ability = ABILITY_CONTRARY; currentHP = 1; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_PHANPY) { Moves(MOVE_BULLDOZE, MOVE_HIGH_HORSEPOWER); } + OPPONENT(species) { Moves(MOVE_CELEBRATE, MOVE_POUND); HP(currentHP); Ability(ability); } + } WHEN { + if (currentHP != 1) + TURN { EXPECT_MOVE(opponentLeft, MOVE_BULLDOZE); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_HIGH_HORSEPOWER); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will choose Beat Up on an ally with Justified if it will benefit the ally") +{ + ASSUME(GetMoveEffect(MOVE_BEAT_UP) == EFFECT_BEAT_UP); + ASSUME(GetMoveType(MOVE_BEAT_UP) == TYPE_DARK); + + u32 defAbility, atkAbility, currentHP; + + PARAMETRIZE { defAbility = ABILITY_FLASH_FIRE; atkAbility = ABILITY_SCRAPPY; currentHP = 400; } + PARAMETRIZE { defAbility = ABILITY_JUSTIFIED; atkAbility = ABILITY_SCRAPPY; currentHP = 400; } + PARAMETRIZE { defAbility = ABILITY_JUSTIFIED; atkAbility = ABILITY_MOLD_BREAKER; currentHP = 400; } + PARAMETRIZE { defAbility = ABILITY_JUSTIFIED; atkAbility = ABILITY_SCRAPPY; currentHP = 1; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_CLEFABLE); + OPPONENT(SPECIES_PANGORO) { Ability(atkAbility); Moves(MOVE_BEAT_UP); } + OPPONENT(SPECIES_GROWLITHE) { Moves(MOVE_CELEBRATE, MOVE_TACKLE); HP(currentHP); Ability(defAbility); } + } WHEN { + if (!(currentHP == 1) && (defAbility == ABILITY_JUSTIFIED) && (atkAbility != ABILITY_MOLD_BREAKER)) + TURN { EXPECT_MOVE(opponentLeft, MOVE_BEAT_UP, target: opponentRight); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_BEAT_UP, target: playerLeft); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will choose Earthquake if partner is not alive") +{ + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND); + + GIVEN { + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } + OPPONENT(SPECIES_PIKACHU) { HP(1); Moves(MOVE_CELEBRATE); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_SCRATCH, target: opponentRight); } + TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will choose Earthquake if it kills one opposing mon and does not kill the partner needlessly") +{ + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND); + + u32 currentHP; + PARAMETRIZE { currentHP = 1; } + PARAMETRIZE { currentHP = 200; } + + GIVEN { + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } + OPPONENT(SPECIES_PARAS) { Moves(MOVE_CELEBRATE); HP(currentHP); } + } WHEN { + if (currentHP == 1) + TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will choose Earthquake if it kills one opposing mon and a partner it believes is about to die") +{ + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND); + + GIVEN { + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT); + PLAYER(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE, MOVE_SCRATCH); } + PLAYER(SPECIES_WOBBUFFET) { HP(1); Moves(MOVE_CELEBRATE, MOVE_SCRATCH); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } + OPPONENT(SPECIES_PARAS) { Moves(MOVE_CELEBRATE); HP(1); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_SCRATCH); } + TURN { MOVE(playerRight, MOVE_SCRATCH); } + TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will choose Earthquake if it kills both opposing mons") +{ + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND); + + GIVEN { + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_EARTHQUAKE, MOVE_SCRATCH); } + OPPONENT(SPECIES_PARAS) { Moves(MOVE_CELEBRATE); HP(1); } + } WHEN { + TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will trigger its ally's Weakness Policy") +{ + ASSUME(gItemsInfo[ITEM_WEAKNESS_POLICY].holdEffect == HOLD_EFFECT_WEAKNESS_POLICY); + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND); + + u32 species; + PARAMETRIZE { species = SPECIES_INCINEROAR; } + PARAMETRIZE { species = SPECIES_CLEFFA; } + + GIVEN { + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_EARTHQUAKE, MOVE_STOMPING_TANTRUM); } + OPPONENT(species) { Moves(MOVE_CELEBRATE); Item(ITEM_WEAKNESS_POLICY); } + } WHEN { + if (species == SPECIES_INCINEROAR) + TURN { EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + else + TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_EARTHQUAKE); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI will only explode and kill everything on the field with Risky or Will Suicide") +{ + ASSUME(GetMoveTarget(MOVE_EXPLOSION) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveEffect(MOVE_EXPLOSION) == EFFECT_EXPLOSION); + + u32 aiFlags; + + PARAMETRIZE { aiFlags = AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT; } + PARAMETRIZE { aiFlags = AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_RISKY; } + PARAMETRIZE { aiFlags = AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_WILL_SUICIDE; } + + GIVEN { + AI_FLAGS(aiFlags); + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + PLAYER(SPECIES_WOBBUFFET) { HP(1); } + OPPONENT(SPECIES_ELECTRODE) { Moves(MOVE_EXPLOSION, MOVE_ELECTRO_BALL); } + OPPONENT(SPECIES_ELECTRODE) { Moves(MOVE_CELEBRATE); HP(1); } + } WHEN { + if (aiFlags == (AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT)) + TURN { EXPECT_MOVE(opponentLeft, MOVE_ELECTRO_BALL); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_EXPLOSION); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI sees corresponding absorbing abilities on partners") +{ + ASSUME(GetMoveTarget(MOVE_DISCHARGE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_DISCHARGE) == TYPE_ELECTRIC); + ASSUME(GetMoveTarget(MOVE_LAVA_PLUME) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_LAVA_PLUME) == TYPE_FIRE); + ASSUME(GetMoveTarget(MOVE_SURF) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_SURF) == TYPE_WATER); + ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_EARTHQUAKE) == TYPE_GROUND); + + u32 ability, move, species; + + PARAMETRIZE { species = SPECIES_PSYDUCK; ability = ABILITY_CLOUD_NINE; move = MOVE_DISCHARGE; } + PARAMETRIZE { species = SPECIES_PIKACHU; ability = ABILITY_LIGHTNING_ROD; move = MOVE_DISCHARGE; } + PARAMETRIZE { species = SPECIES_LANTURN; ability = ABILITY_VOLT_ABSORB; move = MOVE_DISCHARGE; } + PARAMETRIZE { species = SPECIES_EMOLGA; ability = ABILITY_MOTOR_DRIVE; move = MOVE_DISCHARGE; } + PARAMETRIZE { species = SPECIES_SEAKING; ability = ABILITY_LIGHTNING_ROD; move = MOVE_DISCHARGE; } + PARAMETRIZE { species = SPECIES_GROWLITHE; ability = ABILITY_FLASH_FIRE; move = MOVE_LAVA_PLUME; } + PARAMETRIZE { species = SPECIES_DACHSBUN; ability = ABILITY_WELL_BAKED_BODY; move = MOVE_LAVA_PLUME; } + PARAMETRIZE { species = SPECIES_QUAGSIRE; ability = ABILITY_WATER_ABSORB; move = MOVE_SURF; } + PARAMETRIZE { species = SPECIES_SHELLOS; ability = ABILITY_STORM_DRAIN; move = MOVE_SURF; } + PARAMETRIZE { species = SPECIES_UNOWN; ability = ABILITY_LEVITATE; move = MOVE_EARTHQUAKE; } + PARAMETRIZE { species = SPECIES_ORTHWORM; ability = ABILITY_EARTH_EATER; move = MOVE_EARTHQUAKE; } + + GIVEN { + ASSUME(GetMoveTarget(MOVE_DISCHARGE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_HP_AWARE); + PLAYER(SPECIES_ZIGZAGOON); + PLAYER(SPECIES_ZIGZAGOON); + OPPONENT(SPECIES_SLAKING) { Moves(move, MOVE_SCRATCH); } + OPPONENT(species) { HP(1); Ability(ability); Moves(MOVE_POUND, MOVE_EMBER, MOVE_ROUND); } + } WHEN { + if (ability != ABILITY_CLOUD_NINE) + TURN { EXPECT_MOVE(opponentLeft, move); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI knows if redirection abilities provide immunity to allies") +{ + KNOWN_FAILING; + ASSUME(GetMoveTarget(MOVE_DISCHARGE) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_DISCHARGE) == TYPE_ELECTRIC); + ASSUME(GetMoveTarget(MOVE_SURF) == MOVE_TARGET_FOES_AND_ALLY); + ASSUME(GetMoveType(MOVE_SURF) == TYPE_WATER); + + u32 ability, move, species, config; + + PARAMETRIZE { species = SPECIES_SEAKING; ability = ABILITY_LIGHTNING_ROD; move = MOVE_DISCHARGE; config = GEN_4; } + PARAMETRIZE { species = SPECIES_SEAKING; ability = ABILITY_LIGHTNING_ROD; move = MOVE_DISCHARGE; config = GEN_5; } + PARAMETRIZE { species = SPECIES_SHELLOS; ability = ABILITY_STORM_DRAIN; move = MOVE_SURF; config = GEN_4; } + PARAMETRIZE { species = SPECIES_SHELLOS; ability = ABILITY_STORM_DRAIN; move = MOVE_SURF; config = GEN_5; } + + GIVEN { + ASSUME(GetMoveTarget(MOVE_DISCHARGE) == MOVE_TARGET_FOES_AND_ALLY); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_HP_AWARE); + WITH_CONFIG(B_REDIRECT_ABILITY_IMMUNITY, config); + PLAYER(SPECIES_ZIGZAGOON); + PLAYER(SPECIES_ZIGZAGOON); + OPPONENT(SPECIES_SLAKING) { Moves(move, MOVE_SCRATCH); } + OPPONENT(species) { HP(1); Ability(ability); Moves(MOVE_ROUND); } + } WHEN { + if (config == GEN_5) + TURN { EXPECT_MOVE(opponentLeft, move); } + else + TURN { EXPECT_MOVE(opponentLeft, MOVE_SCRATCH); } + } +} + +AI_DOUBLE_BATTLE_TEST("AI recognizes Volt Absorb received from Trace") +{ + KNOWN_FAILING; // MGriffin's PR that switched two turn charging moves in AI tests broke this test, waiting on a fix + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_MAGNETON); + PLAYER(SPECIES_GARDEVOIR) { Ability(ABILITY_TRACE); } + OPPONENT(SPECIES_JOLTEON) { Ability(ABILITY_VOLT_ABSORB); Moves(MOVE_THUNDER_WAVE, MOVE_THUNDERSHOCK, MOVE_WATER_GUN); } + OPPONENT(SPECIES_JOLTEON) { Ability(ABILITY_VOLT_ABSORB); Moves(MOVE_THUNDER_WAVE, MOVE_THUNDERSHOCK, MOVE_WATER_GUN); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_THUNDERSHOCK); NOT_EXPECT_MOVE(opponentLeft, MOVE_THUNDER_WAVE); NOT_EXPECT_MOVE(opponentRight, MOVE_THUNDER_WAVE); } + } THEN { + EXPECT(gAiLogicData->abilities[B_POSITION_PLAYER_RIGHT] == ABILITY_VOLT_ABSORB); + } +} + +AI_DOUBLE_BATTLE_TEST("AI prioritizes Skill Swapping Contrary to allied mons that would benefit from it") +{ + GIVEN { + ASSUME(GetMoveEffect(MOVE_SKILL_SWAP) == EFFECT_SKILL_SWAP); + ASSUME(GetMoveAdditionalEffectById(MOVE_OVERHEAT, 0)->moveEffect == MOVE_EFFECT_SP_ATK_MINUS_2); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_DOUBLE_BATTLE); + PLAYER(SPECIES_WOBBUFFET) { Speed(3); } + PLAYER(SPECIES_WOBBUFFET) { Speed(3); } + OPPONENT(SPECIES_SPINDA) { Ability(ABILITY_CONTRARY); Speed(5); Moves(MOVE_SKILL_SWAP, MOVE_ENCORE, MOVE_FAKE_TEARS, MOVE_SWAGGER); } + OPPONENT(SPECIES_ARCANINE) { Ability(ABILITY_INTIMIDATE); Speed(4); Moves (MOVE_OVERHEAT); } + } WHEN { + TURN { EXPECT_MOVE(opponentLeft, MOVE_SKILL_SWAP, target:opponentRight); EXPECT_MOVE(opponentRight, MOVE_OVERHEAT); } + } +} +