This commit is contained in:
AgustinGDLV 2023-03-24 22:40:51 -07:00
commit 5d508f7963
35 changed files with 979 additions and 360 deletions

View File

@ -8691,6 +8691,7 @@ BattleScript_IntimidateLoopIncrement:
addbyte gBattlerTarget, 1
jumpifbytenotequal gBattlerTarget, gBattlersCount, BattleScript_IntimidateLoop
BattleScript_IntimidateEnd:
copybyte sBATTLER, gBattlerAttacker
destroyabilitypopup
pause B_WAIT_TIME_MED
end3

View File

@ -552,7 +552,6 @@ struct BattleStruct
u8 wildVictorySong;
u8 dynamicMoveType;
u8 wrappedBy[MAX_BATTLERS_COUNT];
u16 assistPossibleMoves[PARTY_SIZE * MAX_MON_MOVES]; // Each of mons can know max 4 moves.
u8 focusPunchBattlerId;
u8 battlerPreventingSwitchout;
u8 moneyMultiplier:6;

View File

@ -1001,7 +1001,4 @@
#define ITEM_B_USE_MEDICINE 1
#define ITEM_B_USE_OTHER 2
// Check if the item is one that can be used on a Pokemon.
#define ITEM_HAS_EFFECT(item) ((item) >= ITEM_POTION && (item) <= LAST_BERRY_INDEX)
#endif // GUARD_CONSTANTS_ITEMS_H

View File

@ -2,6 +2,7 @@
#define GUARD_POKEMON_H
#include "sprite.h"
#include "constants/items.h"
#include "constants/region_map_sections.h"
#include "constants/map_groups.h"
@ -401,7 +402,7 @@ extern const struct BattleMove gBattleMoves[];
extern const u8 gFacilityClassToPicIndex[];
extern const u8 gFacilityClassToTrainerClass[];
extern const struct SpeciesInfo gSpeciesInfo[];
extern const u8 *const gItemEffectTable[];
extern const u8 *const gItemEffectTable[ITEMS_COUNT];
extern const u32 gExperienceTables[][MAX_LEVEL + 1];
extern const struct LevelUpMove *const gLevelUpLearnsets[];
extern const u16 *const gTeachableLearnsets[];

View File

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

View File

@ -1085,13 +1085,13 @@ static bool8 ShouldUseItem(void)
item = gBattleResources->battleHistory->trainerItems[i];
if (item == ITEM_NONE)
continue;
if (gItemEffectTable[item - ITEM_POTION] == NULL)
if (gItemEffectTable[item] == NULL)
continue;
if (item == ITEM_ENIGMA_BERRY_E_READER)
itemEffects = gSaveBlock1Ptr->enigmaBerry.itemEffect;
else
itemEffects = gItemEffectTable[item - ITEM_POTION];
itemEffects = gItemEffectTable[item];
*(gBattleStruct->AI_itemType + gActiveBattler / 2) = GetAI_ItemType(item, itemEffects);

View File

@ -23,6 +23,7 @@
#include "main.h"
#include "palette.h"
#include "money.h"
#include "malloc.h"
#include "bg.h"
#include "string_util.h"
#include "pokemon_icon.h"
@ -1926,15 +1927,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)
@ -2114,10 +2125,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];
@ -3163,9 +3172,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];
@ -3562,7 +3571,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
@ -3783,20 +3792,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
{
@ -12521,7 +12533,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);
@ -12737,7 +12749,7 @@ static void Cmd_tryKO(void)
if (gCurrentMove == MOVE_SHEER_COLD && !IS_BATTLER_OF_TYPE(gBattlerAttacker, TYPE_ICE))
odds -= 10;
#endif
if (Random() % 100 + 1 < odds && gBattleMons[gBattlerAttacker].level >= gBattleMons[gBattlerTarget].level)
if (RandomPercentage(RNG_ACCURACY, odds) && gBattleMons[gBattlerAttacker].level >= gBattleMons[gBattlerTarget].level)
lands = TRUE;
}
@ -14988,38 +15000,40 @@ static void Cmd_assistattackselect(void)
s32 chooseableMovesNo = 0;
struct Pokemon *party;
s32 monId, moveId;
u16 *validMoves = gBattleStruct->assistPossibleMoves;
u16 *validMoves = Alloc(sizeof(u16) * PARTY_SIZE * MAX_MON_MOVES);
if (GET_BATTLER_SIDE(gBattlerAttacker) != B_SIDE_PLAYER)
party = gEnemyParty;
else
party = gPlayerParty;
for (monId = 0; monId < PARTY_SIZE; monId++)
if (validMoves != NULL)
{
if (monId == gBattlerPartyIndexes[gBattlerAttacker])
continue;
if (GetMonData(&party[monId], MON_DATA_SPECIES_OR_EGG) == SPECIES_NONE)
continue;
if (GetMonData(&party[monId], MON_DATA_SPECIES_OR_EGG) == SPECIES_EGG)
continue;
if (GET_BATTLER_SIDE(gBattlerAttacker) != B_SIDE_PLAYER)
party = gEnemyParty;
else
party = gPlayerParty;
for (moveId = 0; moveId < MAX_MON_MOVES; moveId++)
for (monId = 0; monId < PARTY_SIZE; monId++)
{
s32 i = 0;
u16 move = GetMonData(&party[monId], MON_DATA_MOVE1 + moveId);
if (sForbiddenMoves[move] & FORBIDDEN_ASSIST)
if (monId == gBattlerPartyIndexes[gBattlerAttacker])
continue;
if (GetMonData(&party[monId], MON_DATA_SPECIES_OR_EGG) == SPECIES_NONE)
continue;
if (GetMonData(&party[monId], MON_DATA_SPECIES_OR_EGG) == SPECIES_EGG)
continue;
validMoves[chooseableMovesNo] = move;
chooseableMovesNo++;
for (moveId = 0; moveId < MAX_MON_MOVES; moveId++)
{
u16 move = GetMonData(&party[monId], MON_DATA_MOVE1 + moveId);
if (sForbiddenMoves[move] & FORBIDDEN_ASSIST)
continue;
validMoves[chooseableMovesNo++] = move;
}
}
}
if (chooseableMovesNo)
{
gHitMarker &= ~HITMARKER_ATTACKSTRING_PRINTED;
gCalledMove = validMoves[((Random() & 0xFF) * chooseableMovesNo) >> 8];
gCalledMove = validMoves[Random() % chooseableMovesNo];
gBattlerTarget = GetMoveTarget(gCalledMove, NO_TARGET_OVERRIDE);
gBattlescriptCurrInstr = cmd->nextInstr;
}
@ -15027,6 +15041,8 @@ static void Cmd_assistattackselect(void)
{
gBattlescriptCurrInstr = cmd->failInstr;
}
TRY_FREE_AND_SET_NULL(validMoves);
}
static void Cmd_trysetmagiccoat(void)

