From d1ca1f667f6fb4623197151a0565f7c56357d123 Mon Sep 17 00:00:00 2001 From: Pawkkie <61265402+Pawkkie@users.noreply.github.com> Date: Fri, 28 Jun 2024 03:04:24 -0400 Subject: [PATCH] Smarter Choice AI for Status Moves (#4872) * Smarter choice item usage * Clarify test name / line ending * Review feedback * Review feedback pt. 2 --- src/battle_ai_main.c | 16 ++++ src/battle_ai_switch_items.c | 26 ++++- test/battle/ai_choice.c | 181 +++++++++++++++++++++++++++++++++++ 3 files changed, 221 insertions(+), 2 deletions(-) create mode 100644 test/battle/ai_choice.c diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 136dd27967..53a62a08bd 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -2654,6 +2654,22 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) return 0; // cannot even select } // move effect checks + // Choice items + if (HOLD_EFFECT_CHOICE(aiData->holdEffects[battlerAtk]) && gBattleMons[battlerAtk].ability != ABILITY_KLUTZ) + { + // Don't use user-target moves ie. Swords Dance, with exceptions + if ((moveTarget & MOVE_TARGET_USER) + && moveEffect != EFFECT_DESTINY_BOND && moveEffect != EFFECT_WISH && moveEffect != EFFECT_HEALING_WISH + && !(moveEffect == EFFECT_AURORA_VEIL && (AI_GetWeather(aiData) & (B_WEATHER_SNOW | B_WEATHER_HAIL)))) + ADJUST_SCORE(-30); + // Don't use a status move if the mon is the last one in the party, has no good switchin, or is trapped + else if (GetBattleMoveCategory(move) == DAMAGE_CATEGORY_STATUS + && (CountUsablePartyMons(battlerAtk) < 1 + || AI_DATA->mostSuitableMonId[battlerAtk] == PARTY_SIZE + || IsBattlerTrapped(battlerAtk, TRUE))) + ADJUST_SCORE(-30); + } + if (score < 0) score = 0; diff --git a/src/battle_ai_switch_items.c b/src/battle_ai_switch_items.c index f67172fcb2..1300edbd46 100644 --- a/src/battle_ai_switch_items.c +++ b/src/battle_ai_switch_items.c @@ -919,6 +919,24 @@ static bool32 ShouldSwitchIfEncored(u32 battler, bool32 emitResult) return FALSE; } +static bool32 ShouldSwitchIfBadChoiceLock(u32 battler, bool32 emitResult) +{ + u32 holdEffect = GetBattlerHoldEffect(battler, FALSE); + + if (HOLD_EFFECT_CHOICE(holdEffect) && gBattleMons[battler].ability != ABILITY_KLUTZ) + { + if (gMovesInfo[gLastUsedMove].category == DAMAGE_CATEGORY_STATUS) + { + gBattleStruct->AI_monToSwitchIntoId[battler] = PARTY_SIZE; + if (emitResult) + BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); + return TRUE; + } + } + + return FALSE; +} + // AI should switch if it's become setup fodder and has something better to switch to static bool32 AreAttackingStatsLowered(u32 battler, bool32 emitResult) { @@ -941,7 +959,8 @@ static bool32 AreAttackingStatsLowered(u32 battler, bool32 emitResult) if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (Random() & 1)) { gBattleStruct->AI_monToSwitchIntoId[battler] = PARTY_SIZE; - BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); + if (emitResult) + BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); return TRUE; } } @@ -949,7 +968,8 @@ static bool32 AreAttackingStatsLowered(u32 battler, bool32 emitResult) else if (attackingStage < DEFAULT_STAT_STAGE - 2) { gBattleStruct->AI_monToSwitchIntoId[battler] = PARTY_SIZE; - BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); + if (emitResult) + BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0); return TRUE; } } @@ -1076,6 +1096,8 @@ bool32 ShouldSwitch(u32 battler, bool32 emitResult) return TRUE; if (ShouldSwitchIfEncored(battler, emitResult)) return TRUE; + if (ShouldSwitchIfBadChoiceLock(battler, emitResult)) + return TRUE; if (AreAttackingStatsLowered(battler, emitResult)) return TRUE; diff --git a/test/battle/ai_choice.c b/test/battle/ai_choice.c new file mode 100644 index 0000000000..a589a24d7c --- /dev/null +++ b/test/battle/ai_choice.c @@ -0,0 +1,181 @@ +#include "global.h" +#include "test/battle.h" + +ASSUMPTIONS +{ + ASSUME(gItemsInfo[ITEM_CHOICE_SPECS].holdEffect == HOLD_EFFECT_CHOICE_SPECS); + ASSUME(gItemsInfo[ITEM_CHOICE_BAND].holdEffect == HOLD_EFFECT_CHOICE_BAND); + ASSUME(gItemsInfo[ITEM_CHOICE_SCARF].holdEffect == HOLD_EFFECT_CHOICE_SCARF); +} + +AI_SINGLE_BATTLE_TEST("Choiced Pokémon switch out after using a status move once") +{ + u32 j, ability = ABILITY_NONE, heldItem = ITEM_NONE; + + static const u32 choiceItems[] = { + ITEM_CHOICE_SPECS, + ITEM_CHOICE_BAND, + ITEM_CHOICE_SCARF, + }; + + for (j = 0; j < ARRAY_COUNT(choiceItems); j++) + { + PARAMETRIZE{ ability = ABILITY_NONE; heldItem = choiceItems[j]; } + PARAMETRIZE{ ability = ABILITY_KLUTZ; heldItem = choiceItems[j]; } + } + + GIVEN { + ASSUME(gMovesInfo[MOVE_YAWN].category == DAMAGE_CATEGORY_STATUS); + ASSUME(gMovesInfo[MOVE_YAWN].effect == EFFECT_YAWN); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_RHYDON) + OPPONENT(SPECIES_LOPUNNY) { Moves(MOVE_YAWN, MOVE_TACKLE); Item(heldItem); Ability(ability); } + OPPONENT(SPECIES_SWAMPERT) { Moves(MOVE_WATERFALL); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_YAWN); } + if (ability == ABILITY_KLUTZ) { // Klutz ignores item + TURN { EXPECT_MOVE(opponent, MOVE_TACKLE); } + } + else { + TURN { EXPECT_SWITCH(opponent, 1); } + } + } +} + +AI_SINGLE_BATTLE_TEST("Choiced Pokémon won't use stat boosting moves") +{ + // Moves defined by MOVE_TARGET_USER (with exceptions?) + u32 j, ability = ABILITY_NONE, heldItem = ITEM_NONE; + + static const u32 choiceItems[] = { + ITEM_CHOICE_SPECS, + ITEM_CHOICE_BAND, + ITEM_CHOICE_SCARF, + }; + + for (j = 0; j < ARRAY_COUNT(choiceItems); j++) + { + PARAMETRIZE{ ability = ABILITY_NONE; heldItem = choiceItems[j]; } + PARAMETRIZE{ ability = ABILITY_KLUTZ; heldItem = choiceItems[j]; } + } + + GIVEN { + ASSUME(gMovesInfo[MOVE_SWORDS_DANCE].target == MOVE_TARGET_USER); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_RHYDON) + OPPONENT(SPECIES_LOPUNNY) { Moves(MOVE_SWORDS_DANCE, MOVE_TACKLE); Item(heldItem); Ability(ability); } + OPPONENT(SPECIES_SWAMPERT) { Moves(MOVE_WATERFALL); } + } WHEN { + if (ability == ABILITY_KLUTZ) { // Klutz ignores item + TURN { EXPECT_MOVE(opponent, MOVE_SWORDS_DANCE); } + } + else { + TURN { EXPECT_MOVE(opponent, MOVE_TACKLE); } + } + } +} + +AI_SINGLE_BATTLE_TEST("Choiced Pokémon won't use status move if they are the only party member") +{ + u32 j, ability = ABILITY_NONE, isAlive = 0, heldItem = ITEM_NONE; + + static const u32 choiceItems[] = { + ITEM_CHOICE_SPECS, + ITEM_CHOICE_BAND, + ITEM_CHOICE_SCARF, + }; + + for (j = 0; j < ARRAY_COUNT(choiceItems); j++) + { + PARAMETRIZE{ ability = ABILITY_NONE; heldItem = choiceItems[j]; isAlive = 0; } + PARAMETRIZE{ ability = ABILITY_KLUTZ; heldItem = choiceItems[j]; isAlive = 0; } + PARAMETRIZE{ ability = ABILITY_NONE; heldItem = choiceItems[j]; isAlive = 1; } + PARAMETRIZE{ ability = ABILITY_KLUTZ; heldItem = choiceItems[j]; isAlive = 1; } + } + + GIVEN { + ASSUME(gMovesInfo[MOVE_YAWN].category == DAMAGE_CATEGORY_STATUS); + ASSUME(gMovesInfo[MOVE_YAWN].effect == EFFECT_YAWN); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_RHYDON) + OPPONENT(SPECIES_LOPUNNY) { Moves(MOVE_YAWN, MOVE_TACKLE); Item(heldItem); Ability(ability); } + OPPONENT(SPECIES_SWAMPERT) { HP(isAlive); Moves(MOVE_WATERFALL); } + } WHEN { + if (isAlive == 1 || ability == ABILITY_KLUTZ) { + TURN { EXPECT_MOVE(opponent, MOVE_YAWN); } + } + else { + TURN { EXPECT_MOVE(opponent, MOVE_TACKLE); } + } + } +} + +AI_SINGLE_BATTLE_TEST("Choiced Pokémon won't use status move if they don't have a good switchin") +{ + u32 j, ability = ABILITY_NONE, move = MOVE_NONE, species = SPECIES_NONE, heldItem = ITEM_NONE; + + static const u32 choiceItems[] = { + ITEM_CHOICE_SPECS, + ITEM_CHOICE_BAND, + ITEM_CHOICE_SCARF, + }; + + for (j = 0; j < ARRAY_COUNT(choiceItems); j++) + { + PARAMETRIZE{ ability = ABILITY_NONE; heldItem = choiceItems[j]; species = SPECIES_SWAMPERT; move = MOVE_WATERFALL; } + PARAMETRIZE{ ability = ABILITY_KLUTZ; heldItem = choiceItems[j]; species = SPECIES_SWAMPERT; move = MOVE_WATERFALL; } + PARAMETRIZE{ ability = ABILITY_NONE; heldItem = choiceItems[j]; species = SPECIES_ELEKID; move = MOVE_THUNDER_WAVE; } + PARAMETRIZE{ ability = ABILITY_KLUTZ; heldItem = choiceItems[j]; species = SPECIES_ELEKID; move = MOVE_THUNDER_WAVE; } + } + + GIVEN { + ASSUME(gMovesInfo[MOVE_YAWN].category == DAMAGE_CATEGORY_STATUS); + ASSUME(gMovesInfo[MOVE_YAWN].effect == EFFECT_YAWN); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(SPECIES_RHYDON) + OPPONENT(SPECIES_LOPUNNY) { Moves(MOVE_YAWN, MOVE_TACKLE); Item(heldItem); Ability(ability); } + OPPONENT(species) { Moves(move); } + } WHEN { + if (species == SPECIES_SWAMPERT || ability == ABILITY_KLUTZ) { + TURN { EXPECT_MOVE(opponent, MOVE_YAWN); } + } + else { + TURN { EXPECT_MOVE(opponent, MOVE_TACKLE); } + } + } +} + +AI_SINGLE_BATTLE_TEST("Choiced Pokémon won't use status move if they are trapped") +{ + u32 j, aiAbility = ABILITY_NONE, playerAbility = MOVE_NONE, species = SPECIES_NONE, heldItem = ITEM_NONE; + + static const u32 choiceItems[] = { + ITEM_CHOICE_SPECS, + ITEM_CHOICE_BAND, + ITEM_CHOICE_SCARF, + }; + + for (j = 0; j < ARRAY_COUNT(choiceItems); j++) + { + PARAMETRIZE{ aiAbility = ABILITY_NONE; heldItem = choiceItems[j]; species = SPECIES_RHYDON; playerAbility = ABILITY_LIGHTNING_ROD; } + PARAMETRIZE{ aiAbility = ABILITY_KLUTZ; heldItem = choiceItems[j]; species = SPECIES_RHYDON; playerAbility = ABILITY_LIGHTNING_ROD; } + PARAMETRIZE{ aiAbility = ABILITY_NONE; heldItem = choiceItems[j]; species = SPECIES_DUGTRIO; playerAbility = ABILITY_ARENA_TRAP; } + PARAMETRIZE{ aiAbility = ABILITY_KLUTZ; heldItem = choiceItems[j]; species = SPECIES_DUGTRIO; playerAbility = ABILITY_ARENA_TRAP; } + } + + GIVEN { + ASSUME(gMovesInfo[MOVE_YAWN].category == DAMAGE_CATEGORY_STATUS); + ASSUME(gMovesInfo[MOVE_YAWN].effect == EFFECT_YAWN); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT); + PLAYER(species) { Ability(playerAbility); } + OPPONENT(SPECIES_LOPUNNY) { Moves(MOVE_YAWN, MOVE_TACKLE); Item(heldItem); Ability(aiAbility); } + OPPONENT(SPECIES_SWAMPERT) { Moves(MOVE_WATERFALL); } + } WHEN { + if (playerAbility != ABILITY_ARENA_TRAP || aiAbility == ABILITY_KLUTZ) { + TURN { EXPECT_MOVE(opponent, MOVE_YAWN); } + } + else { + TURN { EXPECT_MOVE(opponent, MOVE_TACKLE); } + } + } +}