Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RNG for Metronome, multi-hit moves, and Loaded Dice #3159

Merged
merged 3 commits into from
Jul 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions include/random.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ void SeedRng2(u16 seed);
* RandomUniform(tag, lo, hi) returns a number from lo to hi inclusive
* with uniform probability.
*
* RandomUniformExcept(tag, lo, hi, reject) returns a number from lo to
* hi inclusive with uniform probability, excluding those for which
* reject returns TRUE.
*
* 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).
Expand All @@ -56,8 +60,11 @@ enum RandomTag
RNG_FLAME_BODY,
RNG_FORCE_RANDOM_SWITCH,
RNG_FROZEN,
RNG_HITS,
RNG_HOLD_EFFECT_FLINCH,
RNG_INFATUATION,
RNG_LOADED_DICE,
RNG_METRONOME,
RNG_PARALYSIS,
RNG_POISON_POINT,
RNG_RAMPAGE_TURNS,
Expand Down Expand Up @@ -103,10 +110,12 @@ enum RandomTag
})

u32 RandomUniform(enum RandomTag, u32 lo, u32 hi);
u32 RandomUniformExcept(enum RandomTag, u32 lo, u32 hi, bool32 (*reject)(u32));
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 RandomUniformExceptDefault(enum RandomTag, u32 lo, u32 hi, bool32 (*reject)(u32));
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);

Expand Down
72 changes: 26 additions & 46 deletions src/battle_script_commands.c
Original file line number Diff line number Diff line change
Expand Up @@ -12206,30 +12206,14 @@ static void Cmd_setmultihitcounter(void)
}
else
{
#if B_MULTI_HIT_CHANCE >= GEN_5
// Based on Gen 5's odds
// 35% for 2 hits
// 35% for 3 hits
// 15% for 4 hits
// 15% for 5 hits
gMultiHitCounter = Random() % 100;
if (gMultiHitCounter < 35)
gMultiHitCounter = 2;
else if (gMultiHitCounter < 35 + 35)
gMultiHitCounter = 3;
else if (gMultiHitCounter < 35 + 35 + 15)
gMultiHitCounter = 4;
else
gMultiHitCounter = 5;
#else
// 2 and 3 hits: 37.5%
// 4 and 5 hits: 12.5%
gMultiHitCounter = Random() % 4;
if (gMultiHitCounter > 1)
gMultiHitCounter = (Random() % 4) + 2;
else
gMultiHitCounter += 2;
#endif
// WARNING: These seem to be unused, see SetRandomMultiHitCounter.
#if B_MULTI_HIT_CHANCE >= GEN_5
// 35%: 2 hits, 35%: 3 hits, 15% 4 hits, 15% 5 hits.
gMultiHitCounter = RandomWeighted(RNG_HITS, 0, 0, 7, 7, 3, 3);
#else
// 37.5%: 2 hits, 37.5%: 3 hits, 12.5% 4 hits, 12.5% 5 hits.
gMultiHitCounter = RandomWeighted(RNG_HITS, 0, 0, 3, 3, 1, 1);
#endif
}
}

Expand Down Expand Up @@ -12977,41 +12961,37 @@ static void Cmd_mimicattackcopy(void)
}
}

static bool32 InvalidMetronomeMove(u32 move)
{
return gBattleMoves[move].effect == EFFECT_PLACEHOLDER
|| sForbiddenMoves[move] & FORBIDDEN_METRONOME;
}