View File

@ -3539,7 +3539,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;
@ -3657,9 +3657,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;
@ -3685,7 +3685,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
@ -3700,7 +3700,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();
}
@ -5650,7 +5650,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);
@ -5668,7 +5668,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();
@ -5684,7 +5684,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();
@ -5700,7 +5700,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)
@ -5918,7 +5918,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])
{
@ -7638,7 +7638,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)
{
@ -8197,7 +8197,7 @@ u32 GetBattlerHoldEffect(u8 battlerId, bool32 checkNegating)
return gBattleStruct->debugHoldEffects[battlerId];
else
#endif
if (gBattleMons[battlerId].item == ITEM_ENIGMA_BERRY)
if (gBattleMons[battlerId].item == ITEM_ENIGMA_BERRY_E_READER)
return gEnigmaBerries[battlerId].holdEffect;
else
return ItemId_GetHoldEffect(gBattleMons[battlerId].item);
@ -8206,7 +8206,7 @@ u32 GetBattlerHoldEffect(u8 battlerId, bool32 checkNegating)
//
static u32 GetBattlerItemHoldEffectParam(u8 battlerId, u16 item)
{
if (item == ITEM_ENIGMA_BERRY)
if (item == ITEM_ENIGMA_BERRY_E_READER)
return gEnigmaBerries[battlerId].holdEffectParam;
else
return ItemId_GetHoldEffectParam(item);
@ -8214,7 +8214,7 @@ static u32 GetBattlerItemHoldEffectParam(u8 battlerId, u16 item)
u32 GetBattlerHoldEffectParam(u8 battlerId)
{
if (gBattleMons[battlerId].item == ITEM_ENIGMA_BERRY)
if (gBattleMons[battlerId].item == ITEM_ENIGMA_BERRY_E_READER)
return gEnigmaBerries[battlerId].holdEffectParam;
else
return ItemId_GetHoldEffectParam(gBattleMons[battlerId].item);
@ -9758,7 +9758,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;
}

View File

@ -199,7 +199,7 @@ bool32 IsViableZMove(u8 battlerId, u16 move)
holdEffect = gBattleStruct->debugHoldEffects[battlerId];
else
#endif
if (item == ITEM_ENIGMA_BERRY)
if (item == ITEM_ENIGMA_BERRY_E_READER)
return FALSE; // HoldEffect = gEnigmaBerries[battlerId].holdEffect;
else
holdEffect = ItemId_GetHoldEffect(item);

View File

@ -447,128 +447,128 @@ const u8 gItemEffect_TamatoBerry[10] = {
EV_BERRY_FRIENDSHIP_CHANGE,
};
const u8 *const gItemEffectTable[] =
const u8 *const gItemEffectTable[ITEMS_COUNT] =
{
// Medicine
[ITEM_POTION - ITEM_POTION] = gItemEffect_Potion,
[ITEM_SUPER_POTION - ITEM_POTION] = gItemEffect_SuperPotion,
[ITEM_HYPER_POTION - ITEM_POTION] = gItemEffect_HyperPotion,
[ITEM_MAX_POTION - ITEM_POTION] = gItemEffect_MaxPotion,
[ITEM_FULL_RESTORE - ITEM_POTION] = gItemEffect_FullRestore,
[ITEM_REVIVE - ITEM_POTION] = gItemEffect_Revive,
[ITEM_MAX_REVIVE - ITEM_POTION] = gItemEffect_MaxRevive,
[ITEM_FRESH_WATER - ITEM_POTION] = gItemEffect_FreshWater,
[ITEM_SODA_POP - ITEM_POTION] = gItemEffect_SodaPop,
[ITEM_LEMONADE - ITEM_POTION] = gItemEffect_Lemonade,
[ITEM_MOOMOO_MILK - ITEM_POTION] = gItemEffect_MoomooMilk,
[ITEM_ENERGY_POWDER - ITEM_POTION] = gItemEffect_EnergyPowder,
[ITEM_ENERGY_ROOT - ITEM_POTION] = gItemEffect_EnergyRoot,
[ITEM_HEAL_POWDER - ITEM_POTION] = gItemEffect_HealPowder,
[ITEM_REVIVAL_HERB - ITEM_POTION] = gItemEffect_RevivalHerb,
[ITEM_ANTIDOTE - ITEM_POTION] = gItemEffect_Antidote,
[ITEM_PARALYZE_HEAL - ITEM_POTION] = gItemEffect_ParalyzeHeal,
[ITEM_BURN_HEAL - ITEM_POTION] = gItemEffect_BurnHeal,
[ITEM_ICE_HEAL - ITEM_POTION] = gItemEffect_IceHeal,
[ITEM_AWAKENING - ITEM_POTION] = gItemEffect_Awakening,
[ITEM_FULL_HEAL - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_ETHER - ITEM_POTION] = gItemEffect_Ether,
[ITEM_MAX_ETHER - ITEM_POTION] = gItemEffect_MaxEther,
[ITEM_ELIXIR - ITEM_POTION] = gItemEffect_Elixir,
[ITEM_MAX_ELIXIR - ITEM_POTION] = gItemEffect_MaxElixir,
[ITEM_BERRY_JUICE - ITEM_POTION] = gItemEffect_BerryJuice,
[ITEM_SACRED_ASH - ITEM_POTION] = gItemEffect_SacredAsh,
[ITEM_SWEET_HEART - ITEM_POTION] = gItemEffect_Potion,
[ITEM_MAX_HONEY - ITEM_POTION] = gItemEffect_MaxRevive,
[ITEM_POTION] = gItemEffect_Potion,
[ITEM_SUPER_POTION] = gItemEffect_SuperPotion,
[ITEM_HYPER_POTION] = gItemEffect_HyperPotion,
[ITEM_MAX_POTION] = gItemEffect_MaxPotion,
[ITEM_FULL_RESTORE] = gItemEffect_FullRestore,
[ITEM_REVIVE] = gItemEffect_Revive,
[ITEM_MAX_REVIVE] = gItemEffect_MaxRevive,
[ITEM_FRESH_WATER] = gItemEffect_FreshWater,
[ITEM_SODA_POP] = gItemEffect_SodaPop,
[ITEM_LEMONADE] = gItemEffect_Lemonade,
[ITEM_MOOMOO_MILK] = gItemEffect_MoomooMilk,
[ITEM_ENERGY_POWDER] = gItemEffect_EnergyPowder,
[ITEM_ENERGY_ROOT] = gItemEffect_EnergyRoot,
[ITEM_HEAL_POWDER] = gItemEffect_HealPowder,
[ITEM_REVIVAL_HERB] = gItemEffect_RevivalHerb,
[ITEM_ANTIDOTE] = gItemEffect_Antidote,
[ITEM_PARALYZE_HEAL] = gItemEffect_ParalyzeHeal,
[ITEM_BURN_HEAL] = gItemEffect_BurnHeal,
[ITEM_ICE_HEAL] = gItemEffect_IceHeal,
[ITEM_AWAKENING] = gItemEffect_Awakening,
[ITEM_FULL_HEAL] = gItemEffect_FullHeal,
[ITEM_ETHER] = gItemEffect_Ether,
[ITEM_MAX_ETHER] = gItemEffect_MaxEther,
[ITEM_ELIXIR] = gItemEffect_Elixir,
[ITEM_MAX_ELIXIR] = gItemEffect_MaxElixir,
[ITEM_BERRY_JUICE] = gItemEffect_BerryJuice,
[ITEM_SACRED_ASH] = gItemEffect_SacredAsh,
[ITEM_SWEET_HEART] = gItemEffect_Potion,
[ITEM_MAX_HONEY] = gItemEffect_MaxRevive,
// Regional Specialties
[ITEM_PEWTER_CRUNCHIES - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_RAGE_CANDY_BAR - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_LAVA_COOKIE - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_OLD_GATEAU - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_CASTELIACONE - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_LUMIOSE_GALETTE - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_SHALOUR_SABLE - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_BIG_MALASADA - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_PEWTER_CRUNCHIES] = gItemEffect_FullHeal,
[ITEM_RAGE_CANDY_BAR] = gItemEffect_FullHeal,
[ITEM_LAVA_COOKIE] = gItemEffect_FullHeal,
[ITEM_OLD_GATEAU] = gItemEffect_FullHeal,
[ITEM_CASTELIACONE] = gItemEffect_FullHeal,
[ITEM_LUMIOSE_GALETTE] = gItemEffect_FullHeal,
[ITEM_SHALOUR_SABLE] = gItemEffect_FullHeal,
[ITEM_BIG_MALASADA] = gItemEffect_FullHeal,
// Vitamins
[ITEM_HP_UP - ITEM_POTION] = gItemEffect_HPUp,
[ITEM_PROTEIN - ITEM_POTION] = gItemEffect_Protein,
[ITEM_IRON - ITEM_POTION] = gItemEffect_Iron,
[ITEM_CALCIUM - ITEM_POTION] = gItemEffect_Calcium,
[ITEM_ZINC - ITEM_POTION] = gItemEffect_Zinc,
[ITEM_CARBOS - ITEM_POTION] = gItemEffect_Carbos,
[ITEM_PP_UP - ITEM_POTION] = gItemEffect_PPUp,
[ITEM_PP_MAX - ITEM_POTION] = gItemEffect_PPMax,
[ITEM_HP_UP] = gItemEffect_HPUp,
[ITEM_PROTEIN] = gItemEffect_Protein,
[ITEM_IRON] = gItemEffect_Iron,
[ITEM_CALCIUM] = gItemEffect_Calcium,
[ITEM_ZINC] = gItemEffect_Zinc,
[ITEM_CARBOS] = gItemEffect_Carbos,
[ITEM_PP_UP] = gItemEffect_PPUp,
[ITEM_PP_MAX] = gItemEffect_PPMax,
// EV Feathers
[ITEM_HEALTH_FEATHER - ITEM_POTION] = gItemEffect_HpFeather,
[ITEM_MUSCLE_FEATHER - ITEM_POTION] = gItemEffect_AtkFeather,
[ITEM_RESIST_FEATHER - ITEM_POTION] = gItemEffect_DefFeather,
[ITEM_GENIUS_FEATHER - ITEM_POTION] = gItemEffect_SpatkFeather,
[ITEM_CLEVER_FEATHER - ITEM_POTION] = gItemEffect_SpdefFeather,
[ITEM_SWIFT_FEATHER - ITEM_POTION] = gItemEffect_SpeedFeather,
[ITEM_HEALTH_FEATHER] = gItemEffect_HpFeather,
[ITEM_MUSCLE_FEATHER] = gItemEffect_AtkFeather,
[ITEM_RESIST_FEATHER] = gItemEffect_DefFeather,
[ITEM_GENIUS_FEATHER] = gItemEffect_SpatkFeather,
[ITEM_CLEVER_FEATHER] = gItemEffect_SpdefFeather,
[ITEM_SWIFT_FEATHER] = gItemEffect_SpeedFeather,
// Candy
[ITEM_RARE_CANDY - ITEM_POTION] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_XS - ITEM_POTION] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_S - ITEM_POTION] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_M - ITEM_POTION] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_L - ITEM_POTION] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_XL - ITEM_POTION] = gItemEffect_RareCandy,
//[ITEM_DYNAMAX_CANDY - ITEM_POTION] = gItemEffect_DynamaxCandy, // Todo
[ITEM_RARE_CANDY] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_XS] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_S] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_M] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_L] = gItemEffect_RareCandy,
[ITEM_EXP_CANDY_XL] = gItemEffect_RareCandy,
//[ITEM_DYNAMAX_CANDY] = gItemEffect_DynamaxCandy, // Todo
// Medicinal Flutes
[ITEM_BLUE_FLUTE - ITEM_POTION] = gItemEffect_BlueFlute,
[ITEM_YELLOW_FLUTE - ITEM_POTION] = gItemEffect_YellowFlute,
[ITEM_RED_FLUTE - ITEM_POTION] = gItemEffect_RedFlute,
[ITEM_BLUE_FLUTE] = gItemEffect_BlueFlute,
[ITEM_YELLOW_FLUTE] = gItemEffect_YellowFlute,
[ITEM_RED_FLUTE] = gItemEffect_RedFlute,
// X Items
[ITEM_X_ATTACK - ITEM_POTION] = gItemEffect_XAttack,
[ITEM_X_DEFENSE - ITEM_POTION] = gItemEffect_XDefense,
[ITEM_X_SPEED - ITEM_POTION] = gItemEffect_XSpeed,
[ITEM_X_ACCURACY - ITEM_POTION] = gItemEffect_XAccuracy,
[ITEM_X_SP_ATK - ITEM_POTION] = gItemEffect_XSpecialAttack,
[ITEM_X_SP_DEF - ITEM_POTION] = gItemEffect_XSpecialDefense,
[ITEM_X_ATTACK] = gItemEffect_XAttack,
[ITEM_X_DEFENSE] = gItemEffect_XDefense,
[ITEM_X_SPEED] = gItemEffect_XSpeed,
[ITEM_X_ACCURACY] = gItemEffect_XAccuracy,
[ITEM_X_SP_ATK] = gItemEffect_XSpecialAttack,
[ITEM_X_SP_DEF] = gItemEffect_XSpecialDefense,
[ITEM_DIRE_HIT - ITEM_POTION] = gItemEffect_DireHit,
[ITEM_GUARD_SPEC - ITEM_POTION] = gItemEffect_GuardSpec,
[ITEM_DIRE_HIT] = gItemEffect_DireHit,
[ITEM_GUARD_SPEC] = gItemEffect_GuardSpec,
//[ITEM_MAX_MUSHROOMS - ITEM_POTION] = gItemEffect_MaxMushrooms, // Todo
//[ITEM_MAX_MUSHROOMS] = gItemEffect_MaxMushrooms, // Todo
// Evolution Items
[ITEM_FIRE_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_WATER_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_THUNDER_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_LEAF_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_ICE_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_SUN_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_MOON_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_SHINY_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_DUSK_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_DAWN_STONE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_SWEET_APPLE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_TART_APPLE - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_CRACKED_POT - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_CHIPPED_POT - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_GALARICA_CUFF - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_GALARICA_WREATH - ITEM_POTION] = gItemEffect_EvoItem,
[ITEM_FIRE_STONE] = gItemEffect_EvoItem,
[ITEM_WATER_STONE] = gItemEffect_EvoItem,
[ITEM_THUNDER_STONE] = gItemEffect_EvoItem,
[ITEM_LEAF_STONE] = gItemEffect_EvoItem,
[ITEM_ICE_STONE] = gItemEffect_EvoItem,
[ITEM_SUN_STONE] = gItemEffect_EvoItem,
[ITEM_MOON_STONE] = gItemEffect_EvoItem,
[ITEM_SHINY_STONE] = gItemEffect_EvoItem,
[ITEM_DUSK_STONE] = gItemEffect_EvoItem,
[ITEM_DAWN_STONE] = gItemEffect_EvoItem,
[ITEM_SWEET_APPLE] = gItemEffect_EvoItem,
[ITEM_TART_APPLE] = gItemEffect_EvoItem,
[ITEM_CRACKED_POT] = gItemEffect_EvoItem,
[ITEM_CHIPPED_POT] = gItemEffect_EvoItem,
[ITEM_GALARICA_CUFF] = gItemEffect_EvoItem,
[ITEM_GALARICA_WREATH] = gItemEffect_EvoItem,
// Berries
[ITEM_CHERI_BERRY - ITEM_POTION] = gItemEffect_CheriBerry,
[ITEM_CHESTO_BERRY - ITEM_POTION] = gItemEffect_ChestoBerry,
[ITEM_PECHA_BERRY - ITEM_POTION] = gItemEffect_PechaBerry,
[ITEM_RAWST_BERRY - ITEM_POTION] = gItemEffect_RawstBerry,
[ITEM_ASPEAR_BERRY - ITEM_POTION] = gItemEffect_AspearBerry,
[ITEM_LEPPA_BERRY - ITEM_POTION] = gItemEffect_LeppaBerry,
[ITEM_ORAN_BERRY - ITEM_POTION] = gItemEffect_OranBerry,
[ITEM_PERSIM_BERRY - ITEM_POTION] = gItemEffect_PersimBerry,
[ITEM_LUM_BERRY - ITEM_POTION] = gItemEffect_FullHeal,
[ITEM_SITRUS_BERRY - ITEM_POTION] = gItemEffect_SitrusBerry,
[ITEM_POMEG_BERRY - ITEM_POTION] = gItemEffect_PomegBerry,
[ITEM_KELPSY_BERRY - ITEM_POTION] = gItemEffect_KelpsyBerry,
[ITEM_QUALOT_BERRY - ITEM_POTION] = gItemEffect_QualotBerry,
[ITEM_HONDEW_BERRY - ITEM_POTION] = gItemEffect_HondewBerry,
[ITEM_GREPA_BERRY - ITEM_POTION] = gItemEffect_GrepaBerry,
[ITEM_TAMATO_BERRY - ITEM_POTION] = gItemEffect_TamatoBerry,
[LAST_BERRY_INDEX - ITEM_POTION] = NULL,
[ITEM_CHERI_BERRY] = gItemEffect_CheriBerry,
[ITEM_CHESTO_BERRY] = gItemEffect_ChestoBerry,
[ITEM_PECHA_BERRY] = gItemEffect_PechaBerry,
[ITEM_RAWST_BERRY] = gItemEffect_RawstBerry,
[ITEM_ASPEAR_BERRY] = gItemEffect_AspearBerry,
[ITEM_LEPPA_BERRY] = gItemEffect_LeppaBerry,
[ITEM_ORAN_BERRY] = gItemEffect_OranBerry,
[ITEM_PERSIM_BERRY] = gItemEffect_PersimBerry,
[ITEM_LUM_BERRY] = gItemEffect_FullHeal,
[ITEM_SITRUS_BERRY] = gItemEffect_SitrusBerry,
[ITEM_POMEG_BERRY] = gItemEffect_PomegBerry,
[ITEM_KELPSY_BERRY] = gItemEffect_KelpsyBerry,
[ITEM_QUALOT_BERRY] = gItemEffect_QualotBerry,
[ITEM_HONDEW_BERRY] = gItemEffect_HondewBerry,
[ITEM_GREPA_BERRY] = gItemEffect_GrepaBerry,
[ITEM_TAMATO_BERRY] = gItemEffect_TamatoBerry,
[LAST_BERRY_INDEX] = NULL,
};

