Merge branch '_RHH/master' into _RHH/upcoming

# Conflicts:
#	ld_script_modern.ld
#	src/battle_ai_switch_items.c
This commit is contained in:
Eduardo Quezada 2024-02-01 12:52:31 -03:00
commit 09d12fb154
11 changed files with 139 additions and 58 deletions

View file

@ -7,11 +7,11 @@ JASC-PAL
201 201 201
169 169 169
129 129 129
249 153 161
233 49 49
193 33 41
145 17 33
249 153 161
106 106 106
37 37 37
106 106 106
0 0 0
106 106 106
193 33 41
141 251 184
52 66 162

View file

@ -337,7 +337,7 @@ struct AiLogicData
bool8 shouldSwitchMon; // Because all available moves have no/little effect. Each bit per battler.
u8 monToSwitchId[MAX_BATTLERS_COUNT]; // ID of the mon to switch.
bool8 weatherHasEffect; // The same as WEATHER_HAS_EFFECT. Stored here, so it's called only once.
u8 mostSuitableMonId; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId.
u8 mostSuitableMonId[MAX_BATTLERS_COUNT]; // Stores result of GetMostSuitableMonToSwitchInto, which decides which generic mon the AI would switch into if they decide to switch. This can be overruled by specific mons found in ShouldSwitch; the final resulting mon is stored in AI_monToSwitchIntoId.
struct SwitchinCandidate switchinCandidate; // Struct used for deciding which mon to switch to in battle_ai_switch_items.c
};

View file

@ -18,6 +18,11 @@ SECTIONS {
ALIGN(4)
{
__ewram_start = .;
/*
We link malloc.o here to prevent `gHeap` from landing in the middle of EWRAM.
Otherwise this causes corruption issues on some ld versions
*/
gflib/malloc.o(ewram_data);
*(.ewram*)
__ewram_end = .;
} > EWRAM

View file

@ -5,6 +5,7 @@
#include "battle_anim.h"
#include "battle_ai_util.h"
#include "battle_ai_main.h"
#include "battle_controllers.h"
#include "battle_factory.h"
#include "battle_setup.h"
#include "battle_z_move.h"
@ -455,11 +456,21 @@ void SetAiLogicDataForTurn(struct AiLogicData *aiData)
}
}
static bool32 AI_SwitchMonIfSuitable(u32 battler)
static bool32 AI_SwitchMonIfSuitable(u32 battler, bool32 doubleBattle)
{
u32 monToSwitchId = AI_DATA->mostSuitableMonId;
if (monToSwitchId != PARTY_SIZE)
u32 monToSwitchId = AI_DATA->mostSuitableMonId[battler];
if (monToSwitchId != PARTY_SIZE && IsValidForBattle(&GetBattlerParty(battler)[monToSwitchId]))
{
gBattleMoveDamage = monToSwitchId;
// Edge case: See if partner already chose to switch into the same mon
if (doubleBattle)
{
u32 partner = BATTLE_PARTNER(battler);
if (AI_DATA->shouldSwitchMon & gBitTable[partner] && AI_DATA->monToSwitchId[partner] == monToSwitchId)
{
return FALSE;
}
}
AI_DATA->shouldSwitchMon |= gBitTable[battler];
AI_DATA->monToSwitchId[battler] = monToSwitchId;
return TRUE;
@ -496,7 +507,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle)
break;
}
}
if (i == MAX_BATTLERS_COUNT && AI_SwitchMonIfSuitable(battler))
if (i == MAX_BATTLERS_COUNT && AI_SwitchMonIfSuitable(battler, doubleBattle))
return TRUE;
}
else
@ -507,7 +518,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle)
break;
}
if (i == MAX_MON_MOVES && AI_SwitchMonIfSuitable(battler))
if (i == MAX_MON_MOVES && AI_SwitchMonIfSuitable(battler, doubleBattle))
return TRUE;
}
@ -519,7 +530,7 @@ static bool32 AI_ShouldSwitchIfBadMoves(u32 battler, bool32 doubleBattle)
&& IsTruantMonVulnerable(battler, gBattlerTarget)
&& gDisableStructs[battler].truantCounter
&& gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2
&& AI_SwitchMonIfSuitable(battler))
&& AI_SwitchMonIfSuitable(battler, doubleBattle))
{
return TRUE;
}

View file

