Repository: rh-hideout/pokeemerald-expansion
Pull request #6766: AI double battle friendly fire logic improvements
Author: login: surskitty
Labels: name: category: battle-ai
description: Pertains to Battle Engine Upgrade's AI
Created: 2025-05-03T18:29:30Z | Merged: 2025-05-16T15:41:04Z

These are AI improvements for attacking the ally and for using moves that damage both enemies and the partner pokemon.  The focus here is on better awareness of Justified, Contrary, Weakness Policy, and Explosion.

## Description
Double battles specific AI tests have been added to their own file, to make it easier to see how doubles specifically will behave.
Acceptable number of hits to KO for attacking the ally are now an AI config setting, homogenizing Water Compaction/Justified/Rattled/Anger Point/etc.
The AI will no longer kill an ally with Justified or Contrary for no reason.
The AI _will_ kill its ally if it will also kill both opponents with non-explosion moves.  It will only explode to wipe out all pokemon on the field if it is Risky or Will Suicide.
The AI will trigger its ally's Weakness Policy if it has a move that also damages both opponents.
The AI will trigger its ally's Defiant et al if it will not lower their HP unacceptably.
The AI will kill its ally with a spread move if it believes its ally was going to die anyway and it either scores a kill on an opponent or triggers its Moxie-like ability.
The AI no longer thinks that Crafty Shield protects it from Explosion.

## Issue(s) that this PR fixes
Addresses many of the major elements of #5215.  Does not include anything about Skill Swap because those are a WIP.

## Not implemented
I do not believe any of these are blocking.
- The AI isn't currently recognizing generational configs for Lightningrod and Storm Drain and treating it as an immunity in all cases. This one is beyond me to fix.
- I feel confident IsAllyProtectingFromMove() is missing some cases.
- Contrary and Beast Boost could use logic in ShouldTriggerAbility(). These are beyond me at the moment as well.
-  Moxie et al could use more complicated logic and I would be fine with adding that.
- Abilities that do not have logic but probably should include Anger Shell, Battle Bond, Electromorphosis, Power of Alchemy, Seed Sower, Soul Heart, Stamina, Supreme Overlord, Wind Rider, and Wind Power.  I am absolutely not touching Battle Bond and don't want to deal with Seed Sower either.  The others I _can_ do but would probably be better in another PR.

## **People who collaborated with me in this PR**
@Pawkkie

## **Discord contact info**
wildvenonat


Diff:
diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h
index 2fce8cce955f..7d5c0c964785 100644
--- a/include/battle_ai_util.h
+++ b/include/battle_ai_util.h
@@ -194,6 +194,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);
@@ -209,6 +213,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 c734eb2c1d2d..db98f72ee91c 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 0a43bc81ed8c..b32acaf32d95 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 = ((gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_PREDICT_MOVE) && aiData->predictingMove) ? gAiLogicData->predictedMove[battlerDef] : aiData->lastUsedMove[battlerDef];
@@ -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);
+                    isMoveAffectedByPartnerAbility = FALSE;
                 }
-                break;  // handled in AI_HPAware
+                break;
+            case ABILITY_LIGHTNING_ROD:
             case ABILITY_MOTOR_DRIVE:
-                if (moveType == TYPE_ELECTRIC && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPEED))
+            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);
+                    }
                 }
-                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))
+                else
                 {
-                    RETURN_SCORE_PLUS(WEAK_EFFECT);
+                    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;
                 }
-                RETURN_SCORE_MINUS(10);
                 break;
-            case ABILITY_FLASH_FIRE:
-                if (moveType == TYPE_FIRE
-                    && HasMoveWithType(battlerAtkPartner, TYPE_FIRE)
-                    && !gDisableStructs[battlerAtkPartner].flashFireBoosted)
+            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_SAP_SIPPER:
-                if (moveType == TYPE_GRASS
-                    && HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_PHYSICAL)
-                    && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK))
+            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;
+                }
+                break;
+            case ABILITY_FLASH_FIRE:
+            case ABILITY_WELL_BAKED_BODY:
+                if (moveType == TYPE_FIRE)
+                {
+                    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)
+                {
+                    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 0b532b865cb3..28fa993944a5 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)
@@ -4531,6 +4576,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 3f799d09abb4..55942c2834a2 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 2f30c3df3a37..f80597f476b1 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 000000000000..b162c8c67fc2
--- /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); }
+    }
+}
+


