"""Ports of the libtcod random number generator.
Usually it's recommend to the Python's standard library `random` module
instead of this one.
However, you will need to use these generators to get deterministic results
from the :any:`Noise` and :any:`BSP` classes.
"""
from __future__ import annotations
import os
import random
import warnings
from typing import Any, Hashable
from typing_extensions import deprecated
import tcod.constants
from tcod.cffi import ffi, lib
MERSENNE_TWISTER = tcod.constants.RNG_MT
COMPLEMENTARY_MULTIPLY_WITH_CARRY = tcod.constants.RNG_CMWC
MULTIPLY_WITH_CARRY = tcod.constants.RNG_CMWC
[docs]
class Random:
"""The libtcod random number generator.
`algorithm` defaults to Mersenne Twister, it can be one of:
* tcod.random.MERSENNE_TWISTER
* tcod.random.MULTIPLY_WITH_CARRY
`seed` is a 32-bit number or any Python hashable object like a string.
Using the same seed will cause the generator to return deterministic
values. The default `seed` of None will generate a random seed instead.
Attributes:
random_c (CData): A cffi pointer to a TCOD_random_t object.
.. warning::
A non-integer seed is only deterministic if the environment variable
``PYTHONHASHSEED`` is set. In the future this function will only
accept `int`'s as a seed.
.. versionchanged:: 9.1
Added `tcod.random.MULTIPLY_WITH_CARRY` constant.
`algorithm` parameter now defaults to `tcod.random.MERSENNE_TWISTER`.
"""
def __init__(
self,
algorithm: int = MERSENNE_TWISTER,
seed: Hashable | None = None,
) -> None:
"""Create a new instance using this algorithm and seed."""
if seed is None:
seed = random.getrandbits(32)
elif not isinstance(seed, int):
warnings.warn(
"In the future this class will only accept integer seeds.",
DeprecationWarning,
stacklevel=2,
)
if __debug__ and "PYTHONHASHSEED" not in os.environ:
warnings.warn(
"Python's hash algorithm is not configured to be"
" deterministic so this non-integer seed will not be"
" deterministic."
"\nYou should do one of the following to fix this error:"
"\n* Use an integer as a seed instead (recommended.)"
"\n* Set the PYTHONHASHSEED environment variable before"
" starting Python.",
RuntimeWarning,
stacklevel=2,
)
seed = hash(seed)
self.random_c = ffi.gc(
lib.TCOD_random_new_from_seed(algorithm, seed & 0xFFFFFFFF),
lib.TCOD_random_delete,
)
@classmethod
def _new_from_cdata(cls, cdata: Any) -> Random: # noqa: ANN401
"""Return a new instance encapsulating this cdata."""
self: Random = object.__new__(cls)
self.random_c = cdata
return self
[docs]
def randint(self, low: int, high: int) -> int:
"""Return a random integer within the linear range: low <= n <= high.
Args:
low (int): The lower bound of the random range.
high (int): The upper bound of the random range.
Returns:
int: A random integer.
"""
return int(lib.TCOD_random_get_i(self.random_c, low, high))
[docs]
def gauss(self, mu: float, sigma: float) -> float:
"""Return a random number using Gaussian distribution.
Args:
mu (float): The median returned value.
sigma (float): The standard deviation.
Returns:
float: A random float.
.. versionchanged:: 16.2
Renamed from `guass` to `gauss`.
"""
return float(lib.TCOD_random_get_gaussian_double(self.random_c, mu, sigma))
@deprecated("This is a typo, rename this to 'gauss'", category=FutureWarning)
def guass(self, mu: float, sigma: float) -> float: # noqa: D102
return self.gauss(mu, sigma)
[docs]
def inverse_gauss(self, mu: float, sigma: float) -> float:
"""Return a random Gaussian number using the Box-Muller transform.
Args:
mu (float): The median returned value.
sigma (float): The standard deviation.
Returns:
float: A random float.
.. versionchanged:: 16.2
Renamed from `inverse_guass` to `inverse_gauss`.
"""
return float(lib.TCOD_random_get_gaussian_double_inv(self.random_c, mu, sigma))
@deprecated("This is a typo, rename this to 'inverse_gauss'", category=FutureWarning)
def inverse_guass(self, mu: float, sigma: float) -> float: # noqa: D102
return self.inverse_gauss(mu, sigma)
[docs]
def __getstate__(self) -> dict[str, Any]:
"""Pack the self.random_c attribute into a portable state."""
state = self.__dict__.copy()
state["random_c"] = {
"mt_cmwc": {
"algorithm": self.random_c.mt_cmwc.algorithm,
"distribution": self.random_c.mt_cmwc.distribution,
"mt": list(self.random_c.mt_cmwc.mt),
"cur_mt": self.random_c.mt_cmwc.cur_mt,
"Q": list(self.random_c.mt_cmwc.Q),
"c": self.random_c.mt_cmwc.c,
"cur": self.random_c.mt_cmwc.cur,
}
}
return state
[docs]
def __setstate__(self, state: dict[str, Any]) -> None:
"""Create a new cdata object with the stored parameters."""
if "algo" in state["random_c"]:
# Handle old/deprecated format. Covert to libtcod's new union type.
state["random_c"]["algorithm"] = state["random_c"]["algo"]
del state["random_c"]["algo"]
state["random_c"] = {"mt_cmwc": state["random_c"]}
state["random_c"] = ffi.new("TCOD_Random*", state["random_c"])
self.__dict__.update(state)