@ -167,7 +167,7 @@ static bool32 HasBadOdds(u32 battler, bool32 emitResult)
}
// If we don't have any other viable options, don't switch out
if (AI_DATA->mostSuitableMonId == PARTY_SIZE)
if (AI_DATA->mostSuitableMonId[battler] == PARTY_SIZE)
return FALSE;
// Start assessing whether or not mon has bad odds
@ -603,12 +603,12 @@ static bool32 ShouldSwitchIfAbilityBenefit(u32 battler, bool32 emitResult)
moduloChance = 4; //25%
//Attempt to cure bad ailment
if (gBattleMons[battler].status1 & (STATUS1_SLEEP | STATUS1_FREEZE | STATUS1_TOXIC_POISON)
&& AI_DATA->mostSuitableMonId != PARTY_SIZE)
&& AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE)
break;
//Attempt to cure lesser ailment
if ((gBattleMons[battler].status1 & STATUS1_ANY)
&& (gBattleMons[battler].hp >= gBattleMons[battler].maxHP / 2)
&& AI_DATA->mostSuitableMonId != PARTY_SIZE
&& AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE
&& Random() % (moduloChance*chanceReducer) == 0)
break;
@ -620,7 +620,7 @@ static bool32 ShouldSwitchIfAbilityBenefit(u32 battler, bool32 emitResult)
if (gBattleMons[battler].status1 & STATUS1_ANY)
return FALSE;
if ((gBattleMons[battler].hp <= ((gBattleMons[battler].maxHP * 2) / 3))
&& AI_DATA->mostSuitableMonId != PARTY_SIZE
&& AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE
&& Random() % (moduloChance*chanceReducer) == 0)
break;
@ -856,7 +856,7 @@ static bool32 ShouldSwitchIfEncored(u32 battler, bool32 emitResult)
return FALSE;
// If not Encored or if no good switchin, don't switch
if (gDisableStructs[battler].encoredMove == MOVE_NONE || AI_DATA->mostSuitableMonId == PARTY_SIZE)
if (gDisableStructs[battler].encoredMove == MOVE_NONE || AI_DATA->mostSuitableMonId[battler] == PARTY_SIZE)
return FALSE;
// Otherwise 50% chance to switch out
@ -890,7 +890,7 @@ static bool32 AreAttackingStatsLowered(u32 battler, bool32 emitResult)
// 50% chance if attack at -2 and have a good candidate mon
else if (attackingStage == DEFAULT_STAT_STAGE - 2)
{
if (AI_DATA->mostSuitableMonId != PARTY_SIZE && (Random() & 1))
if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (Random() & 1))
{
gBattleStruct->AI_monToSwitchIntoId[battler] = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
@ -915,7 +915,7 @@ static bool32 AreAttackingStatsLowered(u32 battler, bool32 emitResult)
// 50% chance if attack at -2 and have a good candidate mon
else if (spAttackingStage == DEFAULT_STAT_STAGE - 2)
{
if (AI_DATA->mostSuitableMonId != PARTY_SIZE && (Random() & 1))
if (AI_DATA->mostSuitableMonId[battler] != PARTY_SIZE && (Random() & 1))
{
gBattleStruct->AI_monToSwitchIntoId[battler] = PARTY_SIZE;
BtlController_EmitTwoReturnValues(battler, 1, B_ACTION_SWITCH, 0);
@ -1064,7 +1064,7 @@ void AI_TrySwitchOrUseItem(u32 battler)
{
if (gBattleStruct->AI_monToSwitchIntoId[battler] == PARTY_SIZE)
{
s32 monToSwitchId = AI_DATA->mostSuitableMonId;
s32 monToSwitchId = AI_DATA->mostSuitableMonId[battler];
if (monToSwitchId == PARTY_SIZE)
{
if (!(gBattleTypeFlags & BATTLE_TYPE_DOUBLE))

View file

@ -4075,7 +4075,7 @@ static void HandleTurnActionSelectionState(void)
if ((gBattleTypeFlags & BATTLE_TYPE_HAS_AI || IsWildMonSmart())
&& (BattlerHasAi(battler) && !(gBattleTypeFlags & BATTLE_TYPE_PALACE)))
{
AI_DATA->mostSuitableMonId = GetMostSuitableMonToSwitchInto(battler, FALSE);
AI_DATA->mostSuitableMonId[battler] = GetMostSuitableMonToSwitchInto(battler, FALSE);
gBattleStruct->aiMoveOrAction[battler] = ComputeBattleAiScores(battler);
}
// fallthrough

View file

@ -6550,7 +6550,7 @@ static u8 ItemHealHp(u32 battler, u32 itemId, bool32 end2, bool32 percentHeal)
gBattlescriptCurrInstr = BattleScript_ItemHealHP_RemoveItemRet;
}
if (gBattleResources->flags->flags[battler] & RESOURCE_FLAG_EMERGENCY_EXIT
&& GetNonDynamaxMaxHP(battler) > gBattleMons[battler].maxHP / 2)
&& GetNonDynamaxHP(battler) >= GetNonDynamaxMaxHP(battler) / 2)
gBattleResources->flags->flags[battler] &= ~RESOURCE_FLAG_EMERGENCY_EXIT;
return ITEM_HP_CHANGE;

View file

@ -8,10 +8,7 @@ SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage")
OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(262); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(player, MOVE_SUPER_FANG);
SEND_OUT(opponent, 1);
}
TURN { MOVE(player, MOVE_SUPER_FANG); SEND_OUT(opponent, 1); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SUPER_FANG, player);
HP_BAR(opponent);
@ -19,16 +16,14 @@ SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage")
}
}
SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage after a restore hp hold effect was used")
SINGLE_BATTLE_TEST("Emergency Exit does not switch out when going below 50% max-HP but healed via held item back above the threshold")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET)
OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(262); Item(ITEM_SITRUS_BERRY); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(player, MOVE_SUPER_FANG);
}
TURN { MOVE(player, MOVE_SUPER_FANG); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SUPER_FANG, player);
HP_BAR(opponent);
@ -36,3 +31,19 @@ SINGLE_BATTLE_TEST("Emergency Exit switches out when taking 50% max-hp damage af
NOT ABILITY_POPUP(opponent, ABILITY_EMERGENCY_EXIT);
}
}
SINGLE_BATTLE_TEST("Emergency Exit switches out when going below 50% max-HP but healing via held item is not enough to go back above the threshold")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET)
OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); MaxHP(263); HP(133); Item(ITEM_ORAN_BERRY); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_SUPER_FANG); SEND_OUT(opponent, 1); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SUPER_FANG, player);
HP_BAR(opponent);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
ABILITY_POPUP(opponent, ABILITY_EMERGENCY_EXIT);
}
}