Reviews:
user: login: AlexOn1ine
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-04T08:25:52Z
user: login: Pawkkie
body: Super sweet PR! Some additional comments based on your PR description:

Can you change the "People who collaborated" to just say exactly "@Pawkkie", processing this field is automated and anything other than exactly that attribution tends to break the scripts lol

> The AI isn't currently recognizing generational configs for Lightningrod and Storm Drain and treating it as an immunity in all cases. This one is beyond me to fix.

Can you open an issue for this?

> Contrary and Beast Boost could use logic in ShouldTriggerAbility(). These are beyond me at the moment as well.

Same here

The rest I think don't need issues as they're just cool feature ideas you're thinking of working on (which would be AWESOME), but if those two would improve this and they're out of scope for you, mark them down as battle AI feature requests and they may get handled eventually :)
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T17:36:16Z
user: login: surskitty
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T17:52:01Z
user: login: surskitty
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T17:52:36Z
user: login: surskitty
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T18:25:12Z
user: login: surskitty
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T18:27:08Z
user: login: surskitty
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T18:27:25Z
user: login: Pawkkie
body: Last round, very minor :)
state: CHANGES_REQUESTED
author_association: COLLABORATOR
submitted_at: 2025-05-08T21:47:00Z
user: login: Pawkkie
body: Thank you for your patience! I'm sure users will be happy to finally see a doubles AI PR in the next release changelog :D
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-05-16T15:40:56Z

Review comments:
diff_hunk: @@ -4517,3 +4575,75 @@ bool32 HasBattlerSideAbility(u32 battler, u32 ability, struct AiLogicData *aiDat
         return TRUE;
     return FALSE;
 }
