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
This commit is contained in:
Pawkkie 2024-07-01 02:53:07 -04:00 committed by GitHub
parent c3634db0ea
commit 7e167f0b3c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 167 additions and 6 deletions

View File

@ -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

View File

@ -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

View File

@ -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.

View File

@ -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

View File

@ -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); }
}
}
}