diff --git a/docs/tutorials/ai_flags.md b/docs/tutorials/ai_flags.md index 7910049fbe..98657210d9 100644 --- a/docs/tutorials/ai_flags.md +++ b/docs/tutorials/ai_flags.md @@ -147,6 +147,13 @@ AI has full knowledge of player moves, abilities, and hold items, and can use th ## `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_ASSUME_STATUS_MOVES` +A more restricted version of `AI_FLAG_OMNISCIENT`. The AI has a _chance_ to know what status moves the player has, plus additionally Fake Out and fixed percentage moves like Super Fang. The intention is so that if the AI has a counterplay implemented, it will seem to have guessed if the player's pokemon has a move, without giving the AI perfect information. For example, with Omniscient set, the AI will not usually put a pokemon to sleep if it has Sleep Talk; with neither Assume Powerful Status nor Omniscient set, the AI will always assume the pokemon does not have Sleep Talk. + +By default, there are three groups of higher likelihood status moves defined in `include/config/ai.h` under `ASSUME_STATUS_HIGH_ODDS`, `ASSUME_STATUS_MEDIUM_ODDS`, and `ASSUME_STATUS_LOW_ODDS`. Moves are sorted in `src/battle_ai_util.c` within `ShouldRecordStatusMove()`. + +Any move that is not special cased is then potentially caught by `ASSUME_ALL_STATUS_ODDS`. + ## `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 5be5916018..f4bd20d3d8 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -70,6 +70,8 @@ bool32 IsAiVsAiBattle(void); bool32 BattlerHasAi(u32 battlerId); bool32 IsAiBattlerAware(u32 battlerId); bool32 IsAiBattlerAssumingStab(void); +bool32 IsAiBattlerAssumingStatusMoves(void); +bool32 ShouldRecordStatusMove(u32 move); 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 57fc7ecb60..44d2f3236d 100644 --- a/include/config/ai.h +++ b/include/config/ai.h @@ -76,6 +76,14 @@ // 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_ASSUME_STATUS_MOVES settings +#define ASSUME_STATUS_MOVES_HAS_TUNING TRUE // Flag has varying rates for different kinds of status move. + // Setting to false also means it will not alert on Fake Out or Super Fang. +#define ASSUME_STATUS_HIGH_ODDS 90 // Chance for AI to see extremely likely moves for a pokemon to have, like Spore +#define ASSUME_STATUS_MEDIUM_ODDS 70 // Chance for AI to see moderately likely moves for a pokemon to have, like Protect +#define ASSUME_STATUS_LOW_ODDS 40 // Chance for AI to see niche moves a pokemon may have but probably won't, like Entrainment +#define ASSUME_ALL_STATUS_ODDS 25 // Chance for the AI to see any kind of status move. + // AI_FLAG_SMART_SWITCHING settings #define SMART_SWITCHING_OMNISCIENT FALSE // AI will use omniscience for switching calcs, regardless of omniscience setting otherwise diff --git a/include/constants/battle_ai.h b/include/constants/battle_ai.h index 4fc13aaa1b..db386be4b0 100644 --- a/include/constants/battle_ai.h +++ b/include/constants/battle_ai.h @@ -34,8 +34,9 @@ #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_ASSUME_STATUS_MOVES (1 << 29) // AI has a chance to know certain non-damaging moves, and also Fake Out and Super Fang. Restricted version of AI_FLAG_OMNISCIENT. -#define AI_FLAG_COUNT 29 +#define AI_FLAG_COUNT 30 // Flags at and after 32 need different formatting, as in // #define AI_FLAG_PLACEHOLDER ((u64)1 << 32) diff --git a/include/random.h b/include/random.h index f23ff2f184..130fb32e69 100644 --- a/include/random.h +++ b/include/random.h @@ -202,6 +202,12 @@ enum RandomTag RNG_AI_BOOST_INTO_HAZE, RNG_HEALER, RNG_DEXNAV_ENCOUNTER_LEVEL, + RNG_AI_ASSUME_STATUS_SLEEP, + RNG_AI_ASSUME_STATUS_NONVOLATILE, + RNG_AI_ASSUME_STATUS_HIGH_ODDS, + RNG_AI_ASSUME_STATUS_MEDIUM_ODDS, + RNG_AI_ASSUME_STATUS_LOW_ODDS, + RNG_AI_ASSUME_ALL_STATUS, }; #define RandomWeighted(tag, ...) \ diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 44c45c15f1..2b6094c925 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -546,6 +546,17 @@ void RecordMovesBasedOnStab(u32 battler) } } +void RecordStatusMoves(u32 battler) +{ + u32 i; + for (i = 0; i < MAX_MON_MOVES; i++) + { + u32 playerMove = gBattleMons[battler].moves[i]; + if (ShouldRecordStatusMove(playerMove)) + RecordKnownMove(battler, playerMove); + } +} + void SetBattlerAiData(u32 battler, struct AiLogicData *aiData) { u32 ability, holdEffect; @@ -561,6 +572,9 @@ void SetBattlerAiData(u32 battler, struct AiLogicData *aiData) if (IsAiBattlerAssumingStab()) RecordMovesBasedOnStab(battler); + + if (IsAiBattlerAssumingStatusMoves()) + RecordStatusMoves(battler); } #define BYPASSES_ACCURACY_CALC 101 // 101 indicates for ai that the move will always hit diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 7f3b6b74e2..625ea3a9e1 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -141,6 +141,15 @@ bool32 IsAiBattlerAssumingStab() return FALSE; } +bool32 IsAiBattlerAssumingStatusMoves() +{ + if (gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_ASSUME_STATUS_MOVES + || gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_RIGHT] & AI_FLAG_ASSUME_STATUS_MOVES) + return TRUE; + + return FALSE; +} + bool32 IsAiBattlerPredictingAbility(u32 battlerId) { if (gAiThinkingStruct->aiFlags[B_POSITION_OPPONENT_LEFT] & AI_FLAG_WEIGH_ABILITY_PREDICTION @@ -249,6 +258,66 @@ void SaveBattlerData(u32 battlerId) gAiThinkingStruct->saved[battlerId].types[1] = gBattleMons[battlerId].types[1]; } +bool32 ShouldRecordStatusMove(u32 move) +{ + if (ASSUME_STATUS_MOVES_HAS_TUNING) + { + switch (GetMoveEffect(move)) + { + // variable odds by additional effect + case EFFECT_NON_VOLATILE_STATUS: + if (GetMoveNonVolatileStatus(move) == MOVE_EFFECT_SLEEP && RandomPercentage(RNG_AI_ASSUME_STATUS_SLEEP, ASSUME_STATUS_HIGH_ODDS)) + return TRUE; + else if (RandomPercentage(RNG_AI_ASSUME_STATUS_NONVOLATILE, ASSUME_STATUS_MEDIUM_ODDS)) + return TRUE; + break; + // High odds + case EFFECT_AURORA_VEIL: + case EFFECT_CHILLY_RECEPTION: + case EFFECT_FIRST_TURN_ONLY: + case EFFECT_FOLLOW_ME: + case EFFECT_INSTRUCT: + case EFFECT_JUNGLE_HEALING: + case EFFECT_SHED_TAIL: + return RandomPercentage(RNG_AI_ASSUME_STATUS_HIGH_ODDS, ASSUME_STATUS_HIGH_ODDS); + // Medium odds + case EFFECT_AFTER_YOU: + case EFFECT_DOODLE: + case EFFECT_ENCORE: + case EFFECT_HAZE: + case EFFECT_PARTING_SHOT: + case EFFECT_PROTECT: + case EFFECT_REST: + case EFFECT_ROAR: + case EFFECT_ROOST: + case EFFECT_SLEEP_TALK: + case EFFECT_TAUNT: + case EFFECT_TAILWIND: + case EFFECT_TRICK: + case EFFECT_TRICK_ROOM: + // defoggables / screens and hazards + case EFFECT_LIGHT_SCREEN: + case EFFECT_REFLECT: + case EFFECT_SPIKES: + case EFFECT_STEALTH_ROCK: + case EFFECT_STICKY_WEB: + case EFFECT_TOXIC_SPIKES: + return RandomPercentage(RNG_AI_ASSUME_STATUS_MEDIUM_ODDS, ASSUME_STATUS_MEDIUM_ODDS); + // Low odds + case EFFECT_ENTRAINMENT: + case EFFECT_FIXED_PERCENT_DAMAGE: + case EFFECT_GASTRO_ACID: + case EFFECT_IMPRISON: + case EFFECT_TELEPORT: + return RandomPercentage(RNG_AI_ASSUME_STATUS_LOW_ODDS, ASSUME_STATUS_LOW_ODDS); + default: + break; + } + } + + return RandomPercentage(RNG_AI_ASSUME_ALL_STATUS, ASSUME_ALL_STATUS_ODDS) && IsBattleMoveStatus(move); +} + static bool32 ShouldFailForIllusion(u32 illusionSpecies, u32 battlerId) { u32 i, j; diff --git a/test/battle/ai/ai_assume_status_moves.c b/test/battle/ai/ai_assume_status_moves.c new file mode 100644 index 0000000000..8fbaf01101 --- /dev/null +++ b/test/battle/ai/ai_assume_status_moves.c @@ -0,0 +1,49 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +AI_DOUBLE_BATTLE_TEST("AI_FLAG_ASSUME_STATUS_MOVES correctly records assumed status moves") +{ + PASSES_RANDOMLY(ASSUME_STATUS_HIGH_ODDS, 100, RNG_AI_ASSUME_STATUS_HIGH_ODDS); + PASSES_RANDOMLY(ASSUME_STATUS_MEDIUM_ODDS, 100, RNG_AI_ASSUME_STATUS_MEDIUM_ODDS); + PASSES_RANDOMLY(ASSUME_STATUS_LOW_ODDS, 100, RNG_AI_ASSUME_STATUS_LOW_ODDS); + PASSES_RANDOMLY(ASSUME_ALL_STATUS_ODDS, 100, RNG_AI_ASSUME_ALL_STATUS); + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_ASSUME_STATUS_MOVES); + PLAYER(SPECIES_TYPHLOSION) { Moves(MOVE_TACKLE, MOVE_COURT_CHANGE, MOVE_FAKE_OUT); } + PLAYER(SPECIES_ZIGZAGOON) { Moves(MOVE_HAIL, MOVE_SHED_TAIL, 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 { + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][0], MOVE_TACKLE); + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][1], MOVE_COURT_CHANGE); + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][2], MOVE_FAKE_OUT); + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_LEFT][3], MOVE_NONE); + + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][0], MOVE_HAIL); + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][1], MOVE_SHED_TAIL); + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][2], MOVE_THUNDERBOLT); + EXPECT_EQ(gBattleHistory->usedMoves[B_POSITION_PLAYER_RIGHT][3], MOVE_NONE); + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_ASSUME_STATUS_MOVES changes behavior") +{ + if (ASSUME_STATUS_MOVES_HAS_TUNING) + PASSES_RANDOMLY(ASSUME_STATUS_MEDIUM_ODDS, 100, RNG_AI_ASSUME_STATUS_MEDIUM_ODDS); + else + PASSES_RANDOMLY(ASSUME_ALL_STATUS_ODDS, 100, RNG_AI_ASSUME_ALL_STATUS); + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_ASSUME_STATUS_MOVES); + PLAYER(SPECIES_ZIGZAGOON) { Moves(MOVE_REST, MOVE_HEADBUTT); } + OPPONENT(SPECIES_ZIGZAGOON) { Moves(MOVE_WORRY_SEED, MOVE_HEADBUTT); } + } WHEN { + TURN { MOVE(player, MOVE_HEADBUTT); EXPECT_MOVE(opponent, MOVE_WORRY_SEED); } + } +} + +