From 7e167f0b3c9e23eba98f294622c5fc7967072820 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Mon, 1 Jul 2024 02:53:07 -0400 Subject: [PATCH] Add AI_FLAG_SEQUENCE_SWITCHING (#4878) * AI_FLAG_BAD_SWITCHING * Cleanup and tests, U-Turn / Baton Pass / Parting Shot aren't working properly * Fix U-Turn etc. tests * Test overhaul with Egg's suggestions * Add EFFECT_HIT_SWITCH_TARGET to tests * Review feedback * Re-review feedback --- include/battle_ai_switch_items.h | 2 +- include/constants/battle_ai.h | 1 + src/battle_ai_main.c | 3 +- src/battle_ai_switch_items.c | 38 ++++++- test/battle/ai_flag_sequence_switching.c | 129 +++++++++++++++++++++++ 5 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 test/battle/ai_flag_sequence_switching.c diff --git a/include/battle_ai_switch_items.h b/include/battle_ai_switch_items.h index 16f6468757..b52e792612 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, bool32 switchAfterMonKOd); +u32 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd); bool32 ShouldSwitch(u32 battler, bool32 emitResult); #endif // GUARD_BATTLE_AI_SWITCH_ITEMS_H diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index db3051b257..9391732060 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -47,6 +47,7 @@ #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. Automatically included by AI_FLAG_SMART_SWITCHING. #define AI_FLAG_CONSERVATIVE (1 << 19) // AI assumes all moves will low roll damage +#define AI_FLAG_SEQUENCE_SWITCHING (1 << 20) // AI switches in mons in exactly party order, and never switches mid-battle #define AI_FLAG_COUNT 20 diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 53a62a08bd..52278b0b73 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -511,7 +511,8 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle) if (CountUsablePartyMons(battler) > 0 && !IsBattlerTrapped(battler, TRUE) && !(gBattleTypeFlags & (BATTLE_TYPE_ARENA | BATTLE_TYPE_PALACE)) - && AI_THINKING_STRUCT->aiFlags[battler] & (AI_FLAG_CHECK_VIABILITY | AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_PREFER_BATON_PASS)) + && AI_THINKING_STRUCT->aiFlags[battler] & (AI_FLAG_CHECK_VIABILITY | AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_PREFER_BATON_PASS) + && !(AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING)) { // Consider switching if all moves are worthless to use. if (GetTotalBaseStat(gBattleMons[battler].species) >= 310 // Mon is not weak. diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 1300edbd46..48a77a988a 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -1020,6 +1020,10 @@ bool32 ShouldSwitch(u32 battler, bool32 emitResult) if (gBattleTypeFlags & BATTLE_TYPE_ARENA) return FALSE; + // Sequence Switching AI never switches mid-battle + if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) + return FALSE; + availableToSwitch = 0; if (gBattleTypeFlags & BATTLE_TYPE_DOUBLE) @@ -1787,7 +1791,7 @@ static bool32 CanAbilityTrapOpponent(u16 ability, u32 opponent) // 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, bool32 isSwitchAfterKO) +static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, u32 battler, u32 opposingBattler, u32 battlerIn1, u32 battlerIn2, bool32 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, trapperId = PARTY_SIZE; @@ -1993,17 +1997,37 @@ static u32 GetBestMonIntegrated(struct Pokemon *party, int firstId, int lastId, // If ace mon is the last available Pokemon and U-Turn/Volt Switch was used - switch to the mon. else if (aceMonId != PARTY_SIZE - && (gMovesInfo[gLastUsedMove].effect == EFFECT_HIT_ESCAPE || gMovesInfo[gLastUsedMove].effect == EFFECT_PARTING_SHOT)) + && (gMovesInfo[gLastUsedMove].effect == EFFECT_HIT_ESCAPE || gMovesInfo[gLastUsedMove].effect == EFFECT_PARTING_SHOT || gMovesInfo[gLastUsedMove].effect == EFFECT_BATON_PASS)) return aceMonId; } return PARTY_SIZE; } -u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd) +static u32 GetNextMonInParty(struct Pokemon *party, int firstId, int lastId, u32 battlerIn1, u32 battlerIn2) +{ + u32 i; + // 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; + } + return i; + } + return PARTY_SIZE; +} + +u32 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd) { u32 opposingBattler = 0; u32 bestMonId = PARTY_SIZE; - u8 battlerIn1 = 0, battlerIn2 = 0; + u32 battlerIn1 = 0, battlerIn2 = 0; s32 firstId = 0; s32 lastId = 0; // + 1 struct Pokemon *party; @@ -2039,6 +2063,12 @@ u8 GetMostSuitableMonToSwitchInto(u32 battler, bool32 switchAfterMonKOd) else party = gEnemyParty; + if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_SEQUENCE_SWITCHING) + { + bestMonId = GetNextMonInParty(party, firstId, lastId, battlerIn1, battlerIn2); + return bestMonId; + } + // 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[battler] & AI_FLAG_SMART_MON_CHOICES && !(gBattleTypeFlags & BATTLE_TYPE_DOUBLE)) // Double Battles aren't included in AI_FLAG_SMART_MON_CHOICE. Defaults to regular switch in logic diff --git a/test/battle/ai_flag_sequence_switching.c b/test/battle/ai_flag_sequence_switching.c new file mode 100644 index 0000000000..1b4a264a24 --- /dev/null +++ b/test/battle/ai_flag_sequence_switching.c @@ -0,0 +1,129 @@ +#include "global.h" +#include "test/battle.h" + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SEQUENCE_SWITCHING: AI will always switch after a KO in exactly party order") +{ + u32 aiSequenceSwitchingFlag = 0; + + PARAMETRIZE { aiSequenceSwitchingFlag = 0; } + PARAMETRIZE { aiSequenceSwitchingFlag = AI_FLAG_SEQUENCE_SWITCHING; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSequenceSwitchingFlag); + PLAYER(SPECIES_SWELLOW) { Level (50); } + OPPONENT(SPECIES_MACHOP) { Level(5); } + OPPONENT(SPECIES_MACHOKE) { Level(5); } + OPPONENT(SPECIES_MACHAMP) { Level(5); } + OPPONENT(SPECIES_MANKEY) { Level(5); } + OPPONENT(SPECIES_PRIMEAPE) { Level(5); } + OPPONENT(SPECIES_MAGNEZONE) { Level(100); } + } WHEN { + TURN { MOVE(player, MOVE_WING_ATTACK); } + if (aiSequenceSwitchingFlag) { + TURN { MOVE(player, MOVE_WING_ATTACK); } + TURN { MOVE(player, MOVE_WING_ATTACK); } + TURN { MOVE(player, MOVE_WING_ATTACK); } + TURN { MOVE(player, MOVE_WING_ATTACK); } + } + } SCENE { + if (aiSequenceSwitchingFlag) { + MESSAGE("{PKMN} TRAINER LEAF sent out Machoke!"); + MESSAGE("{PKMN} TRAINER LEAF sent out Machamp!"); + MESSAGE("{PKMN} TRAINER LEAF sent out Mankey!"); + MESSAGE("{PKMN} TRAINER LEAF sent out Primeape!"); + MESSAGE("{PKMN} TRAINER LEAF sent out Magnezone!"); + } + else { + MESSAGE("{PKMN} TRAINER LEAF sent out Magnezone!"); + } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SEQUENCE_SWITCHING: Roar and Dragon Tail still force switch to random party member") +{ + u32 move; + + PARAMETRIZE { move = MOVE_ROAR; } + PARAMETRIZE {move = MOVE_DRAGON_TAIL; } + + PASSES_RANDOMLY(1, 2, RNG_FORCE_RANDOM_SWITCH); + GIVEN { + ASSUME(gMovesInfo[MOVE_ROAR].effect == EFFECT_ROAR); + ASSUME(gMovesInfo[MOVE_DRAGON_TAIL].effect == EFFECT_HIT_SWITCH_TARGET); + AI_FLAGS(AI_FLAG_SEQUENCE_SWITCHING); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_BULBASAUR); + OPPONENT(SPECIES_CHARMANDER); + OPPONENT(SPECIES_SQUIRTLE) { HP(0); } + } WHEN { + TURN { MOVE(player, move); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, move, player); + MESSAGE("Foe Bulbasaur was dragged out!"); + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SEQUENCE_SWITCHING: AI will always switch into lowest party index after U-Turn, Parting Shot, and Baton Pass") +{ + u32 j, aiSequenceSwitchingFlag = 0, move = MOVE_NONE; + + static const u32 switchMoves[] = { + MOVE_U_TURN, + MOVE_PARTING_SHOT, + MOVE_BATON_PASS, + }; + + for (j = 0; j < ARRAY_COUNT(switchMoves); j++) + { + PARAMETRIZE { aiSequenceSwitchingFlag = 0; move = switchMoves[j]; } + PARAMETRIZE { aiSequenceSwitchingFlag = AI_FLAG_SEQUENCE_SWITCHING; move = switchMoves[j]; } + } + + GIVEN { + ASSUME(gMovesInfo[MOVE_U_TURN].effect == EFFECT_HIT_ESCAPE); + ASSUME(gMovesInfo[MOVE_PARTING_SHOT].effect == EFFECT_PARTING_SHOT); + ASSUME(gMovesInfo[MOVE_BATON_PASS].effect == EFFECT_BATON_PASS); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSequenceSwitchingFlag); + PLAYER(SPECIES_SWELLOW) { Level (50); } + OPPONENT(SPECIES_MACHOP) { Level(1); Moves(move); } + OPPONENT(SPECIES_MACHOP) { Level(2); Moves(move); } + OPPONENT(SPECIES_MACHOP) { Level(3); Moves(move); } + OPPONENT(SPECIES_MACHOP) { Level(4); Moves(move); } + OPPONENT(SPECIES_MACHOP) { Level(5); Moves(move); } + OPPONENT(SPECIES_MAGNEZONE) { Level(100); Moves(move); } + } WHEN { + if (aiSequenceSwitchingFlag) { + TURN { EXPECT_MOVE(opponent, move) ; EXPECT_SEND_OUT(opponent, 1); } + TURN { EXPECT_MOVE(opponent, move) ; EXPECT_SEND_OUT(opponent, 0); } + TURN { EXPECT_MOVE(opponent, move) ; EXPECT_SEND_OUT(opponent, 1); } + TURN { EXPECT_MOVE(opponent, move) ; EXPECT_SEND_OUT(opponent, 0); } + TURN { EXPECT_MOVE(opponent, move) ; EXPECT_SEND_OUT(opponent, 1); } + } + else { + TURN { EXPECT_MOVE(opponent, move) ; EXPECT_SEND_OUT(opponent, 5); } + } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_SEQUENCE_SWITCHING: AI will not switch mid-battle") +{ + u32 aiSequenceSwitchingFlag = 0; + + PARAMETRIZE { aiSequenceSwitchingFlag = 0; } + PARAMETRIZE { aiSequenceSwitchingFlag = AI_FLAG_SEQUENCE_SWITCHING; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiSequenceSwitchingFlag); + PLAYER(SPECIES_ZIGZAGOON) { Moves(MOVE_LICK); } + OPPONENT(SPECIES_GASTLY) { Moves(MOVE_SHADOW_BALL); } + OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_KARATE_CHOP); } + } WHEN { + if (aiSequenceSwitchingFlag == AI_FLAG_SEQUENCE_SWITCHING) { + TURN { MOVE(player, MOVE_LICK) ; EXPECT_MOVE(opponent, MOVE_SHADOW_BALL); } + } + else { + TURN { MOVE(player, MOVE_LICK) ; EXPECT_SWITCH(opponent, 1); } + } + } +}