Skip to content

Commit

Permalink
RandomElement for structured RNG (#2868)
Browse files Browse the repository at this point in the history
  • Loading branch information
AsparagusEduardo committed Apr 10, 2023
2 parents 5d885b0 + 35952cf commit 5c1efe9
Show file tree
Hide file tree
Showing 8 changed files with 214 additions and 86 deletions.
33 changes: 30 additions & 3 deletions include/random.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,12 @@ void SeedRng2(u16 seed);
*
* RandomTag identifies the purpose of the value.
*
* RandomUniform(tag, lo, hi) returns a number from lo to hi inclusive.
* RandomUniform(tag, lo, hi) returns a number from lo to hi inclusive
* with uniform probability.
*
* RandomElement(tag, array) returns an element in array with uniform
* probability. The array must be known at compile-time (e.g. a global
* const array).
*
* RandomPercentage(tag, t) returns FALSE with probability (1-t)/100,
* and TRUE with probability t/100.
Expand All @@ -47,6 +52,7 @@ enum RandomTag
RNG_CRITICAL_HIT,
RNG_CUTE_CHARM,
RNG_DAMAGE_MODIFIER,
RNG_DIRE_CLAW,
RNG_FLAME_BODY,
RNG_FORCE_RANDOM_SWITCH,
RNG_FROZEN,
Expand All @@ -60,6 +66,7 @@ enum RandomTag
RNG_SPEED_TIE,
RNG_STATIC,
RNG_STENCH,
RNG_TRI_ATTACK,
};

#define RandomWeighted(tag, ...) \
Expand All @@ -73,14 +80,34 @@ enum RandomTag

#define RandomPercentage(tag, t) \
({ \
const u8 weights[] = { 100 - t, t }; \
RandomWeightedArray(tag, 100, ARRAY_COUNT(weights), weights); \
u32 r; \
if (t <= 0) \
{ \
r = FALSE; \
} \
else if (t >= 100) \
{ \
r = TRUE; \
} \
else \
{ \
const u8 weights[] = { 100 - t, t }; \
r = RandomWeightedArray(tag, 100, ARRAY_COUNT(weights), weights); \
} \
r; \
})

#define RandomElement(tag, array) \
({ \
*(typeof((array)[0]) *)RandomElementArray(tag, array, sizeof((array)[0]), ARRAY_COUNT(array)); \
})

u32 RandomUniform(enum RandomTag, u32 lo, u32 hi);
u32 RandomWeightedArray(enum RandomTag, u32 sum, u32 n, const u8 *weights);
const void *RandomElementArray(enum RandomTag, const void *array, size_t size, size_t count);

u32 RandomUniformDefault(enum RandomTag, u32 lo, u32 hi);
u32 RandomWeightedArrayDefault(enum RandomTag, u32 sum, u32 n, const u8 *weights);
const void *RandomElementArrayDefault(enum RandomTag, const void *array, size_t size, size_t count);

#endif // GUARD_RANDOM_H
5 changes: 3 additions & 2 deletions src/battle_script_commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -3318,7 +3318,8 @@ void SetMoveEffect(bool32 primary, u32 certain)
}
else
{
gBattleScripting.moveEffect = Random() % 3 + 3;
static const u8 sTriAttackEffects[] = { MOVE_EFFECT_BURN, MOVE_EFFECT_FREEZE, MOVE_EFFECT_PARALYSIS };
gBattleScripting.moveEffect = RandomElement(RNG_TRI_ATTACK, sTriAttackEffects);
SetMoveEffect(FALSE, 0);
}
break;
Expand Down Expand Up @@ -3779,7 +3780,7 @@ void SetMoveEffect(bool32 primary, u32 certain)
if (!gBattleMons[gEffectBattler].status1)
{
static const u8 sDireClawEffects[] = { MOVE_EFFECT_POISON, MOVE_EFFECT_PARALYSIS, MOVE_EFFECT_SLEEP };
gBattleScripting.moveEffect = sDireClawEffects[Random() % ARRAY_COUNT(sDireClawEffects)];
gBattleScripting.moveEffect = RandomElement(RNG_DIRE_CLAW, sDireClawEffects);
SetMoveEffect(TRUE, 0);
}
break;
Expand Down
10 changes: 9 additions & 1 deletion src/random.c
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,12 @@ 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);

