""" 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 === @dataclass 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 } @dataclass 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) @dataclass 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) @dataclass 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 @dataclass 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) @dataclass 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 @dataclass 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 @dataclass 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) @dataclass class StoryClue: id: str content: str truth_id: str noticeability: float = 0.5 planted_at: float = 0.0 clue_type: str = "visual" @dataclass 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 ] }