Fix move comparison scoring (#7301)

This commit is contained in:
Pawkkie 2025-07-10 03:51:46 -04:00 committed by GitHub
parent d213b1fad7
commit e4d9298200
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 137 additions and 77 deletions

View File

@ -23,6 +23,15 @@ enum DamageCalcContext
AI_ATTACKING,
};
// Higher priority at the bottom; note that these are used in the formula MAX_MON_MOVES ^ AiCompareMovesPriority, which must fit within a u32.
// In expansion where MAX_MON_MOVES is 4, this means that AiCompareMovesPriority can range from 0 - 15 inclusive.
enum AiCompareMovesPriority
{
PRIORITY_EFFECT,
PRIORITY_ACCURACY,
PRIORITY_NOT_CHARGING
};
enum AIPivot
{
DONT_PIVOT,

View File

@ -10,5 +10,6 @@ s32 MathUtil_Div32(s32 x, s32 y);
s16 MathUtil_Inv16(s16 y);
s16 MathUtil_Inv16Shift(u8 s, s16 y);
s32 MathUtil_Inv32(s32 y);
u32 MathUtil_Exponent(u32 x, u32 y);
#endif // GUARD_MATH_UTIL_H

View File

@ -14,6 +14,7 @@
#include "debug.h"
#include "event_data.h"
#include "item.h"
#include "math_util.h"
#include "pokemon.h"
#include "random.h"
#include "recorded_battle.h"
@ -37,6 +38,7 @@ static u32 ChooseMoveOrAction_Doubles(u32 battler);
static inline void BattleAI_DoAIProcessing(struct AiThinkingStruct *aiThink, u32 battlerAtk, u32 battlerDef);
static inline void BattleAI_DoAIProcessing_PredictedSwitchin(struct AiThinkingStruct *aiThink, struct AiLogicData *aiData, u32 battlerAtk, u32 battlerDef);
static bool32 IsPinchBerryItemEffect(enum ItemHoldEffect holdEffect);
static void AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef);
// ewram
EWRAM_DATA const u8 *gAIScriptPtr = NULL; // Still used in contests
@ -693,6 +695,9 @@ static u32 ChooseMoveOrAction_Singles(u32 battler)
gAiThinkingStruct->aiLogicId++;
}
if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_CHECK_VIABILITY)
AI_CompareDamagingMoves(battler, opposingBattler);
for (i = 0; i < MAX_MON_MOVES; i++)
{
gAiBattleData->finalScore[battler][opposingBattler][i] = gAiThinkingStruct->score[i];
@ -769,6 +774,8 @@ static u32 ChooseMoveOrAction_Doubles(u32 battler)
flags >>= (u64)1;
gAiThinkingStruct->aiLogicId++;
}
if (gAiThinkingStruct->aiFlags[battler] & AI_FLAG_CHECK_VIABILITY)
AI_CompareDamagingMoves(battler, gBattlerTarget);
mostViableMovesScores[0] = gAiThinkingStruct->score[0];
mostViableMovesIndices[0] = 0;
@ -3648,104 +3655,124 @@ static inline bool32 ShouldUseSpreadDamageMove(u32 battlerAtk, u32 move, u32 mov
&& noOfHitsToFaintPartner < (friendlyFireThreshold * 2));
}
static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId)
static bool32 ShouldCompareMove(u32 battlerAtk, u32 battlerDef, u32 moveIndex, u16 move)
{
u32 i;
if (IS_TARGETING_PARTNER(battlerAtk, battlerDef))
return FALSE;
if (GetMovePower(move) == 0)
return FALSE;
if (GetNoOfHitsToKOBattler(battlerAtk, battlerDef, moveIndex, AI_ATTACKING) == 0)
return FALSE;
if (gAiThinkingStruct->aiFlags[battlerAtk] & (AI_FLAG_RISKY | AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE) && GetBestDmgMoveFromBattler(battlerAtk, battlerDef, AI_ATTACKING) == move)
return FALSE;
return TRUE;
}
static void AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef)
{
u32 i, currId;
u32 tempMoveScores[MAX_MON_MOVES];
u32 moveComparisonScores[MAX_MON_MOVES];
u32 bestScore = AI_SCORE_DEFAULT;
bool32 multipleBestMoves = FALSE;
s32 viableMoveScores[MAX_MON_MOVES];
s32 bestViableMoveScore;
s32 noOfHits[MAX_MON_MOVES];
s32 score = 0;
s32 leastHits = 1000;
u16 *moves = GetMovesArray(battlerAtk);
bool8 isTwoTurnNotSemiInvulnerableMove[MAX_MON_MOVES];
for (i = 0; i < MAX_MON_MOVES; i++)
{
if (moves[i] != MOVE_NONE && GetMovePower(moves[i]) != 0)
{
noOfHits[i] = GetNoOfHitsToKOBattler(battlerAtk, battlerDef, i, AI_ATTACKING);
if (ShouldUseSpreadDamageMove(battlerAtk,moves[i], i, noOfHits[i]))
{
noOfHits[i] = -1;
viableMoveScores[i] = 0;
isTwoTurnNotSemiInvulnerableMove[i] = FALSE;
}
else if (noOfHits[i] < leastHits && noOfHits[i] != 0)
{
leastHits = noOfHits[i];
}
viableMoveScores[i] = AI_SCORE_DEFAULT;
isTwoTurnNotSemiInvulnerableMove[i] = IsTwoTurnNotSemiInvulnerableMove(battlerAtk, moves[i]);
}
else
{
noOfHits[i] = -1;
viableMoveScores[i] = 0;
isTwoTurnNotSemiInvulnerableMove[i] = FALSE;
}
}
// Priority list:
// 1. Less no of hits to ko
// 2. Not charging
// 3. More accuracy
// 4. Better effect
// Current move requires the least hits to KO. Compare with other moves.
if (leastHits == noOfHits[currId])
for (currId = 0; currId < MAX_MON_MOVES; currId++)
{
moveComparisonScores[currId] = 0;
if (!ShouldCompareMove(battlerAtk, battlerDef, currId, moves[currId]))
continue;
for (i = 0; i < MAX_MON_MOVES; i++)
{
if (i == currId)
continue;
if (noOfHits[currId] == noOfHits[i])
if (moves[i] != MOVE_NONE && GetMovePower(moves[i]) != 0)
{
multipleBestMoves = TRUE;
// We need to make sure it's the current move which is objectively better.
if (isTwoTurnNotSemiInvulnerableMove[i] && !isTwoTurnNotSemiInvulnerableMove[currId])
viableMoveScores[i] -= 3;
else if (!isTwoTurnNotSemiInvulnerableMove[i] && isTwoTurnNotSemiInvulnerableMove[currId])
viableMoveScores[currId] -= 3;
switch (CompareMoveAccuracies(battlerAtk, battlerDef, currId, i))
noOfHits[i] = GetNoOfHitsToKOBattler(battlerAtk, battlerDef, i, AI_ATTACKING);
if (ShouldUseSpreadDamageMove(battlerAtk,moves[i], i, noOfHits[i]))
{
case 1:
viableMoveScores[i] -= 2;
break;
case -1:
viableMoveScores[currId] -= 2;
break;
noOfHits[i] = -1;
tempMoveScores[i] = 0;
isTwoTurnNotSemiInvulnerableMove[i] = FALSE;
}
switch (AI_WhichMoveBetter(moves[currId], moves[i], battlerAtk, battlerDef, noOfHits[currId]))
else if (noOfHits[i] < leastHits && noOfHits[i] != 0)
{
case 1:
viableMoveScores[i] -= 1;
break;
case -1:
viableMoveScores[currId] -= 1;
break;
leastHits = noOfHits[i];
}
tempMoveScores[i] = AI_SCORE_DEFAULT;
isTwoTurnNotSemiInvulnerableMove[i] = IsTwoTurnNotSemiInvulnerableMove(battlerAtk, moves[i]);
}
else
{
noOfHits[i] = -1;
tempMoveScores[i] = 0;
isTwoTurnNotSemiInvulnerableMove[i] = FALSE;
}
}
// Turns out the current move deals the most dmg compared to the other 3.
if (!multipleBestMoves)
ADJUST_SCORE(BEST_DAMAGE_MOVE);
else
// Priority list:
// 1. Less no of hits to ko
// 2. Not charging
// 3. More accuracy
// 4. Better effect
// Current move requires the least hits to KO. Compare with other moves.
if (leastHits == noOfHits[currId])
{
bestViableMoveScore = 0;
for (i = 0; i < MAX_MON_MOVES; i++)
{
if (viableMoveScores[i] > bestViableMoveScore)
bestViableMoveScore = viableMoveScores[i];
if (i == currId)
continue;
if (noOfHits[currId] == noOfHits[i])
{
multipleBestMoves = TRUE;
// We need to make sure it's the current move which is objectively better.
if (isTwoTurnNotSemiInvulnerableMove[i] && !isTwoTurnNotSemiInvulnerableMove[currId])
tempMoveScores[currId] += MathUtil_Exponent(MAX_MON_MOVES, PRIORITY_NOT_CHARGING);
else if (!isTwoTurnNotSemiInvulnerableMove[i] && isTwoTurnNotSemiInvulnerableMove[currId])
tempMoveScores[i] += MathUtil_Exponent(MAX_MON_MOVES, PRIORITY_NOT_CHARGING);
switch (CompareMoveAccuracies(battlerAtk, battlerDef, currId, i))
{
case 1:
tempMoveScores[currId] += MathUtil_Exponent(MAX_MON_MOVES, PRIORITY_ACCURACY);
break;
case -1:
tempMoveScores[i] += MathUtil_Exponent(MAX_MON_MOVES, PRIORITY_ACCURACY);
break;
}
switch (AI_WhichMoveBetter(moves[currId], moves[i], battlerAtk, battlerDef, noOfHits[currId]))
{
case 1:
tempMoveScores[currId] += MathUtil_Exponent(MAX_MON_MOVES, PRIORITY_EFFECT);
break;
case -1:
tempMoveScores[i] += MathUtil_Exponent(MAX_MON_MOVES, PRIORITY_EFFECT);
break;
}
}
}
// Unless a better move was found increase score of current move
if (viableMoveScores[currId] == bestViableMoveScore)
ADJUST_SCORE(BEST_DAMAGE_MOVE);
// Turns out the current move deals the most dmg compared to the other 3.
if (!multipleBestMoves)
moveComparisonScores[currId] = UINT32_MAX;
else
moveComparisonScores[currId] = tempMoveScores[currId];
}
}
return score;
// Find highest comparison score
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (moveComparisonScores[i] > bestScore)
bestScore = moveComparisonScores[i];
}
// Increase score for corresponding move(s), accomodating ties
for (int i = 0; i < MAX_MON_MOVES; i++)
{
if (moveComparisonScores[i] == bestScore)
gAiThinkingStruct->score[i] += BEST_DAMAGE_MOVE;
}
}
static u32 AI_CalcHoldEffectMoveScore(u32 battlerAtk, u32 battlerDef, u32 move)
@ -5336,8 +5363,6 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score
if (gAiThinkingStruct->aiFlags[battlerAtk] & (AI_FLAG_RISKY | AI_FLAG_PREFER_HIGHEST_DAMAGE_MOVE)
&& GetBestDmgMoveFromBattler(battlerAtk, battlerDef, AI_ATTACKING) == move)
ADJUST_SCORE(BEST_DAMAGE_MOVE);
else
ADJUST_SCORE(AI_CompareDamagingMoves(battlerAtk, battlerDef, gAiThinkingStruct->movesetIndex));
}
}

