mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2026-03-24 14:06:31 +01:00
130 lines
4.4 KiB
Python
130 lines
4.4 KiB
Python
"""Service wrapper for deterministic seeded RNG operations.
|
|
|
|
Follows the R9 BaseService pattern. Route handlers should prefer this
|
|
over calling random_util functions directly so that validation and error
|
|
handling are centralised.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import random
|
|
from typing import Optional, Union
|
|
|
|
from code.exceptions import InvalidSeedError
|
|
from code.random_util import (
|
|
derive_seed_from_string,
|
|
generate_seed as _generate_seed,
|
|
get_random,
|
|
)
|
|
from code.web.services.base import BaseService
|
|
|
|
SeedLike = Union[int, str]
|
|
|
|
|
|
class RandomService(BaseService):
|
|
"""Service for deterministic random build operations.
|
|
|
|
All methods return independent ``random.Random`` instances so that
|
|
concurrent requests cannot pollute each other's RNG stream.
|
|
"""
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Validation #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def validate_seed(self, seed: Optional[SeedLike]) -> None:
|
|
"""Validate that *seed* is an acceptable type and value.
|
|
|
|
Args:
|
|
seed: The seed to validate. ``None`` is always valid (triggers
|
|
auto-generation at build time).
|
|
|
|
Raises:
|
|
InvalidSeedError: If the seed is the wrong type, negative, or
|
|
an empty string.
|
|
"""
|
|
if seed is None:
|
|
return
|
|
|
|
if isinstance(seed, bool):
|
|
# bool is a subclass of int in Python — reject explicitly
|
|
raise InvalidSeedError(
|
|
"Seed must be an integer or string, not bool",
|
|
details={"seed": str(seed), "type": type(seed).__name__},
|
|
)
|
|
|
|
if isinstance(seed, int):
|
|
if seed < 0:
|
|
raise InvalidSeedError(
|
|
f"Integer seed must be non-negative, got {seed}",
|
|
details={"seed": seed},
|
|
)
|
|
return
|
|
|
|
if isinstance(seed, str):
|
|
if not seed:
|
|
raise InvalidSeedError(
|
|
"String seed cannot be empty",
|
|
details={"seed": seed},
|
|
)
|
|
return
|
|
|
|
raise InvalidSeedError(
|
|
f"Seed must be an int or str, got {type(seed).__name__}",
|
|
details={"seed": str(seed), "type": type(seed).__name__},
|
|
)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Seed derivation #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def derive_seed(self, seed: SeedLike) -> int:
|
|
"""Derive a stable 63-bit positive integer from *seed*.
|
|
|
|
Delegates to :func:`random_util.derive_seed_from_string` after
|
|
validating the input.
|
|
|
|
Args:
|
|
seed: Integer or string seed value.
|
|
|
|
Returns:
|
|
A stable non-negative 63-bit integer.
|
|
|
|
Raises:
|
|
InvalidSeedError: If *seed* fails validation.
|
|
"""
|
|
self.validate_seed(seed)
|
|
try:
|
|
return derive_seed_from_string(seed)
|
|
except Exception as exc:
|
|
raise InvalidSeedError(
|
|
f"Failed to derive seed from {type(seed).__name__}",
|
|
details={"seed": str(seed), "error": str(exc)},
|
|
) from exc
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# RNG creation #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def create_rng(self, seed: Optional[SeedLike] = None) -> random.Random:
|
|
"""Return a ``random.Random`` instance, optionally seeded.
|
|
|
|
Args:
|
|
seed: Integer or string seed. Pass ``None`` for an unseeded
|
|
(non-deterministic) instance.
|
|
|
|
Returns:
|
|
An independent ``random.Random`` instance.
|
|
"""
|
|
return get_random(seed)
|
|
|
|
# ------------------------------------------------------------------ #
|
|
# Seed generation #
|
|
# ------------------------------------------------------------------ #
|
|
|
|
def generate_seed(self) -> int:
|
|
"""Generate a high-entropy 63-bit seed.
|
|
|
|
Returns:
|
|
A fresh positive integer via ``secrets.randbits(63)``.
|
|
"""
|
|
return _generate_seed()
|