diff --git a/include/battle_ai_main.h b/include/battle_ai_main.h index e2c7804091..04441312f2 100644 --- a/include/battle_ai_main.h +++ b/include/battle_ai_main.h @@ -32,7 +32,7 @@ #define BEST_DAMAGE_MOVE 1 // Move with the most amount of hits with the best accuracy/effect #define POWERFUL_STATUS_MOVE 10 // Moves with this score will be chosen over a move that faints target -// Temporary scores that are added together to determine a final score at the at of AI_CalcMoveEffectScore +// Temporary scores that are added together to determine a final score at the end of AI_CalcMoveEffectScore #define WEAK_EFFECT 1 #define DECENT_EFFECT 2 #define GOOD_EFFECT 4 @@ -49,6 +49,10 @@ #define SLOW_KILL 4 // AI is slower and faints target #define LAST_CHANCE 2 // AI faints to target. It should try and do damage with a priority move +// AI_Risky +#define STRONG_RISKY_EFFECT 3 +#define AVERAGE_RISKY_EFFECT 2 + #include "test_runner.h" // Logs for debugging AI tests. diff --git a/include/battle_ai_util.h b/include/battle_ai_util.h index b6319b7f66..afd468fbb6 100644 --- a/include/battle_ai_util.h +++ b/include/battle_ai_util.h @@ -5,6 +5,10 @@ #define AI_STRIKES_FIRST(battlerAi, battlerDef, move)((AI_WhoStrikesFirst(battlerAi, battlerDef, move) == AI_IS_FASTER)) +// Roll boundaries used by AI when scoring. Doesn't affect actual damage dealt. +#define MAX_ROLL_PERCENTAGE 100 +#define MIN_ROLL_PERCENTAGE 85 + enum { DMG_ROLL_LOWEST, diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index fda99dcafe..3f8178a327 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -1434,8 +1434,9 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) case EFFECT_MIRROR_COAT: if (IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) || gBattleMons[battlerDef].status2 & (STATUS2_INFATUATION | STATUS2_CONFUSION)) ADJUST_SCORE(-1); - if (predictedMove == MOVE_NONE || GetBattleMoveCategory(predictedMove) == DAMAGE_CATEGORY_STATUS - || DoesSubstituteBlockMove(battlerAtk, BATTLE_PARTNER(battlerDef), predictedMove)) + if ((predictedMove == MOVE_NONE || GetBattleMoveCategory(predictedMove) == DAMAGE_CATEGORY_STATUS + || DoesSubstituteBlockMove(battlerAtk, BATTLE_PARTNER(battlerDef), predictedMove)) + && !(predictedMove == MOVE_NONE && (AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_RISKY))) // Let Risky AI predict blindly based on stats ADJUST_SCORE(-10); break; @@ -1911,7 +1912,8 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) ADJUST_SCORE(-8); //No point in healing, but should at least do it if nothing better break; case EFFECT_RECOIL_IF_MISS: - if (aiData->abilities[battlerAtk] != ABILITY_MAGIC_GUARD && AI_DATA->moveAccuracy[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex] < 75) + if (aiData->abilities[battlerAtk] != ABILITY_MAGIC_GUARD && AI_DATA->moveAccuracy[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex] < 75 + && !(AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_RISKY)) ADJUST_SCORE(-6); break; case EFFECT_TRANSFORM: @@ -4742,7 +4744,12 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score if (GetNoOfHitsToKOBattler(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex) == 0) ADJUST_SCORE(-20); else - score += AI_CompareDamagingMoves(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex); + { + if ((AI_THINKING_STRUCT->aiFlags[battlerAtk] & AI_FLAG_RISKY) && GetBestDmgMoveFromBattler(battlerAtk, battlerDef) == move) + score += 1; + else + score += AI_CompareDamagingMoves(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex); + } } score += AI_CalcMoveEffectScore(battlerAtk, battlerDef, move); @@ -4888,27 +4895,37 @@ static s32 AI_Risky(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) if (gMovesInfo[move].criticalHitStage > 0) ADJUST_SCORE(DECENT_EFFECT); + // +3 Score switch (gMovesInfo[move].effect) { - case EFFECT_SLEEP: - case EFFECT_EXPLOSION: - case EFFECT_MIRROR_MOVE: - case EFFECT_OHKO: - case EFFECT_CONFUSE: - case EFFECT_METRONOME: - case EFFECT_PSYWAVE: case EFFECT_COUNTER: - case EFFECT_DESTINY_BOND: - case EFFECT_SWAGGER: - case EFFECT_ATTRACT: - case EFFECT_PRESENT: - case EFFECT_BELLY_DRUM: + if (gSpeciesInfo[gBattleMons[battlerDef].species].baseAttack >= gSpeciesInfo[gBattleMons[battlerDef].species].baseSpAttack + 10) + ADJUST_SCORE(STRONG_RISKY_EFFECT); + break; case EFFECT_MIRROR_COAT: - case EFFECT_FOCUS_PUNCH: + if (gSpeciesInfo[gBattleMons[battlerDef].species].baseSpAttack >= gSpeciesInfo[gBattleMons[battlerDef].species].baseAttack + 10) + ADJUST_SCORE(STRONG_RISKY_EFFECT); + break; + case EFFECT_EXPLOSION: + ADJUST_SCORE(STRONG_RISKY_EFFECT); + break; + + // +2 Score case EFFECT_REVENGE: - case EFFECT_FILLET_AWAY: - if (Random() & 1) - ADJUST_SCORE(DECENT_EFFECT); + if (gSpeciesInfo[gBattleMons[battlerDef].species].baseSpeed >= gSpeciesInfo[gBattleMons[battlerAtk].species].baseSpeed + 10) + ADJUST_SCORE(AVERAGE_RISKY_EFFECT); + break; + case EFFECT_BELLY_DRUM: + if (gBattleMons[battlerAtk].hp >= gBattleMons[battlerAtk].maxHP * 90 / 100) + ADJUST_SCORE(AVERAGE_RISKY_EFFECT); + break; + case EFFECT_MAX_HP_50_RECOIL: + case EFFECT_MIND_BLOWN: + case EFFECT_SWAGGER: + case EFFECT_FLATTER: + case EFFECT_ATTRACT: + case EFFECT_OHKO: + ADJUST_SCORE(AVERAGE_RISKY_EFFECT); break; case EFFECT_HIT: { @@ -4920,7 +4937,7 @@ static s32 AI_Risky(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) { case MOVE_EFFECT_ALL_STATS_UP: if (Random() & 1) - ADJUST_SCORE(DECENT_EFFECT); + ADJUST_SCORE(AVERAGE_RISKY_EFFECT); break; default: break; diff --git a/src/battle_ai_util.c b/src/battle_ai_util.c index 6afc4b65ae..9d4f03fb6e 100644 --- a/src/battle_ai_util.c +++ b/src/battle_ai_util.c @@ -369,13 +369,15 @@ s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *type static inline s32 LowestRollDmg(s32 dmg) { - dmg *= 100 - 15; + dmg *= MIN_ROLL_PERCENTAGE; dmg /= 100; return dmg; } static inline s32 HighestRollDmg(s32 dmg) { + dmg *= MAX_ROLL_PERCENTAGE; + dmg /= 100; return dmg; } diff --git a/src/battle_main.c b/src/battle_main.c index 8168d10207..d34b52e51f 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -54,6 +54,7 @@ #include "wild_encounter.h" #include "window.h" #include "constants/abilities.h" +#include "constants/battle_ai.h" #include "constants/battle_move_effects.h" #include "constants/battle_string_ids.h" #include "constants/battle_partner.h" @@ -4438,7 +4439,10 @@ static void HandleTurnActionSelectionState(void) if ((gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart()) && (BattlerHasAi(battler) && !(gBattleTypeFlags & BATTLE_TYPE_PALACE))) { - AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, FALSE); + if (AI_THINKING_STRUCT->aiFlags[battler] & AI_FLAG_RISKY) // Risky AI switches aggressively even mid battle + AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, TRUE); + else + AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, FALSE); gBattleStruct->aiMoveOrAction[battler] = ComputeBattleAiScores(battler); } // fallthrough diff --git a/test/battle/ai_flag_risky.c b/test/battle/ai_flag_risky.c new file mode 100644 index 0000000000..eeef2fbe4e --- /dev/null +++ b/test/battle/ai_flag_risky.c @@ -0,0 +1,91 @@ +#include "global.h" +#include "test/battle.h" + +AI_SINGLE_BATTLE_TEST("AI_FLAG_RISKY: AI will blindly Mirror Coat against special attackers") +{ + u32 aiRiskyFlag = 0; + + PARAMETRIZE{ aiRiskyFlag = 0; } + PARAMETRIZE{ aiRiskyFlag = AI_FLAG_RISKY; } + + GIVEN { + ASSUME(gMovesInfo[MOVE_MIRROR_COAT].effect == EFFECT_MIRROR_COAT); + ASSUME(gSpeciesInfo[SPECIES_GROVYLE].baseSpAttack == 85); + ASSUME(gSpeciesInfo[SPECIES_GROVYLE].baseAttack == 65); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiRiskyFlag); + PLAYER(SPECIES_GROVYLE) { Level(20); Moves(MOVE_ENERGY_BALL); } + OPPONENT(SPECIES_CASTFORM) { Level(20); Moves(MOVE_TACKLE, MOVE_MIRROR_COAT); } + } WHEN { + TURN { MOVE(player, MOVE_ENERGY_BALL) ; EXPECT_MOVE(opponent, aiRiskyFlag ? MOVE_MIRROR_COAT : MOVE_TACKLE); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_RISKY: AI will blindly Counter against physical attackers") +{ + u32 aiRiskyFlag = 0; + + PARAMETRIZE{ aiRiskyFlag = 0; } + PARAMETRIZE{ aiRiskyFlag = AI_FLAG_RISKY; } + + GIVEN { + ASSUME(gMovesInfo[MOVE_COUNTER].effect == EFFECT_COUNTER); + ASSUME(gSpeciesInfo[SPECIES_MARSHTOMP].baseAttack == 85); + ASSUME(gSpeciesInfo[SPECIES_MARSHTOMP].baseSpAttack == 60); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiRiskyFlag); + PLAYER(SPECIES_MARSHTOMP) { Level(20); Moves(MOVE_WATERFALL); } + OPPONENT(SPECIES_CASTFORM) { Level(20); Moves(MOVE_TACKLE, MOVE_COUNTER); } + } WHEN { + TURN { MOVE(player, MOVE_WATERFALL) ; EXPECT_MOVE(opponent, aiRiskyFlag ? MOVE_COUNTER : MOVE_TACKLE); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_RISKY: AI will prioritize Revenge if slower") +{ + u32 aiRiskyFlag = 0; + + PARAMETRIZE{ aiRiskyFlag = 0; } + PARAMETRIZE{ aiRiskyFlag = AI_FLAG_RISKY; } + + GIVEN { + ASSUME(gMovesInfo[MOVE_REVENGE].effect == EFFECT_REVENGE); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiRiskyFlag); + PLAYER(SPECIES_GROVYLE) { Level(20); Speed(4); Moves(MOVE_ENERGY_BALL); } + OPPONENT(SPECIES_CASTFORM) { Level(19); Speed(3); Moves(MOVE_TACKLE, MOVE_REVENGE); } + } WHEN { + TURN { MOVE(player, MOVE_ENERGY_BALL) ; EXPECT_MOVE(opponent, aiRiskyFlag ? MOVE_REVENGE : MOVE_TACKLE); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_RISKY: Mid-battle switches prioritize offensive options") +{ + u32 aiRiskyFlag = 0; + + PARAMETRIZE{ aiRiskyFlag = 0; } + PARAMETRIZE{ aiRiskyFlag = AI_FLAG_RISKY; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_MON_CHOICES | aiRiskyFlag); + PLAYER(SPECIES_SWELLOW) { Level(30); Moves(MOVE_WING_ATTACK, MOVE_BOOMBURST); Speed(5); } + OPPONENT(SPECIES_PONYTA) { Level(1); Moves(MOVE_NONE); Speed(4); } // Forces switchout + OPPONENT(SPECIES_ARON) { Level(30); Moves(MOVE_HEADBUTT); Speed(4); SpDefense(41); } // Mid battle, AI sends out Aron + OPPONENT(SPECIES_ELECTRODE) { Level(30); Moves(MOVE_CHARGE_BEAM); Speed(6); } + } WHEN { + TURN { MOVE(player, MOVE_WING_ATTACK); EXPECT_SWITCH(opponent, aiRiskyFlag? 2 : 1); } + } +} + +AI_SINGLE_BATTLE_TEST("AI_FLAG_RISKY: AI prefers high damage moves at the expense of accuracy regardless of KO thresholds") +{ + u32 aiRiskyFlag = 0; + + PARAMETRIZE{ aiRiskyFlag = 0; } + PARAMETRIZE{ aiRiskyFlag = AI_FLAG_RISKY; } + + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | aiRiskyFlag); + PLAYER(SPECIES_GOLDEEN) { Level(5); Moves(MOVE_TACKLE); } + OPPONENT(SPECIES_CASTFORM) { Level(20); Moves(MOVE_THUNDER, MOVE_THUNDERBOLT); } + } WHEN { + TURN { MOVE(player, MOVE_TACKLE); EXPECT_MOVE(opponent, aiRiskyFlag ? MOVE_THUNDER : MOVE_THUNDERBOLT); } + } +}