"""Tests for the RNG protocol, SeededRng, and ScriptedRng.""" import pytest from pf1e_simulator.rng import RngExhaustedError, ScriptedRng, SeededRng class TestSeededRngRollDie: """Given: a SeededRng with a fixed seed When: roll_die is called Then: returns values in 1..=sides deterministically.""" def test_roll_die_returns_in_range(self) -> None: rng = SeededRng(42) for _ in range(100): result = rng.roll_die(6) assert 1 <= result <= 6 def test_roll_die_d20_in_range(self) -> None: rng = SeededRng(42) for _ in range(100): result = rng.d20() assert 1 <= result <= 20 def test_d20_is_roll_die_20(self) -> None: rng1 = SeededRng(123) rng2 = SeededRng(123) assert rng1.d20() == rng2.roll_die(20) class TestSeededRngChoiceIndex: """Given: a SeededRng When: choice_index(n) is called Then: returns values in 0..n-1.""" def test_choice_index_in_range(self) -> None: rng = SeededRng(42) for _ in range(100): result = rng.choice_index(5) assert 0 <= result <= 4 class TestSeededRngReproducibility: """Given: two SeededRng instances with the same seed When: each produces 1000 rolls Then: the streams are identical.""" def test_identical_d20_streams(self) -> None: rng1 = SeededRng(42) rng2 = SeededRng(42) rolls1 = [rng1.d20() for _ in range(1000)] rolls2 = [rng2.d20() for _ in range(1000)] assert rolls1 == rolls2 def test_identical_choice_index_streams(self) -> None: rng1 = SeededRng(42) rng2 = SeededRng(42) idx1 = [rng1.choice_index(8) for _ in range(1000)] idx2 = [rng2.choice_index(8) for _ in range(1000)] assert idx1 == idx2 def test_different_seeds_produce_different_streams(self) -> None: rng1 = SeededRng(1) rng2 = SeededRng(2) rolls1 = [rng1.d20() for _ in range(100)] rolls2 = [rng2.d20() for _ in range(100)] assert rolls1 != rolls2 class TestScriptedRng: """Given: a ScriptedRng with a queue of ints When: roll_die / choice_index is called Then: returns queued values in order.""" def test_roll_die_returns_queued_values(self) -> None: rng = ScriptedRng([3, 1, 4, 1, 5]) assert rng.roll_die(6) == 3 assert rng.roll_die(6) == 1 assert rng.roll_die(6) == 4 def test_d20_returns_queued_values(self) -> None: rng = ScriptedRng([20, 1, 13]) assert rng.d20() == 20 assert rng.d20() == 1 assert rng.d20() == 13 def test_choice_index_returns_queued_values(self) -> None: rng = ScriptedRng([0, 2, 1]) assert rng.choice_index(5) == 0 assert rng.choice_index(5) == 2 assert rng.choice_index(5) == 1 def test_raises_on_exhaustion_roll_die(self) -> None: rng = ScriptedRng([5]) rng.roll_die(6) with pytest.raises(RngExhaustedError): rng.roll_die(6) def test_raises_on_exhaustion_d20(self) -> None: rng = ScriptedRng([10]) rng.d20() with pytest.raises(RngExhaustedError): rng.d20() def test_raises_on_exhaustion_choice_index(self) -> None: rng = ScriptedRng([0]) rng.choice_index(3) with pytest.raises(RngExhaustedError): rng.choice_index(3) def test_exhaustion_error_carries_message(self) -> None: rng = ScriptedRng([1]) rng.roll_die(6) with pytest.raises(RngExhaustedError, match="exhausted"): rng.roll_die(6)