GameConfigIdea / narrativeengine_hfport /narrative_engine.py
kwabs22
Port changes from duplicate space to original
9328e91
"""
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
]
}