"""Seeded and scripted RNG protocol for deterministic Monte Carlo simulation. Design: every die roll in the engine flows through the `Rng` protocol. `SeededRng` wraps `random.Random(seed)` for reproducible streams. `ScriptedRng` pops from a fixed queue — used in tests to script exact outcomes. Security note: `random.Random` is NOT cryptographically secure. This is by design — the simulator needs reproducibility, not security. """ from __future__ import annotations import random from collections import deque from typing import Protocol _EXHAUSTED_MSG = "ScriptedRng queue exhausted" class RngExhaustedError(Exception): """Raised when a ScriptedRng queue is exhausted.""" class Rng(Protocol): """Protocol for deterministic die-roll sources.""" def roll_die(self, sides: int) -> int: """Return a uniform integer in 1..=sides.""" ... def d20(self) -> int: """Return roll_die(20).""" ... def d100(self) -> int: """Return roll_die(100).""" ... def choice_index(self, n: int) -> int: """Return a uniform integer in 0..n-1 for deterministic tie-breaks.""" ... class SeededRng: """Rng backed by random.Random(seed) for reproducible streams.""" def __init__(self, seed: int) -> None: self._rng = random.Random(seed) def roll_die(self, sides: int) -> int: return self._rng.randint(1, sides) def d20(self) -> int: return self.roll_die(20) def d100(self) -> int: return self.roll_die(100) def choice_index(self, n: int) -> int: return self._rng.randrange(n) class ScriptedRng: """Rng that pops predetermined values from a queue. Each call to roll_die, d20, d100, or choice_index consumes one value. Raises RngExhaustedError when the queue is empty. """ def __init__(self, queue: list[int]) -> None: self._queue: deque[int] = deque(queue) def roll_die(self, _sides: int) -> int: if not self._queue: raise RngExhaustedError(_EXHAUSTED_MSG) return self._queue.popleft() def d20(self) -> int: return self.roll_die(20) def d100(self) -> int: return self.roll_die(100) def choice_index(self, _n: int) -> int: if not self._queue: raise RngExhaustedError(_EXHAUSTED_MSG) return self._queue.popleft()