From 97376a5b5a8ca11a0ec0523c49e5d1e56bdfcd16 Mon Sep 17 00:00:00 2001 From: Martin Griffin Date: Thu, 4 Sep 2025 20:41:00 +0100 Subject: [PATCH 1/2] Support gimmicks in AI tests --- include/test/battle.h | 3 ++- include/test_runner.h | 4 ++- src/battle_main.c | 5 +++- test/battle/ai/ai_smart_tera.c | 13 +++++----- test/test_runner_battle.c | 47 +++++++++++++++++++++++++++++----- 5 files changed, 55 insertions(+), 17 deletions(-) diff --git a/include/test/battle.h b/include/test/battle.h index d909aa0332..28547f2614 100644 --- a/include/test/battle.h +++ b/include/test/battle.h @@ -638,7 +638,8 @@ struct BattlerTurn struct ExpectedAIAction { - u16 sourceLine; + u16 sourceLine:13; // TODO: Avoid stealing these bits. + enum Gimmick gimmick:3; u8 type:4; // which action u8 moveSlots:4; // Expected move(s) to be chosen or not, marked as bits. u8 target:4; // move target or id of mon which gets sent out diff --git a/include/test_runner.h b/include/test_runner.h index 9e0d96ff5b..3b11568f60 100644 --- a/include/test_runner.h +++ b/include/test_runner.h @@ -7,6 +7,8 @@ extern const bool8 gTestRunnerSkipIsFail; #if TESTING +enum Gimmick; + void TestRunner_Battle_RecordAbilityPopUp(u32 battlerId, u32 ability); void TestRunner_Battle_RecordAnimation(u32 animType, u32 animId); void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP); @@ -14,7 +16,7 @@ void TestRunner_Battle_RecordExp(u32 battlerId, u32 oldExp, u32 newExp); void TestRunner_Battle_RecordMessage(const u8 *message); void TestRunner_Battle_RecordStatus1(u32 battlerId, u32 status1); void TestRunner_Battle_AfterLastTurn(void); -void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target); +void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target, enum Gimmick gimmick); void TestRunner_Battle_CheckSwitch(u32 battlerId, u32 partyIndex); void TestRunner_Battle_CheckAiMoveScores(u32 battlerId); void TestRunner_Battle_AISetScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score); diff --git a/src/battle_main.c b/src/battle_main.c index 796582aedd..488a1e81f8 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -4502,7 +4502,10 @@ static void HandleTurnActionSelectionState(void) if (gTestRunnerEnabled) { - TestRunner_Battle_CheckChosenMove(battler, gChosenMoveByBattler[battler], gBattleStruct->moveTarget[battler]); + UNUSED enum Gimmick gimmick = GIMMICK_NONE; + if (gBattleResources->bufferB[battler][2] & RET_GIMMICK) + gimmick = gBattleStruct->gimmick.usableGimmick[battler]; + TestRunner_Battle_CheckChosenMove(battler, gChosenMoveByBattler[battler], gBattleStruct->moveTarget[battler], gimmick); } } break; diff --git a/test/battle/ai/ai_smart_tera.c b/test/battle/ai/ai_smart_tera.c index 550ba7d2a1..dadc910fdb 100644 --- a/test/battle/ai/ai_smart_tera.c +++ b/test/battle/ai/ai_smart_tera.c @@ -4,7 +4,6 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will tera if it enables a ko") { - KNOWN_FAILING; // Tests don't currently give the AI the capability to tera, even with a tera type set. GIVEN { ASSUME(GetMovePower(MOVE_SEED_BOMB) == 80); ASSUME(GetMovePower(MOVE_AQUA_TAIL) == 90); @@ -14,9 +13,9 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will tera if it enables a ko") OPPONENT(SPECIES_WOBBUFFET) { Speed(1); Moves(MOVE_AQUA_TAIL, MOVE_SEED_BOMB); TeraType(TYPE_GRASS); } OPPONENT(SPECIES_WOBBUFFET) { HP(1); Speed(100); TeraType(TYPE_FIRE); } } WHEN { - TURN { EXPECT_MOVE(opponent, MOVE_SEED_BOMB); } + TURN { EXPECT_MOVE(opponent, MOVE_SEED_BOMB, gimmick: GIMMICK_TERA); SEND_OUT(player, 1); } } SCENE { - MESSAGE("The opposing Wobbuffet terastilized into the Grass type!"); + MESSAGE("The opposing Wobbuffet terastallized into the Grass type!"); MESSAGE("Wobbuffet fainted!"); } } @@ -34,7 +33,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will not tera if it gets outsped a } WHEN { TURN { } } SCENE { - NOT MESSAGE("The opposing Wobbuffet terastilized into the Grass type!"); + NOT MESSAGE("The opposing Wobbuffet terastallized into the Grass type!"); } } @@ -50,7 +49,7 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will not tera if it gets ko'd by p } WHEN { TURN { } } SCENE { - NOT MESSAGE("The opposing Wobbuffet terastilized into the Grass type!"); + NOT MESSAGE("The opposing Wobbuffet terastallized into the Grass type!"); } } @@ -68,6 +67,6 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will not tera if it gets ko'd by p // } WHEN { // TURN { MOVE(player, MOVE_SEED_BOMB); } // } SCENE { -// MESSAGE("The opposing Wobbuffet terastilized into the Fire type!"); +// MESSAGE("The opposing Wobbuffet terastallized into the Fire type!"); // } -// } \ No newline at end of file +// } diff --git a/test/test_runner_battle.c b/test/test_runner_battle.c index a0f3e0e953..ba1c0ba109 100644 --- a/test/test_runner_battle.c +++ b/test/test_runner_battle.c @@ -765,6 +765,16 @@ static const char *const sBattleActionNames[] = [B_ACTION_SWITCH] = "SWITCH", }; +static const char *const sGimmickIdentifiers[GIMMICKS_COUNT] = +{ + [GIMMICK_NONE] = "N/A", + [GIMMICK_MEGA] = "Mega Evolution", + [GIMMICK_ULTRA_BURST] = "Ultra Burst", + [GIMMICK_Z_MOVE] = "Z-Move", + [GIMMICK_DYNAMAX] = "Dynamax", + [GIMMICK_TERA] = "Terastallize", +}; + static u32 CountAiExpectMoves(struct ExpectedAIAction *expectedAction, u32 battlerId, bool32 printLog) { u32 i, countExpected = 0; @@ -780,7 +790,7 @@ static u32 CountAiExpectMoves(struct ExpectedAIAction *expectedAction, u32 battl return countExpected; } -void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target) +void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target, enum Gimmick gimmick) { const char *filename = gTestRunnerState.test->filename; u32 id = DATA.trial.aiActionsPlayed[battlerId]; @@ -802,6 +812,9 @@ void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target) if (expectedAction->explicitTarget && expectedAction->target != target) Test_ExitWithResult(TEST_RESULT_FAIL, SourceLine(0), ":L%s:%d: Expected target %s, got %s", filename, expectedAction->sourceLine, BattlerIdentifier(expectedAction->target), BattlerIdentifier(target)); + if (expectedAction->gimmick != GIMMICKS_COUNT && expectedAction->gimmick != gimmick) + Test_ExitWithResult(TEST_RESULT_FAIL, SourceLine(0), ":L%s:%d: Expected gimmick %s, got %s", filename, expectedAction->sourceLine, sGimmickIdentifiers[expectedAction->gimmick], sGimmickIdentifiers[gimmick]); + for (i = 0; i < MAX_MON_MOVES; i++) { if ((1u << i) & expectedAction->moveSlots) @@ -1616,6 +1629,17 @@ void ClosePokemon(u32 sourceLine) DATA.currentMon = NULL; } +static void SetGimmick(u32 sourceLine, u32 side, u32 partyIndex, enum Gimmick gimmick) +{ + enum Gimmick currentGimmick = DATA.chosenGimmick[side][partyIndex]; + if (!((currentGimmick == GIMMICK_ULTRA_BURST && gimmick == GIMMICK_Z_MOVE) + || (currentGimmick == GIMMICK_Z_MOVE && gimmick == GIMMICK_ULTRA_BURST))) + { + INVALID_IF(currentGimmick != GIMMICK_NONE && currentGimmick != gimmick, "Cannot set %s because %s already set", sGimmickIdentifiers[gimmick], sGimmickIdentifiers[currentGimmick]); + } + DATA.chosenGimmick[side][partyIndex] = gimmick; +} + void Gender_(u32 sourceLine, u32 gender) { const struct SpeciesInfo *info; @@ -1778,6 +1802,15 @@ void Item_(u32 sourceLine, u32 item) INVALID_IF(!DATA.currentMon, "Item outside of PLAYER/OPPONENT"); INVALID_IF(item >= ITEMS_COUNT, "Illegal item: %d", item); SetMonData(DATA.currentMon, MON_DATA_HELD_ITEM, &item); + switch (GetItemHoldEffect(item)) + { + case HOLD_EFFECT_MEGA_STONE: + SetGimmick(sourceLine, DATA.currentSide, DATA.currentPartyIndex, GIMMICK_MEGA); + break; + case HOLD_EFFECT_Z_CRYSTAL: + SetGimmick(sourceLine, DATA.currentSide, DATA.currentPartyIndex, GIMMICK_Z_MOVE); + break; + } } void Moves_(u32 sourceLine, u16 moves[MAX_MON_MOVES]) @@ -1834,18 +1867,21 @@ void DynamaxLevel_(u32 sourceLine, u32 dynamaxLevel) { INVALID_IF(!DATA.currentMon, "DynamaxLevel outside of PLAYER/OPPONENT"); SetMonData(DATA.currentMon, MON_DATA_DYNAMAX_LEVEL, &dynamaxLevel); + SetGimmick(sourceLine, DATA.currentSide, DATA.currentPartyIndex, GIMMICK_DYNAMAX); } void GigantamaxFactor_(u32 sourceLine, bool32 gigantamaxFactor) { INVALID_IF(!DATA.currentMon, "GigantamaxFactor outside of PLAYER/OPPONENT"); SetMonData(DATA.currentMon, MON_DATA_GIGANTAMAX_FACTOR, &gigantamaxFactor); + SetGimmick(sourceLine, DATA.currentSide, DATA.currentPartyIndex, GIMMICK_DYNAMAX); } void TeraType_(u32 sourceLine, u32 teraType) { INVALID_IF(!DATA.currentMon, "TeraType outside of PLAYER/OPPONENT"); SetMonData(DATA.currentMon, MON_DATA_TERA_TYPE, &teraType); + SetGimmick(sourceLine, DATA.currentSide, DATA.currentPartyIndex, GIMMICK_TERA); } void Shadow_(u32 sourceLine, bool32 isShadow) @@ -2146,11 +2182,7 @@ void MoveGetIdAndSlot(s32 battlerId, struct MoveContext *ctx, u32 *moveId, u32 * INVALID_IF(ctx->gimmick != GIMMICK_Z_MOVE && ctx->gimmick != GIMMICK_ULTRA_BURST && holdEffect == HOLD_EFFECT_Z_CRYSTAL, "Cannot use another gimmick while holding a Z-Crystal"); // Check multiple gimmick use. - INVALID_IF(DATA.chosenGimmick[side][DATA.currentMonIndexes[battlerId]] != GIMMICK_NONE - && !(DATA.chosenGimmick[side][DATA.currentMonIndexes[battlerId]] == GIMMICK_ULTRA_BURST - && ctx->gimmick == GIMMICK_Z_MOVE), "Cannot use multiple gimmicks on the same battler"); - - DATA.chosenGimmick[side][DATA.currentMonIndexes[battlerId]] = ctx->gimmick; + SetGimmick(sourceLine, side, DATA.currentMonIndexes[battlerId], ctx->gimmick); *moveSlot |= RET_GIMMICK; } } @@ -2280,11 +2312,12 @@ static void TryMarkExpectMove(u32 sourceLine, struct BattlePokemon *battler, str id = DATA.expectedAiActionIndex[battlerId]; DATA.expectedAiActions[battlerId][id].type = B_ACTION_USE_MOVE; - DATA.expectedAiActions[battlerId][id].moveSlots |= 1 << moveSlot; + DATA.expectedAiActions[battlerId][id].moveSlots |= 1 << (moveSlot & ~RET_GIMMICK); DATA.expectedAiActions[battlerId][id].target = target; DATA.expectedAiActions[battlerId][id].explicitTarget = ctx->explicitTarget; DATA.expectedAiActions[battlerId][id].sourceLine = sourceLine; DATA.expectedAiActions[battlerId][id].actionSet = TRUE; + DATA.expectedAiActions[battlerId][id].gimmick = ctx->explicitGimmick ? ctx->gimmick : GIMMICKS_COUNT; if (ctx->explicitNotExpected) DATA.expectedAiActions[battlerId][id].notMove = ctx->notExpected; From 9e56a259e6b08627f4389becbdff2f65e61578b2 Mon Sep 17 00:00:00 2001 From: surskitty Date: Thu, 4 Sep 2025 17:08:30 -0400 Subject: [PATCH 2/2] Simple gimmick tests --- test/battle/ai/ai_smart_tera.c | 33 +++++++++++++++---------------- test/battle/ai/gimmick_dynamax.c | 15 ++++++++++++++ test/battle/ai/gimmick_mega.c | 19 ++++++++++++++++++ test/battle/ai/gimmick_z_move.c | 34 ++++++++++++++++++++++++++++++++ 4 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 test/battle/ai/gimmick_dynamax.c create mode 100644 test/battle/ai/gimmick_mega.c create mode 100644 test/battle/ai/gimmick_z_move.c diff --git a/test/battle/ai/ai_smart_tera.c b/test/battle/ai/ai_smart_tera.c index dadc910fdb..7bed476b43 100644 --- a/test/battle/ai/ai_smart_tera.c +++ b/test/battle/ai/ai_smart_tera.c @@ -53,20 +53,19 @@ AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI will not tera if it gets ko'd by p } } -// AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI might tera if it gets saved from a ko") -// { -// KNOWN_FAILING; // Tests don't currently give the AI the capability to tera, even with a tera type set. -// PASSES_RANDOMLY(90, 100, RNG_AI_CONSERVE_TERA); -// GIVEN { -// ASSUME(GetMovePower(MOVE_SEED_BOMB) == 80); -// AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_TERA | AI_FLAG_OMNISCIENT ); -// PLAYER(SPECIES_WOBBUFFET) { HP(47); Speed(100); Moves(MOVE_SEED_BOMB); } -// PLAYER(SPECIES_WOBBUFFET) { Speed(100); } -// OPPONENT(SPECIES_WOBBUFFET) { Speed(100); HP(30); Moves(MOVE_SEED_BOMB); TeraType(TYPE_FIRE); } -// OPPONENT(SPECIES_WOBBUFFET) { Speed(100); TeraType(TYPE_FIRE); } -// } WHEN { -// TURN { MOVE(player, MOVE_SEED_BOMB); } -// } SCENE { -// MESSAGE("The opposing Wobbuffet terastallized into the Fire type!"); -// } -// } +AI_SINGLE_BATTLE_TEST("AI_FLAG_SMART_TERA: AI might tera if it gets saved from a ko") +{ + PASSES_RANDOMLY(90, 100, RNG_AI_CONSERVE_TERA); + GIVEN { + ASSUME(GetMovePower(MOVE_SEED_BOMB) == 80); + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_TERA | AI_FLAG_OMNISCIENT ); + PLAYER(SPECIES_WOBBUFFET) { HP(47); Speed(100); Moves(MOVE_SEED_BOMB); } + PLAYER(SPECIES_WOBBUFFET) { Speed(100); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(100); HP(30); Moves(MOVE_SEED_BOMB); TeraType(TYPE_FIRE); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(100); TeraType(TYPE_FIRE); } + } WHEN { + TURN { MOVE(player, MOVE_SEED_BOMB); } + } SCENE { + MESSAGE("The opposing Wobbuffet terastallized into the Fire type!"); + } +} diff --git a/test/battle/ai/gimmick_dynamax.c b/test/battle/ai/gimmick_dynamax.c new file mode 100644 index 0000000000..1f97e490b9 --- /dev/null +++ b/test/battle/ai/gimmick_dynamax.c @@ -0,0 +1,15 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +AI_SINGLE_BATTLE_TEST("AI uses Dynamax") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_SCRATCH); DynamaxLevel(10); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_SCRATCH, gimmick: GIMMICK_DYNAMAX); } + } +} + diff --git a/test/battle/ai/gimmick_mega.c b/test/battle/ai/gimmick_mega.c new file mode 100644 index 0000000000..ef94223122 --- /dev/null +++ b/test/battle/ai/gimmick_mega.c @@ -0,0 +1,19 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" + +AI_SINGLE_BATTLE_TEST("AI uses Mega Evolution") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_VENUSAUR) { Item(ITEM_VENUSAURITE); Moves(MOVE_SLUDGE_BOMB); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_SLUDGE_BOMB, gimmick: GIMMICK_MEGA); } + } SCENE { + ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_MEGA_EVOLUTION, opponent); + } THEN { + EXPECT_EQ(opponent->species, SPECIES_VENUSAUR_MEGA); + } +} + diff --git a/test/battle/ai/gimmick_z_move.c b/test/battle/ai/gimmick_z_move.c new file mode 100644 index 0000000000..bf6b3cc3ec --- /dev/null +++ b/test/battle/ai/gimmick_z_move.c @@ -0,0 +1,34 @@ +#include "global.h" +#include "test/battle.h" +#include "battle_ai_util.h" +#include "constants/battle_z_move_effects.h" + +AI_SINGLE_BATTLE_TEST("AI uses Z-moves.") +{ + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + ASSUME(GetMoveType(MOVE_QUICK_ATTACK) == TYPE_NORMAL); + PLAYER(SPECIES_WOBBUFFET); + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); Moves(MOVE_QUICK_ATTACK); } + } WHEN { + TURN { EXPECT_MOVE(opponent, MOVE_QUICK_ATTACK, gimmick: GIMMICK_Z_MOVE); } + } +} + +AI_SINGLE_BATTLE_TEST("AI does not use damaging Z-moves if the player would faint anyway.") +{ + u32 currentHP; + PARAMETRIZE { currentHP = 1; } + PARAMETRIZE { currentHP = 500; } + GIVEN { + AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_OMNISCIENT ); + ASSUME(GetMoveType(MOVE_QUICK_ATTACK) == TYPE_NORMAL); + PLAYER(SPECIES_WOBBUFFET) { HP(currentHP); } + OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_NORMALIUM_Z); Moves(MOVE_QUICK_ATTACK); } + } WHEN { + if (currentHP != 1) + TURN { EXPECT_MOVE(opponent, MOVE_QUICK_ATTACK, gimmick: GIMMICK_Z_MOVE); } + else + TURN { EXPECT_MOVE(opponent, MOVE_QUICK_ATTACK, gimmick: GIMMICK_NONE); } + } +}