Spaces:
Paused
Paused
| """Load & save world bundle artifacts (Section 6). | |
| Artifacts are markdown files with optional YAML frontmatter: | |
| --- | |
| <yaml machine fields> | |
| --- | |
| <prose body> | |
| This module parses frontmatter, builds the typed models in ``models.py``, and | |
| writes artifacts back in the same shape (used by the generator). | |
| """ | |
| from __future__ import annotations | |
| from dataclasses import dataclass | |
| from pathlib import Path | |
| from typing import Any | |
| import yaml | |
| from .models import ( | |
| Alibi, | |
| CharacterCard, | |
| ClueNode, | |
| EnvObject, | |
| KnowledgeBoundary, | |
| Solution, | |
| Timeline, | |
| TimelineSlice, | |
| WorldMeta, | |
| ) | |
| _FM_DELIM = "---" | |
| def parse_frontmatter(text: str) -> tuple[dict[str, Any], str]: | |
| """Split a markdown doc into (frontmatter dict, body).""" | |
| stripped = text.lstrip() | |
| if not stripped.startswith(_FM_DELIM): | |
| return {}, text | |
| # Find the closing delimiter after the opening one. | |
| lines = stripped.splitlines() | |
| # lines[0] == '---' | |
| end = None | |
| for i in range(1, len(lines)): | |
| if lines[i].strip() == _FM_DELIM: | |
| end = i | |
| break | |
| if end is None: | |
| return {}, text | |
| fm_text = "\n".join(lines[1:end]) | |
| body = "\n".join(lines[end + 1 :]).lstrip("\n") | |
| data = yaml.safe_load(fm_text) or {} | |
| if not isinstance(data, dict): | |
| data = {} | |
| return data, body | |
| def write_frontmatter(frontmatter: dict[str, Any], body: str) -> str: | |
| """Serialize a (frontmatter, body) pair into a markdown doc.""" | |
| fm = yaml.safe_dump(frontmatter, sort_keys=False, allow_unicode=True).rstrip() | |
| return f"{_FM_DELIM}\n{fm}\n{_FM_DELIM}\n\n{body.strip()}\n" | |
| class World: | |
| """A loaded world bundle (engine-side view; holds ground truth).""" | |
| meta: WorldMeta | |
| world_md: str | |
| solution: Solution | |
| timeline: Timeline | |
| environment: list[EnvObject] | |
| clues: list[ClueNode] | |
| characters: dict[str, CharacterCard] | |
| alibis: dict[str, Alibi] | |
| path: Path | |
| def character(self, name: str) -> CharacterCard | None: | |
| # Case-insensitive lookup. An exact full-name match always wins; | |
| # otherwise accept any single name word ("clara", "pollard"). A query | |
| # that matches more than one person (e.g. "vale") is ambiguous and | |
| # resolves to nothing — use the full name to disambiguate. | |
| query = name.strip().lower() | |
| for cname, card in self.characters.items(): | |
| if cname.lower() == query: | |
| return card | |
| partial = self.candidates(name) | |
| return partial[0] if len(partial) == 1 else None | |
| def candidates(self, name: str) -> list[CharacterCard]: | |
| """Characters whose name contains the query as a whole word. Used to | |
| report ambiguity (e.g. "vale" → Edmund Vale, Tessa Vale).""" | |
| query = name.strip().lower() | |
| return [ | |
| card | |
| for cname, card in self.characters.items() | |
| if query in {w.strip(".,").lower() for w in cname.split()} | |
| ] | |
| def clue(self, clue_id: str) -> ClueNode | None: | |
| for c in self.clues: | |
| if c.id == clue_id: | |
| return c | |
| return None | |
| def env_object(self, obj_id: str) -> EnvObject | None: | |
| for o in self.environment: | |
| if o.id == obj_id: | |
| return o | |
| return None | |
| def suspects(self) -> list[CharacterCard]: | |
| return [c for c in self.characters.values() if c.role != "victim"] | |
| # -------------------------------------------------------------------------- | |
| # Loading | |
| # -------------------------------------------------------------------------- | |
| def _read(path: Path) -> str: | |
| return path.read_text(encoding="utf-8") if path.exists() else "" | |
| def load_world(world_dir: Path) -> World: | |
| world_id = world_dir.name | |
| world_md_path = world_dir / "world.md" | |
| world_fm, world_body = parse_frontmatter(_read(world_md_path)) | |
| meta = WorldMeta(world_id=world_id, **{ | |
| k: world_fm[k] | |
| for k in ("created", "seed", "one_line", "setting", "twist_tag", | |
| "play_count", "title") | |
| if k in world_fm | |
| }) | |
| sol_fm, sol_body = parse_frontmatter(_read(world_dir / "solution.md")) | |
| solution = Solution( | |
| culprit=sol_fm.get("culprit", ""), | |
| means=sol_fm.get("means", ""), | |
| motive=sol_fm.get("motive", ""), | |
| opportunity=sol_fm.get("opportunity", ""), | |
| true_sequence=sol_fm.get("true_sequence", ""), | |
| body=sol_body, | |
| ) | |
| tl_fm, _ = parse_frontmatter(_read(world_dir / "timeline.md")) | |
| timeline = Timeline( | |
| slices=[TimelineSlice(**s) for s in tl_fm.get("slices", [])] | |
| ) | |
| env_fm, _ = parse_frontmatter(_read(world_dir / "environment.md")) | |
| environment = [EnvObject(**o) for o in env_fm.get("objects", [])] | |
| clue_fm, _ = parse_frontmatter(_read(world_dir / "clue_graph.md")) | |
| clues = [ClueNode(**c) for c in clue_fm.get("nodes", [])] | |
| characters: dict[str, CharacterCard] = {} | |
| char_dir = world_dir / "characters" | |
| if char_dir.exists(): | |
| for cpath in sorted(char_dir.glob("*.md")): | |
| fm, body = parse_frontmatter(_read(cpath)) | |
| knows = KnowledgeBoundary(**(fm.get("knows") or {})) | |
| card = CharacterCard( | |
| name=fm.get("name", cpath.stem), | |
| role=fm.get("role", "suspect"), | |
| guilty=fm.get("guilty", False), | |
| truth=fm.get("truth", ""), | |
| knows=knows, | |
| cover=fm.get("cover", ""), | |
| never_admit=fm.get("never_admit", []), | |
| cracks_when=fm.get("cracks_when", ""), | |
| crack_behavior=fm.get("crack_behavior", "deflect"), | |
| tells=fm.get("tells", []), | |
| secret_kind=fm.get("secret_kind", "guilty"), | |
| exoneration=fm.get("exoneration"), | |
| voice=body, | |
| ) | |
| characters[card.name] = card | |
| alibis: dict[str, Alibi] = {} | |
| alibi_dir = world_dir / "alibis" | |
| if alibi_dir.exists(): | |
| for apath in sorted(alibi_dir.glob("*.md")): | |
| fm, body = parse_frontmatter(_read(apath)) | |
| alibi = Alibi( | |
| id=fm.get("id", apath.stem), | |
| characters=fm.get("characters", []), | |
| agreed_facts=fm.get("agreed_facts", ""), | |
| agreed_timeline=fm.get("agreed_timeline", ""), | |
| cover_per_character=fm.get("cover_per_character", {}), | |
| seams=fm.get("seams", []), | |
| body=body, | |
| ) | |
| alibis[alibi.id] = alibi | |
| return World( | |
| meta=meta, | |
| world_md=world_body, | |
| solution=solution, | |
| timeline=timeline, | |
| environment=environment, | |
| clues=clues, | |
| characters=characters, | |
| alibis=alibis, | |
| path=world_dir, | |
| ) | |