From 419219bb3102a54b3db85b9787f5cc98e6e8a2e5 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Thu, 10 Jul 2025 13:58:31 -0400 Subject: [PATCH] Add AI_FLAG_ASSUME_STAB (#6797) --- docs/tutorials/ai_flags.md | 3 ++ include/battle_ai_util.h | 1 + include/config/ai.h | 13 +++-- include/constants/battle_ai.h | 3 +- include/pokemon.h | 1 + src/battle_ai_main.c | 14 ++++++ src/battle_ai_switch_items.c | 6 ++- src/battle_ai_util.c | 13 ++++- src/pokemon.c | 8 ++++ test/battle/ai/ai_assume_stab.c | 85 +++++++++++++++++++++++++++++++++ test/battle/ai/ai_flag_risky.c | 2 +- test/battle/ai/ai_switching.c | 6 +-- 12 files changed, 142 insertions(+), 13 deletions(-) create mode 100644 test/battle/ai/ai_assume_stab.c diff --git a/docs/tutorials/ai_flags.md b/docs/tutorials/ai_flags.md index af3d3c0718..7910049fbe 100644 --- a/docs/tutorials/ai_flags.md +++ b/docs/tutorials/ai_flags.md @@ -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. diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 991584a534..b0ad69f5d9 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -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); diff --git a/include/config/ai.h b/include/config/ai.h index c51ff6d790..57fc7ecb60 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -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 diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index 1f19d3e966..4fc13aaa1b 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -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) diff --git a/include/pokemon.h b/include/pokemon.h index f6c242606f..1a6bb8c696 100644 --- a/include/pokemon.h +++ b/include/pokemon.h @@ -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 diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index dae2895d96..6fee14ca15 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -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 diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index 393eb054bf..587b085500 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -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); diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 6d2fb0afce..f0defb570a 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -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 diff --git a/src/pokemon.c b/src/pokemon.c index 4a3b0bf706..91524ae97d 100644 --- a/src/pokemon.c +++ b/src/pokemon.c @@ -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; +} diff --git a/test/battle/ai/ai_assume_stab.c b/test/battle/ai/ai_assume_stab.c new file mode 100644 index 0000000000..6147229ae7 --- /dev/null +++ b/test/battle/ai/ai_assume_stab.c @@ -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); + } + } +} diff --git a/test/battle/ai/ai_flag_risky.c b/test/battle/ai/ai_flag_risky.c index 5a076f6fe1..5af51b9e8c 100644 --- a/test/battle/ai/ai_flag_risky.c +++ b/test/battle/ai/ai_flag_risky.c @@ -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 diff --git a/test/battle/ai/ai_switching.c b/test/battle/ai/ai_switching.c index 4dce36a219..8a41e9fdfe 100644 --- a/test/battle/ai/ai_switching.c +++ b/test/battle/ai/ai_switching.c @@ -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); }