From a4482f0ee237ea32b0bdd0db088393b09a608fc6 Mon Sep 17 00:00:00 2001 From: FosterProgramming Date: Sun, 23 Nov 2025 19:04:44 +0100 Subject: [PATCH] Rework switch AI and add more tests for ace pokemon flags (#8321) --- include/battle_ai_switch_items.h | 3 +- src/battle_ai_main.c | 4 +- src/battle_ai_switch_items.c | 55 ++++++++++-------- src/battle_ai_util.c | 4 +- src/battle_controller_opponent.c | 96 ++----------------------------- test/battle/ai/ai_multi.c | 97 ++++++++++++++++++++++++++++++++ test/battle/ai/ai_switching.c | 2 +- 7 files changed, 142 insertions(+), 119 deletions(-) diff --git a/include/battle_ai_switch_items.h b/include/battle_ai_switch_items.h index 35085563a4..7612d2084a 100644 --- a/include/battle_ai_switch_items.h +++ b/include/battle_ai_switch_items.h @@ -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); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 9612d544f6..5db2732d4d 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -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) diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 1a393fc66f..7d150d1a3b 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -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; } } diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 8ab0c83a92..57d46b4810 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -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 diff --git a/src/battle_controller_opponent.c b/src/battle_controller_opponent.c index b03dcd958d..e7477f9542 100644 --- a/src/battle_controller_opponent.c +++ b/src/battle_controller_opponent.c @@ -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); diff --git a/test/battle/ai/ai_multi.c b/test/battle/ai/ai_multi.c index ddbfdb49b0..b46573b4ca 100644 --- a/test/battle/ai/ai_multi.c +++ b/test/battle/ai/ai_multi.c @@ -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); + } +} diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index f3c8cb6103..82311202c6 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -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); } } }