1use crate::worldgen::Rng;
7use std::collections::HashMap;
8
9#[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#[derive(Debug, Clone)]
41pub struct Production {
42 pub symbol: String,
43 pub expansions: Vec<(Vec<String>, f32)>, }
45
46#[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 pub fn default_grammar() -> Self {
56 let mut productions = HashMap::new();
57 let mut terminals = HashMap::new();
58
59 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 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 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; } 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 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 if let Some((expansion, _)) = expansions.first() {
133 for sym in expansion {
134 self.expand(sym, rng, result, depth + 1);
135 }
136 }
137 } else {
138 result.push(format!("[{}]", symbol));
140 }
141 }
142}
143
144#[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}