View file

@ -702,3 +702,31 @@ AI_SINGLE_BATTLE_TEST("First Impression is not chosen if it's blocked by certain
TURN { EXPECT_MOVE(opponent, MOVE_LUNGE); }
}
}
AI_DOUBLE_BATTLE_TEST("AI will not try to switch for the same pokemon for 2 spots in a double battle")
{
u32 flags;
PARAMETRIZE {flags = AI_FLAG_SMART_SWITCHING; }
PARAMETRIZE {flags = 0; }
GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT | flags);
PLAYER(SPECIES_RATTATA);
PLAYER(SPECIES_RATTATA);
// No moves to damage player.
OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_HAUNTER) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_GENGAR) { Moves(MOVE_SHADOW_BALL); }
OPPONENT(SPECIES_RATICATE) { Moves(MOVE_HEADBUTT); }
} WHEN {
TURN { EXPECT_SWITCH(opponentLeft, 3); };
} SCENE {
MESSAGE("{PKMN} TRAINER LEAF withdrew Gengar!");
MESSAGE("{PKMN} TRAINER LEAF sent out Raticate!");
NONE_OF {
MESSAGE("{PKMN} TRAINER LEAF withdrew Haunter!");
MESSAGE("{PKMN} TRAINER LEAF sent out Raticate!");
}
}
}

View file

