mirror of
https://github.com/mwisnowski/mtg_python_deckbuilder.git
synced 2025-12-17 08:00:13 +01:00
feat(themes): whitelist governance, synergy cap, docs + tests; feat(random): laid roadwork for random implementation, testing in headless confirmed
This commit is contained in:
parent
03e839fb87
commit
16261bbf09
34 changed files with 12594 additions and 23 deletions
69
code/random_util.py
Normal file
69
code/random_util.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import secrets
|
||||
import random
|
||||
from typing import Union
|
||||
|
||||
"""
|
||||
Seeded RNG utilities for deterministic behavior.
|
||||
|
||||
Contract (minimal):
|
||||
- derive_seed_from_string(s): produce a stable, platform-independent int seed from a string or int.
|
||||
- set_seed(seed): return a new random.Random instance seeded deterministically.
|
||||
- generate_seed(): return a high-entropy, non-negative int suitable for seeding.
|
||||
- get_random(seed=None): convenience to obtain a new Random instance (seeded when provided).
|
||||
|
||||
No globals/state: each call returns an independent Random instance.
|
||||
"""
|
||||
|
||||
|
||||
SeedLike = Union[int, str]
|
||||
|
||||
|
||||
def _to_bytes(s: str) -> bytes:
|
||||
try:
|
||||
return s.encode("utf-8", errors="strict")
|
||||
except Exception:
|
||||
# Best-effort fallback
|
||||
return s.encode("utf-8", errors="ignore")
|
||||
|
||||
|
||||
def derive_seed_from_string(seed: SeedLike) -> int:
|
||||
"""Derive a stable positive integer seed from a string or int.
|
||||
|
||||
- int inputs are normalized to a non-negative 63-bit value.
|
||||
- str inputs use SHA-256 to generate a deterministic 63-bit value.
|
||||
"""
|
||||
if isinstance(seed, int):
|
||||
# Normalize to 63-bit positive
|
||||
return abs(int(seed)) & ((1 << 63) - 1)
|
||||
# String path: deterministic, platform-independent
|
||||
data = _to_bytes(str(seed))
|
||||
h = hashlib.sha256(data).digest()
|
||||
# Use first 8 bytes (64 bits) and mask to 63 bits to avoid sign issues
|
||||
n = int.from_bytes(h[:8], byteorder="big", signed=False)
|
||||
return n & ((1 << 63) - 1)
|
||||
|
||||
|
||||
def set_seed(seed: SeedLike) -> random.Random:
|
||||
"""Return a new Random instance seeded deterministically from the given seed."""
|
||||
r = random.Random()
|
||||
r.seed(derive_seed_from_string(seed))
|
||||
return r
|
||||
|
||||
|
||||
def get_random(seed: SeedLike | None = None) -> random.Random:
|
||||
"""Return a new Random instance; seed when provided.
|
||||
|
||||
This avoids mutating the module-global PRNG and keeps streams isolated.
|
||||
"""
|
||||
if seed is None:
|
||||
return random.Random()
|
||||
return set_seed(seed)
|
||||
|
||||
|
||||
def generate_seed() -> int:
|
||||
"""Return a high-entropy positive 63-bit integer suitable for seeding."""
|
||||
# secrets is preferred for entropy here; mask to 63 bits for consistency
|
||||
return secrets.randbits(63)
|
||||
Loading…
Add table
Add a link
Reference in a new issue