Rework switch AI and add more tests for ace pokemon flags (#8321)

This commit is contained in:
FosterProgramming 2025-11-23 19:04:44 +01:00 committed by GitHub
parent c7c97531ec
commit a4482f0ee2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 142 additions and 119 deletions

View File

@ -38,7 +38,8 @@ enum ShouldSwitchScenario
enum SwitchType
{
SWITCH_AFTER_KO,
SWITCH_MID_BATTLE,
SWITCH_MID_BATTLE_FORCED,
SWITCH_MID_BATTLE_OPTIONAL,
};
void GetAIPartyIndexes(u32 battlerId, s32 *firstId, s32 *lastId);

View File

@ -359,13 +359,13 @@ void ComputeBattlerDecisions(u32 battler)
if (isAiBattler || CanAiPredictMove())
{
// Risky AI switches aggressively even mid battle
enum SwitchType switchType = (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_RISKY) ? SWITCH_AFTER_KO : SWITCH_MID_BATTLE;
enum SwitchType switchType = (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_RISKY) ? SWITCH_AFTER_KO : SWITCH_MID_BATTLE_OPTIONAL;
gAiLogicData->aiCalcInProgress = TRUE;
// Setup battler and prediction data
BattleAI_SetupAIData(0xF, battler);
SetupAIPredictionData(battler, switchType);
SetupAIPredictionData(battler, SWITCH_MID_BATTLE_OPTIONAL);
// AI's own switching data
if (isAiBattler)

View File

@ -1517,22 +1517,19 @@ static u32 GetBestMonDmg(struct Pokemon *party, int firstId, int lastId, u8 inva
return bestMonId;
}
static u32 GetFirstNonInvalidMon(u32 firstId, u32 lastId, u32 invalidMons, u32 battlerIn1, u32 battlerIn2)
static u32 GetFirstNonInvalidMon(u32 firstId, u32 lastId, u32 invalidMons)
{
if (!IsDoubleBattle())
return PARTY_SIZE;
if (PARTY_SIZE != gBattleStruct->monToSwitchIntoId[battlerIn1]
&& PARTY_SIZE != gBattleStruct->monToSwitchIntoId[battlerIn2])
return PARTY_SIZE;
for (u32 chosenMonId = (lastId-1); chosenMonId >= firstId; chosenMonId--)
u32 chosenMonId = PARTY_SIZE;
for (u32 i = (lastId-1); i > firstId; i--)
{
if ((1 << (chosenMonId)) & invalidMons)
continue;
return chosenMonId; // first non invalid mon found
if (!((1 << i) & invalidMons))
{
// first non invalid mon found
chosenMonId = i;
break;
}
}
return PARTY_SIZE;
return chosenMonId;
}
bool32 IsMonGrounded(enum HoldEffect heldItemEffect, enum Ability ability, enum Type type1, enum Type type2)
@ -2125,7 +2122,7 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
int batonPassId = PARTY_SIZE, typeMatchupId = PARTY_SIZE, typeMatchupEffectiveId = PARTY_SIZE, defensiveMonId = PARTY_SIZE, aceMonId = PARTY_SIZE, trapperId = PARTY_SIZE;
int i, j, aliveCount = 0, bits = 0, aceMonCount = 0;
s32 defensiveMonHitKOThreshold = 3; // 3HKO threshold that candidate defensive mons must exceed
s32 playerMonHP = gBattleMons[opposingBattler].hp, maxDamageDealt = 0, damageDealt = 0;
s32 playerMonHP = gBattleMons[opposingBattler].hp, maxDamageDealt = 0, damageDealt = 0, monMaxDamage = 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;
@ -2167,6 +2164,8 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
hitsToKOAIPriority = GetSwitchinHitsToKO(GetMaxPriorityDamagePlayerCouldDealToSwitchin(battler, opposingBattler, gAiLogicData->switchinCandidate.battleMon, &bestPlayerPriorityMove), battler);
typeMatchup = GetBattleMonTypeMatchup(gBattleMons[opposingBattler], gAiLogicData->switchinCandidate.battleMon);
monMaxDamage = 0;
// Check through current mon's moves
for (j = 0; j < MAX_MON_MOVES; j++)
{
@ -2232,6 +2231,8 @@ 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)
{
@ -2275,6 +2276,8 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
trapperId = i;
}
}
if (monMaxDamage == 0)
invalidMons |= 1u << i;
}
batonPassId = GetRandomSwitchinWithBatonPass(aliveCount, bits, firstId, lastId, i);
@ -2304,16 +2307,19 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId,
else if (batonPassId != PARTY_SIZE) return batonPassId;
else if (generic1v1MonId != PARTY_SIZE) return generic1v1MonId;
}
// 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
&& (IsSwitchOutEffect(GetMoveEffect(gCurrentMove)) || gAiLogicData->ejectButtonSwitch || gAiLogicData->ejectPackSwitch))
return aceMonId;
if (switchType == SWITCH_MID_BATTLE_OPTIONAL)
return PARTY_SIZE;
// Fallback
u32 bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons, battlerIn1, battlerIn2);
u32 bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons);
if (bestMonId != PARTY_SIZE)
return bestMonId;
// 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)
return aceMonId;
return PARTY_SIZE;
}
@ -2429,16 +2435,17 @@ u32 GetMostSuitableMonToSwitchInto(u32 battler, enum SwitchType switchType)
if (bestMonId != PARTY_SIZE)
return bestMonId;
// 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
&& (IsSwitchOutEffect(GetMoveEffect(gCurrentMove)) || gAiLogicData->ejectButtonSwitch || gAiLogicData->ejectPackSwitch))
return aceMonId;
if (switchType == SWITCH_MID_BATTLE_OPTIONAL)
return PARTY_SIZE;
// Fallback
bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons, battlerIn1, battlerIn2);
bestMonId = GetFirstNonInvalidMon(firstId, lastId, invalidMons);
if (bestMonId != PARTY_SIZE)
return bestMonId;
if (aceMonId != PARTY_SIZE && CountUsablePartyMons(battler) <= aceMonCount)
return aceMonId;
return PARTY_SIZE;
}
}

