Skip to main content

proof_engine/narrative/
poetry.rs

1//! Procedural poetry and song — generated verse with meter and rhyme.
2//! Uses mathematical patterns like Fibonacci syllable counts.
3
4use crate::worldgen::Rng;
5
6/// Meter type for generated verse.
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum Meter { Iambic, Trochaic, Anapestic, Dactylic, Free, Fibonacci }
9
10/// A line of poetry.
11#[derive(Debug, Clone)]
12pub struct VerseLine { pub text: String, pub syllable_count: usize, pub rhyme_sound: String }
13
14/// A complete poem.
15#[derive(Debug, Clone)]
16pub struct Poem {
17    pub title: String,
18    pub lines: Vec<VerseLine>,
19    pub meter: Meter,
20    pub rhyme_scheme: String,
21}
22
23/// Generate a poem.
24pub fn generate_poem(theme: &str, meter: Meter, lines: usize, rng: &mut Rng) -> Poem {
25    let syllable_counts = match meter {
26        Meter::Fibonacci => fibonacci_syllables(lines),
27        Meter::Iambic => vec![10; lines],
28        Meter::Trochaic => vec![8; lines],
29        _ => (0..lines).map(|_| rng.range_usize(5, 12)).collect(),
30    };
31
32    let rhyme_endings = generate_rhyme_pairs(lines, rng);
33    let verse_lines: Vec<VerseLine> = syllable_counts.iter().enumerate().map(|(i, &count)| {
34        let text = generate_line(theme, count, rng);
35        VerseLine { text, syllable_count: count, rhyme_sound: rhyme_endings[i].clone() }
36    }).collect();
37
38    let scheme = if lines == 4 { "ABAB".to_string() }
39        else if lines == 2 { "AA".to_string() }
40        else { "Free".to_string() };
41
42    Poem { title: format!("Ode to {}", capitalize(theme)), lines: verse_lines, meter, rhyme_scheme: scheme }
43}
44
45fn fibonacci_syllables(n: usize) -> Vec<usize> {
46    let mut fibs = Vec::with_capacity(n);
47    let (mut a, mut b) = (1usize, 1usize);
48    for _ in 0..n {
49        fibs.push(a.max(1).min(13));
50        let next = a + b;
51        a = b;
52        b = next;
53    }
54    fibs
55}
56
57fn generate_line(theme: &str, target_syllables: usize, rng: &mut Rng) -> String {
58    let words_by_syl: Vec<(&str, usize)> = vec![
59        ("the", 1), ("of", 1), ("and", 1), ("in", 1), ("a", 1), ("to", 1), ("with", 1),
60        ("is", 1), ("was", 1), ("on", 1), ("by", 1), ("no", 1), ("from", 1),
61        ("dark", 1), ("light", 1), ("wind", 1), ("stone", 1), ("fire", 1), ("rain", 1),
62        ("shadow", 2), ("river", 2), ("mountain", 2), ("silence", 2), ("ancient", 2),
63        ("golden", 2), ("silver", 2), ("fallen", 2), ("rising", 2), ("burning", 2),
64        ("wandering", 3), ("eternal", 3), ("beautiful", 3), ("forgotten", 3), ("awakening", 4),
65        ("remembering", 4), ("everlasting", 4),
66    ];
67
68    let mut line = Vec::new();
69    let mut remaining = target_syllables;
70    while remaining > 0 {
71        let candidates: Vec<_> = words_by_syl.iter().filter(|(_, s)| *s <= remaining).collect();
72        if candidates.is_empty() { break; }
73        let &(word, syls) = candidates[rng.next_u64() as usize % candidates.len()];
74        line.push(word);
75        remaining -= syls;
76    }
77
78    let mut text = line.join(" ");
79    if !text.is_empty() {
80        let first = text.remove(0).to_uppercase().to_string();
81        text = first + &text;
82    }
83    text
84}
85
86fn generate_rhyme_pairs(n: usize, rng: &mut Rng) -> Vec<String> {
87    let endings = ["ight", "ane", "ow", "air", "ound", "ong", "aze", "ear", "ire", "one"];
88    (0..n).map(|i| {
89        let pair_idx = i / 2;
90        endings[pair_idx % endings.len()].to_string()
91    }).collect()
92}
93
94fn capitalize(s: &str) -> String {
95    let mut c = s.chars();
96    match c.next() {
97        None => String::new(),
98        Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
99    }
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn test_fibonacci_poem() {
108        let mut rng = Rng::new(42);
109        let poem = generate_poem("shadow", Meter::Fibonacci, 6, &mut rng);
110        assert_eq!(poem.lines.len(), 6);
111        // Fibonacci: 1, 1, 2, 3, 5, 8
112        assert_eq!(poem.lines[0].syllable_count, 1);
113        assert_eq!(poem.lines[4].syllable_count, 5);
114    }
115
116    #[test]
117    fn test_generate_line() {
118        let mut rng = Rng::new(42);
119        let line = generate_line("war", 8, &mut rng);
120        assert!(!line.is_empty());
121    }
122}