diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index 66fe8d550d..6890096a79 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -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, diff --git a/include/math_util.h b/include/math_util.h index 04013fc03f..71d79d9ba8 100755 --- a/include/math_util.h +++ b/include/math_util.h @@ -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 diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 1b8520fa5d..acf0bdb722 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -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)); } } diff --git a/src/math_util.c b/src/math_util.c index f77c82608a..a29b03a15b 100644 --- a/src/math_util.c +++ b/src/math_util.c @@ -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; +} diff --git a/test/battle/ai/ai.c b/test/battle/ai/ai.c index 249e686c16..3fe975fc66 100644 --- a/test/battle/ai/ai.c +++ b/test/battle/ai/ai.c @@ -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); } + } +}