AI battle tests + AI logic fixes (#3361)

This commit is contained in:
DizzyEggg 2023-10-04 19:53:29 +02:00 committed by GitHub
parent ef1073ddee
commit be5683e899
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 2116 additions and 1011 deletions

View File

@ -1,21 +1,38 @@
#ifndef GUARD_BATTLE_AI_MAIN_H
#define GUARD_BATTLE_AI_MAIN_H
// return values for BattleAI_ChooseMoveOrAction
// return vals for BattleAI_ChooseMoveOrAction
// 0 - 3 are move idx
#define AI_CHOICE_FLEE 4
#define AI_CHOICE_WATCH 5
#define AI_CHOICE_SWITCH 7
#include "test_runner.h"
// Logs for debugging AI tests.
#define SET_SCORE(battler, movesetIndex, val) \
do \
{ \
TestRunner_Battle_AISetScore(__FILE__, __LINE__, battler, movesetIndex, val); \
AI_THINKING_STRUCT->score[movesetIndex] = val; \
} while (0) \
#define ADJUST_SCORE(val) \
do \
{ \
TestRunner_Battle_AIAdjustScore(__FILE__, __LINE__, sBattler_AI, AI_THINKING_STRUCT->movesetIndex, val); \
score += val; \
} while (0) \
#define RETURN_SCORE_PLUS(val) \
{ \
score += val; \
ADJUST_SCORE(val); \
return score; \
}
#define RETURN_SCORE_MINUS(val) \
{ \
score -= val; \
ADJUST_SCORE(-val); \
return score; \
}

View File

@ -36,7 +36,6 @@ bool32 CanTargetMoveFaintAi(u32 move, u32 battlerDef, u32 battlerAtk, u32 nHits)
bool32 CanTargetFaintAiWithMod(u32 battlerDef, u32 battlerAtk, s32 hpMod, s32 dmgMod);
s32 AI_DecideKnownAbilityForTurn(u32 battlerId);
u32 AI_DecideHoldEffectForTurn(u32 battlerId);
u32 AI_GetMoveAccuracy(u32 battlerAtk, u32 battlerDef, u32 move);
bool32 DoesBattlerIgnoreAbilityChecks(u32 atkAbility, u32 move);
u32 AI_GetWeather(struct AiLogicData *aiData);
bool32 CanAIFaintTarget(u32 battlerAtk, u32 battlerDef, u32 numHits);
@ -85,7 +84,7 @@ bool32 ShouldLowerEvasion(u32 battlerAtk, u32 battlerDef, u32 defAbility);
// move checks
bool32 IsAffectedByPowder(u32 battler, u32 ability, u32 holdEffect);
bool32 MovesWithSplitUnusable(u32 attacker, u32 target, u32 split);
u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef);
u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef, s32 noOfHitsToKo);
s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower);
s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather);
bool32 AI_IsDamagedByRecoil(u32 battler);
@ -169,7 +168,7 @@ bool32 IsTargetingPartner(u32 battlerAtk, u32 battlerDef);
bool32 DoesPartnerHaveSameMoveEffect(u32 battlerAtkPartner, u32 battlerDef, u32 move, u32 partnerMove);
bool32 PartnerHasSameMoveEffectWithoutTarget(u32 battlerAtkPartner, u32 move, u32 partnerMove);
bool32 PartnerMoveEffectIsStatusSameTarget(u32 battlerAtkPartner, u32 battlerDef, u32 partnerMove);
bool32 PartnerMoveEffectIsWeather(u32 battlerAtkPartner, u32 partnerMove);
bool32 IsMoveEffectWeather(u32 move);
bool32 PartnerMoveEffectIsTerrain(u32 battlerAtkPartner, u32 partnerMove);
bool32 PartnerMoveIs(u32 battlerAtkPartner, u32 partnerMove, u32 moveCheck);
bool32 PartnerMoveIsSameAsAttacker(u32 battlerAtkPartner, u32 battlerDef, u32 move, u32 partnerMove);

View File

@ -147,6 +147,7 @@ bool32 TryChangeBattleWeather(u32 battler, u32 weatherEnumId, bool32 viaAbility)
u32 AbilityBattleEffects(u32 caseID, u32 battler, u32 ability, u32 special, u32 moveArg);
bool32 TryPrimalReversion(u32 battler);
bool32 IsNeutralizingGasOnField(void);
bool32 IsMoldBreakerTypeAbility(u32 ability);
u32 GetBattlerAbility(u32 battler);
u32 IsAbilityOnSide(u32 battler, u32 ability);
u32 IsAbilityOnOpposingSide(u32 battler, u32 ability);

View File

@ -24,14 +24,6 @@
#define AI_EFFECTIVENESS_x0_125 1
#define AI_EFFECTIVENESS_x0 0
// ai weather
#define AI_WEATHER_NONE 0
#define AI_WEATHER_SUN 1
#define AI_WEATHER_RAIN 2
#define AI_WEATHER_SANDSTORM 3
#define AI_WEATHER_HAIL 4
#define AI_WEATHER_SNOW 5
// get_how_powerful_move_is
#define MOVE_POWER_OTHER 0
#define MOVE_POWER_BEST 1
@ -67,4 +59,6 @@
#define AI_FLAG_SAFARI (1 << 30)
#define AI_FLAG_FIRST_BATTLE (1 << 31)
#define AI_SCORE_DEFAULT 100 // Default score for all AI moves.
#endif // GUARD_CONSTANTS_BATTLE_AI_H

View File

