diff --git a/include/battle.h b/include/battle.h index 2259da5ccbb4..7bdb889fc63f 100644 --- a/include/battle.h +++ b/include/battle.h @@ -281,6 +281,12 @@ struct AIPartyData // Opposing battlers - party mons. u8 count[NUM_BATTLE_SIDES]; }; +struct SwitchinCandidate +{ + struct BattlePokemon battleMon; + bool8 hypotheticalStatus; +}; + // Ai Data used when deciding which move to use, computed only once before each turn's start. struct AiLogicData { @@ -300,6 +306,8 @@ struct AiLogicData bool8 shouldSwitchMon; // Because all available moves have no/little effect. Each bit per battler. u8 monToSwitchId[MAX_BATTLERS_COUNT]; // ID of the mon to switch. bool8 weatherHasEffect; // The same as WEATHER_HAS_EFFECT. Stored here, so it's called only once. + u8 mostSuitableMonId; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId. + struct SwitchinCandidate switchinCandidate; // Struct used for deciding which mon to switch to in battle_ai_switch_items.c }; struct AI_ThinkingStruct diff --git a/include/battle_ai_switch_items.h b/include/battle_ai_switch_items.h index 9a7e5f7e74d5..8c22baa3122e 100644 --- a/include/battle_ai_switch_items.h +++ b/include/battle_ai_switch_items.h @@ -3,7 +3,7 @@ void GetAIPartyIndexes(u32 battlerId, s32 *firstId, s32 *lastId); void AI_TrySwitchOrUseItem(u32 battler); -u8 GetMostSuitableMonToSwitchInto(u32 battler); +u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd); bool32 ShouldSwitch(u32 battler); #endif // GUARD_BATTLE_AI_SWITCH_ITEMS_H diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 8de2462d90c3..4eaa438304bd 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -179,7 +179,6 @@ bool32 ShouldUseWishAromatherapy(u32 battlerAtk, u32 battlerDef, u32 move); // party logic struct BattlePokemon *AllocSaveBattleMons(void); void FreeRestoreBattleMons(struct BattlePokemon *savedBattleMons); -s32 AI_CalcPartyMonBestMoveDamage(u32 battlerAtk, u32 battlerDef, struct Pokemon *attackerMon, struct Pokemon *targetMon); s32 CountUsablePartyMons(u32 battlerId); bool32 IsPartyFullyHealedExceptBattler(u32 battler); bool32 PartyHasMoveSplit(u32 battlerId, u32 split); @@ -194,4 +193,6 @@ void IncreaseSleepScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); void IncreaseConfusionScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); void IncreaseFrostbiteScore(u32 battlerAtk, u32 battlerDef, u32 move, s32 *score); +s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool8 isPartyMonAttacker); + #endif //GUARD_BATTLE_AI_UTIL_H diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index 32b7f7d10c3c..945e2d48e606 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -59,6 +59,7 @@ #define AI_FLAG_SMART_SWITCHING (1 << 15) // AI includes a lot more switching checks #define AI_FLAG_ACE_POKEMON (1 << 16) // AI has an Ace Pokemon. The last Pokemon in the party will not be used until it's the last one remaining. #define AI_FLAG_OMNISCIENT (1 << 17) // AI has full knowledge of player moves, abilities, hold items +#define AI_FLAG_SMART_MON_CHOICES (1 << 18) // AI will make smarter decisions when choosing which mon to send out mid-battle and after a KO, which are separate decisions. Pairs very well with AI_FLAG_SMART_SWITCHING. #define AI_FLAG_COUNT 18 diff --git a/include/constants/items.h b/include/constants/items.h index 71efbb37034b..553835b8ac0d 100644 --- a/include/constants/items.h +++ b/include/constants/items.h @@ -607,6 +607,20 @@ #define ITEM_UTILITY_UMBRELLA 513 // Berries +#if B_CONFUSE_BERRIES_HEAL >= GEN_8 + #define CONFUSE_BERRY_HEAL_FRACTION 3 +#elif B_CONFUSE_BERRIES_HEAL == GEN_7 + #define CONFUSE_BERRY_HEAL_FRACTION 2 +#else + #define CONFUSE_BERRY_HEAL_FRACTION 8 +#endif + +#if B_CONFUSE_BERRIES_HEAL >= GEN_7 + #define CONFUSE_BERRY_HP_FRACTION 4 +#else + #define CONFUSE_BERRY_HP_FRACTION 2 +#endif + #define ITEM_CHERI_BERRY 514 #define ITEM_CHESTO_BERRY 515 #define ITEM_PECHA_BERRY 516 diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 0df54f9d2707..c8b0d9c9dc11 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -414,7 +414,7 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData) static bool32 AI_SwitchMonIfSuitable(u32 battler) { - u32 monToSwitchId = GetMostSuitableMonToSwitchInto(battler); + u32 monToSwitchId = AI_DATA->mostSuitableMonId; if (monToSwitchId != PARTY_SIZE) { AI_DATA->shouldSwitchMon |= gBitTable[battler]; diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index b2f56326f7d6..6a3d663252a1 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -28,7 +28,12 @@ static bool8 ShouldUseItem(u32 battler); static bool32 AiExpectsToFaintPlayer(u32 battler); static bool32 AI_ShouldHeal(u32 battler, u32 healAmount); static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount); -static bool32 IsAiPartyMonOHKOBy(u32 battlerAi, u32 battlerAtk, struct Pokemon *aiMon); + +static void InitializeSwitchinCandidate(struct Pokemon *mon) +{ + PokemonToBattleMon(mon, &AI_DATA->switchinCandidate.battleMon); + AI_DATA->switchinCandidate.hypotheticalStatus = FALSE; +} static bool32 IsAceMon(u32 battler, u32 monPartyId) { @@ -58,6 +63,156 @@ void GetAIPartyIndexes(u32 battler, s32 *firstId, s32 *lastId) } } +// Note that as many return statements as possible are INTENTIONALLY put after all of the loops; +// the function can take a max of about 0.06s to run, and this prevents the player from identifying +// whether the mon will switch or not by seeing how long the delay is before they select a move +static bool8 HasBadOdds(u32 battler) + +{ + //Variable initialization + u8 opposingPosition, atkType1, atkType2, defType1, defType2, effectiveness; + s32 i, damageDealt = 0, maxDamageDealt = 0, damageTaken = 0, maxDamageTaken = 0; + u32 aiMove, playerMove, aiBestMove = MOVE_NONE, aiAbility = GetBattlerAbility(battler), opposingBattler, weather = AI_GetWeather(AI_DATA); + bool8 getsOneShot = FALSE, hasStatusMove = FALSE, hasSuperEffectiveMove = FALSE; + u16 typeEffectiveness = UQ_4_12(1.0), aiMoveEffect; //baseline typing damage + + // Only use this if AI_FLAG_SMART_SWITCHING is set for the trainer + if (!(AI_THINKING_STRUCT->aiFlags & AI_FLAG_SMART_SWITCHING)) + return FALSE; + + // Won't bother configuring this for double battles + if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE) + return FALSE; + + opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler)); + opposingBattler = GetBattlerAtPosition(opposingPosition); + + // Gets types of player (opposingBattler) and computer (battler) + atkType1 = gBattleMons[opposingBattler].type1; + atkType2 = gBattleMons[opposingBattler].type2; + defType1 = gBattleMons[battler].type1; + defType2 = gBattleMons[battler].type2; + + // Check AI moves for damage dealt + for (i = 0; i < MAX_MON_MOVES; i++) + { + aiMove = gBattleMons[battler].moves[i]; + aiMoveEffect = gBattleMoves[aiMove].effect; + if (aiMove != MOVE_NONE) + { + // Check if mon has an "important" status move + if (aiMoveEffect == EFFECT_REFLECT || aiMoveEffect == EFFECT_LIGHT_SCREEN + || aiMoveEffect == EFFECT_SPIKES || aiMoveEffect == EFFECT_TOXIC_SPIKES || aiMoveEffect == EFFECT_STEALTH_ROCK || aiMoveEffect == EFFECT_STICKY_WEB || aiMoveEffect == EFFECT_LEECH_SEED + || aiMoveEffect == EFFECT_EXPLOSION + || aiMoveEffect == EFFECT_SLEEP || aiMoveEffect == EFFECT_YAWN || aiMoveEffect == EFFECT_TOXIC || aiMoveEffect == EFFECT_WILL_O_WISP || aiMoveEffect == EFFECT_PARALYZE + || aiMoveEffect == EFFECT_TRICK || aiMoveEffect == EFFECT_TRICK_ROOM || aiMoveEffect== EFFECT_WONDER_ROOM || aiMoveEffect == EFFECT_PSYCHO_SHIFT || aiMoveEffect == EFFECT_FAKE_OUT + ) + { + hasStatusMove = TRUE; + } + + // Only check damage if move has power + if (gBattleMoves[aiMove].power != 0) + { + // Check if mon has a super effective move + if (AI_GetTypeEffectiveness(aiMove, battler, opposingBattler) >= UQ_4_12(2.0)) + hasSuperEffectiveMove = TRUE; + + // Get maximum damage mon can deal + damageDealt = AI_DATA->simulatedDmg[battler][opposingBattler][i]; + if(damageDealt > maxDamageDealt) + { + maxDamageDealt = damageDealt; + aiBestMove = aiMove; + } + + } + } + } + + // Calculate type advantage + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType1))); + if (atkType2 != atkType1) + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType1))); + if (defType2 != defType1) + { + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType2))); + if (atkType2 != atkType1) + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType2))); + } + + // Get max damage mon could take + for (i = 0; i < MAX_MON_MOVES; i++) + { + playerMove = gBattleMons[opposingBattler].moves[i]; + if (playerMove != MOVE_NONE && gBattleMoves[playerMove].power != 0) + { + damageTaken = AI_CalcDamage(playerMove, opposingBattler, battler, &effectiveness, FALSE, weather); + if (damageTaken > maxDamageTaken) + maxDamageTaken = damageTaken; + } + } + + // Check if mon gets one shot + if(maxDamageTaken > gBattleMons[battler].hp) + { + getsOneShot = TRUE; + } + + // Check if current mon can outspeed and KO in spite of bad matchup, and don't switch out if it can + if(damageDealt > gBattleMons[opposingBattler].hp) + { + if (AI_WhoStrikesFirst(battler, opposingBattler, aiBestMove) == AI_IS_FASTER) + return FALSE; + } + + // If we don't have any other viable options, don't switch out + if (AI_DATA->mostSuitableMonId == PARTY_SIZE) + return FALSE; + + // Start assessing whether or not mon has bad odds + // Jump straight to swtiching out in cases where mon gets OHKO'd + if (((getsOneShot && gBattleMons[opposingBattler].speed > gBattleMons[battler].speed) // If the player OHKOs and outspeeds OR OHKOs, doesn't outspeed but isn't 2HKO'd + || (getsOneShot && gBattleMons[opposingBattler].speed <= gBattleMons[battler].speed && maxDamageDealt < gBattleMons[opposingBattler].hp / 2)) + && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator + || (aiAbility == ABILITY_REGENERATOR + && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) + { + // 50% chance to stay in regardless + if (Random() % 2 == 0) + return FALSE; + + // Switch mon out + *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; + BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); + return TRUE; + } + + // General bad type matchups have more wiggle room + if (typeEffectiveness >= UQ_4_12(2.0)) // If the player has at least a 2x type advantage + { + if (!hasSuperEffectiveMove // If the AI doesn't have a super effective move + && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2 // And the current mon has at least 1/2 their HP, or 1/4 HP and Regenerator + || (aiAbility == ABILITY_REGENERATOR + && gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 4))) + { + // Then check if they have an important status move, which is worth using even in a bad matchup + if(hasStatusMove) + return FALSE; + + // 50% chance to stay in regardless + if (Random() % 2 == 0) + return FALSE; + + // Switch mon out + *(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE; + BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); + return TRUE; + } + } + return FALSE; +} + static bool8 ShouldSwitchIfAllBadMoves(u32 battler) { if (AI_DATA->shouldSwitchMon & gBitTable[battler]) @@ -437,12 +592,12 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler) moduloChance = 4; //25% //Attempt to cure bad ailment if (gBattleMons[battler].status1 & (STATUS1_SLEEP | STATUS1_FREEZE | STATUS1_TOXIC_POISON) - && GetMostSuitableMonToSwitchInto(battler) != PARTY_SIZE) + && AI_DATA->mostSuitableMonId != PARTY_SIZE) break; //Attempt to cure lesser ailment if ((gBattleMons[battler].status1 & STATUS1_ANY) && (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2) - && GetMostSuitableMonToSwitchInto(battler) != PARTY_SIZE + && AI_DATA->mostSuitableMonId != PARTY_SIZE && Random() % (moduloChance*chanceReducer) == 0) break; @@ -454,7 +609,7 @@ static bool8 ShouldSwitchIfAbilityBenefit(u32 battler) if (gBattleMons[battler].status1 & STATUS1_ANY) return FALSE; if ((gBattleMons[battler].hp <= ((gBattleMons[battler].maxHP * 2) / 3)) - && GetMostSuitableMonToSwitchInto(battler) != PARTY_SIZE + && AI_DATA->mostSuitableMonId != PARTY_SIZE && Random() % (moduloChance*chanceReducer) == 0) break; @@ -706,6 +861,8 @@ bool32 ShouldSwitch(u32 battler) return TRUE; if (ShouldSwitchIfAbilityBenefit(battler)) return TRUE; + if (HasBadOdds(battler)) + return TRUE; //Removing switch capabilites under specific conditions //These Functions prevent the "FindMonWithFlagsAndSuperEffective" from getting out of hand. @@ -742,7 +899,7 @@ void AI_TrySwitchOrUseItem(u32 battler) { if (*(gBattleStruct->AI_monToSwitchIntoId + battler) == PARTY_SIZE) { - s32 monToSwitchId = GetMostSuitableMonToSwitchInto(battler); + s32 monToSwitchId = AI_DATA->mostSuitableMonId; if (monToSwitchId == PARTY_SIZE) { if (!(gBattleTypeFlags & BATTLE_TYPE_DOUBLE)) @@ -802,8 +959,6 @@ static u32 GetBestMonBatonPass(struct Pokemon *party, int firstId, int lastId, u { if (invalidMons & gBitTable[i]) continue; - if (IsAiPartyMonOHKOBy(battler, opposingBattler, &party[i])) - continue; for (j = 0; j < MAX_MON_MOVES; j++) { @@ -848,9 +1003,6 @@ static u32 GetBestMonTypeMatchup(struct Pokemon *party, int firstId, int lastId, u8 defType1 = gSpeciesInfo[species].types[0]; u8 defType2 = gSpeciesInfo[species].types[1]; - if (IsAiPartyMonOHKOBy(battler, opposingBattler, &party[i])) - continue; - typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType1))); if (atkType2 != atkType1) typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType1))); @@ -897,6 +1049,7 @@ static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 inva int i, j; int dmg, bestDmg = 0; int bestMonId = PARTY_SIZE; + u32 aiMove; gMoveResultFlags = 0; // If we couldn't find the best mon in terms of typing, find the one that deals most damage. @@ -904,21 +1057,683 @@ static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 inva { if (gBitTable[i] & invalidMons) continue; - if (IsAiPartyMonOHKOBy(battler, opposingBattler, &party[i])) + InitializeSwitchinCandidate(&party[i]); + for (j = 0; j < MAX_MON_MOVES; j++) + { + aiMove = AI_DATA->switchinCandidate.battleMon.moves[j]; + if (aiMove != MOVE_NONE && gBattleMoves[aiMove].power != 0) + { + aiMove = GetMonData(&party[i], MON_DATA_MOVE1 + j); + dmg = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE); + if (bestDmg < dmg) + { + bestDmg = dmg; + bestMonId = i; + } + } + } + } + + return bestMonId; +} + +static bool32 IsMonGrounded(u16 heldItemEffect, u32 ability, u8 type1, u8 type2) +{ + // List that makes mon not grounded + if (type1 == TYPE_FLYING || type2 == TYPE_FLYING || ability == ABILITY_LEVITATE + || (heldItemEffect == HOLD_EFFECT_AIR_BALLOON && ability != ABILITY_KLUTZ)) + { + // List that overrides being off the ground + if ((heldItemEffect == HOLD_EFFECT_IRON_BALL && ability != ABILITY_KLUTZ) || (gFieldStatuses & STATUS_FIELD_GRAVITY) || (gFieldStatuses & STATUS_FIELD_MAGIC_ROOM)) + return TRUE; + else + return FALSE; + } + else + return TRUE; +} + +// Gets hazard damage +static u32 GetSwitchinHazardsDamage(u32 battler, struct BattlePokemon *battleMon) +{ + u8 defType1 = battleMon->type1, defType2 = battleMon->type2, tSpikesLayers; + u16 heldItemEffect = gItems[battleMon->item].holdEffect; + u32 maxHP = battleMon->maxHP, ability = battleMon->ability, status = battleMon->status1; + u32 spikesDamage = 0, tSpikesDamage = 0, hazardDamage = 0; + u32 hazardFlags = gSideStatuses[GetBattlerSide(battler)] & (SIDE_STATUS_SPIKES | SIDE_STATUS_STEALTH_ROCK | SIDE_STATUS_STICKY_WEB | SIDE_STATUS_TOXIC_SPIKES | SIDE_STATUS_SAFEGUARD); + + // Check ways mon might avoid all hazards + if (ability != ABILITY_MAGIC_GUARD || (heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS && + !((gFieldStatuses & STATUS_FIELD_MAGIC_ROOM) || ability == ABILITY_KLUTZ))) + { + // Stealth Rock + if ((hazardFlags & SIDE_STATUS_STEALTH_ROCK) && heldItemEffect != HOLD_EFFECT_HEAVY_DUTY_BOOTS) + hazardDamage += GetStealthHazardDamageByTypesAndHP(gBattleMoves[MOVE_STEALTH_ROCK].type, defType1, defType2, battleMon->hp); + // Spikes + if ((hazardFlags & SIDE_STATUS_SPIKES) && IsMonGrounded(heldItemEffect, ability, defType1, defType2)) + { + spikesDamage = maxHP / ((5 - gSideTimers[GetBattlerSide(battler)].spikesAmount) * 2); + if (spikesDamage == 0) + spikesDamage = 1; + hazardDamage += spikesDamage; + } + + // Toxic Spikes + // TODO: CanBePoisoned compatibility to avoid duplicate code + if ((hazardFlags & SIDE_STATUS_TOXIC_SPIKES) && (defType1 != TYPE_POISON && defType2 != TYPE_POISON + && defType1 != TYPE_STEEL && defType2 != TYPE_STEEL + && ability != ABILITY_IMMUNITY && ability != ABILITY_POISON_HEAL && ability != ABILITY_COMATOSE + && status == 0 + && !(hazardFlags & SIDE_STATUS_SAFEGUARD) + && !(IsAbilityOnSide(battler, ABILITY_PASTEL_VEIL)) + && !(IsBattlerTerrainAffected(battler, STATUS_FIELD_MISTY_TERRAIN)) + && !(IsAbilityStatusProtected(battler)) + && heldItemEffect != HOLD_EFFECT_CURE_PSN && heldItemEffect != HOLD_EFFECT_CURE_STATUS + && IsMonGrounded(heldItemEffect, ability, defType1, defType2))) + { + tSpikesLayers = gSideTimers[GetBattlerSide(battler)].toxicSpikesAmount; + if (tSpikesLayers == 1) + { + tSpikesDamage = maxHP / 8; + if (tSpikesDamage == 0) + tSpikesDamage = 1; + } + else if (tSpikesLayers >= 2) + { + tSpikesDamage = maxHP / 16; + if (tSpikesDamage == 0) + tSpikesDamage = 1; + } + hazardDamage += tSpikesDamage; + } + } + return hazardDamage; +} + +// Gets damage / healing from weather +static s32 GetSwitchinWeatherImpact(void) +{ + s32 weatherImpact = 0, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability; + u16 item = AI_DATA->switchinCandidate.battleMon.item; + + if (WEATHER_HAS_EFFECT) + { + // Damage + if (item != ITEM_SAFETY_GOGGLES) + { + if ((gBattleWeather & B_WEATHER_HAIL) && (AI_DATA->switchinCandidate.battleMon.type1 != TYPE_ICE || AI_DATA->switchinCandidate.battleMon.type2 != TYPE_ICE) + && ability != ABILITY_OVERCOAT && ability != ABILITY_SNOW_CLOAK && ability != ABILITY_ICE_BODY) + { + weatherImpact = maxHP / 16; + if (weatherImpact == 0) + weatherImpact = 1; + } + else if ((gBattleWeather & B_WEATHER_SANDSTORM) && (AI_DATA->switchinCandidate.battleMon.type1 != TYPE_GROUND && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_GROUND + && AI_DATA->switchinCandidate.battleMon.type1 != TYPE_ROCK && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_ROCK + && AI_DATA->switchinCandidate.battleMon.type1 != TYPE_STEEL && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_STEEL + && ability != ABILITY_OVERCOAT && ability != ABILITY_SAND_VEIL && ability != ABILITY_SAND_RUSH && ability != ABILITY_SAND_FORCE)) + { + weatherImpact = maxHP / 16; + if (weatherImpact == 0) + weatherImpact = 1; + } + } + if ((gBattleWeather & B_WEATHER_SUN) && (ability == ABILITY_SOLAR_POWER || ability == ABILITY_DRY_SKIN)) + { + weatherImpact = maxHP / 8; + if (weatherImpact == 0) + weatherImpact = 1; + } + + // Healing + if (gBattleWeather & B_WEATHER_RAIN) + { + if (ability == ABILITY_DRY_SKIN) + { + weatherImpact = maxHP / 8; + if (weatherImpact == 0) + weatherImpact = 1; + } + else if (ability == ABILITY_RAIN_DISH) + { + weatherImpact = maxHP / 16; + if (weatherImpact == 0) + weatherImpact = 1; + } + } + if (((gBattleWeather & B_WEATHER_HAIL) || (gBattleWeather & B_WEATHER_SNOW)) && ability == ABILITY_ICE_BODY) + { + weatherImpact = maxHP / 16; + if (weatherImpact == 0) + weatherImpact =1; + } + } + return weatherImpact; +} + +// Gets one turn of recurring healing +static u32 GetSwitchinRecurringHealing(void) +{ + u32 recurringHealing = 0, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability; + u16 item = AI_DATA->switchinCandidate.battleMon.item; + + // Items + if (ability != ABILITY_KLUTZ) + { + if (item == ITEM_BLACK_SLUDGE && (AI_DATA->switchinCandidate.battleMon.type1 == TYPE_POISON || AI_DATA->switchinCandidate.battleMon.type2 == TYPE_POISON)) + { + recurringHealing = maxHP / 16; + if (recurringHealing == 0) + recurringHealing = 1; + } + else if (item == ITEM_LEFTOVERS) + { + recurringHealing = maxHP / 16; + if (recurringHealing == 0) + recurringHealing = 1; + } + } // Intentionally omitting Shell Bell for its inconsistency + + // Abilities + if (ability == ABILITY_POISON_HEAL && (AI_DATA->switchinCandidate.battleMon.status1 & STATUS1_POISON)) + { + recurringHealing = maxHP / 8; + if (recurringHealing == 0) + recurringHealing = 1; + } + return recurringHealing; +} + +// Gets one turn of recurring damage +static u32 GetSwitchinRecurringDamage(void) +{ + u32 passiveDamage = 0, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, ability = AI_DATA->switchinCandidate.battleMon.ability; + u16 item = AI_DATA->switchinCandidate.battleMon.item; + + // Items + if (ability != ABILITY_MAGIC_GUARD && ability != ABILITY_KLUTZ) + { + if (item == ITEM_BLACK_SLUDGE && AI_DATA->switchinCandidate.battleMon.type1 != TYPE_POISON && AI_DATA->switchinCandidate.battleMon.type2 != TYPE_POISON) + { + passiveDamage = maxHP / 8; + if (passiveDamage == 0) + passiveDamage = 1; + } + else if (item == ITEM_LIFE_ORB && ability != ABILITY_SHEER_FORCE) + { + passiveDamage = maxHP / 10; + if (passiveDamage == 0) + passiveDamage = 1; + } + else if (item == ITEM_STICKY_BARB) + { + passiveDamage = maxHP / 8; + if(passiveDamage == 0) + passiveDamage = 1; + } + } + return passiveDamage; +} + +// Gets one turn of status damage +static u32 GetSwitchinStatusDamage(u32 battler) +{ + u8 defType1 = AI_DATA->switchinCandidate.battleMon.type1, defType2 = AI_DATA->switchinCandidate.battleMon.type2; + u8 tSpikesLayers = gSideTimers[GetBattlerSide(battler)].toxicSpikesAmount; + u16 heldItemEffect = gItems[AI_DATA->switchinCandidate.battleMon.item].holdEffect; + u32 status = AI_DATA->switchinCandidate.battleMon.status1, ability = AI_DATA->switchinCandidate.battleMon.ability, maxHP = AI_DATA->switchinCandidate.battleMon.maxHP; + u32 statusDamage = 0; + + // Status condition damage + if ((status != 0) && AI_DATA->switchinCandidate.battleMon.ability != ABILITY_MAGIC_GUARD) + { + if (status & STATUS1_BURN) + { + #if B_BURN_DAMAGE >= GEN_7 + statusDamage = maxHP / 16; + #else + statusDamage = maxHP / 8; + #endif + if(ability == ABILITY_HEATPROOF) + statusDamage = statusDamage / 2; + if (statusDamage == 0) + statusDamage = 1; + } + else if (status & STATUS1_FROSTBITE) + { + #if B_BURN_DAMAGE >= GEN_7 + statusDamage = maxHP / 16; + #else + statusDamage = maxHP / 8; + #endif + if (statusDamage == 0) + statusDamage = 1; + } + else if ((status & STATUS1_POISON) && ability != ABILITY_POISON_HEAL) + { + statusDamage = maxHP / 8; + if (statusDamage == 0) + statusDamage = 1; + } + else if ((status & STATUS1_TOXIC_POISON) && ability != ABILITY_POISON_HEAL) + { + if ((status & STATUS1_TOXIC_COUNTER) != STATUS1_TOXIC_TURN(15)) // not 16 turns + AI_DATA->switchinCandidate.battleMon.status1 += STATUS1_TOXIC_TURN(1); + statusDamage *= AI_DATA->switchinCandidate.battleMon.status1 & STATUS1_TOXIC_COUNTER >> 8; + if (statusDamage == 0) + statusDamage = 1; + } + } + + // Apply hypothetical poisoning from Toxic Spikes, which means the first turn of damage already added in GetSwitchinHazardsDamage + // Do this last to skip one iteration of Poison / Toxic damage, and start counting Toxic damage one turn later. + if (tSpikesLayers != 0 && (defType1 != TYPE_POISON && defType2 != TYPE_POISON + && ability != ABILITY_IMMUNITY && ability != ABILITY_POISON_HEAL + && status == 0 + && !(heldItemEffect == HOLD_EFFECT_HEAVY_DUTY_BOOTS + && (((gFieldStatuses & STATUS_FIELD_MAGIC_ROOM) || ability == ABILITY_KLUTZ))) + && heldItemEffect != HOLD_EFFECT_CURE_PSN && heldItemEffect != HOLD_EFFECT_CURE_STATUS + && IsMonGrounded(heldItemEffect, ability, defType1, defType2))) + { + if (tSpikesLayers == 1) + { + AI_DATA->switchinCandidate.battleMon.status1 = STATUS1_POISON; // Assign "hypothetical" status to the switchin candidate so we can get the damage it would take from TSpikes + AI_DATA->switchinCandidate.hypotheticalStatus = TRUE; + } + if (tSpikesLayers == 2) + { + AI_DATA->switchinCandidate.battleMon.status1 = STATUS1_TOXIC_POISON; // Assign "hypothetical" status to the switchin candidate so we can get the damage it would take from TSpikes + AI_DATA->switchinCandidate.battleMon.status1 += STATUS1_TOXIC_TURN(1); + AI_DATA->switchinCandidate.hypotheticalStatus = TRUE; + } + } + return statusDamage; +} + +// Gets number of hits to KO factoring in hazards, healing held items, status, and weather +static u32 GetSwitchinHitsToKO(s32 damageTaken, u32 battler) +{ + u32 startingHP = AI_DATA->switchinCandidate.battleMon.hp - GetSwitchinHazardsDamage(battler, &AI_DATA->switchinCandidate.battleMon); + s32 weatherImpact = GetSwitchinWeatherImpact(); // Signed to handle both damage and healing in the same value + u32 recurringDamage = GetSwitchinRecurringDamage(); + u32 recurringHealing = GetSwitchinRecurringHealing(); + u32 statusDamage = GetSwitchinStatusDamage(battler); + u32 hitsToKO = 0, singleUseItemHeal = 0; + u16 maxHP = AI_DATA->switchinCandidate.battleMon.maxHP, item = AI_DATA->switchinCandidate.battleMon.item, heldItemEffect = gItems[AI_DATA->switchinCandidate.battleMon.item].holdEffect; + u8 weatherDuration = gWishFutureKnock.weatherDuration, holdEffectParam = gItems[AI_DATA->switchinCandidate.battleMon.item].holdEffectParam; + u32 opposingBattler = GetBattlerAtPosition(BATTLE_OPPOSITE(GetBattlerPosition(battler))); + u32 opposingAbility = gBattleMons[opposingBattler].ability; + bool8 usedSingleUseHealingItem = FALSE; + s32 currentHP = startingHP; + + // No damage being dealt + if (damageTaken + statusDamage + recurringDamage == 0) + return startingHP; + + // Mon fainted to hazards + if (startingHP == 0) + return 1; + + // Find hits to KO + while (currentHP > 0) + { + // Remove weather damage when it would run out + if (weatherImpact != 0 && weatherDuration == 0) + weatherImpact = 0; + + // Take attack damage for the turn + currentHP = currentHP - damageTaken; + + // If mon is still alive, apply weather impact first, as it might KO the mon before it can heal with its item (order is weather -> item -> status) + if (currentHP != 0) + currentHP = currentHP + weatherImpact; + + // Check if we're at a single use healing item threshold + if (AI_DATA->switchinCandidate.battleMon.ability != ABILITY_KLUTZ && usedSingleUseHealingItem == FALSE) + { + if (currentHP < maxHP / 2) + { + if (item == ITEM_BERRY_JUICE) + { + singleUseItemHeal = holdEffectParam; + } + else if (opposingAbility != ABILITY_UNNERVE && heldItemEffect == HOLD_EFFECT_RESTORE_HP) + { + // By default, this should only encompass Oran Berry and Sitrus Berry. + singleUseItemHeal = holdEffectParam; + if (singleUseItemHeal == 0) + singleUseItemHeal = 1; + } + } + else if (currentHP < maxHP / CONFUSE_BERRY_HP_FRACTION + && opposingAbility != ABILITY_UNNERVE + && (item == ITEM_AGUAV_BERRY || item == ITEM_FIGY_BERRY || item == ITEM_IAPAPA_BERRY || item == ITEM_MAGO_BERRY || item == ITEM_WIKI_BERRY)) + { + singleUseItemHeal = maxHP / CONFUSE_BERRY_HEAL_FRACTION; + if (singleUseItemHeal == 0) + singleUseItemHeal = 1; + } + + // If we used one, apply it without overcapping our maxHP + if (singleUseItemHeal > 0) + { + if ((currentHP + singleUseItemHeal) > maxHP) + currentHP = maxHP; + else + currentHP = currentHP + singleUseItemHeal; + usedSingleUseHealingItem = TRUE; + } + } + + // Healing from items occurs before status so we can do the rest in one line + if (currentHP != 0) + currentHP = currentHP + recurringHealing - recurringDamage - statusDamage; + + // Recalculate toxic damage if needed + if (AI_DATA->switchinCandidate.battleMon.status1 & STATUS1_TOXIC_POISON) + statusDamage = GetSwitchinStatusDamage(battler); + + // Reduce weather duration + if (weatherDuration != 0) + weatherDuration--; + + hitsToKO++; + } + + // If mon had a hypothetical status from TSpikes, clear it + if (AI_DATA->switchinCandidate.hypotheticalStatus == TRUE) + { + AI_DATA->switchinCandidate.battleMon.status1 = 0; + AI_DATA->switchinCandidate.hypotheticalStatus = FALSE; + } + return hitsToKO; +} + +static u16 GetSwitchinTypeMatchup(u32 opposingBattler, struct BattlePokemon battleMon) +{ + + // Check type matchup + u16 typeEffectiveness = UQ_4_12(1.0); + u8 atkType1 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[0], atkType2 = gSpeciesInfo[gBattleMons[opposingBattler].species].types[1], + defType1 = battleMon.type1, defType2 = battleMon.type2; + + // Multiply type effectiveness by a factor depending on type matchup + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType1))); + if (atkType2 != atkType1) + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType1))); + if (defType2 != defType1) + { + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType1, defType2))); + if (atkType2 != atkType1) + typeEffectiveness = uq4_12_multiply(typeEffectiveness, (GetTypeModifier(atkType2, defType2))); + } + return typeEffectiveness; +} + +static int GetRandomSwitchinWithBatonPass(int aliveCount, int bits, int firstId, int lastId, int currentMonId) +{ + // Breakout early if there aren't any Baton Pass mons to save computation time + if (bits == 0) + return PARTY_SIZE; + + // GetBestMonBatonPass randomly chooses between all mons that met Baton Pass check + if ((aliveCount == 2 || (aliveCount > 2 && Random() % 3 == 0)) && bits) + { + do + { + return (Random() % (lastId - firstId)) + firstId; + } while (!(bits & gBitTable[currentMonId])); + } + + // Catch any other cases (such as only one mon alive and it has Baton Pass) + else + return PARTY_SIZE; +} + +static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattler, struct BattlePokemon battleMon) +{ + int i = 0; + u32 playerMove; + s32 damageTaken = 0, maxDamageTaken = 0; + + for (i = 0; i < MAX_MON_MOVES; i++) + { + playerMove = gBattleMons[opposingBattler].moves[i]; + if (playerMove != MOVE_NONE && gBattleMoves[playerMove].power != 0) + { + damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, FALSE); + if (damageTaken > maxDamageTaken) + maxDamageTaken = damageTaken; + } + } + return maxDamageTaken; +} + +// This function splits switching behaviour mid-battle from after a KO. +// Mid battle, it integrates GetBestMonTypeMatchup (vanilla with modifications), GetBestMonDefensive (custom), and GetBestMonBatonPass (vanilla with modifications) +// After a KO, integrates GetBestMonRevengeKiller (custom), GetBestMonTypeMatchup (vanilla with modifications), GetBestMonBatonPass (vanilla with modifications), and GetBestMonDmg (vanilla) +// the Type Matchup code will prioritize switching into a mon with the best type matchup and also a super effective move, or just best type matchup if no super effective move is found +// the Most Defensive code will prioritize switching into the mon that takes the most hits to KO, with a minimum of 4 hits required to be considered a valid option +// the Baton Pass code will prioritize switching into a mon with Baton Pass if it can get in, boost, and BP out without being KO'd, and randomizes between multiple valid options +// the Revenge Killer code will prioritize, in order, OHKO and outspeeds / OHKO, slower but not 2HKO'd / 2HKO, outspeeds and not OHKO'd / 2HKO, slower but not 3HKO'd +// the Most Damage code will prioritize switching into whatever mon deals the most damage, which is generally not as good as having a good Type Matchup +// Everything runs in the same loop to minimize computation time. This makes it harder to read, but hopefully the comments can guide you! + +static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, u32 battler, u32 opposingBattler, u8 battlerIn1, u8 battlerIn2, bool8 isSwitchAfterKO) +{ + int revengeKillerId = PARTY_SIZE, slowRevengeKillerId = PARTY_SIZE, fastThreatenId = PARTY_SIZE, slowThreatenId = PARTY_SIZE, damageMonId = PARTY_SIZE; + int batonPassId = PARTY_SIZE, typeMatchupId = PARTY_SIZE, typeMatchupEffectiveId = PARTY_SIZE, defensiveMonId = PARTY_SIZE, aceMonId = PARTY_SIZE; + int i, j, aliveCount = 0, bits = 0; + s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed + u32 aiMove, hitsToKO, hitsToKOThreshold, maxHitsToKO = 0; + s32 playerMonSpeed = gBattleMons[opposingBattler].speed, playerMonHP = gBattleMons[opposingBattler].hp, aiMonSpeed, maxDamageDealt = 0, damageDealt = 0; + u16 bestResist = UQ_4_12(1.0), bestResistEffective = UQ_4_12(1.0), typeMatchup; + + if (isSwitchAfterKO) + hitsToKOThreshold = 1; // After a KO, mons at minimum need to not be 1-shot, as they switch in for free + else + hitsToKOThreshold = 2; // When switching in otherwise need to not be 2-shot, as they do not switch in for free + + // Iterate through mons + for (i = firstId; i < lastId; i++) + { + // Check mon validity + if (!IsValidForBattle(&party[i]) + || gBattlerPartyIndexes[battlerIn1] == i + || gBattlerPartyIndexes[battlerIn2] == i + || i == *(gBattleStruct->monToSwitchIntoId + battlerIn1) + || i == *(gBattleStruct->monToSwitchIntoId + battlerIn2)) + { + continue; + } + // Save Ace Pokemon for last + else if (IsAceMon(battler, i)) + { + aceMonId = i; + continue; + } + else + aliveCount++; + + InitializeSwitchinCandidate(&party[i]); + + // While not really invalid per say, not really wise to switch into this mon + if (AI_DATA->switchinCandidate.battleMon.ability == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler)) continue; - dmg = AI_CalcPartyMonBestMoveDamage(battler, opposingBattler, &party[i], NULL); - if (bestDmg < dmg) + // Get max number of hits for player to KO AI mon + hitsToKO = GetSwitchinHitsToKO(GetMaxDamagePlayerCouldDealToSwitchin(battler, opposingBattler, AI_DATA->switchinCandidate.battleMon), battler); + + // Track max hits to KO and set GetBestMonDefensive if applicable + if(hitsToKO > maxHitsToKO) { - bestDmg = dmg; - bestMonId = i; + maxHitsToKO = hitsToKO; + if(maxHitsToKO > defensiveMonHitKOThreshold) + defensiveMonId = i; + } + + typeMatchup = GetSwitchinTypeMatchup(opposingBattler, AI_DATA->switchinCandidate.battleMon); + + // Check that good type matchups gets at least two turns and set GetBestMonTypeMatchup if applicable + if (typeMatchup < bestResist) + { + if ((hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed) || hitsToKO > hitsToKOThreshold + 1) // Need to take an extra hit if slower + { + bestResist = typeMatchup; + typeMatchupId = i; + } + } + + aiMonSpeed = AI_DATA->switchinCandidate.battleMon.speed; + + // Check through current mon's moves + for (j = 0; j < MAX_MON_MOVES; j++) + { + aiMove = AI_DATA->switchinCandidate.battleMon.moves[j]; + + // Only do damage calc if switching after KO, don't need it otherwise and saves ~0.02s per turn + if (isSwitchAfterKO && aiMove != MOVE_NONE && gBattleMoves[aiMove].power != 0) + damageDealt = AI_CalcPartyMonDamage(aiMove, battler, opposingBattler, AI_DATA->switchinCandidate.battleMon, TRUE); + + // Check for Baton Pass; hitsToKO requirements mean mon can boost and BP without dying whether it's slower or not + if (aiMove == MOVE_BATON_PASS && ((hitsToKO > hitsToKOThreshold + 1 && AI_DATA->switchinCandidate.battleMon.speed < playerMonSpeed) || (hitsToKO > hitsToKOThreshold && AI_DATA->switchinCandidate.battleMon.speed > playerMonSpeed))) + bits |= gBitTable[i]; + + // Check for mon with resistance and super effective move for GetBestMonTypeMatchup + if (aiMove != MOVE_NONE && gBattleMoves[aiMove].power != 0) + { + if (typeMatchup < bestResistEffective) + { + if (AI_GetTypeEffectiveness(aiMove, battler, opposingBattler) >= UQ_4_12(2.0)) + { + // Assuming a super effective move would do significant damage or scare the player out, so not being as conservative here + if (hitsToKO > hitsToKOThreshold) + { + bestResistEffective = typeMatchup; + typeMatchupEffectiveId = i; + } + } + } + + // If a self destruction move doesn't OHKO, don't factor it into revenge killing + if (gBattleMoves[aiMove].effect == EFFECT_EXPLOSION && damageDealt < playerMonHP) + continue; + + // Check that mon isn't one shot and set GetBestMonDmg if applicable + if (damageDealt > maxDamageDealt) + { + if(hitsToKO > hitsToKOThreshold) + { + maxDamageDealt = damageDealt; + damageMonId = i; + } + } + + // Check if current mon can revenge kill in some capacity + // If AI mon can one shot + if (damageDealt > playerMonHP) + { + // If AI mon is faster and doesn't die to hazards + if ((aiMonSpeed > playerMonSpeed || gBattleMoves[aiMove].priority > 0) && AI_DATA->switchinCandidate.battleMon.hp > GetSwitchinHazardsDamage(battler, &AI_DATA->switchinCandidate.battleMon)) + { + // We have a revenge killer + revengeKillerId = i; + } + + // If AI mon is slower + else + { + // If AI mon can't be OHKO'd + if (hitsToKO > hitsToKOThreshold) + { + // We have a slow revenge killer + slowRevengeKillerId = i; + } + } + } + + // If AI mon can two shot + if (damageDealt > playerMonHP / 2) + { + // If AI mon is faster + if (aiMonSpeed > playerMonSpeed || gBattleMoves[aiMove].priority > 0) + { + // If AI mon can't be OHKO'd + if (hitsToKO > hitsToKOThreshold) + { + // We have a fast threaten + fastThreatenId = i; + } + } + // If AI mon is slower + else + { + // If AI mon can't be 2HKO'd + if (hitsToKO > hitsToKOThreshold + 1) + { + // We have a slow threaten + slowThreatenId = i; + } + } + } + } } } - return bestMonId; + batonPassId = GetRandomSwitchinWithBatonPass(aliveCount, bits, firstId, lastId, i); + + // Different switching priorities depending on switching mid battle vs switching after a KO + if (isSwitchAfterKO) + { + // Return GetBestMonRevengeKiller > GetBestMonTypeMatchup > GetBestMonBatonPass > GetBestMonDmg + if (revengeKillerId != PARTY_SIZE) + return revengeKillerId; + + else if (slowRevengeKillerId != PARTY_SIZE) + return slowRevengeKillerId; + + else if (fastThreatenId != PARTY_SIZE) + return fastThreatenId; + + else if (slowThreatenId != PARTY_SIZE) + return slowThreatenId; + + else if (typeMatchupEffectiveId != PARTY_SIZE) + return typeMatchupEffectiveId; + + else if (typeMatchupId != PARTY_SIZE) + return typeMatchupId; + + else if (batonPassId != PARTY_SIZE) + return batonPassId; + + else if (damageMonId != PARTY_SIZE) + return damageMonId; + } + else + { + // Return GetBestMonTypeMatchup > GetBestMonDefensive > GetBestMonBatonPass + if (typeMatchupEffectiveId != PARTY_SIZE) + return typeMatchupEffectiveId; + + else if (typeMatchupId != PARTY_SIZE) + return typeMatchupId; + + else if (defensiveMonId != PARTY_SIZE) + return defensiveMonId; + + else if (batonPassId != PARTY_SIZE) + return batonPassId; + + // If ace mon is the last available Pokemon and U-Turn/Volt Switch was used - switch to the mon. + else if (aceMonId != PARTY_SIZE + && (gBattleMoves[gLastUsedMove].effect == EFFECT_HIT_ESCAPE || gBattleMoves[gLastUsedMove].effect == EFFECT_PARTING_SHOT)) + return aceMonId; + } + return PARTY_SIZE; } -u8 GetMostSuitableMonToSwitchInto(u32 battler) +u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd) { u32 opposingBattler = 0; u32 bestMonId = PARTY_SIZE; @@ -926,8 +1741,6 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler) s32 firstId = 0; s32 lastId = 0; // + 1 struct Pokemon *party; - s32 i, j, aliveCount = 0; - u32 invalidMons = 0, aceMonId = PARTY_SIZE; if (*(gBattleStruct->monToSwitchIntoId + battler) != PARTY_SIZE) return *(gBattleStruct->monToSwitchIntoId + battler); @@ -960,46 +1773,59 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler) else party = gEnemyParty; - // Get invalid slots ids. - for (i = firstId; i < lastId; i++) + // Split ideal mon decision between after previous mon KO'd (prioritize offensive options) and after switching active mon out (prioritize defensive options), and expand the scope of both. + // Only use better mon selection if AI_FLAG_SMART_MON_CHOICES is set for the trainer. + if (AI_THINKING_STRUCT->aiFlags & AI_FLAG_SMART_MON_CHOICES) { - if (!IsValidForBattle(&party[i]) - || gBattlerPartyIndexes[battlerIn1] == i - || gBattlerPartyIndexes[battlerIn2] == i - || i == *(gBattleStruct->monToSwitchIntoId + battlerIn1) - || i == *(gBattleStruct->monToSwitchIntoId + battlerIn2) - || (GetMonAbility(&party[i]) == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))) // While not really invalid per say, not really wise to switch into this mon.) - { - invalidMons |= gBitTable[i]; - } - else if (IsAceMon(battler, i))// Save Ace Pokemon for last. - { - aceMonId = i; - invalidMons |= gBitTable[i]; - } - else - { - aliveCount++; - } + bestMonId = GetBestMonIntegrated(party, firstId, lastId, battler, opposingBattler, battlerIn1, battlerIn2, switchAfterMonKOd); + return bestMonId; } - bestMonId = GetBestMonBatonPass(party, firstId, lastId, invalidMons, aliveCount, battler, opposingBattler); - if (bestMonId != PARTY_SIZE) - return bestMonId; + // This all handled by the GetBestMonIntegrated function if the AI_FLAG_SMART_MON_CHOICES flag is set + else + { + s32 i, aliveCount = 0; + u32 invalidMons = 0, aceMonId = PARTY_SIZE; + // Get invalid slots ids. + for (i = firstId; i < lastId; i++) + { + if (!IsValidForBattle(&party[i]) + || gBattlerPartyIndexes[battlerIn1] == i + || gBattlerPartyIndexes[battlerIn2] == i + || i == *(gBattleStruct->monToSwitchIntoId + battlerIn1) + || i == *(gBattleStruct->monToSwitchIntoId + battlerIn2) + || (GetMonAbility(&party[i]) == ABILITY_TRUANT && IsTruantMonVulnerable(battler, opposingBattler))) // While not really invalid per say, not really wise to switch into this mon.) + { + invalidMons |= gBitTable[i]; + } + else if (IsAceMon(battler, i))// Save Ace Pokemon for last. + { + aceMonId = i; + invalidMons |= gBitTable[i]; + } + else + { + aliveCount++; + } + } + bestMonId = GetBestMonBatonPass(party, firstId, lastId, invalidMons, aliveCount, battler, opposingBattler); + if (bestMonId != PARTY_SIZE) + return bestMonId; - bestMonId = GetBestMonTypeMatchup(party, firstId, lastId, invalidMons, battler, opposingBattler); - if (bestMonId != PARTY_SIZE) - return bestMonId; + bestMonId = GetBestMonTypeMatchup(party, firstId, lastId, invalidMons, battler, opposingBattler); + if (bestMonId != PARTY_SIZE) + return bestMonId; - bestMonId = GetBestMonDmg(party, firstId, lastId, invalidMons, battler, opposingBattler); - if (bestMonId != PARTY_SIZE) - return bestMonId; + bestMonId = GetBestMonDmg(party, firstId, lastId, invalidMons, battler, opposingBattler); + if (bestMonId != PARTY_SIZE) + return bestMonId; - // If ace mon is the last available Pokemon and switch move was used - switch to the mon. - if (aceMonId != PARTY_SIZE) - return aceMonId; + // If ace mon is the last available Pokemon and switch move was used - switch to the mon. + if (aceMonId != PARTY_SIZE) + return aceMonId; - return PARTY_SIZE; + return PARTY_SIZE; + } } static bool32 AiExpectsToFaintPlayer(u32 battler) @@ -1161,30 +1987,4 @@ static bool32 AI_OpponentCanFaintAiWithMod(u32 battler, u32 healAmount) } } return FALSE; -} - -static bool32 IsAiPartyMonOHKOBy(u32 battlerAi, u32 battlerAtk, struct Pokemon *aiMon) -{ - bool32 ret = FALSE; - struct BattlePokemon *savedBattleMons; - s32 hp = GetMonData(aiMon, MON_DATA_HP); - s32 bestDmg = AI_CalcPartyMonBestMoveDamage(battlerAtk, battlerAi, NULL, aiMon); - - switch (GetNoOfHitsToKO(bestDmg, hp)) - { - case 1: - ret = TRUE; - break; - case 2: // if AI mon is faster allow 2 turns - savedBattleMons = AllocSaveBattleMons(); - PokemonToBattleMon(aiMon, &gBattleMons[battlerAi]); - if (AI_WhoStrikesFirst(battlerAi, battlerAtk, 0) == AI_IS_SLOWER) - ret = TRUE; - else - ret = FALSE; - FreeRestoreBattleMons(savedBattleMons); - break; - } - - return ret; -} +} \ No newline at end of file diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 37bdd38e8837..8dc165a6d8fb 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -3528,32 +3528,16 @@ void FreeRestoreBattleMons(struct BattlePokemon *savedBattleMons) } // party logic -s32 AI_CalcPartyMonBestMoveDamage(u32 battlerAtk, u32 battlerDef, struct Pokemon *attackerMon, struct Pokemon *targetMon) +s32 AI_CalcPartyMonDamage(u32 move, u32 battlerAtk, u32 battlerDef, struct BattlePokemon switchinCandidate, bool8 isPartyMonAttacker) { - s32 i, move, bestDmg, dmg; + s32 dmg; u8 effectiveness; struct BattlePokemon *savedBattleMons = AllocSaveBattleMons(); - - if (attackerMon != NULL) - PokemonToBattleMon(attackerMon, &gBattleMons[battlerAtk]); - if (targetMon != NULL) - PokemonToBattleMon(targetMon, &gBattleMons[battlerDef]); - - for (bestDmg = 0, i = 0; i < MAX_MON_MOVES; i++) - { - if (BattlerHasAi(battlerAtk)) - move = GetMonData(attackerMon, MON_DATA_MOVE1 + i); - else - move = AI_PARTY->mons[GetBattlerSide(battlerAtk)][gBattlerPartyIndexes[battlerAtk]].moves[i]; - - if (move != MOVE_NONE && gBattleMoves[move].power != 0) - { - dmg = AI_CalcDamageSaveBattlers(move, battlerAtk, battlerDef, &effectiveness, FALSE); - if (dmg > bestDmg) - bestDmg = dmg; - } - } - + if(isPartyMonAttacker) + gBattleMons[battlerAtk] = switchinCandidate; + else + gBattleMons[battlerDef] = switchinCandidate; + dmg = AI_CalcDamage(move, battlerAtk, battlerDef, &effectiveness, FALSE, AI_GetWeather(AI_DATA)); FreeRestoreBattleMons(savedBattleMons); return dmg; } diff --git a/src/battle_controller_opponent.c b/src/battle_controller_opponent.c index a74ca529b629..88b2dc96f336 100644 --- a/src/battle_controller_opponent.c +++ b/src/battle_controller_opponent.c @@ -650,7 +650,7 @@ static void OpponentHandleChoosePokemon(u32 battler) // Switching out else if (*(gBattleStruct->AI_monToSwitchIntoId + battler) == PARTY_SIZE) { - chosenMonId = GetMostSuitableMonToSwitchInto(battler); + chosenMonId = GetMostSuitableMonToSwitchInto(battler, TRUE); if (chosenMonId == PARTY_SIZE) { s32 battler1, battler2, firstId, lastId; diff --git a/src/battle_controller_player_partner.c b/src/battle_controller_player_partner.c index 361460ac72e8..f56c14a07669 100644 --- a/src/battle_controller_player_partner.c +++ b/src/battle_controller_player_partner.c @@ -399,7 +399,7 @@ static void PlayerPartnerHandleChoosePokemon(u32 battler) // Switching out else if (gBattleStruct->monToSwitchIntoId[battler] >= PARTY_SIZE || !IsValidForBattle(&gPlayerParty[gBattleStruct->monToSwitchIntoId[battler]])) { - chosenMonId = GetMostSuitableMonToSwitchInto(battler); + chosenMonId = GetMostSuitableMonToSwitchInto(battler, TRUE); if (chosenMonId == PARTY_SIZE || !IsValidForBattle(&gPlayerParty[chosenMonId])) // just switch to the next mon { diff --git a/src/battle_main.c b/src/battle_main.c index ea69b943f8f5..abbb1738fa37 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4081,6 +4081,7 @@ static void HandleTurnActionSelectionState(void) if ((gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart()) && (BattlerHasAi(battler) && !(gBattleTypeFlags & BATTLE_TYPE_PALACE))) { + AI_DATA->mostSuitableMonId = GetMostSuitableMonToSwitchInto(battler, FALSE); gBattleStruct->aiMoveOrAction[battler] = ComputeBattleAiScores(battler); } // fallthrough diff --git a/src/data/items.h b/src/data/items.h index 5e295ae5460c..dc6a948c2140 100644 --- a/src/data/items.h +++ b/src/data/items.h @@ -6436,14 +6436,6 @@ const struct Item gItems[] = .flingPower = 10, }, -#if B_CONFUSE_BERRIES_HEAL >= GEN_8 - #define CONFUSE_BERRY_HEAL_FRACTION 3 -#elif B_CONFUSE_BERRIES_HEAL == GEN_7 - #define CONFUSE_BERRY_HEAL_FRACTION 2 -#else - #define CONFUSE_BERRY_HEAL_FRACTION 8 -#endif - [ITEM_FIGY_BERRY] = { .name = _("Figy Berry"), @@ -6509,8 +6501,6 @@ const struct Item gItems[] = .flingPower = 10, }, -#undef CONFUSE_BERRY_HEAL_FRACTION - [ITEM_RAZZ_BERRY] = { .name = _("Razz Berry"), diff --git a/test/battle/ai.c b/test/battle/ai.c new file mode 100644 index 000000000000..b3f336d15ede --- /dev/null +++ b/test/battle/ai.c @@ -0,0 +1,611 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +AI_SINGLE_BATTLE_TEST("AI gets baited by Protect Switch tactics") // This behavior is to be fixed. +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); + PLAYER(SPECIES_STUNFISK); + PLAYER(SPECIES_PELIPPER); + OPPONENT(SPECIES_DARKRAI) { Moves(MOVE_TACKLE, MOVE_PECK, MOVE_EARTHQUAKE, MOVE_THUNDERBOLT); } + OPPONENT(SPECIES_SCIZOR) { Moves(MOVE_HYPER_BEAM, MOVE_FACADE, MOVE_GIGA_IMPACT, MOVE_EXTREME_SPEED); } + } WHEN { + + TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake + TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt + TURN { SWITCH(player, 0); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt + TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake + TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE);} // E-quake + TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers Bubble over Water Gun if it's slower") +{ + u32 speedPlayer, speedAi; + + PARAMETRIZE { speedPlayer = 200; speedAi = 10; } + PARAMETRIZE { speedPlayer = 10; speedAi = 200; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_SCIZOR) { Speed(speedPlayer); } + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_WATER_GUN, MOVE_BUBBLE); Speed(speedAi); } + } WHEN { + if (speedPlayer > speedAi) + { + TURN { SCORE_GT(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + TURN { SCORE_GT(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + } + else + { + TURN { SCORE_EQ(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + TURN { SCORE_EQ(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); } + } + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers Water Gun over Bubble if it knows that foe has Contrary") +{ + u32 abilityAI; + + PARAMETRIZE { abilityAI = ABILITY_MOXIE; } + PARAMETRIZE { abilityAI = ABILITY_MOLD_BREAKER; } // Mold Breaker ignores Contrary. + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_SHUCKLE) { Ability(ABILITY_CONTRARY); } + OPPONENT(SPECIES_PINSIR) { Moves(MOVE_WATER_GUN, MOVE_BUBBLE); Ability(abilityAI); } + } WHEN { + TURN { MOVE(player, MOVE_DEFENSE_CURL); } + TURN { MOVE(player, MOVE_DEFENSE_CURL); + if (abilityAI == ABILITY_MOLD_BREAKER) { SCORE_EQ(opponent, MOVE_WATER_GUN, MOVE_BUBBLE); } + else { SCORE_GT(opponent, MOVE_WATER_GUN, MOVE_BUBBLE); }} + } SCENE { + MESSAGE("Shuckle's Defense fell!"); // Contrary activates + } THEN { + EXPECT(gBattleResources->aiData->abilities[B_POSITION_PLAYER_LEFT] == ABILITY_CONTRARY); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers moves with better accuracy, but only if they both require the same number of hits to ko") +{ + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + u16 hp, expectedMove, turns, abilityAtk, expectedMove2; + + abilityAtk = ABILITY_NONE; + expectedMove2 = MOVE_NONE; + + // Here it's a simple test, both Slam and Strength deal the same damage, but Strength always hits, whereas Slam often misses. + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_TACKLE; hp = 490; expectedMove = MOVE_STRENGTH; turns = 4; } + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 365; expectedMove = MOVE_STRENGTH; turns = 3; } + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 245; expectedMove = MOVE_STRENGTH; turns = 2; } + PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 125; expectedMove = MOVE_STRENGTH; turns = 1; } + // Mega Kick deals more damage, but can miss more often. Here, AI should choose Mega Kick if it can faint target in less number of turns than Strength. Otherwise, it should use Strength. + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 170; expectedMove = MOVE_MEGA_KICK; turns = 1; } + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 245; expectedMove = MOVE_STRENGTH; turns = 2; } + // Swift always hits and Guts has accuracy of 100%. Hustle lowers accuracy of all physical moves. + PARAMETRIZE { abilityAtk = ABILITY_HUSTLE; move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 5; expectedMove = MOVE_SWIFT; turns = 1; } + PARAMETRIZE { abilityAtk = ABILITY_HUSTLE; move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_GUST; move4 = MOVE_TACKLE; hp = 5; expectedMove = MOVE_GUST; turns = 1; } + // Mega Kick and Slam both have lower accuracy. Gust and Tackle both have 100, so AI can choose either of them. + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_SLAM; move3 = MOVE_TACKLE; move4 = MOVE_GUST; hp = 5; expectedMove = MOVE_GUST; expectedMove2 = MOVE_TACKLE; turns = 1; } + // All moves hit with No guard ability + PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_GUST; hp = 5; expectedMove = MOVE_MEGA_KICK; expectedMove2 = MOVE_GUST; turns = 1; } + // Tests to compare move that always hits and a beneficial effect. A move with higher acc should be chosen in this case. + PARAMETRIZE { move1 = MOVE_SHOCK_WAVE; move2 = MOVE_ICY_WIND; hp = 5; expectedMove = MOVE_SHOCK_WAVE; turns = 1; } + PARAMETRIZE { move1 = MOVE_SHOCK_WAVE; move2 = MOVE_ICY_WIND; move3 = MOVE_THUNDERBOLT; hp = 5; expectedMove = MOVE_SHOCK_WAVE; expectedMove2 = MOVE_THUNDERBOLT; turns = 1; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(hp); } + PLAYER(SPECIES_WOBBUFFET); + ASSUME(gBattleMoves[MOVE_SWIFT].accuracy == 0); + ASSUME(gBattleMoves[MOVE_SLAM].power == gBattleMoves[MOVE_STRENGTH].power); + ASSUME(gBattleMoves[MOVE_MEGA_KICK].power > gBattleMoves[MOVE_STRENGTH].power); + ASSUME(gBattleMoves[MOVE_SLAM].accuracy < gBattleMoves[MOVE_STRENGTH].accuracy); + ASSUME(gBattleMoves[MOVE_MEGA_KICK].accuracy < gBattleMoves[MOVE_STRENGTH].accuracy); + ASSUME(gBattleMoves[MOVE_TACKLE].accuracy == 100); + ASSUME(gBattleMoves[MOVE_GUST].accuracy == 100); + ASSUME(gBattleMoves[MOVE_SHOCK_WAVE].accuracy == 0); + ASSUME(gBattleMoves[MOVE_THUNDERBOLT].accuracy == 100); + ASSUME(gBattleMoves[MOVE_ICY_WIND].accuracy != 100); + OPPONENT(SPECIES_EXPLOUD) { Moves(move1, move2, move3, move4); Ability(abilityAtk); SpAttack(50); } // Low Sp.Atk, so Swift deals less damage than Strength. + } WHEN { + switch (turns) + { + case 1: + if (expectedMove2 != MOVE_NONE) { + TURN { EXPECT_MOVES(opponent, expectedMove, expectedMove2); SEND_OUT(player, 1); } + } + else { + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + } + break; + case 2: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + case 3: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + case 4: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + } + } SCENE { + MESSAGE("Wobbuffet fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers moves which deal more damage instead of moves which are super-effective but deal less damage") +{ + u8 turns = 0; + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + u16 expectedMove, abilityAtk, abilityDef; + + abilityAtk = ABILITY_NONE; + + // Scald and Poison Jab take 3 hits, Waterfall takes 2. + PARAMETRIZE { move1 = MOVE_WATERFALL; move2 = MOVE_SCALD; move3 = MOVE_POISON_JAB; move4 = MOVE_WATER_GUN; expectedMove = MOVE_WATERFALL; turns = 2; } + // Poison Jab takes 3 hits, Water gun 5. Immunity so there's no poison chip damage. + PARAMETRIZE { move1 = MOVE_POISON_JAB; move2 = MOVE_WATER_GUN; expectedMove = MOVE_POISON_JAB; abilityDef = ABILITY_IMMUNITY; turns = 3; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_TYPHLOSION) { Ability(abilityDef); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(move1, move2, move3, move4); Ability(abilityAtk); } + } WHEN { + switch (turns) + { + case 2: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + case 3: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + } + } SCENE { + MESSAGE("Typhlosion fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers Earthquake over Drill Run if both require the same number of hits to ko") +{ + // Drill Run has less accuracy than E-quake, but can score a higher crit. However the chance is too small, so AI should ignore it. + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_TYPHLOSION); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GEODUDE) { Moves(MOVE_EARTHQUAKE, MOVE_DRILL_RUN); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } + TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); SEND_OUT(player, 1); } + } + SCENE { + MESSAGE("Typhlosion fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers a weaker move over a one with a downside effect if both require the same number of hits to ko") +{ + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + u16 hp, expectedMove, turns; + + // Both moves require the same number of turns but Flamethrower will be chosen over Overheat (powerful effect) + PARAMETRIZE { move1 = MOVE_OVERHEAT; move2 = MOVE_FLAMETHROWER; hp = 300; expectedMove = MOVE_FLAMETHROWER; turns = 2; } + // Overheat kill in least amount of turns + PARAMETRIZE { move1 = MOVE_OVERHEAT; move2 = MOVE_FLAMETHROWER; hp = 250; expectedMove = MOVE_OVERHEAT; turns = 1; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(hp); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_TYPHLOSION) { Moves(move1, move2, move3, move4); } + } WHEN { + switch (turns) + { + case 1: + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + case 2: + TURN { EXPECT_MOVE(opponent, expectedMove); } + TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + break; + } + } SCENE { + MESSAGE("Wobbuffet fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI prefers moves with the best possible score, chosen randomly if tied") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(5); }; + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_THUNDERBOLT, MOVE_SLUDGE_BOMB, MOVE_TAKE_DOWN); } + } WHEN { + TURN { EXPECT_MOVES(opponent, MOVE_THUNDERBOLT, MOVE_SLUDGE_BOMB); SEND_OUT(player, 1); } + } + SCENE { + MESSAGE("Wobbuffet fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI can choose a status move that boosts the attack by two") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(278); }; + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_KANGASKHAN) { Moves(MOVE_STRENGTH, MOVE_HORN_ATTACK, MOVE_SWORDS_DANCE); } + } WHEN { + TURN { EXPECT_MOVES(opponent, MOVE_STRENGTH, MOVE_SWORDS_DANCE); } + TURN { EXPECT_MOVE(opponent, MOVE_STRENGTH); SEND_OUT(player, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI chooses the safest option to faint the target, taking into account accuracy and move effect") +{ + u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE; + u16 expectedMove, expectedMove2 = MOVE_NONE; + u16 abilityAtk = ABILITY_NONE, holdItemAtk = ITEM_NONE; + + // Psychic is not very effective, but always hits. Solarbeam requires a charging turn, Double Edge has recoil and Focus Blast can miss; + PARAMETRIZE { abilityAtk = ABILITY_STURDY; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; expectedMove = MOVE_PSYCHIC; } + // Same as above, but ai mon has rock head ability, so it can use Double Edge without taking recoil damage. Psychic can also lower Special Defense, + // but because it faints the target it doesn't matter. + PARAMETRIZE { abilityAtk = ABILITY_ROCK_HEAD; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; + expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_DOUBLE_EDGE; } + // This time it's Solarbeam + Psychic, because the weather is sunny. + PARAMETRIZE { abilityAtk = ABILITY_DROUGHT; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; + expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SOLAR_BEAM; } + // Psychic and Solar Beam are chosen because user is holding Power Herb + PARAMETRIZE { abilityAtk = ABILITY_STURDY; holdItemAtk = ITEM_POWER_HERB; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; + expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SOLAR_BEAM; } + // Psychic and Skull Bash are chosen because user is holding Power Herb + PARAMETRIZE { abilityAtk = ABILITY_STURDY; holdItemAtk = ITEM_POWER_HERB; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SKULL_BASH; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; + expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SKULL_BASH; } + // Skull Bash is chosen because it's the most accurate and is holding Power Herb + PARAMETRIZE { abilityAtk = ABILITY_STURDY; holdItemAtk = ITEM_POWER_HERB; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SKULL_BASH; move3 = MOVE_SLAM; move4 = MOVE_DOUBLE_EDGE; + expectedMove = MOVE_SKULL_BASH; } + // Crabhammer is chosen even if Skull Bash is more accurate, the user has no Power Herb + PARAMETRIZE { abilityAtk = ABILITY_STURDY; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SKULL_BASH; move3 = MOVE_SLAM; move4 = MOVE_CRABHAMMER; + expectedMove = MOVE_CRABHAMMER; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(5); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_GEODUDE) { Moves(move1, move2, move3, move4); Ability(abilityAtk); Item(holdItemAtk); } + } WHEN { + TURN { if (expectedMove2 == MOVE_NONE) { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); } + else {EXPECT_MOVES(opponent, expectedMove, expectedMove2); SCORE_EQ(opponent, expectedMove, expectedMove2); SEND_OUT(player, 1);} + } + } + SCENE { + MESSAGE("Wobbuffet fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI won't use Solar Beam if there is no Sun up or the user is not holding Power Herb") +{ + u16 abilityAtk = ABILITY_NONE; + u16 holdItemAtk = ITEM_NONE; + + PARAMETRIZE { abilityAtk = ABILITY_DROUGHT; } + PARAMETRIZE { holdItemAtk = ITEM_POWER_HERB; } + PARAMETRIZE { } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET) { HP(211); } + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_TYPHLOSION) { Moves(MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); Ability(abilityAtk); Item(holdItemAtk); } + } WHEN { + if (abilityAtk == ABILITY_DROUGHT) { + TURN { EXPECT_MOVES(opponent, MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); } + TURN { EXPECT_MOVES(opponent, MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); SEND_OUT(player, 1); } + } else if (holdItemAtk == ITEM_POWER_HERB) { + TURN { EXPECT_MOVES(opponent, MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); MOVE(player, MOVE_KNOCK_OFF); } + TURN { EXPECT_MOVE(opponent, MOVE_GRASS_PLEDGE); SEND_OUT(player, 1); } + } else { + TURN { EXPECT_MOVE(opponent, MOVE_GRASS_PLEDGE); } + TURN { EXPECT_MOVE(opponent, MOVE_GRASS_PLEDGE); SEND_OUT(player, 1); } + } + } SCENE { + MESSAGE("Wobbuffet fainted!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI won't use ground type attacks against flying type Pokemon unless Gravity is in effect") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_CROBAT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_EARTHQUAKE, MOVE_TACKLE, MOVE_POISON_STING, MOVE_GUST); } + } WHEN { + TURN { NOT_EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } + TURN { MOVE(player, MOVE_GRAVITY); NOT_EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } + TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); SEND_OUT(player, 1); } + } SCENE { + MESSAGE("Gravity intensified!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI will not switch in a Pokemon which is slower and gets 1HKOed after fainting") +{ + bool32 alakazamFaster; + u32 speedAlakazm; + + KNOWN_FAILING; + + PARAMETRIZE{ speedAlakazm = 200; alakazamFaster = FALSE; } + PARAMETRIZE{ speedAlakazm = 400; alakazamFaster = TRUE; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING); + PLAYER(SPECIES_WEAVILE) { Speed(300); Ability(ABILITY_SHADOW_TAG); } // Weavile has Shadow Tag, so AI can't switch on the first turn, but has to do it after fainting. + OPPONENT(SPECIES_KADABRA) { Speed(200); Moves(MOVE_PSYCHIC, MOVE_DISABLE, MOVE_TAUNT, MOVE_CALM_MIND); } + OPPONENT(SPECIES_ALAKAZAM) { Speed(speedAlakazm); Moves(MOVE_FOCUS_BLAST, MOVE_PSYCHIC); } // Alakazam has a move which OHKOes Weavile, but it doesn't matter if he's getting KO-ed first. + OPPONENT(SPECIES_BLASTOISE) { Speed(200); Moves(MOVE_BUBBLE_BEAM, MOVE_WATER_GUN, MOVE_LEER, MOVE_STRENGTH); } // Can't OHKO, but survives a hit from Weavile's Night Slash. + } WHEN { + TURN { MOVE(player, MOVE_NIGHT_SLASH) ; EXPECT_SEND_OUT(opponent, alakazamFaster ? 1 : 2); } + } SCENE { + MESSAGE("Foe Kadabra fainted!"); + if (alakazamFaster) { + MESSAGE("{PKMN} TRAINER LEAF sent out Alakazam!"); + } else { + MESSAGE("{PKMN} TRAINER LEAF sent out Blastoise!"); + } + } +} + +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_TACKLE, 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_TACKLE); } + 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("Foe 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 (gBattleMoves[j].split == SPLIT_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_TACKLE, 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("Foe Wobbuffet used Helping Hand!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI without any flags chooses moves at random - singles") +{ + GIVEN { + AI_FLAGS(0); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); } + } WHEN { + TURN { EXPECT_MOVES(opponent, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); + SCORE_EQ_VAL(opponent, MOVE_SPLASH, AI_SCORE_DEFAULT); + SCORE_EQ_VAL(opponent, MOVE_EXPLOSION, AI_SCORE_DEFAULT); + SCORE_EQ_VAL(opponent, MOVE_RAGE, AI_SCORE_DEFAULT); + SCORE_EQ_VAL(opponent, MOVE_HELPING_HAND, AI_SCORE_DEFAULT); + } + } +} + +AI_DOUBLE_BATTLE_TEST("AI without any flags chooses moves at random - doubles") +{ + GIVEN { + AI_FLAGS(0); + PLAYER(SPECIES_WOBBUFFET); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); } + OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); } + } WHEN { + TURN { EXPECT_MOVES(opponentLeft, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); + EXPECT_MOVES(opponentRight, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); + SCORE_EQ_VAL(opponentLeft, MOVE_SPLASH, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentLeft, MOVE_EXPLOSION, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentLeft, MOVE_RAGE, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_SPLASH, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_EXPLOSION, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_RAGE, AI_SCORE_DEFAULT, target:playerLeft); + SCORE_EQ_VAL(opponentRight, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft); + } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI will not switch in a Pokemon which is slower and gets 1HKOed after fainting") +{ + bool32 alakazamFirst; + u32 speedAlakazm; + u32 aiSmartSwitchFlags = 0; + + PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = TRUE; } // AI will always send out Alakazan as it sees a KO with Focus Blast, even if Alakazam dies before it can get it off + PARAMETRIZE{ speedAlakazm = 200; alakazamFirst = FALSE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES lets AI see that Alakazam would be KO'd before it can KO, and won't switch it in + PARAMETRIZE{ speedAlakazm = 400; alakazamFirst = TRUE; aiSmartSwitchFlags = AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES recognizes that Alakazam is faster and can KO, and will switch it in + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); + PLAYER(SPECIES_WEAVILE) { Speed(300); Ability(ABILITY_SHADOW_TAG); } // Weavile has Shadow Tag, so AI can't switch on the first turn, but has to do it after fainting. + OPPONENT(SPECIES_KADABRA) { Speed(200); Moves(MOVE_PSYCHIC, MOVE_DISABLE, MOVE_TAUNT, MOVE_CALM_MIND); } + OPPONENT(SPECIES_ALAKAZAM) { Speed(speedAlakazm); Moves(MOVE_FOCUS_BLAST, MOVE_PSYCHIC); } // Alakazam has a move which OHKOes Weavile, but it doesn't matter if he's getting KO-ed first. + OPPONENT(SPECIES_BLASTOISE) { Speed(200); Moves(MOVE_BUBBLE_BEAM, MOVE_WATER_GUN, MOVE_LEER, MOVE_STRENGTH); } // Can't OHKO, but survives a hit from Weavile's Night Slash. + } WHEN { + TURN { MOVE(player, MOVE_NIGHT_SLASH) ; EXPECT_SEND_OUT(opponent, alakazamFirst ? 1 : 2); } // AI doesn't send out Alakazam if it gets outsped + } SCENE { + MESSAGE("Foe Kadabra fainted!"); + if (alakazamFirst) { + MESSAGE("{PKMN} TRAINER LEAF sent out Alakazam!"); + } else { + MESSAGE("{PKMN} TRAINER LEAF sent out Blastoise!"); + } + } +} + +AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) {Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_CROBAT) {Moves(MOVE_TACKLE); } + } WHEN { + TURN { MOVE(player, MOVE_PERISH_SONG); } + TURN { ; } + TURN { ; } + TURN { EXPECT_SWITCH(opponent, 1); } + } SCENE { + MESSAGE("{PKMN} TRAINER LEAF sent out Crobat!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI considers hazard damage when choosing which Pokemon to switch in") +{ + u32 aiIsSmart = 0; + u32 aiSmartSwitchFlags = 0; + + PARAMETRIZE{ aiIsSmart = 0; aiSmartSwitchFlags = 0; } // AI doesn't care about hazard damage resulting in Pokemon being KO'd + PARAMETRIZE{ aiIsSmart = 1; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // AI_FLAG_SMART_MON_CHOICES avoids being KO'd as a result of hazards damage + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); + PLAYER(SPECIES_MEGANIUM) { Speed(100); SpDefense(328); SpAttack(265); Moves(MOVE_STEALTH_ROCK, MOVE_SURF); } // Meganium does ~56% minimum ~66% maximum, enough to KO Charizard after rocks and never KO Typhlosion after rocks + OPPONENT(SPECIES_PONYTA) { Level(5); Speed(5); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_CHARIZARD) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium + OPPONENT(SPECIES_TYPHLOSION) { Speed(200); Moves(MOVE_FLAMETHROWER); SpAttack(317); SpDefense(207); MaxHP(297); } // Outspeends and 2HKOs Meganium + } WHEN { + TURN { MOVE(player, MOVE_STEALTH_ROCK) ;} + TURN { MOVE(player, MOVE_SURF) ; EXPECT_SEND_OUT(opponent, aiIsSmart ? 2 : 1); } // AI sends out Typhlosion to get the KO with the flag rather than Charizard + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize type matchup + SE move, then type matchup") +{ + u32 aiSmartSwitchFlags = 0; + u32 move1; + u32 move2; + u32 expectedIndex; + + PARAMETRIZE{ expectedIndex = 3; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; } // When not smart, AI will only switch in a defensive mon if it has a SE move, otherwise will just default to damage + PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = 0; } + PARAMETRIZE{ expectedIndex = 2; move1 = MOVE_TACKLE; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } // When smart, AI will prioritize SE move, but still switch in good type matchup without SE move + PARAMETRIZE{ expectedIndex = 1; move1 = MOVE_GIGA_DRAIN; move2 = MOVE_TACKLE; aiSmartSwitchFlags = AI_FLAG_SMART_MON_CHOICES; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSmartSwitchFlags); + PLAYER(SPECIES_MARSHTOMP) { Level(30); Moves(MOVE_MUD_BOMB, MOVE_WATER_GUN, MOVE_GROWL, MOVE_MUD_SHOT); Speed(5); } + OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(6); } // Forces switchout + OPPONENT(SPECIES_TANGELA) { Level(30); Moves(move1); Speed(4); } + OPPONENT(SPECIES_LOMBRE) { Level(30); Moves(move2); Speed(4); } + OPPONENT(SPECIES_HARIYAMA) { Level(30); Moves(MOVE_VITAL_THROW); Speed(4); } + } WHEN { + TURN { MOVE(player, MOVE_GROWL) ; EXPECT_SWITCH(opponent, expectedIndex); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize defensive options") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } + OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout + OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } // Mid battle, AI sends out Aron + OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } + } WHEN { + TURN { MOVE(player, MOVE_WING_ATTACK) ; EXPECT_SWITCH(opponent, 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Post-KO switches prioritize offensive options") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES); + PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } + OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_TACKLE); Speed(4); } + OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); } // Mid battle, AI sends out Aron + OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } + } WHEN { + TURN { MOVE(player, MOVE_WING_ATTACK) ; EXPECT_SEND_OUT(opponent, 2); } + } +}