AI Tests: Gimmick Support (#7694)

This commit is contained in:
Alex 2025-09-05 22:40:19 +02:00 committed by GitHub
commit a6ffd07d1c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 137 additions and 32 deletions

View File

@ -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

View File

@ -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);

View File

@ -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;

View File

@ -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,24 +49,23 @@ 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!");
}
}
// 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 terastilized 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!");
}
}

View File

@ -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); }
}
}

View File

@ -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);
}
}

View File

@ -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); }
}
}

View File

@ -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;