AI double battle friendly fire logic improvements (#6766)

This commit is contained in:
surskitty 2025-05-16 11:41:03 -04:00 committed by GitHub
parent d1307371db
commit e44d0326be
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 857 additions and 242 deletions

View File

@ -197,6 +197,10 @@ bool32 ShouldTrap(u32 battlerAtk, u32 battlerDef, u32 move);
bool32 IsWakeupTurn(u32 battler);
bool32 AI_IsBattlerAsleepOrComatose(u32 battlerId);
// ability logic
bool32 IsMoxieTypeAbility(u32 ability);
bool32 ShouldTriggerAbility(u32 battler, u32 ability);
// partner logic
#define IS_TARGETING_PARTNER(battlerAtk, battlerDef)((battlerAtk) == (battlerDef ^ BIT_FLANK))
u32 GetAllyChosenMove(u32 battlerId);
@ -212,6 +216,8 @@ bool32 PartnerMoveIsSameAsAttacker(u32 battlerAtkPartner, u32 battlerDef, u32 mo
bool32 PartnerMoveIsSameNoTarget(u32 battlerAtkPartner, u32 move, u32 partnerMove);
bool32 PartnerMoveActivatesSleepClause(u32 move);
bool32 ShouldUseWishAromatherapy(u32 battlerAtk, u32 battlerDef, u32 move);
u32 GetFriendlyFireKOThreshold(u32 battler);
bool32 IsAllyProtectingFromMove(u32 battlerAtk, u32 attackerMove, u32 allyMove);
// party logic
struct BattlePokemon *AllocSaveBattleMons(void);

View File

@ -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

View File

@ -1110,7 +1110,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
RETURN_SCORE_MINUS(20);
break;
case ABILITY_JUSTIFIED:
if (moveType == TYPE_DARK && !IsBattleMoveStatus(move))
if (moveType == TYPE_DARK && !IsBattleMoveStatus(move) && !IS_TARGETING_PARTNER(battlerAtk, battlerDef))
RETURN_SCORE_MINUS(10);
break;
case ABILITY_RATTLED:
@ -2221,8 +2221,7 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
&& move != MOVE_CRAFTY_SHIELD) //These moves have infinite usage
{
if (GetBattlerSecondaryDamage(battlerAtk) >= gBattleMons[battlerAtk].hp
&& aiData->abilities[battlerDef] != ABILITY_MOXIE
&& aiData->abilities[battlerDef] != ABILITY_BEAST_BOOST)
&& !(IsMoxieTypeAbility(aiData->abilities[battlerDef])))
{
ADJUST_SCORE(-10); //Don't protect if you're going to faint after protecting
}
@ -2929,7 +2928,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
u32 atkPartnerAbility = aiData->abilities[BATTLE_PARTNER(battlerAtk)];
u32 atkPartnerHoldEffect = aiData->holdEffects[BATTLE_PARTNER(battlerAtk)];
enum BattleMoveEffects partnerEffect = GetMoveEffect(aiData->partnerMove);
bool32 partnerProtecting = (partnerEffect == EFFECT_PROTECT);
bool32 partnerProtecting = IsAllyProtectingFromMove(battlerAtk, move, aiData->partnerMove) && !MoveIgnoresProtect(move);
bool32 attackerHasBadAbility = (gAbilitiesInfo[aiData->abilities[battlerAtk]].aiRating < 0);
bool32 partnerHasBadAbility = (gAbilitiesInfo[atkPartnerAbility].aiRating < 0);
u32 predictedMove = GetIncomingMove(battlerAtk, battlerDef, gAiLogicData);
@ -2937,6 +2936,12 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
SetTypeBeforeUsingMove(move, battlerAtk);
moveType = GetBattleMoveType(move);
u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk);
u32 noOfHitsToKOPartner = GetNoOfHitsToKOBattler(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING);
bool32 wouldPartnerFaint = CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING)
&& !partnerProtecting && IsBattlerAlive(battlerAtkPartner);
bool32 isFriendlyFireOK = !wouldPartnerFaint && (noOfHitsToKOPartner == 0 || noOfHitsToKOPartner > friendlyFireThreshold);
// check what effect partner is using
if (aiData->partnerMove != 0)
{
@ -2987,7 +2992,7 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
{
case EFFECT_HELPING_HAND:
if (!IsBattlerAlive(battlerAtkPartner) || !HasDamagingMove(battlerAtkPartner))
ADJUST_SCORE(-5);
ADJUST_SCORE(-20);
break;
case EFFECT_PERISH_SONG:
if (aiData->partnerMove != 0 && HasTrappingMoveEffect(battlerAtkPartner))
@ -3054,9 +3059,103 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
break;
} // global move effect check
// Specific logic for spread moves.
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
// Don't kill your partner for no reason.
if (wouldPartnerFaint)
{
// If it kills both opponents, that's a good reason.
if (CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING)
&& CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), gAiThinkingStruct->movesetIndex, AI_ATTACKING))
{
switch (effect)
{
case EFFECT_EXPLOSION:
if (gAiThinkingStruct->aiFlags[battlerAtk] & (AI_FLAG_RISKY | AI_FLAG_WILL_SUICIDE))
{
RETURN_SCORE_PLUS(10);
}
break;
default:
RETURN_SCORE_PLUS(10);
break;
}
}
// Both opponents can kill the partner
else if (CanTargetFaintAi(BATTLE_OPPOSITE(battlerAtk), battlerAtkPartner)
&& (CanTargetFaintAi(BATTLE_OPPOSITE(battlerAtkPartner), battlerAtkPartner)))
{
// The spread move should kill an opponent.
if (CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING)
|| CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), gAiThinkingStruct->movesetIndex, AI_ATTACKING))
{
ADJUST_SCORE(WEAK_EFFECT);
}
// Alternatively, it benefits from the ally's death, and it will probably die anyway.
if (IsMoxieTypeAbility(aiData->abilities[battlerAtk]))
{
ADJUST_SCORE(GOOD_EFFECT);
}
if ((aiData->abilities[battlerAtk] == ABILITY_RECEIVER) && !partnerHasBadAbility)
{
ADJUST_SCORE(GOOD_EFFECT);
}
if (HasMove(battlerAtk, MOVE_LAST_RESPECTS))
{
ADJUST_SCORE(GOOD_EFFECT);
}
}
// It can kill one opponent when killing its ally, and its ally is not in extreme danger.
// This is easy for the player to cheese.
else if (CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtk), gAiThinkingStruct->movesetIndex, AI_ATTACKING)
|| CanIndexMoveFaintTarget(battlerAtk, BATTLE_OPPOSITE(battlerAtkPartner), gAiThinkingStruct->movesetIndex, AI_ATTACKING))
{
if (gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_WILL_SUICIDE)
{
RETURN_SCORE_PLUS(10);
}
else
{
ADJUST_SCORE(-DECENT_EFFECT);
}
}
// No reason to kill partner has been found.
else
{
RETURN_SCORE_MINUS(10);
}
}
// Partner will not faint.
else {
// Triggering your ally's hold item should only be done deliberately with a spread move.
switch (atkPartnerHoldEffect)
{
case HOLD_EFFECT_WEAKNESS_POLICY:
if (aiData->effectiveness[battlerAtk][battlerAtkPartner][gAiThinkingStruct->movesetIndex] >= UQ_4_12(2.0) && isFriendlyFireOK)
{
ADJUST_SCORE(GOOD_EFFECT);
}
break;
default:
break;
}
}
}
// check specific target
if (IS_TARGETING_PARTNER(battlerAtk, battlerDef))
{
bool32 isMoveAffectedByPartnerAbility = TRUE;
if (wouldPartnerFaint)
{
RETURN_SCORE_MINUS(30);
}
// partner ability checks
if (!partnerProtecting && moveTarget != MOVE_TARGET_BOTH && !DoesBattlerIgnoreAbilityChecks(battlerAtk, aiData->abilities[battlerAtk], move))
{
@ -3066,95 +3165,241 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
if (MoveAlwaysCrits(move)
&& BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)
&& AI_IsFaster(battlerAtk, battlerAtkPartner, move)
&& !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING))
&& isFriendlyFireOK)
{
RETURN_SCORE_PLUS(GOOD_EFFECT);
if (MoveAlwaysCrits(move))
{
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
RETURN_SCORE_PLUS(GOOD_EFFECT);
}
// todo: figuring out a non-guaranteed crit roll that's useful
}
break;
case ABILITY_VOLT_ABSORB:
if (!(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_HP_AWARE))
else
{
RETURN_SCORE_MINUS(10);
}
break; // handled in AI_HPAware
case ABILITY_MOTOR_DRIVE:
if (moveType == TYPE_ELECTRIC && BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPEED))
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_LIGHTNING_ROD:
if (B_REDIRECT_ABILITY_IMMUNITY >= GEN_5
&& moveType == TYPE_ELECTRIC
&& HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_SPECIAL)
&& BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPATK))
case ABILITY_MOTOR_DRIVE:
case ABILITY_VOLT_ABSORB:
if (moveType == TYPE_ELECTRIC)
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
if (B_REDIRECT_ABILITY_IMMUNITY < GEN_5 && atkPartnerAbility == ABILITY_LIGHTNING_ROD)
{
RETURN_SCORE_MINUS(10);
}
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
else if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
else
{
RETURN_SCORE_MINUS(10);
}
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_WATER_ABSORB:
case ABILITY_DRY_SKIN:
case ABILITY_EARTH_EATER:
if (!(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_HP_AWARE))
case ABILITY_LEVITATE:
if (moveType == TYPE_GROUND)
{
RETURN_SCORE_MINUS(10);
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
else if (atkPartnerAbility == ABILITY_EARTH_EATER && !(gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_HP_AWARE))
{
RETURN_SCORE_MINUS(10);
}
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break; // handled in AI_HPAware
case ABILITY_DRY_SKIN:
case ABILITY_WATER_ABSORB:
case ABILITY_STORM_DRAIN:
if (B_REDIRECT_ABILITY_IMMUNITY >= GEN_5
&& moveType == TYPE_WATER
&& HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_SPECIAL)
&& BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPATK))
if (moveType == TYPE_WATER)
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
if (B_REDIRECT_ABILITY_IMMUNITY < GEN_5 && atkPartnerAbility == ABILITY_STORM_DRAIN)
{
RETURN_SCORE_MINUS(10);
}
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
else if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
else
{
RETURN_SCORE_MINUS(10);
}
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_WATER_COMPACTION:
if (moveType == TYPE_WATER && GetNoOfHitsToKOBattler(battlerAtk, battlerDef, gAiThinkingStruct->movesetIndex, AI_ATTACKING) >= 4)
if (moveType == TYPE_WATER && isFriendlyFireOK
&& ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
RETURN_SCORE_PLUS(WEAK_EFFECT); // only mon with this ability is weak to water so only make it okay if we do very little damage
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS)
{
ADJUST_SCORE(DECENT_EFFECT);
}
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_STEAM_ENGINE:
if (isFriendlyFireOK && (moveType == TYPE_WATER || moveType == TYPE_FIRE)
&& ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_THERMAL_EXCHANGE:
if (moveType == TYPE_FIRE && isFriendlyFireOK
&& !IsBattleMoveStatus(move)
&& ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS)
{
ADJUST_SCORE(WEAK_EFFECT);
}
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
RETURN_SCORE_MINUS(10);
break;
case ABILITY_FLASH_FIRE:
if (moveType == TYPE_FIRE
&& HasMoveWithType(battlerAtkPartner, TYPE_FIRE)
&& !gDisableStructs[battlerAtkPartner].flashFireBoosted)
case ABILITY_WELL_BAKED_BODY:
if (moveType == TYPE_FIRE)
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_SAP_SIPPER:
if (moveType == TYPE_GRASS
&& HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_PHYSICAL)
&& BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK))
if (moveType == TYPE_GRASS)
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
if (ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_JUSTIFIED:
if (moveType == TYPE_DARK
if (moveType == TYPE_DARK && isFriendlyFireOK
&& !IsBattleMoveStatus(move)
&& HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_PHYSICAL)
&& BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)
&& !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING))
&& ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
RETURN_SCORE_PLUS(WEAK_EFFECT);
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(GOOD_EFFECT);
}
if (GetMoveStrikeCount(move) > 1 && effect != EFFECT_DRAGON_DARTS)
{
ADJUST_SCORE(WEAK_EFFECT);
}
ADJUST_SCORE(WEAK_EFFECT); // Beat Up is handled later
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_RATTLED:
if (!IsBattleMoveStatus(move)
if (!IsBattleMoveStatus(move) && isFriendlyFireOK
&& (moveType == TYPE_DARK || moveType == TYPE_GHOST || moveType == TYPE_BUG)
&& BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_SPEED)
&& !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING))
&& ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(DECENT_EFFECT);
}
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
case ABILITY_CONTRARY:
if (IsStatLoweringEffect(effect))
case ABILITY_DEFIANT:
case ABILITY_COMPETITIVE:
if (IsStatLoweringEffect(effect) && isFriendlyFireOK && ShouldTriggerAbility(battlerAtkPartner, atkPartnerAbility))
{
RETURN_SCORE_PLUS(DECENT_EFFECT);
if (moveTarget == MOVE_TARGET_FOES_AND_ALLY)
{
ADJUST_SCORE(GOOD_EFFECT);
}
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
else
{
isMoveAffectedByPartnerAbility = FALSE;
}
break;
}
@ -3163,6 +3408,11 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
// attacker move effects specifically targeting partner
if (!partnerProtecting)
{
if (wouldPartnerFaint)
{
RETURN_SCORE_MINUS(30);
}
switch (effect)
{
case EFFECT_SPICY_EXTRACT:
@ -3200,11 +3450,16 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
case EFFECT_BEAT_UP:
if (atkPartnerAbility == ABILITY_JUSTIFIED
&& moveType == TYPE_DARK
&& !DoesBattlerIgnoreAbilityChecks(battlerAtk, aiData->abilities[battlerAtk], move)
&& !IsBattleMoveStatus(move)
&& HasMoveWithCategory(battlerAtkPartner, DAMAGE_CATEGORY_PHYSICAL)
&& BattlerStatCanRise(battlerAtkPartner, atkPartnerAbility, STAT_ATK)
&& !CanIndexMoveFaintTarget(battlerAtk, battlerAtkPartner, gAiThinkingStruct->movesetIndex, AI_ATTACKING))
&& !wouldPartnerFaint)
{
if (isFriendlyFireOK)
{
ADJUST_SCORE(DECENT_EFFECT);
}
RETURN_SCORE_PLUS(WEAK_EFFECT);
}
break;
@ -3298,7 +3553,12 @@ static s32 AI_DoubleBattle(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
} // attacker move effects
} // check partner protecting
ADJUST_SCORE(-30); // otherwise, don't target partner
if ((isMoveAffectedByPartnerAbility && (score <= AI_SCORE_DEFAULT)) || !isMoveAffectedByPartnerAbility)
{
RETURN_SCORE_MINUS(10);
}
}
else // checking opponent
{
@ -3356,12 +3616,13 @@ static inline bool32 ShouldUseSpreadDamageMove(u32 battlerAtk, u32 move, u32 mov
{
u32 partnerBattler = BATTLE_PARTNER(battlerAtk);
u32 noOfHitsToFaintPartner = GetNoOfHitsToKOBattler(battlerAtk, partnerBattler, moveIndex, AI_ATTACKING);
u32 friendlyFireThreshold = GetFriendlyFireKOThreshold(battlerAtk);
return (IsDoubleBattle()
&& noOfHitsToFaintPartner != 0 // Immunity check
&& IsBattlerAlive(partnerBattler)
&& GetBattlerMoveTargetType(battlerAtk, move) == MOVE_TARGET_FOES_AND_ALLY
&& !(noOfHitsToFaintPartner < 4 && hitsToFaintOpposingBattler == 1)
&& noOfHitsToFaintPartner < 7);
&& !(noOfHitsToFaintPartner < friendlyFireThreshold && hitsToFaintOpposingBattler == 1)
&& noOfHitsToFaintPartner < (friendlyFireThreshold * 2));
}
static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId)