static void Cmd_metronome(void)
{
CMD_ARGS();

#if B_METRONOME_MOVES >= GEN_9
u16 moveCount = MOVES_COUNT_GEN9;
u32 moveCount = MOVES_COUNT_GEN9;
#elif B_METRONOME_MOVES >= GEN_8
u16 moveCount = MOVES_COUNT_GEN8;
u32 moveCount = MOVES_COUNT_GEN8;
#elif B_METRONOME_MOVES >= GEN_7
u16 moveCount = MOVES_COUNT_GEN7;
u32 moveCount = MOVES_COUNT_GEN7;
#elif B_METRONOME_MOVES >= GEN_6
u16 moveCount = MOVES_COUNT_GEN6;
u32 moveCount = MOVES_COUNT_GEN6;
#elif B_METRONOME_MOVES >= GEN_5
u16 moveCount = MOVES_COUNT_GEN5;
u32 moveCount = MOVES_COUNT_GEN5;
#elif B_METRONOME_MOVES >= GEN_4
u16 moveCount = MOVES_COUNT_GEN4;
u32 moveCount = MOVES_COUNT_GEN4;
#elif B_METRONOME_MOVES >= GEN_3
u16 moveCount = MOVES_COUNT_GEN3;
u32 moveCount = MOVES_COUNT_GEN3;
#endif

while (TRUE)
{
gCurrentMove = (Random() % (moveCount - 1)) + 1;
if (gBattleMoves[gCurrentMove].effect == EFFECT_PLACEHOLDER)
continue;

if (!(sForbiddenMoves[gCurrentMove] & FORBIDDEN_METRONOME))
{
gHitMarker &= ~HITMARKER_ATTACKSTRING_PRINTED;
SetAtkCancellerForCalledMove();
gBattlescriptCurrInstr = gBattleScriptsForMoveEffects[gBattleMoves[gCurrentMove].effect];
gBattlerTarget = GetMoveTarget(gCurrentMove, NO_TARGET_OVERRIDE);
return;
}
}
gCurrentMove = RandomUniformExcept(RNG_METRONOME, 1, moveCount - 1, InvalidMetronomeMove);
gHitMarker &= ~HITMARKER_ATTACKSTRING_PRINTED;
SetAtkCancellerForCalledMove();
gBattlescriptCurrInstr = gBattleScriptsForMoveEffects[gBattleMoves[gCurrentMove].effect];
gBattlerTarget = GetMoveTarget(gCurrentMove, NO_TARGET_OVERRIDE);
}

static void Cmd_dmgtolevel(void)
Expand Down
36 changes: 10 additions & 26 deletions src/battle_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -10899,35 +10899,19 @@ bool32 CanTargetBattler(u8 battlerAtk, u8 battlerDef, u16 move)

static void SetRandomMultiHitCounter()
{
#if (B_MULTI_HIT_CHANCE >= GEN_5)
// Based on Gen 5's odds
// 35% for 2 hits
// 35% for 3 hits
// 15% for 4 hits
// 15% for 5 hits
gMultiHitCounter = Random() % 100;
if (gMultiHitCounter < 35)
gMultiHitCounter = 2;
else if (gMultiHitCounter < 35 + 35)
gMultiHitCounter = 3;
else if (gMultiHitCounter < 35 + 35 + 15)
gMultiHitCounter = 4;
if (GetBattlerHoldEffect(gBattlerAttacker, TRUE) == HOLD_EFFECT_LOADED_DICE)
{
gMultiHitCounter = RandomUniform(RNG_LOADED_DICE, 4, 5);
}
else
gMultiHitCounter = 5;
{
#if B_MULTI_HIT_CHANCE >= GEN_5
// 35%: 2 hits, 35%: 3 hits, 15% 4 hits, 15% 5 hits.
gMultiHitCounter = RandomWeighted(RNG_HITS, 0, 0, 7, 7, 3, 3);
#else
// 2 and 3 hits: 37.5%
// 4 and 5 hits: 12.5%
gMultiHitCounter = Random() % 4;
if (gMultiHitCounter > 1)
gMultiHitCounter = (Random() % 4) + 2;
else
gMultiHitCounter += 2;
// 37.5%: 2 hits, 37.5%: 3 hits, 12.5% 4 hits, 12.5% 5 hits.
gMultiHitCounter = RandomWeighted(RNG_HITS, 0, 0, 3, 3, 1, 1);
#endif

if (gMultiHitCounter < 4 && GetBattlerHoldEffect(gBattlerAttacker, TRUE) == HOLD_EFFECT_LOADED_DICE)
{
// If roll 4 or 5 Loaded Dice doesn't do anything. Otherwise it rolls the number of hits as 5 minus a random integer from 0 to 1 inclusive.
gMultiHitCounter = 5 - (Random() & 1);
}
}

Expand Down
13 changes: 13 additions & 0 deletions src/random.c
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ u16 Random2(void)
__attribute__((weak, alias("RandomUniformDefault")))
u32 RandomUniform(enum RandomTag tag, u32 lo, u32 hi);

__attribute__((weak, alias("RandomUniformExceptDefault")))
u32 RandomUniformExcept(enum RandomTag, u32 lo, u32 hi, bool32 (*reject)(u32));

__attribute__((weak, alias("RandomWeightedArrayDefault")))
u32 RandomWeightedArray(enum RandomTag tag, u32 sum, u32 n, const u8 *weights);

Expand All @@ -46,6 +49,16 @@ u32 RandomUniformDefault(enum RandomTag tag, u32 lo, u32 hi)
return lo + (((hi - lo + 1) * Random()) >> 16);
}