@ -30,7 +30,6 @@ void TestRunner_Battle(const struct Test *);
static bool32 MgbaOpen_(void);
static void MgbaExit_(u8 exitCode);
static s32 MgbaPuts_(const char *s);
static s32 MgbaVPrintf_(const char *fmt, va_list va);
static void Intr_Timer2(void);
@ -293,12 +292,6 @@ top:
color = "";
}
if (gTestRunnerState.result == TEST_RESULT_PASS
&& gTestRunnerState.result != gTestRunnerState.expectedResult)
{
MgbaPuts_("\e[31mPlease remove KNOWN_FAILING if this test intentionally PASSes\e[0m");
}
switch (gTestRunnerState.result)
{
case TEST_RESULT_FAIL:
@ -313,7 +306,10 @@ top:
}
break;
case TEST_RESULT_PASS:
result = "PASS";
if (gTestRunnerState.result != gTestRunnerState.expectedResult)
result = "KNOWN_FAILING_PASS";
else
result = "PASS";
break;
case TEST_RESULT_ASSUMPTION_FAIL:
result = "ASSUMPTION_FAIL";
@ -341,7 +337,12 @@ top:
}
if (gTestRunnerState.result == TEST_RESULT_PASS)
MgbaPrintf_(":P%s%s\e[0m", color, result);
{
if (gTestRunnerState.result != gTestRunnerState.expectedResult)
MgbaPrintf_(":U%s%s\e[0m", color, result);
else
MgbaPrintf_(":P%s%s\e[0m", color, result);
}
else if (gTestRunnerState.result == TEST_RESULT_ASSUMPTION_FAIL)
MgbaPrintf_(":A%s%s\e[0m", color, result);
else if (gTestRunnerState.result == TEST_RESULT_TODO)
@ -513,11 +514,6 @@ static void MgbaExit_(u8 exitCode)
asm("swi 0x3" :: "r" (_exitCode));
}
static s32 MgbaPuts_(const char *s)
{
return MgbaPrintf_("%s", s);
}
s32 MgbaPrintf_(const char *fmt, ...)
{
va_list va;

View file

@ -35,7 +35,7 @@
#define min(a, b) ((a) < (b) ? (a) : (b))
#define MAX_PROCESSES 32 // See also test/test.h
#define MAX_FAILED_TESTS_TO_LIST 100
#define MAX_SUMMARY_TESTS_TO_LIST 50
#define MAX_TEST_LIST_BUFFER_LENGTH 256
#define ARRAY_COUNT(arr) (sizeof((arr)) / sizeof((arr)[0]))
@ -54,11 +54,13 @@ struct Runner
char *output_buffer;
int passes;
int knownFails;
int knownFailsPassing;
int todos;
int assumptionFails;
int fails;
int results;
char failedTestNames[MAX_FAILED_TESTS_TO_LIST][MAX_TEST_LIST_BUFFER_LENGTH];
char failedTestNames[MAX_SUMMARY_TESTS_TO_LIST][MAX_TEST_LIST_BUFFER_LENGTH];
char knownFailingPassedTestNames[MAX_SUMMARY_TESTS_TO_LIST][MAX_TEST_LIST_BUFFER_LENGTH];
};
static unsigned nrunners = 0;
@ -107,6 +109,11 @@ static void handle_read(int i, struct Runner *runner)
case 'K':
runner->knownFails++;
goto add_to_results;
case 'U':
if (runner->knownFailsPassing < MAX_SUMMARY_TESTS_TO_LIST)
strcpy(runner->knownFailingPassedTestNames[runner->knownFailsPassing], runner->test_name);
runner->knownFailsPassing++;
goto add_to_results;
case 'T':
runner->todos++;
goto add_to_results;
@ -114,7 +121,7 @@ static void handle_read(int i, struct Runner *runner)
runner->assumptionFails++;
goto add_to_results;
case 'F':
if (runner->fails < MAX_FAILED_TESTS_TO_LIST)
if (runner->fails < MAX_SUMMARY_TESTS_TO_LIST)
strcpy(runner->failedTestNames[runner->fails], runner->test_name);
runner->fails++;
add_to_results:
@ -519,12 +526,14 @@ int main(int argc, char *argv[])
int exit_code = 0;
int passes = 0;
int knownFails = 0;
int knownFailsPassing = 0;
int todos = 0;
int assumptionFails = 0;
int fails = 0;
int results = 0;
char failedTestNames[MAX_FAILED_TESTS_TO_LIST * MAX_PROCESSES][MAX_TEST_LIST_BUFFER_LENGTH];
char failedTestNames[MAX_SUMMARY_TESTS_TO_LIST * MAX_PROCESSES][MAX_TEST_LIST_BUFFER_LENGTH];
char knownFailingPassedTestNames[MAX_SUMMARY_TESTS_TO_LIST * MAX_PROCESSES][MAX_TEST_LIST_BUFFER_LENGTH];
for (int i = 0; i < nrunners; i++)
{
@ -540,18 +549,25 @@ int main(int argc, char *argv[])
exit_code = WEXITSTATUS(wstatus);
passes += runners[i].passes;
knownFails += runners[i].knownFails;
for (int j = 0; j < runners[i].knownFailsPassing; j++)
{
if (j < MAX_SUMMARY_TESTS_TO_LIST)
strcpy(knownFailingPassedTestNames[fails], runners[i].knownFailingPassedTestNames[j]);
knownFailsPassing++;
}
todos += runners[i].todos;
assumptionFails += runners[i].assumptionFails;
for (int j = 0; j < runners[i].fails; j++)
{
if (j < MAX_FAILED_TESTS_TO_LIST)
if (j < MAX_SUMMARY_TESTS_TO_LIST)
strcpy(failedTestNames[fails], runners[i].failedTestNames[j]);
fails++;
}
results += runners[i].results;
}
qsort(failedTestNames, min(fails, MAX_FAILED_TESTS_TO_LIST), sizeof(char) * MAX_TEST_LIST_BUFFER_LENGTH, compare_strings);
qsort(failedTestNames, min(fails, MAX_SUMMARY_TESTS_TO_LIST), sizeof(char) * MAX_TEST_LIST_BUFFER_LENGTH, compare_strings);
qsort(knownFailingPassedTestNames, min(fails, MAX_SUMMARY_TESTS_TO_LIST), sizeof(char) * MAX_TEST_LIST_BUFFER_LENGTH, compare_strings);
if (results == 0)
{
@ -559,28 +575,42 @@ int main(int argc, char *argv[])
}
else
{
fprintf(stdout, "\n");
if (fails > 0)
{
fprintf(stdout, "\n- Tests \e[31mFAILED\e[0m : %d Add TESTS='X' to run tests with the defined prefix.\n", fails);
fprintf(stdout, "- Tests \e[31mFAILED\e[0m : %d Add TESTS='X' to run tests with the defined prefix.\n", fails);
for (int i = 0; i < fails; i++)
{
if (i >= MAX_FAILED_TESTS_TO_LIST)
if (i >= MAX_SUMMARY_TESTS_TO_LIST)
{
fprintf(stdout, " - \e[31mand %d more...\e[0m\n", fails - MAX_FAILED_TESTS_TO_LIST);
fprintf(stdout, " - \e[31mand %d more...\e[0m\n", fails - MAX_SUMMARY_TESTS_TO_LIST);
break;
}
fprintf(stdout, " - \e[31m%s\e[0m.\n", failedTestNames[i]);
}
}
fprintf(stdout, "- Tests \e[32mPASSED\e[0m: %d\n", passes);
if (knownFailsPassing > 0)
{
fprintf(stdout, "- \e[31mKNOWN_FAILING_PASSED\e[0m: %d \e[31mPlease remove KNOWN_FAILING if these tests intentionally PASS\e[0m\n", knownFailsPassing);
for (int i = 0; i < knownFailsPassing; i++)
{
if (i >= MAX_SUMMARY_TESTS_TO_LIST)
{
fprintf(stdout, " - \e[31mand %d more...\e[0m\n", knownFailsPassing - MAX_SUMMARY_TESTS_TO_LIST);
break;
}
fprintf(stdout, " - \e[31m%s\e[0m.\n", knownFailingPassedTestNames[i]);
}
}
fprintf(stdout, "- Tests \e[32mPASSED\e[0m: %d\n", passes);
if (knownFails > 0)
fprintf(stdout, "- Tests \e[33mKNOWN_FAILING\e[0m: %d\n", knownFails);
fprintf(stdout, "- Tests \e[33mKNOWN_FAILING\e[0m: %d\n", knownFails);
if (todos > 0)
fprintf(stdout, "- Tests \e[33mTO_DO\e[0m: %d\n", todos);
fprintf(stdout, "- Tests \e[33mTO_DO\e[0m: %d\n", todos);
if (assumptionFails > 0)
fprintf(stdout, "- \e[33mASSUMPTIONS_FAILED\e[0m: %d\n", assumptionFails);
fprintf(stdout, "- \e[33mASSUMPTIONS_FAILED\e[0m: %d\n", assumptionFails);
fprintf(stdout, "- Tests \e[34mTOTAL\e[0m: %d\n", results);
fprintf(stdout, "- Tests \e[34mTOTAL\e[0m: %d\n", results);
}
fprintf(stdout, "\n");