feat(dice): seeded RNG protocol and dice notation parser
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
"""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)
|
||||
Reference in New Issue
Block a user