From 9d97537ee2d939dfef711d13799e0a2a91a49acf Mon Sep 17 00:00:00 2001 From: Martin Griffin Date: Sat, 3 Aug 2024 16:29:47 +0100 Subject: [PATCH] Fix speed ties (#4780) * Fix speed ties * fixup! Fix speed ties * fixup! Fix speed ties * fixup! fixup! Fix speed ties * fixup! Fix speed ties * Workaround for Comatose-Ditto interaction --- include/battle.h | 1 + include/battle_main.h | 1 + src/battle_ai_main.c | 4 +- src/battle_main.c | 76 +++++++++++++++++++++++++++++++--- test/battle/ability/comatose.c | 5 ++- test/battle/move.c | 59 ++++++++++++++++++++++++-- 6 files changed, 135 insertions(+), 11 deletions(-) diff --git a/include/battle.h b/include/battle.h index c48dc78835..1aa26a9470 100644 --- a/include/battle.h +++ b/include/battle.h @@ -795,6 +795,7 @@ struct BattleStruct u8 quickClawRandom[MAX_BATTLERS_COUNT]; u8 quickDrawRandom[MAX_BATTLERS_COUNT]; u8 shellSideArmCategory[MAX_BATTLERS_COUNT][MAX_BATTLERS_COUNT]; + u8 speedTieBreaks; // MAX_BATTLERS_COUNT! values. u8 boosterEnergyActivates; u8 distortedTypeMatchups; u8 categoryOverride; // for Z-Moves and Max Moves diff --git a/include/battle_main.h b/include/battle_main.h index 59a515b508..eb0af5aa77 100644 --- a/include/battle_main.h +++ b/include/battle_main.h @@ -75,6 +75,7 @@ s8 GetChosenMovePriority(u32 battlerId); s8 GetMovePriority(u32 battlerId, u16 move); s32 GetWhichBattlerFasterArgs(u32 battler1, u32 battler2, bool32 ignoreChosenMoves, u32 ability1, u32 ability2, u32 holdEffectBattler1, u32 holdEffectBattler2, u32 speedBattler1, u32 speedBattler2, s32 priority1, s32 priority2); +s32 GetWhichBattlerFasterOrTies(u32 battler1, u32 battler2, bool32 ignoreChosenMoves); s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves); void RunBattleScriptCommands_PopCallbacksStack(void); void RunBattleScriptCommands(void); diff --git a/src/battle_ai_main.c b/src/battle_ai_main.c index 08b8e4335c..63bab46ec9 100644 --- a/src/battle_ai_main.c +++ b/src/battle_ai_main.c @@ -2722,7 +2722,7 @@ static s32 AI_TryToFaint(u32 battlerAtk, u32 battlerDef, u32 move, s32 score) ADJUST_SCORE(SLOW_KILL); } else if (CanTargetFaintAi(battlerDef, battlerAtk) - && GetWhichBattlerFaster(battlerAtk, battlerDef, TRUE) != AI_IS_FASTER + && GetWhichBattlerFasterOrTies(battlerAtk, battlerDef, TRUE) != AI_IS_FASTER && GetMovePriority(battlerAtk, move) > 0) { ADJUST_SCORE(LAST_CHANCE); @@ -4117,7 +4117,7 @@ static u32 AI_CalcMoveEffectScore(u32 battlerAtk, u32 battlerDef, u32 move) if (IsStatBoostingBerry(item) && aiData->hpPercents[battlerAtk] > 60) ADJUST_SCORE(WEAK_EFFECT); else if (ShouldRestoreHpBerry(battlerAtk, item) && !CanAIFaintTarget(battlerAtk, battlerDef, 0) - && ((GetWhichBattlerFaster(battlerAtk, battlerDef, TRUE) == 1 && CanTargetFaintAiWithMod(battlerDef, battlerAtk, 0, 0)) + && ((GetWhichBattlerFasterOrTies(battlerAtk, battlerDef, TRUE) == 1 && CanTargetFaintAiWithMod(battlerDef, battlerAtk, 0, 0)) || !CanTargetFaintAiWithMod(battlerDef, battlerAtk, toHeal, 0))) ADJUST_SCORE(WEAK_EFFECT); // Recycle healing berry if we can't otherwise faint the target and the target wont kill us after we activate the berry } diff --git a/src/battle_main.c b/src/battle_main.c index 2b95bad975..9df19b9566 100644 --- a/src/battle_main.c +++ b/src/battle_main.c @@ -120,6 +120,7 @@ static void SpriteCB_UnusedBattleInit(struct Sprite *sprite); static void SpriteCB_UnusedBattleInit_Main(struct Sprite *sprite); static u32 Crc32B (const u8 *data, u32 size); static u32 GeneratePartyHash(const struct Trainer *trainer, u32 i); +static s32 Factorial(s32); EWRAM_DATA u16 gBattle_BG0_X = 0; EWRAM_DATA u16 gBattle_BG0_Y = 0; @@ -3807,6 +3808,8 @@ static void TryDoEventsBeforeFirstTurn(void) } #endif // TESTING + gBattleStruct->speedTieBreaks = RandomUniform(RNG_SPEED_TIE, 0, Factorial(MAX_BATTLERS_COUNT) - 1); + for (i = 0; i < gBattlersCount; i++) gBattlerByTurnOrder[i] = i; for (i = 0; i < gBattlersCount - 1; i++) @@ -3977,6 +3980,8 @@ void BattleTurnPassed(void) { s32 i; + gBattleStruct->speedTieBreaks = RandomUniform(RNG_SPEED_TIE, 0, Factorial(MAX_BATTLERS_COUNT) - 1); + TurnValuesCleanUp(TRUE); if (gBattleOutcome == 0) { @@ -4865,9 +4870,10 @@ s32 GetWhichBattlerFasterArgs(u32 battler1, u32 battler2, bool32 ignoreChosenMov strikesFirst = 1; else { - if (speedBattler1 == speedBattler2 && Random() & 1) + if (speedBattler1 == speedBattler2) { - strikesFirst = 0; // same speeds, same priorities + // same speeds, same priorities + strikesFirst = 0; } else if (speedBattler1 < speedBattler2) { @@ -4898,7 +4904,7 @@ s32 GetWhichBattlerFasterArgs(u32 battler1, u32 battler2, bool32 ignoreChosenMov return strikesFirst; } -s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) +s32 GetWhichBattlerFasterOrTies(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) { s32 priority1 = 0, priority2 = 0; u32 ability1 = GetBattlerAbility(battler1); @@ -4916,8 +4922,60 @@ s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) priority2 = GetChosenMovePriority(battler2); } - return GetWhichBattlerFasterArgs(battler1, battler2, ignoreChosenMoves, ability1, ability2, - holdEffectBattler1, holdEffectBattler2, speedBattler1, speedBattler2, priority1, priority2); + return GetWhichBattlerFasterArgs( + battler1, battler2, + ignoreChosenMoves, + ability1, ability2, + holdEffectBattler1, holdEffectBattler2, + speedBattler1, speedBattler2, + priority1, priority2 + ); +} + +// 24 == MAX_BATTLERS_COUNT!. +// These are the possible orders if all the battlers speed tie. An order +// is chosen at the start of the turn. +static const u8 sBattlerOrders[24][4] = +{ + { 0, 1, 2, 3 }, + { 0, 1, 3, 2 }, + { 0, 2, 1, 3 }, + { 0, 2, 3, 1 }, + { 0, 3, 1, 2 }, + { 0, 3, 2, 1 }, + { 1, 0, 2, 3 }, + { 1, 0, 3, 2 }, + { 1, 2, 0, 3 }, + { 1, 2, 3, 0 }, + { 1, 3, 0, 2 }, + { 1, 3, 2, 0 }, + { 2, 0, 1, 3 }, + { 2, 0, 3, 1 }, + { 2, 1, 0, 3 }, + { 2, 1, 3, 0 }, + { 2, 3, 0, 1 }, + { 2, 3, 1, 0 }, + { 3, 0, 1, 2 }, + { 3, 0, 2, 1 }, + { 3, 1, 0, 2 }, + { 3, 1, 2, 0 }, + { 3, 2, 0, 1 }, + { 3, 2, 1, 0 }, +}; + +s32 GetWhichBattlerFaster(u32 battler1, u32 battler2, bool32 ignoreChosenMoves) +{ + s32 strikesFirst = GetWhichBattlerFasterOrTies(battler1, battler2, ignoreChosenMoves); + if (strikesFirst == 0) + { + s32 order1 = sBattlerOrders[gBattleStruct->speedTieBreaks][battler1]; + s32 order2 = sBattlerOrders[gBattleStruct->speedTieBreaks][battler2]; + if (order1 < order2) + strikesFirst = 1; + else + strikesFirst = -1; + } + return strikesFirst; } static void SetActionsAndBattlersTurnOrder(void) @@ -5890,3 +5948,11 @@ bool32 IsWildMonSmart(void) return FALSE; #endif } + +static s32 Factorial(s32 n) +{ + s32 f = 1, i; + for (i = 2; i <= n; i++) + f *= i; + return f; +} diff --git a/test/battle/ability/comatose.c b/test/battle/ability/comatose.c index bd991c258e..cc65e9afac 100644 --- a/test/battle/ability/comatose.c +++ b/test/battle/ability/comatose.c @@ -34,7 +34,10 @@ SINGLE_BATTLE_TEST("Comatose may be suppressed if pokemon transformed into a pok PARAMETRIZE { move = MOVE_THUNDER_WAVE; } GIVEN { - PLAYER(SPECIES_KOMALA) { Ability(ABILITY_COMATOSE); Speed(30); } + // FIXME: Explicit moves currently required here because Ditto + // expects to find Celebrate in slot 1 during the second turn + // (after transforming). + PLAYER(SPECIES_KOMALA) { Ability(ABILITY_COMATOSE); Speed(30); Moves(MOVE_CELEBRATE, MOVE_GASTRO_ACID, move); } OPPONENT(SPECIES_DITTO) { Speed(20); } } WHEN { TURN { MOVE(player, MOVE_GASTRO_ACID); MOVE(opponent, MOVE_TRANSFORM); } diff --git a/test/battle/move.c b/test/battle/move.c index 936a821081..228a09a7c6 100644 --- a/test/battle/move.c +++ b/test/battle/move.c @@ -66,10 +66,9 @@ SINGLE_BATTLE_TEST("Turn order is determined by Speed if priority ties") } } -SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie") +SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie [singles]") { - KNOWN_FAILING; // The algorithm is significantly biased. - PASSES_RANDOMLY(1, 2); + PASSES_RANDOMLY(1, 2, RNG_SPEED_TIE); GIVEN { PLAYER(SPECIES_WOBBUFFET) { Speed(1); } OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } @@ -81,6 +80,60 @@ SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie" } } +DOUBLE_BATTLE_TEST("Turn order is determined randomly if priority and Speed tie [doubles]") +{ + struct BattlePokemon *order[4] = { NULL, NULL, NULL, NULL }; + u32 a, b, c, d; + + // TODO: Test all of these in a single PASSES_RANDOMLY pass rather + // than 24 PARAMETRIZEd passes. + PARAMETRIZE { a = 0; b = 1; c = 2; d = 3; } + PARAMETRIZE { a = 0; b = 1; c = 3; d = 2; } + PARAMETRIZE { a = 0; b = 2; c = 1; d = 3; } + PARAMETRIZE { a = 0; b = 2; c = 3; d = 1; } + PARAMETRIZE { a = 0; b = 3; c = 1; d = 2; } + PARAMETRIZE { a = 0; b = 3; c = 2; d = 1; } + PARAMETRIZE { a = 1; b = 0; c = 2; d = 3; } + PARAMETRIZE { a = 1; b = 0; c = 3; d = 2; } + PARAMETRIZE { a = 1; b = 2; c = 0; d = 3; } + PARAMETRIZE { a = 1; b = 2; c = 3; d = 0; } + PARAMETRIZE { a = 1; b = 3; c = 0; d = 2; } + PARAMETRIZE { a = 1; b = 3; c = 2; d = 0; } + PARAMETRIZE { a = 2; b = 0; c = 1; d = 3; } + PARAMETRIZE { a = 2; b = 0; c = 3; d = 1; } + PARAMETRIZE { a = 2; b = 1; c = 0; d = 3; } + PARAMETRIZE { a = 2; b = 1; c = 3; d = 0; } + PARAMETRIZE { a = 2; b = 3; c = 0; d = 1; } + PARAMETRIZE { a = 2; b = 3; c = 1; d = 0; } + PARAMETRIZE { a = 3; b = 0; c = 1; d = 2; } + PARAMETRIZE { a = 3; b = 0; c = 2; d = 1; } + PARAMETRIZE { a = 3; b = 1; c = 0; d = 2; } + PARAMETRIZE { a = 3; b = 1; c = 2; d = 0; } + PARAMETRIZE { a = 3; b = 2; c = 0; d = 1; } + PARAMETRIZE { a = 3; b = 2; c = 1; d = 0; } + + order[a] = playerLeft; + order[b] = playerRight; + order[c] = opponentLeft; + order[d] = opponentRight; + + PASSES_RANDOMLY(1, 24, RNG_SPEED_TIE); + + GIVEN { + PLAYER(SPECIES_WOBBUFFET) { Speed(1); } + PLAYER(SPECIES_WYNAUT) { Speed(1); } + OPPONENT(SPECIES_WOBBUFFET) { Speed(1); } + OPPONENT(SPECIES_WYNAUT) { Speed(1); } + } WHEN { + TURN { MOVE(playerLeft, MOVE_SPLASH); MOVE(playerRight, MOVE_SPLASH); MOVE(opponentLeft, MOVE_SPLASH); MOVE(opponentRight, MOVE_SPLASH); } + } SCENE { + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[0]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[1]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[2]); + ANIMATION(ANIM_TYPE_MOVE, MOVE_SPLASH, order[3]); + } +} + SINGLE_BATTLE_TEST("Critical hits occur at a 1/24 rate") { ASSUME(B_CRIT_CHANCE >= GEN_7);