Add AI_FLAG_RANDOMIZE_SWITCHINS (#6222)

This commit is contained in:
Pawkkie 2025-12-02 09:38:05 -05:00 committed by GitHub
parent dea8974ddc
commit 00b3179f75
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 184 additions and 67 deletions

View File

@ -204,3 +204,6 @@ AI will predict what move the player is going to use based on what move it would
## `AI_FLAG_PP_STALL_PREVENTION`
This flag aims to prevent the player from PP stalling the AI by switching between immunities. The AI mon's move scores will slowly decay for absorbed moves over time, eventually making its moves unpredictable. More detailed control for this behaviour can be customized in the `ai.h` config file.
## `AI_FLAG_RANDOMIZE_SWITCHIN`
AI will randomly choose between eligible switchin candidates rather than always picking the last one in the party. For example, if the AI has two mons that can revenge kill the player's mon after a KO, by default the AI will only track the most recent eligible candidate, and will always send in the last one in party order as a result. With this flag, it will instead track all of the eligible mons, and randomly choose between them when deciding which to send out.

0
include/battle.h Executable file → Normal file
View File

View File

@ -95,6 +95,9 @@
// AI_FLAG_SMART_SWITCHING settings
#define SMART_SWITCHING_OMNISCIENT FALSE // AI will use omniscience for switching calcs, regardless of omniscience setting otherwise
// AI_FLAG_RANDOMIZE_SWITCHIN settings
#define RANDOMIZE_SWITCHIN_ANY_VALID TRUE // If AI has no good candidate mons, it will still choose randomly from all valid options rather than defaulting to the last one in party order
// Configurations specifically for AI_FLAG_DOUBLE_BATTLE.
#define FRIENDLY_FIRE_RISKY_THRESHOLD 2 // AI_FLAG_RISKY acceptable number of hits to KO the partner via friendly fire
#define FRIENDLY_FIRE_NORMAL_THRESHOLD 3 // typical acceptable number of hits to KO the partner via friendly fire

View File

@ -39,10 +39,11 @@
#define AI_FLAG_ASSUME_STATUS_MOVES AI_FLAG(29) // AI has a chance to know certain non-damaging moves, and also Fake Out and Super Fang. Restricted version of AI_FLAG_OMNISCIENT.
#define AI_FLAG_ATTACKS_PARTNER AI_FLAG(30) // AI specific to double battles; AI can deliberately attack its 'partner.'
#define AI_FLAG_KNOW_OPPONENT_PARTY AI_FLAG(31) // AI knows all the species in the player's party, but not moves/items/abilities unless they've been seen.
#define AI_FLAG_RANDOMIZE_SWITCHIN AI_FLAG(32) // AI will randomly choose between eligible switchin candidates of a given category instead of picking the last one in the party. Does nothing without AI_FLAG_SMART_MON_CHOICES
// The following options are enough to have a basic/smart trainer. Any other addtion could make the trainer worse/better depending on the flag
#define AI_FLAG_BASIC_TRAINER (AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY)
#define AI_FLAG_SMART_TRAINER (AI_FLAG_BASIC_TRAINER | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PP_STALL_PREVENTION | AI_FLAG_SMART_TERA)
#define AI_FLAG_SMART_TRAINER (AI_FLAG_BASIC_TRAINER | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_PP_STALL_PREVENTION | AI_FLAG_SMART_TERA | AI_FLAG_RANDOMIZE_SWITCHIN)
#define AI_FLAG_PREDICTION (AI_FLAG_PREDICT_SWITCH | AI_FLAG_PREDICT_INCOMING_MON | AI_FLAG_PREDICT_MOVE)
#define AI_FLAG_ASSUMPTIONS (AI_FLAG_ASSUME_STAB | AI_FLAG_ASSUME_STATUS_MOVES | AI_FLAG_WEIGH_ABILITY_PREDICTION)

View File

@ -204,6 +204,10 @@ enum RandomTag
RNG_AI_BOOST_INTO_HAZE,
RNG_AI_SHOULD_RECOVER,
RNG_AI_PRIORITIZE_LAST_CHANCE,
RNG_AI_RANDOM_SWITCHIN_POST_KO,
RNG_AI_RANDOM_SWITCHIN_MID_BATTLE,
RNG_AI_RANDOM_VALID_SWITCHIN_POST_KO,
RNG_AI_RANDOM_VALID_SWITCHIN_MID_BATTLE,
RNG_HEALER,
RNG_DEXNAV_ENCOUNTER_LEVEL,
RNG_AI_ASSUME_STATUS_SLEEP,
@ -271,6 +275,9 @@ const void *RandomElementArrayDefault(enum RandomTag, const void *array, size_t
u8 RandomWeightedIndex(u8 *weights, u8 length);
u32 RandomBit(enum RandomTag tag, u32 bits);
u32 RandomBitIndex(enum RandomTag tag, u32 bits);
#if TESTING
u32 RandomUniformTrials(enum RandomTag tag, u32 lo, u32 hi, bool32 (*reject)(u32), void *caller);
u32 RandomUniformDefaultValue(enum RandomTag tag, u32 lo, u32 hi, bool32 (*reject)(u32), void *caller);

View File

@ -1592,17 +1592,14 @@ static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 inva
static u32 GetFirstNonInvalidMon(u32 firstId, u32 lastId, u32 invalidMons)
{
u32 chosenMonId = PARTY_SIZE;
for (u32 i = (lastId-1); i > firstId; i--)
{
if (!((1 << i) & invalidMons))
{
// first non invalid mon found
chosenMonId = i;
break;
return i;
}
}
return chosenMonId;
return PARTY_SIZE;
}
bool32 IsMonGrounded(enum HoldEffect heldItemEffect, enum Ability ability, enum Type type1, enum Type type2)
@ -2058,24 +2055,58 @@ static u32 GetBattleMonTypeMatchup(struct BattlePokemon opposingBattleMon, struc
return typeEffectiveness1 + typeEffectiveness2;
}
static int GetRandomSwitchinWithBatonPass(int aliveCount, int bits, int firstId, int lastId, int currentMonId)
static u32 GetSwitchinCandidate(u32 switchinCategory, u32 battler, int firstId, int lastId, enum SwitchType switchType)
{
// Breakout early if there aren't any Baton Pass mons to save computation time
if (bits == 0)
u32 i;
if (switchinCategory == 0)
return PARTY_SIZE;
// GetBestMonBatonPass randomly chooses between all mons that met Baton Pass check
if ((aliveCount == 2 || (aliveCount > 2 && Random() % 3 == 0)) && bits)
// Randomize between eligible mons
if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_RANDOMIZE_SWITCHIN)
{
do
{
return (Random() % (lastId - firstId)) + firstId;
} while (!(bits & (1 << (currentMonId))));
// This split is necessary because the test system can't handle multiple calls with the same random tag in the same turn
if (switchType == SWITCH_AFTER_KO)
return RandomBitIndex(RNG_AI_RANDOM_SWITCHIN_POST_KO, switchinCategory); // Can't pass this anything with no set bits
else
return RandomBitIndex(RNG_AI_RANDOM_SWITCHIN_MID_BATTLE, switchinCategory); // Can't pass this anything with no set bits
}
// Catch any other cases (such as only one mon alive and it has Baton Pass)
else
// Pick last eligible mon in party order
for (i = lastId; i >= firstId; i--)
{
if (switchinCategory & (1 << i))
return i;
}
return PARTY_SIZE;
}
static u32 GetValidSwitchinCandidate(u32 validMonIds, u32 battler, u32 firstId, u32 lastId, enum SwitchType switchType)
{
u32 i;
if (validMonIds == 0)
return PARTY_SIZE;
// Randomize between valid mons
if ((gAiThinkingStruct->aiFlags[battler] & AI_FLAG_RANDOMIZE_SWITCHIN) && RANDOMIZE_SWITCHIN_ANY_VALID)
{
// This split is necessary because the test system can't handle multiple calls with the same random tag in the same turn
if (switchType == SWITCH_AFTER_KO)
return RandomBitIndex(RNG_AI_RANDOM_VALID_SWITCHIN_POST_KO, validMonIds); // Can't pass this anything with no set bits
else
return RandomBitIndex(RNG_AI_RANDOM_VALID_SWITCHIN_MID_BATTLE, validMonIds); // Can't pass this anything with no set bits
}
// Pick last valid mon in party order
for (i = (lastId-1); i > firstId; i--)
{
if (validMonIds & (1 << i))
return i;
}
return PARTY_SIZE;
}
static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattler, struct BattlePokemon battleMon, u32 *bestPlayerMove)
@ -2220,15 +2251,15 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
{
struct IncomingHealInfo healInfoData;
const struct IncomingHealInfo *healInfo = &healInfoData;
int revengeKillerId = PARTY_SIZE, slowRevengeKillerId = PARTY_SIZE, fastThreatenId = PARTY_SIZE, slowThreatenId = PARTY_SIZE, damageMonId = PARTY_SIZE, generic1v1MonId = PARTY_SIZE;
int batonPassId = PARTY_SIZE, typeMatchupId = PARTY_SIZE, typeMatchupEffectiveId = PARTY_SIZE, defensiveMonId = PARTY_SIZE, aceMonId = PARTY_SIZE, trapperId = PARTY_SIZE, healingCandidateId = PARTY_SIZE;
int i, j, aliveCount = 0, bits = 0, aceMonCount = 0;
u32 revengeKillerIds = 0, slowRevengeKillerIds = 0, fastThreatenIds = 0, slowThreatenIds = 0, damageMonIds = 0, generic1v1MonIds = 0;
u32 batonPassIds = 0, typeMatchupIds = 0, typeMatchupEffectiveIds = 0, defensiveMonIds = 0, trapperIds = 0, healingCandidateIds = 0;
int i, j, aceMonId = PARTY_SIZE, aliveCount = 0, aceMonCount = 0;
s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed
s32 playerMonHP = gBattleMons[opposingBattler].hp, maxDamageDealt = 0, damageDealt = 0, bestHealGain = 0, monMaxDamage = 0;
s32 playerMonHP = gBattleMons[opposingBattler].hp, maxDamageDealt = 0, damageDealt = 0, bestHealGain = 0;
u32 aiMove, hitsToKOAI, hitsToKOPlayer, hitsToKOAIPriority, bestPlayerMove = MOVE_NONE, bestPlayerPriorityMove = MOVE_NONE, maxHitsToKO = 0;
u32 bestResist = UQ_4_12(2.0), bestResistEffective = UQ_4_12(2.0), typeMatchup; // 2.0 is the default "Neutral" matchup from GetBattleMonTypeMatchup
bool32 isFreeSwitch = IsFreeSwitch(switchType, battlerIn1, opposingBattler), isSwitchinFirst, isSwitchinFirstPriority, canSwitchinWin1v1;
u32 invalidMons = 0;
u32 validMonIds = 0;
uq4_12_t effectiveness = UQ_4_12(1.0);
GetIncomingHealInfo(battler, &healInfoData);
@ -2243,7 +2274,6 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
|| i == gBattleStruct->monToSwitchIntoId[battlerIn1]
|| i == gBattleStruct->monToSwitchIntoId[battlerIn2])
{
invalidMons |= 1u << i;
continue;
}
// Save Ace Pokemon for last
@ -2251,11 +2281,13 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
{
aceMonId = i;
aceMonCount++;
invalidMons |= 1u << i;
continue;
}
else
{
aliveCount++;
validMonIds |= (1u << i);
}
InitializeSwitchinCandidate(&party[i]);
@ -2279,8 +2311,6 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
canSwitchinWin1v1 = FALSE;
bool32 anyMoveCanWin1v1 = FALSE;
monMaxDamage = 0;
// Check through current mon's moves
for (j = 0; j < MAX_MON_MOVES; j++)
{
@ -2302,7 +2332,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (GetMoveEffect(aiMove) == EFFECT_BATON_PASS)
{
if ((isSwitchinFirst && hitsToKOAI > 1) || hitsToKOAI > 2) // Need to take an extra hit if slower
bits |= 1u << i;
batonPassIds |= (1u << i);
}
// Check that good type matchups get at least two turns and set best type matchup mon
@ -2311,7 +2341,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (canSwitchinWin1v1)
{
bestResist = typeMatchup;
typeMatchupId = i;
typeMatchupIds |= (1u << i);
}
}
@ -2320,11 +2350,11 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
{
maxHitsToKO = hitsToKOAI;
if (maxHitsToKO > defensiveMonHitKOThreshold)
defensiveMonId = i;
defensiveMonIds |= (1u << i);
}
if (canSwitchinWin1v1)
generic1v1MonId = i;
generic1v1MonIds |= (1u << i);
// Check for mon with resistance and super effective move for best type matchup mon with effective move
if (aiMove != MOVE_NONE && !IsBattleMoveStatus(aiMove))
@ -2336,7 +2366,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (canSwitchinWin1v1)
{
bestResistEffective = typeMatchup;
typeMatchupEffectiveId = i;
typeMatchupEffectiveIds |= (1u << i);
}
}
}
@ -2347,15 +2377,13 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
&& damageDealt < playerMonHP)
continue;
if (damageDealt > monMaxDamage)
monMaxDamage = damageDealt;
// Check that mon isn't one shot and set best damage mon
if (damageDealt > maxDamageDealt)
{
if ((isFreeSwitch && hitsToKOAI > 1) || hitsToKOAI > 2) // This is a "default", we have uniquely low standards
{
maxDamageDealt = damageDealt;
damageMonId = i;
damageMonIds |= (1u << i);
}
}
@ -2366,9 +2394,9 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (canSwitchinWin1v1)
{
if (isSwitchinFirst)
revengeKillerId = i;
revengeKillerIds |= (1u << i);
else
slowRevengeKillerId = i;
slowRevengeKillerIds |= (1u << i);
}
}
@ -2378,9 +2406,9 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (canSwitchinWin1v1)
{
if (isSwitchinFirst)
fastThreatenId = i;
fastThreatenIds |= (1u << i);
else
slowThreatenId = i;
slowThreatenIds |= (1u << i);
}
}
@ -2389,7 +2417,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
|| (CanAbilityTrapOpponent(gAiLogicData->abilities[opposingBattler], opposingBattler) && gAiLogicData->switchinCandidate.battleMon.ability == ABILITY_TRACE))
&& CountUsablePartyMons(opposingBattler) > 0
&& canSwitchinWin1v1)
trapperId = i;
trapperIds |= (1u << i);
}
}
@ -2411,51 +2439,46 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
if (anyMoveCanWin1v1 && healGain > (s32)(maxHP / 4) && healGain > bestHealGain)
{
bestHealGain = healGain;
healingCandidateId = i;
healingCandidateIds |= (1u << i);
}
}
if (monMaxDamage == 0)
invalidMons |= 1u << i;
}
batonPassId = GetRandomSwitchinWithBatonPass(aliveCount, bits, firstId, lastId, i);
// Different switching priorities depending on switching mid battle vs switching after a KO or slow switch
if (isFreeSwitch)
{
// Return Trapper > Revenge Killer > Type Matchup > Healing Candidate > Baton Pass > Best Damage
if (trapperId != PARTY_SIZE) return trapperId;
else 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 (healingCandidateId != PARTY_SIZE) return healingCandidateId;
else if (batonPassId != PARTY_SIZE) return batonPassId;
else if (generic1v1MonId != PARTY_SIZE) return generic1v1MonId;
else if (damageMonId != PARTY_SIZE) return damageMonId;
if (trapperIds != 0) return GetSwitchinCandidate(trapperIds, battler, firstId, lastId, switchType);
else if (revengeKillerIds != 0) return GetSwitchinCandidate(revengeKillerIds, battler, firstId, lastId, switchType);
else if (slowRevengeKillerIds != 0) return GetSwitchinCandidate(slowRevengeKillerIds, battler, firstId, lastId, switchType);
else if (fastThreatenIds != 0) return GetSwitchinCandidate(fastThreatenIds, battler, firstId, lastId, switchType);
else if (slowThreatenIds != 0) return GetSwitchinCandidate(slowThreatenIds, battler, firstId, lastId, switchType);
else if (typeMatchupEffectiveIds != 0) return GetSwitchinCandidate(typeMatchupEffectiveIds, battler, firstId, lastId, switchType);
else if (typeMatchupIds != 0) return GetSwitchinCandidate(typeMatchupIds, battler, firstId, lastId, switchType);
else if (healingCandidateIds != 0) return GetSwitchinCandidate(healingCandidateIds, battler, firstId, lastId, switchType);
else if (batonPassIds != 0) return GetSwitchinCandidate(batonPassIds, battler, firstId, lastId, switchType);
else if (generic1v1MonIds != 0) return GetSwitchinCandidate(generic1v1MonIds, battler, firstId, lastId, switchType);
else if (damageMonIds != 0) return GetSwitchinCandidate(damageMonIds, battler, firstId, lastId, switchType);
}
else
{
// Return Trapper > Type Matchup > Best Defensive > Healing Candidate > Baton Pass
if (trapperId != PARTY_SIZE) return trapperId;
else if (typeMatchupEffectiveId != PARTY_SIZE) return typeMatchupEffectiveId;
else if (typeMatchupId != PARTY_SIZE) return typeMatchupId;
else if (defensiveMonId != PARTY_SIZE) return defensiveMonId;
else if (healingCandidateId != PARTY_SIZE) return healingCandidateId;
else if (batonPassId != PARTY_SIZE) return batonPassId;
else if (generic1v1MonId != PARTY_SIZE) return generic1v1MonId;
if (trapperIds != 0) return GetSwitchinCandidate(trapperIds, battler, firstId, lastId, switchType);
else if (typeMatchupEffectiveIds != 0) return GetSwitchinCandidate(typeMatchupEffectiveIds, battler, firstId, lastId, switchType);
else if (typeMatchupIds != 0) return GetSwitchinCandidate(typeMatchupIds, battler, firstId, lastId, switchType);
else if (defensiveMonIds != 0) return GetSwitchinCandidate(defensiveMonIds, battler, firstId, lastId, switchType);
else if (healingCandidateIds != 0) return GetSwitchinCandidate(healingCandidateIds, battler, firstId, lastId, switchType);
else if (batonPassIds != 0) return GetSwitchinCandidate(batonPassIds, battler, firstId, lastId, switchType);
else if (generic1v1MonIds != 0) return GetSwitchinCandidate(generic1v1MonIds, battler, firstId, lastId, switchType);
}
// Not required to switch here and no good candidates, bail
if (switchType == SWITCH_MID_BATTLE_OPTIONAL)
return PARTY_SIZE;
// Fallback
u32 bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons);
if (bestMonId != PARTY_SIZE)
return bestMonId;
if (validMonIds != 0)
return GetValidSwitchinCandidate(validMonIds, battler, firstId, lastId, switchType);
// If ace mon is the last available Pokemon and U-Turn/Volt Switch or Eject Pack/Button was used - switch to the mon.
if (aceMonId != PARTY_SIZE && CountUsablePartyMons(battler) <= aceMonCount)