@ -199,6 +199,17 @@
* - Instead of player and opponent there is playerLeft, playerRight,
* opponentLeft, and opponentRight.
*
* AI_SINGLE_BATTLE_TEST(name, results...) and AI_DOUBLE_BATTLE_TEST(name, results...)
* Define battles where opponent mons are controlled by AI, the same that runs
* when battling regular Trainers. The flags for AI should be specified by
* the AI_FLAGS command.
* The rules remain the same as with the SINGLE and DOUBLE battle tests
* with some differences:
* - opponent's action is specified by the EXPECT_MOVE(s) / EXPECT_SEND_OUT / EXPECT_SWITCH commands
* - we don't control what opponent actually does, instead we make sure the opponent does what we expect it to do
* - we still control the player's action the same way
* - apart from the EXPECTED commands, there's also a new SCORE_ and SCORE__VAL commands
*
* KNOWN_FAILING
* Marks a test as not passing due to a bug. If there is an issue number
* associated with the bug it should be included in a comment. If the
@ -289,6 +300,11 @@
* Note if Moves is specified then MOVE will not automatically add moves
* to the moveset.
*
* AI_FLAGS
* Specifies which AI flags are run during the test. Has use only for AI tests.
* The most common combination is AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT)
* which is the general 'smart' AI.
*
* WHEN
* Contains the choices that battlers make during the battle.
*
@ -470,6 +486,7 @@
#include "recorded_battle.h"
#include "util.h"
#include "constants/abilities.h"
#include "constants/battle_ai.h"
#include "constants/battle_anim.h"
#include "constants/battle_move_effects.h"
#include "constants/hold_effects.h"
@ -483,8 +500,9 @@
#define BATTLE_TEST_STACK_SIZE 1024
#define MAX_TURNS 16
#define MAX_QUEUED_EVENTS 25
#define MAX_EXPECTED_ACTIONS 10
enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES, BATTLE_TEST_WILD };
enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES, BATTLE_TEST_WILD, BATTLE_TEST_AI_SINGLES, BATTLE_TEST_AI_DOUBLES };
typedef void (*SingleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *);
typedef void (*DoubleBattleTestFunction)(void *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *);
@ -584,6 +602,42 @@ struct BattlerTurn
struct TurnRNG rng;
};
struct ExpectedAIAction
{
u16 sourceLine;
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
u8 explicitTarget:1; // For double battles, if it's set it requires the move to hit a specific target, otherwise any target is fine.
u8 pass:1; // No matter what AI does, it always passes.
u8 notMove:1; // We're expecting AI to choose any move EXCEPT the specified one.
u8 actionSet:1; // Action was set and is expected to happen. Set only for battlers controlled by AI.
};
#define MAX_AI_SCORE_COMPARISION_PER_TURN 4
#define MAX_AI_LOG_LINES 10
struct ExpectedAiScore
{
// We can compare AI's move score to a value or to another move's score.
u8 moveSlot1:2;
u8 moveSlot2:2;
u8 target:2;
s8 value; // value
u8 cmp:3; // Uses battle script command's CMP_ macros
u8 toValue:1; // compare to value, not to move
u8 set:1;
u16 sourceLine;
};
struct AILogLine
{
const char *file;
u16 line:15;
u16 set:1; // Whether score was set, or added/subtracted
s16 score;
};
struct BattleTestData
{
u8 stack[BATTLE_TEST_STACK_SIZE];
@ -606,6 +660,8 @@ struct BattleTestData
u8 turns;
u8 actionBattlers;
u8 moveBattlers;
bool8 hasAI:1;
bool8 logAI:1;
struct RecordedBattleSave recordedBattle;
u8 battleRecordTypes[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
@ -619,14 +675,20 @@ struct BattleTestData
u8 queueGroupStart;
u8 queuedEvent;
struct QueuedEvent queuedEvents[MAX_QUEUED_EVENTS];
u8 expectedAiActionIndex[MAX_BATTLERS_COUNT];
u8 aiActionsPlayed[MAX_BATTLERS_COUNT];
struct ExpectedAIAction expectedAiActions[MAX_BATTLERS_COUNT][MAX_EXPECTED_ACTIONS];
struct ExpectedAiScore expectedAiScores[MAX_BATTLERS_COUNT][MAX_TURNS][MAX_AI_SCORE_COMPARISION_PER_TURN]; // Max 4 comparisions per turn
struct AILogLine aiLogLines[MAX_BATTLERS_COUNT][MAX_MON_MOVES][MAX_AI_LOG_LINES];
u8 aiLogPrintedForMove[MAX_BATTLERS_COUNT]; // Marks ai score log as printed for move, so the same log isn't displayed multiple times.
};
struct BattleTestRunnerState
{
u8 battlersCount;
u8 parametersCount; // Valid only in BattleTest_Setup.
u8 parameters;
u8 runParameter;
u16 parametersCount; // Valid only in BattleTest_Setup.
u16 parameters;
u16 runParameter;
u16 rngTag;
u16 rngTrialOffset;
u16 trials;
@ -682,7 +744,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState;
TO_DO; \
}
#define SINGLE_BATTLE_TEST(_name, ...) \
#define BATTLE_TEST_ARGS_SINGLE(_name, _type, ...) \
struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
@ -692,7 +754,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState;
.runner = &gBattleTestRunner, \
.data = (void *)&(const struct BattleTest) \
{ \
.type = BATTLE_TEST_SINGLES, \
.type = _type, \
.sourceLine = __LINE__, \
.function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \
.resultsSize = sizeof(struct CAT(Result, __LINE__)), \
@ -700,25 +762,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState;
}; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent)
#define WILD_BATTLE_TEST(_name, ...) \
struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *); \
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
{ \
.name = _name, \
.filename = __FILE__, \
.runner = &gBattleTestRunner, \
.data = (void *)&(const struct BattleTest) \
{ \
.type = BATTLE_TEST_WILD, \
.sourceLine = __LINE__, \
.function = { .singles = (SingleBattleTestFunction)CAT(Test, __LINE__) }, \
.resultsSize = sizeof(struct CAT(Result, __LINE__)), \
}, \
}; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent)
#define DOUBLE_BATTLE_TEST(_name, ...) \
#define BATTLE_TEST_ARGS_DOUBLE(_name, _type, ...) \
struct CAT(Result, __LINE__) { MEMBERS(__VA_ARGS__) }; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *, u32, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *, struct BattlePokemon *); \
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
@ -728,7 +772,7 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState;
.runner = &gBattleTestRunner, \
.data = (void *)&(const struct BattleTest) \
{ \
.type = BATTLE_TEST_DOUBLES, \
.type = _type, \
.sourceLine = __LINE__, \
.function = { .doubles = (DoubleBattleTestFunction)CAT(Test, __LINE__) }, \
.resultsSize = sizeof(struct CAT(Result, __LINE__)), \
@ -736,6 +780,14 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState;
}; \
static void CAT(Test, __LINE__)(struct CAT(Result, __LINE__) *results, u32 i, struct BattlePokemon *playerLeft, struct BattlePokemon *opponentLeft, struct BattlePokemon *playerRight, struct BattlePokemon *opponentRight)
#define SINGLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_SINGLE(_name, BATTLE_TEST_SINGLES, __VA_ARGS__)
#define WILD_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_SINGLE(_name, BATTLE_TEST_WILD, __VA_ARGS__)
#define AI_SINGLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_SINGLE(_name, BATTLE_TEST_AI_SINGLES, __VA_ARGS__)
#define DOUBLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_DOUBLE(_name, BATTLE_TEST_DOUBLES, __VA_ARGS__)
#define AI_DOUBLE_BATTLE_TEST(_name, ...) BATTLE_TEST_ARGS_DOUBLE(_name, BATTLE_TEST_AI_DOUBLES, __VA_ARGS__)
/* Parametrize */
#undef PARAMETRIZE // Override test/test.h's implementation.
@ -763,6 +815,8 @@ struct moveWithPP {
#define GIVEN for (; gBattleTestRunnerState->runGiven; gBattleTestRunnerState->runGiven = FALSE)
#define RNGSeed(seed) RNGSeed_(__LINE__, seed)
#define AI_FLAGS(flags) AIFlags_(__LINE__, flags)
#define AI_LOG AILogScores(__LINE__)
#define PLAYER(species) for (OpenPokemon(__LINE__, B_SIDE_PLAYER, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__))
#define OPPONENT(species) for (OpenPokemon(__LINE__, B_SIDE_OPPONENT, species); gBattleTestRunnerState->data.currentMon; ClosePokemon(__LINE__))
@ -779,7 +833,7 @@ struct moveWithPP {
#define SpDefense(spDefense) SpDefense_(__LINE__, spDefense)
#define Speed(speed) Speed_(__LINE__, speed)
#define Item(item) Item_(__LINE__, item)
#define Moves(move1, ...) Moves_(__LINE__, (const u16 [MAX_MON_MOVES]) { move1, __VA_ARGS__ })
#define Moves(move1, ...) do { u16 moves_[MAX_MON_MOVES] = {move1, __VA_ARGS__}; Moves_(__LINE__, moves_); } while(0)
#define MovesWithPP(movewithpp1, ...) MovesWithPP_(__LINE__, (struct moveWithPP[MAX_MON_MOVES]) {movewithpp1, __VA_ARGS__})
#define Friendship(friendship) Friendship_(__LINE__, friendship)
#define Status1(status1) Status1_(__LINE__, status1)
@ -789,6 +843,8 @@ void OpenPokemon(u32 sourceLine, u32 side, u32 species);
void ClosePokemon(u32 sourceLine);
void RNGSeed_(u32 sourceLine, u32 seed);
void AIFlags_(u32 sourceLine, u32 flags);
void AILogScores(u32 sourceLine);
void Gender_(u32 sourceLine, u32 gender);
void Nature_(u32 sourceLine, u32 nature);
void Ability_(u32 sourceLine, u32 ability);
@ -801,12 +857,28 @@ void SpAttack_(u32 sourceLine, u32 spAttack);
void SpDefense_(u32 sourceLine, u32 spDefense);
void Speed_(u32 sourceLine, u32 speed);
void Item_(u32 sourceLine, u32 item);
void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES]);
void Moves_(u32 sourceLine, u16 moves[MAX_MON_MOVES]);
void MovesWithPP_(u32 sourceLine, struct moveWithPP moveWithPP[MAX_MON_MOVES]);
void Friendship_(u32 sourceLine, u32 friendship);
void Status1_(u32 sourceLine, u32 status1);
void OTName_(u32 sourceLine, const u8 *otName);
// Created for easy use of EXPECT_MOVES, so the user can provide 1, 2, 3 or 4 moves for AI which can pass the test.
struct FourMoves
{
u16 moves[MAX_MON_MOVES];
};
struct TestAIScoreStruct
{
u32 move1;
bool8 explicitMove1;
u32 valueOrMoveId2;
bool8 explicitValueOrMoveId2;
struct BattlePokemon *target;
bool8 explicitTarget;
};
#define PLAYER_PARTY (gBattleTestRunnerState->data.recordedBattle.playerParty)
#define OPPONENT_PARTY (gBattleTestRunnerState->data.recordedBattle.opponentParty)
@ -819,6 +891,22 @@ enum { TURN_CLOSED, TURN_OPEN, TURN_CLOSING };
#define TURN for (OpenTurn(__LINE__); gBattleTestRunnerState->data.turnState == TURN_OPEN; CloseTurn(__LINE__))
#define MOVE(battler, ...) Move(__LINE__, battler, (struct MoveContext) { APPEND_TRUE(__VA_ARGS__) })
#define EXPECT_MOVE(battler, ...) ExpectMove(__LINE__, battler, (struct MoveContext) { APPEND_TRUE(__VA_ARGS__) })
#define NOT_EXPECT_MOVE(battler, _move) ExpectMove(__LINE__, battler, (struct MoveContext) { .move = _move, .explicitMove = TRUE, .notExpected = TRUE, .explicitNotExpected = TRUE, })
#define EXPECT_MOVES(battler, ...) ExpectMoves(__LINE__, battler, FALSE, (struct FourMoves) { __VA_ARGS__ })
#define NOT_EXPECT_MOVES(battler, ...) ExpectMoves(__LINE__, battler, TRUE, (struct FourMoves) { __VA_ARGS__ })
#define EXPECT_SEND_OUT(battler, partyIndex) ExpectSendOut(__LINE__, battler, partyIndex)
#define EXPECT_SWITCH(battler, partyIndex) ExpectSwitch(__LINE__, battler, partyIndex)
#define SCORE_EQ(battler, ...) Score(__LINE__, battler, CMP_EQUAL, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define SCORE_NE(battler, ...) Score(__LINE__, battler, CMP_NOT_EQUAL, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define SCORE_GT(battler, ...) Score(__LINE__, battler, CMP_GREATER_THAN, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define SCORE_LT(battler, ...) Score(__LINE__, battler, CMP_LESS_THAN, FALSE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define SCORE_EQ_VAL(battler, ...) Score(__LINE__, battler, CMP_EQUAL, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define SCORE_NE_VAL(battler, ...) Score(__LINE__, battler, CMP_NOT_EQUAL, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define SCORE_GT_VAL(battler, ...) Score(__LINE__, battler, CMP_GREATER_THAN, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define SCORE_LT_VAL(battler, ...) Score(__LINE__, battler, CMP_LESS_THAN, TRUE, (struct TestAIScoreStruct) { APPEND_TRUE(__VA_ARGS__) } )
#define FORCED_MOVE(battler) ForcedMove(__LINE__, battler)
#define SWITCH(battler, partyIndex) Switch(__LINE__, battler, partyIndex)
#define SKIP_TURN(battler) SkipTurn(__LINE__, battler)
@ -845,6 +933,8 @@ struct MoveContext
// TODO: u8 zMove:1;
u16 allowed:1;
u16 explicitAllowed:1;
u16 notExpected:1; // Has effect only with EXPECT_MOVE
u16 explicitNotExpected:1;
struct BattlePokemon *target;
bool8 explicitTarget;
struct TurnRNG rng;
@ -864,6 +954,11 @@ struct ItemContext
void OpenTurn(u32 sourceLine);
void CloseTurn(u32 sourceLine);
void Move(u32 sourceLine, struct BattlePokemon *, struct MoveContext);
void ExpectMove(u32 sourceLine, struct BattlePokemon *, struct MoveContext);
void ExpectMoves(u32 sourceLine, struct BattlePokemon *battler, bool32 notExpected, struct FourMoves moves);
void ExpectSendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex);
void ExpectSwitch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex);
void Score(u32 sourceLine, struct BattlePokemon *battler, u32 cmp, bool32 toValue, struct TestAIScoreStruct cmpCtx);
void ForcedMove(u32 sourceLine, struct BattlePokemon *);
void Switch(u32 sourceLine, struct BattlePokemon *, u32 partyIndex);
void SkipTurn(u32 sourceLine, struct BattlePokemon *);

View File

@ -14,11 +14,18 @@ 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_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);
void TestRunner_Battle_AIAdjustScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score);
void TestRunner_Battle_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType);
u32 TestRunner_Battle_GetForcedAbility(u32 side, u32 partyIndex);
s32 MgbaPrintf_(const char *fmt, ...);
#else
#define TestRunner_Battle_RecordAbilityPopUp(...) (void)0
@ -28,11 +35,18 @@ u32 TestRunner_Battle_GetForcedAbility(u32 side, u32 partyIndex);
#define TestRunner_Battle_RecordMessage(...) (void)0
#define TestRunner_Battle_RecordStatus1(...) (void)0
#define TestRunner_Battle_AfterLastTurn(...) (void)0
#define TestRunner_Battle_CheckChosenMove(...) (void)0
#define TestRunner_Battle_CheckSwitch(...) (void)0
#define TestRunner_Battle_CheckAiMoveScores(...) (void)0
#define TestRunner_Battle_AISetScore(...) (void)0
#define TestRunner_Battle_AIAdjustScore(...) (void)0
#define TestRunner_Battle_CheckBattleRecordActionType(...) (void)0
#define TestRunner_Battle_GetForcedAbility(...) (u32)0
#define MgbaPrintf_(...) (u32)0
#endif
#endif

File diff suppressed because it is too large Load Diff

View File