+
+u32 FriendlyFireHitsToKO(u32 battler)
+{
+    if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_RISKY)
+        return FRIENDLY_FIRE_RISKY_THRESHOLD;
+    if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_CONSERVATIVE)
+        return FRIENDLY_FIRE_CONSERVATIVE_THRESHOLD;
+
+    return FRIENDLY_FIRE_NORMAL_THRESHOLD;
+}
+
+bool32 IsMoxieTypeAbility(u32 abilityId)
+{
+    switch (abilityId)
path: src/battle_ai_util.c
user: login: AlexOn1ine
body: ```suggestion
bool32 IsMoxieTypeAbility(u32 ability)
{
    switch (ability)
```
for consistency :sweat_smile: 
created_at: 2025-05-04T08:25:52Z
author_association: COLLABORATOR
diff_hunk: @@ -4517,3 +4575,75 @@ bool32 HasBattlerSideAbility(u32 battler, u32 ability, struct AiLogicData *aiDat
         return TRUE;
     return FALSE;
 }
+
+u32 FriendlyFireHitsToKO(u32 battler)
path: src/battle_ai_util.c
user: login: Pawkkie
body: Can we call this `GetFriendlyFireKOThreshold` instead to better match the structure of similar simple access functions / the defines?
created_at: 2025-05-08T17:03:11Z
author_association: COLLABORATOR
diff_hunk: @@ -2984,107 +2989,351 @@ 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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                && CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                switch (effect)
+                {
+                case EFFECT_EXPLOSION:
+                    if (AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                    || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                if (AI_THINKING_STRUCT->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);
+            }
+        } 
+
+    // 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][AI_THINKING_STRUCT->movesetIndex] >= UQ_4_12(2.0))
+            {
+                ADJUST_SCORE(WEAK_EFFECT);
path: src/battle_ai_main.c
user: login: Pawkkie
body: Doesn't having this here mean that the AI gets a score bonus for doing this even before it checks whether friendly fire is ok? I would think we'd only want the score increase to be after checking that conditional as otherwise we can just one shot our partner before the policy goes off
created_at: 2025-05-08T17:08:01Z
author_association: COLLABORATOR
diff_hunk: @@ -2984,107 +2989,351 @@ 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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                && CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                switch (effect)
+                {
+                case EFFECT_EXPLOSION:
+                    if (AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                    || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                if (AI_THINKING_STRUCT->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);
+            }
+        } 
+
+    // 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][AI_THINKING_STRUCT->movesetIndex] >= UQ_4_12(2.0))
+            {
+                ADJUST_SCORE(WEAK_EFFECT);
+                
+                if (isFriendlyFireOK)
+                {
+                    ADJUST_SCORE(WEAK_EFFECT);
+                }
+            }            
+            break;
+        default:
+            break;
+        }
+    }
+
     // check specific target
     if (IS_TARGETING_PARTNER(battlerAtk, battlerDef))
     {
+        bool32 isMoveRelevant = TRUE;
+
+        if (wouldPartnerFaint)
+        {
+            RETURN_SCORE_MINUS(30);
+        }
+        
         // partner ability checks
         if (!partnerProtecting && moveTarget != MOVE_TARGET_BOTH && !DoesBattlerIgnoreAbilityChecks(battlerAtk, aiData->abilities[battlerAtk], move))
         {
             switch (atkPartnerAbility)
             {
             case ABILITY_ANGER_POINT:
-                if (MoveAlwaysCrits(move)
-                    && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)
-                    && AI_IsFaster(battlerAtk, battlerAtkPartner, move)
-                    && !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+                if (BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)
+                    && AI_IsFaster(battlerAtk, battlerAtkPartner, move) 
+                    && 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);
+                    }
+                    // Multi-hit moves have a higher chance of critting
+                    if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS)
+                    {
+                        ADJUST_SCORE(WEAK_EFFECT);
path: src/battle_ai_main.c
user: login: Pawkkie
body: This feels dangerous, I'm not convincing giving a score increase to generically Pin Missile'ing your partner over the opponent leads to good gameplay, am I missing something
created_at: 2025-05-08T17:11:11Z
author_association: COLLABORATOR
diff_hunk: @@ -3093,6 +3342,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_PLUS(-30);
path: src/battle_ai_main.c
user: login: Pawkkie
body: You've been using `RETURN_SCORE_MINUS` elsewhere, probably should here also
created_at: 2025-05-08T17:13:18Z
author_association: COLLABORATOR
diff_hunk: @@ -3228,7 +3487,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 ((isMoveRelevant && (score <= AI_SCORE_DEFAULT)) || !isMoveRelevant)
path: src/battle_ai_main.c
user: login: Pawkkie
body: I feel like I don't understand what `isMoveRelevant` is here for.

Is this just ensuring that if the move doesn't explicitly trigger a benefit it gets the negative score?

I think adding a short comment here clarifying that would be helpful :)
created_at: 2025-05-08T17:15:02Z
author_association: COLLABORATOR
diff_hunk: @@ -1581,6 +1581,64 @@ bool32 IsHazardClearingMove(u32 move)
     return FALSE;
 }
 
