use serde::{Deserialize, Serialize};
use crate::mood::{Emotion, MoodVector};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Appraisal {
pub event: String,
pub desirability: f32,
pub praiseworthiness: f32,
pub likelihood: f32,
pub causal_agent: Option<String>,
pub is_self: bool,
}
impl Appraisal {
#[must_use]
pub fn event(description: impl Into<String>, desirability: f32) -> Self {
Self {
event: description.into(),
desirability: desirability.clamp(-1.0, 1.0),
praiseworthiness: 0.0,
likelihood: 1.0,
causal_agent: None,
is_self: false,
}
}
#[must_use]
pub fn with_praise(mut self, praiseworthiness: f32) -> Self {
self.praiseworthiness = praiseworthiness.clamp(-1.0, 1.0);
self
}
#[must_use]
pub fn with_likelihood(mut self, likelihood: f32) -> Self {
self.likelihood = likelihood.clamp(0.0, 1.0);
self
}
#[must_use]
pub fn caused_by(mut self, agent: impl Into<String>) -> Self {
self.causal_agent = Some(agent.into());
self
}
#[must_use]
pub fn by_self(mut self) -> Self {
self.is_self = true;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum AppraisedEmotion {
Joy,
Distress,
Hope,
Fear,
Relief,
Disappointment,
Pride,
Shame,
Admiration,
Reproach,
Gratitude,
Anger,
}
impl std::fmt::Display for AppraisedEmotion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let s = match self {
Self::Joy => "joy",
Self::Distress => "distress",
Self::Hope => "hope",
Self::Fear => "fear",
Self::Relief => "relief",
Self::Disappointment => "disappointment",
Self::Pride => "pride",
Self::Shame => "shame",
Self::Admiration => "admiration",
Self::Reproach => "reproach",
Self::Gratitude => "gratitude",
Self::Anger => "anger",
};
f.write_str(s)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppraisalResult {
pub emotions: Vec<(AppraisedEmotion, f32)>,
pub mood_delta: MoodVector,
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn appraise(appraisal: &Appraisal, affinity_to_cause: Option<f32>) -> AppraisalResult {
let d = appraisal.desirability;
let p = appraisal.praiseworthiness;
let l = appraisal.likelihood;
let affinity = affinity_to_cause.unwrap_or(0.0);
let mut emotions: Vec<(AppraisedEmotion, f32)> = Vec::new();
let mut delta = MoodVector::neutral();
if l > 0.7 {
if d > 0.1 {
emotions.push((AppraisedEmotion::Joy, d));
delta.joy += d * 0.6;
delta.arousal += d * 0.2;
} else if d < -0.1 {
emotions.push((AppraisedEmotion::Distress, d.abs()));
delta.joy += d * 0.6; delta.arousal += d.abs() * 0.2;
delta.frustration += d.abs() * 0.3;
}
}
if l < 0.7 && l > 0.0 {
if d > 0.1 {
emotions.push((AppraisedEmotion::Hope, d * l));
delta.interest += d * l * 0.4;
delta.arousal += l * 0.2;
} else if d < -0.1 {
emotions.push((AppraisedEmotion::Fear, d.abs() * l));
delta.trust -= d.abs() * l * 0.3;
delta.arousal += l * 0.3;
delta.dominance -= d.abs() * l * 0.2;
}
}
if l >= 1.0 {
}
if p.abs() > 0.1 {
if appraisal.is_self {
if p > 0.0 {
emotions.push((AppraisedEmotion::Pride, p));
delta.dominance += p * 0.4;
delta.joy += p * 0.2;
} else {
emotions.push((AppraisedEmotion::Shame, p.abs()));
delta.dominance -= p.abs() * 0.4;
delta.joy -= p.abs() * 0.2;
}
} else if p > 0.0 {
emotions.push((AppraisedEmotion::Admiration, p));
delta.trust += p * 0.3;
} else {
emotions.push((AppraisedEmotion::Reproach, p.abs()));
delta.trust -= p.abs() * 0.3;
delta.frustration += p.abs() * 0.2;
}
}
if d > 0.1 && !appraisal.is_self && appraisal.causal_agent.is_some() {
let gratitude = d * (1.0 + affinity).clamp(0.0, 2.0) * 0.5;
emotions.push((AppraisedEmotion::Gratitude, gratitude));
delta.trust += gratitude * 0.3;
}
if d < -0.1 && !appraisal.is_self && appraisal.causal_agent.is_some() {
let anger = d.abs() * (1.0 - affinity).clamp(0.0, 2.0) * 0.5;
emotions.push((AppraisedEmotion::Anger, anger));
delta.frustration += anger * 0.4;
delta.dominance += anger * 0.2;
}
for &e in Emotion::ALL {
delta.set(e, delta.get(e).clamp(-1.0, 1.0));
}
AppraisalResult {
emotions,
mood_delta: delta,
}
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn apply_appraisal(state: &mut crate::mood::EmotionalState, result: &AppraisalResult) {
for &emotion in Emotion::ALL {
let val = result.mood_delta.get(emotion);
if val.abs() > f32::EPSILON {
state.stimulate(emotion, val);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_positive_event() {
let a = Appraisal::event("got promoted", 0.8);
let r = appraise(&a, None);
assert!(r.emotions.iter().any(|(e, _)| *e == AppraisedEmotion::Joy));
assert!(r.mood_delta.joy > 0.0);
}
#[test]
fn test_negative_event() {
let a = Appraisal::event("project failed", -0.7);
let r = appraise(&a, None);
assert!(
r.emotions
.iter()
.any(|(e, _)| *e == AppraisedEmotion::Distress)
);
assert!(r.mood_delta.joy < 0.0);
assert!(r.mood_delta.frustration > 0.0);
}
#[test]
fn test_hope() {
let a = Appraisal::event("might get bonus", 0.6).with_likelihood(0.4);
let r = appraise(&a, None);
assert!(r.emotions.iter().any(|(e, _)| *e == AppraisedEmotion::Hope));
assert!(r.mood_delta.interest > 0.0);
}
#[test]
fn test_fear() {
let a = Appraisal::event("might get fired", -0.8).with_likelihood(0.5);
let r = appraise(&a, None);
assert!(r.emotions.iter().any(|(e, _)| *e == AppraisedEmotion::Fear));
assert!(r.mood_delta.trust < 0.0);
}
#[test]
fn test_pride() {
let a = Appraisal::event("wrote great code", 0.5)
.with_praise(0.8)
.by_self();
let r = appraise(&a, None);
assert!(
r.emotions
.iter()
.any(|(e, _)| *e == AppraisedEmotion::Pride)
);
assert!(r.mood_delta.dominance > 0.0);
}
#[test]
fn test_shame() {
let a = Appraisal::event("broke production", -0.5)
.with_praise(-0.9)
.by_self();
let r = appraise(&a, None);
assert!(
r.emotions
.iter()
.any(|(e, _)| *e == AppraisedEmotion::Shame)
);
assert!(r.mood_delta.dominance < 0.0);
}
#[test]
fn test_gratitude_with_high_affinity() {
let a = Appraisal::event("teammate helped", 0.7).caused_by("alice");
let r = appraise(&a, Some(0.8));
assert!(
r.emotions
.iter()
.any(|(e, _)| *e == AppraisedEmotion::Gratitude)
);
assert!(r.mood_delta.trust > 0.0);
}
#[test]
fn test_anger_with_low_affinity() {
let a = Appraisal::event("rival sabotaged", -0.8).caused_by("rival");
let r = appraise(&a, Some(-0.5));
assert!(
r.emotions
.iter()
.any(|(e, _)| *e == AppraisedEmotion::Anger)
);
assert!(r.mood_delta.frustration > 0.0);
}
#[test]
fn test_admiration() {
let a = Appraisal::event("mentor's speech", 0.3).with_praise(0.9);
let r = appraise(&a, None);
assert!(
r.emotions
.iter()
.any(|(e, _)| *e == AppraisedEmotion::Admiration)
);
}
#[test]
fn test_neutral_event() {
let a = Appraisal::event("nothing happened", 0.0);
let r = appraise(&a, None);
assert!(r.emotions.is_empty());
}
#[test]
fn test_apply_appraisal() {
let mut state = crate::mood::EmotionalState::new();
let a = Appraisal::event("great news", 0.8);
let r = appraise(&a, None);
apply_appraisal(&mut state, &r);
assert!(state.mood.joy > 0.0);
}
#[test]
fn test_builder_pattern() {
let a = Appraisal::event("test", 0.5)
.with_praise(0.3)
.with_likelihood(0.6)
.caused_by("bob")
.by_self();
assert_eq!(a.desirability, 0.5);
assert!(a.is_self);
assert_eq!(a.causal_agent.as_deref(), Some("bob"));
}
#[test]
fn test_serde_roundtrip() {
let a = Appraisal::event("test", 0.5).with_praise(0.3);
let json = serde_json::to_string(&a).unwrap();
let a2: Appraisal = serde_json::from_str(&json).unwrap();
assert!((a2.desirability - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_appraised_emotion_display() {
assert_eq!(AppraisedEmotion::Gratitude.to_string(), "gratitude");
assert_eq!(AppraisedEmotion::Anger.to_string(), "anger");
}
#[test]
fn test_mood_delta_clamped() {
let a = Appraisal::event("extreme", 1.0)
.with_praise(1.0)
.caused_by("x");
let r = appraise(&a, Some(1.0));
for &e in Emotion::ALL {
assert!(((-1.0)..=1.0).contains(&r.mood_delta.get(e)));
}
}
}