mirror of
https://github.com/Ninjdai1/pokeemerald.git
synced 2024-11-16 11:37:40 +01:00
Allow tests to override specific RNG calls
This commit is contained in:
parent
509ff4c7e0
commit
89deda0416
@ -20,4 +20,67 @@ u16 Random2(void);
|
||||
void SeedRng(u16 seed);
|
||||
void SeedRng2(u16 seed);
|
||||
|
||||
/* Structured random number generator.
|
||||
* Instead of the caller converting bits from Random() to a meaningful
|
||||
* value, the caller provides metadata that is used to return the
|
||||
* meaningful value directly. This allows code to interpret the random
|
||||
* call, for example, battle tests know what the domain of a random call
|
||||
* is, and can exhaustively test it.
|
||||
*
|
||||
* RandomTag identifies the purpose of the value.
|
||||
*
|
||||
* RandomUniform(tag, lo, hi) returns a number from lo to hi inclusive.
|
||||
*
|
||||
* RandomPercentage(tag, t) returns FALSE with probability (1-t)/100,
|
||||
* and TRUE with probability t/100.
|
||||
*
|
||||
* RandomWeighted(tag, w0, w1, ... wN) returns a number from 0 to N
|
||||
* inclusive. The return value is proportional to the weights, e.g.
|
||||
* RandomWeighted(..., 1, 1) returns 50% 0s and 50% 1s.
|
||||
* RandomWeighted(..., 2, 1) returns 2/3 0s and 1/3 1s. */
|
||||
|
||||
enum RandomTag
|
||||
{
|
||||
RNG_NONE,
|
||||
RNG_ACCURACY,
|
||||
RNG_CONFUSION,
|
||||
RNG_CRITICAL_HIT,
|
||||
RNG_CUTE_CHARM,
|
||||
RNG_DAMAGE_MODIFIER,
|
||||
RNG_FLAME_BODY,
|
||||
RNG_FORCE_RANDOM_SWITCH,
|
||||
RNG_FROZEN,
|
||||
RNG_HOLD_EFFECT_FLINCH,
|
||||
RNG_INFATUATION,
|
||||
RNG_PARALYSIS,
|
||||
RNG_POISON_POINT,
|
||||
RNG_RAMPAGE_TURNS,
|
||||
RNG_SECONDARY_EFFECT,
|
||||
RNG_SLEEP_TURNS,
|
||||
RNG_SPEED_TIE,
|
||||
RNG_STATIC,
|
||||
RNG_STENCH,
|
||||
};
|
||||
|
||||
#define RandomWeighted(tag, ...) \
|
||||
({ \
|
||||
const u8 weights[] = { __VA_ARGS__ }; \
|
||||
u32 sum, i; \
|
||||
for (i = 0, sum = 0; i < ARRAY_COUNT(weights); i++) \
|
||||
sum += weights[i]; \
|
||||
RandomWeightedArray(tag, sum, ARRAY_COUNT(weights), weights); \
|
||||
})
|
||||
|
||||
#define RandomPercentage(tag, t) \
|
||||
({ \
|
||||
const u8 weights[] = { 100 - t, t }; \
|
||||
RandomWeightedArray(tag, 100, ARRAY_COUNT(weights), weights); \
|
||||
})
|
||||
|
||||
u32 RandomUniform(enum RandomTag, u32 lo, u32 hi);
|
||||
u32 RandomWeightedArray(enum RandomTag, u32 sum, u32 n, const u8 *weights);
|
||||
|
||||
u32 RandomUniformDefault(enum RandomTag, u32 lo, u32 hi);
|
||||
u32 RandomWeightedArrayDefault(enum RandomTag, u32 sum, u32 n, const u8 *weights);
|
||||
|
||||
#endif // GUARD_RANDOM_H
|
||||
|
@ -1917,15 +1917,25 @@ static void Cmd_accuracycheck(void)
|
||||
}
|
||||
else
|
||||
{
|
||||
u32 accuracy;
|
||||
|
||||
GET_MOVE_TYPE(move, type);
|
||||
if (JumpIfMoveAffectedByProtect(move))
|
||||
return;
|
||||
if (AccuracyCalcHelper(move))
|
||||
return;
|
||||
|
||||
// final calculation
|
||||
if ((Random() % 100 + 1) > GetTotalAccuracy(gBattlerAttacker, gBattlerTarget, move, GetBattlerAbility(gBattlerAttacker), GetBattlerAbility(gBattlerTarget),
|
||||
GetBattlerHoldEffect(gBattlerAttacker, TRUE), GetBattlerHoldEffect(gBattlerTarget, TRUE)))
|
||||
accuracy = GetTotalAccuracy(
|
||||
gBattlerAttacker,
|
||||
gBattlerTarget,
|
||||
move,
|
||||
GetBattlerAbility(gBattlerAttacker),
|
||||
GetBattlerAbility(gBattlerTarget),
|
||||
GetBattlerHoldEffect(gBattlerAttacker, TRUE),
|
||||
GetBattlerHoldEffect(gBattlerTarget, TRUE)
|
||||
);
|
||||
|
||||
if (!RandomPercentage(RNG_ACCURACY, accuracy))
|
||||
{
|
||||
gMoveResultFlags |= MOVE_RESULT_MISSED;
|
||||
if (GetBattlerHoldEffect(gBattlerAttacker, TRUE) == HOLD_EFFECT_BLUNDER_POLICY)
|
||||
@ -2104,10 +2114,8 @@ static void Cmd_critcalc(void)
|
||||
gIsCriticalHit = FALSE;
|
||||
else if (critChance == -2)
|
||||
gIsCriticalHit = TRUE;
|
||||
else if (Random() % sCriticalHitChance[critChance] == 0)
|
||||
gIsCriticalHit = TRUE;
|
||||
else
|
||||
gIsCriticalHit = FALSE;
|
||||
gIsCriticalHit = RandomWeighted(RNG_CRITICAL_HIT, sCriticalHitChance[critChance] - 1, 1);
|
||||
|
||||
// Counter for EVO_CRITICAL_HITS.
|
||||
partySlot = gBattlerPartyIndexes[gBattlerAttacker];
|
||||
@ -3153,9 +3161,9 @@ void SetMoveEffect(bool32 primary, u32 certain)
|
||||
|
||||
if (sStatusFlagsForMoveEffects[gBattleScripting.moveEffect] == STATUS1_SLEEP)
|
||||
#if B_SLEEP_TURNS >= GEN_5
|
||||
gBattleMons[gEffectBattler].status1 |= ((Random() % 3) + 2);
|
||||
gBattleMons[gEffectBattler].status1 |= STATUS1_SLEEP_TURN(1 + RandomUniform(RNG_SLEEP_TURNS, 1, 3));
|
||||
#else
|
||||
gBattleMons[gEffectBattler].status1 |= ((Random() % 4) + 3);
|
||||
gBattleMons[gEffectBattler].status1 |= STATUS1_SLEEP_TURN(1 + RandomUniform(RNG_SLEEP_TURNS, 2, 5));
|
||||
#endif
|
||||
else
|
||||
gBattleMons[gEffectBattler].status1 |= sStatusFlagsForMoveEffects[gBattleScripting.moveEffect];
|
||||
@ -3558,7 +3566,7 @@ void SetMoveEffect(bool32 primary, u32 certain)
|
||||
{
|
||||
gBattleMons[gEffectBattler].status2 |= STATUS2_MULTIPLETURNS;
|
||||
gLockedMoves[gEffectBattler] = gCurrentMove;
|
||||
gBattleMons[gEffectBattler].status2 |= STATUS2_LOCK_CONFUSE_TURN((Random() & 1) + 2); // thrash for 2-3 turns
|
||||
gBattleMons[gEffectBattler].status2 |= STATUS2_LOCK_CONFUSE_TURN(RandomUniform(RNG_RAMPAGE_TURNS, 2, 3));
|
||||
}
|
||||
break;
|
||||
case MOVE_EFFECT_SP_ATK_TWO_DOWN: // Overheat
|
||||
@ -3779,20 +3787,23 @@ static void Cmd_seteffectwithchance(void)
|
||||
else
|
||||
percentChance = gBattleMoves[gCurrentMove].secondaryEffectChance;
|
||||
|
||||
if (gBattleScripting.moveEffect & MOVE_EFFECT_CERTAIN
|
||||
&& !(gMoveResultFlags & MOVE_RESULT_NO_EFFECT))
|
||||
if (!(gMoveResultFlags & MOVE_RESULT_NO_EFFECT)
|
||||
&& gBattleScripting.moveEffect)
|
||||
{
|
||||
gBattleScripting.moveEffect &= ~MOVE_EFFECT_CERTAIN;
|
||||
SetMoveEffect(FALSE, MOVE_EFFECT_CERTAIN);
|
||||
}
|
||||
else if (Random() % 100 < percentChance
|
||||
&& gBattleScripting.moveEffect
|
||||
&& !(gMoveResultFlags & MOVE_RESULT_NO_EFFECT))
|
||||
{
|
||||
if (percentChance >= 100)
|
||||
if (gBattleScripting.moveEffect & MOVE_EFFECT_CERTAIN
|
||||
|| percentChance >= 100)
|
||||
{
|
||||
gBattleScripting.moveEffect &= ~MOVE_EFFECT_CERTAIN;
|
||||
SetMoveEffect(FALSE, MOVE_EFFECT_CERTAIN);
|
||||
else
|
||||
}
|
||||
else if (RandomPercentage(RNG_SECONDARY_EFFECT, percentChance))
|
||||
{
|
||||
SetMoveEffect(FALSE, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
gBattlescriptCurrInstr = cmd->nextInstr;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@ -12343,7 +12354,7 @@ static void Cmd_forcerandomswitch(void)
|
||||
*(gBattleStruct->battlerPartyIndexes + gBattlerTarget) = gBattlerPartyIndexes[gBattlerTarget];
|
||||
gBattlescriptCurrInstr = BattleScript_RoarSuccessSwitch;
|
||||
gBattleStruct->forcedSwitch |= gBitTable[gBattlerTarget];
|
||||
*(gBattleStruct->monToSwitchIntoId + gBattlerTarget) = validMons[Random() % validMonsCount];
|
||||
*(gBattleStruct->monToSwitchIntoId + gBattlerTarget) = validMons[RandomUniform(RNG_FORCE_RANDOM_SWITCH, 0, validMonsCount - 1)];
|
||||
|
||||
if (!IsMultiBattle())
|
||||
SwitchPartyOrder(gBattlerTarget);
|
||||
|
@ -3479,7 +3479,7 @@ u8 AtkCanceller_UnableToUseMove(void)
|
||||
case CANCELLER_FROZEN: // check being frozen
|
||||
if (gBattleMons[gBattlerAttacker].status1 & STATUS1_FREEZE && !(gBattleMoves[gCurrentMove].flags & FLAG_THAW_USER))
|
||||
{
|
||||
if (Random() % 5)
|
||||
if (!RandomPercentage(RNG_FROZEN, 20))
|
||||
{
|
||||
gBattlescriptCurrInstr = BattleScript_MoveUsedIsFrozen;
|
||||
gHitMarker |= HITMARKER_NO_ATTACKSTRING;
|
||||
@ -3597,9 +3597,9 @@ u8 AtkCanceller_UnableToUseMove(void)
|
||||
{
|
||||
// confusion dmg
|
||||
#if B_CONFUSION_SELF_DMG_CHANCE >= GEN_7
|
||||
if (Random() % 3 == 0)
|
||||
if (RandomWeighted(RNG_CONFUSION, 2, 1))
|
||||
#else
|
||||
if (Random() % 2 == 0)
|
||||
if (RandomWeighted(RNG_CONFUSION, 1, 1))
|
||||
#endif
|
||||
{
|
||||
gBattleCommunication[MULTISTRING_CHOOSER] = TRUE;
|
||||
@ -3625,7 +3625,7 @@ u8 AtkCanceller_UnableToUseMove(void)
|
||||
gBattleStruct->atkCancellerTracker++;
|
||||
break;
|
||||
case CANCELLER_PARALYSED: // paralysis
|
||||
if ((gBattleMons[gBattlerAttacker].status1 & STATUS1_PARALYSIS) && (Random() % 4) == 0)
|
||||
if ((gBattleMons[gBattlerAttacker].status1 & STATUS1_PARALYSIS) && !RandomPercentage(RNG_PARALYSIS, 75))
|
||||
{
|
||||
gProtectStructs[gBattlerAttacker].prlzImmobility = TRUE;
|
||||
// This is removed in FRLG and Emerald for some reason
|
||||
@ -3640,7 +3640,7 @@ u8 AtkCanceller_UnableToUseMove(void)
|
||||
if (gBattleMons[gBattlerAttacker].status2 & STATUS2_INFATUATION)
|
||||
{
|
||||
gBattleScripting.battler = CountTrailingZeroBits((gBattleMons[gBattlerAttacker].status2 & STATUS2_INFATUATION) >> 0x10);
|
||||
if (Random() & 1)
|
||||
if (!RandomPercentage(RNG_INFATUATION, 50))
|
||||
{
|
||||
BattleScriptPushCursor();
|
||||
}
|
||||
@ -5645,7 +5645,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move
|
||||
&& TARGET_TURN_DAMAGED
|
||||
&& CanBePoisoned(gBattlerTarget, gBattlerAttacker)
|
||||
&& IsMoveMakingContact(move, gBattlerAttacker)
|
||||
&& (Random() % 3) == 0)
|
||||
&& RandomWeighted(RNG_POISON_POINT, 2, 1))
|
||||
{
|
||||
gBattleScripting.moveEffect = MOVE_EFFECT_AFFECTS_USER | MOVE_EFFECT_POISON;
|
||||
PREPARE_ABILITY_BUFFER(gBattleTextBuff1, gLastUsedAbility);
|
||||
@ -5663,7 +5663,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move
|
||||
&& TARGET_TURN_DAMAGED
|
||||
&& CanBeParalyzed(gBattlerAttacker)
|
||||
&& IsMoveMakingContact(move, gBattlerAttacker)
|
||||
&& (Random() % 3) == 0)
|
||||
&& RandomWeighted(RNG_STATIC, 2, 1))
|
||||
{
|
||||
gBattleScripting.moveEffect = MOVE_EFFECT_AFFECTS_USER | MOVE_EFFECT_PARALYSIS;
|
||||
BattleScriptPushCursor();
|
||||
@ -5679,7 +5679,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move
|
||||
&& (IsMoveMakingContact(move, gBattlerAttacker))
|
||||
&& TARGET_TURN_DAMAGED
|
||||
&& CanBeBurned(gBattlerAttacker)
|
||||
&& (Random() % 3) == 0)
|
||||
&& RandomWeighted(RNG_FLAME_BODY, 2, 1))
|
||||
{
|
||||
gBattleScripting.moveEffect = MOVE_EFFECT_AFFECTS_USER | MOVE_EFFECT_BURN;
|
||||
BattleScriptPushCursor();
|
||||
@ -5695,7 +5695,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move
|
||||
&& (IsMoveMakingContact(move, gBattlerAttacker))
|
||||
&& TARGET_TURN_DAMAGED
|
||||
&& gBattleMons[gBattlerTarget].hp != 0
|
||||
&& (Random() % 3) == 0
|
||||
&& RandomWeighted(RNG_CUTE_CHARM, 2, 1)
|
||||
&& GetBattlerAbility(gBattlerAttacker) != ABILITY_OBLIVIOUS
|
||||
&& !IsAbilityOnSide(gBattlerAttacker, ABILITY_AROMA_VEIL)
|
||||
&& GetGenderFromSpeciesAndPersonality(speciesAtk, pidAtk) != GetGenderFromSpeciesAndPersonality(speciesDef, pidDef)
|
||||
@ -5915,7 +5915,7 @@ u8 AbilityBattleEffects(u8 caseID, u8 battler, u16 ability, u8 special, u16 move
|
||||
if (!(gMoveResultFlags & MOVE_RESULT_NO_EFFECT)
|
||||
&& gBattleMons[gBattlerTarget].hp != 0
|
||||
&& !gProtectStructs[gBattlerAttacker].confusionSelfDmg
|
||||
&& (Random() % 10) == 0
|
||||
&& RandomWeighted(RNG_STENCH, 9, 1)
|
||||
&& !IS_MOVE_STATUS(move)
|
||||
&& !sMovesNotAffectedByStench[gCurrentMove])
|
||||
{
|
||||
@ -7635,7 +7635,7 @@ u8 ItemBattleEffects(u8 caseID, u8 battlerId, bool8 moveTurn)
|
||||
if (gBattleMoveDamage != 0 // Need to have done damage
|
||||
&& !(gMoveResultFlags & MOVE_RESULT_NO_EFFECT)
|
||||
&& TARGET_TURN_DAMAGED
|
||||
&& (Random() % 100) < atkHoldEffectParam
|
||||
&& RandomPercentage(RNG_HOLD_EFFECT_FLINCH, atkHoldEffectParam)
|
||||
&& gBattleMoves[gCurrentMove].flags & FLAG_KINGS_ROCK_AFFECTED
|
||||
&& gBattleMons[gBattlerTarget].hp)
|
||||
{
|
||||
@ -9752,7 +9752,7 @@ static s32 DoMoveDamageCalc(u16 move, u8 battlerAtk, u8 battlerDef, u8 moveType,
|
||||
// Add a random factor.
|
||||
if (randomFactor)
|
||||
{
|
||||
dmg *= 100 - (Random() % 16);
|
||||
dmg *= 100 - RandomUniform(RNG_DAMAGE_MODIFIER, 0, 15);
|
||||
dmg /= 100;
|
||||
}
|
||||
|
||||
|
24
src/random.c
24
src/random.c
@ -31,3 +31,27 @@ u16 Random2(void)
|
||||
gRng2Value = ISO_RANDOMIZE1(gRng2Value);
|
||||
return gRng2Value >> 16;
|
||||
}
|
||||
|
||||
__attribute__((weak, alias("RandomUniformDefault")))
|
||||
u32 RandomUniform(enum RandomTag tag, u32 lo, u32 hi);
|
||||
|
||||
__attribute__((weak, alias("RandomWeightedArrayDefault")))
|
||||
u32 RandomWeightedArray(enum RandomTag tag, u32 sum, u32 n, const u8 *weights);
|
||||
|
||||
u32 RandomUniformDefault(enum RandomTag tag, u32 lo, u32 hi)
|
||||
{
|
||||
return lo + (((hi - lo) * Random()) >> 16);
|
||||
}
|
||||
|
||||
u32 RandomWeightedArrayDefault(enum RandomTag tag, u32 sum, u32 n, const u8 *weights)
|
||||
{
|
||||
s32 i, targetSum;
|
||||
targetSum = (sum * Random()) >> 16;
|
||||
for (i = 0; i < n - 1; i++)
|
||||
{
|
||||
targetSum -= weights[i];
|
||||
if (targetSum < 0)
|
||||
return i;
|
||||
}
|
||||
return n - 1;
|
||||
}
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
SINGLE_BATTLE_TEST("Compound Eyes raises accuracy")
|
||||
{
|
||||
PASSES_RANDOMLY(91, 100);
|
||||
PASSES_RANDOMLY(91, 100, RNG_ACCURACY);
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_THUNDER].accuracy == 70);
|
||||
PLAYER(SPECIES_BUTTERFREE) { Ability(ABILITY_COMPOUND_EYES); };
|
||||
@ -22,7 +22,7 @@ SINGLE_BATTLE_TEST("Compound Eyes raises accuracy")
|
||||
SINGLE_BATTLE_TEST("Compound Eyes does not affect OHKO moves")
|
||||
{
|
||||
KNOWN_FAILING;
|
||||
PASSES_RANDOMLY(30, 100);
|
||||
PASSES_RANDOMLY(30, 100, RNG_ACCURACY);
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_FISSURE].accuracy == 30);
|
||||
ASSUME(gBattleMoves[MOVE_FISSURE].effect == EFFECT_OHKO);
|
||||
|
@ -1,9 +1,6 @@
|
||||
#include "global.h"
|
||||
#include "test_battle.h"
|
||||
|
||||
// TODO: Currently PASSES_RANDOMLY is incapable of testing Cute Charm
|
||||
// because it only activates 33% of the time, but we only want to
|
||||
// measure the 50% of the time that the infatuation prevents our move.
|
||||
SINGLE_BATTLE_TEST("Cute Charm inflicts infatuation on contact")
|
||||
{
|
||||
u32 move;
|
||||
|
@ -16,7 +16,7 @@ SINGLE_BATTLE_TEST("Sand Veil prevents damage from sandstorm")
|
||||
|
||||
SINGLE_BATTLE_TEST("Sand Veil reduces accuracy during sandstorm")
|
||||
{
|
||||
PASSES_RANDOMLY(4,5);
|
||||
PASSES_RANDOMLY(4, 5, RNG_ACCURACY);
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_POUND].accuracy == 100);
|
||||
PLAYER(SPECIES_SANDSHREW) { Ability(ABILITY_SAND_VEIL); };
|
||||
|
@ -3,7 +3,7 @@
|
||||
|
||||
SINGLE_BATTLE_TEST("Stench has a 10% chance to flinch")
|
||||
{
|
||||
PASSES_RANDOMLY(1,10);
|
||||
PASSES_RANDOMLY(1, 10, RNG_STENCH);
|
||||
GIVEN {
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].power > 0);
|
||||
PLAYER(SPECIES_GRIMER) { Ability(ABILITY_STENCH); };
|
||||
@ -17,7 +17,8 @@ SINGLE_BATTLE_TEST("Stench has a 10% chance to flinch")
|
||||
|
||||
SINGLE_BATTLE_TEST("Stench does not stack with King's Rock")
|
||||
{
|
||||
PASSES_RANDOMLY(1,10);
|
||||
KNOWN_FAILING;
|
||||
PASSES_RANDOMLY(1, 10, RNG_STENCH);
|
||||
GIVEN {
|
||||
ASSUME(gItems[ITEM_KINGS_ROCK].holdEffect == HOLD_EFFECT_FLINCH);
|
||||
ASSUME(gBattleMoves[MOVE_TACKLE].power > 0);
|
||||
|
@ -8,7 +8,7 @@ ASSUMPTIONS
|
||||
|
||||
SINGLE_BATTLE_TEST("Red Card switches the attacker with a random non-fainted replacement")
|
||||
{
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
PASSES_RANDOMLY(1, 2, RNG_FORCE_RANDOM_SWITCH);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_RED_CARD); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
@ -27,7 +27,7 @@ SINGLE_BATTLE_TEST("Red Card switches the attacker with a random non-fainted rep
|
||||
|
||||
DOUBLE_BATTLE_TEST("Red Card switches the target with a random non-battler, non-fainted replacement")
|
||||
{
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
PASSES_RANDOMLY(1, 2, RNG_FORCE_RANDOM_SWITCH);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Item(ITEM_RED_CARD); }
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
|
26
test/move.c
26
test/move.c
@ -10,7 +10,7 @@ SINGLE_BATTLE_TEST("Accuracy controls the proportion of misses")
|
||||
PARAMETRIZE { move = MOVE_RAZOR_LEAF; }
|
||||
PARAMETRIZE { move = MOVE_SCRATCH; }
|
||||
ASSUME(0 < gBattleMoves[move].accuracy && gBattleMoves[move].accuracy <= 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100, RNG_ACCURACY);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
@ -27,10 +27,9 @@ SINGLE_BATTLE_TEST("Secondary Effect Chance controls the proportion of secondary
|
||||
PARAMETRIZE { move = MOVE_THUNDER_SHOCK; }
|
||||
PARAMETRIZE { move = MOVE_DISCHARGE; }
|
||||
PARAMETRIZE { move = MOVE_NUZZLE; }
|
||||
ASSUME(gBattleMoves[move].accuracy == 100);
|
||||
ASSUME(gBattleMoves[move].effect == EFFECT_PARALYZE_HIT);
|
||||
ASSUME(0 < gBattleMoves[move].secondaryEffectChance && gBattleMoves[move].secondaryEffectChance <= 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[move].secondaryEffectChance, 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[move].secondaryEffectChance, 100, RNG_SECONDARY_EFFECT);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
@ -70,6 +69,7 @@ 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")
|
||||
{
|
||||
KNOWN_FAILING; // The algorithm is significantly biased.
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Speed(1); }
|
||||
@ -85,15 +85,29 @@ SINGLE_BATTLE_TEST("Turn order is determined randomly if priority and speed tie"
|
||||
SINGLE_BATTLE_TEST("Critical hits occur at a 1/24 rate")
|
||||
{
|
||||
ASSUME(B_CRIT_CHANCE >= GEN_7);
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
|
||||
PASSES_RANDOMLY(100 / 24, 100);
|
||||
PASSES_RANDOMLY(1, 24, RNG_CRITICAL_HIT);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SCRATCH); }
|
||||
} SCENE {
|
||||
MESSAGE("It's a critical hit!");
|
||||
MESSAGE("A critical hit!");
|
||||
}
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Slash's critical hits occur at a 1/8 rate")
|
||||
{
|
||||
ASSUME(B_CRIT_CHANCE >= GEN_7);
|
||||
ASSUME(gBattleMoves[MOVE_SLASH].flags & FLAG_HIGH_CRIT);
|
||||
PASSES_RANDOMLY(1, 8, RNG_CRITICAL_HIT);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_SLASH); }
|
||||
} SCENE {
|
||||
MESSAGE("A critical hit!");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -9,7 +9,7 @@ ASSUMPTIONS
|
||||
SINGLE_BATTLE_TEST("Sand Attack lowers Accuracy")
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100, RNG_ACCURACY);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
|
@ -9,7 +9,7 @@ ASSUMPTIONS
|
||||
SINGLE_BATTLE_TEST("Double Team raises Evasion")
|
||||
{
|
||||
ASSUME(gBattleMoves[MOVE_SCRATCH].accuracy == 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100);
|
||||
PASSES_RANDOMLY(gBattleMoves[MOVE_SCRATCH].accuracy * 3 / 4, 100, RNG_ACCURACY);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
|
@ -9,8 +9,7 @@ ASSUMPTIONS
|
||||
|
||||
SINGLE_BATTLE_TEST("Dragon Tail switches the target with a random non-fainted replacement")
|
||||
{
|
||||
KNOWN_FAILING; // Only 18/50. Waiting for an improved PASSES_RANDOMLY.
|
||||
PASSES_RANDOMLY(90 * 1, 100 * 2);
|
||||
PASSES_RANDOMLY(1, 2, RNG_FORCE_RANDOM_SWITCH);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
@ -27,7 +26,7 @@ SINGLE_BATTLE_TEST("Dragon Tail switches the target with a random non-fainted re
|
||||
|
||||
DOUBLE_BATTLE_TEST("Dragon Tail switches the target with a random non-battler, non-fainted replacement")
|
||||
{
|
||||
PASSES_RANDOMLY(90 * 1, 100 * 2);
|
||||
PASSES_RANDOMLY(1, 2, RNG_FORCE_RANDOM_SWITCH);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
|
@ -8,7 +8,7 @@ ASSUMPTIONS
|
||||
|
||||
SINGLE_BATTLE_TEST("Thrash lasts for 2 or 3 turns")
|
||||
{
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
PASSES_RANDOMLY(1, 2, RNG_RAMPAGE_TURNS);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
@ -26,7 +26,6 @@ SINGLE_BATTLE_TEST("Thrash lasts for 2 or 3 turns")
|
||||
SINGLE_BATTLE_TEST("Thrash confuses the user after it finishes")
|
||||
{
|
||||
GIVEN {
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
@ -45,7 +44,6 @@ SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 1
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
@ -61,7 +59,6 @@ SINGLE_BATTLE_TEST("Thrash does not confuse the user if it is canceled on turn 2
|
||||
{
|
||||
GIVEN {
|
||||
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
@ -78,7 +75,6 @@ SINGLE_BATTLE_TEST("Thrash confuses the user if it is canceled on turn 3 of 3")
|
||||
KNOWN_FAILING;
|
||||
GIVEN {
|
||||
ASSUME(B_RAMPAGE_CANCELLING >= GEN_5);
|
||||
RNGSeed(0x00000000);
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
|
@ -8,7 +8,7 @@ ASSUMPTIONS
|
||||
|
||||
SINGLE_BATTLE_TEST("Roar switches the target with a random non-fainted replacement")
|
||||
{
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
PASSES_RANDOMLY(1, 2, RNG_FORCE_RANDOM_SWITCH);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
@ -25,7 +25,7 @@ SINGLE_BATTLE_TEST("Roar switches the target with a random non-fainted replaceme
|
||||
|
||||
DOUBLE_BATTLE_TEST("Roar switches the target with a random non-battler, non-fainted replacement")
|
||||
{
|
||||
PASSES_RANDOMLY(1, 2);
|
||||
PASSES_RANDOMLY(1, 2, RNG_FORCE_RANDOM_SWITCH);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
PLAYER(SPECIES_WYNAUT);
|
||||
|
@ -6,16 +6,33 @@ ASSUMPTIONS
|
||||
ASSUME(gBattleMoves[MOVE_HYPNOSIS].effect == EFFECT_SLEEP);
|
||||
}
|
||||
|
||||
SINGLE_BATTLE_TEST("Hypnosis inflicts sleep")
|
||||
SINGLE_BATTLE_TEST("Hypnosis inflicts 1-3 turns of sleep")
|
||||
{
|
||||
u32 turns, count;
|
||||
ASSUME(B_SLEEP_TURNS >= GEN_5);
|
||||
PARAMETRIZE { turns = 1; }
|
||||
PARAMETRIZE { turns = 2; }
|
||||
PARAMETRIZE { turns = 3; }
|
||||
PASSES_RANDOMLY(1, 3, RNG_SLEEP_TURNS);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET);
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_HYPNOSIS); }
|
||||
TURN { MOVE(player, MOVE_HYPNOSIS); MOVE(opponent, MOVE_CELEBRATE); }
|
||||
for (count = 0; count < turns; ++count)
|
||||
TURN {}
|
||||
} SCENE {
|
||||
ANIMATION(ANIM_TYPE_MOVE, MOVE_HYPNOSIS, player);
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_SLP, opponent);
|
||||
MESSAGE("Foe Wobbuffet fell asleep!");
|
||||
STATUS_ICON(opponent, sleep: TRUE);
|
||||
for (count = 0; count < turns; ++count)
|
||||
{
|
||||
if (count < turns - 1)
|
||||
MESSAGE("Foe Wobbuffet is fast asleep.");
|
||||
ANIMATION(ANIM_TYPE_STATUS, B_ANIM_STATUS_SLP, opponent);
|
||||
}
|
||||
MESSAGE("Foe Wobbuffet woke up!");
|
||||
STATUS_ICON(opponent, none: TRUE);
|
||||
}
|
||||
}
|
||||
|
81
test/random.c
Normal file
81
test/random.c
Normal file
@ -0,0 +1,81 @@
|
||||
#include "global.h"
|
||||
#include "test.h"
|
||||
#include "random.h"
|
||||
|
||||
TEST("RandomUniform generates lo..hi")
|
||||
{
|
||||
u32 lo, hi, i;
|
||||
PARAMETRIZE { lo = 0; hi = 1; }
|
||||
PARAMETRIZE { lo = 0; hi = 2; }
|
||||
PARAMETRIZE { lo = 0; hi = 3; }
|
||||
PARAMETRIZE { lo = 2; hi = 4; }
|
||||
for (i = 0; i < 1024; i++)
|
||||
{
|
||||
u32 r = RandomUniformDefault(RNG_NONE, lo, hi);
|
||||
EXPECT(lo <= r && r <= hi);
|
||||
}
|
||||
}
|
||||
|
||||
TEST("RandomWeighted generates 0..n-1")
|
||||
{
|
||||
u32 n, sum, i;
|
||||
static const u8 ws[8] = { 1, 1, 1, 1, 1, 1, 1, 1 };
|
||||
PARAMETRIZE { n = 1; }
|
||||
PARAMETRIZE { n = 2; }
|
||||
PARAMETRIZE { n = 3; }
|
||||
PARAMETRIZE { n = 4; }
|
||||
ASSUME(n <= ARRAY_COUNT(ws));
|
||||
for (i = 0, sum = 0; i < n; i++)
|
||||
sum += ws[i];
|
||||
for (i = 0; i < 1024; i++)
|
||||
{
|
||||
u32 r = RandomWeightedArrayDefault(RNG_NONE, sum, n, ws);
|
||||
EXPECT(0 <= r && r < n);
|
||||
}
|
||||
}
|
||||
|
||||
TEST("RandomUniform generates uniform distribution")
|
||||
{
|
||||
u32 i, error;
|
||||
u16 distribution[4];
|
||||
|
||||
memset(distribution, 0, sizeof(distribution));
|
||||
for (i = 0; i < 4096; i++)
|
||||
{
|
||||
u32 r = RandomUniformDefault(RNG_NONE, 0, ARRAY_COUNT(distribution));
|
||||
EXPECT(0 <= r && r < ARRAY_COUNT(distribution));
|
||||
distribution[r]++;
|
||||
}
|
||||
|
||||
error = 0;
|
||||
for (i = 0; i < ARRAY_COUNT(distribution); i++)
|
||||
error += abs(UQ_4_12(0.25) - distribution[i]);
|
||||
|
||||
EXPECT_LT(error, UQ_4_12(0.025));
|
||||
}
|
||||
|
||||
TEST("RandomWeighted generates distribution in proportion to the weights")
|
||||
{
|
||||
u32 i, sum, error;
|
||||
static const u8 ws[4] = { 1, 2, 2, 3 };
|
||||
u16 distribution[ARRAY_COUNT(ws)];
|
||||
|
||||
for (i = 0, sum = 0; i < ARRAY_COUNT(ws); i++)
|
||||
sum += ws[i];
|
||||
|
||||
memset(distribution, 0, sizeof(distribution));
|
||||
for (i = 0; i < 4096; i++)
|
||||
{
|
||||
u32 r = RandomWeightedArrayDefault(RNG_NONE, sum, ARRAY_COUNT(ws), ws);
|
||||
EXPECT(0 <= r && r < ARRAY_COUNT(ws));
|
||||
distribution[r]++;
|
||||
}
|
||||
|
||||
error = 0;
|
||||
error += abs(UQ_4_12(0.125) - distribution[0]);
|
||||
error += abs(UQ_4_12(0.250) - distribution[1]);
|
||||
error += abs(UQ_4_12(0.250) - distribution[2]);
|
||||
error += abs(UQ_4_12(0.375) - distribution[3]);
|
||||
|
||||
EXPECT_LT(error, UQ_4_12(0.025));
|
||||
}
|
@ -72,7 +72,7 @@ SINGLE_BATTLE_TEST("Burn reduces attack by 50%", s16 damage)
|
||||
|
||||
SINGLE_BATTLE_TEST("Freeze has a 20% chance of being thawed")
|
||||
{
|
||||
PASSES_RANDOMLY(20, 100);
|
||||
PASSES_RANDOMLY(20, 100, RNG_FROZEN);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
@ -90,9 +90,8 @@ SINGLE_BATTLE_TEST("Freeze is thawed by opponent's Fire-type attacks")
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_FREEZE); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
} WHEN {
|
||||
TURN { MOVE(player, MOVE_CELEBRATE); MOVE(opponent, MOVE_EMBER); }
|
||||
TURN { MOVE(opponent, MOVE_EMBER); MOVE(player, MOVE_CELEBRATE); }
|
||||
} SCENE {
|
||||
MESSAGE("Wobbuffet is frozen solid!");
|
||||
MESSAGE("Foe Wobbuffet used Ember!");
|
||||
MESSAGE("Wobbuffet was defrosted!");
|
||||
STATUS_ICON(player, none: TRUE);
|
||||
@ -145,7 +144,7 @@ SINGLE_BATTLE_TEST("Paralysis reduces speed by 50%")
|
||||
|
||||
SINGLE_BATTLE_TEST("Paralysis has a 25% chance of skipping the turn")
|
||||
{
|
||||
PASSES_RANDOMLY(25, 100);
|
||||
PASSES_RANDOMLY(25, 100, RNG_PARALYSIS);
|
||||
GIVEN {
|
||||
PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); }
|
||||
OPPONENT(SPECIES_WOBBUFFET);
|
||||
|
@ -65,8 +65,9 @@
|
||||
* single turn. MOVE causes the player to use Stun Spore and adds the
|
||||
* move to the Pokémon's moveset if an explicit Moves was not specified.
|
||||
* Pokémon that are not mentioned in a TURN use Celebrate.
|
||||
* The test runner attempts to rig the RNG so that the first move used
|
||||
* in a turn does not miss and activates its secondary effects (if any).
|
||||
* The test runner rigs the RNG so that unless otherwise specified,
|
||||
* moves always hit, never critical hit, always activate their secondary
|
||||
* effects, and always roll the same damage modifier.
|
||||
*
|
||||
* SCENE describes the player-visible output of the battle. In this case
|
||||
* ANIMATION checks that the Stun Spore animation played, MESSAGE checks
|
||||
@ -228,12 +229,35 @@
|
||||
* }
|
||||
* }
|
||||
*
|
||||
* PASSES_RANDOMLY(successes, trials)
|
||||
* Checks that the test passes approximately successes/trials. Used for
|
||||
* testing RNG-based attacks, e.g.:
|
||||
* PASSES_RANDOMLY(successes, trials, [tag])
|
||||
* Checks that the test passes successes/trials. If tag is provided, the
|
||||
* test is run for each value that the tag can produce. For example, to
|
||||
* check that Paralysis causes the turn to be skipped 25/100 times, we
|
||||
* can write the following test that passes only if the Pokémon is fully
|
||||
* paralyzed and specify that we expect it to pass 25/100 times when
|
||||
* RNG_PARALYSIS varies:
|
||||
* SINGLE_BATTLE_TEST("Paralysis has a 25% chance of skipping the turn")
|
||||
* {
|
||||
* PASSES_RANDOMLY(25, 100, RNG_PARALYSIS);
|
||||
* GIVEN {
|
||||
* PLAYER(SPECIES_WOBBUFFET) { Status1(STATUS1_PARALYSIS); }
|
||||
* OPPONENT(SPECIES_WOBBUFFET);
|
||||
* } WHEN {
|
||||
* TURN { MOVE(player, MOVE_CELEBRATE); }
|
||||
* } SCENE {
|
||||
* MESSAGE("Wobbuffet is paralyzed! It can't move!");
|
||||
* }
|
||||
* }
|
||||
* All BattleRandom calls involving tag will return the same number, so
|
||||
* this cannot be used to have two moves independently hit or miss, for
|
||||
* example.
|
||||
*
|
||||
* If the tag is not provided, runs the test 50 times and computes an
|
||||
* approximate pass ratio.
|
||||
* PASSES_RANDOMLY(gBattleMoves[move].accuracy, 100);
|
||||
* Note that PASSES_RANDOMLY makes the tests run very slowly and should
|
||||
* be avoided where possible.
|
||||
* Note that this mode of PASSES_RANDOMLY makes the tests run very
|
||||
* slowly and should be avoided where possible. If the mechanic you are
|
||||
* testing is missing its tag, you should add it.
|
||||
*
|
||||
* GIVEN
|
||||
* Contains the initial state of the parties before the battle.
|
||||
@ -419,6 +443,7 @@
|
||||
#include "battle_anim.h"
|
||||
#include "data.h"
|
||||
#include "item.h"
|
||||
#include "random.h"
|
||||
#include "recorded_battle.h"
|
||||
#include "test.h"
|
||||
#include "util.h"
|
||||
@ -433,6 +458,7 @@
|
||||
// NOTE: If the stack is too small the test runner will probably crash
|
||||
// or loop.
|
||||
#define BATTLE_TEST_STACK_SIZE 1024
|
||||
#define MAX_TURNS 16
|
||||
#define MAX_QUEUED_EVENTS 25
|
||||
|
||||
enum { BATTLE_TEST_SINGLES, BATTLE_TEST_DOUBLES };
|
||||
@ -512,6 +538,13 @@ struct QueuedEvent
|
||||
} as;
|
||||
};
|
||||
|
||||
struct BattlerTurn
|
||||
{
|
||||
u8 hit:2;
|
||||
u8 criticalHit:2;
|
||||
u8 secondaryEffect:2;
|
||||
};
|
||||
|
||||
struct BattleTestData
|
||||
{
|
||||
u8 stack[BATTLE_TEST_STACK_SIZE];
|
||||
@ -533,14 +566,13 @@ struct BattleTestData
|
||||
u8 turns;
|
||||
u8 actionBattlers;
|
||||
u8 moveBattlers;
|
||||
bool8 hasRNGActions:1;
|
||||
|
||||
struct RecordedBattleSave recordedBattle;
|
||||
u8 battleRecordTypes[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
|
||||
u8 battleRecordSourceLineOffsets[MAX_BATTLERS_COUNT][BATTLER_RECORD_SIZE];
|
||||
u16 recordIndexes[MAX_BATTLERS_COUNT];
|
||||
struct BattlerTurn battleRecordTurns[MAX_TURNS][MAX_BATTLERS_COUNT];
|
||||
u8 lastActionTurn;
|
||||
u8 nextRNGTurn;
|
||||
|
||||
u8 queuedEventsCount;
|
||||
u8 queueGroupType;
|
||||
@ -555,11 +587,12 @@ struct BattleTestRunnerState
|
||||
u8 parametersCount; // Valid only in BattleTest_Setup.
|
||||
u8 parameters;
|
||||
u8 runParameter;
|
||||
u16 rngTag;
|
||||
u8 trials;
|
||||
u8 expectedPasses;
|
||||
u8 observedPasses;
|
||||
u8 skippedTrials;
|
||||
u8 runTrial;
|
||||
u16 expectedRatio;
|
||||
u16 observedRatio;
|
||||
u16 trialRatio;
|
||||
bool8 runRandomly:1;
|
||||
bool8 runGiven:1;
|
||||
bool8 runWhen:1;
|
||||
@ -645,9 +678,14 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState;
|
||||
|
||||
/* Randomly */
|
||||
|
||||
#define PASSES_RANDOMLY(passes, trials) for (; gBattleTestRunnerState->runRandomly; gBattleTestRunnerState->runRandomly = FALSE) Randomly(__LINE__, passes, trials)
|
||||
#define PASSES_RANDOMLY(passes, trials, ...) for (; gBattleTestRunnerState->runRandomly; gBattleTestRunnerState->runRandomly = FALSE) Randomly(__LINE__, passes, trials, (struct RandomlyContext) { __VA_ARGS__ })
|
||||
|
||||
void Randomly(u32 sourceLine, u32 passes, u32 trials);
|
||||
struct RandomlyContext
|
||||
{
|
||||
u16 tag;
|
||||
};
|
||||
|
||||
void Randomly(u32 sourceLine, u32 passes, u32 trials, struct RandomlyContext);
|
||||
|
||||
/* Given */
|
||||
|
||||
@ -721,6 +759,8 @@ struct MoveContext
|
||||
u16 explicitHit:1;
|
||||
u16 criticalHit:1;
|
||||
u16 explicitCriticalHit:1;
|
||||
u16 secondaryEffect:1;
|
||||
u16 explicitSecondaryEffect:1;
|
||||
u16 megaEvolve:1;
|
||||
u16 explicitMegaEvolve:1;
|
||||
// TODO: u8 zMove:1;
|
||||
|
@ -15,26 +15,10 @@
|
||||
#define STATE gBattleTestRunnerState
|
||||
#define DATA gBattleTestRunnerState->data
|
||||
|
||||
/* RNG seeds for controlling the first move of the turn.
|
||||
* Found via brute force. */
|
||||
#define RNG_SEED_DEFAULT 0x00000000
|
||||
|
||||
/* Default seed, triggers most things.
|
||||
* The 1st roll % 100 is <= 29, to make 30%+ accuracycheck pass.
|
||||
* The 2nd roll is not a critical hit at the regular crit stage.
|
||||
* The 3rd roll is consumed by damagecalc.
|
||||
* The 4th roll is consumed by adjustdamage.
|
||||
* The 5th roll % 100 is <= 9, to make 10%+ seteffectwithchance pass
|
||||
* and % 3 is == 0, to make Poison Point and other 1/3s pass. */
|
||||
#define RNG_SEED_DEFAULT 0x000002BE
|
||||
|
||||
/* Causes the first attack to critical hit if B_CRIT_CHANCE >= GEN_6.
|
||||
* The 2nd roll % 24 == 0 to be a critical hit at any stage.
|
||||
* The other rolls match RNG_SEED_DEFAULT. */
|
||||
#define RNG_SEED_CRITICAL_HIT 0x0000A9F4
|
||||
|
||||
/* Causes the first attack to miss if possible.
|
||||
* The 1st roll % 100 is 99, to make 99%- accuracycheck fail. */
|
||||
#define RNG_SEED_MISS 0x00000074
|
||||
#undef Q_4_12
|
||||
#define Q_4_12(n) (s32)((n) * 4096)
|
||||
|
||||
EWRAM_DATA struct BattleTestRunnerState *gBattleTestRunnerState = NULL;
|
||||
|
||||
@ -129,12 +113,13 @@ static u32 BattleTest_EstimateCost(void *data)
|
||||
if (!STATE)
|
||||
return 0;
|
||||
STATE->runRandomly = TRUE;
|
||||
DATA.recordedBattle.rngSeed = RNG_SEED_DEFAULT;
|
||||
InvokeTestFunction(test);
|
||||
cost = 1;
|
||||
if (STATE->parametersCount != 0)
|
||||
cost *= STATE->parametersCount;
|
||||
if (STATE->trials != 0)
|
||||
if (STATE->trials == 1)
|
||||
cost *= 3;
|
||||
else if (STATE->trials > 1)
|
||||
cost *= STATE->trials;
|
||||
FREE_AND_SET_NULL(STATE);
|
||||
return cost;
|
||||
@ -162,6 +147,28 @@ static void BattleTest_SetUp(void *data)
|
||||
}
|
||||
}
|
||||
|
||||
static void PrintTestName(void)
|
||||
{
|
||||
if (STATE->trials && STATE->parameters)
|
||||
{
|
||||
if (STATE->trials == 1)
|
||||
MgbaPrintf_(":N%s %d/%d (%d/?)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1);
|
||||
else
|
||||
MgbaPrintf_(":N%s %d/%d (%d/%d)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1, STATE->trials);
|
||||
}
|
||||
else if (STATE->trials)
|
||||
{
|
||||
if (STATE->trials == 1)
|
||||
MgbaPrintf_(":N%s (%d/?)", gTestRunnerState.test->name, STATE->runTrial + 1);
|
||||
else
|
||||
MgbaPrintf_(":N%s (%d/%d)", gTestRunnerState.test->name, STATE->runTrial + 1, STATE->trials);
|
||||
}
|
||||
else if (STATE->parameters)
|
||||
{
|
||||
MgbaPrintf_(":N%s %d/%d", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters);
|
||||
}
|
||||
}
|
||||
|
||||
// This does not take into account priority, statuses, or any other
|
||||
// modifiers.
|
||||
static void SetImplicitSpeeds(void)
|
||||
@ -280,12 +287,82 @@ static void BattleTest_Run(void *data)
|
||||
STATE->checkProgressTrial = 0;
|
||||
STATE->checkProgressTurn = 0;
|
||||
|
||||
if (STATE->trials && STATE->parameters)
|
||||
MgbaPrintf_(":N%s %d/%d (%d/%d)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1, STATE->trials);
|
||||
else if (STATE->trials)
|
||||
MgbaPrintf_(":N%s (%d/%d)", gTestRunnerState.test->name, STATE->runTrial + 1, STATE->trials);
|
||||
else if (STATE->parameters)
|
||||
MgbaPrintf_(":N%s %d/%d", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters);
|
||||
PrintTestName();
|
||||
}
|
||||
|
||||
u32 RandomUniform(enum RandomTag tag, u32 lo, u32 hi)
|
||||
{
|
||||
if (tag == STATE->rngTag)
|
||||
{
|
||||
u32 n = hi - lo + 1;
|
||||
if (STATE->trials == 1)
|
||||
{
|
||||
STATE->trials = n;
|
||||
PrintTestName();
|
||||
}
|
||||
else if (STATE->trials != n)
|
||||
{
|
||||
Test_ExitWithResult(TEST_RESULT_ERROR, "RandomUniform called with inconsistent trials %d and %d", STATE->trials, n);
|
||||
}
|
||||
STATE->trialRatio = Q_4_12(1) / n;
|
||||
return STATE->runTrial + lo;
|
||||
}
|
||||
|
||||
return hi;
|
||||
}
|
||||
|
||||
u32 RandomWeightedArray(enum RandomTag tag, u32 sum, u32 n, const u8 *weights)
|
||||
{
|
||||
const struct BattlerTurn *turn = NULL;
|
||||
u32 default_ = n-1;
|
||||
|
||||
if (gCurrentTurnActionNumber < gBattlersCount)
|
||||
{
|
||||
u32 battlerId = gBattlerByTurnOrder[gCurrentTurnActionNumber];
|
||||
turn = &DATA.battleRecordTurns[gBattleResults.battleTurnCounter][battlerId];
|
||||
}
|
||||
|
||||
switch (tag)
|
||||
{
|
||||
case RNG_ACCURACY:
|
||||
ASSUME(n == 2);
|
||||
if (turn && turn->hit)
|
||||
return turn->hit - 1;
|
||||
default_ = TRUE;
|
||||
break;
|
||||
|
||||
case RNG_CRITICAL_HIT:
|
||||
ASSUME(n == 2);
|
||||
if (turn && turn->criticalHit)
|
||||
return turn->criticalHit - 1;
|
||||
default_ = FALSE;
|
||||
break;
|
||||
|
||||
case RNG_SECONDARY_EFFECT:
|
||||
ASSUME(n == 2);
|
||||
if (turn && turn->secondaryEffect)
|
||||
return turn->secondaryEffect - 1;
|
||||
default_ = TRUE;
|
||||
break;
|
||||
}
|
||||
|
||||
if (tag == STATE->rngTag)
|
||||
{
|
||||
if (STATE->trials == 1)
|
||||
{
|
||||
STATE->trials = n;
|
||||
PrintTestName();
|
||||
}
|
||||
else if (STATE->trials != n)
|
||||
{
|
||||
Test_ExitWithResult(TEST_RESULT_ERROR, "RandomWeighted called with inconsistent trials %d and %d", STATE->trials, n);
|
||||
}
|
||||
// TODO: Detect inconsistent sum.
|
||||
STATE->trialRatio = Q_4_12(weights[STATE->runTrial]) / sum;
|
||||
return STATE->runTrial;
|
||||
}
|
||||
|
||||
return default_;
|
||||
}
|
||||
|
||||
static s32 TryAbilityPopUp(s32 i, s32 n, u32 battlerId, u32 ability)
|
||||
@ -711,42 +788,36 @@ static void CB2_BattleTest_NextTrial(void)
|
||||
|
||||
SetMainCallback2(CB2_BattleTest_NextParameter);
|
||||
|
||||
switch (gTestRunnerState.result)
|
||||
{
|
||||
case TEST_RESULT_FAIL:
|
||||
break;
|
||||
case TEST_RESULT_PASS:
|
||||
STATE->observedRatio += STATE->trialRatio;
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (STATE->rngTag)
|
||||
STATE->trialRatio = 0;
|
||||
|
||||
if (++STATE->runTrial < STATE->trials)
|
||||
{
|
||||
switch (gTestRunnerState.result)
|
||||
{
|
||||
case TEST_RESULT_FAIL:
|
||||
break;
|
||||
case TEST_RESULT_PASS:
|
||||
STATE->observedPasses++;
|
||||
break;
|
||||
case TEST_RESULT_ASSUMPTION_FAIL:
|
||||
STATE->skippedTrials++;
|
||||
if (STATE->skippedTrials > STATE->trials / 4)
|
||||
Test_ExitWithResult(TEST_RESULT_INVALID, "25%% of the trials were SKIPed");
|
||||
break;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
if (STATE->parameters)
|
||||
MgbaPrintf_(":N%s %d/%d (%d/%d)", gTestRunnerState.test->name, STATE->runParameter + 1, STATE->parameters, STATE->runTrial + 1, STATE->trials);
|
||||
else
|
||||
MgbaPrintf_(":N%s (%d/%d)", gTestRunnerState.test->name, STATE->runTrial + 1, STATE->trials);
|
||||
gTestRunnerState.result = TEST_RESULT_PASS;
|
||||
PrintTestName();
|
||||
gTestRunnerState.result = TEST_RESULT_PASS;
|
||||
DATA.recordedBattle.rngSeed = ISO_RANDOMIZE1(STATE->runTrial);
|
||||
DATA.queuedEvent = 0;
|
||||
DATA.lastActionTurn = 0;
|
||||
DATA.nextRNGTurn = 0;
|
||||
SetVariablesForRecordedBattle(&DATA.recordedBattle);
|
||||
SetMainCallback2(CB2_InitBattle);
|
||||
}
|
||||
else
|
||||
{
|
||||
// This is a tolerance of +/- 4%.
|
||||
if (abs(STATE->observedPasses - STATE->expectedPasses) <= 2)
|
||||
// This is a tolerance of +/- ~2%.
|
||||
if (abs(STATE->observedRatio - STATE->expectedRatio) <= Q_4_12(0.02))
|
||||
gTestRunnerState.result = TEST_RESULT_PASS;
|
||||
else
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "Expected %d/%d passes, observed %d/%d", STATE->expectedPasses, STATE->trials, STATE->observedPasses, STATE->trials);
|
||||
Test_ExitWithResult(TEST_RESULT_FAIL, "Expected %q passes/successes, observed %q", STATE->expectedRatio, STATE->observedRatio);
|
||||
}
|
||||
}
|
||||
|
||||
@ -773,7 +844,8 @@ static bool32 BattleTest_CheckProgress(void *data)
|
||||
|
||||
static bool32 BattleTest_HandleExitWithResult(void *data, enum TestResult result)
|
||||
{
|
||||
if (result != TEST_RESULT_INVALID
|
||||
if (result != TEST_RESULT_ASSUMPTION_FAIL
|
||||
&& result != TEST_RESULT_INVALID
|
||||
&& result != TEST_RESULT_ERROR
|
||||
&& result != TEST_RESULT_TIMEOUT
|
||||
&& STATE->runTrial < STATE->trials)
|
||||
@ -787,16 +859,25 @@ static bool32 BattleTest_HandleExitWithResult(void *data, enum TestResult result
|
||||
}
|
||||
}
|
||||
|
||||
void Randomly(u32 sourceLine, u32 passes, u32 trials)
|
||||
void Randomly(u32 sourceLine, u32 passes, u32 trials, struct RandomlyContext ctx)
|
||||
{
|
||||
INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set");
|
||||
// This is a precision of 2%.
|
||||
STATE->trials = 50;
|
||||
STATE->expectedPasses = STATE->trials * passes / trials;
|
||||
STATE->observedPasses = 0;
|
||||
STATE->skippedTrials = 0;
|
||||
INVALID_IF(passes > trials, "%d passes specified, but only %d trials", passes, trials);
|
||||
STATE->rngTag = ctx.tag;
|
||||
STATE->runTrial = 0;
|
||||
DATA.recordedBattle.rngSeed = 0;
|
||||
STATE->expectedRatio = Q_4_12(passes) / trials;
|
||||
STATE->observedRatio = 0;
|
||||
if (STATE->rngTag)
|
||||
{
|
||||
STATE->trials = 1;
|
||||
STATE->trialRatio = Q_4_12(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set");
|
||||
STATE->trials = 50;
|
||||
STATE->trialRatio = Q_4_12(1) / STATE->trials;
|
||||
DATA.recordedBattle.rngSeed = 0;
|
||||
}
|
||||
}
|
||||
|
||||
void RNGSeed_(u32 sourceLine, u32 seed)
|
||||
@ -1037,16 +1118,6 @@ static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32
|
||||
|
||||
void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32 actionType)
|
||||
{
|
||||
// TODO: Support explicit seeds for each turn?
|
||||
if (DATA.nextRNGTurn == gBattleResults.battleTurnCounter
|
||||
&& (DATA.recordedBattle.rngSeed == RNG_SEED_DEFAULT
|
||||
|| DATA.recordedBattle.rngSeed == RNG_SEED_CRITICAL_HIT
|
||||
|| DATA.recordedBattle.rngSeed == RNG_SEED_MISS))
|
||||
{
|
||||
gRngValue = DATA.recordedBattle.rngSeed;
|
||||
DATA.nextRNGTurn++;
|
||||
}
|
||||
|
||||
// An illegal move choice will cause the battle to request a new
|
||||
// move slot and target. This detects the move slot.
|
||||
if (actionType == RECORDED_MOVE_SLOT
|
||||
@ -1122,10 +1193,11 @@ void BattleTest_CheckBattleRecordActionType(u32 battlerId, u32 recordIndex, u32
|
||||
void OpenTurn(u32 sourceLine)
|
||||
{
|
||||
INVALID_IF(DATA.turnState != TURN_CLOSED, "Nested TURN");
|
||||
if (DATA.turns == MAX_TURNS)
|
||||
Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: TURN exceeds MAX_TURNS", gTestRunnerState.test->filename, sourceLine);
|
||||
DATA.turnState = TURN_OPEN;
|
||||
DATA.actionBattlers = 0x00;
|
||||
DATA.moveBattlers = 0x00;
|
||||
DATA.hasRNGActions = FALSE;
|
||||
}
|
||||
|
||||
static void SetSlowerThan(s32 battlerId)
|
||||
@ -1253,21 +1325,12 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.explicitHit && !ctx.hit)
|
||||
{
|
||||
if (DATA.hasRNGActions != 0)
|
||||
Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: hit only supported on the first move", gTestRunnerState.test->filename, sourceLine);
|
||||
INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set");
|
||||
DATA.recordedBattle.rngSeed = RNG_SEED_MISS;
|
||||
}
|
||||
|
||||
if (ctx.explicitCriticalHit && ctx.criticalHit)
|
||||
{
|
||||
if (DATA.hasRNGActions != 0)
|
||||
Test_ExitWithResult(TEST_RESULT_ERROR, "%s:%d: criticalHit only supported on the first move", gTestRunnerState.test->filename, sourceLine);
|
||||
INVALID_IF(DATA.recordedBattle.rngSeed != RNG_SEED_DEFAULT, "RNG seed already set");
|
||||
DATA.recordedBattle.rngSeed = RNG_SEED_CRITICAL_HIT;
|
||||
}
|
||||
if (ctx.explicitHit)
|
||||
DATA.battleRecordTurns[DATA.turns][battlerId].hit = 1 + ctx.hit;
|
||||
if (ctx.explicitCriticalHit)
|
||||
DATA.battleRecordTurns[DATA.turns][battlerId].criticalHit = 1 + ctx.criticalHit;
|
||||
if (ctx.explicitSecondaryEffect)
|
||||
DATA.battleRecordTurns[DATA.turns][battlerId].secondaryEffect = 1 + ctx.secondaryEffect;
|
||||
|
||||
if (!(DATA.actionBattlers & (1 << battlerId)))
|
||||
{
|
||||
@ -1288,14 +1351,6 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
|
||||
DATA.actionBattlers |= 1 << battlerId;
|
||||
DATA.moveBattlers |= 1 << battlerId;
|
||||
}
|
||||
|
||||
// WARNING: Approximation. The move could still cause the RNG to
|
||||
// advance.
|
||||
if (gBattleMoves[moveId].accuracy != 0
|
||||
|| gBattleMoves[moveId].split != SPLIT_STATUS)
|
||||
{
|
||||
DATA.hasRNGActions = TRUE;
|
||||
}
|
||||
}
|
||||
|
||||
void ForcedMove(u32 sourceLine, struct BattlePokemon *battler)
|
||||
|
Loading…
Reference in New Issue
Block a user