View File

@ -84,3 +84,12 @@ s32 MathUtil_Inv32(s32 y)
x = 0x10000;
return x / y;
}
u32 MathUtil_Exponent(u32 x, u32 y)
{
u32 result = 1;
for (u32 index = 0; index < y; index++)
result *= x;
return result;
}

View File

@ -865,3 +865,19 @@ AI_SINGLE_BATTLE_TEST("AI will not set up Weather if it wont have any affect")
TURN { MOVE(player, MOVE_SCRATCH); EXPECT_MOVE(opponent, MOVE_RAIN_DANCE); }
}
}
AI_SINGLE_BATTLE_TEST("Move scoring comparison properly awards bonus point to best OHKO move")
{
GIVEN {
ASSUME(MoveHasAdditionalEffect(MOVE_THUNDER, MOVE_EFFECT_PARALYSIS));
ASSUME(GetMoveAdditionalEffectCount(MOVE_WATER_SPOUT) == 0);
ASSUME(GetMoveAdditionalEffectCount(MOVE_WATER_GUN) == 0);
ASSUME(GetMoveAdditionalEffectCount(MOVE_ORIGIN_PULSE) == 0);
ASSUME(GetMoveAccuracy(MOVE_WATER_SPOUT) > GetMoveAccuracy(MOVE_THUNDER));
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_TRY_TO_FAINT | AI_FLAG_CHECK_VIABILITY);
PLAYER(SPECIES_WAILORD) { Level(50); }
OPPONENT(SPECIES_WAILORD) { Moves(MOVE_THUNDER, MOVE_WATER_SPOUT, MOVE_WATER_GUN, MOVE_SURF); }
} WHEN {
TURN { EXPECT_MOVE(opponent, MOVE_WATER_SPOUT); }
}
}