Improve AI's ShouldRecover (#7342)

This commit is contained in:
Pawkkie 2025-07-16 17:30:51 -04:00 committed by GitHub
parent 0406caa687
commit 65a63fb9f0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 111 additions and 23 deletions

View File

@ -93,7 +93,7 @@ bool32 AI_CanBattlerEscape(u32 battler);
bool32 IsBattlerTrapped(u32 battlerAtk, u32 battlerDef);
s32 AI_WhoStrikesFirst(u32 battlerAI, u32 battler2, u32 moveConsidered);
bool32 CanTargetFaintAi(u32 battlerDef, u32 battlerAtk);
u32 NoOfHitsForTargetToFaintAI(u32 battlerDef, u32 battlerAtk);
u32 NoOfHitsForTargetToFaintBattler(u32 battlerDef, u32 battlerAtk);
u32 GetBestDmgMoveFromBattler(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext);
u32 GetBestDmgFromBattler(u32 battler, u32 battlerTarget, enum DamageCalcContext calcContext);
bool32 CanTargetMoveFaintAi(u32 move, u32 battlerDef, u32 battlerAtk, u32 nHits);
@ -115,7 +115,7 @@ bool32 ShouldTryOHKO(u32 battlerAtk, u32 battlerDef, u32 atkAbility, u32 defAbil
bool32 ShouldUseRecoilMove(u32 battlerAtk, u32 battlerDef, u32 recoilDmg, u32 moveIndex);
u32 GetBattlerSideSpeedAverage(u32 battler);
bool32 ShouldAbsorb(u32 battlerAtk, u32 battlerDef, u32 move, s32 damage);
bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent, enum DamageCalcContext calcContext);
bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent);
bool32 ShouldSetScreen(u32 battlerAtk, u32 battlerDef, enum BattleMoveEffects moveEffect);
enum AIPivot ShouldPivot(u32 battlerAtk, u32 battlerDef, u32 defAbility, u32 move, u32 moveIndex);
bool32 IsRecycleEncouragedItem(u32 item);

View File

@ -56,6 +56,8 @@
// AI move scoring
#define STATUS_MOVE_FOCUS_PUNCH_CHANCE 50 // Chance the AI will use a status move if the player's best move is Focus Punch
#define BOOST_INTO_HAZE_CHANCE 0 // Chance the AI will use a stat boosting move if the player has used Haze
#define SHOULD_RECOVER_CHANCE 50 // Chance the AI will give recovery moves score increase if less than ENABLE_RECOVERY_THRESHOLD and in no immediate danger
#define ENABLE_RECOVERY_THRESHOLD 60 // HP percentage beneath which SHOULD_RECOVER_CHANCE is active
// AI damage calc considerations
#define RISKY_AI_CRIT_STAGE_THRESHOLD 2 // Stat stages at which Risky will assume it gets a crit

View File

@ -200,6 +200,7 @@ enum RandomTag
RNG_AI_PREDICT_MOVE,
RNG_AI_STATUS_FOCUS_PUNCH,
RNG_AI_BOOST_INTO_HAZE,
RNG_AI_SHOULD_RECOVER,
RNG_HEALER,
RNG_DEXNAV_ENCOUNTER_LEVEL,
RNG_AI_ASSUME_STATUS_SLEEP,

View File

