Fix doubles moves bad / choice lock bad switch AI (#9078)
Some checks are pending
CI / build (push) Waiting to run
CI / docs_validate (push) Waiting to run
CI / allcontributors (push) Waiting to run

This commit is contained in:
Pawkkie 2026-01-31 14:43:34 -05:00 committed by GitHub
parent 109a3cd9e7
commit 3c08fac37b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 145 additions and 37 deletions

View File

@ -441,43 +441,66 @@ static u32 FindMonWithMoveOfEffectiveness(enum BattlerId battler, enum BattlerId
return FALSE; // There is not a single Pokémon in the party that has a move with this effectiveness threshold
}
static bool32 CanMoveAffectTarget(struct BattleContext *ctx, u32 moveIndex)
{
if (ctx->move != MOVE_NONE
&& gAiLogicData->effectiveness[ctx->battlerAtk][ctx->battlerDef][moveIndex] > UQ_4_12(0.0)
&& !AI_CanMoveBeBlockedByTarget(ctx))
return TRUE;
return FALSE;
}
static bool32 IsMoveBad(struct BattleContext *ctx, u32 moveIndex)
{
if (CanMoveAffectTarget(ctx, moveIndex))
return FALSE;
if (!ALL_MOVES_BAD_STATUS_MOVES_BAD || GetMovePower(ctx->move) != 0) // If using ALL_MOVES_BAD_STATUS_MOVES_BAD, then need power to be non-zero
return TRUE;
return FALSE;
}
static bool32 ShouldSwitchIfAllMovesBad(enum BattlerId battler)
{
u32 moveIndex;
enum BattlerId opposingBattler = GetOppositeBattler(battler);
enum Move aiMove;
struct BattleContext ctx = {0};
ctx.battlerAtk = battler;
ctx.battlerDef = opposingBattler;
ctx.abilityAtk = gAiLogicData->abilities[ctx.battlerAtk];
ctx.abilityDef = gAiLogicData->abilities[ctx.battlerDef];
ctx.holdEffectAtk = gAiLogicData->holdEffects[ctx.battlerAtk];
ctx.holdEffectDef = gAiLogicData->holdEffects[ctx.battlerDef];
// Switch if no moves affect opponents
if (IsDoubleBattle())
if (HasTwoOpponents(battler))
{
enum BattlerId opposingPartner = BATTLE_PARTNER(opposingBattler);
for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
for (u32 moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
{
aiMove = gBattleMons[battler].moves[moveIndex];
if (aiMove == MOVE_NONE)
continue;
if (gAiLogicData->effectiveness[battler][opposingBattler][moveIndex] > UQ_4_12(0.0)
|| gAiLogicData->effectiveness[battler][opposingPartner][moveIndex] > UQ_4_12(0.0))
ctx.move = ctx.chosenMove = gBattleMons[battler].moves[moveIndex];
ctx.moveType = GetBattleMoveType(ctx.move);
// Check if move is bad in the context of both opposing battlers
if (!IsMoveBad(&ctx, moveIndex))
{
return FALSE;
}
else
{
// Set partner data in ctx
ctx.battlerDef = opposingPartner;
ctx.abilityDef = gAiLogicData->abilities[ctx.battlerDef];
ctx.holdEffectDef = gAiLogicData->holdEffects[ctx.battlerDef];
if (!IsMoveBad(&ctx, moveIndex))
return FALSE;
}
}
}
else
{
struct BattleContext ctx = {0};
ctx.battlerAtk = battler;
ctx.battlerDef = opposingBattler;
ctx.abilityAtk = gAiLogicData->abilities[ctx.battlerAtk];
ctx.abilityDef = gAiLogicData->abilities[ctx.battlerDef];
ctx.holdEffectAtk = gAiLogicData->holdEffects[ctx.battlerAtk];
ctx.holdEffectDef = gAiLogicData->holdEffects[ctx.battlerDef];
for (moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
for (u32 moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
{
aiMove = gBattleMons[battler].moves[moveIndex];
if (aiMove != MOVE_NONE
&& gAiLogicData->effectiveness[battler][opposingBattler][moveIndex] > UQ_4_12(0.0)
&& !AI_CanMoveBeBlockedByTarget(&ctx)
&& (!ALL_MOVES_BAD_STATUS_MOVES_BAD || GetMovePower(aiMove) != 0)) // If using ALL_MOVES_BAD_STATUS_MOVES_BAD, then need power to be non-zero
ctx.move = ctx.chosenMove = gBattleMons[battler].moves[moveIndex];
ctx.moveType = GetBattleMoveType(ctx.move);
if (!IsMoveBad(&ctx, moveIndex))
return FALSE;
}
}
@ -975,7 +998,7 @@ static bool32 CanUseSuperEffectiveMoveAgainstOpponents(enum BattlerId battler)
if (CanUseSuperEffectiveMoveAgainstOpponent(battler, opposingBattler))
return TRUE;
if (IsDoubleBattle() && CanUseSuperEffectiveMoveAgainstOpponent(battler, BATTLE_PARTNER(BATTLE_OPPOSITE(battler))))
if (HasTwoOpponents(battler) && CanUseSuperEffectiveMoveAgainstOpponent(battler, BATTLE_PARTNER(BATTLE_OPPOSITE(battler))))
return TRUE;
return FALSE;
@ -1117,27 +1140,44 @@ static bool32 ShouldSwitchIfEncored(enum BattlerId battler)
static bool32 ShouldSwitchIfBadChoiceLock(enum BattlerId battler)
{
enum Move lastUsedMove = gAiLogicData->lastUsedMove[battler];
enum Move choicedMove = gBattleStruct->choicedMove[battler];
enum BattlerId opposingBattler = GetOppositeBattler(battler);
bool32 moveAffectsTarget = TRUE;
struct BattleContext ctx = {0};
ctx.battlerAtk = battler;
ctx.battlerDef = opposingBattler;
ctx.move = ctx.chosenMove = lastUsedMove;
ctx.moveType = GetBattleMoveType(lastUsedMove);
ctx.move = ctx.chosenMove = choicedMove;
ctx.moveType = GetBattleMoveType(choicedMove);
ctx.abilityAtk = gAiLogicData->abilities[ctx.battlerAtk];
ctx.abilityDef = gAiLogicData->abilities[ctx.battlerDef];
ctx.holdEffectAtk = gAiLogicData->holdEffects[ctx.battlerAtk];
ctx.holdEffectDef = gAiLogicData->holdEffects[ctx.battlerDef];
if (lastUsedMove != MOVE_NONE
&& (AI_GetMoveEffectiveness(lastUsedMove, battler, opposingBattler) == UQ_4_12(0.0) || AI_CanMoveBeBlockedByTarget(&ctx)))
moveAffectsTarget = FALSE;
// Not locked in to anything yet, or not choiced
if (choicedMove == MOVE_NONE)
return FALSE;
if (IsHoldEffectChoice(ctx.holdEffectAtk) && IsBattlerItemEnabled(battler))
u32 moveIndex = GetMoveIndex(battler, choicedMove);
if (HasTwoOpponents(battler))
{
if ((GetMoveCategory(lastUsedMove) == DAMAGE_CATEGORY_STATUS || !moveAffectsTarget) && RandomPercentage(RNG_AI_SWITCH_CHOICE_LOCKED, GetSwitchChance(SHOULD_SWITCH_CHOICE_LOCKED)))
enum BattlerId opposingPartner = BATTLE_PARTNER(opposingBattler);
if (IsHoldEffectChoice(ctx.holdEffectAtk) && IsBattlerItemEnabled(battler))
{
if (GetMoveCategory(choicedMove) == DAMAGE_CATEGORY_STATUS || !CanMoveAffectTarget(&ctx, moveIndex))
{
// Set partner data in ctx
ctx.battlerDef = opposingPartner;
ctx.abilityDef = gAiLogicData->abilities[ctx.battlerDef];
ctx.holdEffectDef = gAiLogicData->holdEffects[ctx.battlerDef];
if (!CanMoveAffectTarget(&ctx, moveIndex) && RandomPercentage(RNG_AI_SWITCH_CHOICE_LOCKED, GetSwitchChance(SHOULD_SWITCH_CHOICE_LOCKED)))
return SetSwitchinAndSwitch(battler, PARTY_SIZE);
}
}
}
else if (IsHoldEffectChoice(ctx.holdEffectAtk) && IsBattlerItemEnabled(battler))
{
if ((GetMoveCategory(choicedMove) == DAMAGE_CATEGORY_STATUS || !CanMoveAffectTarget(&ctx, moveIndex)) && RandomPercentage(RNG_AI_SWITCH_CHOICE_LOCKED, GetSwitchChance(SHOULD_SWITCH_CHOICE_LOCKED)))
return SetSwitchinAndSwitch(battler, PARTY_SIZE);
}
@ -1319,9 +1359,19 @@ bool32 ShouldSwitchIfAllScoresBad(enum BattlerId battler)
for (u32 moveIndex = 0; moveIndex < MAX_MON_MOVES; moveIndex++)
{
score = gAiBattleData->finalScore[battler][opposingBattler][moveIndex];
if (score > AI_BAD_SCORE_THRESHOLD)
return FALSE;
if (HasTwoOpponents(battler))
{
u32 score1 = gAiBattleData->finalScore[battler][opposingBattler][moveIndex];
u32 score2 = gAiBattleData->finalScore[battler][BATTLE_PARTNER(opposingBattler)][moveIndex];
if (score1 > AI_BAD_SCORE_THRESHOLD || score2 > AI_BAD_SCORE_THRESHOLD)
return FALSE;
}
else
{
score = gAiBattleData->finalScore[battler][opposingBattler][moveIndex];
if (score > AI_BAD_SCORE_THRESHOLD)
return FALSE;
}
}
if (RandomPercentage(RNG_AI_SWITCH_ALL_SCORES_BAD, GetSwitchChance(SHOULD_SWITCH_ALL_SCORES_BAD))
&& (gAiLogicData->mostSuitableMonId[battler] != PARTY_SIZE || !ALL_SCORES_BAD_NEEDS_GOOD_SWITCHIN))
@ -1347,7 +1397,8 @@ bool32 ShouldStayInToUseMove(enum BattlerId battler)
&& !GetHitEscapeTransformState(battler, aiMove))
continue;
if (gAiBattleData->finalScore[battler][opposingBattler][moveIndex] > AI_GOOD_SCORE_THRESHOLD)
if (gAiBattleData->finalScore[battler][opposingBattler][moveIndex] > AI_GOOD_SCORE_THRESHOLD
|| (HasTwoOpponents(battler) && gAiBattleData->finalScore[battler][BATTLE_PARTNER(opposingBattler)][moveIndex] > AI_GOOD_SCORE_THRESHOLD))
return TRUE;
}
}

View File

@ -256,3 +256,30 @@ AI_SINGLE_BATTLE_TEST("Choiced Pokémon will only see choiced moves when conside
TURN { MOVE(player, MOVE_WATER_GUN); EXPECT_SWITCH(opponent, 1); }
}
}
AI_DOUBLE_BATTLE_TEST("Choiced Pokémon won't switch out if they can still affect one opposing Pokémon in doubles")
{
u32 defendingSpecies = SPECIES_NONE;
enum Ability defendingAbility = ABILITY_NONE;
PARAMETRIZE { defendingSpecies = SPECIES_VAPOREON; defendingAbility = ABILITY_WATER_ABSORB; }
PARAMETRIZE { defendingSpecies = SPECIES_ZIGZAGOON; defendingAbility = SPECIES_ZIGZAGOON; }
PASSES_RANDOMLY(SHOULD_SWITCH_CHOICE_LOCKED_PERCENTAGE, 100, RNG_AI_SWITCH_CHOICE_LOCKED);
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY | AI_FLAG_RISKY | AI_FLAG_SMART_SWITCHING | AI_FLAG_OMNISCIENT | AI_FLAG_SMART_MON_CHOICES);
PLAYER(SPECIES_CHARMANDER) { Level(5); Moves(MOVE_CELEBRATE); }
PLAYER(SPECIES_VAPOREON) { Ability(ABILITY_WATER_ABSORB); Moves(MOVE_CELEBRATE); }
PLAYER(defendingSpecies) { Ability(defendingAbility); SpDefense(500); Moves(MOVE_CELEBRATE); }
OPPONENT(SPECIES_VAPOREON) { Moves(MOVE_SCALD); Item(ITEM_CHOICE_SPECS); }
OPPONENT(SPECIES_VAPOREON) { Moves(MOVE_SCALD); Item(ITEM_CHOICE_SPECS); }
OPPONENT(SPECIES_ZIGZAGOON);
OPPONENT(SPECIES_ZIGZAGOON);
} WHEN {
TURN { SWITCH(playerLeft, 2); MOVE(playerRight, MOVE_CELEBRATE); EXPECT_MOVE(opponentLeft, MOVE_SCALD, target:playerLeft); EXPECT_MOVE(opponentRight, MOVE_SCALD, target:playerLeft); }
if (defendingSpecies == SPECIES_VAPOREON)
TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_CELEBRATE); EXPECT_SWITCH(opponentLeft, 3); EXPECT_MOVE(opponentRight, MOVE_SCALD); }
else
TURN { MOVE(playerLeft, MOVE_CELEBRATE); MOVE(playerRight, MOVE_CELEBRATE); EXPECT_MOVE(opponentLeft, MOVE_SCALD, target:playerLeft); EXPECT_MOVE(opponentRight, MOVE_SCALD, target:playerLeft); SEND_OUT(playerLeft, 0); }
}
}