View File

@ -4567,7 +4567,9 @@ s32 CountUsablePartyMons(u32 battlerId)
}
ret = 0;
for (i = 0; i < PARTY_SIZE; i++)
s32 firstId, lastId;
GetAIPartyIndexes(battlerId, &firstId, &lastId);
for (i = firstId; i < lastId; i++)
{
if (i != battlerOnField1 && i != battlerOnField2
&& GetMonData(&party[i], MON_DATA_HP) != 0

View File

@ -51,8 +51,6 @@ static void OpponentHandleChoosePokemon(u32 battler);
static void OpponentHandleIntroTrainerBallThrow(u32 battler);
static void OpponentHandleDrawPartyStatusSummary(u32 battler);
static void OpponentHandleEndLinkBattle(u32 battler);
static u8 CountAIAliveNonEggMonsExcept(u8 slotToIgnore);
static void OpponentBufferRunCommand(u32 battler);
static void (*const sOpponentBufferCommands[CONTROLLER_CMDS_COUNT])(u32 battler) =
@ -527,70 +525,9 @@ static void OpponentHandleChooseItem(u32 battler)
BtlController_Complete(battler);
}
static inline bool32 IsAcePokemon(u32 chosenMonId, u32 pokemonInBattle, u32 battler)
{
return gAiThinkingStruct->aiFlags[battler] & AI_FLAG_ACE_POKEMON
&& (chosenMonId == CalculateEnemyPartyCountInSide(battler) - 1)
&& CountAIAliveNonEggMonsExcept(PARTY_SIZE) != pokemonInBattle;
}
static inline bool32 IsDoubleAceSlot(u32 battler, u32 partyId)
{
u32 partyCountEnd;
if (!(gAiThinkingStruct->aiFlags[battler] & AI_FLAG_DOUBLE_ACE_POKEMON))
return FALSE;
partyCountEnd = CalculateEnemyPartyCountInSide(battler);
if (partyCountEnd == 0)
return FALSE;
if (partyId == partyCountEnd - 1)
return TRUE;
if (partyCountEnd > 1 && partyId == partyCountEnd - 2)
return TRUE;
return FALSE;
}
static inline bool32 IsDoubleAcePokemon(u32 chosenMonId, u32 pokemonInBattle, u32 battler)
{
s32 battler1, battler2, firstId, lastId;
s32 i;
if (!IsDoubleAceSlot(battler, chosenMonId))
return FALSE;
if (!IsDoubleBattle())
{
battler2 = battler1 = GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT);
}
else
{
battler1 = GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT);
battler2 = GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT);
}
GetAIPartyIndexes(battler, &firstId, &lastId);
for (i = firstId; i < lastId; i++)
{
if (!IsValidForBattle(&gEnemyParty[i])
|| i == gBattlerPartyIndexes[battler1]
|| i == gBattlerPartyIndexes[battler2]
|| i == chosenMonId)
continue;
if (!IsAcePokemon(i, pokemonInBattle, battler) && !IsDoubleAceSlot(battler, i))
return TRUE;
}
return FALSE;
}
static void OpponentHandleChoosePokemon(u32 battler)
{
s32 chosenMonId;
s32 pokemonInBattle = 1;
enum SwitchType switchType = SWITCH_AFTER_KO;
// Choosing Revival Blessing target
@ -602,7 +539,7 @@ static void OpponentHandleChoosePokemon(u32 battler)
else if (gBattleStruct->AI_monToSwitchIntoId[battler] == PARTY_SIZE)
{
if (IsSwitchOutEffect(GetMoveEffect(gCurrentMove)) || gAiLogicData->ejectButtonSwitch || gAiLogicData->ejectPackSwitch)
switchType = SWITCH_MID_BATTLE;
switchType = SWITCH_MID_BATTLE_FORCED;
// reset the AI data to consider the correct on-field state at time of switch
SetBattlerAiData(GetBattlerAtPosition(B_POSITION_PLAYER_LEFT), gAiLogicData);
@ -610,7 +547,7 @@ static void OpponentHandleChoosePokemon(u32 battler)
SetBattlerAiData(GetBattlerAtPosition(B_POSITION_PLAYER_RIGHT), gAiLogicData);
chosenMonId = GetMostSuitableMonToSwitchInto(battler, switchType);
if (chosenMonId == PARTY_SIZE)
if (chosenMonId == PARTY_SIZE) // Advanced logic failed so we pick the next available battler
{
s32 battler1, battler2, firstId, lastId;
@ -622,19 +559,14 @@ static void OpponentHandleChoosePokemon(u32 battler)
{
battler1 = GetBattlerAtPosition(B_POSITION_OPPONENT_LEFT);
battler2 = GetBattlerAtPosition(B_POSITION_OPPONENT_RIGHT);
pokemonInBattle = 2;
}
GetAIPartyIndexes(battler, &firstId, &lastId);
for (chosenMonId = (lastId-1); chosenMonId >= firstId; chosenMonId--)
for (chosenMonId = firstId; chosenMonId < lastId; chosenMonId++)
{
if (!IsValidForBattle(&gEnemyParty[chosenMonId])
|| chosenMonId == gBattlerPartyIndexes[battler1]
|| chosenMonId == gBattlerPartyIndexes[battler2])
continue;
if (!IsAcePokemon(chosenMonId, pokemonInBattle, battler)
&& !IsDoubleAcePokemon(chosenMonId, pokemonInBattle, battler))
if (IsValidForBattle(&gEnemyParty[chosenMonId])
&& chosenMonId != gBattlerPartyIndexes[battler1]
&& chosenMonId != gBattlerPartyIndexes[battler2])
break;
}
}
@ -653,22 +585,6 @@ static void OpponentHandleChoosePokemon(u32 battler)
BtlController_Complete(battler);
}
static u8 CountAIAliveNonEggMonsExcept(u8 slotToIgnore)
{
u16 i, count;
for (i = 0, count = 0; i < PARTY_SIZE; i++)
{
if (i != slotToIgnore
&& IsValidForBattle(&gEnemyParty[i]))
{
count++;
}
}
return count;
}
static void OpponentHandleIntroTrainerBallThrow(u32 battler)
{
BtlController_HandleIntroTrainerBallThrow(battler, 0, NULL, 0, Intro_TryShinyAnimShowHealthbox);

View File

@ -125,3 +125,100 @@ AI_TWO_VS_ONE_BATTLE_TEST("Battler 3 has Battler 1 AI flags set correctly (2v1)"
TURN { EXPECT_MOVE(opponentLeft, MOVE_EXPLOSION, target: playerLeft); EXPECT_MOVE(opponentRight, MOVE_EXPLOSION); }
}
}
AI_MULTI_BATTLE_TEST("Partner will not steal your pokemon when running out")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PARTNER(SPECIES_WYNAUT) { Moves(MOVE_MEMENTO); }
MULTI_OPPONENT_A(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE); }
MULTI_OPPONENT_B(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE); }
} WHEN {
TURN {EXPECT_MOVE(playerRight, MOVE_MEMENTO, target:opponentLeft);}
TURN {}
} THEN {
EXPECT_EQ(gAbsentBattlerFlags, (1u << GetBattlerAtPosition(B_POSITION_PLAYER_RIGHT)));
}
}
AI_MULTI_BATTLE_TEST("Partner will not steal your pokemon to delay using their ace")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
BATTLER_AI_FLAGS(B_POSITION_PLAYER_RIGHT, AI_FLAG_ACE_POKEMON);
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PARTNER(SPECIES_WYNAUT) { Moves(MOVE_MEMENTO); }
MULTI_PARTNER(SPECIES_METAGROSS) { Moves(MOVE_CELEBRATE); }
MULTI_OPPONENT_A(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE); }
MULTI_OPPONENT_B(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE); }
} WHEN {
TURN {EXPECT_MOVE(playerRight, MOVE_MEMENTO, target:opponentLeft);}
TURN {}
} THEN {
EXPECT_EQ(SPECIES_METAGROSS, playerRight->species);
}
}
AI_MULTI_BATTLE_TEST("AI opponents do not steal their partner pokemon in multi battle to delay using their ace")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
BATTLER_AI_FLAGS(B_POSITION_OPPONENT_LEFT, AI_FLAG_ACE_POKEMON);
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PARTNER(SPECIES_WOBBUFFET) { }
MULTI_OPPONENT_A(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE); HP(1);}
MULTI_OPPONENT_A(SPECIES_VENUSAUR) { Moves(MOVE_GIGA_DRAIN); }
MULTI_OPPONENT_B(SPECIES_WYNAUT) { Moves(MOVE_CELEBRATE); }
} WHEN {
TURN {MOVE(playerLeft, MOVE_TACKLE, target: opponentLeft); }
TURN {MOVE(playerLeft, MOVE_TACKLE, target: opponentLeft); }
} THEN {
EXPECT_EQ(SPECIES_VENUSAUR, opponentLeft->species);
}
}
AI_MULTI_BATTLE_TEST("AI opponents do not steal their partner pokemon in multi battle when forced out")
{
u32 item, move;
PARAMETRIZE {item = ITEM_EJECT_BUTTON; move = MOVE_TACKLE;}
PARAMETRIZE {item = ITEM_EJECT_PACK; move = MOVE_TAIL_WHIP;}
PARAMETRIZE {item = ITEM_NONE; move = MOVE_ROAR;}
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
BATTLER_AI_FLAGS(B_POSITION_OPPONENT_LEFT, AI_FLAG_ACE_POKEMON);
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PARTNER(SPECIES_WOBBUFFET) { }
MULTI_OPPONENT_A(SPECIES_WOBBUFFET) { Moves(MOVE_CELEBRATE); Item(item);}
MULTI_OPPONENT_A(SPECIES_VENUSAUR) { Moves(MOVE_GIGA_DRAIN); }
MULTI_OPPONENT_B(SPECIES_WYNAUT) { Moves(MOVE_CELEBRATE); }
} WHEN {
TURN {MOVE(playerLeft, move, target: opponentLeft); }
} THEN {
EXPECT_EQ(SPECIES_VENUSAUR, opponentLeft->species);
}
}
AI_MULTI_BATTLE_TEST("AI opponents do not steal their partner pokemon in multi battle when forced out 2")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
BATTLER_AI_FLAGS(B_POSITION_OPPONENT_LEFT, AI_FLAG_ACE_POKEMON);
MULTI_PLAYER(SPECIES_WOBBUFFET) { }
MULTI_PARTNER(SPECIES_WOBBUFFET) { }
MULTI_OPPONENT_A(SPECIES_GOLISOPOD) { Moves(MOVE_CELEBRATE); HP(101); MaxHP(200); Ability(ABILITY_EMERGENCY_EXIT);}
MULTI_OPPONENT_A(SPECIES_VENUSAUR) { Moves(MOVE_GIGA_DRAIN); }
MULTI_OPPONENT_B(SPECIES_WYNAUT) { Moves(MOVE_CELEBRATE); }
} WHEN {
TURN {MOVE(playerLeft, MOVE_TACKLE, target: opponentLeft); }
} THEN {
EXPECT_EQ(SPECIES_VENUSAUR, opponentLeft->species);
}
}

View File

@ -359,7 +359,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, 4); }
TURN { MOVE(player, MOVE_SHADOW_BALL); EXPECT_SWITCH(opponent, 2); EXPECT_SEND_OUT(opponent, 0);}
TURN { MOVE(player, MOVE_SHADOW_BALL); EXPECT_MOVE(opponent, MOVE_TACKLE); }
}
}