Add AI_FLAG_ASSUME_STAB (#6797)

This commit is contained in:
Pawkkie 2025-07-10 13:58:31 -04:00 committed by GitHub
parent 0d09e5cce8
commit 419219bb31
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 142 additions and 13 deletions

View File

@ -144,6 +144,9 @@ Marks the last two Pokémon in the party as Ace Pokémon, with the same behaviou
## `AI_FLAG_OMNISCIENT`
AI has full knowledge of player moves, abilities, and hold items, and can use this knowledge when making decisions.
## `AI_FLAG_ASSUME_STAB`
A significantly more restricted version of `AI_FLAG_OMNISCIENT`, the AI only knows the player's STAB moves, as their existence would be reasonable to assume in almost any case.
## `AI_FLAG_SMART_MON_CHOICES`
Affects what the AI chooses to send out after a switch. AI will make smarter decisions when choosing which mon to send out mid-battle and after a KO, which are handled separately. Automatically included when `AI_FLAG_SMART_SWITCHING` is enabled.

View File

@ -60,6 +60,7 @@ u32 AI_GetDamage(u32 battlerAtk, u32 battlerDef, u32 moveIndex, enum DamageCalcC
bool32 IsAiVsAiBattle(void);
bool32 BattlerHasAi(u32 battlerId);
bool32 IsAiBattlerAware(u32 battlerId);
bool32 IsAiBattlerAssumingStab(void);
void ClearBattlerMoveHistory(u32 battlerId);
void RecordLastUsedMoveBy(u32 battlerId, u32 move);
void RecordAllMoves(u32 battler);

View File

@ -69,10 +69,15 @@
#define AI_CONSERVE_TERA_CHANCE_PER_MON 10 // Chance for AI with smart tera flag to decide not to tera before considering defensive benefit is this*(X-1), where X is the number of alive pokemon that could tera
#define AI_TERA_PREDICT_CHANCE 40 // Chance for AI with smart tera flag to tera in the situation where tera would save it from a KO, but could be punished by a KO from a different move.
// AI PP Stall detection chance per roll
#define PP_STALL_DISREGARD_MOVE_PERCENTAGE 50
// Score reduction if any roll for PP stall detection passes
#define PP_STALL_SCORE_REDUCTION 20
// AI_FLAG_PP_STALL_PREVENTION settings
#define PP_STALL_DISREGARD_MOVE_PERCENTAGE 50 // Detection chance per roll
#define PP_STALL_SCORE_REDUCTION 20 // Score reduction if any roll for PP stall detection passes
// AI_FLAG_ASSUME_STAB settings
#define ASSUME_STAB_SEES_ABILITY FALSE // Flag also gives omniscience for player's ability. Can use AI_FLAG_WEIGH_ABILITY_PREDICTION instead for smarter prediction without omniscience.
// AI_FLAG_SMART_SWITCHING settings
#define SMART_SWITCHING_OMNISCIENT FALSE // AI will use omniscience for switching calcs, regardless of omniscience setting otherwise
// AI's acceptable number of hits to KO the partner via friendly fire in a double battle.
#define FRIENDLY_FIRE_RISKY_THRESHOLD 2

View File

@ -33,8 +33,9 @@
#define AI_FLAG_PP_STALL_PREVENTION (1 << 25) // AI keeps track of the player's switches where the incoming mon is immune to the chosen move
#define AI_FLAG_PREDICT_MOVE (1 << 26) // AI will predict the player's move based on what move it would use in the same situation. Recommend using AI_FLAG_OMNISCIENT
#define AI_FLAG_SMART_TERA (1 << 27) // AI will make smarter decisions when choosing whether to terrastalize (default is to always tera whenever available).
#define AI_FLAG_ASSUME_STAB (1 << 28) // AI knows player's STAB moves, but nothing else. Restricted version of AI_FLAG_OMNISCIENT.
#define AI_FLAG_COUNT 28
#define AI_FLAG_COUNT 29
// Flags at and after 32 need different formatting, as in
// #define AI_FLAG_PLACEHOLDER ((u64)1 << 32)

View File

@ -881,5 +881,6 @@ u32 GetTeraTypeFromPersonality(struct Pokemon *mon);
struct Pokemon *GetSavedPlayerPartyMon(u32 index);
u8 *GetSavedPlayerPartyCount(void);
void SavePlayerPartyMon(u32 index, struct Pokemon *mon);
u32 IsSpeciesOfType(u32 species, u32 type);
#endif // GUARD_POKEMON_H

View File

@ -533,6 +533,17 @@ void Ai_UpdateFaintData(u32 battler)
aiMon->isFainted = TRUE;
}
void RecordMovesBasedOnStab(u32 battler)
{
u32 i;
for (i = 0; i < MAX_MON_MOVES; i++)
{
u32 playerMove = gBattleMons[battler].moves[i];
if (IsSpeciesOfType(gBattleMons[battler].species, GetMoveType(playerMove)) && GetMovePower(playerMove != 0))
RecordKnownMove(battler, playerMove);
}
}
void SetBattlerAiData(u32 battler, struct AiLogicData *aiData)
{
u32 ability, holdEffect;
@ -545,6 +556,9 @@ void SetBattlerAiData(u32 battler, struct AiLogicData *aiData)
aiData->hpPercents[battler] = GetHealthPercentage(battler);
aiData->moveLimitations[battler] = CheckMoveLimitations(battler, 0, MOVE_LIMITATIONS_ALL);
aiData->speedStats[battler] = GetBattlerTotalSpeedStatArgs(battler, ability, holdEffect);
if (IsAiBattlerAssumingStab())
RecordMovesBasedOnStab(battler);
}
#define BYPASSES_ACCURACY_CALC 101 // 101 indicates for ai that the move will always hit

View File

@ -192,6 +192,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler)
opposingPosition = BATTLE_OPPOSITE(GetBattlerPosition(battler));
opposingBattler = GetBattlerAtPosition(opposingPosition);
u16 *playerMoves = GetMovesArray(opposingBattler);
// Gets types of player (opposingBattler) and computer (battler)
atkType1 = gBattleMons[opposingBattler].types[0];
@ -255,7 +256,7 @@ static bool32 ShouldSwitchIfHasBadOdds(u32 battler)
// Get max damage mon could take
for (i = 0; i < MAX_MON_MOVES; i++)
{
playerMove = gBattleMons[opposingBattler].moves[i];
playerMove = SMART_SWITCHING_OMNISCIENT ? gBattleMons[opposingBattler].moves[i] : playerMoves[i];
if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH)
{
damageTaken = AI_GetDamage(opposingBattler, battler, i, AI_DEFENDING, gAiLogicData);
@ -1941,11 +1942,12 @@ static s32 GetMaxDamagePlayerCouldDealToSwitchin(u32 battler, u32 opposingBattle
{
int i = 0;
u32 playerMove;
u16 *playerMoves = GetMovesArray(opposingBattler);
s32 damageTaken = 0, maxDamageTaken = 0;
for (i = 0; i < MAX_MON_MOVES; i++)
{
playerMove = gBattleMons[opposingBattler].moves[i];
playerMove = SMART_SWITCHING_OMNISCIENT ? gBattleMons[opposingBattler].moves[i] : playerMoves[i];
if (playerMove != MOVE_NONE && !IsBattleMoveStatus(playerMove) && GetMoveEffect(playerMove) != EFFECT_FOCUS_PUNCH)
{
damageTaken = AI_CalcPartyMonDamage(playerMove, opposingBattler, battler, battleMon, AI_DEFENDING);

View File

@ -132,6 +132,15 @@ bool32 IsAiBattlerAware(u32 battlerId)
return BattlerHasAi(battlerId);
}
bool32 IsAiBattlerAssumingStab()
{
if (gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_ASSUME_STAB
|| gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_RIGHT] & AI_FLAG_ASSUME_STAB)
return TRUE;
return FALSE;
}
bool32 IsAiBattlerPredictingAbility(u32 battlerId)
{
if (gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_WEIGH_ABILITY_PREDICTION
@ -1423,8 +1432,8 @@ s32 AI_DecideKnownAbilityForTurn(u32 battlerId)
if (gDisableStructs[battlerId].overwrittenAbility)
return gDisableStructs[battlerId].overwrittenAbility;
// The AI knows its own ability.
if (IsAiBattlerAware(battlerId))
// The AI knows its own ability, and omniscience handling
if (IsAiBattlerAware(battlerId) || (IsAiBattlerAssumingStab() && ASSUME_STAB_SEES_ABILITY))
return knownAbility;
// Check neutralizing gas, gastro acid

View File

@ -7154,3 +7154,11 @@ void SavePlayerPartyMon(u32 index, struct Pokemon *mon)
{
gSaveBlock1Ptr->playerParty[index] = *mon;
}
u32 IsSpeciesOfType(u32 species, u32 type)
{
if (gSpeciesInfo[species].types[0] == type
|| gSpeciesInfo[species].types[1] == type)
return TRUE;
return FALSE;
}

View File

@ -0,0 +1,85 @@
#include "global.h"
#include "test/battle.h"
#include "battle_ai_util.h"
AI_SINGLE_BATTLE_TEST("AI_FLAG_ASSUME_STAB sees the player's STAB moves")
{
u32 aiFlag = 0;
PARAMETRIZE { aiFlag = AI_FLAG_ASSUME_STAB; }
PARAMETRIZE { aiFlag = AI_FLAG_OMNISCIENT; }
PARAMETRIZE { aiFlag = 0; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | aiFlag);
PLAYER(SPECIES_TYPHLOSION) { Speed(5); Moves(MOVE_TACKLE, MOVE_FLAMETHROWER); }
OPPONENT(SPECIES_ZIGZAGOON) { Speed(1); Moves(MOVE_TACKLE); Level(1); }
OPPONENT(SPECIES_SCIZOR) { Speed(4); Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_BLISSEY) { Speed(4); Moves(MOVE_TACKLE); }
} WHEN {
if (aiFlag == AI_FLAG_ASSUME_STAB || aiFlag == AI_FLAG_OMNISCIENT)
TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, MOVE_TACKLE); EXPECT_SEND_OUT(opponent, 2); }
else
TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, MOVE_TACKLE); EXPECT_SEND_OUT(opponent, 1); }
}
}
AI_SINGLE_BATTLE_TEST("AI_FLAG_ASSUME_STAB does not see the player's non-STAB moves")
{
u32 aiFlag = 0;
PARAMETRIZE { aiFlag = AI_FLAG_ASSUME_STAB; }
PARAMETRIZE { aiFlag = AI_FLAG_OMNISCIENT; }
PARAMETRIZE { aiFlag = 0; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | aiFlag);
PLAYER(SPECIES_GOREBYSS) { Speed(5); Moves(MOVE_TACKLE, MOVE_FLAMETHROWER); }
OPPONENT(SPECIES_ZIGZAGOON) { Speed(1); Moves(MOVE_TACKLE); Level(1); }
OPPONENT(SPECIES_SCIZOR) { Speed(4); Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_BLISSEY) { Speed(4); Moves(MOVE_TACKLE); }
} WHEN {
if (aiFlag == AI_FLAG_OMNISCIENT)
TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, MOVE_TACKLE); EXPECT_SEND_OUT(opponent, 2); }
else
TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, MOVE_TACKLE); EXPECT_SEND_OUT(opponent, 1); }
}
}
AI_DOUBLE_BATTLE_TEST("AI correctly records assumed STAB moves")
{
u32 aiFlag = 0;
// Not checking Omniscient here because it doesn't change the battle history
PARAMETRIZE { aiFlag = AI_FLAG_ASSUME_STAB; }
PARAMETRIZE { aiFlag = 0; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiFlag);
PLAYER(SPECIES_TYPHLOSION) { Moves(MOVE_TACKLE, MOVE_FLAMETHROWER, MOVE_SMOG); }
PLAYER(SPECIES_ZIGZAGOON) { Moves(MOVE_TACKLE, MOVE_HEADBUTT, MOVE_THUNDERBOLT); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_TACKLE); }
} WHEN {
TURN { MOVE(playerLeft, MOVE_TACKLE, target:opponentLeft); MOVE(playerRight, MOVE_THUNDERBOLT, target:opponentRight); }
} THEN {
if (aiFlag == AI_FLAG_ASSUME_STAB)
{
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][0], MOVE_TACKLE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][1], MOVE_FLAMETHROWER);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][2], MOVE_NONE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][3], MOVE_NONE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][0], MOVE_TACKLE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][1], MOVE_HEADBUTT);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][2], MOVE_THUNDERBOLT);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][3], MOVE_NONE);
}
else if (aiFlag == 0)
{
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][0], MOVE_TACKLE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][1], MOVE_NONE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][2], MOVE_NONE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][3], MOVE_NONE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][0], MOVE_NONE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][1], MOVE_NONE);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][2], MOVE_THUNDERBOLT);
EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][3], MOVE_NONE);
}
}
}

