AI_FLAG_ASSUME_STATUS_MOVES -- AI flag to randomly know some of the player's status moves (#7324)

This commit is contained in:
Pawkkie 2025-07-14 15:04:31 -04:00 committed by GitHub
commit d27884cbb8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 157 additions and 1 deletions

View File

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

View File

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

View File

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

View File

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

View File

@ -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, ...) \

View File

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

View File

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

View File

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