+bool32 IsAllyProtectingFromMove(u32 battlerAtk, u32 attackerMove, u32 allyMove)
+{
+    enum BattleMoveEffects effect = GetMoveEffect(allyMove);
+    struct AiLogicData *aiData = AI_DATA;
+
+    if (effect != EFFECT_PROTECT)
+    {
+        return FALSE;
+    }
+    else
+    {
+        enum ProtectMethod protectMethod = GetMoveProtectMethod(allyMove);
+        
+        if (protectMethod == PROTECT_QUICK_GUARD)
+        {
+            u32 attackerAbility = aiData->abilities[battlerAtk];
+            u32 priority = GetMovePriority(attackerMove);
path: src/battle_ai_util.c
user: login: Pawkkie
body: Is there a specific reason you aren't using `GetBattleMovePriority`? It handles all of these abilities for you
created_at: 2025-05-08T17:16:37Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,376 @@
+#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")
+{
+    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")
+{
+    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);
+
+    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);
path: test/battle/ai/ai_doubles.c
user: login: Pawkkie
body: Need ASSUMEs for Bulldoze targeting / typing
created_at: 2025-05-08T17:27:20Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,376 @@
+#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")
+{
+    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")
+{
+    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);
+
+    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")
+{
+    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);
path: test/battle/ai/ai_doubles.c
user: login: Pawkkie
body: Need ASSUMEs for type / move effect
created_at: 2025-05-08T17:27:46Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,376 @@
+#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")
+{
+    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")
+{
+    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);
+
+    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")
+{
+    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")
+{
+    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")
+{
+    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 about to die")
path: test/battle/ai/ai_doubles.c
user: login: Pawkkie
body: it believes *is* about to die
created_at: 2025-05-08T17:30:09Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,376 @@
+#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")
+{
+    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")
+{
+    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);
+
+    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")
+{
+    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")
+{
+    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")
+{
+    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 about to die")
+{
+    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")
+{
+    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")
+{
+    u32 species;
+    PARAMETRIZE { species = SPECIES_INCINEROAR; }
+    PARAMETRIZE { species = SPECIES_CLEFFA; }
+
+    GIVEN {
+        ASSUME(GetMoveTarget(MOVE_EARTHQUAKE) == MOVE_TARGET_FOES_AND_ALLY);
path: test/battle/ai/ai_doubles.c
user: login: Pawkkie
body: Assume for weakness policy effect
created_at: 2025-05-08T17:30:51Z
author_association: COLLABORATOR
diff_hunk: @@ -2984,107 +2989,351 @@ 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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                && CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                switch (effect)
+                {
+                case EFFECT_EXPLOSION:
+                    if (AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                    || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                if (AI_THINKING_STRUCT->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);
+            }
+        } 
+
+    // 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][AI_THINKING_STRUCT->movesetIndex] >= UQ_4_12(2.0))
+            {
+                ADJUST_SCORE(WEAK_EFFECT);
path: src/battle_ai_main.c
user: login: surskitty
body: ... Yes, you're correct!  I forgot to put that in an else block from the overall one-shot check.
created_at: 2025-05-08T17:52:01Z
author_association: COLLABORATOR
diff_hunk: @@ -3093,6 +3342,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_PLUS(-30);
path: src/battle_ai_main.c
user: login: surskitty
body: I forgor it existed.
created_at: 2025-05-08T17:52:36Z
author_association: COLLABORATOR
diff_hunk: @@ -1581,6 +1581,64 @@ bool32 IsHazardClearingMove(u32 move)
     return FALSE;
 }
 
+bool32 IsAllyProtectingFromMove(u32 battlerAtk, u32 attackerMove, u32 allyMove)
+{
+    enum BattleMoveEffects effect = GetMoveEffect(allyMove);
+    struct AiLogicData *aiData = AI_DATA;
+
+    if (effect != EFFECT_PROTECT)
+    {
+        return FALSE;
+    }
+    else
+    {
+        enum ProtectMethod protectMethod = GetMoveProtectMethod(allyMove);
+        
+        if (protectMethod == PROTECT_QUICK_GUARD)
+        {
+            u32 attackerAbility = aiData->abilities[battlerAtk];
+            u32 priority = GetMovePriority(attackerMove);
path: src/battle_ai_util.c
user: login: surskitty
body: I didn't know it exists.
created_at: 2025-05-08T18:25:12Z
author_association: COLLABORATOR
diff_hunk: @@ -3228,7 +3487,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 ((isMoveRelevant && (score <= AI_SCORE_DEFAULT)) || !isMoveRelevant)
path: src/battle_ai_main.c
user: login: surskitty
body: I renamed it to be clearer what it is :) it is now isMoveAffectedByPartnerAbility.  I left it to percolate until a better name arose ehehehe. 

It's sanity checking whether the various ADJUST_SCORE made it actually beneficial in any way to attack the ally.
created_at: 2025-05-08T18:27:07Z
author_association: COLLABORATOR
diff_hunk: @@ -2984,107 +2989,351 @@ 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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                && CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                switch (effect)
+                {
+                case EFFECT_EXPLOSION:
+                    if (AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                    || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                if (AI_THINKING_STRUCT->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);
+            }
+        } 
+
+    // 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][AI_THINKING_STRUCT->movesetIndex] >= UQ_4_12(2.0))
+            {
+                ADJUST_SCORE(WEAK_EFFECT);
+                
+                if (isFriendlyFireOK)
+                {
+                    ADJUST_SCORE(WEAK_EFFECT);
+                }
+            }            
+            break;
+        default:
+            break;
+        }
+    }
+
     // check specific target
     if (IS_TARGETING_PARTNER(battlerAtk, battlerDef))
     {
+        bool32 isMoveRelevant = TRUE;
+
+        if (wouldPartnerFaint)
+        {
+            RETURN_SCORE_MINUS(30);
+        }
+        
         // partner ability checks
         if (!partnerProtecting && moveTarget != MOVE_TARGET_BOTH && !DoesBattlerIgnoreAbilityChecks(battlerAtk, aiData->abilities[battlerAtk], move))
         {
             switch (atkPartnerAbility)
             {
             case ABILITY_ANGER_POINT:
-                if (MoveAlwaysCrits(move)
-                    && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)
-                    && AI_IsFaster(battlerAtk, battlerAtkPartner, move)
-                    && !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+                if (BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)
+                    && AI_IsFaster(battlerAtk, battlerAtkPartner, move) 
+                    && 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);
+                    }
+                    // Multi-hit moves have a higher chance of critting
+                    if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS)
+                    {
+                        ADJUST_SCORE(WEAK_EFFECT);
path: src/battle_ai_main.c
user: login: surskitty
body: u rite
created_at: 2025-05-08T18:27:25Z
author_association: COLLABORATOR
diff_hunk: @@ -2984,107 +2989,351 @@ 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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                && CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                switch (effect)
+                {
+                case EFFECT_EXPLOSION:
+                    if (AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                    || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                if (AI_THINKING_STRUCT->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.
path: src/battle_ai_main.c
user: login: Pawkkie
body: Comment tab alignment
created_at: 2025-05-08T21:44:08Z
author_association: COLLABORATOR
diff_hunk: @@ -2984,107 +2989,351 @@ 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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                && CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                switch (effect)
+                {
+                case EFFECT_EXPLOSION:
+                    if (AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                    || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->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), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING) 
+                || CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), AI_THINKING_STRUCT->movesetIndex, AI_ATTACKING))
+            {
+                if (AI_THINKING_STRUCT->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][AI_THINKING_STRUCT->movesetIndex] >= UQ_4_12(2.0))
+                {
+                    ADJUST_SCORE(WEAK_EFFECT);
path: src/battle_ai_main.c
user: login: Pawkkie
body: I still don't think this should have a boosted score if friendly fire is not okay
created_at: 2025-05-08T21:45:34Z
author_association: COLLABORATOR

PR comments:
user: login: surskitty
created_at: 2025-05-04T01:15:10Z
body: I just did a refactor of some of the ability checks so it should be a little easier to fiddle with immunity abilities.
author_association: COLLABORATOR
user: login: surskitty
created_at: 2025-05-04T01:19:17Z
body: OK it's more legible now.  Hopefully,
author_association: COLLABORATOR
user: login: surskitty
created_at: 2025-05-09T20:20:47Z
body: Should be good :)
author_association: COLLABORATOR
user: login: Pawkkie
created_at: 2025-05-15T16:10:09Z
body: Goodness I am so sorry I forgot about this!

If you can solve conflicts I'll get this merged today, review is already finished :)
author_association: COLLABORATOR
user: login: surskitty
created_at: 2025-05-15T17:10:00Z
body: Merge conflicts should be solved ...!
author_association: COLLABORATOR