Skip to main content

proof_engine/narrative/
motivation.rs

1//! Character motivation engine — goals, beliefs, desires as utility functions.
2
3use crate::worldgen::Rng;
4use std::collections::HashMap;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
7pub enum Need { Survival, Safety, Belonging, Esteem, SelfActualization, Power, Revenge, Love, Knowledge, Wealth }
8
9#[derive(Debug, Clone)]
10pub struct Motivation {
11    pub needs: HashMap<Need, f32>,
12    pub goals: Vec<Goal>,
13    pub beliefs: Vec<Belief>,
14    pub personality: Personality,
15}
16
17#[derive(Debug, Clone)]
18pub struct Goal { pub description: String, pub need: Need, pub priority: f32, pub progress: f32, pub completed: bool }
19
20#[derive(Debug, Clone)]
21pub struct Belief { pub subject: String, pub value: f32 }
22
23#[derive(Debug, Clone, Copy)]
24pub struct Personality {
25    pub openness: f32, pub conscientiousness: f32, pub extraversion: f32,
26    pub agreeableness: f32, pub neuroticism: f32,
27}
28
29impl Motivation {
30    pub fn random(rng: &mut Rng) -> Self {
31        let mut needs = HashMap::new();
32        for &need in &[Need::Survival, Need::Safety, Need::Belonging, Need::Esteem, Need::Power, Need::Knowledge, Need::Wealth] {
33            needs.insert(need, rng.range_f32(0.2, 0.8));
34        }
35        Self {
36            needs,
37            goals: Vec::new(),
38            beliefs: Vec::new(),
39            personality: Personality {
40                openness: rng.next_f32(), conscientiousness: rng.next_f32(),
41                extraversion: rng.next_f32(), agreeableness: rng.next_f32(),
42                neuroticism: rng.next_f32(),
43            },
44        }
45    }
46
47    /// Evaluate utility of an action based on which needs it satisfies.
48    pub fn evaluate_action(&self, need_impacts: &[(Need, f32)]) -> f32 {
49        need_impacts.iter().map(|(need, impact)| {
50            let weight = self.needs.get(need).copied().unwrap_or(0.0);
51            weight * impact
52        }).sum()
53    }
54
55    /// Choose the highest-priority unfinished goal.
56    pub fn active_goal(&self) -> Option<&Goal> {
57        self.goals.iter().filter(|g| !g.completed)
58            .max_by(|a, b| a.priority.partial_cmp(&b.priority).unwrap())
59    }
60}
61
62#[cfg(test)]
63mod tests {
64    use super::*;
65
66    #[test]
67    fn test_motivation_random() {
68        let mut rng = Rng::new(42);
69        let m = Motivation::random(&mut rng);
70        assert!(!m.needs.is_empty());
71    }
72
73    #[test]
74    fn test_evaluate_action() {
75        let mut rng = Rng::new(42);
76        let m = Motivation::random(&mut rng);
77        let score = m.evaluate_action(&[(Need::Survival, 1.0), (Need::Wealth, 0.5)]);
78        assert!(score > 0.0);
79    }
80}