View File

@ -1588,6 +1588,51 @@ bool32 IsHazardClearingMove(u32 move)
return FALSE;
}
bool32 IsAllyProtectingFromMove(u32 battlerAtk, u32 attackerMove, u32 allyMove)
{
enum BattleMoveEffects effect = GetMoveEffect(allyMove);
if (effect != EFFECT_PROTECT)
{
return FALSE;
}
else
{
enum ProtectMethod protectMethod = GetMoveProtectMethod(allyMove);
if (protectMethod == PROTECT_QUICK_GUARD)
{
u32 priority = GetBattleMovePriority(battlerAtk, gAiLogicData->abilities[battlerAtk], attackerMove);
return (priority > 0);
}
if (IsBattleMoveStatus(attackerMove))
{
switch (protectMethod)
{
case PROTECT_NORMAL:
case PROTECT_CRAFTY_SHIELD:
case PROTECT_MAX_GUARD:
case PROTECT_WIDE_GUARD:
return TRUE;
default:
return FALSE;
}
}
else
{
switch (protectMethod)
{
case PROTECT_CRAFTY_SHIELD:
return FALSE;
default:
return TRUE;
}
}
}
}
bool32 IsMoveRedirectionPrevented(u32 battlerAtk, u32 move, u32 atkAbility)
{
if (gAiThinkingStruct->aiFlags[battlerAtk] & AI_FLAG_NEGATE_UNAWARE)
@ -4566,6 +4611,78 @@ bool32 HasBattlerSideAbility(u32 battler, u32 ability, struct AiLogicData *aiDat
return FALSE;
}
u32 GetFriendlyFireKOThreshold(u32 battler)
{
if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_RISKY)
return FRIENDLY_FIRE_RISKY_THRESHOLD;
if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_CONSERVATIVE)
return FRIENDLY_FIRE_CONSERVATIVE_THRESHOLD;
return FRIENDLY_FIRE_NORMAL_THRESHOLD;
}
bool32 IsMoxieTypeAbility(u32 ability)
{
switch (ability)
{
case ABILITY_MOXIE:
case ABILITY_BEAST_BOOST:
case ABILITY_CHILLING_NEIGH:
case ABILITY_AS_ONE_ICE_RIDER:
case ABILITY_GRIM_NEIGH:
case ABILITY_AS_ONE_SHADOW_RIDER:
return TRUE;
default:
return FALSE;
}
}
// Should the AI use a spread move to deliberately activate its partner's ability?
bool32 ShouldTriggerAbility(u32 battler, u32 ability)
{
switch (ability)
{
case ABILITY_LIGHTNING_ROD:
case ABILITY_STORM_DRAIN:
if (B_REDIRECT_ABILITY_IMMUNITY < GEN_5)
return FALSE;
else
return (BattlerStatCanRise(battler, ability, STAT_SPATK) && HasMoveWithCategory(battler, DAMAGE_CATEGORY_SPECIAL));
case ABILITY_DEFIANT:
case ABILITY_JUSTIFIED:
case ABILITY_MOXIE:
case ABILITY_SAP_SIPPER:
case ABILITY_THERMAL_EXCHANGE:
return (BattlerStatCanRise(battler, ability, STAT_ATK) && HasMoveWithCategory(battler, DAMAGE_CATEGORY_PHYSICAL));
case ABILITY_COMPETITIVE:
return (BattlerStatCanRise(battler, ability, STAT_SPATK) && HasMoveWithCategory(battler, DAMAGE_CATEGORY_SPECIAL));
case ABILITY_CONTRARY:
return TRUE;
case ABILITY_DRY_SKIN:
case ABILITY_VOLT_ABSORB:
case ABILITY_WATER_ABSORB:
return (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_HP_AWARE);
case ABILITY_RATTLED:
case ABILITY_STEAM_ENGINE:
return BattlerStatCanRise(battler, ability, STAT_SPEED);
case ABILITY_FLASH_FIRE:
return (HasMoveWithType(battler, TYPE_FIRE) && !gDisableStructs[battler].flashFireBoosted);
case ABILITY_WATER_COMPACTION:
case ABILITY_WELL_BAKED_BODY:
return (BattlerStatCanRise(battler, ability, STAT_DEF));
default:
return FALSE;
}
}
u32 GetThinkingBattler(u32 battler)
{
if (gAiLogicData->aiPredictionInProgress)

View File

@ -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;

View File

@ -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 {

412
test/battle/ai/ai_doubles.c Normal file
View File

@ -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); }
}
}