Skip to main content

proof_engine/narrative/
story_grammar.rs

1//! Story grammar system — generate plot structures from narrative grammars.
2//!
3//! Uses context-free grammars with weighted productions to generate
4//! narrative arcs: setup, rising action, climax, falling action, resolution.
5
6use crate::worldgen::Rng;
7use std::collections::HashMap;
8
9/// A story beat (atomic narrative unit).
10#[derive(Debug, Clone)]
11pub struct StoryBeat {
12    pub beat_type: BeatType,
13    pub description: String,
14    pub tension_delta: f32,
15    pub characters_involved: Vec<String>,
16    pub location: String,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
20pub enum BeatType {
21    Introduction, Incitement, RisingAction, Complication, Crisis,
22    Climax, Reversal, FallingAction, Resolution, Denouement,
23    Reveal, Betrayal, Sacrifice, Reunion, Discovery, Loss, Victory, Defeat,
24}
25
26impl BeatType {
27    pub fn tension_contribution(self) -> f32 {
28        match self {
29            Self::Introduction => 0.1, Self::Incitement => 0.3, Self::RisingAction => 0.2,
30            Self::Complication => 0.3, Self::Crisis => 0.4, Self::Climax => 0.5,
31            Self::Reversal => -0.2, Self::FallingAction => -0.3, Self::Resolution => -0.4,
32            Self::Denouement => -0.2, Self::Reveal => 0.2, Self::Betrayal => 0.4,
33            Self::Sacrifice => 0.1, Self::Reunion => -0.1, Self::Discovery => 0.15,
34            Self::Loss => 0.3, Self::Victory => -0.3, Self::Defeat => 0.2,
35        }
36    }
37}
38
39/// A production rule in the story grammar.
40#[derive(Debug, Clone)]
41pub struct Production {
42    pub symbol: String,
43    pub expansions: Vec<(Vec<String>, f32)>, // (expansion, weight)
44}
45
46/// A story grammar.
47#[derive(Debug, Clone)]
48pub struct StoryGrammar {
49    pub productions: HashMap<String, Vec<(Vec<String>, f32)>>,
50    pub terminals: HashMap<String, Vec<String>>,
51}
52
53impl StoryGrammar {
54    /// Create the default narrative grammar.
55    pub fn default_grammar() -> Self {
56        let mut productions = HashMap::new();
57        let mut terminals = HashMap::new();
58
59        // Story structure
60        productions.insert("STORY".to_string(), vec![
61            (vec!["SETUP".into(), "CONFLICT".into(), "RESOLUTION".into()], 0.5),
62            (vec!["SETUP".into(), "CONFLICT".into(), "TWIST".into(), "RESOLUTION".into()], 0.3),
63            (vec!["IN_MEDIAS_RES".into(), "FLASHBACK".into(), "CONFLICT".into(), "RESOLUTION".into()], 0.2),
64        ]);
65
66        productions.insert("SETUP".to_string(), vec![
67            (vec!["INTRODUCE_HERO".into(), "ESTABLISH_WORLD".into()], 0.5),
68            (vec!["ESTABLISH_WORLD".into(), "INTRODUCE_HERO".into(), "INTRODUCE_MENTOR".into()], 0.3),
69            (vec!["INTRODUCE_HERO".into(), "ORDINARY_WORLD".into()], 0.2),
70        ]);
71
72        productions.insert("CONFLICT".to_string(), vec![
73            (vec!["INCITING_INCIDENT".into(), "RISING_ACTION".into(), "CLIMAX".into()], 0.4),
74            (vec!["INCITING_INCIDENT".into(), "COMPLICATION".into(), "RISING_ACTION".into(), "CRISIS".into(), "CLIMAX".into()], 0.4),
75            (vec!["CALL_TO_ADVENTURE".into(), "TRIALS".into(), "ORDEAL".into()], 0.2),
76        ]);
77
78        productions.insert("RESOLUTION".to_string(), vec![
79            (vec!["FALLING_ACTION".into(), "DENOUEMENT".into()], 0.5),
80            (vec!["AFTERMATH".into(), "NEW_NORMAL".into()], 0.3),
81            (vec!["BITTERSWEET_END".into()], 0.2),
82        ]);
83
84        // Terminal beat templates
85        terminals.insert("INTRODUCE_HERO".into(), vec![
86            "A {hero} lives in {place}, unaware of their destiny.".into(),
87            "In {place}, {hero} goes about their ordinary life.".into(),
88        ]);
89        terminals.insert("INCITING_INCIDENT".into(), vec![
90            "A {threat} appears, shattering the peace of {place}.".into(),
91            "{hero} discovers a {secret} that changes everything.".into(),
92        ]);
93        terminals.insert("CLIMAX".into(), vec![
94            "{hero} faces {villain} in a final confrontation.".into(),
95            "Everything comes to a head at {place}.".into(),
96        ]);
97
98        Self { productions, terminals }
99    }
100
101    /// Generate a story outline from the grammar.
102    pub fn generate(&self, rng: &mut Rng) -> Vec<String> {
103        let mut result = Vec::new();
104        self.expand("STORY", rng, &mut result, 0);
105        result
106    }
107
108    fn expand(&self, symbol: &str, rng: &mut Rng, result: &mut Vec<String>, depth: usize) {
109        if depth > 20 { return; } // prevent infinite recursion
110
111        if let Some(templates) = self.terminals.get(symbol) {
112            if let Some(t) = rng.pick(templates) {
113                result.push(t.clone());
114            }
115            return;
116        }
117
118        if let Some(expansions) = self.productions.get(symbol) {
119            // Weighted random selection
120            let total: f32 = expansions.iter().map(|(_, w)| w).sum();
121            let mut target = rng.next_f32() * total;
122            for (expansion, weight) in expansions {
123                target -= weight;
124                if target <= 0.0 {
125                    for sym in expansion {
126                        self.expand(sym, rng, result, depth + 1);
127                    }
128                    return;
129                }
130            }
131            // Fallback: first expansion
132            if let Some((expansion, _)) = expansions.first() {
133                for sym in expansion {
134                    self.expand(sym, rng, result, depth + 1);
135                }
136            }
137        } else {
138            // Unknown symbol: treat as terminal
139            result.push(format!("[{}]", symbol));
140        }
141    }
142}
143
144/// A complete generated story.
145#[derive(Debug, Clone)]
146pub struct Story {
147    pub title: String,
148    pub beats: Vec<StoryBeat>,
149    pub theme: String,
150    pub arc_type: ArcType,
151}
152
153#[derive(Debug, Clone, Copy, PartialEq, Eq)]
154pub enum ArcType { HeroJourney, Tragedy, Comedy, Redemption, Mystery, Quest, Revenge }
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_grammar_generates() {
162        let grammar = StoryGrammar::default_grammar();
163        let mut rng = Rng::new(42);
164        let outline = grammar.generate(&mut rng);
165        assert!(!outline.is_empty(), "grammar should produce output");
166    }
167
168    #[test]
169    fn test_beat_tension() {
170        assert!(BeatType::Climax.tension_contribution() > BeatType::Introduction.tension_contribution());
171        assert!(BeatType::Resolution.tension_contribution() < 0.0);
172    }
173}