kipyin/pokemaster

[Feature] Add `random()` method to `PRNG`

Closed this issue · 2 comments

Feature Description and Rationale

Is your feature request related to a problem? Please describe.

Currently, using pokemaster.prng.PRNG to generate a random number requires manual calculations. This is error-prone. For example, in the following code, what I've written generates a random number between [0, 1] inclusive on both ends:

def wild_pokemon_held_item(
prng: PRNG, national_id: int, compound_eyes: bool
) -> Optional[pokedex.db.tables.Item]:
"""Determine the held item of a wild Pokémon."""
pokemon_ = get_pokemon(national_id=national_id)
held_item_chance = prng() / 0xFFFF

Whereas random functions normally generate numbers between [0, 1), a left-close-right-open range.

Describe the solution you'd like

Adding a random() method to pokemaster.prng.PRNG class will make getting random numbers with that PRNG much easier.

We can also add:

  • PRNG().choice(items) randomly selects an item from items;
  • PRNG().shuffle(items) returns the elements in items but in random order;
  • PRNG().randint(a, b) returns a random integer between a and b.

and more functions from the random standard library, if needed.

Usage Example

>>> from pokemaster.prng import PRNG
>>> prng = PRNG()
>>> prng.random()
0.0
>>> prng.random()
0.912078857421875
>>> prng.uniform(0.85, 1.0)
0.87911376953125

Suggested Implementation/Change

In pokemater.prng, add the following methods to PRNG class:

class PRNG:
    ...
    def random(self) -> float:
        """Return a random number from the uniform distribution [0, 1)."""
        return self.next() / 0x10000

    def uniform(
        self, min: Union[int, float] = None, max: Union[int, float] = None
    ) -> float:
        """Return a random number from the uniform distribution [min, max)

        Usage::

            PRNG.uniform() -> a random number between [0, 1)
            PRNG.uniform(n) -> a random number between [0, n)
            PRNG.uniform(m, n) -> a random number between [m, n)

        """
        if min is not None and not isinstance(min, Real):
            raise TypeError(f"'min' must be an int or a float.")
        if max is not None and not isinstance(max, Real):
            raise TypeError(f"'max' must be an int or a float.")

        if min is None and max is None:
            # PRNG.uniform() -> [0, 1)
            return self.random()
        elif (min is not None and max is None) or (
            min is None and max is not None
        ):
            # PRNG.uniform(n) -> [0, n)
            cap = min or max
            return self.random() * cap
        else:
            # PRNG.uniform(m, n) -> [m, n)
            if max <= min:
                raise ValueError("'max' must be strictly greater than 'min'.")
            return self.random() * (max - min) + min

PRNG().uniform() is especially useful for calculating the random part of the damage modifier.

Release 0.2.1 fixes this.