@ -3897,11 +3897,22 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move)
ADJUST_SCORE(DECENT_EFFECT);
break;
case EFFECT_DREAM_EATER:
case EFFECT_STRENGTH_SAP:
case EFFECT_AQUA_RING:
if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_BIG_ROOT)
ADJUST_SCORE(DECENT_EFFECT);
break;
case EFFECT_STRENGTH_SAP:
u32 atkStat = gBattleMons[battlerDef].attack;
u32 atkStage = gBattleMons[battlerDef].statStages[STAT_ATK];
atkStat *= gStatStageRatios[atkStage][0];
atkStat /= gStatStageRatios[atkStage][1];
u32 healPercent = atkStat * 100 / gBattleMons[battlerAtk].maxHP;
if (ShouldRecover(battlerAtk, battlerDef, move, healPercent))
{
ADJUST_SCORE(GOOD_EFFECT);
if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_BIG_ROOT)
ADJUST_SCORE(WEAK_EFFECT);
}
case EFFECT_EXPLOSION:
case EFFECT_MISTY_EXPLOSION:
case EFFECT_MEMENTO:
@ -4054,7 +4065,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move)
break;
}
if (ShouldRecover(battlerAtk, battlerDef, move, healPercent, AI_DEFENDING))
if (ShouldRecover(battlerAtk, battlerDef, move, healPercent))
ADJUST_SCORE(DECENT_EFFECT);
}
break;
@ -4064,7 +4075,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move)
case EFFECT_MORNING_SUN:
case EFFECT_SYNTHESIS:
case EFFECT_MOONLIGHT:
if (ShouldRecover(battlerAtk, battlerDef, move, 50, AI_DEFENDING))
if (ShouldRecover(battlerAtk, battlerDef, move, 50))
ADJUST_SCORE(GOOD_EFFECT);
break;
case EFFECT_LIGHT_SCREEN:
@ -4082,7 +4093,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move)
{
break;
}
else if (ShouldRecover(battlerAtk, battlerDef, move, 100, AI_DEFENDING))
else if (ShouldRecover(battlerAtk, battlerDef, move, 100))
{
if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_CURE_SLP
|| aiData->holdEffects[battlerAtk] == HOLD_EFFECT_CURE_STATUS
@ -5044,9 +5055,9 @@ case EFFECT_GUARD_SPLIT:
ADJUST_SCORE(GOOD_EFFECT);
break;
case EFFECT_SHORE_UP:
if ((AI_GetWeather() & B_WEATHER_SANDSTORM) && ShouldRecover(battlerAtk, battlerDef, move, 67, AI_DEFENDING))
if ((AI_GetWeather() & B_WEATHER_SANDSTORM) && ShouldRecover(battlerAtk, battlerDef, move, 67))
ADJUST_SCORE(DECENT_EFFECT);
else if (ShouldRecover(battlerAtk, battlerDef, move, 50, AI_DEFENDING))
else if (ShouldRecover(battlerAtk, battlerDef, move, 50))
ADJUST_SCORE(DECENT_EFFECT);
break;
case EFFECT_ENDEAVOR:
@ -5072,8 +5083,8 @@ case EFFECT_GUARD_SPLIT:
//case EFFECT_SKY_DROP
//break;
case EFFECT_JUNGLE_HEALING:
if (ShouldRecover(battlerAtk, battlerDef, move, 25, AI_DEFENDING)
|| ShouldRecover(BATTLE_PARTNER(battlerAtk), battlerDef, move, 25, AI_DEFENDING)
if (ShouldRecover(battlerAtk, battlerDef, move, 25)
|| ShouldRecover(BATTLE_PARTNER(battlerAtk), battlerDef, move, 25)
|| gBattleMons[battlerAtk].status1 & STATUS1_ANY
|| gBattleMons[BATTLE_PARTNER(battlerAtk)].status1 & STATUS1_ANY)
ADJUST_SCORE(GOOD_EFFECT);

View File

@ -1349,7 +1349,7 @@ bool32 CanTargetFaintAi(u32 battlerDef, u32 battlerAtk)
return FALSE;
}
u32 NoOfHitsForTargetToFaintAI(u32 battlerDef, u32 battlerAtk)
u32 NoOfHitsForTargetToFaintBattler(u32 battlerDef, u32 battlerAtk)
{
u32 i;
u32 currNumberOfHits;
@ -1367,6 +1367,32 @@ u32 NoOfHitsForTargetToFaintAI(u32 battlerDef, u32 battlerAtk)
return leastNumberOfHits;
}
u32 NoOfHitsForTargetToFaintBattlerWithMod(u32 battlerDef, u32 battlerAtk, s32 hpMod)
{
u32 i;
u32 currNumberOfHits;
u32 leastNumberOfHits = UNKNOWN_NO_OF_HITS;
u32 hpCheck = gBattleMons[battlerAtk].hp + hpMod;
u32 damageDealt = 0;
if (hpCheck > gBattleMons[battlerAtk].maxHP)
hpCheck = gBattleMons[battlerAtk].maxHP;
for (i = 0; i < MAX_MON_MOVES; i++)
{
damageDealt = AI_GetDamage(battlerDef, battlerAtk, i, AI_DEFENDING, gAiLogicData);
if (damageDealt == 0)
continue;
currNumberOfHits = hpCheck / (damageDealt + 1) + 1;
if (currNumberOfHits != 0)
{
if (currNumberOfHits < leastNumberOfHits)
leastNumberOfHits = currNumberOfHits;
}
}
return leastNumberOfHits;
}
u32 GetBestDmgMoveFromBattler(u32 battlerAtk, u32 battlerDef, enum DamageCalcContext calcContext)
{
struct AiLogicData *aiData = gAiLogicData;
@ -3710,21 +3736,30 @@ bool32 ShouldAbsorb(u32 battlerAtk, u32 battlerDef, u32 move, s32 damage)
return FALSE;
}
bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent, enum DamageCalcContext calcContext)
bool32 ShouldRecover(u32 battlerAtk, u32 battlerDef, u32 move, u32 healPercent)
{
if (move == 0xFFFF || AI_IsFaster(battlerAtk, battlerDef, move))
u32 maxHP = gBattleMons[battlerAtk].maxHP;
u32 healAmount = (healPercent * maxHP) / 100;
if (healAmount > maxHP)
healAmount = maxHP;
if (gStatuses3[battlerAtk] & STATUS3_HEAL_BLOCK)
healAmount = 0;
if (AI_IsFaster(battlerAtk, battlerDef, move))
{
// using item or user going first
s32 damage = AI_GetDamage(battlerAtk, battlerDef, gAiThinkingStruct->movesetIndex, calcContext, gAiLogicData);
s32 healAmount = (healPercent * damage) / 100;
if (gStatuses3[battlerAtk] & STATUS3_HEAL_BLOCK)
healAmount = 0;
if (CanTargetFaintAi(battlerDef, battlerAtk)
&& !CanTargetFaintAiWithMod(battlerDef, battlerAtk, healAmount, 0))
return TRUE; // target can faint attacker unless they heal
else if (!CanTargetFaintAi(battlerDef, battlerAtk) && gAiLogicData->hpPercents[battlerAtk] < 60 && (Random() % 3))
return TRUE; // target can't faint attacker at all, attacker health is about half, 2/3rds rate of encouraging healing
else if (!CanTargetFaintAi(battlerDef, battlerAtk) && gAiLogicData->hpPercents[battlerAtk] < ENABLE_RECOVERY_THRESHOLD && RandomPercentage(RNG_AI_SHOULD_RECOVER, SHOULD_RECOVER_CHANCE))
return TRUE; // target can't faint attacker at all, generally safe
}
else
{
if (!CanTargetFaintAi(battlerDef, battlerAtk)
&& GetBestDmgFromBattler(battlerDef, battlerAtk, AI_DEFENDING) < healAmount
&& NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk) < NoOfHitsForTargetToFaintBattlerWithMod(battlerDef, battlerAtk, healAmount))
return TRUE; // target can't faint attacker and is dealing less damage than we're healing
else if (!CanTargetFaintAi(battlerDef, battlerAtk) && gAiLogicData->hpPercents[battlerAtk] < ENABLE_RECOVERY_THRESHOLD && RandomPercentage(RNG_AI_SHOULD_RECOVER, SHOULD_RECOVER_CHANCE))
return TRUE; // target can't faint attacker at all, generally safe
}
return FALSE;
}
@ -3972,7 +4007,7 @@ bool32 ShouldUseWishAromatherapy(u32 battlerAtk, u32 battlerDef, u32 move)
switch (GetMoveEffect(move))
{
case EFFECT_WISH:
return ShouldRecover(battlerAtk, battlerDef, move, 50, AI_DEFENDING); // Switch recovery isn't good idea in doubles
return ShouldRecover(battlerAtk, battlerDef, move, 50); // Switch recovery isn't good idea in doubles
case EFFECT_HEAL_BELL:
if (hasStatus)
return TRUE;
@ -4272,7 +4307,7 @@ bool32 HasMoveThatChangesKOThreshold(u32 battlerId, u32 noOfHitsToFaint, u32 aiI
static enum AIScore IncreaseStatUpScoreInternal(u32 battlerAtk, u32 battlerDef, enum StatChange statId, bool32 considerContrary)
{
enum AIScore tempScore = NO_INCREASE;
u32 noOfHitsToFaint = NoOfHitsForTargetToFaintAI(battlerDef, battlerAtk);
u32 noOfHitsToFaint = NoOfHitsForTargetToFaintBattler(battlerDef, battlerAtk);
u32 aiIsFaster = AI_IsFaster(battlerAtk, battlerDef, TRUE);
u32 shouldSetUp = ((noOfHitsToFaint >= 2 && aiIsFaster) || (noOfHitsToFaint >= 3 && !aiIsFaster) || noOfHitsToFaint == UNKNOWN_NO_OF_HITS);
u32 i;

View File

@ -945,3 +945,42 @@ AI_SINGLE_BATTLE_TEST("AI won't setup if otherwise good scenario is changed by t
TURN { MOVE(player, MOVE_SURF); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); }
}
}
AI_SINGLE_BATTLE_TEST("AI will use Recovery move if it outheals your damage and outspeeds")
{
PASSES_RANDOMLY(100, 100, RNG_AI_SHOULD_RECOVER);
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT);
PLAYER(SPECIES_LINOONE) { Speed(2); Moves(MOVE_HEADBUTT); }
OPPONENT(SPECIES_GASTRODON) { Speed(5); Moves(MOVE_SCALD, MOVE_RECOVER); HP(1); }
} WHEN {
TURN { MOVE(player, MOVE_HEADBUTT); EXPECT_MOVE(opponent, MOVE_RECOVER); }
}
}
AI_SINGLE_BATTLE_TEST("AI will use recovery move if it outheals your damage and is outsped")
{
u32 aiMove = MOVE_NONE;
PASSES_RANDOMLY(100, 100, RNG_AI_SHOULD_RECOVER);
PARAMETRIZE{ aiMove = MOVE_RECOVER; }
PARAMETRIZE{ aiMove = MOVE_STRENGTH_SAP; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT);
PLAYER(SPECIES_LINOONE) { Speed(5); Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_GASTRODON) { Speed(2); Moves(MOVE_SCALD, aiMove); HP(200); MaxHP(400); }
} WHEN {
TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, aiMove); }
}
}
AI_SINGLE_BATTLE_TEST("AI will use recovery move if is in no immediate danger beneath an HP threshold")
{
PASSES_RANDOMLY(SHOULD_RECOVER_CHANCE, 100, RNG_AI_SHOULD_RECOVER);
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT);
PLAYER(SPECIES_LINOONE) { Speed(2); Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_GASTRODON) { Speed(5); Moves(MOVE_SCALD, MOVE_RECOVER); HP(200); MaxHP(400); }
} WHEN {
TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, MOVE_RECOVER); }
}
}