bhava 2.0.0

Emotion and personality engine — trait spectrums, mood vectors, archetypes, behavioral mapping
Documentation
//! Experience-driven personality growth — traits evolve from accumulated events.
//!
//! Instead of mutating toward a predetermined target, personality changes emerge
//! from accumulated "trait pressure" generated by emotional events. A shy NPC
//! forced into social situations gradually becomes less shy — without specifying
//! the target.
//!
//! Pressure accumulates from appraisal events and decays slowly if not reinforced.
//! When pressure exceeds a threshold, the trait shifts one level.

use serde::{Deserialize, Serialize};

use crate::appraisal::AppraisedEmotion;
use crate::traits::{PersonalityProfile, TraitKind, TraitLevel};

/// Tracks accumulated trait pressure from experiences.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct GrowthLedger {
    /// Accumulated pressure per trait dimension (positive = push higher, negative = push lower).
    pressure: [f32; TraitKind::COUNT],
    /// How much pressure is needed to shift a trait one level.
    pub threshold: f32,
    /// How fast pressure decays if not reinforced (0.0–1.0 per tick).
    pub decay_rate: f32,
}

impl Default for GrowthLedger {
    fn default() -> Self {
        Self {
            pressure: [0.0; TraitKind::COUNT],
            threshold: 3.0,
            decay_rate: 0.05,
        }
    }
}

impl GrowthLedger {
    /// Create a new growth ledger with default thresholds.
    #[must_use]
    pub fn new() -> Self {
        Self::default()
    }

    /// Create with custom threshold and decay.
    #[must_use]
    pub fn with_params(threshold: f32, decay_rate: f32) -> Self {
        Self {
            pressure: [0.0; TraitKind::COUNT],
            threshold: threshold.max(0.1),
            decay_rate: decay_rate.clamp(0.0, 1.0),
        }
    }

    /// Apply trait pressure from an appraised emotion.
    ///
    /// Maps OCC emotions to trait pressures:
    /// - Pride → Confidence+, Autonomy+
    /// - Shame → Confidence-, Directness-
    /// - Gratitude → Warmth+, Trust/Empathy+
    /// - Anger → Directness+, Patience-
    /// - Joy → Warmth+, Humor+
    /// - Fear → RiskTolerance-, Confidence-
    /// - Admiration → Empathy+, Curiosity+
    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
    pub fn apply_emotion(&mut self, emotion: AppraisedEmotion, intensity: f32) {
        let mappings: &[(TraitKind, f32)] = match emotion {
            AppraisedEmotion::Pride => &[(TraitKind::Confidence, 0.5), (TraitKind::Autonomy, 0.3)],
            AppraisedEmotion::Shame => {
                &[(TraitKind::Confidence, -0.5), (TraitKind::Directness, -0.3)]
            }
            AppraisedEmotion::Gratitude => &[(TraitKind::Warmth, 0.4), (TraitKind::Empathy, 0.3)],
            AppraisedEmotion::Anger => &[
                (TraitKind::Directness, 0.4),
                (TraitKind::Patience, -0.4),
                (TraitKind::Skepticism, 0.2),
            ],
            AppraisedEmotion::Joy => &[(TraitKind::Warmth, 0.3), (TraitKind::Humor, 0.2)],
            AppraisedEmotion::Distress => &[
                (TraitKind::Patience, -0.2),
                (TraitKind::RiskTolerance, -0.2),
            ],
            AppraisedEmotion::Hope => {
                &[(TraitKind::RiskTolerance, 0.2), (TraitKind::Curiosity, 0.2)]
            }
            AppraisedEmotion::Fear => &[
                (TraitKind::RiskTolerance, -0.4),
                (TraitKind::Confidence, -0.3),
            ],
            AppraisedEmotion::Admiration => {
                &[(TraitKind::Empathy, 0.3), (TraitKind::Curiosity, 0.2)]
            }
            AppraisedEmotion::Reproach => {
                &[(TraitKind::Skepticism, 0.3), (TraitKind::Warmth, -0.2)]
            }
            _ => &[],
        };

        for &(kind, weight) in mappings {
            self.pressure[kind.index()] += weight * intensity;
        }
    }

    /// Apply raw pressure to a specific trait.
    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
    pub fn apply_pressure(&mut self, kind: TraitKind, amount: f32) {
        self.pressure[kind.index()] += amount;
    }

    /// Get current pressure for a trait.
    #[must_use]
    pub fn get_pressure(&self, kind: TraitKind) -> f32 {
        self.pressure[kind.index()]
    }

    /// Decay all pressures (call periodically).
    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
    pub fn decay(&mut self) {
        for p in &mut self.pressure {
            *p *= 1.0 - self.decay_rate;
        }
    }

