use crate::state::Mood;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct EmotionalSnapshot {
valence: f32,
arousal: f32,
dominance: f32,
}
impl EmotionalSnapshot {
#[must_use]
pub fn new(valence: f32, arousal: f32, dominance: f32) -> Self {
EmotionalSnapshot {
valence: valence.clamp(-1.0, 1.0),
arousal: arousal.clamp(-1.0, 1.0),
dominance: dominance.clamp(-1.0, 1.0),
}
}
#[must_use]
pub fn from_mood(mood: &Mood) -> Self {
EmotionalSnapshot {
valence: mood.valence_effective(),
arousal: mood.arousal_effective(),
dominance: mood.dominance_effective(),
}
}
#[must_use]
pub fn neutral() -> Self {
EmotionalSnapshot {
valence: 0.0,
arousal: 0.0,
dominance: 0.0,
}
}
#[must_use]
pub fn valence(&self) -> f32 {
self.valence
}
#[must_use]
pub fn arousal(&self) -> f32 {
self.arousal
}
#[must_use]
pub fn dominance(&self) -> f32 {
self.dominance
}
#[must_use]
pub fn compute_congruence(&self, valence: f32, arousal: f32, _dominance: f32) -> f32 {
const VALENCE_WEIGHT: f32 = 0.70;
const AROUSAL_WEIGHT: f32 = 0.30;
let valence_match = (1.0 - (self.valence - valence).abs()).clamp(0.0, 1.0);
let arousal_match = (1.0 - (self.arousal - arousal).abs()).clamp(0.0, 1.0);
valence_match * VALENCE_WEIGHT + arousal_match * AROUSAL_WEIGHT
}
#[must_use]
pub fn compute_congruence_with_mood(&self, mood: &Mood) -> f32 {
self.compute_congruence(
mood.valence_effective(),
mood.arousal_effective(),
mood.dominance_effective(),
)
}
}
impl Default for EmotionalSnapshot {
fn default() -> Self {
EmotionalSnapshot::neutral()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn emotional_snapshot_captures_pad() {
let snapshot = EmotionalSnapshot::new(0.5, 0.3, -0.2);
assert!((snapshot.valence() - 0.5).abs() < f32::EPSILON);
assert!((snapshot.arousal() - 0.3).abs() < f32::EPSILON);
assert!((snapshot.dominance() - (-0.2)).abs() < f32::EPSILON);
}
#[test]
fn emotional_snapshot_bounds_enforced() {
let snapshot = EmotionalSnapshot::new(1.5, -1.5, 2.0);
assert!((snapshot.valence() - 1.0).abs() < f32::EPSILON);
assert!((snapshot.arousal() - (-1.0)).abs() < f32::EPSILON);
assert!((snapshot.dominance() - 1.0).abs() < f32::EPSILON);
let snapshot2 = EmotionalSnapshot::new(-2.0, -2.0, -2.0);
assert!((snapshot2.valence() - (-1.0)).abs() < f32::EPSILON);
assert!((snapshot2.arousal() - (-1.0)).abs() < f32::EPSILON);
assert!((snapshot2.dominance() - (-1.0)).abs() < f32::EPSILON);
}
#[test]
fn from_mood_captures_effective_values() {
let mut mood = Mood::new().with_valence_base(0.3);
mood.add_valence_delta(0.2);
mood.add_arousal_delta(0.4);
mood.add_dominance_delta(-0.1);
let snapshot = EmotionalSnapshot::from_mood(&mood);
assert!((snapshot.valence() - 0.5).abs() < f32::EPSILON);
assert!((snapshot.arousal() - 0.4).abs() < f32::EPSILON);
assert!((snapshot.dominance() - (-0.1)).abs() < f32::EPSILON);
}
#[test]
fn neutral_is_all_zeros() {
let snapshot = EmotionalSnapshot::neutral();
assert!(snapshot.valence().abs() < f32::EPSILON);
assert!(snapshot.arousal().abs() < f32::EPSILON);
assert!(snapshot.dominance().abs() < f32::EPSILON);
}
#[test]
fn default_is_neutral() {
let snapshot = EmotionalSnapshot::default();
assert!(snapshot.valence().abs() < f32::EPSILON);
assert!(snapshot.arousal().abs() < f32::EPSILON);
assert!(snapshot.dominance().abs() < f32::EPSILON);
}
#[test]
fn compute_congruence_perfect_match() {
let snapshot = EmotionalSnapshot::new(0.5, 0.3, -0.2);
let congruence = snapshot.compute_congruence(0.5, 0.3, -0.2);
assert!((congruence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_congruence_uses_pad_weights_70_30() {
let snapshot = EmotionalSnapshot::new(1.0, 1.0, 1.0);
let congruence_valence_diff = snapshot.compute_congruence(-1.0, 1.0, 1.0);
assert!((congruence_valence_diff - 0.30).abs() < 0.01);
let congruence_arousal_diff = snapshot.compute_congruence(1.0, -1.0, 1.0);
assert!((congruence_arousal_diff - 0.70).abs() < 0.01);
let congruence_dominance_diff = snapshot.compute_congruence(1.0, 1.0, -1.0);
assert!((congruence_dominance_diff - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_congruence_max_difference() {
let snapshot = EmotionalSnapshot::new(1.0, 1.0, 1.0);
let congruence = snapshot.compute_congruence(-1.0, -1.0, -1.0);
assert!(congruence.abs() < f32::EPSILON);
}
#[test]
fn compute_congruence_with_mood() {
let snapshot = EmotionalSnapshot::new(0.5, 0.3, -0.2);
let mood = Mood::new()
.with_valence_base(0.5)
.with_arousal_base(0.3)
.with_dominance_base(-0.2);
let congruence = snapshot.compute_congruence_with_mood(&mood);
assert!((congruence - 1.0).abs() < f32::EPSILON);
}
#[test]
fn compute_congruence_partial_match() {
let snapshot = EmotionalSnapshot::new(0.5, 0.5, 0.5);
let congruence = snapshot.compute_congruence(0.0, 0.0, 0.0);
assert!((congruence - 0.5).abs() < 0.01);
}
#[test]
fn clone_and_copy() {
let snapshot = EmotionalSnapshot::new(0.5, 0.3, -0.2);
let cloned = snapshot.clone();
let copied = snapshot;
assert_eq!(snapshot, cloned);
assert_eq!(snapshot, copied);
}
#[test]
fn equality() {
let snapshot1 = EmotionalSnapshot::new(0.5, 0.3, -0.2);
let snapshot2 = EmotionalSnapshot::new(0.5, 0.3, -0.2);
let snapshot3 = EmotionalSnapshot::new(0.5, 0.3, 0.0);
assert_eq!(snapshot1, snapshot2);
assert_ne!(snapshot1, snapshot3);
}
#[test]
fn debug_format() {
let snapshot = EmotionalSnapshot::new(0.5, 0.3, -0.2);
let debug = format!("{:?}", snapshot);
assert!(debug.contains("EmotionalSnapshot"));
assert!(debug.contains("valence"));
assert!(debug.contains("arousal"));
assert!(debug.contains("dominance"));
}
}