View File

@ -4288,7 +4288,10 @@ static bool8 IsHPRecoveryItem(u16 item)
if (item == ITEM_ENIGMA_BERRY_E_READER)
effect = gSaveBlock1Ptr->enigmaBerry.itemEffect;
else
effect = gItemEffectTable[item - ITEM_POTION];
effect = gItemEffectTable[item];
if (effect == NULL)
return FALSE;
if (effect[4] & ITEM4_HEAL_HP)
return TRUE;
@ -4799,9 +4802,9 @@ void ItemUseCB_PPRecovery(u8 taskId, TaskFunc task)
if (item == ITEM_ENIGMA_BERRY_E_READER)
effect = gSaveBlock1Ptr->enigmaBerry.itemEffect;
else
effect = gItemEffectTable[item - ITEM_POTION];
effect = gItemEffectTable[item];
if (!(effect[4] & ITEM4_HEAL_PP_ONE))
if (effect == NULL || !(effect[4] & ITEM4_HEAL_PP_ONE))
{
gPartyMenu.data1 = 0;
TryUsePPItem(taskId);
@ -5674,14 +5677,14 @@ u8 GetItemEffectType(u16 item)
const u8 *itemEffect;
u32 statusCure;
if (!ITEM_HAS_EFFECT(item))
return ITEM_EFFECT_NONE;
// Read the item's effect properties.
if (item == ITEM_ENIGMA_BERRY_E_READER)
itemEffect = gSaveBlock1Ptr->enigmaBerry.itemEffect;
else
itemEffect = gItemEffectTable[item - ITEM_POTION];
itemEffect = gItemEffectTable[item];
if (itemEffect == NULL)
return ITEM_EFFECT_NONE;
if ((itemEffect[0] & ITEM0_DIRE_HIT) || itemEffect[1] || (itemEffect[3] & ITEM3_GUARD_SPEC))
return ITEM_EFFECT_X_ITEM;

View File

@ -5818,9 +5818,7 @@ bool8 PokemonUseItemEffects(struct Pokemon *mon, u16 item, u8 partyIndex, u8 mov
}
// Skip using the item if it won't do anything
if (!ITEM_HAS_EFFECT(item))
return TRUE;
if (gItemEffectTable[item - ITEM_POTION] == NULL && item != ITEM_ENIGMA_BERRY_E_READER)
if (gItemEffectTable[item] == NULL && item != ITEM_ENIGMA_BERRY_E_READER)
return TRUE;
// Get item effect
@ -5833,7 +5831,7 @@ bool8 PokemonUseItemEffects(struct Pokemon *mon, u16 item, u8 partyIndex, u8 mov
}
else
{
itemEffect = gItemEffectTable[item - ITEM_POTION];
itemEffect = gItemEffectTable[item];
}
// Do item effect
@ -6387,9 +6385,9 @@ u8 GetItemEffectParamOffset(u16 itemId, u8 effectByte, u8 effectBit)
offset = ITEM_EFFECT_ARG_START;
temp = gItemEffectTable[itemId - ITEM_POTION];
temp = gItemEffectTable[itemId];
if (!temp && itemId != ITEM_ENIGMA_BERRY_E_READER)
if (temp != NULL && !temp && itemId != ITEM_ENIGMA_BERRY_E_READER)
return 0;
if (itemId == ITEM_ENIGMA_BERRY_E_READER)
@ -6517,7 +6515,7 @@ u8 *UseStatIncreaseItem(u16 itemId)
}
else
{
itemEffect = gItemEffectTable[itemId - ITEM_POTION];
itemEffect = gItemEffectTable[itemId];
}
gPotentialItemEffectBattler = gBattlerInMenuId;
@ -6591,7 +6589,7 @@ u16 GetEvolutionTargetSpecies(struct Pokemon *mon, u8 mode, u16 evolutionItem, s
partnerSpecies = GetMonData(tradePartner, MON_DATA_SPECIES, 0);
partnerHeldItem = GetMonData(tradePartner, MON_DATA_HELD_ITEM, 0);
if (partnerHeldItem == ITEM_ENIGMA_BERRY)
if (partnerHeldItem == ITEM_ENIGMA_BERRY_E_READER)
partnerHoldEffect = gSaveBlock1Ptr->enigmaBerry.holdEffect;
else
partnerHoldEffect = ItemId_GetHoldEffect(partnerHeldItem);
@ -7261,7 +7259,7 @@ void MonGainEVs(struct Pokemon *mon, u16 defeatedSpecies)
u8 bonus;
heldItem = GetMonData(mon, MON_DATA_HELD_ITEM, 0);
if (heldItem == ITEM_ENIGMA_BERRY)
if (heldItem == ITEM_ENIGMA_BERRY_E_READER)
{
if (gMain.inBattle)
holdEffect = gEnigmaBerries[0].holdEffect;

View File

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

View File

@ -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); };
@ -21,12 +21,11 @@ SINGLE_BATTLE_TEST("Compound Eyes raises accuracy")
// than we expect.
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);
PLAYER(SPECIES_BUTTERFREE) { Ability(ABILITY_TINTED_LENS); };
PLAYER(SPECIES_BUTTERFREE) { Ability(ABILITY_COMPOUND_EYES); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_FISSURE); }

