From 97376a5b5a8ca11a0ec0523c49e5d1e56bdfcd16 Mon Sep 17 00:00:00 2001 From: Martin Griffin Date: Thu, 4 Sep 2025 20:41:00 +0100 Subject: [PATCH] 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;