from __future__ import annotations import json import random from dataclasses import dataclass from pathlib import Path from typing import Any @dataclass(frozen=True) class WorldTheme: title: str answer: str foyer_label: str foyer_description: str shrine_label: str shrine_description: str workshop_label: str workshop_description: str courtyard_label: str courtyard_description: str gallery_label: str gallery_description: str entry_chest_label: str entry_chest_description: str iron_door_label: str iron_door_description: str ash_mural_label: str ash_mural_description: str ash_mural_text: str iron_chest_label: str iron_chest_description: str stone_well_label: str stone_well_description: str water_plaque_label: str water_plaque_description: str water_plaque_text: str cartographer_label: str cartographer_description: str faded_letter_label: str faded_letter_description: str faded_letter_text: str stone_guardian_label: str stone_guardian_description: str brass_key_label: str brass_key_description: str torch_label: str torch_description: str torn_map_left_label: str torn_map_left_description: str torn_map_right_label: str torn_map_right_description: str full_map_label: str full_map_description: str lens_label: str lens_description: str initial_clue_text: str river_clue_text: str waterwarden_clue_text: str _WORLD_THEMES: tuple[WorldTheme, ...] = ( WorldTheme( title="The River Ward", answer="mira", foyer_label="Foyer", foyer_description="A drafty entry hall with passages north, south, east, and west.", shrine_label="Shrine", shrine_description="An open shrine watched by a silent stone guardian.", workshop_label="Workshop", workshop_description="An ash-streaked workshop lit by a guttering lamp.", courtyard_label="Courtyard", courtyard_description="Rainwater gathers around a cracked stone well.", gallery_label="Gallery", gallery_description="Portraits of the wardens hang above a long dust-covered table.", entry_chest_label="Entry Chest", entry_chest_description="A squat travel chest sits beside the door.", iron_door_label="Iron Door", iron_door_description="A blackened iron door seals the workshop.", ash_mural_label="Ash Mural", ash_mural_description="An ash-dark mural is impossible to make out with the naked eye.", ash_mural_text="The mural preserves one line: the betrayer's name begins with M.", iron_chest_label="Iron Chest", iron_chest_description="A soot-stained iron chest is tucked under a bench.", stone_well_label="Stone Well", stone_well_description="Etchings circle the well's rim, but they only align from the proper vantage.", water_plaque_label="Water Plaque", water_plaque_description="A bronze plaque slides out from the well masonry.", water_plaque_text="The betrayer lived closest to the river gate.", cartographer_label="Cartographer", cartographer_description="The cartographer studies the walls and waits for a completed survey.", faded_letter_label="Faded Letter", faded_letter_description="A faded letter is still too blurred to decipher.", faded_letter_text="Of the wardens, only Mira kept quarters beside the water.", stone_guardian_label="Stone Guardian", stone_guardian_description="The guardian asks for the betrayer's name once you are ready.", brass_key_label="Brass Key", brass_key_description="A brass key with soot in its teeth.", torch_label="Torch", torch_description="A pitch torch with a steady flame.", torn_map_left_label="Torn Map Left", torn_map_left_description="The left half of a survey map.", torn_map_right_label="Torn Map Right", torn_map_right_description="The right half of a survey map.", full_map_label="Full Map", full_map_description="A restored map of the ward.", lens_label="Lens", lens_description="A polished lens in a brass frame.", initial_clue_text="The betrayer's name begins with M.", river_clue_text="The betrayer lived closest to the river gate.", waterwarden_clue_text="Of the wardens, only Mira kept quarters beside the water.", ), WorldTheme( title="The Ember Vault", answer="vesna", foyer_label="Receiving Hall", foyer_description="A warm stone hall lined with soot and copper hooks.", shrine_label="Crucible Shrine", shrine_description="A brass sentinel stands before a furnace-bright altar.", workshop_label="Forge Annex", workshop_description="Bellows creak above benches powdered with black ash.", courtyard_label="Quench Yard", courtyard_description="A cracked basin gathers rain beside the old quench line.", gallery_label="Ledger Hall", gallery_description="Burned account books rest beneath portraits of furnace wardens.", entry_chest_label="Courier Trunk", entry_chest_description="A courier trunk waits under a soot-marked peg rail.", iron_door_label="Furnace Door", iron_door_description="A scorched iron door blocks the annex.", ash_mural_label="Cinder Frieze", ash_mural_description="A smoke-dark frieze only sharpens under moving flame.", ash_mural_text="A surviving line says the betrayer's name begins with V.", iron_chest_label="Coal Locker", iron_chest_description="A riveted locker is wedged beneath a slagged bench.", stone_well_label="Quench Basin", stone_well_description="Marks on the basin align only when seen with the full survey.", water_plaque_label="Cooling Plaque", water_plaque_description="A brass plate rises from a seam in the basin stone.", water_plaque_text="The betrayer worked closest to the quench trench.", cartographer_label="Quartermaster", cartographer_description="The quartermaster trades only for a complete furnace survey.", faded_letter_label="Scorched Ledger", faded_letter_description="Heat has blurred the ink into copper-colored streaks.", faded_letter_text="Only Vesna kept the cooling ledgers beside the trench.", stone_guardian_label="Brass Sentinel", stone_guardian_description="The sentinel requests the betrayer's name when the case is ready.", brass_key_label="Copper Key", brass_key_description="A copper key with furnace grit packed in the cuts.", torch_label="Coal Torch", torch_description="A coal torch that burns with a steady orange core.", torn_map_left_label="Smelter Map Left", torn_map_left_description="The left half of a furnace survey.", torn_map_right_label="Smelter Map Right", torn_map_right_description="The right half of a furnace survey.", full_map_label="Furnace Survey", full_map_description="A restored survey of the ember vault.", lens_label="Gauge Lens", lens_description="A thick gauge lens set in a brass ring.", initial_clue_text="The betrayer's name begins with V.", river_clue_text="The betrayer worked closest to the quench trench.", waterwarden_clue_text="Only Vesna kept the cooling ledgers beside the trench.", ), WorldTheme( title="The Astral Archive", answer="selene", foyer_label="Entry Rotunda", foyer_description="A quiet rotunda opens toward stacked corridors and a dim observatory stair.", shrine_label="Moon Chapel", shrine_description="A silver warden stands beneath a ceiling of cold stars.", workshop_label="Chart Room", workshop_description="Tables of brass instruments glint in powdery moon dust.", courtyard_label="Star Court", courtyard_description="A dry fountain mirrors the constellations in chipped stone.", gallery_label="Catalog Hall", gallery_description="Glass cases hold the names of long-dead archivists.", entry_chest_label="Porter's Case", entry_chest_description="A leather case rests under the chart hooks.", iron_door_label="Star Door", iron_door_description="A ribbed iron door seals the chart room.", ash_mural_label="Night Chart", ash_mural_description="The chart is unreadable until lit from the proper angle.", ash_mural_text="One surviving note says the betrayer's name begins with S.", iron_chest_label="Index Chest", iron_chest_description="A narrow chest sits below a shelf of cracked lenses.", stone_well_label="Dry Fountain", stone_well_description="Its star marks align only when the full survey is restored.", water_plaque_label="Star Plaque", water_plaque_description="A silver plaque slides free from the fountain rim.", water_plaque_text="The betrayer slept nearest the eastern telescope.", cartographer_label="Archivist", cartographer_description="The archivist will trade for a complete celestial survey.", faded_letter_label="Blurred Index", faded_letter_description="The index script is too faint without magnification.", faded_letter_text="Among the archivists, only Selene kept quarters by the east telescope.", stone_guardian_label="Silver Warden", stone_guardian_description="The warden will hear the accusation once you have evidence.", brass_key_label="Star Key", brass_key_description="A slim key engraved with a crescent notch.", torch_label="Lamp Wand", torch_description="A narrow lamp wand with a clean blue flame.", torn_map_left_label="Celestial Map Left", torn_map_left_description="The left half of a star survey.", torn_map_right_label="Celestial Map Right", torn_map_right_description="The right half of a star survey.", full_map_label="Celestial Survey", full_map_description="A restored survey of the astral archive.", lens_label="Astrolabe Lens", lens_description="A polished lens mounted in silver wire.", initial_clue_text="The betrayer's name begins with S.", river_clue_text="The betrayer slept nearest the eastern telescope.", waterwarden_clue_text="Among the archivists, only Selene kept quarters by the east telescope.", ), WorldTheme( title="The Glass Conservatory", answer="liora", foyer_label="Gate House", foyer_description="A humid gate house opens onto vine-choked passages.", shrine_label="Bloom Shrine", shrine_description="A mossy guardian waits among chipped planters.", workshop_label="Potting Room", workshop_description="Clay dust and root knives cover the worktables.", courtyard_label="Glass Court", courtyard_description="A cracked basin sits beneath panes webbed with ivy.", gallery_label="Seed Gallery", gallery_description="Pressed flowers hang beside records of vanished caretakers.", entry_chest_label="Garden Chest", entry_chest_description="A cedar chest is tucked beside the rain cloaks.", iron_door_label="Greenhouse Door", iron_door_description="A warped iron door blocks the potting room.", ash_mural_label="Vine Panel", ash_mural_description="The panel's scratches only read clearly under a steady flame.", ash_mural_text="A scratched line says the betrayer's name begins with L.", iron_chest_label="Tool Locker", iron_chest_description="A damp locker crouches under a potting bench.", stone_well_label="Ivy Basin", stone_well_description="The etched rings align only when the full garden survey is in hand.", water_plaque_label="Root Plaque", water_plaque_description="A greened plaque slides from the basin wall.", water_plaque_text="The betrayer tended the beds nearest the rain cistern.", cartographer_label="Head Gardener", cartographer_description="The gardener will barter only for a complete bed map.", faded_letter_label="Watered Note", faded_letter_description="The note is blurred by old rain and fertilizer.", faded_letter_text="Only Liora kept the cistern ledgers beside the rain beds.", stone_guardian_label="Moss Guardian", stone_guardian_description="The guardian listens when you are ready to name the betrayer.", brass_key_label="Trellis Key", brass_key_description="A greened key shaped like a curling vine.", torch_label="Glass Lantern", torch_description="A glass-sided lantern with a bright white flame.", torn_map_left_label="Bed Map Left", torn_map_left_description="The left half of a conservatory plan.", torn_map_right_label="Bed Map Right", torn_map_right_description="The right half of a conservatory plan.", full_map_label="Bed Survey", full_map_description="A restored survey of the conservatory beds.", lens_label="Prism Lens", lens_description="A prism lens wrapped in tarnished copper.", initial_clue_text="The betrayer's name begins with L.", river_clue_text="The betrayer tended the beds nearest the rain cistern.", waterwarden_clue_text="Only Liora kept the cistern ledgers beside the rain beds.", ), WorldTheme( title="The Salt Bastion", answer="corin", foyer_label="Watch Hall", foyer_description="A salt-stung hall opens toward barracks, chapel, and the sea court.", shrine_label="Tide Chapel", shrine_description="A stone warden keeps watch over a shrine of ropes and shells.", workshop_label="Signal Room", workshop_description="Lantern hooks sway above benches dusted with salt ash.", courtyard_label="Sea Court", courtyard_description="A dry cistern sits beneath walls pitted by ocean wind.", gallery_label="Roll Hall", gallery_description="Roster boards hang beneath portraits of old coast captains.", entry_chest_label="Harbor Chest", entry_chest_description="A travel chest sits beside a rack of oilskins.", iron_door_label="Beacon Door", iron_door_description="A rusted iron door bars the signal room.", ash_mural_label="Signal Board", ash_mural_description="Salt haze hides the markings until a lamp is raised close.", ash_mural_text="A surviving mark says the betrayer's name begins with C.", iron_chest_label="Tar Locker", iron_chest_description="A tar-black locker hides below a signal bench.", stone_well_label="Dry Cistern", stone_well_description="Its carved rings make sense only with the restored coast survey.", water_plaque_label="Harbor Plaque", water_plaque_description="A plaque rises from a crack in the cistern lip.", water_plaque_text="The betrayer bunked nearest the harbor chain.", cartographer_label="Harbor Clerk", cartographer_description="The clerk trades only for a complete bastion survey.", faded_letter_label="Salted Roll", faded_letter_description="Salt has crusted over the roster names.", faded_letter_text="Only Corin kept the harbor ledgers beside the chain gate.", stone_guardian_label="Stone Warden", stone_guardian_description="The warden asks for the betrayer's name when the proof is ready.", brass_key_label="Anchor Key", brass_key_description="A heavy key stamped with a worn anchor.", torch_label="Signal Lamp", torch_description="A shuttered lamp with a disciplined yellow flame.", torn_map_left_label="Coast Map Left", torn_map_left_description="The left half of a bastion survey.", torn_map_right_label="Coast Map Right", torn_map_right_description="The right half of a bastion survey.", full_map_label="Coast Survey", full_map_description="A restored survey of the salt bastion.", lens_label="Captain's Lens", lens_description="A salt-clear lens held in a bronze ring.", initial_clue_text="The betrayer's name begins with C.", river_clue_text="The betrayer bunked nearest the harbor chain.", waterwarden_clue_text="Only Corin kept the harbor ledgers beside the chain gate.", ), ) def sample_world_definition(seed: int | None = None, difficulty_target: float = 1.5) -> dict[str, Any]: theme = _select_theme(seed) return _build_world(theme, difficulty_target=difficulty_target) def load_world(path: str) -> dict[str, Any]: return json.loads(Path(path).read_text(encoding="utf-8")) def _select_theme(seed: int | None) -> WorldTheme: if seed is None: return _WORLD_THEMES[0] rng = random.Random(seed) return _WORLD_THEMES[rng.randrange(len(_WORLD_THEMES))] def _build_world(theme: WorldTheme, *, difficulty_target: float) -> dict[str, Any]: return { "meta": { "title": theme.title, "difficulty_target": difficulty_target, "start_node_id": "foyer", "win_condition": { "type": "deduce", "target_npc_id": "stone_guardian", "answer_string": theme.answer, }, }, "nodes": [ {"id": "foyer", "type": "location", "label": theme.foyer_label, "description": theme.foyer_description}, {"id": "shrine", "type": "location", "label": theme.shrine_label, "description": theme.shrine_description}, {"id": "workshop", "type": "location", "label": theme.workshop_label, "description": theme.workshop_description}, {"id": "courtyard", "type": "location", "label": theme.courtyard_label, "description": theme.courtyard_description}, {"id": "gallery", "type": "location", "label": theme.gallery_label, "description": theme.gallery_description}, { "id": "entry_chest", "type": "container", "label": theme.entry_chest_label, "description": theme.entry_chest_description, "parent_id": "foyer", "open": False, "locked": False, "lock_key_id": None, }, { "id": "iron_door", "type": "door", "label": theme.iron_door_label, "description": theme.iron_door_description, "open": False, "locked": True, "lock_key_id": "brass_key", }, { "id": "ash_mural", "type": "readable", "label": theme.ash_mural_label, "description": theme.ash_mural_description, "parent_id": "workshop", "clue_id": "initial_clue", "requires_item_id": "torch", "consumes_item": False, "text_content": theme.ash_mural_text, }, { "id": "iron_chest", "type": "container", "label": theme.iron_chest_label, "description": theme.iron_chest_description, "parent_id": "workshop", "open": False, "locked": False, "lock_key_id": None, }, { "id": "stone_well", "type": "fixture", "label": theme.stone_well_label, "description": theme.stone_well_description, "parent_id": "courtyard", "requires_item_id": "full_map", "reveals_item_id": None, "reveals_readable_id": "water_plaque", "consumes_item": False, }, { "id": "water_plaque", "type": "readable", "label": theme.water_plaque_label, "description": theme.water_plaque_description, "parent_id": "courtyard", "clue_id": "river_clue", "requires_item_id": None, "consumes_item": False, "text_content": theme.water_plaque_text, }, { "id": "cartographer", "type": "npc", "label": theme.cartographer_label, "description": theme.cartographer_description, "parent_id": "gallery", "requires_item_id": "full_map", "gives_item_id": "lens", "gives_clue_id": None, }, { "id": "faded_letter", "type": "readable", "label": theme.faded_letter_label, "description": theme.faded_letter_description, "parent_id": "gallery", "clue_id": "waterwarden_clue", "requires_item_id": "lens", "consumes_item": False, "text_content": theme.faded_letter_text, }, { "id": "stone_guardian", "type": "npc", "label": theme.stone_guardian_label, "description": theme.stone_guardian_description, "parent_id": "shrine", "requires_item_id": None, "gives_item_id": None, "gives_clue_id": None, }, ], "edges": [ {"id": "foyer_north", "from_node_id": "foyer", "to_node_id": "shrine", "direction": "north", "type": "passage", "required_item_id": None, "door_node_id": None}, {"id": "shrine_south", "from_node_id": "shrine", "to_node_id": "foyer", "direction": "south", "type": "passage", "required_item_id": None, "door_node_id": None}, {"id": "foyer_east", "from_node_id": "foyer", "to_node_id": "workshop", "direction": "east", "type": "locked_passage", "required_item_id": "brass_key", "door_node_id": "iron_door"}, {"id": "workshop_west", "from_node_id": "workshop", "to_node_id": "foyer", "direction": "west", "type": "locked_passage", "required_item_id": "brass_key", "door_node_id": "iron_door"}, {"id": "foyer_west", "from_node_id": "foyer", "to_node_id": "courtyard", "direction": "west", "type": "passage", "required_item_id": None, "door_node_id": None}, {"id": "courtyard_east", "from_node_id": "courtyard", "to_node_id": "foyer", "direction": "east", "type": "passage", "required_item_id": None, "door_node_id": None}, {"id": "foyer_south", "from_node_id": "foyer", "to_node_id": "gallery", "direction": "south", "type": "passage", "required_item_id": None, "door_node_id": None}, {"id": "gallery_north", "from_node_id": "gallery", "to_node_id": "foyer", "direction": "north", "type": "passage", "required_item_id": None, "door_node_id": None}, ], "items": [ {"id": "brass_key", "label": theme.brass_key_label, "description": theme.brass_key_description, "subtype": "key", "start_node_id": "entry_chest"}, {"id": "torch", "label": theme.torch_label, "description": theme.torch_description, "subtype": "puzzle", "start_node_id": "workshop"}, {"id": "torn_map_left", "label": theme.torn_map_left_label, "description": theme.torn_map_left_description, "subtype": "puzzle", "start_node_id": "iron_chest"}, {"id": "torn_map_right", "label": theme.torn_map_right_label, "description": theme.torn_map_right_description, "subtype": "puzzle", "start_node_id": "courtyard"}, {"id": "full_map", "label": theme.full_map_label, "description": theme.full_map_description, "subtype": "puzzle", "start_node_id": None}, {"id": "lens", "label": theme.lens_label, "description": theme.lens_description, "subtype": "puzzle", "start_node_id": None}, ], "clues": [ {"id": "initial_clue", "text": theme.initial_clue_text}, {"id": "river_clue", "text": theme.river_clue_text}, {"id": "waterwarden_clue", "text": theme.waterwarden_clue_text}, ], "recipes": [ { "id": "restore_map", "input_item_ids": ["torn_map_left", "torn_map_right"], "output_item_id": "full_map", } ], "quest_chain": [ {"step_id": "open_entry_chest", "description": f"Open the {theme.entry_chest_label.lower()}.", "requires_step_ids": [], "action": "open(entry_chest)"}, {"step_id": "take_brass_key", "description": f"Take the {theme.brass_key_label.lower()}.", "requires_step_ids": ["open_entry_chest"], "action": "take(brass_key,entry_chest)"}, {"step_id": "unlock_workshop", "description": f"Unlock the {theme.iron_door_label.lower()}.", "requires_step_ids": ["take_brass_key"], "action": "unlock(iron_door,brass_key)"}, {"step_id": "open_workshop", "description": f"Open the {theme.iron_door_label.lower()}.", "requires_step_ids": ["unlock_workshop"], "action": "open(iron_door)"}, {"step_id": "go_workshop", "description": f"Enter the {theme.workshop_label.lower()}.", "requires_step_ids": ["open_workshop"], "action": "go(workshop)"}, {"step_id": "take_torch", "description": f"Take the {theme.torch_label.lower()}.", "requires_step_ids": ["go_workshop"], "action": "take(torch,workshop)"}, {"step_id": "use_torch_on_mural", "description": f"Use the {theme.torch_label.lower()} on the {theme.ash_mural_label.lower()}.", "requires_step_ids": ["take_torch"], "action": "use(torch,ash_mural)"}, {"step_id": "open_iron_chest", "description": f"Open the {theme.iron_chest_label.lower()}.", "requires_step_ids": ["go_workshop"], "action": "open(iron_chest)"}, {"step_id": "take_left_map", "description": f"Take the {theme.torn_map_left_label.lower()}.", "requires_step_ids": ["open_iron_chest"], "action": "take(torn_map_left,iron_chest)"}, {"step_id": "return_foyer", "description": f"Return to the {theme.foyer_label.lower()}.", "requires_step_ids": ["take_left_map"], "action": "go(foyer)"}, {"step_id": "go_courtyard", "description": f"Head to the {theme.courtyard_label.lower()}.", "requires_step_ids": ["return_foyer"], "action": "go(courtyard)"}, {"step_id": "take_right_map", "description": f"Take the {theme.torn_map_right_label.lower()}.", "requires_step_ids": ["go_courtyard"], "action": "take(torn_map_right,courtyard)"}, {"step_id": "combine_map", "description": f"Restore the {theme.full_map_label.lower()}.", "requires_step_ids": ["take_right_map"], "action": "combine(torn_map_left,torn_map_right)"}, {"step_id": "use_map_on_well", "description": f"Use the {theme.full_map_label.lower()} on the {theme.stone_well_label.lower()}.", "requires_step_ids": ["combine_map"], "action": "use(full_map,stone_well)"}, {"step_id": "read_plaque", "description": f"Read the {theme.water_plaque_label.lower()}.", "requires_step_ids": ["use_map_on_well"], "action": "read(water_plaque)"}, {"step_id": "go_foyer_again", "description": f"Go back to the {theme.foyer_label.lower()}.", "requires_step_ids": ["read_plaque"], "action": "go(foyer)"}, {"step_id": "go_gallery", "description": f"Head to the {theme.gallery_label.lower()}.", "requires_step_ids": ["go_foyer_again"], "action": "go(gallery)"}, {"step_id": "give_map", "description": f"Give the map to the {theme.cartographer_label.lower()}.", "requires_step_ids": ["go_gallery"], "action": "give(full_map,cartographer)"}, {"step_id": "use_lens_on_letter", "description": f"Use the {theme.lens_label.lower()} on the {theme.faded_letter_label.lower()}.", "requires_step_ids": ["give_map"], "action": "use(lens,faded_letter)"}, {"step_id": "return_foyer_final", "description": f"Return to the {theme.foyer_label.lower()} again.", "requires_step_ids": ["use_lens_on_letter"], "action": "go(foyer)"}, {"step_id": "go_shrine", "description": f"Go to the {theme.shrine_label.lower()}.", "requires_step_ids": ["return_foyer_final"], "action": "go(shrine)"}, {"step_id": "talk_guardian", "description": f"Speak to the {theme.stone_guardian_label.lower()}.", "requires_step_ids": ["go_shrine"], "action": "talk(stone_guardian)"}, {"step_id": "submit_answer", "description": "Submit the betrayer's name.", "requires_step_ids": ["talk_guardian"], "action": f'submit("{theme.answer}")'}, ], }