View File

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

View File

@ -95,3 +95,30 @@ DOUBLE_BATTLE_TEST("Intimidate doesn't activate on an empty field in a double ba
MESSAGE("Foe Arbok's Intimidate cuts Abra's attack!");
}
}
SINGLE_BATTLE_TEST("Intimidate and Eject Button force the opponent to Attack")
{
GIVEN {
ASSUME(gItems[ITEM_EJECT_BUTTON].holdEffect == HOLD_EFFECT_EJECT_BUTTON);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_HITMONTOP) { Moves(MOVE_TACKLE); };
} WHEN {
TURN {
MOVE(player, MOVE_QUICK_ATTACK);
MOVE(opponent, MOVE_TACKLE);
SEND_OUT(opponent, 1);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Wobbuffet is switched out with the Eject Button!");
MESSAGE("2 sent out Hitmontop!");
ABILITY_POPUP(opponent, ABILITY_INTIMIDATE);
MESSAGE("Foe Hitmontop's Intimidate cuts Wobbuffet's attack!");
NONE_OF {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
MESSAGE("Foe Hitmontop used Tackle!");
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,189 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gItems[ITEM_EJECT_BUTTON].holdEffect == HOLD_EFFECT_EJECT_BUTTON);
}
SINGLE_BATTLE_TEST("Eject Button is not triggered when there is nothing to switch in")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_WOBBUFFET) { HP(0); };
} WHEN {
TURN {
MOVE(player, MOVE_QUICK_ATTACK);
MOVE(opponent, MOVE_TACKLE);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_QUICK_ATTACK, player);
NONE_OF {
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Wobbuffet is switched out with the Eject Button!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
}
}
SINGLE_BATTLE_TEST("Eject Button is not activated by a Sheer Force boosted move")
{
GIVEN {
PLAYER(SPECIES_NIDOKING) { Ability(ABILITY_SHEER_FORCE); };
OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(player, MOVE_FLAMETHROWER);
MOVE(opponent, MOVE_TACKLE);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_FLAMETHROWER, player);
NONE_OF {
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Wobbuffet is switched out with the Eject Button!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
}
}
SINGLE_BATTLE_TEST("Eject Button will not activate under Substitute")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_RAICHU) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(opponent, MOVE_SUBSTITUTE);
MOVE(player, MOVE_TACKLE);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_SUBSTITUTE, opponent);
MESSAGE("Foe Raichu made a SUBSTITUTE!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
MESSAGE("The SUBSTITUTE took damage for Foe Raichu!");
NONE_OF {
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Raichu is switched out with the Eject Button!");
}
}
}
SINGLE_BATTLE_TEST("Eject Button is not blocked by trapping abilities or moves")
{
GIVEN {
PLAYER(SPECIES_DUGTRIO) { Ability(ABILITY_ARENA_TRAP); };
OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(player, MOVE_TACKLE);
SEND_OUT(opponent, 1);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Wobbuffet is switched out with the Eject Button!");
MESSAGE("2 sent out Wobbuffet!");
}
}
SINGLE_BATTLE_TEST("Eject Button is not triggered after the mon loses Eject Button")
{
GIVEN {
PLAYER(SPECIES_RAICHU);
OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(player, MOVE_KNOCK_OFF);
MOVE(opponent, MOVE_TACKLE);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_KNOCK_OFF, player);
NONE_OF {
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Wobbuffet is switched out with the Eject Button!");
}
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
}
}
SINGLE_BATTLE_TEST("Eject Button is not triggered after given to player by Picketpocket")
{
GIVEN {
PLAYER(SPECIES_REGIELEKI) { Item(ITEM_EJECT_BUTTON); };
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_SNEASEL) { Ability(ABILITY_PICKPOCKET); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(player, MOVE_TACKLE);
MOVE(opponent, MOVE_TACKLE);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, player);
ABILITY_POPUP(opponent, ABILITY_PICKPOCKET);
MESSAGE("Foe Sneasel stole Regieleki's Eject Button!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_TACKLE, opponent);
}
}
SINGLE_BATTLE_TEST("Eject Button has no chance to activate after Dragon Tail")
{
GIVEN {
PLAYER(SPECIES_KOMMO_O);
OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_CHANSEY);
} WHEN {
TURN {
MOVE(player, MOVE_DRAGON_TAIL);
MOVE(opponent, MOVE_TACKLE);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DRAGON_TAIL, player);
MESSAGE("Foe Chansey was dragged out!");
NONE_OF {
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Chansey is switched out with the Eject Button!");
}
}
}
SINGLE_BATTLE_TEST("Eject Button prevents Volt Switch / U-Turn from activating")
{
GIVEN {
PLAYER(SPECIES_MANECTRIC);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) { Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_WYNAUT);
} WHEN {
TURN {
MOVE(player, MOVE_VOLT_SWITCH);
SEND_OUT(opponent, 1);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_VOLT_SWITCH, player);
MESSAGE("Foe Wobbuffet is switched out with the Eject Button!");
}
}
SINGLE_BATTLE_TEST("Eject Button is activated before Emergency Exit")
{
GIVEN {
PLAYER(SPECIES_LATIAS);
OPPONENT(SPECIES_GOLISOPOD) { Ability(ABILITY_EMERGENCY_EXIT); Item(ITEM_EJECT_BUTTON); };
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN {
MOVE(player, MOVE_THUNDERBOLT);
SEND_OUT(opponent, 1);
}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_THUNDERBOLT, player);
ANIMATION(ANIM_TYPE_GENERAL, B_ANIM_HELD_ITEM_EFFECT, opponent);
MESSAGE("Foe Golisopod is switched out with the Eject Button!");
}
}

