Spaces:
Running on Zero
Running on Zero
| """ | |
| Narrative Engine - Python Port | |
| A comprehensive narrative generation system integrating 7 storytelling paradigms: | |
| 1. Struggle Propagation - Character wants/struggles with butterfly effects | |
| 2. Mystery Iceberg - Layered interpretations with hidden depths | |
| 3. Reveal Ripple Observer - Information timing and audience knowledge | |
| 4. Compressed Narrative - Song-like emotional density | |
| 5. Emotional Mechanics - HOW to evoke emotions mechanically | |
| 6. State Evocation - Stories without struggle (presence-based) | |
| 7. Discussion Mechanics - What makes content discussable | |
| """ | |
| from dataclasses import dataclass, field | |
| from enum import Enum | |
| from typing import Dict, List, Optional, Set, Any | |
| import random | |
| import uuid | |
| from datetime import datetime | |
| # === ENUMS === | |
| class NarrativeMode(Enum): | |
| STRUGGLE = "struggle" | |
| COMPRESSED = "compressed" | |
| MEDITATIVE = "meditative" | |
| HYBRID = "hybrid" | |
| class DepthLevel(Enum): | |
| OBVIOUS = 1 | |
| NOTICEABLE = 2 | |
| HIDDEN = 3 | |
| BURIED = 4 | |
| THEORETICAL = 5 | |
| class TruthLayer(Enum): | |
| SURFACE = "surface" | |
| SHALLOW = "shallow" | |
| MID = "mid" | |
| DEEP = "deep" | |
| ABYSS = "abyss" | |
| class RevealMethod(Enum): | |
| DIALOGUE = "dialogue" | |
| VISUAL = "visual" | |
| DISCOVERY = "discovery" | |
| DEDUCTION = "deduction" | |
| IMPLICATION = "implication" | |
| CONFESSION = "confession" | |
| class NodeType(Enum): | |
| RESOURCE = "resource" | |
| INFORMATION = "information" | |
| PERSON = "person" | |
| LOCATION = "location" | |
| STATUS = "status" | |
| TIME_WINDOW = "time_window" | |
| POWER = "power" | |
| class EmotionCategory(Enum): | |
| PRIMAL = "primal" | |
| SOCIAL = "social" | |
| COMPLEX = "complex" | |
| TRANSCENDENT = "transcendent" | |
| # === DATA CLASSES === | |
| class Character: | |
| id: str | |
| name: str | |
| role: str = "" | |
| background: str = "" | |
| wants: Dict[str, dict] = field(default_factory=dict) | |
| knowledge: Set[str] = field(default_factory=set) | |
| secrets: List[str] = field(default_factory=list) | |
| struggle_score: float = 0.5 | |
| def add_want(self, want_id: str, description: str, target_nodes: List[str] = None): | |
| self.wants[want_id] = { | |
| "description": description, | |
| "target_nodes": target_nodes or [], | |
| "progress": 0.0 | |
| } | |
| class WorldNode: | |
| id: str | |
| node_type: NodeType | |
| name: str | |
| capacity: int = 1 | |
| description: str = "" | |
| metadata: Dict[str, Any] = field(default_factory=dict) | |
| connections: List[str] = field(default_factory=list) | |
| current_holders: List[str] = field(default_factory=list) | |
| class Interpretation: | |
| id: str | |
| reading: str | |
| description: str | |
| plausibility: float = 0.5 | |
| darkness_level: float = 0.5 | |
| depth: DepthLevel = DepthLevel.NOTICEABLE | |
| evidence_ids: List[str] = field(default_factory=list) | |
| class Evidence: | |
| id: str | |
| evidence_type: str | |
| content: str | |
| description: str = "" | |
| depth: DepthLevel = DepthLevel.NOTICEABLE | |
| supports: List[str] = field(default_factory=list) | |
| contradicts: List[str] = field(default_factory=list) | |
| is_red_herring: bool = False | |
| class HiddenConnection: | |
| id: str | |
| entity1: str | |
| entity2: str | |
| connection_type: str | |
| description: str | |
| revealed: bool = False | |
| depth: DepthLevel = DepthLevel.BURIED | |
| clue_ids: List[str] = field(default_factory=list) | |
| class UnresolvedThread: | |
| id: str | |
| element: str | |
| description: str | |
| possible_meanings: List[str] = field(default_factory=list) | |
| speculation_hooks: List[str] = field(default_factory=list) | |
| will_resolve: bool = False | |
| class Revelation: | |
| id: str | |
| content: str | |
| trigger_condition: str = "" | |
| emotional_impact: str = "medium" | |
| reframes_events: List[str] = field(default_factory=list) | |
| new_questions: List[str] = field(default_factory=list) | |
| triggered: bool = False | |
| class LayeredEvent: | |
| id: str | |
| surface_description: str | |
| character_ids: List[str] = field(default_factory=list) | |
| interpretations: List[Interpretation] = field(default_factory=list) | |
| evidence: List[Evidence] = field(default_factory=list) | |
| hidden_connections: List[HiddenConnection] = field(default_factory=list) | |
| unresolved_threads: List[UnresolvedThread] = field(default_factory=list) | |
| revelations: List[Revelation] = field(default_factory=list) | |
| timestamp: datetime = field(default_factory=datetime.now) | |
| class StoryClue: | |
| id: str | |
| content: str | |
| truth_id: str | |
| noticeability: float = 0.5 | |
| planted_at: float = 0.0 | |
| clue_type: str = "visual" | |
| class StorySetup: | |
| title: str | |
| place: Dict[str, Any] | |
| time: Dict[str, Any] | |
| protagonist: Dict[str, Any] | |
| hook: str | |
| stakes: str | |
| # === MOTIVATION TEMPLATES === | |
| MOTIVATION_TEMPLATES = { | |
| "self_interest": { | |
| "reading": "self_interest", | |
| "description": "Character acted for personal gain", | |
| "evidence_types": ["behavioral", "historical"], | |
| "darkness_level": 0.4 | |
| }, | |
| "protection": { | |
| "reading": "protecting_someone", | |
| "description": "Character acted to protect someone they care about", | |
| "evidence_types": ["visual", "dialogue"], | |
| "darkness_level": 0.3 | |
| }, | |
| "coercion": { | |
| "reading": "being_coerced", | |
| "description": "Character was forced by external pressure", | |
| "evidence_types": ["behavioral", "environmental"], | |
| "darkness_level": 0.6 | |
| }, | |
| "greater_good": { | |
| "reading": "greater_good", | |
| "description": "Character believes this serves a larger purpose", | |
| "evidence_types": ["dialogue", "historical"], | |
| "darkness_level": 0.5 | |
| }, | |
| "revenge": { | |
| "reading": "revenge", | |
| "description": "Character is settling an old score", | |
| "evidence_types": ["historical", "behavioral"], | |
| "darkness_level": 0.7 | |
| }, | |
| "love": { | |
| "reading": "love", | |
| "description": "Character acted out of love", | |
| "evidence_types": ["visual", "dialogue"], | |
| "darkness_level": 0.2 | |
| }, | |
| "madness": { | |
| "reading": "psychological_break", | |
| "description": "Character is not acting rationally", | |
| "evidence_types": ["behavioral", "visual"], | |
| "darkness_level": 0.8 | |
| }, | |
| "manipulation": { | |
| "reading": "being_manipulated", | |
| "description": "Character is a pawn in someone else's game", | |
| "evidence_types": ["environmental", "absence"], | |
| "darkness_level": 0.6 | |
| } | |
| } | |
| # === EVIDENCE GENERATORS === | |
| EVIDENCE_TEMPLATES = { | |
| "dialogue": { | |
| "self_interest": [ | |
| '"I\'ve worked too hard to lose this now."', | |
| '"Everyone looks out for themselves in the end."', | |
| 'Character mentions personal stakes before others\'' | |
| ], | |
| "protecting_someone": [ | |
| '"I had no choice." (said with glance toward someone)', | |
| 'Mentions family/loved one unprompted', | |
| '"You don\'t understand what\'s at stake."' | |
| ], | |
| "being_coerced": [ | |
| '"They didn\'t give me a choice."', | |
| 'Avoids eye contact when explaining', | |
| 'Uses passive voice: "It had to be done."' | |
| ], | |
| "greater_good": [ | |
| '"Sometimes we have to sacrifice..."', | |
| 'References historical precedent', | |
| '"Future generations will understand."' | |
| ], | |
| "revenge": [ | |
| 'Cold tone when mentioning target', | |
| '"This has been a long time coming."', | |
| 'Knows too many details about target\'s vulnerabilities' | |
| ], | |
| "love": [ | |
| 'Voice softens when certain person mentioned', | |
| 'Keeps token/photo hidden', | |
| 'Acts against self-interest for someone' | |
| ] | |
| }, | |
| "visual": { | |
| "self_interest": [ | |
| 'Expensive items visible in background', | |
| 'Character checks reflection/appearance', | |
| 'Eyes dart to valuable objects' | |
| ], | |
| "protecting_someone": [ | |
| 'Photo partially visible in wallet/desk', | |
| 'Character positions themselves between threat and someone', | |
| 'Worn item suggesting long attachment' | |
| ], | |
| "being_coerced": [ | |
| 'Bruises/marks partially covered', | |
| 'Constant checking of phone/door', | |
| 'Involuntary flinch at certain words' | |
| ], | |
| "love": [ | |
| 'Lingering gaze when person not looking', | |
| 'Unconsciously mirrors person\'s posture', | |
| 'Touches gift/token from person' | |
| ] | |
| }, | |
| "behavioral": { | |
| "self_interest": [ | |
| 'Always negotiates for better terms', | |
| 'Keeps records of favors owed', | |
| 'Exits conversations that don\'t benefit them' | |
| ], | |
| "being_coerced": [ | |
| 'Behavior changed suddenly at specific point', | |
| 'Avoids certain locations/people', | |
| 'Startles easily, hypervigilant' | |
| ], | |
| "psychological_break": [ | |
| 'Sleep patterns disrupted', | |
| 'Laughs at inappropriate moments', | |
| 'Repeats phrases/behaviors compulsively' | |
| ], | |
| "revenge": [ | |
| 'Has been researching target for months', | |
| 'Keeps detailed notes/clippings', | |
| 'Refuses all attempts at reconciliation' | |
| ] | |
| }, | |
| "absence": { | |
| "default": [ | |
| 'Never mentions certain parts of their past', | |
| 'No photos from a particular time period', | |
| 'Conspicuously avoids certain topics', | |
| 'Gap in their history unexplained', | |
| 'Others speak of someone they never acknowledge' | |
| ] | |
| } | |
| } | |
| RED_HERRINGS = [ | |
| 'A faded photograph lies face-down on the shelf, its subject impossible to see', | |
| 'The clock on the wall stopped at exactly 3:47 AM', | |
| 'Three cigarette butts in the ashtray, but no one here smokes', | |
| 'A single white feather rests on the windowsill', | |
| 'The mirror reflects the room slightly wrong', | |
| 'A name has been scratched out of the guest book', | |
| 'The drawer won\'t quite close; something is wedged behind it', | |
| 'A circle of salt under the rug, carefully hidden', | |
| 'The dog refuses to enter this room', | |
| 'Someone has underlined the same word three times in red' | |
| ] | |
| UNRESOLVED_THREAD_TEMPLATES = [ | |
| { | |
| "element": "Mysterious observer in background", | |
| "meanings": ["Guardian protecting protagonist", "Antagonist surveilling", | |
| "Future self watching", "Random bystander"], | |
| "hooks": ["Who are they?", "How long have they been watching?", "What do they know?"] | |
| }, | |
| { | |
| "element": "Object/detail that doesn't fit", | |
| "meanings": ["Clue to larger conspiracy", "Character's hidden past", | |
| "Foreshadowing future event", "Red herring"], | |
| "hooks": ["Where did it come from?", "Who put it there?", "What does it mean?"] | |
| }, | |
| { | |
| "element": "Unexplained character absence", | |
| "meanings": ["They knew what would happen", "They caused it", | |
| "They're dead", "They never existed"], | |
| "hooks": ["Where were they?", "Did they know?", "Are they involved?"] | |
| }, | |
| { | |
| "element": "Contradictory information", | |
| "meanings": ["Someone is lying", "Multiple timelines", | |
| "Unreliable narrator", "Reality is fractured"], | |
| "hooks": ["Which version is true?", "Who benefits from the lie?", "Is reality fractured?"] | |
| }, | |
| { | |
| "element": "Cryptic final words/gesture", | |
| "meanings": ["Warning about future danger", "Confession", | |
| "Code for accomplice", "Meaningless"], | |
| "hooks": ["What did they mean?", "Who was it for?", "Did anyone else notice?"] | |
| } | |
| ] | |
| # === MYSTERY ICEBERG GENERATOR === | |
| class MysteryIcebergGenerator: | |
| """Generates layered mystery content for narrative events.""" | |
| def __init__(self): | |
| self.events: Dict[str, LayeredEvent] = {} | |
| self.evidence: Dict[str, Evidence] = {} | |
| self.connections: Dict[str, HiddenConnection] = {} | |
| self.threads: Dict[str, UnresolvedThread] = {} | |
| self.revelations: Dict[str, Revelation] = {} | |
| def _generate_id(self, prefix: str) -> str: | |
| return f"{prefix}_{uuid.uuid4().hex[:8]}" | |
| def add_mystery_layers( | |
| self, | |
| surface_event: str, | |
| character_ids: List[str] = None, | |
| force_interpretations: List[str] = None, | |
| num_interpretations: int = 3, | |
| include_hidden_connection: bool = True, | |
| include_unresolved_thread: bool = True | |
| ) -> LayeredEvent: | |
| """Add mystery layers to a surface event.""" | |
| character_ids = character_ids or [] | |
| event_id = self._generate_id("event") | |
| # Generate interpretations | |
| interpretations = self._generate_interpretations( | |
| surface_event, | |
| force_interpretations or num_interpretations | |
| ) | |
| # Generate evidence | |
| evidence = self._generate_evidence(interpretations, surface_event) | |
| # Generate hidden connection | |
| hidden_connections = [] | |
| if include_hidden_connection and len(character_ids) >= 2: | |
| conn = self._generate_hidden_connection(character_ids, event_id) | |
| if conn: | |
| hidden_connections.append(conn) | |
| # Generate unresolved thread | |
| unresolved_threads = [] | |
| if include_unresolved_thread: | |
| thread = self._generate_unresolved_thread(surface_event, event_id) | |
| unresolved_threads.append(thread) | |
| # Generate revelation potentials | |
| revelations = self._generate_revelations(interpretations, hidden_connections, event_id) | |
| # Create layered event | |
| layered_event = LayeredEvent( | |
| id=event_id, | |
| surface_description=surface_event, | |
| character_ids=character_ids, | |
| interpretations=interpretations, | |
| evidence=evidence, | |
| hidden_connections=hidden_connections, | |
| unresolved_threads=unresolved_threads, | |
| revelations=revelations | |
| ) | |
| # Store | |
| self.events[event_id] = layered_event | |
| for e in evidence: | |
| self.evidence[e.id] = e | |
| for c in hidden_connections: | |
| self.connections[c.id] = c | |
| for t in unresolved_threads: | |
| self.threads[t.id] = t | |
| for r in revelations: | |
| self.revelations[r.id] = r | |
| return layered_event | |
| def _generate_interpretations( | |
| self, | |
| surface_event: str, | |
| count_or_specific | |
| ) -> List[Interpretation]: | |
| """Generate multiple valid interpretations.""" | |
| interpretations = [] | |
| templates = list(MOTIVATION_TEMPLATES.values()) | |
| if isinstance(count_or_specific, list): | |
| # Specific interpretations requested | |
| for reading in count_or_specific: | |
| template = MOTIVATION_TEMPLATES.get(reading, templates[0]) | |
| interpretations.append(Interpretation( | |
| id=self._generate_id("interp"), | |
| reading=template["reading"], | |
| description=template["description"], | |
| plausibility=0.3 + random.random() * 0.5, | |
| darkness_level=template["darkness_level"], | |
| depth=DepthLevel(min(2 + len(interpretations), 5)) | |
| )) | |
| else: | |
| # Random interpretations | |
| random.shuffle(templates) | |
| for i, template in enumerate(templates[:count_or_specific]): | |
| interpretations.append(Interpretation( | |
| id=self._generate_id("interp"), | |
| reading=template["reading"], | |
| description=template["description"], | |
| plausibility=0.2 + random.random() * 0.6, | |
| darkness_level=template["darkness_level"], | |
| depth=DepthLevel(min(2 + i, 5)) | |
| )) | |
| # Normalize plausibility | |
| total = sum(i.plausibility for i in interpretations) | |
| if total > 0: | |
| for i in interpretations: | |
| i.plausibility /= total | |
| return interpretations | |
| def _generate_evidence( | |
| self, | |
| interpretations: List[Interpretation], | |
| surface_event: str | |
| ) -> List[Evidence]: | |
| """Generate evidence supporting interpretations.""" | |
| evidence = [] | |
| for interp in interpretations: | |
| reading = interp.reading | |
| # Get dialogue evidence | |
| if reading in EVIDENCE_TEMPLATES.get("dialogue", {}): | |
| content = random.choice(EVIDENCE_TEMPLATES["dialogue"][reading]) | |
| evidence.append(Evidence( | |
| id=self._generate_id("evidence"), | |
| evidence_type="dialogue", | |
| content=content, | |
| description=f'Evidence supporting "{reading}" interpretation', | |
| depth=interp.depth, | |
| supports=[interp.id] | |
| )) | |
| # Get visual evidence | |
| if reading in EVIDENCE_TEMPLATES.get("visual", {}): | |
| content = random.choice(EVIDENCE_TEMPLATES["visual"][reading]) | |
| evidence.append(Evidence( | |
| id=self._generate_id("evidence"), | |
| evidence_type="visual", | |
| content=content, | |
| depth=interp.depth, | |
| supports=[interp.id] | |
| )) | |
| # Maybe add red herring | |
| if random.random() < 0.25: | |
| evidence.append(Evidence( | |
| id=self._generate_id("evidence"), | |
| evidence_type="environmental", | |
| content=random.choice(RED_HERRINGS), | |
| description="An evocative detail that draws attention", | |
| depth=DepthLevel.NOTICEABLE, | |
| is_red_herring=True | |
| )) | |
| return evidence | |
| def _generate_hidden_connection( | |
| self, | |
| character_ids: List[str], | |
| event_id: str | |
| ) -> Optional[HiddenConnection]: | |
| """Generate a hidden connection between characters.""" | |
| if len(character_ids) < 2: | |
| return None | |
| entity1 = character_ids[0] | |
| entity2 = character_ids[1] | |
| connection_types = [ | |
| "biological_parent", "sibling", "former_partners", | |
| "childhood_friends", "same_organization", "shared_trauma" | |
| ] | |
| conn_type = random.choice(connection_types) | |
| return HiddenConnection( | |
| id=self._generate_id("connection"), | |
| entity1=entity1, | |
| entity2=entity2, | |
| connection_type=conn_type, | |
| description=f"{entity1} and {entity2} have a hidden {conn_type.replace('_', ' ')} connection", | |
| depth=DepthLevel.BURIED | |
| ) | |
| def _generate_unresolved_thread( | |
| self, | |
| surface_event: str, | |
| event_id: str | |
| ) -> UnresolvedThread: | |
| """Generate an unresolved mystery thread.""" | |
| template = random.choice(UNRESOLVED_THREAD_TEMPLATES) | |
| return UnresolvedThread( | |
| id=self._generate_id("thread"), | |
| element=template["element"], | |
| description=f'During "{surface_event}": {template["element"]}', | |
| possible_meanings=template["meanings"], | |
| speculation_hooks=template["hooks"], | |
| will_resolve=False | |
| ) | |
| def _generate_revelations( | |
| self, | |
| interpretations: List[Interpretation], | |
| hidden_connections: List[HiddenConnection], | |
| event_id: str | |
| ) -> List[Revelation]: | |
| """Generate potential revelations.""" | |
| revelations = [] | |
| # Revelation for darkest interpretation | |
| if interpretations: | |
| darkest = max(interpretations, key=lambda i: i.darkness_level) | |
| revelations.append(Revelation( | |
| id=self._generate_id("revelation"), | |
| content=f'The truth is "{darkest.reading}": {darkest.description}', | |
| trigger_condition="Specific evidence discovered or character confession", | |
| emotional_impact="devastating" if darkest.darkness_level > 0.6 else "high", | |
| reframes_events=[event_id], | |
| new_questions=[ | |
| "How long has this been true?", | |
| "Who else knows?", | |
| "What does this mean for other events?" | |
| ] | |
| )) | |
| # Revelation for hidden connections | |
| for conn in hidden_connections: | |
| revelations.append(Revelation( | |
| id=self._generate_id("revelation"), | |
| content=conn.description, | |
| trigger_condition="Characters confronted with evidence", | |
| emotional_impact="high", | |
| reframes_events=[event_id], | |
| new_questions=[ | |
| "How long have they hidden this?", | |
| "Who else is connected?", | |
| "What else are they hiding?" | |
| ] | |
| )) | |
| return revelations | |
| def generate_iceberg_summary(self, event_id: str) -> Dict[str, Any]: | |
| """Generate an iceberg visualization summary.""" | |
| event = self.events.get(event_id) | |
| if not event: | |
| return {} | |
| return { | |
| "surface": { | |
| "name": "What Everyone Sees", | |
| "content": [event.surface_description] | |
| }, | |
| "shallow": { | |
| "name": "Details Attentive Viewers Notice", | |
| "content": [e.content for e in event.evidence if e.depth.value <= 2] | |
| }, | |
| "mid": { | |
| "name": "Implications That Spark Discussion", | |
| "content": [f"Theory: {i.description}" for i in event.interpretations] | |
| }, | |
| "deep": { | |
| "name": "Hidden Connections", | |
| "content": [c.description for c in event.hidden_connections] | |
| }, | |
| "abyss": { | |
| "name": "Unresolved Mysteries", | |
| "content": [hook for t in event.unresolved_threads for hook in t.speculation_hooks] | |
| } | |
| } | |
| # === NARRATIVE ENGINE === | |
| class NarrativeEngine: | |
| """Unified narrative engine integrating all storytelling paradigms.""" | |
| def __init__(self): | |
| self.mode = NarrativeMode.STRUGGLE | |
| self.characters: Dict[str, Character] = {} | |
| self.nodes: Dict[str, WorldNode] = {} | |
| self.mystery = MysteryIcebergGenerator() | |
| self.narrative_log: List[Dict[str, Any]] = [] | |
| self.audience_knowledge: Set[str] = set() | |
| self.story_setup: Optional[StorySetup] = None | |
| self.story_clues: List[StoryClue] = [] | |
| def set_mode(self, mode: NarrativeMode) -> "NarrativeEngine": | |
| self.mode = mode | |
| return self | |
| def register_character( | |
| self, | |
| character_id: str, | |
| name: str = None, | |
| role: str = "", | |
| background: str = "", | |
| secrets: List[str] = None | |
| ) -> "NarrativeEngine": | |
| """Register a character with the engine.""" | |
| self.characters[character_id] = Character( | |
| id=character_id, | |
| name=name or character_id, | |
| role=role, | |
| background=background, | |
| secrets=secrets or [] | |
| ) | |
| return self | |
| def add_want( | |
| self, | |
| character_id: str, | |
| want_id: str, | |
| description: str, | |
| target_nodes: List[str] = None | |
| ) -> "NarrativeEngine": | |
| """Add a want/goal to a character.""" | |
| if character_id in self.characters: | |
| self.characters[character_id].add_want(want_id, description, target_nodes) | |
| return self | |
| def create_node( | |
| self, | |
| node_id: str, | |
| node_type: NodeType, | |
| name: str, | |
| capacity: int = 1, | |
| description: str = "", | |
| metadata: Dict[str, Any] = None | |
| ) -> "NarrativeEngine": | |
| """Create a world node.""" | |
| self.nodes[node_id] = WorldNode( | |
| id=node_id, | |
| node_type=node_type, | |
| name=name, | |
| capacity=capacity, | |
| description=description, | |
| metadata=metadata or {} | |
| ) | |
| return self | |
| def connect_nodes( | |
| self, | |
| node1_id: str, | |
| node2_id: str, | |
| bidirectional: bool = True | |
| ) -> "NarrativeEngine": | |
| """Connect two nodes.""" | |
| if node1_id in self.nodes and node2_id in self.nodes: | |
| if node2_id not in self.nodes[node1_id].connections: | |
| self.nodes[node1_id].connections.append(node2_id) | |
| if bidirectional and node1_id not in self.nodes[node2_id].connections: | |
| self.nodes[node2_id].connections.append(node1_id) | |
| return self | |
| def execute_action( | |
| self, | |
| character_id: str, | |
| action: str, | |
| target_node_id: str, | |
| add_mystery_layers: bool = True, | |
| narrative_context: str = "" | |
| ) -> Dict[str, Any]: | |
| """Execute an action with full narrative generation.""" | |
| character = self.characters.get(character_id) | |
| node = self.nodes.get(target_node_id) | |
| if not character or not node: | |
| return {"success": False, "error": "Character or node not found"} | |
| # Build surface description | |
| action_descriptions = { | |
| "acquire": f"{character.name} acquires {node.name}", | |
| "release": f"{character.name} releases {node.name}", | |
| "consume": f"{character.name} consumes/destroys {node.name}", | |
| "reveal": f"{character.name} reveals {node.name}", | |
| "compromise": f"{character.name} compromises {node.name}", | |
| "pressure": f"{character.name} pressures {node.name}", | |
| "accelerate": f"{character.name} accelerates the timeline for {node.name}" | |
| } | |
| surface_event = action_descriptions.get(action, f"{character.name} acts on {node.name}") | |
| if narrative_context: | |
| surface_event = f"{surface_event} ({narrative_context})" | |
| # Add mystery layers if requested | |
| layered_event = None | |
| iceberg = None | |
| if add_mystery_layers: | |
| layered_event = self.mystery.add_mystery_layers( | |
| surface_event, | |
| character_ids=[character_id], | |
| include_hidden_connection=False, | |
| include_unresolved_thread=random.random() < 0.5 | |
| ) | |
| iceberg = self.mystery.generate_iceberg_summary(layered_event.id) | |
| # Log the narrative entry | |
| narrative_entry = { | |
| "timestamp": datetime.now().isoformat(), | |
| "surface_event": surface_event, | |
| "actor": character_id, | |
| "action": action, | |
| "target_node": target_node_id, | |
| "mystery": { | |
| "event_id": layered_event.id if layered_event else None, | |
| "interpretations": [ | |
| {"reading": i.reading, "plausibility": i.plausibility} | |
| for i in (layered_event.interpretations if layered_event else []) | |
| ] | |
| } if layered_event else None | |
| } | |
| self.narrative_log.append(narrative_entry) | |
| self.audience_knowledge.add(surface_event) | |
| return { | |
| "success": True, | |
| "narrative": narrative_entry, | |
| "layered_event": layered_event, | |
| "iceberg": iceberg | |
| } | |
| def get_narrative_at_depth(self, depth: DepthLevel = DepthLevel.NOTICEABLE) -> List[Dict]: | |
| """Get the narrative visible at a specific depth level.""" | |
| result = [] | |
| for entry in self.narrative_log: | |
| base = { | |
| "event": entry["surface_event"], | |
| "actor": entry["actor"] | |
| } | |
| if entry.get("mystery") and entry["mystery"].get("event_id"): | |
| event_id = entry["mystery"]["event_id"] | |
| event = self.mystery.events.get(event_id) | |
| if event: | |
| base["interpretations"] = [ | |
| i.description for i in event.interpretations | |
| if i.depth.value <= depth.value | |
| ] | |
| base["evidence"] = [ | |
| e.content for e in event.evidence | |
| if e.depth.value <= depth.value | |
| ] | |
| result.append(base) | |
| return result | |
| def generate_full_iceberg(self) -> Dict[str, Any]: | |
| """Generate a complete iceberg for the entire narrative.""" | |
| levels = { | |
| "surface": { | |
| "name": "The Plot (What Everyone Sees)", | |
| "content": [e["surface_event"] for e in self.narrative_log] | |
| }, | |
| "shallow": { | |
| "name": "The Details (Attentive Viewers Notice)", | |
| "content": [] | |
| }, | |
| "mid": { | |
| "name": "The Interpretations (Fan Discussions)", | |
| "content": [] | |
| }, | |
| "deep": { | |
| "name": "The Connections (Deep Lore)", | |
| "content": [] | |
| }, | |
| "abyss": { | |
| "name": "The Mysteries (Endless Debate)", | |
| "content": [] | |
| } | |
| } | |
| for event in self.mystery.events.values(): | |
| # Shallow: visible evidence | |
| levels["shallow"]["content"].extend([ | |
| e.content for e in event.evidence if e.depth.value <= 2 | |
| ]) | |
| # Mid: interpretations | |
| levels["mid"]["content"].extend([ | |
| f"Theory: {i.description}" for i in event.interpretations | |
| ]) | |
| # Deep: hidden connections | |
| levels["deep"]["content"].extend([ | |
| c.description for c in event.hidden_connections | |
| ]) | |
| # Abyss: unresolved speculation | |
| for thread in event.unresolved_threads: | |
| levels["abyss"]["content"].extend(thread.speculation_hooks) | |
| # Deduplicate | |
| for level in levels.values(): | |
| level["content"] = list(set(level["content"])) | |
| return levels | |
| def get_character_perspective(self, character_id: str) -> Dict[str, Any]: | |
| """Get what a character knows vs doesn't know.""" | |
| character = self.characters.get(character_id) | |
| if not character: | |
| return {} | |
| connections = [ | |
| c for c in self.mystery.connections.values() | |
| if c.entity1 == character_id or c.entity2 == character_id | |
| ] | |
| return { | |
| "character_id": character_id, | |
| "name": character.name, | |
| "current_struggle": character.struggle_score, | |
| "knows": list(character.knowledge), | |
| "hidden_from_them": [ | |
| c.description for c in connections if not c.revealed | |
| ], | |
| "wants": character.wants | |
| } | |
| def to_dict(self) -> Dict[str, Any]: | |
| """Serialize engine state to dictionary.""" | |
| return { | |
| "mode": self.mode.value, | |
| "characters": { | |
| k: { | |
| "id": v.id, | |
| "name": v.name, | |
| "role": v.role, | |
| "background": v.background, | |
| "wants": v.wants, | |
| "struggle_score": v.struggle_score | |
| } for k, v in self.characters.items() | |
| }, | |
| "nodes": { | |
| k: { | |
| "id": v.id, | |
| "type": v.node_type.value, | |
| "name": v.name, | |
| "description": v.description, | |
| "connections": v.connections | |
| } for k, v in self.nodes.items() | |
| }, | |
| "narrative_log": self.narrative_log, | |
| "audience_knowledge": list(self.audience_knowledge), | |
| "story_setup": { | |
| "title": self.story_setup.title, | |
| "place": self.story_setup.place, | |
| "time": self.story_setup.time, | |
| "protagonist": self.story_setup.protagonist, | |
| "hook": self.story_setup.hook, | |
| "stakes": self.story_setup.stakes | |
| } if self.story_setup else None, | |
| "story_clues": [ | |
| { | |
| "id": c.id, | |
| "content": c.content, | |
| "truth_id": c.truth_id, | |
| "noticeability": c.noticeability, | |
| "type": c.clue_type | |
| } for c in self.story_clues | |
| ] | |
| } | |