A 5e storytelling engine with an LLM DM
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at main 176 lines 6.5 kB view raw
1"""Tests for the CultureForge and high-level Generator.""" 2 3from pathlib import Path 4 5import pytest 6 7from storied.names.forge import CultureForge, ForgedCulture 8from storied.names.generator import Generator 9 10 11@pytest.fixture 12def world_dir(tmp_path: Path) -> Path: 13 """A fresh world directory the forge can write to.""" 14 return tmp_path / "test-world" 15 16 17class TestCultureForge: 18 def test_forge_writes_yaml_file(self, world_dir: Path): 19 forge = CultureForge(world_path=world_dir) 20 culture = forge.forge(seed=42) 21 path = world_dir / "cultures" / f"{culture.name}.yaml" 22 assert path.exists() 23 24 def test_forge_returns_self_named_culture(self, world_dir: Path): 25 forge = CultureForge(world_path=world_dir) 26 culture = forge.forge(seed=42) 27 assert culture.name 28 assert culture.name.isalpha() 29 assert culture.name == culture.name.lower() 30 31 def test_forge_self_name_in_length_window(self, world_dir: Path): 32 forge = CultureForge(world_path=world_dir) 33 for seed in range(10): 34 culture = forge.forge(seed=seed * 100) 35 assert 4 <= len(culture.name) <= 9, ( 36 f"Culture name {culture.name!r} out of range" 37 ) 38 39 def test_forge_is_deterministic(self, tmp_path: Path): 40 forge1 = CultureForge(world_path=tmp_path / "a") 41 forge2 = CultureForge(world_path=tmp_path / "b") 42 c1 = forge1.forge(seed=12345) 43 c2 = forge2.forge(seed=12345) 44 assert c1.name == c2.name 45 46 def test_forge_different_seeds_yield_different_cultures( 47 self, 48 world_dir: Path, 49 ): 50 forge = CultureForge(world_path=world_dir) 51 cultures = [forge.forge(seed=s) for s in range(10)] 52 names = {c.name for c in cultures} 53 assert len(names) >= 9 # tolerate one rare collision 54 55 def test_forge_with_feel_picks_matching_inventory(self, world_dir: Path): 56 # Forge several coastal cultures and check they came from 57 # inventories tagged "coastal" in the data file. 58 coastal_inventories = { 59 "welsh", 60 "old-norse", 61 "polynesian", 62 "austronesian", 63 "iberian", 64 "japonic", 65 "arabic-port", 66 "yoruboid", 67 "hellenic", 68 "cornish", 69 } 70 for seed in range(5): 71 forge2 = CultureForge(world_path=world_dir.parent / f"w{seed}") 72 culture = forge2.forge(feel="coastal", seed=seed) 73 assert culture.source_inventory in coastal_inventories 74 assert culture.feel == "coastal" 75 76 def test_forge_collision_handling(self, world_dir: Path): 77 forge = CultureForge(world_path=world_dir) 78 c1 = forge.forge(seed=99) 79 # Forge again with the same seed — must produce a unique name 80 c2 = forge.forge(seed=99) 81 assert c1.name != c2.name 82 83 def test_load_round_trips(self, world_dir: Path): 84 forge = CultureForge(world_path=world_dir) 85 original = forge.forge(seed=42) 86 loaded = forge.load(original.name) 87 assert loaded is not None 88 assert loaded.name == original.name 89 assert loaded.source_inventory == original.source_inventory 90 assert loaded.templates == original.templates 91 assert loaded.forbidden_clusters == original.forbidden_clusters 92 assert loaded.morphology.applies == original.morphology.applies 93 94 def test_load_missing_returns_none(self, world_dir: Path): 95 forge = CultureForge(world_path=world_dir) 96 assert forge.load("nonexistent") is None 97 98 def test_list_names_empty(self, world_dir: Path): 99 forge = CultureForge(world_path=world_dir) 100 assert forge.list_names() == [] 101 102 def test_list_names_after_forge(self, world_dir: Path): 103 forge = CultureForge(world_path=world_dir) 104 names = [forge.forge(seed=s).name for s in range(3)] 105 assert sorted(names) == forge.list_names() 106 107 108class TestGenerator: 109 @pytest.fixture 110 def populated_world(self, tmp_path: Path) -> Path: 111 world = tmp_path / "world" 112 forge = CultureForge(world_path=world) 113 forge.forge(seed=42, feel="coastal") 114 forge.forge(seed=99, feel="highborn") 115 return world 116 117 def test_sample_returns_names(self, populated_world: Path): 118 gen = Generator(world_path=populated_world) 119 cultures = gen.list_cultures() 120 assert cultures 121 names = gen.sample(culture=cultures[0].name, count=5) 122 assert len(names) == 5 123 assert all(isinstance(n, str) for n in names) 124 125 def test_sample_distinct(self, populated_world: Path): 126 gen = Generator(world_path=populated_world) 127 cultures = gen.list_cultures() 128 names = gen.sample(culture=cultures[0].name, count=10) 129 assert len(set(names)) == len(names) 130 131 def test_sample_capitalized(self, populated_world: Path): 132 gen = Generator(world_path=populated_world) 133 cultures = gen.list_cultures() 134 for name in gen.sample(culture=cultures[0].name, count=5): 135 assert name[0].isupper() 136 137 def test_sample_unknown_culture_returns_empty(self, tmp_path: Path): 138 gen = Generator(world_path=tmp_path) 139 assert gen.sample(culture="ghost", count=5) == [] 140 141 def test_place_kind(self, populated_world: Path): 142 gen = Generator(world_path=populated_world) 143 cultures = gen.list_cultures() 144 places = gen.sample( 145 culture=cultures[0].name, 146 count=3, 147 kind="place", 148 ) 149 assert len(places) == 3 150 151 def test_list_cultures_returns_loaded(self, populated_world: Path): 152 gen = Generator(world_path=populated_world) 153 cultures = gen.list_cultures() 154 assert len(cultures) == 2 155 for c in cultures: 156 assert isinstance(c, ForgedCulture) 157 158 def test_rarity_uncommon(self, populated_world: Path): 159 gen = Generator(world_path=populated_world) 160 cultures = gen.list_cultures() 161 names = gen.sample( 162 culture=cultures[0].name, 163 count=3, 164 rarity="uncommon", 165 ) 166 assert len(names) == 3 167 168 def test_rarity_archaic(self, populated_world: Path): 169 gen = Generator(world_path=populated_world) 170 cultures = gen.list_cultures() 171 names = gen.sample( 172 culture=cultures[0].name, 173 count=3, 174 rarity="archaic", 175 ) 176 assert len(names) == 3