View File

@ -403,6 +403,36 @@ AI_SINGLE_BATTLE_TEST("AI will switch out if it has no move that affects the pla
}
}
AI_DOUBLE_BATTLE_TEST("AI will switch out if it has no moves that affect either of the player's battlers")
{
PASSES_RANDOMLY(SHOULD_SWITCH_ALL_MOVES_BAD_PERCENTAGE, 100, RNG_AI_SWITCH_ALL_MOVES_BAD);
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_RATTATA);
PLAYER(SPECIES_RATTATA);
OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_RATTATA) { Moves(MOVE_SCRATCH); }
OPPONENT(SPECIES_RATTATA) { Moves(MOVE_SCRATCH);}
} WHEN {
TURN { EXPECT_SWITCH(opponentLeft, 2); EXPECT_MOVE(opponentRight, MOVE_SCRATCH); }
}
}
AI_DOUBLE_BATTLE_TEST("AI will not switch out if it's moves can still affect one of the player's battlers")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_GASTLY);
PLAYER(SPECIES_RATTATA);
OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_RATTATA) { Moves(MOVE_SCRATCH); }
OPPONENT(SPECIES_RATTATA) { Moves(MOVE_SCRATCH);}
} WHEN {
TURN { EXPECT_MOVE(opponentLeft, MOVE_SHADOW_BALL); EXPECT_MOVE(opponentRight, MOVE_SCRATCH); }
}
}
AI_SINGLE_BATTLE_TEST("When AI switches out due to having no move that affects the player, AI will send in a mon that can hit the player, even if not ideal")
{
GIVEN {