@ -744,6 +744,13 @@ s32 AI_CalcDamageSaveBattlers(u32 move, u32 battlerAtk, u32 battlerDef, u8 *type
AI_CalcDamage(move, battlerAtk, battlerDef, typeEffectiveness, considerZPower, AI_GetWeather(AI_DATA));
}
static inline s32 LowestRollDmg(s32 dmg)
{
dmg *= 100 - 15;
dmg /= 100;
return dmg;
}
s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectiveness, bool32 considerZPower, u32 weather)
{
s32 dmg, moveType;
@ -802,11 +809,11 @@ s32 AI_CalcDamage(u32 move, u32 battlerAtk, u32 battlerDef, u8 *typeEffectivenes
aiData->abilities[battlerAtk], aiData->abilities[battlerDef]);
u32 critChance = GetCritHitChance(critChanceIndex);
// With critChance getting closer to 1, dmg gets closer to critDmg.
dmg = (critDmg + normalDmg * (critChance - 1)) / (critChance);
dmg = LowestRollDmg((critDmg + normalDmg * (critChance - 1)) / (critChance));
}
else
{
dmg = normalDmg;
dmg = LowestRollDmg(normalDmg);
}
if (!gBattleStruct->zmove.active)
@ -890,7 +897,7 @@ bool32 AI_IsDamagedByRecoil(u32 battler)
}
// Decide whether move having an additional effect for .
static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move)
static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move, s32 noOfHitsToKo)
{
u32 i;
u32 abilityDef = AI_DATA->abilities[battlerDef];
@ -966,32 +973,32 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move)
return TRUE;
break;
case EFFECT_ATTACK_DOWN_HIT:
if (ShouldLowerStat(battlerDef, abilityDef, STAT_ATK) && abilityDef != ABILITY_HYPER_CUTTER)
if (ShouldLowerStat(battlerDef, abilityDef, STAT_ATK) && abilityDef != ABILITY_HYPER_CUTTER && noOfHitsToKo != 1)
return TRUE;
break;
case EFFECT_DEFENSE_DOWN_HIT:
if (ShouldLowerStat(battlerDef, abilityDef, STAT_DEF))
if (ShouldLowerStat(battlerDef, abilityDef, STAT_DEF) && noOfHitsToKo != 1)
return TRUE;
break;
case EFFECT_SPEED_DOWN_HIT:
if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPEED))
if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPEED) && noOfHitsToKo != 1)
return TRUE;
break;
case EFFECT_SPECIAL_ATTACK_DOWN_HIT:
if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPATK))
if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPATK) && noOfHitsToKo != 1)
return TRUE;
break;
case EFFECT_SPECIAL_DEFENSE_DOWN_HIT:
case EFFECT_SPECIAL_DEFENSE_DOWN_HIT_2:
if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPDEF))
if (ShouldLowerStat(battlerDef, abilityDef, STAT_SPDEF) && noOfHitsToKo != 1)
return TRUE;
break;
case EFFECT_ACCURACY_DOWN_HIT:
if (ShouldLowerStat(battlerDef, abilityDef, STAT_ACC))
if (ShouldLowerStat(battlerDef, abilityDef, STAT_ACC) && noOfHitsToKo != 1)
return TRUE;
break;
case EFFECT_EVASION_DOWN_HIT:
if (ShouldLowerStat(battlerDef, abilityDef, STAT_EVASION))
if (ShouldLowerStat(battlerDef, abilityDef, STAT_EVASION) && noOfHitsToKo != 1)
return TRUE;
break;
case EFFECT_ALL_STATS_UP_HIT:
@ -1006,8 +1013,38 @@ static bool32 AI_IsMoveEffectInPlus(u32 battlerAtk, u32 battlerDef, u32 move)
return FALSE;
}
static bool32 AI_IsMoveEffectInMinus(u32 battlerAtk, u32 battlerDef, u32 move, s32 noOfHitsToKo)
{
u32 abilityAtk = AI_DATA->abilities[battlerAtk];
u32 abilityDef = AI_DATA->abilities[battlerDef];
switch (gBattleMoves[move].effect)
{
case EFFECT_RECHARGE:
return TRUE;
case EFFECT_RECOIL_25:
case EFFECT_RECOIL_IF_MISS:
case EFFECT_RECOIL_50:
case EFFECT_RECOIL_33:
case EFFECT_RECOIL_33_STATUS:
if (AI_IsDamagedByRecoil(battlerAtk))
return TRUE;
break;
case EFFECT_SPEED_DOWN_HIT:
case EFFECT_ATTACK_DOWN_HIT:
case EFFECT_DEFENSE_DOWN_HIT:
case EFFECT_SPECIAL_ATTACK_DOWN_HIT:
case EFFECT_SPECIAL_DEFENSE_DOWN_HIT:
case EFFECT_SPECIAL_DEFENSE_DOWN_HIT_2:
if (noOfHitsToKo != 1 && abilityDef == ABILITY_CONTRARY && !IsMoldBreakerTypeAbility(abilityAtk))
return TRUE;
break;
}
return FALSE;
}
// Checks if one of the moves has side effects or perks, assuming equal dmg or equal no of hits to KO
u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef)
u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef, s32 noOfHitsToKo)
{
bool32 effect1, effect2;
s32 defAbility = AI_DATA->abilities[battlerDef];
@ -1022,22 +1059,18 @@ u32 AI_WhichMoveBetter(u32 move1, u32 move2, u32 battlerAtk, u32 battlerDef)
if (IS_MOVE_PHYSICAL(move2) && !IS_MOVE_PHYSICAL(move1))
return 0;
}
// Check recoil
if (AI_IsDamagedByRecoil(battlerAtk))
{
if (IS_MOVE_RECOIL(move1) && !IS_MOVE_RECOIL(move2) && gBattleMoves[move2].effect != EFFECT_RECHARGE)
return 1;
if (IS_MOVE_RECOIL(move2) && !IS_MOVE_RECOIL(move1) && gBattleMoves[move1].effect != EFFECT_RECHARGE)
return 0;
}
// Check recharge
if (gBattleMoves[move1].effect == EFFECT_RECHARGE && gBattleMoves[move2].effect != EFFECT_RECHARGE)
return 1;
if (gBattleMoves[move2].effect == EFFECT_RECHARGE && gBattleMoves[move1].effect != EFFECT_RECHARGE)
// Check additional effects.
effect1 = AI_IsMoveEffectInMinus(battlerAtk, battlerDef, move1, noOfHitsToKo);
effect2 = AI_IsMoveEffectInMinus(battlerAtk, battlerDef, move2, noOfHitsToKo);
if (effect2 && !effect1)
return 0;
// Check additional effect.
effect1 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move1);
effect2 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move2);
if (effect1 && !effect2)
return 1;
effect1 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move1, noOfHitsToKo);
effect2 = AI_IsMoveEffectInPlus(battlerAtk, battlerDef, move2, noOfHitsToKo);
if (effect2 && !effect1)
return 1;
if (effect1 && !effect2)
@ -1132,7 +1165,7 @@ void SetMovesDamageResults(u32 battlerAtk, u16 *moves)
bestId = j;
if (moveDmgs[j] == moveDmgs[bestId])
{
switch (AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[j], battlerAtk, battlerDef))
switch (AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[j], battlerAtk, battlerDef, GetNoOfHitsToKO(moveDmgs[j], hp)))
{
case 2:
if (Random() & 1)
@ -1149,7 +1182,7 @@ void SetMovesDamageResults(u32 battlerAtk, u16 *moves)
result = MOVE_POWER_BEST;
else if ((moveDmgs[currId] >= hp || moveDmgs[bestId] < hp) // If current move can faint as well, or if neither can
&& GetNoOfHitsToKO(moveDmgs[currId], hp) - GetNoOfHitsToKO(moveDmgs[bestId], hp) <= 2 // Consider a move weak if it needs to be used at least 2 times more to faint the target, compared to the best move.
&& AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[currId], battlerAtk, battlerDef) != 0)
&& AI_WhichMoveBetter(gBattleMons[battlerAtk].moves[bestId], gBattleMons[battlerAtk].moves[currId], battlerAtk, battlerDef, GetNoOfHitsToKO(moveDmgs[currId], hp)) != 0)
result = MOVE_POWER_GOOD;
else
result = MOVE_POWER_WEAK;
@ -1478,9 +1511,7 @@ bool32 DoesBattlerIgnoreAbilityChecks(u32 atkAbility, u32 move)
return TRUE;
}
if (atkAbility == ABILITY_MOLD_BREAKER
|| atkAbility == ABILITY_TERAVOLT
|| atkAbility == ABILITY_TURBOBLAZE)
if (IsMoldBreakerTypeAbility(atkAbility))
return TRUE;
return FALSE;
@ -1612,12 +1643,6 @@ bool32 IsMoveRedirectionPrevented(u32 move, u32 atkAbility)
return FALSE;
}
u32 AI_GetMoveAccuracy(u32 battlerAtk, u32 battlerDef, u32 move)
{
return GetTotalAccuracy(battlerAtk, battlerDef, move, AI_DATA->abilities[battlerAtk], AI_DATA->abilities[battlerDef],
AI_DATA->holdEffects[battlerAtk], AI_DATA->holdEffects[battlerDef]);
}
bool32 IsSemiInvulnerable(u32 battlerDef, u32 move)
{
if (gStatuses3[battlerDef] & STATUS3_PHANTOM_FORCE)
@ -1676,7 +1701,7 @@ bool32 IsMoveEncouragedToHit(u32 battlerAtk, u32 battlerDef, u32 move)
bool32 ShouldTryOHKO(u32 battlerAtk, u32 battlerDef, u32 atkAbility, u32 defAbility, u32 move)
{
u32 holdEffect = AI_DATA->holdEffects[battlerDef];
u32 accuracy = AI_GetMoveAccuracy(battlerAtk, battlerDef, move);
u32 accuracy = AI_DATA->moveAccuracy[battlerAtk][battlerDef][AI_THINKING_STRUCT->movesetIndex];
gPotentialItemEffectBattler = battlerDef;
if (holdEffect == HOLD_EFFECT_FOCUS_BAND && (Random() % 100) < AI_DATA->holdEffectParams[battlerDef])
@ -1865,8 +1890,7 @@ void ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove,
// stat stages
bool32 ShouldLowerStat(u32 battler, u32 battlerAbility, u32 stat)
{
if ((gBattleMons[battler].statStages[stat] > MIN_STAT_STAGE && battlerAbility != ABILITY_CONTRARY)
|| (battlerAbility == ABILITY_CONTRARY && gBattleMons[battler].statStages[stat] < MAX_STAT_STAGE))
if (gBattleMons[battler].statStages[stat] > MIN_STAT_STAGE && battlerAbility != ABILITY_CONTRARY)
{
if (AI_DATA->holdEffects[battler] == HOLD_EFFECT_CLEAR_AMULET
|| battlerAbility == ABILITY_CLEAR_BODY
@ -1874,6 +1898,14 @@ bool32 ShouldLowerStat(u32 battler, u32 battlerAbility, u32 stat)
|| battlerAbility == ABILITY_FULL_METAL_BODY)
return FALSE;
// If AI is faster and doesn't have any mons left, lowering speed doesn't give any
if (stat == STAT_SPEED)
{
if (AI_WhoStrikesFirst(sBattler_AI, battler, AI_THINKING_STRUCT->moveConsidered) == AI_IS_FASTER
&& CountUsablePartyMons(sBattler_AI) == 0
&& !HasMoveEffect(sBattler_AI, EFFECT_ELECTRO_BALL))
return FALSE;
}
return TRUE;
}
@ -2157,7 +2189,7 @@ bool32 HasMoveWithLowAccuracy(u32 battlerAtk, u32 battlerDef, u32 accCheck, bool
|| AI_GetBattlerMoveTargetType(battlerAtk, moves[i]) & (MOVE_TARGET_USER | MOVE_TARGET_OPPONENTS_FIELD))
continue;
if (AI_GetMoveAccuracy(battlerAtk, battlerDef, moves[i]) <= accCheck)
if (AI_DATA->moveAccuracy[battlerAtk][battlerDef][i] <= accCheck)
return TRUE;
}
}
@ -2178,7 +2210,7 @@ bool32 HasSleepMoveWithLowAccuracy(u32 battlerAtk, u32 battlerDef)
if (!(gBitTable[i] & moveLimitations))
{
if (gBattleMoves[moves[i]].effect == EFFECT_SLEEP
&& AI_GetMoveAccuracy(battlerAtk, battlerDef, moves[i]) < 85)
&& AI_DATA->moveAccuracy[battlerAtk][battlerDef][i] < 85)
return TRUE;
}
}
@ -3341,7 +3373,7 @@ bool32 DoesPartnerHaveSameMoveEffect(u32 battlerAtkPartner, u32 battlerDef, u32
return FALSE;
if (gBattleMoves[move].effect == gBattleMoves[partnerMove].effect
&& gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE
&& partnerMove != MOVE_NONE
&& gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef)
{
return TRUE;
@ -3356,7 +3388,7 @@ bool32 PartnerHasSameMoveEffectWithoutTarget(u32 battlerAtkPartner, u32 move, u3
return FALSE;
if (gBattleMoves[move].effect == gBattleMoves[partnerMove].effect
&& gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE)
&& partnerMove != MOVE_NONE)
return TRUE;
return FALSE;
}
@ -3367,7 +3399,7 @@ bool32 PartnerMoveEffectIsStatusSameTarget(u32 battlerAtkPartner, u32 battlerDef
if (!IsDoubleBattle())
return FALSE;
if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE
if (partnerMove != MOVE_NONE
&& gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef
&& (gBattleMoves[partnerMove].effect == EFFECT_SLEEP
|| gBattleMoves[partnerMove].effect == EFFECT_POISON
@ -3379,20 +3411,15 @@ bool32 PartnerMoveEffectIsStatusSameTarget(u32 battlerAtkPartner, u32 battlerDef
return FALSE;
}
//PARTNER_MOVE_EFFECT_IS_WEATHER
bool32 PartnerMoveEffectIsWeather(u32 battlerAtkPartner, u32 partnerMove)
bool32 IsMoveEffectWeather(u32 move)
{
if (!IsDoubleBattle())
return FALSE;
if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE
&& (gBattleMoves[partnerMove].effect == EFFECT_SUNNY_DAY
|| gBattleMoves[partnerMove].effect == EFFECT_RAIN_DANCE
|| gBattleMoves[partnerMove].effect == EFFECT_SANDSTORM
|| gBattleMoves[partnerMove].effect == EFFECT_HAIL
|| gBattleMoves[partnerMove].effect == EFFECT_SNOWSCAPE))
if (move != MOVE_NONE
&& (gBattleMoves[move].effect == EFFECT_SUNNY_DAY
|| gBattleMoves[move].effect == EFFECT_RAIN_DANCE
|| gBattleMoves[move].effect == EFFECT_SANDSTORM
|| gBattleMoves[move].effect == EFFECT_HAIL
|| gBattleMoves[move].effect == EFFECT_SNOWSCAPE))
return TRUE;
return FALSE;
}
@ -3402,7 +3429,7 @@ bool32 PartnerMoveEffectIsTerrain(u32 battlerAtkPartner, u32 partnerMove)
if (!IsDoubleBattle())
return FALSE;
if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE
if (partnerMove != MOVE_NONE
&& (gBattleMoves[partnerMove].effect == EFFECT_GRASSY_TERRAIN
|| gBattleMoves[partnerMove].effect == EFFECT_MISTY_TERRAIN
|| gBattleMoves[partnerMove].effect == EFFECT_ELECTRIC_TERRAIN
@ -3418,7 +3445,7 @@ bool32 PartnerMoveIs(u32 battlerAtkPartner, u32 partnerMove, u32 moveCheck)
if (!IsDoubleBattle())
return FALSE;
if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE && partnerMove == moveCheck)
if (partnerMove != MOVE_NONE && partnerMove == moveCheck)
return TRUE;
return FALSE;
}
@ -3429,7 +3456,7 @@ bool32 PartnerMoveIsSameAsAttacker(u32 battlerAtkPartner, u32 battlerDef, u32 mo
if (!IsDoubleBattle())
return FALSE;
if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE && move == partnerMove && gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef)
if (partnerMove != MOVE_NONE && move == partnerMove && gBattleStruct->moveTarget[battlerAtkPartner] == battlerDef)
return TRUE;
return FALSE;
}
@ -3439,7 +3466,7 @@ bool32 PartnerMoveIsSameNoTarget(u32 battlerAtkPartner, u32 move, u32 partnerMov
{
if (!IsDoubleBattle())
return FALSE;
if (gChosenMoveByBattler[battlerAtkPartner] != MOVE_NONE && move == partnerMove)
if (partnerMove != MOVE_NONE && move == partnerMove)
return TRUE;
return FALSE;
}

View File

@ -38,6 +38,7 @@
#include "constants/songs.h"
#include "constants/trainers.h"
#include "trainer_hill.h"
#include "test_runner.h"
static void OpponentHandleLoadMonSprite(u32 battler);
static void OpponentHandleSwitchInAnim(u32 battler);
@ -689,6 +690,9 @@ static void OpponentHandleChoosePokemon(u32 battler)
*(gBattleStruct->AI_monToSwitchIntoId + battler) = PARTY_SIZE;
*(gBattleStruct->monToSwitchIntoId + battler) = chosenMonId;
}
#if TESTING
TestRunner_Battle_CheckSwitch(battler, chosenMonId);
#endif // TESTING
BtlController_EmitChosenMonReturnValue(battler, BUFFER_B, chosenMonId, NULL);
OpponentBufferExecCompleted(battler);

View File

@ -437,25 +437,7 @@ static void RecordedOpponentHandleMoveAnimation(u32 battler)
static void RecordedOpponentHandlePrintString(u32 battler)
{
u16 *stringId;
gBattle_BG0_X = 0;
gBattle_BG0_Y = 0;
stringId = (u16 *)(&gBattleResources->bufferA[battler][2]);
BufferStringBattle(*stringId, battler);
if (gTestRunnerEnabled)
{
TestRunner_Battle_RecordMessage(gDisplayedStringBattle);
if (gTestRunnerHeadless)
{
RecordedOpponentBufferExecCompleted(battler);
return;
}
}
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG);
gBattlerControllerFuncs[battler] = Controller_WaitForString;
BtlController_HandlePrintString(battler, FALSE, FALSE);
}
static void RecordedOpponentHandleChooseAction(u32 battler)

View File

@ -427,25 +427,7 @@ static void RecordedPlayerHandleMoveAnimation(u32 battler)
static void RecordedPlayerHandlePrintString(u32 battler)
{
u16 *stringId;
gBattle_BG0_X = 0;
gBattle_BG0_Y = 0;
stringId = (u16 *)(&gBattleResources->bufferA[battler][2]);
BufferStringBattle(*stringId, battler);
if (gTestRunnerEnabled)
{
TestRunner_Battle_RecordMessage(gDisplayedStringBattle);
if (gTestRunnerHeadless)
{
RecordedPlayerBufferExecCompleted(battler);
return;
}
}
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG);
gBattlerControllerFuncs[battler] = Controller_WaitForString;
BtlController_HandlePrintString(battler, FALSE, FALSE);
}
static void ChooseActionInBattlePalace(u32 battler)