u32 RandomUniformExceptDefault(enum RandomTag tag, u32 lo, u32 hi, bool32 (*reject)(u32))
{
while (TRUE)
{
u32 n = RandomUniformDefault(tag, lo, hi);
if (!reject(n))
return n;
}
}

u32 RandomWeightedArrayDefault(enum RandomTag tag, u32 sum, u32 n, const u8 *weights)
{
s32 i, targetSum;
Expand Down
20 changes: 6 additions & 14 deletions test/move_effect_metronome.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,13 @@ ASSUMPTIONS
ASSUME(gBattleMoves[MOVE_METRONOME].effect == EFFECT_METRONOME);
}

// To do: Turn the seeds to work with WITH_RNG for Metronome.
#define RNG_METRONOME_SCRATCH 0x118
#define RNG_METRONOME_PSN_POWDER 0x119
#define RNG_METRONOME_ROCK_BLAST 0x1F5

SINGLE_BATTLE_TEST("Metronome picks a random move")
{
GIVEN {
RNGSeed(RNG_METRONOME_SCRATCH);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_METRONOME); }
TURN { MOVE(player, MOVE_METRONOME, WITH_RNG(RNG_METRONOME, MOVE_SCRATCH)); }
} SCENE {
MESSAGE("Wobbuffet used Metronome!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_METRONOME, player);
Expand All @@ -34,11 +28,10 @@ SINGLE_BATTLE_TEST("Metronome's called powder move fails against Grass Types")
ASSUME(gBattleMoves[MOVE_POISON_POWDER].flags & FLAG_POWDER);
ASSUME(gSpeciesInfo[SPECIES_TANGELA].types[0] == TYPE_GRASS);
ASSUME(gBattleMoves[MOVE_POISON_POWDER].effect == EFFECT_POISON);
RNGSeed(RNG_METRONOME_PSN_POWDER);
PLAYER(SPECIES_WOBBUFFET) {Speed(5);}
OPPONENT(SPECIES_TANGELA) {Speed(2);}
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_TANGELA);
} WHEN {
TURN { MOVE(player, MOVE_METRONOME); }
TURN { MOVE(player, MOVE_METRONOME, WITH_RNG(RNG_METRONOME, MOVE_POISON_POWDER)); }
} SCENE {
MESSAGE("Wobbuffet used Metronome!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_METRONOME, player);
Expand All @@ -53,17 +46,16 @@ SINGLE_BATTLE_TEST("Metronome's called multi-hit move hits multiple times")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_ROCK_BLAST].effect == EFFECT_MULTI_HIT);
RNGSeed(RNG_METRONOME_ROCK_BLAST);
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_METRONOME); }
TURN { MOVE(player, MOVE_METRONOME, WITH_RNG(RNG_METRONOME, MOVE_ROCK_BLAST)); }
} SCENE {
MESSAGE("Wobbuffet used Metronome!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_METRONOME, player);
MESSAGE("Wobbuffet used Rock Blast!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_ROCK_BLAST, player);
HP_BAR(opponent);
MESSAGE("Hit 4 time(s)!");
MESSAGE("Hit 5 time(s)!");
}
}
21 changes: 10 additions & 11 deletions test/move_effect_mirror_move.c
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ ASSUMPTIONS
SINGLE_BATTLE_TEST("Mirror Move copies the last used move by the target")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) {Speed(2);}
OPPONENT(SPECIES_WOBBUFFET) {Speed(5);}
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_TACKLE); MOVE(player, MOVE_MIRROR_MOVE); }
} SCENE {
Expand All @@ -26,10 +26,10 @@ SINGLE_BATTLE_TEST("Mirror Move copies the last used move by the target")
SINGLE_BATTLE_TEST("Mirror Move fails if no move was used before")
{
GIVEN {
PLAYER(SPECIES_WOBBUFFET) {Speed(5);}
OPPONENT(SPECIES_WOBBUFFET) {Speed(2);}
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(opponent, MOVE_TACKLE); MOVE(player, MOVE_MIRROR_MOVE); }
TURN { MOVE(player, MOVE_MIRROR_MOVE); MOVE(opponent, MOVE_TACKLE); }
} SCENE {
MESSAGE("Wobbuffet used Mirror Move!");
MESSAGE("The Mirror Move failed!");
Expand All @@ -44,8 +44,8 @@ SINGLE_BATTLE_TEST("Mirror Move's called powder move fails against Grass Types")
ASSUME(gBattleMoves[MOVE_STUN_SPORE].flags & FLAG_POWDER);
ASSUME(gSpeciesInfo[SPECIES_ODDISH].types[0] == TYPE_GRASS);
ASSUME(gBattleMoves[MOVE_STUN_SPORE].effect == EFFECT_PARALYZE);
PLAYER(SPECIES_ODDISH) {Speed(5);}
OPPONENT(SPECIES_WOBBUFFET) {Speed(2);}
PLAYER(SPECIES_ODDISH);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_STUN_SPORE); MOVE(opponent, MOVE_MIRROR_MOVE); }
} SCENE {
Expand All @@ -59,19 +59,18 @@ SINGLE_BATTLE_TEST("Mirror Move's called powder move fails against Grass Types")
}
}