View File

@ -64,7 +64,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_RISKY: Mid-battle switches prioritize offensive o
PARAMETRIZE { aiRiskyFlag = AI_FLAG_RISKY; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | aiRiskyFlag);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_OMNISCIENT | aiRiskyFlag);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); }
OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); SpDefense(41); } // Mid battle, AI sends out Aron

View File

@ -85,7 +85,7 @@ AI_SINGLE_BATTLE_TEST("AI will switch out if it has no move that affects the pla
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 {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_OMNISCIENT);
PLAYER(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL, MOVE_CELEBRATE); }
OPPONENT(SPECIES_ABRA) { Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_ABRA) { Moves(MOVE_TACKLE); }
@ -314,7 +314,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize
AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize defensive options")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_OMNISCIENT);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); } // Mid battle, AI sends out Aron
@ -373,7 +373,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_MON_CHOICES: Mid-battle switches prioritize
GIVEN {
ASSUME(gItemsInfo[ITEM_EJECT_PACK].holdEffect == HOLD_EFFECT_EJECT_PACK);
ASSUME(MoveHasAdditionalEffectSelf(MOVE_OVERHEAT, MOVE_EFFECT_SP_ATK_MINUS_2) == TRUE);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES);
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | AI_FLAG_OMNISCIENT);
PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); SpAttack(50); }
OPPONENT(SPECIES_PONYTA) { Level(1); Item(ITEM_EJECT_PACK); Moves(MOVE_OVERHEAT); Speed(6); } // Forces switchout
OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_IRON_HEAD); Speed(4); SpDefense(50); }