View File

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

View File

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

View File

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

22
test/move_effect_assist.c Normal file
View File

@ -0,0 +1,22 @@
#include "global.h"
#include "test_battle.h"
ASSUMPTIONS
{
ASSUME(gBattleMoves[MOVE_ASSIST].effect == EFFECT_ASSIST);
}
SINGLE_BATTLE_TEST("Assist fails if there are no valid moves to choose from")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) {Moves(MOVE_ASSIST, MOVE_CELEBRATE, MOVE_METRONOME, MOVE_ME_FIRST); }
PLAYER(SPECIES_WOBBUFFET) {Moves(MOVE_ASSIST, MOVE_ENDURE, MOVE_DRAGON_TAIL, MOVE_SPOTLIGHT); }
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_ASSIST); }
} SCENE {
MESSAGE("Wobbuffet used Assist!");
NOT ANIMATION(ANIM_TYPE_MOVE, MOVE_ASSIST, player);
MESSAGE("But it failed!");
}
}

View File

@ -327,7 +327,6 @@ DOUBLE_BATTLE_TEST("Defog lowers evasiveness by 1 and removes Aurora Veil from p
DOUBLE_BATTLE_TEST("Defog lowers evasiveness by 1 and removes everything it can")
{
bool32 defogTurn = FALSE;
GIVEN {
ASSUME(gBattleMoves[MOVE_HAIL].effect == EFFECT_HAIL);
ASSUME(gSpeciesInfo[SPECIES_GLALIE].types[0] == TYPE_ICE);
@ -343,33 +342,29 @@ DOUBLE_BATTLE_TEST("Defog lowers evasiveness by 1 and removes everything it can"
TURN { MOVE(playerLeft, MOVE_STICKY_WEB); MOVE(playerRight, MOVE_SPIKES); MOVE(opponentLeft, MOVE_STICKY_WEB); MOVE(opponentRight, MOVE_SPIKES); }
TURN { SWITCH(playerLeft, 2); SWITCH(playerRight, 3); SWITCH(opponentLeft, 2); SWITCH(opponentRight, 3);}
TURN { MOVE(playerLeft, MOVE_TOXIC_SPIKES); MOVE(playerRight, MOVE_STEALTH_ROCK); MOVE(opponentLeft, MOVE_TOXIC_SPIKES); MOVE(opponentRight, MOVE_STEALTH_ROCK); }
TURN { MOVE(playerLeft, MOVE_HAIL); MOVE(playerRight, MOVE_AURORA_VEIL); MOVE(opponentLeft, MOVE_AURORA_VEIL); MOVE(opponentRight, MOVE_STEALTH_ROCK); }
TURN { MOVE(playerLeft, MOVE_REFLECT); MOVE(playerRight, MOVE_LIGHT_SCREEN); MOVE(opponentLeft, MOVE_REFLECT); MOVE(opponentRight, MOVE_LIGHT_SCREEN); }
TURN { MOVE(playerLeft, MOVE_MIST); MOVE(playerRight, MOVE_SAFEGUARD); MOVE(opponentLeft, MOVE_MIST); MOVE(opponentRight, MOVE_SAFEGUARD); }
TURN { defogTurn = TRUE ; MOVE(opponentRight, MOVE_DEFOG, target:playerLeft);}
TURN { MOVE(playerLeft, MOVE_HAIL); MOVE(playerRight, MOVE_AURORA_VEIL); MOVE(opponentLeft, MOVE_AURORA_VEIL); MOVE(opponentRight, MOVE_LIGHT_SCREEN); }
TURN { MOVE(playerLeft, MOVE_REFLECT); MOVE(playerRight, MOVE_LIGHT_SCREEN); MOVE(opponentLeft, MOVE_REFLECT); MOVE(opponentRight, MOVE_SAFEGUARD); }
TURN { MOVE(playerLeft, MOVE_MIST); MOVE(playerRight, MOVE_SAFEGUARD); MOVE(opponentLeft, MOVE_MIST); MOVE(opponentRight, MOVE_DEFOG, target: playerLeft); }
} SCENE {
if (defogTurn == TRUE)
{
MESSAGE("Foe Glalie used Defog!");
MESSAGE("Glalie is protected by MIST!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_DEFOG, opponentRight);
// Player side
MESSAGE("Ally's Reflect wore off!");
MESSAGE("Ally's Light Screen wore off!");
MESSAGE("Ally's Mist wore off!");
MESSAGE("Ally's Aurora Veil wore off!");
MESSAGE("Ally's Safeguard wore off!");
MESSAGE("Foe Glalie used Defog!");
MESSAGE("Glalie is protected by MIST!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_DEFOG, opponentRight);
// Player side
MESSAGE("Ally's Reflect wore off!");
MESSAGE("Ally's Light Screen wore off!");
MESSAGE("Ally's Mist wore off!");
MESSAGE("Ally's Aurora Veil wore off!");
MESSAGE("Ally's Safeguard wore off!");
MESSAGE("The spikes disappeared from the ground around your team!");
MESSAGE("The pointed stones disappeared from around your team!");
MESSAGE("The poison spikes disappeared from the ground around your team!");
MESSAGE("The sticky web has disappeared from the ground around your team!");
MESSAGE("The spikes disappeared from the ground around your team!");
MESSAGE("The pointed stones disappeared from around your team!");
MESSAGE("The poison spikes disappeared from the ground around your team!");
MESSAGE("The sticky web has disappeared from the ground around your team!");
// Opponent side
MESSAGE("The spikes disappeared from the ground around the opposing team!");
MESSAGE("The pointed stones disappeared from around the opposing team!");
MESSAGE("The poison spikes disappeared from the ground around the opposing team!");
MESSAGE("The sticky web has disappeared from the ground around the opposing team!");
}
// Opponent side
MESSAGE("The spikes disappeared from the ground around the opposing team!");
MESSAGE("The pointed stones disappeared from around the opposing team!");
MESSAGE("The poison spikes disappeared from the ground around the opposing team!");
MESSAGE("The sticky web has disappeared from the ground around the opposing team!");
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -53,6 +53,16 @@ extern const u8 gTestRunnerI;
extern const char gTestRunnerArgv[256];
extern const struct TestRunner gAssumptionsRunner;
struct FunctionTestRunnerState
{
u8 parameters;
u8 runParameter;
};
extern const struct TestRunner gFunctionTestRunner;
extern struct FunctionTestRunnerState *gFunctionTestRunnerState;
extern struct TestRunnerState gTestRunnerState;
void CB2_TestRunner(void);
@ -62,6 +72,17 @@ void Test_ExitWithResult(enum TestResult, const char *fmt, ...);
s32 MgbaPrintf_(const char *fmt, ...);
#define TEST(_name) \
static void CAT(Test, __LINE__)(void); \
__attribute__((section(".tests"))) static const struct Test CAT(sTest, __LINE__) = \
{ \
.name = _name, \
.filename = __FILE__, \
.runner = &gFunctionTestRunner, \
.data = (void *)CAT(Test, __LINE__), \
}; \
static void CAT(Test, __LINE__)(void)
#define ASSUMPTIONS \
static void Assumptions(void); \
__attribute__((section(".tests"))) static const struct Test sAssumptions = \
@ -138,4 +159,6 @@ s32 MgbaPrintf_(const char *fmt, ...);
#define KNOWN_FAILING \
Test_ExpectedResult(TEST_RESULT_FAIL)
#define PARAMETRIZE if (gFunctionTestRunnerState->parameters++ == gFunctionTestRunnerState->runParameter)
#endif

View File

@ -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;
@ -639,13 +672,20 @@ extern struct BattleTestRunnerState *gBattleTestRunnerState;
/* Parametrize */
#undef PARAMETRIZE // Override test/test.h's implementation.
#define PARAMETRIZE if (gBattleTestRunnerState->parametersCount++ == i)
/* 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 */
@ -719,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;

View File

@ -4,6 +4,7 @@
#include "gpu_regs.h"
#include "main.h"
#include "malloc.h"
#include "random.h"
#include "test.h"
#include "test_runner.h"
@ -12,6 +13,7 @@
void CB2_TestRunner(void);
EWRAM_DATA struct TestRunnerState gTestRunnerState;
EWRAM_DATA struct FunctionTestRunnerState *gFunctionTestRunnerState;
void TestRunner_Battle(const struct Test *);
@ -230,6 +232,38 @@ void Test_ExpectedResult(enum TestResult result)
gTestRunnerState.expectedResult = result;
}
static void FunctionTest_SetUp(void *data)
{
(void)data;
gFunctionTestRunnerState = AllocZeroed(sizeof(*gFunctionTestRunnerState));
SeedRng(0);
}
static void FunctionTest_Run(void *data)
{
void (*function)(void) = data;
do
{
if (gFunctionTestRunnerState->parameters)
MgbaPrintf_(":N%s %d/%d", gTestRunnerState.test->name, gFunctionTestRunnerState->runParameter + 1, gFunctionTestRunnerState->parameters);
gFunctionTestRunnerState->parameters = 0;
function();
} while (++gFunctionTestRunnerState->runParameter < gFunctionTestRunnerState->parameters);
}
static void FunctionTest_TearDown(void *data)
{
(void)data;
FREE_AND_SET_NULL(gFunctionTestRunnerState);
}
const struct TestRunner gFunctionTestRunner =
{
.setUp = FunctionTest_SetUp,
.run = FunctionTest_Run,
.tearDown = FunctionTest_TearDown,
};
static void Assumptions_Run(void *data)
{
void (*function)(void) = data;
@ -288,11 +322,12 @@ static void Intr_Timer2(void)
void Test_ExitWithResult(enum TestResult result, const char *fmt, ...)
{
bool32 handled = FALSE;
gTestRunnerState.result = result;
ReinitCallbacks();
if (gTestRunnerState.test->runner->handleExitWithResult
&& !gTestRunnerState.test->runner->handleExitWithResult(gTestRunnerState.test->data, result)
&& gTestRunnerState.result != gTestRunnerState.expectedResult)
if (gTestRunnerState.test->runner->handleExitWithResult)
handled = gTestRunnerState.test->runner->handleExitWithResult(gTestRunnerState.test->data, result);
if (!handled && gTestRunnerState.result != gTestRunnerState.expectedResult)
{
va_list va;
va_start(va, fmt);

View File

@ -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)
@ -1025,6 +1106,31 @@ void Status1_(u32 sourceLine, u32 status1)
SetMonData(DATA.currentMon, MON_DATA_STATUS, &status1);
}
static const char *const sBattlerIdentifiersSingles[] =
{
"player",
"opponent",
};
static const char *const sBattlerIdentifiersDoubles[] =
{
"playerLeft",
"opponentLeft",
"playerRight",
"opponentRight",
};
static const char *BattlerIdentifier(s32 battlerId)
{
const struct BattleTest *test = gTestRunnerState.test->data;
switch (test->type)
{
case BATTLE_TEST_SINGLES: return sBattlerIdentifiersSingles[battlerId];
case BATTLE_TEST_DOUBLES: return sBattlerIdentifiersDoubles[battlerId];
}
return "<unknown>";
}
static void PushBattlerAction(u32 sourceLine, s32 battlerId, u32 actionType, u32 byte)
{
u32 recordIndex = DATA.recordIndexes[battlerId]++;
@ -1037,16 +1143,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 +1218,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)
@ -1195,7 +1292,6 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
else if (moveId == MOVE_NONE)
{
INVALID_IF(DATA.explicitMoves[battlerId & BIT_SIDE] & (1 << DATA.currentMonIndexes[battlerId]), "Missing explicit %S", gMoveNames[ctx.move]);
INVALID_IF(i == MAX_MON_MOVES, "Too many different moves");
SetMonData(mon, MON_DATA_MOVE1 + i, &ctx.move);
SetMonData(DATA.currentMon, MON_DATA_PP1 + i, &gBattleMoves[ctx.move].pp);
moveSlot = i;
@ -1203,6 +1299,7 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
break;
}
}
INVALID_IF(i == MAX_MON_MOVES, "Too many different moves for %s", BattlerIdentifier(battlerId));
}
else if (ctx.explicitMoveSlot)
{
@ -1230,6 +1327,7 @@ void Move(u32 sourceLine, struct BattlePokemon *battler, struct MoveContext ctx)
const struct BattleMove *move = &gBattleMoves[moveId];
if (move->target == MOVE_TARGET_RANDOM
|| move->target == MOVE_TARGET_BOTH
|| move->target == MOVE_TARGET_DEPENDS
|| move->target == MOVE_TARGET_FOES_AND_ALLY
|| move->target == MOVE_TARGET_OPPONENTS_FIELD
|| move->target == MOVE_TARGET_ALL_BATTLERS)
@ -1256,21 +1354,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)))
{
@ -1291,14 +1380,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)