View File

@ -224,3 +224,37 @@ u8 RandomWeightedIndex(u8 *weights, u8 length)
}
return 0;
}
// Returns whole word with just the random bit set; don't call with no set bits
u32 RandomBit(enum RandomTag tag, u32 bits)
{
u32 setBits[32];
u32 n = 0;
for (u32 mask = 1; mask != 0; mask <<= 1)
{
if (bits & mask)
setBits[n++] = mask;
}
if (n == 0)
return 0; // This is a little awkward, there are no set bits!
else
return setBits[RandomUniform(tag, 0, n-1)];
}
// Returns the index instead; don't call with no set bits
u32 RandomBitIndex(enum RandomTag tag, u32 bits)
{
u8 setIndexes[32];
u32 n = 0;
for (u32 i = 0; i < 32; i++)
{
if (bits & (1 << i))
setIndexes[n++] = i;
}
if (n == 0)
return 0; // This is a little awkward, there are no set bits!
else
return setIndexes[RandomUniform(tag, 0, n-1)];
}

View File

@ -414,7 +414,7 @@ AI_SINGLE_BATTLE_TEST("When AI switches out due to having no move that affects t
OPPONENT(SPECIES_ABRA) { Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_ABRA) { Moves(MOVE_TACKLE); }
} WHEN {
TURN { MOVE(player, MOVE_SHADOW_BALL); EXPECT_SWITCH(opponent, 2); EXPECT_SEND_OUT(opponent, 0);}
TURN { MOVE(player, MOVE_SHADOW_BALL); EXPECT_SWITCH(opponent, 2); EXPECT_SEND_OUT(opponent, 4);}
TURN { MOVE(player, MOVE_SHADOW_BALL); EXPECT_MOVE(opponent, MOVE_TACKLE); }
}
}
@ -1781,3 +1781,49 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: AI considers both meeting and
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EXPLOSION); hp > 80 ? EXPECT_SEND_OUT(opponent, 2) : EXPECT_SEND_OUT(opponent, 1); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_RANDOMIZE_SWITCHIN: AI will randomly choose between eligible switchin candidates of the same category")
{
u32 trials; // Two trial counts to ensure randomization is scalable
PARAMETRIZE { trials = 30; }
PARAMETRIZE { trials = 50; }
PASSES_RANDOMLY(10, trials, RNG_AI_RANDOM_SWITCHIN_POST_KO);
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_RANDOMIZE_SWITCHIN);
PLAYER(SPECIES_ZIGZAGOON) { Moves(MOVE_PROTECT, MOVE_TACKLE); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_EXPLOSION); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_CLOSE_COMBAT); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_CLOSE_COMBAT); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_CLOSE_COMBAT); }
if (trials == 50)
{
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_CLOSE_COMBAT); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_CLOSE_COMBAT); }
}
} WHEN {
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EXPLOSION); EXPECT_SEND_OUT(opponent, 2); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_RANDOMIZE_SWITCHIN: AI will randomly choose between all valid switchin candidates if no good options are available")
{
u32 trials; // Two trial counts to ensure randomization is scalable
PARAMETRIZE { trials = 30; }
PARAMETRIZE { trials = 50; }
PASSES_RANDOMLY(10, trials, RNG_AI_RANDOM_VALID_SWITCHIN_POST_KO);
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_SMART_SWITCHING | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_RANDOMIZE_SWITCHIN);
PLAYER(SPECIES_ZIGZAGOON) { Moves(MOVE_PROTECT, MOVE_BITE); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_EXPLOSION); }
OPPONENT(SPECIES_GASTLY) { Moves(MOVE_LICK); }
OPPONENT(SPECIES_GASTLY) { Moves(MOVE_LICK); }
OPPONENT(SPECIES_GASTLY) { Moves(MOVE_LICK); }
if (trials == 50)
{
OPPONENT(SPECIES_GASTLY) { Moves(MOVE_LICK); }
OPPONENT(SPECIES_GASTLY) { Moves(MOVE_LICK); }
}
} WHEN {
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EXPLOSION); EXPECT_SEND_OUT(opponent, 2); }
}
}