__attribute__((weak, alias("RandomElementArrayDefault")))
const void *RandomElementArray(enum RandomTag tag, const void *array, size_t size, size_t count);

u32 RandomUniformDefault(enum RandomTag tag, u32 lo, u32 hi)
{
return lo + (((hi - lo) * Random()) >> 16);
return lo + (((hi - lo + 1) * Random()) >> 16);
}

u32 RandomWeightedArrayDefault(enum RandomTag tag, u32 sum, u32 n, const u8 *weights)
Expand All @@ -55,3 +58,8 @@ u32 RandomWeightedArrayDefault(enum RandomTag tag, u32 sum, u32 n, const u8 *wei
}
return n - 1;
}

const void *RandomElementArrayDefault(enum RandomTag tag, const void *array, size_t size, size_t count)
{
return (const u8 *)array + size * RandomUniformDefault(tag, 0, count - 1);
}
46 changes: 18 additions & 28 deletions test/move_effect_dire_claw.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,14 @@ ASSUMPTIONS
ASSUME(gBattleMoves[MOVE_DIRE_CLAW].effect == EFFECT_DIRE_CLAW);
}

// found by brute-force
#define RNG_SLEEP 0xcb0
#define RNG_POISON 0x2BE
#define RNG_PARALYSIS 5

SINGLE_BATTLE_TEST("Dire Claw can inflict poison, paralysis or sleep")
{
u8 statusAnim;
u32 rng;
KNOWN_FAILING;
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; rng = RNG_POISON; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; rng = RNG_SLEEP; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; }
PASSES_RANDOMLY(1, 3, RNG_DIRE_CLAW);
GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
Expand Down Expand Up @@ -48,15 +41,14 @@ SINGLE_BATTLE_TEST("Dire Claw cannot poison/paralyze poison/electric types respe
u16 species;
u32 rng;
#if B_PARALYZE_ELECTRIC >= GEN_6
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_RAICHU; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_RAICHU; }
#endif // B_PARALYZE_ELECTRIC
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; rng = RNG_POISON; species = SPECIES_ARBOK;}
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; rng = MOVE_EFFECT_POISON; species = SPECIES_ARBOK;}
GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(species);
} WHEN {
TURN { MOVE(player, MOVE_DIRE_CLAW); }
TURN { MOVE(player, MOVE_DIRE_CLAW, WITH_RNG(RNG_DIRE_CLAW, rng)); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DIRE_CLAW, player);
Expand All @@ -76,21 +68,20 @@ SINGLE_BATTLE_TEST("Dire Claw cannot poison/paralyze/cause to fall asleep pokemo
u8 statusAnim;
u16 species, ability;
u32 rng;
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_RAICHU; ability = ABILITY_LIGHTNING_ROD; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_JOLTEON; ability = ABILITY_VOLT_ABSORB; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_RAICHU; ability = ABILITY_LIGHTNING_ROD; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_JOLTEON; ability = ABILITY_VOLT_ABSORB; }
#if P_GEN_4_POKEMON == TRUE
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_ELECTIVIRE; ability = ABILITY_MOTOR_DRIVE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_ELECTIVIRE; ability = ABILITY_MOTOR_DRIVE; }
#endif // P_GEN_4_POKEMON
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; rng = RNG_POISON; species = SPECIES_ZANGOOSE; ability = ABILITY_IMMUNITY; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; rng = RNG_SLEEP; species = SPECIES_VIGOROTH; ability = ABILITY_VITAL_SPIRIT; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; rng = RNG_SLEEP; species = SPECIES_HYPNO; ability = ABILITY_INSOMNIA; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; rng = MOVE_EFFECT_POISON; species = SPECIES_ZANGOOSE; ability = ABILITY_IMMUNITY; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; rng = MOVE_EFFECT_SLEEP; species = SPECIES_VIGOROTH; ability = ABILITY_VITAL_SPIRIT; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; rng = MOVE_EFFECT_SLEEP; species = SPECIES_HYPNO; ability = ABILITY_INSOMNIA; }

GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(species) {Ability(ability);}
} WHEN {
TURN { MOVE(player, MOVE_DIRE_CLAW); }
TURN { MOVE(player, MOVE_DIRE_CLAW, WITH_RNG(RNG_DIRE_CLAW, rng)); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DIRE_CLAW, player);
Expand All @@ -112,15 +103,14 @@ SINGLE_BATTLE_TEST("Dire Claw cannot poison/paralyze/cause to fall asleep a mon
{
u8 statusAnim;
u32 rng;
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; rng = RNG_POISON; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; rng = RNG_SLEEP; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PSN; rng = MOVE_EFFECT_POISON; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_SLP; rng = MOVE_EFFECT_SLEEP; }
GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) {Status1(STATUS1_BURN);}
} WHEN {
TURN { MOVE(player, MOVE_DIRE_CLAW); }
TURN { MOVE(player, MOVE_DIRE_CLAW, WITH_RNG(RNG_DIRE_CLAW, rng)); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_DIRE_CLAW, player);
Expand Down
48 changes: 19 additions & 29 deletions test/move_effect_tri_attack.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,21 +6,14 @@ ASSUMPTIONS
ASSUME(gBattleMoves[MOVE_TRI_ATTACK].effect == EFFECT_TRI_ATTACK);
}

// found by brute-force
#define RNG_PARALYSIS 0xcb0
#define RNG_BURN 0x2BE
#define RNG_FREEZE 5

SINGLE_BATTLE_TEST("Tri Attack can inflict paralysis, burn or freeze")
{
u8 statusAnim;
u32 rng;
KNOWN_FAILING;
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = RNG_BURN; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; rng = RNG_FREEZE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; }
PASSES_RANDOMLY(1, 3, RNG_TRI_ATTACK);
GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
Expand Down Expand Up @@ -48,16 +41,15 @@ SINGLE_BATTLE_TEST("Tri Attack cannot paralyze/burn/freeze electric/fire/ice typ
u16 species;
u32 rng;
#if B_PARALYZE_ELECTRIC >= GEN_6
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_RAICHU;}
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_RAICHU;}
#endif // B_PARALYZE_ELECTRIC
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = RNG_BURN; species = SPECIES_ARCANINE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; rng = RNG_FREEZE; species = SPECIES_GLALIE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = MOVE_EFFECT_BURN; species = SPECIES_ARCANINE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; rng = MOVE_EFFECT_FREEZE; species = SPECIES_GLALIE; }
GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(species);
} WHEN {
TURN { MOVE(player, MOVE_TRI_ATTACK); }
TURN { MOVE(player, MOVE_TRI_ATTACK, WITH_RNG(RNG_TRI_ATTACK, rng)); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TRI_ATTACK, player);
Expand All @@ -80,23 +72,22 @@ SINGLE_BATTLE_TEST("Tri Attack cannot paralyze/burn/freeze pokemon with abilitie
u8 statusAnim;
u16 species, ability;
u32 rng;
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_RAICHU; ability = ABILITY_LIGHTNING_ROD; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_JOLTEON; ability = ABILITY_VOLT_ABSORB; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_RAICHU; ability = ABILITY_LIGHTNING_ROD; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_JOLTEON; ability = ABILITY_VOLT_ABSORB; }
#if P_GEN_4_POKEMON == TRUE
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; species = SPECIES_ELECTIVIRE; ability = ABILITY_MOTOR_DRIVE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; species = SPECIES_ELECTIVIRE; ability = ABILITY_MOTOR_DRIVE; }
#endif // P_GEN_4_POKEMON
#if P_GEN_7_POKEMON == TRUE
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = RNG_BURN; species = SPECIES_DEWPIDER; ability = ABILITY_WATER_BUBBLE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = MOVE_EFFECT_BURN; species = SPECIES_DEWPIDER; ability = ABILITY_WATER_BUBBLE; }
#endif // P_GEN_7_POKEMON
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = RNG_BURN; species = SPECIES_SEAKING; ability = ABILITY_WATER_VEIL; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; rng = RNG_FREEZE; species = SPECIES_CAMERUPT; ability = ABILITY_MAGMA_ARMOR; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = MOVE_EFFECT_BURN; species = SPECIES_SEAKING; ability = ABILITY_WATER_VEIL; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; rng = MOVE_EFFECT_FREEZE; species = SPECIES_CAMERUPT; ability = ABILITY_MAGMA_ARMOR; }

GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(species) {Ability(ability);}
} WHEN {
TURN { MOVE(player, MOVE_TRI_ATTACK); }
TURN { MOVE(player, MOVE_TRI_ATTACK, WITH_RNG(RNG_TRI_ATTACK, rng)); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TRI_ATTACK, player);
Expand All @@ -118,15 +109,14 @@ SINGLE_BATTLE_TEST("Tri Attack cannot paralyze/burn/freeze a mon which is alread
{
u8 statusAnim;
u32 rng;
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = RNG_PARALYSIS; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = RNG_BURN; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; rng = RNG_FREEZE; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_PRZ; rng = MOVE_EFFECT_PARALYSIS; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_BRN; rng = MOVE_EFFECT_BURN; }
PARAMETRIZE { statusAnim = B_ANIM_STATUS_FRZ; rng = MOVE_EFFECT_FREEZE; }
GIVEN {
RNGSeed(rng);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET) {Status1(STATUS1_SLEEP);}
} WHEN {
TURN { MOVE(player, MOVE_TRI_ATTACK); }
TURN { MOVE(player, MOVE_TRI_ATTACK, WITH_RNG(RNG_TRI_ATTACK, rng)); }
TURN {}
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_TRI_ATTACK, player);
Expand Down
33 changes: 32 additions & 1 deletion test/random.c
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,17 @@ TEST("RandomWeighted generates 0..n-1")
}
}

TEST("RandomElement generates an element")
{
u32 i;
static const u8 es[4] = { 1, 2, 4, 8 };
for (i = 0; i < 1024; i++)
{
u32 e = *(const u8 *)RandomElementArrayDefault(RNG_NONE, es, sizeof(es[0]), ARRAY_COUNT(es));
EXPECT(e == 1 || e == 2 || e == 4 || e == 8);
}
}

TEST("RandomUniform generates uniform distribution")
{
u32 i, error;
Expand All @@ -42,7 +53,7 @@ TEST("RandomUniform generates uniform distribution")
memset(distribution, 0, sizeof(distribution));
for (i = 0; i < 4096; i++)
{
u32 r = RandomUniformDefault(RNG_NONE, 0, ARRAY_COUNT(distribution));
u32 r = RandomUniformDefault(RNG_NONE, 0, ARRAY_COUNT(distribution) - 1);
EXPECT(0 <= r && r < ARRAY_COUNT(distribution));
distribution[r]++;
}
Expand Down Expand Up @@ -79,3 +90,23 @@ TEST("RandomWeighted generates distribution in proportion to the weights")

EXPECT_LT(error, UQ_4_12(0.025));
}

TEST("RandomElement generates a uniform distribution")
{
u32 i, error;
static const u8 es[4] = { 1, 2, 4, 8 };
u16 distribution[9];

memset(distribution, 0, sizeof(distribution));
for (i = 0; i < 4096; i++)
{
u32 e = *(const u8 *)RandomElementArrayDefault(RNG_NONE, es, sizeof(es[0]), ARRAY_COUNT(es));
distribution[e]++;
}

error = 0;
for (i = 0; i < ARRAY_COUNT(es); i++)
error += abs(UQ_4_12(0.25) - distribution[es[i]]);

EXPECT_LT(error, UQ_4_12(0.025));
}

0 comments on commit 5c1efe9

Please sign in to comment.