View File

@ -2664,6 +2664,17 @@ void BtlController_HandlePrintString(u32 battler, bool32 updateTvData, bool32 ar
gBattle_BG0_Y = 0;
stringId = (u16 *)(&gBattleResources->bufferA[battler][2]);
BufferStringBattle(*stringId, battler);
if (gTestRunnerEnabled)
{
TestRunner_Battle_RecordMessage(gDisplayedStringBattle);
if (gTestRunnerHeadless)
{
BattleControllerComplete(battler);
return;
}
}
BattlePutTextOnWindow(gDisplayedStringBattle, B_WIN_MSG);
gBattlerControllerFuncs[battler] = Controller_WaitForString;
if (updateTvData)

View File

@ -4375,6 +4375,11 @@ static void HandleTurnActionSelectionState(void)
else if (gBattleResources->bufferB[battler][2] & RET_ULTRA_BURST)
gBattleStruct->burst.toBurst |= gBitTable[battler];
gBattleCommunication[battler]++;
if (gTestRunnerEnabled)
{
TestRunner_Battle_CheckChosenMove(battler, gChosenMoveByBattler[battler], gBattleStruct->moveTarget[battler]);
}
}
break;
}

View File

@ -5096,9 +5096,15 @@ static void Cmd_playstatchangeanimation(void)
// Handle Contrary and Simple
if (ability == ABILITY_CONTRARY)
{
flags ^= STAT_CHANGE_NEGATIVE;
RecordAbilityBattle(battler, ability);
}
else if (ability == ABILITY_SIMPLE)
{
flags |= STAT_CHANGE_BY_TWO;
RecordAbilityBattle(battler, ability);
}
if (flags & STAT_CHANGE_NEGATIVE) // goes down
{
@ -11422,6 +11428,7 @@ static u32 ChangeStatBuffs(s8 statValue, u32 statId, u32 flags, const u8 *BS_ptr
{
statValue ^= STAT_BUFF_NEGATIVE;
gBattleScripting.statChanger ^= STAT_BUFF_NEGATIVE;
RecordAbilityBattle(battler, battlerAbility);
if (flags & STAT_CHANGE_UPDATE_MOVE_EFFECT)
{
flags &= ~STAT_CHANGE_UPDATE_MOVE_EFFECT;

View File

@ -6169,6 +6169,11 @@ bool32 IsMyceliumMightOnField(void)
return FALSE;
}
bool32 IsMoldBreakerTypeAbility(u32 ability)
{
return (ability == ABILITY_MOLD_BREAKER || ability == ABILITY_TERAVOLT || ability == ABILITY_TURBOBLAZE);
}
u32 GetBattlerAbility(u32 battler)
{
if (gStatuses3[battler] & STATUS3_GASTRO_ACID)
@ -6180,9 +6185,7 @@ u32 GetBattlerAbility(u32 battler)
if (IsMyceliumMightOnField())
return ABILITY_NONE;
if ((((gBattleMons[gBattlerAttacker].ability == ABILITY_MOLD_BREAKER
|| gBattleMons[gBattlerAttacker].ability == ABILITY_TERAVOLT
|| gBattleMons[gBattlerAttacker].ability == ABILITY_TURBOBLAZE)
if (((IsMoldBreakerTypeAbility(gBattleMons[gBattlerAttacker].ability)
&& !(gStatuses3[gBattlerAttacker] & STATUS3_GASTRO_ACID))
|| gBattleMoves[gCurrentMove].ignoresTargetAbility)
&& sAbilitiesAffectedByMoldBreaker[gBattleMons[battler].ability]

407
test/battle/ai.c Normal file
View File

@ -0,0 +1,407 @@
#include "global.h"
#include "test/battle.h"
#include "battle_ai_util.h"
AI_SINGLE_BATTLE_TEST("AI gets baited by Protect Switch tactics") // This behavior is to be fixed.
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING);
PLAYER(SPECIES_STUNFISK);
PLAYER(SPECIES_PELIPPER);
OPPONENT(SPECIES_DARKRAI) { Moves(MOVE_TACKLE, MOVE_PECK, MOVE_EARTHQUAKE, MOVE_THUNDERBOLT); }
OPPONENT(SPECIES_SCIZOR) { Moves(MOVE_HYPER_BEAM, MOVE_FACADE, MOVE_GIGA_IMPACT, MOVE_EXTREME_SPEED); }
} WHEN {
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake
TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt
TURN { SWITCH(player, 0); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); } // E-quake
TURN { SWITCH(player, 1); EXPECT_MOVE(opponent, MOVE_EARTHQUAKE);} // E-quake
TURN { MOVE(player, MOVE_PROTECT); EXPECT_MOVE(opponent, MOVE_THUNDERBOLT); } // T-Bolt
}
}
AI_SINGLE_BATTLE_TEST("AI prefers Bubble over Water Gun if it's slower")
{
u32 speedPlayer, speedAi;
PARAMETRIZE { speedPlayer = 200; speedAi = 10; }
PARAMETRIZE { speedPlayer = 10; speedAi = 200; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_SCIZOR) { Speed(speedPlayer); }
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_WATER_GUN, MOVE_BUBBLE); Speed(speedAi); }
} WHEN {
if (speedPlayer > speedAi)
{
TURN { SCORE_GT(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); }
TURN { SCORE_GT(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); }
}
else
{
TURN { SCORE_EQ(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); }
TURN { SCORE_EQ(opponent, MOVE_BUBBLE, MOVE_WATER_GUN); }
}
}
}
AI_SINGLE_BATTLE_TEST("AI prefers Water Gun over Bubble if it knows that foe has Contrary")
{
u32 abilityAI;
PARAMETRIZE { abilityAI = ABILITY_MOXIE; }
PARAMETRIZE { abilityAI = ABILITY_MOLD_BREAKER; } // Mold Breaker ignores Contrary.
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_SHUCKLE) { Ability(ABILITY_CONTRARY); }
OPPONENT(SPECIES_PINSIR) { Moves(MOVE_WATER_GUN, MOVE_BUBBLE); Ability(abilityAI); }
} WHEN {
TURN { MOVE(player, MOVE_DEFENSE_CURL); }
TURN { MOVE(player, MOVE_DEFENSE_CURL);
if (abilityAI == ABILITY_MOLD_BREAKER) { SCORE_EQ(opponent, MOVE_WATER_GUN, MOVE_BUBBLE); }
else { SCORE_GT(opponent, MOVE_WATER_GUN, MOVE_BUBBLE); }}
} SCENE {
MESSAGE("Shuckle's Defense fell!"); // Contrary activates
} THEN {
EXPECT(gBattleResources->aiData->abilities[B_POSITION_PLAYER_LEFT] == ABILITY_CONTRARY);
}
}
AI_SINGLE_BATTLE_TEST("AI prefers moves with better accuracy, but only if they both require the same number of hits to ko")
{
u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE;
u16 hp, expectedMove, turns, abilityAtk, expectedMove2;
abilityAtk = ABILITY_NONE;
expectedMove2 = MOVE_NONE;
// Here it's a simple test, both Slam and Strength deal the same damage, but Strength always hits, whereas Slam often misses.
PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_TACKLE; hp = 490; expectedMove = MOVE_STRENGTH; turns = 4; }
PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 365; expectedMove = MOVE_STRENGTH; turns = 3; }
PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 245; expectedMove = MOVE_STRENGTH; turns = 2; }
PARAMETRIZE { move1 = MOVE_SLAM; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 125; expectedMove = MOVE_STRENGTH; turns = 1; }
// Mega Kick deals more damage, but can miss more often. Here, AI should choose Mega Kick if it can faint target in less number of turns than Strength. Otherwise, it should use Strength.
PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 170; expectedMove = MOVE_MEGA_KICK; turns = 1; }
PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 245; expectedMove = MOVE_STRENGTH; turns = 2; }
// Swift always hits and Guts has accuracy of 100%. Hustle lowers accuracy of all physical moves.
PARAMETRIZE { abilityAtk = ABILITY_HUSTLE; move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_SWIFT; move4 = MOVE_TACKLE; hp = 5; expectedMove = MOVE_SWIFT; turns = 1; }
PARAMETRIZE { abilityAtk = ABILITY_HUSTLE; move1 = MOVE_MEGA_KICK; move2 = MOVE_STRENGTH; move3 = MOVE_GUST; move4 = MOVE_TACKLE; hp = 5; expectedMove = MOVE_GUST; turns = 1; }
// Mega Kick and Slam both have lower accuracy. Gust and Tackle both have 100, so AI can choose either of them.
PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_SLAM; move3 = MOVE_TACKLE; move4 = MOVE_GUST; hp = 5; expectedMove = MOVE_GUST; expectedMove2 = MOVE_TACKLE; turns = 1; }
// All moves hit with No guard ability
PARAMETRIZE { move1 = MOVE_MEGA_KICK; move2 = MOVE_GUST; hp = 5; expectedMove = MOVE_MEGA_KICK; expectedMove2 = MOVE_GUST; turns = 1; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET) { HP(hp); }
PLAYER(SPECIES_WOBBUFFET);
ASSUME(gBattleMoves[MOVE_SWIFT].accuracy == 0);
ASSUME(gBattleMoves[MOVE_SLAM].power == gBattleMoves[MOVE_STRENGTH].power);
ASSUME(gBattleMoves[MOVE_MEGA_KICK].power > gBattleMoves[MOVE_STRENGTH].power);
ASSUME(gBattleMoves[MOVE_SLAM].accuracy < gBattleMoves[MOVE_STRENGTH].accuracy);
ASSUME(gBattleMoves[MOVE_MEGA_KICK].accuracy < gBattleMoves[MOVE_STRENGTH].accuracy);
ASSUME(gBattleMoves[MOVE_TACKLE].accuracy == 100);
ASSUME(gBattleMoves[MOVE_GUST].accuracy == 100);
OPPONENT(SPECIES_EXPLOUD) { Moves(move1, move2, move3, move4); Ability(abilityAtk); SpAttack(50); } // Low Sp.Atk, so Swift deals less damage than Strength.
} WHEN {
switch (turns)
{
case 1:
if (expectedMove2 != MOVE_NONE) {
TURN { EXPECT_MOVES(opponent, expectedMove, expectedMove2); SEND_OUT(player, 1); }
}
else {
TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
}
break;
case 2:
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
break;
case 3:
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
break;
case 4:
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
break;
}
} SCENE {
MESSAGE("Wobbuffet fainted!");
}
}
AI_SINGLE_BATTLE_TEST("AI prefers moves which deal more damage instead of moves which are super-effective but deal less damage")
{
u8 turns = 0;
u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE;
u16 expectedMove, abilityAtk, abilityDef, expectedMove2;
abilityAtk = ABILITY_NONE;
expectedMove2 = MOVE_NONE;
// Scald and Poison Jab take 3 hits, Waterfall takes 2.
PARAMETRIZE { move1 = MOVE_WATERFALL; move2 = MOVE_SCALD; move3 = MOVE_POISON_JAB; move4 = MOVE_WATER_GUN; expectedMove = MOVE_WATERFALL; turns = 2; }
// Poison Jab takes 3 hits, Water gun 5. Immunity so there's no poison chip damage.
PARAMETRIZE { move1 = MOVE_POISON_JAB; move2 = MOVE_WATER_GUN; expectedMove = MOVE_POISON_JAB; abilityDef = ABILITY_IMMUNITY; turns = 3; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_TYPHLOSION) { Ability(abilityDef); }
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_NIDOQUEEN) { Moves(move1, move2, move3, move4); Ability(abilityAtk); }
} WHEN {
switch (turns)
{
case 2:
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
break;
case 3:
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); }
TURN { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
break;
}
} SCENE {
MESSAGE("Typhlosion fainted!");
}
}
AI_SINGLE_BATTLE_TEST("AI prefers Earthquake over Drill Run if both require the same number of hits to ko")
{
// Drill Run has less accuracy than E-quake, but can score a higher crit. However the chance is too small, so AI should ignore it.
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_TYPHLOSION);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_GEODUDE) { Moves(MOVE_EARTHQUAKE, MOVE_DRILL_RUN); }
} WHEN {
TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); }
TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); SEND_OUT(player, 1); }
}
SCENE {
MESSAGE("Typhlosion fainted!");
}
}
AI_SINGLE_BATTLE_TEST("AI chooses the safest option to faint the target, taking into account accuracy and move effect")
{
u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE;
u16 expectedMove, abilityAtk = ABILITY_NONE, abilityDef;
u16 expectedMove2 = MOVE_NONE;
// Psychic is not very effective, but always hits. Solarbeam requires a charging turn, Double Edge has recoil and Focus Blast can miss;
PARAMETRIZE { abilityAtk = ABILITY_STURDY; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; expectedMove = MOVE_PSYCHIC; }
// Same as above, but ai mon has rock head ability, so it can use Double Edge without taking recoil damage. Psychic can also lower Special Defense,
// but because it faints the target it doesn't matter.
PARAMETRIZE { abilityAtk = ABILITY_ROCK_HEAD; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE;
expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_DOUBLE_EDGE; }
// This time it's Solarbeam + Psychic, because the weather is sunny.
PARAMETRIZE { abilityAtk = ABILITY_DROUGHT; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE;
expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SOLAR_BEAM; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET) { HP(5); }
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_GEODUDE) { Moves(move1, move2, move3, move4); Ability(abilityAtk); }
} WHEN {
TURN { if (expectedMove2 == MOVE_NONE) { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
else {EXPECT_MOVES(opponent, expectedMove, expectedMove2); SCORE_EQ(opponent, expectedMove, expectedMove2); SEND_OUT(player, 1);}
}
}
SCENE {
MESSAGE("Wobbuffet fainted!");
}
}
AI_SINGLE_BATTLE_TEST("AI won't use ground type attacks against flying type Pokemon unless Gravity is in effect")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_CROBAT);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_EARTHQUAKE, MOVE_TACKLE, MOVE_POISON_STING, MOVE_GUST); }
} WHEN {
TURN { NOT_EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); }
TURN { MOVE(player, MOVE_GRAVITY); NOT_EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); }
TURN { EXPECT_MOVE(opponent, MOVE_EARTHQUAKE); SEND_OUT(player, 1); }
} SCENE {
MESSAGE("Gravity intensified!");
}
}
AI_SINGLE_BATTLE_TEST("AI will not switch in a Pokemon which is slower and gets 1HKOed after fainting")
{
bool32 alakazamFaster;
u32 speedAlakazm;
KNOWN_FAILING;
PARAMETRIZE{ speedAlakazm = 200; alakazamFaster = FALSE; }
PARAMETRIZE{ speedAlakazm = 400; alakazamFaster = TRUE; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | AI_FLAG_SMART_SWITCHING);
PLAYER(SPECIES_WEAVILE) { Speed(300); Ability(ABILITY_SHADOW_TAG); } // Weavile has Shadow Tag, so AI can't switch on the first turn, but has to do it after fainting.
OPPONENT(SPECIES_KADABRA) { Speed(200); Moves(MOVE_PSYCHIC, MOVE_DISABLE, MOVE_TAUNT, MOVE_CALM_MIND); }
OPPONENT(SPECIES_ALAKAZAM) { Speed(speedAlakazm); Moves(MOVE_FOCUS_BLAST, MOVE_PSYCHIC); } // Alakazam has a move which OHKOes Weavile, but it doesn't matter if he's getting KO-ed first.
OPPONENT(SPECIES_BLASTOISE) { Speed(200); Moves(MOVE_BUBBLE_BEAM, MOVE_WATER_GUN, MOVE_LEER, MOVE_STRENGTH); } // Can't OHKO, but survives a hit from Weavile's Night Slash.
} WHEN {
TURN { MOVE(player, MOVE_NIGHT_SLASH) ; EXPECT_SEND_OUT(opponent, alakazamFaster ? 1 : 2); }
} SCENE {
MESSAGE("Foe Kadabra fainted!");
if (alakazamFaster) {
MESSAGE("{PKMN} TRAINER LEAF sent out Alakazam!");
} else {
MESSAGE("{PKMN} TRAINER LEAF sent out Blastoise!");
}
}
}
AI_SINGLE_BATTLE_TEST("AI switches if Perish Song is about to kill")
{
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) {Moves(MOVE_TACKLE); }
OPPONENT(SPECIES_CROBAT) {Moves(MOVE_TACKLE); }
} WHEN {
TURN { MOVE(player, MOVE_PERISH_SONG); }
TURN { ; }
TURN { ; }
TURN { EXPECT_SWITCH(opponent, 1); }
} SCENE {
MESSAGE("{PKMN} TRAINER LEAF sent out Crobat!");
}
}
AI_DOUBLE_BATTLE_TEST("AI won't use a Weather changing move if partner already chose such move")
{
u32 j, k;
static const u16 weatherMoves[] = {MOVE_SUNNY_DAY, MOVE_HAIL, MOVE_RAIN_DANCE, MOVE_SANDSTORM, MOVE_SNOWSCAPE};
u16 weatherMoveLeft, weatherMoveRight;
for (j = 0; j < ARRAY_COUNT(weatherMoves); j++)
{
for (k = 0; k < ARRAY_COUNT(weatherMoves); k++)
{
PARAMETRIZE { weatherMoveLeft = weatherMoves[j]; weatherMoveRight = weatherMoves[k]; }
}
}
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Moves(weatherMoveLeft); }
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, weatherMoveRight); }
} WHEN {
TURN { NOT_EXPECT_MOVE(opponentRight, weatherMoveRight);
SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:playerRight);
SCORE_LT_VAL(opponentRight, weatherMoveRight, AI_SCORE_DEFAULT, target:opponentLeft);
}
}
}
AI_DOUBLE_BATTLE_TEST("AI will not use Helping Hand if partner does not have any damage moves")
{
u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE;
PARAMETRIZE{ move1 = MOVE_LEER; move2 = MOVE_TOXIC; }
PARAMETRIZE{ move1 = MOVE_HELPING_HAND; move2 = MOVE_PROTECT; }
PARAMETRIZE{ move1 = MOVE_ACUPRESSURE; move2 = MOVE_DOUBLE_TEAM; move3 = MOVE_TOXIC; move4 = MOVE_PROTECT; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND, MOVE_TACKLE); }
OPPONENT(SPECIES_WOBBUFFET) { Moves(move1, move2, move3, move4); }
} WHEN {
TURN { NOT_EXPECT_MOVE(opponentLeft, MOVE_HELPING_HAND);
SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerRight);
SCORE_LT_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:opponentLeft);
}
} SCENE {
NOT MESSAGE("Foe Wobbuffet used Helping Hand!");
}
}
AI_DOUBLE_BATTLE_TEST("AI will not use a status move if partner already chose Helping Hand")
{
s32 j;
u32 statusMove;
for (j = MOVE_NONE + 1; j < MOVES_COUNT; j++)
{
if (gBattleMoves[j].split == SPLIT_STATUS) {
PARAMETRIZE{ statusMove = j; }
}
}
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_HELPING_HAND); }
OPPONENT(SPECIES_WOBBUFFET) { Moves(MOVE_TACKLE, statusMove); }
} WHEN {
TURN { NOT_EXPECT_MOVE(opponentRight, statusMove);
SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:playerRight);
SCORE_LT_VAL(opponentRight, statusMove, AI_SCORE_DEFAULT, target:opponentLeft);
}
} SCENE {
MESSAGE("Foe Wobbuffet used Helping Hand!");
}
}
AI_SINGLE_BATTLE_TEST("AI without any flags chooses moves at random - singles")
{
GIVEN {
AI_FLAGS(0);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); }
} WHEN {
TURN { EXPECT_MOVES(opponent, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND);
SCORE_EQ_VAL(opponent, MOVE_SPLASH, AI_SCORE_DEFAULT);
SCORE_EQ_VAL(opponent, MOVE_EXPLOSION, AI_SCORE_DEFAULT);
SCORE_EQ_VAL(opponent, MOVE_RAGE, AI_SCORE_DEFAULT);
SCORE_EQ_VAL(opponent, MOVE_HELPING_HAND, AI_SCORE_DEFAULT);
}
}
}
AI_DOUBLE_BATTLE_TEST("AI without any flags chooses moves at random - doubles")
{
GIVEN {
AI_FLAGS(0);
PLAYER(SPECIES_WOBBUFFET);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); }
OPPONENT(SPECIES_NIDOQUEEN) { Moves(MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND); }
} WHEN {
TURN { EXPECT_MOVES(opponentLeft, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND);
EXPECT_MOVES(opponentRight, MOVE_SPLASH, MOVE_EXPLOSION, MOVE_RAGE, MOVE_HELPING_HAND);
SCORE_EQ_VAL(opponentLeft, MOVE_SPLASH, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_EQ_VAL(opponentLeft, MOVE_EXPLOSION, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_EQ_VAL(opponentLeft, MOVE_RAGE, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_EQ_VAL(opponentLeft, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_EQ_VAL(opponentRight, MOVE_SPLASH, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_EQ_VAL(opponentRight, MOVE_EXPLOSION, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_EQ_VAL(opponentRight, MOVE_RAGE, AI_SCORE_DEFAULT, target:playerLeft);
SCORE_EQ_VAL(opponentRight, MOVE_HELPING_HAND, AI_SCORE_DEFAULT, target:playerLeft);
}
}
}

View File

@ -1,5 +1,6 @@
#include "global.h"
#include "battle.h"
#include "battle_ai_util.h"
#include "battle_anim.h"
#include "battle_controllers.h"
#include "characters.h"
@ -38,6 +39,9 @@ EWRAM_DATA struct BattleTestRunnerState *gBattleTestRunnerState = NULL;
static void CB2_BattleTest_NextParameter(void);
static void CB2_BattleTest_NextTrial(void);
static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32 byte);
static void PrintAiMoveLog(u32 battlerId, u32 moveSlot, u32 moveId, s32 totalScore);
static void ClearAiLog(u32 battlerId);
static const char *BattlerIdentifier(s32 battlerId);
NAKED static void InvokeSingleTestFunctionWithStack(void *results, u32 i, struct BattlePokemon *player, struct BattlePokemon *opponent, SingleBattleTestFunction function, void *stack)
{
@ -96,23 +100,42 @@ static void InvokeTestFunction(const struct BattleTest *test)
{
case BATTLE_TEST_SINGLES:
case BATTLE_TEST_WILD:
case BATTLE_TEST_AI_SINGLES:
InvokeSingleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]);
break;
case BATTLE_TEST_DOUBLES:
case BATTLE_TEST_AI_DOUBLES:
InvokeDoubleTestFunctionWithStack(STATE->results, STATE->runParameter, &gBattleMons[B_POSITION_PLAYER_LEFT], &gBattleMons[B_POSITION_OPPONENT_LEFT], &gBattleMons[B_POSITION_PLAYER_RIGHT], &gBattleMons[B_POSITION_OPPONENT_RIGHT], test->function.singles, &DATA.stack[BATTLE_TEST_STACK_SIZE]);
break;
}
}
static u32 SourceLine(u32 sourceLineOffset)
static const struct BattleTest *GetBattleTest(void)
{
const struct BattleTest *test = gTestRunnerState.test->data;
return test;
}
static bool32 IsAITest(void)
{
switch (GetBattleTest()->type)
{
case BATTLE_TEST_AI_SINGLES:
case BATTLE_TEST_AI_DOUBLES:
return TRUE;
}
return FALSE;
}
static u32 SourceLine(u32 sourceLineOffset)
{
const struct BattleTest *test = GetBattleTest();
return test->sourceLine + sourceLineOffset;
}
static u32 SourceLineOffset(u32 sourceLine)
{
const struct BattleTest *test = gTestRunnerState.test->data;
const struct BattleTest *test = GetBattleTest();
if (sourceLine - test->sourceLine > 0xFF)
return 0;
else
@ -155,9 +178,12 @@ static void BattleTest_SetUp(void *data)
switch (test->type)
{
case BATTLE_TEST_SINGLES:
case BATTLE_TEST_WILD:
case BATTLE_TEST_AI_SINGLES:
STATE->battlersCount = 2;
break;
case BATTLE_TEST_DOUBLES:
case BATTLE_TEST_AI_DOUBLES:
STATE->battlersCount = 4;
break;
}
@ -234,18 +260,35 @@ static void BattleTest_Run(void *data)
memset(&DATA, 0, sizeof(DATA));
DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT;
DATA.recordedBattle.opponentA = TRAINER_LINK_OPPONENT;
DATA.recordedBattle.textSpeed = OPTIONS_TEXT_SPEED_FAST;
if (test->type == BATTLE_TEST_WILD)
DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER;
else
DATA.recordedBattle.battleFlags = BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_IS_MASTER;
if (test->type == BATTLE_TEST_DOUBLES)
// Set battle flags and opponent ids.
switch (test->type)
{
DATA.recordedBattle.battleFlags |= BATTLE_TYPE_DOUBLE;
case BATTLE_TEST_WILD:
DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER;
break;
case BATTLE_TEST_AI_SINGLES:
DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_TRAINER;
DATA.recordedBattle.opponentA = TRAINER_LEAF;
DATA.hasAI = TRUE;
break;
case BATTLE_TEST_AI_DOUBLES:
DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_TRAINER | BATTLE_TYPE_DOUBLE;
DATA.recordedBattle.opponentA = TRAINER_LEAF;
DATA.recordedBattle.opponentB = TRAINER_RED;
DATA.hasAI = TRUE;
break;
case BATTLE_TEST_SINGLES:
DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER;
DATA.recordedBattle.opponentA = TRAINER_LINK_OPPONENT;
break;
case BATTLE_TEST_DOUBLES:
DATA.recordedBattle.battleFlags = BATTLE_TYPE_IS_MASTER | BATTLE_TYPE_RECORDED_IS_MASTER | BATTLE_TYPE_RECORDED_LINK | BATTLE_TYPE_TRAINER | BATTLE_TYPE_DOUBLE;
DATA.recordedBattle.opponentA = TRAINER_LINK_OPPONENT;
DATA.recordedBattle.opponentB = TRAINER_LINK_OPPONENT;
break;
}
for (i = 0; i < STATE->battlersCount; i++)
{
DATA.recordedBattle.playersName[i][0] = CHAR_1 + i;
@ -711,6 +754,292 @@ void TestRunner_Battle_RecordHP(u32 battlerId, u32 oldHP, u32 newHP)
}
}
static const char *const sBattleActionNames[] =
{
[B_ACTION_USE_MOVE] = "MOVE",
[B_ACTION_USE_ITEM] = "USE_ITEM",
[B_ACTION_SWITCH] = "SWITCH",
};
static u32 CountAiExpectMoves(struct ExpectedAIAction *expectedAction, u32 battlerId, bool32 printLog)
{
u32 i, countExpected = 0;
for (i = 0; i < MAX_MON_MOVES; i++)
{
if (gBitTable[i] & expectedAction->moveSlots)
{
if (printLog)
PrintAiMoveLog(battlerId, i, gBattleMons[battlerId].moves[i], gBattleStruct->aiFinalScore[battlerId][expectedAction->target][i]);
countExpected++;
}
}
return countExpected;
}
void TestRunner_Battle_CheckChosenMove(u32 battlerId, u32 moveId, u32 target)
{
const char *filename = gTestRunnerState.test->filename;
u32 id = DATA.aiActionsPlayed[battlerId];
struct ExpectedAIAction *expectedAction = &DATA.expectedAiActions[battlerId][id];
if (!expectedAction->actionSet)
return;
if (!expectedAction->pass)
{
u32 i, expectedMoveId, countExpected;
bool32 movePasses = FALSE;
if (expectedAction->type != B_ACTION_USE_MOVE)
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected MOVE, got %s", filename, expectedAction->sourceLine, sBattleActionNames[expectedAction->type]);
if (expectedAction->explicitTarget && expectedAction->target != target)
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected target %s, got %s", filename, expectedAction->sourceLine, BattlerIdentifier(expectedAction->target), BattlerIdentifier(target));
for (i = 0; i < MAX_MON_MOVES; i++)
{
if (gBitTable[i] & expectedAction->moveSlots)
{
expectedMoveId = gBattleMons[battlerId].moves[i];
if (!expectedAction->notMove)
{
if (moveId == expectedMoveId)
{
movePasses = TRUE;
break;
}
}
else
{
if (moveId == expectedMoveId)
{
movePasses = FALSE;
break;
}
movePasses = TRUE;
}
}
}
countExpected = CountAiExpectMoves(expectedAction, battlerId, TRUE);
if (!expectedAction->notMove && !movePasses)
{
u32 moveSlot = GetMoveSlot(gBattleMons[battlerId].moves, moveId);
PrintAiMoveLog(battlerId, moveSlot, moveId, gBattleStruct->aiFinalScore[battlerId][expectedAction->target][moveSlot]);
if (countExpected > 1)
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched EXPECT_MOVES %S, got %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId], gMoveNames[moveId]);
else
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched EXPECT_MOVE %S, got %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId], gMoveNames[moveId]);
}
if (expectedAction->notMove && !movePasses)
{
if (countExpected > 1)
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched NOT_EXPECT_MOVES %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId]);
else
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched NOT_EXPECT_MOVE %S", filename, expectedAction->sourceLine, gMoveNames[expectedMoveId]);
}
}
// Turn passed, clear logs from the turn
ClearAiLog(battlerId);
DATA.aiActionsPlayed[battlerId]++;
}
void TestRunner_Battle_CheckSwitch(u32 battlerId, u32 partyIndex)
{
const char *filename = gTestRunnerState.test->filename;
u32 id = DATA.aiActionsPlayed[battlerId];
struct ExpectedAIAction *expectedAction = &DATA.expectedAiActions[battlerId][id];
if (!expectedAction->actionSet)
return;
if (!expectedAction->pass)
{
if (expectedAction->type != B_ACTION_SWITCH)
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected SWITCH/SEND_OUT, got %s", filename, expectedAction->sourceLine, sBattleActionNames[expectedAction->type]);
if (expectedAction->target != partyIndex)
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Expected partyIndex %d, got %d", filename, expectedAction->sourceLine, expectedAction->target, partyIndex);
}
DATA.aiActionsPlayed[battlerId]++;
}
static bool32 CheckComparision(s32 val1, s32 val2, u32 cmp)
{
switch (cmp)
{
case CMP_EQUAL:
return (val1 == val2);
case CMP_NOT_EQUAL:
return (val1 != val2);
case CMP_GREATER_THAN:
return (val1 > val2);
case CMP_LESS_THAN:
return (val1 < val2);
}
return FALSE;
}
static const char *const sCmpToStringTable[] =
{
[CMP_EQUAL] = "EQ",
[CMP_NOT_EQUAL] = "NE",
[CMP_LESS_THAN] = "LT",
[CMP_GREATER_THAN] = "GT",
};
static void CheckIfMaxScoreEqualExpectMove(u32 battlerId, s32 target, struct ExpectedAIAction *aiAction, const char *filename)
{
u32 i;
s32 *scores = gBattleStruct->aiFinalScore[battlerId][target];
s32 bestScore = 0, bestScoreId = 0;
u16 *moves = gBattleMons[battlerId].moves;
for (i = 0; i < MAX_MON_MOVES; i++)
{
if (scores[i] > bestScore)
{
bestScore = scores[i];
bestScoreId = i;
}
}
for (i = 0; i < MAX_MON_MOVES; i++)
{
// We expect move 'i', but it has the same best score as another move that we didn't expect.
if (scores[i] == scores[bestScoreId]
&& !aiAction->notMove
&& (aiAction->moveSlots & gBitTable[i])
&& !(aiAction->moveSlots & gBitTable[bestScoreId]))
{
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: EXPECT_MOVE %S has the same best score(%d) as not expected MOVE %S", filename,
aiAction->sourceLine, gMoveNames[moves[i]], scores[i], gMoveNames[moves[bestScoreId]]);
}
// We DO NOT expect move 'i', but it has the same best score as another move.
if (scores[i] == scores[bestScoreId]
&& aiAction->notMove
&& (aiAction->moveSlots & gBitTable[i])
&& !(aiAction->moveSlots & gBitTable[bestScoreId]))
{
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: NOT_EXPECT_MOVE %S has the same best score(%d) as MOVE %S", filename,
aiAction->sourceLine, gMoveNames[moves[i]], scores[i], gMoveNames[moves[bestScoreId]]);
}
}
}
static void PrintAiMoveLog(u32 battlerId, u32 moveSlot, u32 moveId, s32 totalScore)
{
s32 i, scoreFromLogs = 0;
if (!DATA.logAI) return;
if (DATA.aiLogPrintedForMove[battlerId] & gBitTable[moveSlot]) return;
DATA.aiLogPrintedForMove[battlerId] |= gBitTable[moveSlot];
MgbaPrintf_("Score Log for move %S:\n", gMoveNames[moveId]);
for (i = 0; i < MAX_AI_LOG_LINES; i++)
{
struct AILogLine *log = &DATA.aiLogLines[battlerId][moveSlot][i];
if (log->file)
{
if (log->set)
{
scoreFromLogs = log->score;
MgbaPrintf_("%s:%d: = %d\n", log->file, log->line, log->score);
}
else if (log->score > 0)
{
scoreFromLogs += log->score;
MgbaPrintf_("%s:%d: +%d\n", log->file, log->line, log->score);
}
else
{
scoreFromLogs += log->score;
MgbaPrintf_("%s:%d: %d\n", log->file, log->line, log->score);
}
}
else
{
break;
}
}
if (scoreFromLogs != totalScore)
{
Test_ExitWithResult(TEST_RESULT_ERROR, "Warning! Score from logs(%d) is different than actual score(%d). Make sure all of the score adjustments use the ADJUST_SCORE macro\n", scoreFromLogs, totalScore);
}
MgbaPrintf_("Total: %d\n", totalScore);
}
static void ClearAiLog(u32 battlerId)
{
u32 i, j;
for (i = 0; i < MAX_MON_MOVES; i++)
{
struct AILogLine *logs = DATA.aiLogLines[battlerId][i];
for (j = 0; j < MAX_AI_LOG_LINES; j++)
memset(&logs[j], 0, sizeof(struct AILogLine));
}
DATA.aiLogPrintedForMove[battlerId] = 0;
}
void TestRunner_Battle_CheckAiMoveScores(u32 battlerId)
{
s32 i;
struct ExpectedAIAction *aiAction;
const char *filename = gTestRunnerState.test->filename;
s32 turn = gBattleResults.battleTurnCounter;
for (i = 0; i < MAX_AI_SCORE_COMPARISION_PER_TURN; i++)
{
struct ExpectedAiScore *scoreCtx = &DATA.expectedAiScores[battlerId][turn][i];
if (scoreCtx->set)
{
u32 moveId1 = gBattleMons[battlerId].moves[scoreCtx->moveSlot1];
s32 target = scoreCtx->target;
s32 *scores = gBattleStruct->aiFinalScore[battlerId][target];
if (scoreCtx->toValue)
{
PrintAiMoveLog(battlerId, scoreCtx->moveSlot1, moveId1, scores[scoreCtx->moveSlot1]);
if (!CheckComparision(scores[scoreCtx->moveSlot1], scoreCtx->value, scoreCtx->cmp))
{
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched SCORE_%s_VAL %S %d, got %d",
filename, scoreCtx->sourceLine, sCmpToStringTable[scoreCtx->cmp], gMoveNames[moveId1], scoreCtx->value, scores[scoreCtx->moveSlot1]);
}
}
else
{
u32 moveId2 = gBattleMons[battlerId].moves[scoreCtx->moveSlot2];
PrintAiMoveLog(battlerId, scoreCtx->moveSlot1, moveId1, scores[scoreCtx->moveSlot1]);
PrintAiMoveLog(battlerId, scoreCtx->moveSlot2, moveId2, scores[scoreCtx->moveSlot2]);
if (!CheckComparision(scores[scoreCtx->moveSlot1], scores[scoreCtx->moveSlot2], scoreCtx->cmp))
{
Test_ExitWithResult(TEST_RESULT_FAIL, "%s:%d: Unmatched SCORE_%s, got %S: %d, %S: %d",
filename, scoreCtx->sourceLine, sCmpToStringTable[scoreCtx->cmp], gMoveNames[moveId1], scores[scoreCtx->moveSlot1], gMoveNames[moveId2], scores[scoreCtx->moveSlot2]);
}
}
}
}
// We need to make sure that the expected move has the best score. We have to rule out a situation where the expected move is used, but it has the same number of points as some other moves.
aiAction = &DATA.expectedAiActions[battlerId][DATA.aiActionsPlayed[battlerId]];
if (aiAction->actionSet && !aiAction->pass)
{
s32 target = aiAction->target;
// AI's move targets self, but points for this move are distributed for all other battlers
if (aiAction->target == battlerId)
{
for (i = 0; i < MAX_BATTLERS_COUNT; i++)
{
if (i != battlerId && IsBattlerAlive(i))
CheckIfMaxScoreEqualExpectMove(battlerId, i, aiAction, filename);
}
}
else
{
CheckIfMaxScoreEqualExpectMove(battlerId, target, aiAction, filename);
}
}
}
static s32 TryExp(s32 i, s32 n, u32 battlerId, u32 oldExp, u32 newExp)
{
struct QueuedExpEvent *event;
@ -812,6 +1141,7 @@ static s32 TryMessage(s32 i, s32 n, const u8 *string)
continue;
event = &DATA.queuedEvents[i].as.message;
// MgbaPrintf_("Looking for: %S Found: %S\n", event->pattern, string); // Useful for debugging.
for (j = k = 0; ; j++, k++)
{
if (event->pattern[k] == CHAR_SPACE)
@ -966,7 +1296,7 @@ static const char *const sEventTypeMacros[] =
void TestRunner_Battle_AfterLastTurn(void)
{
const struct BattleTest *test = gTestRunnerState.test->data;
const struct BattleTest *test = GetBattleTest();
if (DATA.turns - 1 != DATA.lastActionTurn)
{
@ -1099,7 +1429,7 @@ static bool32 BattleTest_HandleExitWithResult(void *data, enum TestResult result
void Randomly(u32 sourceLine, u32 passes, u32 trials, struct RandomlyContext ctx)
{
const struct BattleTest *test = gTestRunnerState.test->data;
const struct BattleTest *test = GetBattleTest();
INVALID_IF(STATE->trials != 0, "PASSES_RANDOMLY can only be used once per test");
INVALID_IF(test->resultsSize > 0, "PASSES_RANDOMLY is incompatible with results");
INVALID_IF(passes > trials, "%d passes specified, but only %d trials", passes, trials);
@ -1128,6 +1458,19 @@ void RNGSeed_(u32 sourceLine, u32 seed)
DATA.recordedBattle.rngSeed = seed;
}
void AIFlags_(u32 sourceLine, u32 flags)
{
INVALID_IF(!IsAITest(), "AI_FLAGS is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST");
DATA.recordedBattle.AI_scripts = flags;
DATA.hasAI = TRUE;
}
void AILogScores(u32 sourceLine)
{
INVALID_IF(!IsAITest(), "AI_LOG is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST");
DATA.logAI = TRUE;
}
const struct TestRunner gBattleTestRunner =
{
.estimateCost = BattleTest_EstimateCost,
@ -1327,7 +1670,7 @@ void Item_(u32 sourceLine, u32 item)
SetMonData(DATA.currentMon, MON_DATA_HELD_ITEM, &item);
}
void Moves_(u32 sourceLine, const u16 moves[MAX_MON_MOVES])
void Moves_(u32 sourceLine, u16 moves[MAX_MON_MOVES])
{
s32 i;
INVALID_IF(!DATA.currentMon, "Moves outside of PLAYER/OPPONENT");
@ -1392,11 +1735,16 @@ static const char *const sBattlerIdentifiersDoubles[] =
static const char *BattlerIdentifier(s32 battlerId)
{
const struct BattleTest *test = gTestRunnerState.test->data;
const struct BattleTest *test = GetBattleTest();
switch (test->type)
{
case BATTLE_TEST_SINGLES: return sBattlerIdentifiersSingles[battlerId];
case BATTLE_TEST_DOUBLES: return sBattlerIdentifiersDoubles[battlerId];
case BATTLE_TEST_SINGLES:
case BATTLE_TEST_WILD:
case BATTLE_TEST_AI_SINGLES:
return sBattlerIdentifiersSingles[battlerId];
case BATTLE_TEST_DOUBLES:
case BATTLE_TEST_AI_DOUBLES:
return sBattlerIdentifiersDoubles[battlerId];
}
return "<unknown>";
}
@ -1446,18 +1794,7 @@ void TestRunner_Battle_CheckBattleRecordActionType(u32 battlerId, u32 recordInde
switch (DATA.battleRecordTypes[battlerId][recordIndex])
{
case RECORDED_ACTION_TYPE:
switch (DATA.recordedBattle.battleRecord[battlerId][recordIndex])
{
case B_ACTION_USE_MOVE:
actualMacro = "MOVE";
break;
case B_ACTION_SWITCH:
actualMacro = "SWITCH";
break;
case B_ACTION_USE_ITEM:
actualMacro = "USE_ITEM";
break;
}
actualMacro = sBattleActionNames[DATA.recordedBattle.battleRecord[battlerId][recordIndex]];
break;
case RECORDED_PARTY_INDEX:
actualMacro = "SEND_OUT";
@ -1517,15 +1854,30 @@ static void SetSlowerThan(s32 battlerId)
DATA.slowerThan[battlerId & BIT_SIDE][DATA.currentMonIndexes[battlerId]] |= slowerThan;
}
static void SetAiActionToPass(u32 sourceLine, s32 battlerId)
{
DATA.expectedAiActions[battlerId][DATA.expectedAiActionIndex[battlerId]].actionSet = TRUE;
DATA.expectedAiActions[battlerId][DATA.expectedAiActionIndex[battlerId]].sourceLine = sourceLine;
DATA.expectedAiActions[battlerId][DATA.expectedAiActionIndex[battlerId]].pass = TRUE;
DATA.expectedAiActionIndex[battlerId]++;
}
void CloseTurn(u32 sourceLine)
{
s32 i;
INVALID_IF(DATA.turnState != TURN_OPEN, "Nested TURN");
DATA.turnState = TURN_CLOSING;
// If Move was not specified always use Celebrate. In AI Tests allow any taken action.
for (i = 0; i < STATE->battlersCount; i++)
{
if (!(DATA.actionBattlers & (1 << i)))
Move(sourceLine, &gBattleMons[i], (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE });
{
if (IsAITest() && (i & BIT_SIDE) == B_SIDE_OPPONENT) // If Move was not specified, allow any move used.
SetAiActionToPass(sourceLine, i);
else
Move(sourceLine, &gBattleMons[i], (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE });
}
}
DATA.turnState = TURN_CLOSED;
DATA.turns++;
@ -1541,59 +1893,12 @@ static struct Pokemon *CurrentMon(s32 battlerId)
return &party[DATA.currentMonIndexes[battlerId]];
}
void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
s32 MoveGetTarget(s32 battlerId, u32 moveId, struct MoveContext *ctx, u32 sourceLine)
{
s32 i;
s32 battlerId = battler - gBattleMons;
struct Pokemon *mon = CurrentMon(battlerId);
u32 moveId, moveSlot;
s32 target;
INVALID_IF(DATA.turnState == TURN_CLOSED, "MOVE outside TURN");
if (ctx.explicitMove)
s32 target = battlerId;
if (ctx->explicitTarget)
{
INVALID_IF(ctx.move == MOVE_NONE || ctx.move >= MOVES_COUNT, "Illegal move: %d", ctx.move);
for (i = 0; i < MAX_MON_MOVES; i++)
{
moveId = GetMonData(mon, MON_DATA_MOVE1 + i);
if (moveId == ctx.move)
{
moveSlot = i;
break;
}
else if (moveId == MOVE_NONE)
{
INVALID_IF(DATA.explicitMoves[battlerId & BIT_SIDE] & (1 << DATA.currentMonIndexes[battlerId]), "Missing explicit %S", gMoveNames[ctx.move]);
SetMonData(mon, MON_DATA_MOVE1 + i, &ctx.move);
SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[ctx.move].pp);
moveSlot = i;
moveId = ctx.move;
break;
}
}
INVALID_IF(i == MAX_MON_MOVES, "Too many different moves for %s", BattlerIdentifier(battlerId));
}
else if (ctx.explicitMoveSlot)
{
moveSlot = ctx.moveSlot;
moveId = GetMonData(mon, MON_DATA_MOVE1 + moveSlot);
INVALID_IF(moveId == MOVE_NONE, "Empty moveSlot: %d", ctx.moveSlot);
}
else
{
INVALID("No move or moveSlot");
}
if (ctx.explicitMegaEvolve && ctx.megaEvolve)
moveSlot |= RET_MEGA_EVOLUTION;
if (ctx.explicitUltraBurst && ctx.ultraBurst)
moveSlot |= RET_ULTRA_BURST;
if (ctx.explicitTarget)
{
target = ctx.target - gBattleMons;
target = ctx->target - gBattleMons;
}
else
{
@ -1609,7 +1914,11 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
}
else if (move->target == MOVE_TARGET_SELECTED)
{
INVALID_IF(STATE->battlersCount > 2, "%S requires explicit target", gMoveNames[moveId]);
// In AI Doubles not specified target allows any target for EXPECT_MOVE.
if (GetBattleTest()->type != BATTLE_TEST_AI_DOUBLES)
{
INVALID_IF(STATE->battlersCount > 2, "%S requires explicit target", gMoveNames[moveId]);
}
target = BATTLE_OPPOSITE(battlerId);
}
@ -1623,9 +1932,74 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
}
else
{
INVALID("%S requires explicit target", gMoveNames[moveId]);
// In AI Doubles not specified target allows any target for EXPECT_MOVE.
if (GetBattleTest()->type != BATTLE_TEST_AI_DOUBLES)
{
INVALID("%S requires explicit target", gMoveNames[moveId]);
}
}
}
return target;
}
void MoveGetIdAndSlot(s32 battlerId, struct MoveContext *ctx, u32 *moveId, u32 *moveSlot, u32 sourceLine)
{
u32 i;
struct Pokemon *mon = CurrentMon(battlerId);
if (ctx->explicitMove)
{
INVALID_IF(ctx->move == MOVE_NONE || ctx->move >= MOVES_COUNT, "Illegal move: %d", ctx->move);
for (i = 0; i < MAX_MON_MOVES; i++)
{
*moveId = GetMonData(mon, MON_DATA_MOVE1 + i);
if (*moveId == ctx->move)
{
*moveSlot = i;
break;
}
else if (*moveId == MOVE_NONE)
{
INVALID_IF(DATA.explicitMoves[battlerId & BIT_SIDE] & (1 << DATA.currentMonIndexes[battlerId]), "Missing explicit %S", gMoveNames[ctx->move]);
SetMonData(mon, MON_DATA_MOVE1 + i, &ctx->move);
SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[ctx->move].pp);
*moveSlot = i;
*moveId = ctx->move;
break;
}
}
INVALID_IF(i == MAX_MON_MOVES, "Too many different moves for %s", BattlerIdentifier(battlerId));
}
else if (ctx->explicitMoveSlot)
{
*moveSlot = ctx->moveSlot;
*moveId = GetMonData(mon, MON_DATA_MOVE1 + *moveSlot);
INVALID_IF(moveId == MOVE_NONE, "Empty moveSlot: %d", ctx->moveSlot);
}
else
{
INVALID("No move or moveSlot");
}
if (ctx->explicitMegaEvolve && ctx->megaEvolve)
*moveSlot |= RET_MEGA_EVOLUTION;
if (ctx->explicitUltraBurst && ctx->ultraBurst)
*moveSlot |= RET_ULTRA_BURST;
}
void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
{
s32 i;
s32 battlerId = battler - gBattleMons;
u32 moveId, moveSlot;
s32 target;
INVALID_IF(DATA.turnState == TURN_CLOSED, "MOVE outside TURN");
INVALID_IF(IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT, "MOVE is not allowed for opponent in AI tests. Use EXPECT_MOVE instead");
MoveGetIdAndSlot(battlerId, &ctx, &moveId, &moveSlot, sourceLine);
target = MoveGetTarget(battlerId, moveId, &ctx, sourceLine);
if (ctx.explicitHit)
DATA.battleRecordTurns[DATA.turns][battlerId].hit = 1 + ctx.hit;
@ -1672,6 +2046,153 @@ void ForcedMove(u32 sourceLine, struct BattlePokemon *battler)
}
}
static void TryMarkExpectMove(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext *ctx)
{
s32 battlerId = battler - gBattleMons;
u32 moveId, moveSlot, id;
s32 target;
INVALID_IF(DATA.turnState == TURN_CLOSED, "EXPECT_MOVE outside TURN");
INVALID_IF(!IsAITest(), "EXPECT_MOVE is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST");
MoveGetIdAndSlot(battlerId, ctx, &moveId, &moveSlot, sourceLine);
target = MoveGetTarget(battlerId, moveId, ctx, sourceLine);
id = DATA.expectedAiActionIndex[battlerId];
DATA.expectedAiActions[battlerId][id].type = B_ACTION_USE_MOVE;
DATA.expectedAiActions[battlerId][id].moveSlots |= gBitTable[moveSlot];
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;
if (ctx->explicitNotExpected)
DATA.expectedAiActions[battlerId][id].notMove = ctx->notExpected;
DATA.actionBattlers |= 1 << battlerId;
DATA.moveBattlers |= 1 << battlerId;
}
void ExpectMove(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
{
s32 battlerId = battler - gBattleMons;
TryMarkExpectMove(sourceLine, battler, &ctx);
DATA.expectedAiActionIndex[battlerId]++;
}
void ExpectSendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex)
{
s32 i, id;
s32 battlerId = battler - gBattleMons;
INVALID_IF(DATA.turnState == TURN_CLOSED, "EXPECT_SEND_OUT outside TURN");
INVALID_IF(!IsAITest(), "EXPECT_SEND_OUT is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST");
INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "EXPECT_SEND_OUT to invalid party index");
for (i = 0; i < STATE->battlersCount; i++)
{
if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE))
INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "EXPECT_SEND_OUT to battler");
}
if (!(DATA.actionBattlers & (1 << battlerId)))
{
const struct BattleTest *test = GetBattleTest();
if (IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT) // If Move was not specified, allow any move used.
SetAiActionToPass(sourceLine, battlerId);
else
Move(sourceLine, battler, (struct MoveContext) { move: MOVE_CELEBRATE, explicitMove: TRUE });
}
DATA.currentMonIndexes[battlerId] = partyIndex;
DATA.actionBattlers |= 1 << battlerId;
id = DATA.expectedAiActionIndex[battlerId];
DATA.expectedAiActions[battlerId][id].type = B_ACTION_SWITCH;
DATA.expectedAiActions[battlerId][id].target = partyIndex;
DATA.expectedAiActions[battlerId][id].sourceLine = sourceLine;
DATA.expectedAiActions[battlerId][id].actionSet = TRUE;
DATA.expectedAiActionIndex[battlerId]++;
}
s32 GetAiMoveTargetForScoreCompare(u32 battlerId, u32 moveId, struct MoveContext *ctx, u32 sourceLine)
{
s32 target;
// In Single Battles ai always targets the opposing mon.
if (GetBattleTest()->type == BATTLE_TEST_AI_SINGLES)
{
target = BATTLE_OPPOSITE(battlerId);
}
else
{
// TODO: Fix ai targeting self in double battles.
INVALID_IF(!ctx->explicitTarget, "%S requires explicit target for score comparison in doubles", gMoveNames[moveId]);
target = MoveGetTarget(battlerId, moveId, ctx, sourceLine);
}
return target;
}
void Score(u32 sourceLine, struct BattlePokemon *battler, u32 cmp, bool32 toValue, struct TestAIScoreStruct cmpCtx)
{
u32 moveSlot1, moveSlot2;
s32 i, target;
struct MoveContext moveCtx = {0};
s32 battlerId = battler - gBattleMons;
s32 turn = DATA.turns;
INVALID_IF(!IsAITest(), "SCORE_%s%s is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST", sCmpToStringTable[cmp], (toValue == TRUE) ? "_VAL" : "");
for (i = 0; i < MAX_AI_SCORE_COMPARISION_PER_TURN; i++)
{
if (!DATA.expectedAiScores[battlerId][turn][i].set)
break;
}
INVALID_IF(i == MAX_AI_SCORE_COMPARISION_PER_TURN, "Too many EXPECTs in TURN");
moveCtx.move = cmpCtx.move1;
moveCtx.explicitMove = cmpCtx.explicitMove1;
moveCtx.target = cmpCtx.target;
moveCtx.explicitTarget = cmpCtx.explicitTarget;
MoveGetIdAndSlot(battlerId, &moveCtx, &cmpCtx.move1, &moveSlot1, sourceLine);
// For ai moves, target is never self.
target = GetAiMoveTargetForScoreCompare(battlerId, cmpCtx.move1, &moveCtx, sourceLine);
DATA.expectedAiScores[battlerId][turn][i].target = target;
DATA.expectedAiScores[battlerId][turn][i].moveSlot1 = moveSlot1;
DATA.expectedAiScores[battlerId][turn][i].cmp = cmp;
DATA.expectedAiScores[battlerId][turn][i].toValue = toValue;
if (toValue)
{
DATA.expectedAiScores[battlerId][turn][i].value = cmpCtx.valueOrMoveId2;
}
else
{
moveCtx.move = cmpCtx.valueOrMoveId2;
moveCtx.explicitMove = cmpCtx.explicitValueOrMoveId2;
moveCtx.target = cmpCtx.target;
moveCtx.explicitTarget = cmpCtx.explicitTarget;
MoveGetIdAndSlot(battlerId, &moveCtx, &cmpCtx.valueOrMoveId2, &moveSlot2, sourceLine);
DATA.expectedAiScores[battlerId][turn][i].moveSlot2 = moveSlot2;
}
DATA.expectedAiScores[battlerId][turn][i].sourceLine = sourceLine;
DATA.expectedAiScores[battlerId][turn][i].set = TRUE;
}
void ExpectMoves(u32 sourceLine, struct BattlePokemon *battler, bool32 notExpected, struct FourMoves moves)
{
s32 battlerId = battler - gBattleMons;
u32 i;
for (i = 0; i < MAX_BATTLERS_COUNT; i++)
{
if (moves.moves[i] != MOVE_NONE)
{
struct MoveContext ctx = {0};
ctx.move = moves.moves[i];
ctx.explicitMove = ctx.explicitNotExpected = TRUE;
ctx.notExpected = notExpected;
TryMarkExpectMove(sourceLine, battler, &ctx);
}
}
DATA.expectedAiActionIndex[battlerId]++;
}
void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex)
{
s32 i;
@ -1679,6 +2200,7 @@ void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex)
INVALID_IF(DATA.turnState == TURN_CLOSED, "SWITCH outside TURN");
INVALID_IF(DATA.actionBattlers & (1 << battlerId), "Multiple battler actions");
INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index");
INVALID_IF(IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT, "SWITCH is not allowed for opponent in AI tests. Use EXPECT_SWITCH instead");
for (i = 0; i < STATE->battlersCount; i++)
{
@ -1693,6 +2215,32 @@ void Switch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex)
DATA.actionBattlers |= 1 << battlerId;
}
void ExpectSwitch(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex)
{
s32 i, id;
s32 battlerId = battler - gBattleMons;
INVALID_IF(DATA.turnState == TURN_CLOSED, "EXPECT_SWITCH outside TURN");
INVALID_IF(!IsAITest(), "EXPECT_SWITCH is usable only in AI_SINGLE_BATTLE_TEST & AI_DOUBLE_BATTLE_TEST");
INVALID_IF(DATA.actionBattlers & (1 << battlerId), "Multiple battler actions");
INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "EXPECT_SWITCH to invalid party index");
for (i = 0; i < STATE->battlersCount; i++)
{
if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE))
INVALID_IF(DATA.currentMonIndexes[i] == partyIndex, "EXPECT_SWITCH to battler");
}
DATA.currentMonIndexes[battlerId] = partyIndex;
DATA.actionBattlers |= 1 << battlerId;
id = DATA.expectedAiActionIndex[battlerId];
DATA.expectedAiActions[battlerId][id].type = B_ACTION_SWITCH;
DATA.expectedAiActions[battlerId][id].target = partyIndex;
DATA.expectedAiActions[battlerId][id].sourceLine = sourceLine;
DATA.expectedAiActions[battlerId][id].actionSet = TRUE;
DATA.expectedAiActionIndex[battlerId]++;
}
void SkipTurn(u32 sourceLine, struct BattlePokemon *battler)
{
s32 battlerId = battler - gBattleMons;
@ -1706,6 +2254,7 @@ void SendOut(u32 sourceLine, struct BattlePokemon *battler, u32 partyIndex)
s32 battlerId = battler - gBattleMons;
INVALID_IF(DATA.turnState == TURN_CLOSED, "SEND_OUT outside TURN");
INVALID_IF(partyIndex >= ((battlerId & BIT_SIDE) == B_SIDE_PLAYER ? DATA.playerPartySize : DATA.opponentPartySize), "SWITCH to invalid party index");
INVALID_IF(IsAITest() && (battlerId & BIT_SIDE) == B_SIDE_OPPONENT, "SEND_OUT is not allowed for opponent in AI tests. Use EXPECT_SEND_OUT instead");
for (i = 0; i < STATE->battlersCount; i++)
{
if (battlerId != i && (battlerId & BIT_SIDE) == (i & BIT_SIDE))
@ -1925,7 +2474,6 @@ void QueueExp(u32 sourceLine, struct BattlePokemon *battler, struct ExpEventCont
};
}
void QueueMessage(u32 sourceLine, const u8 *pattern)
{
INVALID_IF(!STATE->runScene, "MESSAGE outside of SCENE");
@ -1994,3 +2542,45 @@ u32 TestRunner_Battle_GetForcedAbility(u32 side, u32 partyIndex)
{
return DATA.forcedAbilities[side][partyIndex];
}
// TODO: Consider storing the last successful i and searching from i+1
// to improve performance.
struct AILogLine *GetLogLine(u32 battlerId, u32 moveIndex)
{
s32 i, j;
for (i = 0; i < MAX_AI_LOG_LINES; i++)
{
struct AILogLine *log = &DATA.aiLogLines[battlerId][moveIndex][i];
if (log->file == NULL)
{
return log;
}
}
Test_ExitWithResult(TEST_RESULT_ERROR, "Too many AI log lines");
}
void TestRunner_Battle_AILogScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score, bool32 setScore)
{
s32 i;
struct AILogLine *log;
if (!DATA.logAI) return;
log = GetLogLine(battlerId, moveIndex);
log->file = file;
log->line = line;
log->score = score;
log->set = setScore;
}
void TestRunner_Battle_AISetScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score)
{
TestRunner_Battle_AILogScore(file, line, battlerId, moveIndex, score, TRUE);
}
void TestRunner_Battle_AIAdjustScore(const char *file, u32 line, u32 battlerId, u32 moveIndex, s32 score)
{
TestRunner_Battle_AILogScore(file, line, battlerId, moveIndex, score, FALSE);
}