use crate::enums::Emotion;
use crate::state::{Hexaco, StateValue};
use crate::types::Duration;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct Mood {
valence: StateValue,
arousal: StateValue,
dominance: StateValue,
}
impl Mood {
const VALENCE_DECAY_HALF_LIFE: Duration = Duration::hours(6);
const AROUSAL_DECAY_HALF_LIFE: Duration = Duration::hours(6);
const DOMINANCE_DECAY_HALF_LIFE: Duration = Duration::hours(12);
#[must_use]
pub fn new() -> Self {
Mood {
valence: StateValue::new(0.0)
.with_bounds(-1.0, 1.0)
.with_decay_half_life(Self::VALENCE_DECAY_HALF_LIFE),
arousal: StateValue::new(0.0)
.with_bounds(-1.0, 1.0)
.with_decay_half_life(Self::AROUSAL_DECAY_HALF_LIFE),
dominance: StateValue::new(0.0)
.with_bounds(-1.0, 1.0)
.with_decay_half_life(Self::DOMINANCE_DECAY_HALF_LIFE),
}
}
#[must_use]
pub fn from_personality(hexaco: &Hexaco) -> Self {
const ATTENUATION: f32 = 0.3;
let valence_base = ATTENUATION * (hexaco.extraversion() * 0.6 - hexaco.neuroticism() * 0.6);
let arousal_base = ATTENUATION * (hexaco.neuroticism() * 0.4 + hexaco.openness() * 0.3);
let dominance_base = ATTENUATION * (hexaco.extraversion() * 0.5);
Mood {
valence: StateValue::new(valence_base)
.with_bounds(-1.0, 1.0)
.with_decay_half_life(Self::VALENCE_DECAY_HALF_LIFE),
arousal: StateValue::new(arousal_base)
.with_bounds(-1.0, 1.0)
.with_decay_half_life(Self::AROUSAL_DECAY_HALF_LIFE),
dominance: StateValue::new(dominance_base)
.with_bounds(-1.0, 1.0)
.with_decay_half_life(Self::DOMINANCE_DECAY_HALF_LIFE),
}
}
#[must_use]
pub fn with_valence_base(mut self, value: f32) -> Self {
self.valence.set_base(value);
self
}
#[must_use]
pub fn with_arousal_base(mut self, value: f32) -> Self {
self.arousal.set_base(value);
self
}
#[must_use]
pub fn with_dominance_base(mut self, value: f32) -> Self {
self.dominance.set_base(value);
self
}
#[must_use]
pub fn valence_effective(&self) -> f32 {
self.valence.effective()
}
#[must_use]
pub fn arousal_effective(&self) -> f32 {
self.arousal.effective()
}
#[must_use]
pub fn dominance_effective(&self) -> f32 {
self.dominance.effective()
}
#[must_use]
pub fn emotion_membership(&self) -> HashMap<Emotion, f64> {
Emotion::membership_from_pad(
self.valence_effective(),
self.arousal_effective(),
self.dominance_effective(),
)
}
#[must_use]
pub fn valence_base(&self) -> f32 {
self.valence.base()
}
#[must_use]
pub fn arousal_base(&self) -> f32 {
self.arousal.base()
}
#[must_use]
pub fn dominance_base(&self) -> f32 {
self.dominance.base()
}
#[must_use]
pub fn valence_delta(&self) -> f32 {
self.valence.delta()
}
#[must_use]
pub fn arousal_delta(&self) -> f32 {
self.arousal.delta()
}
#[must_use]
pub fn dominance_delta(&self) -> f32 {
self.dominance.delta()
}
#[must_use]
pub fn valence(&self) -> &StateValue {
&self.valence
}
#[must_use]
pub fn arousal(&self) -> &StateValue {
&self.arousal
}
#[must_use]
pub fn dominance(&self) -> &StateValue {
&self.dominance
}
pub fn valence_mut(&mut self) -> &mut StateValue {
&mut self.valence
}
pub fn arousal_mut(&mut self) -> &mut StateValue {
&mut self.arousal
}
pub fn dominance_mut(&mut self) -> &mut StateValue {
&mut self.dominance
}
pub fn shift_valence_base(&mut self, amount: f32) {
self.valence.shift_base(amount);
}
pub fn shift_arousal_base(&mut self, amount: f32) {
self.arousal.shift_base(amount);
}
pub fn shift_dominance_base(&mut self, amount: f32) {
self.dominance.shift_base(amount);
}
pub fn add_valence_delta(&mut self, amount: f32) {
self.valence.add_delta(amount);
}
pub fn add_arousal_delta(&mut self, amount: f32) {
self.arousal.add_delta(amount);
}
pub fn add_dominance_delta(&mut self, amount: f32) {
self.dominance.add_delta(amount);
}
pub fn add_valence_chronic_delta(&mut self, amount: f32) {
self.valence.add_chronic_delta(amount);
}
pub fn add_arousal_chronic_delta(&mut self, amount: f32) {
self.arousal.add_chronic_delta(amount);
}
pub fn add_dominance_chronic_delta(&mut self, amount: f32) {
self.dominance.add_chronic_delta(amount);
}
pub fn apply_decay(&mut self, elapsed: Duration) {
self.valence.apply_decay(elapsed);
self.arousal.apply_decay(elapsed);
self.dominance.apply_decay(elapsed);
}
pub fn reset_deltas(&mut self) {
self.valence.reset_delta();
self.arousal.reset_delta();
self.dominance.reset_delta();
}
}
impl Default for Mood {
fn default() -> Self {
Mood::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn mood_contains_only_pad_dimensions() {
let mood = Mood::new();
let _ = mood.valence_effective();
let _ = mood.arousal_effective();
let _ = mood.dominance_effective();
}
#[test]
fn new_creates_neutral_mood() {
let mood = Mood::new();
assert!((mood.valence_effective() - 0.0).abs() < f32::EPSILON);
assert!((mood.arousal_effective() - 0.0).abs() < f32::EPSILON);
assert!((mood.dominance_effective() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn valence_delta_decays_toward_zero() {
let mut mood = Mood::new();
mood.add_valence_delta(0.8);
mood.apply_decay(Duration::hours(6));
assert!((mood.valence_delta() - 0.4).abs() < 0.01);
}
#[test]
fn arousal_delta_decays_toward_zero() {
let mut mood = Mood::new();
mood.add_arousal_delta(0.6);
mood.apply_decay(Duration::hours(6));
assert!((mood.arousal_delta() - 0.3).abs() < 0.01);
}
#[test]
fn dominance_delta_decays_toward_zero() {
let mut mood = Mood::new();
mood.add_dominance_delta(0.4);
mood.apply_decay(Duration::hours(12));
assert!((mood.dominance_delta() - 0.2).abs() < 0.01);
}
#[test]
fn builder_methods_set_base_values() {
let mood = Mood::new()
.with_valence_base(0.3)
.with_arousal_base(-0.2)
.with_dominance_base(0.5);
assert!((mood.valence_base() - 0.3).abs() < f32::EPSILON);
assert!((mood.arousal_base() - (-0.2)).abs() < f32::EPSILON);
assert!((mood.dominance_base() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn add_delta_modifies_effective_value() {
let mut mood = Mood::new().with_valence_base(0.2);
mood.add_valence_delta(0.3);
assert!((mood.valence_effective() - 0.5).abs() < f32::EPSILON);
assert!((mood.valence_delta() - 0.3).abs() < f32::EPSILON);
}
#[test]
fn reset_deltas_clears_all() {
let mut mood = Mood::new();
mood.add_valence_delta(0.5);
mood.add_arousal_delta(0.3);
mood.add_dominance_delta(-0.2);
mood.reset_deltas();
assert!(mood.valence_delta().abs() < f32::EPSILON);
assert!(mood.arousal_delta().abs() < f32::EPSILON);
assert!(mood.dominance_delta().abs() < f32::EPSILON);
}
#[test]
fn emotion_membership_uses_effective_pad() {
let mood = Mood::new()
.with_valence_base(0.5)
.with_arousal_base(0.4)
.with_dominance_base(0.3);
let membership = mood.emotion_membership();
let total: f64 = membership.values().copied().sum();
assert!((total - 1.0).abs() < 1e-6);
}
#[test]
fn effective_values_are_clamped() {
let mut mood = Mood::new().with_valence_base(0.8);
mood.add_valence_delta(0.5);
assert!((mood.valence_effective() - 1.0).abs() < f32::EPSILON);
}
#[test]
fn negative_values_are_valid() {
let mut mood = Mood::new().with_valence_base(-0.3);
mood.add_valence_delta(-0.2);
assert!((mood.valence_effective() - (-0.5)).abs() < f32::EPSILON);
}
#[test]
fn state_value_references_accessible() {
let mut mood = Mood::new();
let _ = mood.valence();
let _ = mood.arousal();
let _ = mood.dominance();
mood.valence_mut().add_delta(0.1);
assert!((mood.valence_delta() - 0.1).abs() < f32::EPSILON);
}
#[test]
fn default_is_neutral() {
let mood = Mood::default();
assert!((mood.valence_effective() - 0.0).abs() < f32::EPSILON);
}
#[test]
fn clone_and_equality() {
let mood1 = Mood::new().with_valence_base(0.5);
let mood2 = mood1.clone();
assert_eq!(mood1, mood2);
}
#[test]
fn debug_format() {
let mood = Mood::new();
let debug = format!("{:?}", mood);
assert!(debug.contains("Mood"));
}
#[test]
fn decay_affects_all_dimensions() {
let mut mood = Mood::new();
mood.add_valence_delta(1.0);
mood.add_arousal_delta(1.0);
mood.add_dominance_delta(1.0);
mood.apply_decay(Duration::hours(24));
assert!(mood.valence_delta() < 0.2);
assert!(mood.arousal_delta() < 0.2);
assert!(mood.dominance_delta() < 0.3);
}
#[test]
fn all_base_accessors() {
let mood = Mood::new()
.with_valence_base(0.3)
.with_arousal_base(-0.2)
.with_dominance_base(0.4);
assert!((mood.valence_base() - 0.3).abs() < f32::EPSILON);
assert!((mood.arousal_base() - (-0.2)).abs() < f32::EPSILON);
assert!((mood.dominance_base() - 0.4).abs() < f32::EPSILON);
}
#[test]
fn all_mutable_refs() {
let mut mood = Mood::new();
mood.arousal_mut().add_delta(0.3);
mood.dominance_mut().add_delta(0.4);
assert!((mood.arousal_delta() - 0.3).abs() < f32::EPSILON);
assert!((mood.dominance_delta() - 0.4).abs() < f32::EPSILON);
}
#[test]
fn from_personality_with_high_extraversion() {
let hexaco = Hexaco::new().with_extraversion(0.8);
let mood = Mood::from_personality(&hexaco);
assert!(mood.valence_base() > 0.0);
assert!(mood.dominance_base() > 0.0);
}
#[test]
fn from_personality_with_high_neuroticism() {
let hexaco = Hexaco::new().with_neuroticism(0.8);
let mood = Mood::from_personality(&hexaco);
assert!(mood.valence_base() < 0.0);
assert!(mood.arousal_base() > 0.0);
}
#[test]
fn from_personality_balanced_produces_neutral() {
let hexaco = Hexaco::new();
let mood = Mood::from_personality(&hexaco);
assert!(mood.valence_base().abs() < 0.01);
assert!(mood.dominance_base().abs() < 0.01);
}
#[test]
fn from_personality_creates_modest_baselines() {
let extreme = Hexaco::new()
.with_extraversion(1.0)
.with_neuroticism(1.0)
.with_openness(1.0);
let mood = Mood::from_personality(&extreme);
assert!(mood.valence_base().abs() < 0.5);
assert!(mood.arousal_base().abs() < 0.5);
assert!(mood.dominance_base().abs() < 0.5);
}
}