// It hits first 2 times, then 5 times with the default rng seed.
SINGLE_BATTLE_TEST("Mirror Move's called multi-hit move hits multiple times")
{
GIVEN {
ASSUME(gBattleMoves[MOVE_BULLET_SEED].effect == EFFECT_MULTI_HIT);
PLAYER(SPECIES_WOBBUFFET) {Speed(5);}
OPPONENT(SPECIES_WOBBUFFET) {Speed(2);}
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_WOBBUFFET);
} WHEN {
TURN { MOVE(player, MOVE_BULLET_SEED); MOVE(opponent, MOVE_MIRROR_MOVE); }
} SCENE {
ANIMATION(ANIM_TYPE_MOVE, MOVE_BULLET_SEED, player);
HP_BAR(opponent);
MESSAGE("Hit 2 time(s)!");
MESSAGE("Hit 5 time(s)!");
MESSAGE("Foe Wobbuffet used Mirror Move!");
MESSAGE("Foe Wobbuffet used Bullet Seed!");
ANIMATION(ANIM_TYPE_MOVE, MOVE_BULLET_SEED, opponent);
Expand Down
42 changes: 42 additions & 0 deletions test/random.c
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,25 @@ TEST("RandomUniform generates lo..hi")
}
}

static bool32 InvalidEven(u32 n)
{
return n % 2 == 0;
}

TEST("RandomUniformExcept 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 = RandomUniformExceptDefault(RNG_NONE, lo, hi, InvalidEven);
EXPECT(lo <= r && r <= hi && r % 2 != 0);
}
}

TEST("RandomWeighted generates 0..n-1")
{
u32 n, sum, i;
Expand Down Expand Up @@ -65,6 +84,29 @@ TEST("RandomUniform generates uniform distribution")
EXPECT_LT(error, UQ_4_12(0.025));
}

TEST("RandomUniformExcept generates uniform distribution")
{
u32 i, error;
u16 distribution[4];

memset(distribution, 0, sizeof(distribution));
for (i = 0; i < 4096; i++)
{
u32 r = RandomUniformExceptDefault(RNG_NONE, 0, ARRAY_COUNT(distribution) - 1, InvalidEven);
EXPECT(0 <= r && r < ARRAY_COUNT(distribution));
distribution[r]++;
}

error = 0;
for (i = 0; i < ARRAY_COUNT(distribution); i++)
{
if (i % 2 != 0)
error += abs(UQ_4_12(0.5) - distribution[i]);
}

EXPECT_LT(error, UQ_4_12(0.05));
}

TEST("RandomWeighted generates distribution in proportion to the weights")
{
u32 i, sum, error;
Expand Down
5 changes: 3 additions & 2 deletions test/test_battle.h
Original file line number Diff line number Diff line change
Expand Up @@ -603,8 +603,9 @@ struct BattleTestRunnerState
u8 parameters;
u8 runParameter;
u16 rngTag;
u8 trials;
u8 runTrial;
u16 rngTrialOffset;
u16 trials;
u16 runTrial;
u16 expectedRatio;
u16 observedRatio;
u16 trialRatio;
Expand Down
Loading