    /// Apply accumulated pressures to a personality profile.
    ///
    /// For each trait where pressure exceeds the threshold, shifts the trait
    /// one level in the pressure direction and resets that pressure.
    /// Returns the number of traits that changed.
    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
    pub fn apply_growth(&mut self, profile: &mut PersonalityProfile) -> usize {
        let mut changed = 0;
        for &kind in TraitKind::ALL {
            let p = self.pressure[kind.index()];
            if p.abs() >= self.threshold {
                let current = profile.get_trait(kind).numeric();
                let direction: i8 = if p > 0.0 { 1 } else { -1 };
                let new_val = (current + direction).clamp(-2, 2);
                if let Some(level) = TraitLevel::from_numeric(new_val)
                    .ok()
                    .filter(|&l| l != profile.get_trait(kind))
                {
                    profile.set_trait(kind, level);
                    changed += 1;
                }
                self.pressure[kind.index()] = 0.0; // reset after shift
            }
        }
        changed
    }

    /// Total absolute pressure across all traits.
    #[must_use]
    pub fn total_pressure(&self) -> f32 {
        self.pressure.iter().map(|p| p.abs()).sum()
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new() {
        let g = GrowthLedger::new();
        assert!(g.total_pressure() < f32::EPSILON);
    }

    #[test]
    fn test_apply_emotion_pride() {
        let mut g = GrowthLedger::new();
        g.apply_emotion(AppraisedEmotion::Pride, 1.0);
        assert!(g.get_pressure(TraitKind::Confidence) > 0.0);
        assert!(g.get_pressure(TraitKind::Autonomy) > 0.0);
    }

    #[test]
    fn test_apply_emotion_shame() {
        let mut g = GrowthLedger::new();
        g.apply_emotion(AppraisedEmotion::Shame, 1.0);
        assert!(g.get_pressure(TraitKind::Confidence) < 0.0);
    }

    #[test]
    fn test_apply_pressure_direct() {
        let mut g = GrowthLedger::new();
        g.apply_pressure(TraitKind::Warmth, 0.5);
        assert!((g.get_pressure(TraitKind::Warmth) - 0.5).abs() < f32::EPSILON);
    }

    #[test]
    fn test_decay() {
        let mut g = GrowthLedger::new();
        g.apply_pressure(TraitKind::Humor, 1.0);
        g.decay();
        assert!(g.get_pressure(TraitKind::Humor) < 1.0);
    }

    #[test]
    fn test_apply_growth_below_threshold() {
        let mut g = GrowthLedger::new();
        let mut p = PersonalityProfile::new("test");
        g.apply_pressure(TraitKind::Warmth, 1.0); // below default threshold of 3.0
        let changed = g.apply_growth(&mut p);
        assert_eq!(changed, 0);
        assert_eq!(p.get_trait(TraitKind::Warmth), TraitLevel::Balanced);
    }

    #[test]
    fn test_apply_growth_above_threshold() {
        let mut g = GrowthLedger::new();
        let mut p = PersonalityProfile::new("test");
        g.apply_pressure(TraitKind::Warmth, 4.0); // above threshold
        let changed = g.apply_growth(&mut p);
        assert_eq!(changed, 1);
        assert_eq!(p.get_trait(TraitKind::Warmth), TraitLevel::High);
        // Pressure should reset
        assert!(g.get_pressure(TraitKind::Warmth).abs() < f32::EPSILON);
    }

    #[test]
    fn test_apply_growth_negative() {
        let mut g = GrowthLedger::new();
        let mut p = PersonalityProfile::new("test");
        g.apply_pressure(TraitKind::Patience, -4.0);
        g.apply_growth(&mut p);
        assert_eq!(p.get_trait(TraitKind::Patience), TraitLevel::Low);
    }

    #[test]
    fn test_accumulated_experiences() {
        let mut g = GrowthLedger::with_params(2.0, 0.01);
        let mut p = PersonalityProfile::new("test");
        // Repeated pride events eventually shift confidence
        for _ in 0..10 {
            g.apply_emotion(AppraisedEmotion::Pride, 0.5);
        }
        let changed = g.apply_growth(&mut p);
        assert!(changed > 0);
        assert!(p.get_trait(TraitKind::Confidence) > TraitLevel::Balanced);
    }

    #[test]
    fn test_growth_respects_limits() {
        let mut g = GrowthLedger::new();
        let mut p = PersonalityProfile::new("test");
        p.set_trait(TraitKind::Warmth, TraitLevel::Highest);
        g.apply_pressure(TraitKind::Warmth, 5.0);
        let changed = g.apply_growth(&mut p);
        assert_eq!(changed, 0); // already at max
    }

    #[test]
    fn test_serde() {
        let mut g = GrowthLedger::new();
        g.apply_pressure(TraitKind::Humor, 1.5);
        let json = serde_json::to_string(&g).unwrap();
        let g2: GrowthLedger = serde_json::from_str(&json).unwrap();
        assert!((g2.get_pressure(TraitKind::Humor) - 1.5).abs() < f32::EPSILON);
    }

    #[test]
    fn test_total_pressure() {
        let mut g = GrowthLedger::new();
        g.apply_pressure(TraitKind::Humor, 1.0);
        g.apply_pressure(TraitKind::Warmth, -0.5);
        assert!((g.total_pressure() - 1.5).abs() < f32::EPSILON);
    }
}