use serde::{Deserialize, Serialize};
use crate::mood::MoodVector;
use crate::types::{Normalized01, ThresholdClassifier};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnergyState {
pub energy: Normalized01,
pub fitness: f32,
pub fatigue: f32,
pub recovery_rate: f32,
pub drain_rate: f32,
pub fitness_tau: f32,
pub fatigue_tau: f32,
pub fitness_gain: f32,
pub fatigue_gain: f32,
}
impl Default for EnergyState {
fn default() -> Self {
Self {
energy: Normalized01::ONE,
fitness: 0.0,
fatigue: 0.0,
recovery_rate: 0.03,
drain_rate: 0.02,
fitness_tau: 60.0,
fatigue_tau: 15.0,
fitness_gain: 0.01,
fatigue_gain: 0.03,
}
}
}
impl EnergyState {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn tick(&mut self, exertion: f32) {
let exertion = exertion.clamp(0.0, 1.0);
if self.fitness_tau > 0.0 {
self.fitness =
self.fitness * (-1.0 / self.fitness_tau).exp() + self.fitness_gain * exertion;
}
if self.fatigue_tau > 0.0 {
self.fatigue =
self.fatigue * (-1.0 / self.fatigue_tau).exp() + self.fatigue_gain * exertion;
}
let mut e = self.energy.get();
if exertion > 0.1 {
e -= exertion * self.drain_rate;
} else {
e += self.recovery_rate * (1.0 - exertion);
}
self.energy = Normalized01::new(e);
self.fitness = self.fitness.clamp(0.0, 5.0);
self.fatigue = self.fatigue.clamp(0.0, 5.0);
}
#[must_use]
#[inline]
pub fn performance(&self) -> f32 {
let raw = self.fitness - self.fatigue;
1.0 / (1.0 + (-4.0 * raw).exp())
}
#[must_use]
pub fn level(&self) -> EnergyLevel {
const CLASSIFIER: ThresholdClassifier<EnergyLevel> = ThresholdClassifier::new(
&[
(0.9, EnergyLevel::Full),
(0.6, EnergyLevel::High),
(0.3, EnergyLevel::Moderate),
(0.1, EnergyLevel::Low),
],
EnergyLevel::Depleted,
);
CLASSIFIER.classify(self.energy.get())
}
#[must_use]
#[inline]
pub fn can_enter_flow(&self) -> bool {
self.energy.get() >= 0.3
}
#[must_use]
#[inline]
pub fn regulation_effectiveness(&self) -> f32 {
0.5 + self.energy.get() * 0.5
}
#[must_use]
#[inline]
pub fn is_depleted(&self) -> bool {
self.energy.get() < 0.1
}
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
pub fn apply_recovery_modifier(&mut self, modifier: f32) {
let bonus = self.recovery_rate * (modifier - 1.0).max(0.0);
self.energy = Normalized01::new(self.energy.get() + bonus);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum EnergyLevel {
Depleted,
Low,
Moderate,
High,
Full,
}
impl_display!(EnergyLevel {
Depleted => "depleted",
Low => "low",
Moderate => "moderate",
High => "high",
Full => "full",
});
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
#[inline]
pub fn exertion_from_mood(mood: &MoodVector) -> f32 {
let max_intensity = (crate::mood::Emotion::ALL.len() as f32).sqrt();
(mood.intensity() / max_intensity).clamp(0.0, 1.0)
}
#[cfg(feature = "traits")]
#[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
#[must_use]
pub fn energy_from_personality(profile: &crate::traits::PersonalityProfile) -> EnergyState {
use crate::traits::TraitKind;
let patience = profile.get_trait(TraitKind::Patience).normalized();
let confidence = profile.get_trait(TraitKind::Confidence).normalized();
let curiosity = profile.get_trait(TraitKind::Curiosity).normalized();
let resilience = (patience + confidence) / 2.0;
EnergyState {
recovery_rate: (0.03 + resilience * 0.01).clamp(0.01, 0.06),
drain_rate: (0.02 + curiosity * 0.005).clamp(0.01, 0.04),
..EnergyState::default()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mood::Emotion;
#[test]
fn test_energy_new() {
let e = EnergyState::new();
assert!((e.energy.get() - 1.0).abs() < f32::EPSILON);
assert_eq!(e.level(), EnergyLevel::Full);
}
#[test]
fn test_tick_rest_recovers() {
let mut e = EnergyState::new();
e.energy = Normalized01::new(0.5);
e.tick(0.0); assert!(e.energy.get() > 0.5, "rest should recover: {}", e.energy);
}
#[test]
fn test_tick_exertion_drains() {
let mut e = EnergyState::new();
e.tick(0.8);
assert!(e.energy.get() < 1.0, "exertion should drain: {}", e.energy);
}
#[test]
fn test_energy_clamped() {
let mut e = EnergyState::new();
for _ in 0..200 {
e.tick(1.0);
}
assert!(e.energy.get() >= 0.0);
assert!(e.energy.get() <= 1.0);
for _ in 0..200 {
e.tick(0.0);
}
assert!(e.energy.get() <= 1.0);
}
#[test]
fn test_banister_fitness_builds() {
let mut e = EnergyState::new();
for _ in 0..20 {
e.tick(0.8);
}
assert!(e.fitness > 0.0, "fitness should build: {}", e.fitness);
}
#[test]
fn test_banister_fatigue_builds_faster() {
let mut e = EnergyState::new();
for _ in 0..10 {
e.tick(0.8);
}
assert!(
e.fatigue > e.fitness,
"fatigue={} should exceed fitness={}",
e.fatigue,
e.fitness
);
}
#[test]
fn test_fatigue_decays_faster_than_fitness() {
let mut e = EnergyState::new();
for _ in 0..20 {
e.tick(0.8);
}
let fitness_after_exertion = e.fitness;
let fatigue_after_exertion = e.fatigue;
for _ in 0..30 {
e.tick(0.0);
}
let fitness_decay_ratio = e.fitness / fitness_after_exertion;
let fatigue_decay_ratio = e.fatigue / fatigue_after_exertion;
assert!(
fatigue_decay_ratio < fitness_decay_ratio,
"fatigue_ratio={} fitness_ratio={}",
fatigue_decay_ratio,
fitness_decay_ratio
);
}
#[test]
fn test_performance_sigmoid_range() {
let mut e = EnergyState::new();
assert!(e.performance() >= 0.0 && e.performance() <= 1.0);
assert!(
(e.performance() - 0.5).abs() < f32::EPSILON,
"zero state: {}",
e.performance()
);
for _ in 0..50 {
e.tick(1.0);
}
assert!(e.performance() < 0.5, "overreached: {}", e.performance());
}
#[test]
fn test_supercompensation() {
let mut e = EnergyState::new();
for _ in 0..20 {
e.tick(0.5);
}
for _ in 0..100 {
e.tick(0.0);
}
assert!(
e.performance() > 0.5,
"supercompensation should raise performance: {}",
e.performance()
);
}
#[test]
fn test_can_enter_flow() {
let mut e = EnergyState::new();
assert!(e.can_enter_flow());
e.energy = Normalized01::new(0.2);
assert!(!e.can_enter_flow());
e.energy = Normalized01::new(0.3);
assert!(e.can_enter_flow());
}
#[test]
fn test_regulation_effectiveness() {
let mut e = EnergyState::new();
assert!((e.regulation_effectiveness() - 1.0).abs() < f32::EPSILON);
e.energy = Normalized01::ZERO;
assert!((e.regulation_effectiveness() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn test_is_depleted() {
let mut e = EnergyState::new();
assert!(!e.is_depleted());
e.energy = Normalized01::new(0.05);
assert!(e.is_depleted());
}
#[test]
fn test_level_classification() {
let mut e = EnergyState::new();
e.energy = Normalized01::new(0.05);
assert_eq!(e.level(), EnergyLevel::Depleted);
e.energy = Normalized01::new(0.2);
assert_eq!(e.level(), EnergyLevel::Low);
e.energy = Normalized01::new(0.5);
assert_eq!(e.level(), EnergyLevel::Moderate);
e.energy = Normalized01::new(0.8);
assert_eq!(e.level(), EnergyLevel::High);
e.energy = Normalized01::new(0.95);
assert_eq!(e.level(), EnergyLevel::Full);
}
#[test]
fn test_exertion_from_mood_neutral() {
let mood = MoodVector::neutral();
assert!(exertion_from_mood(&mood) < f32::EPSILON);
}
#[test]
fn test_exertion_from_mood_intense() {
let mut mood = MoodVector::neutral();
mood.set(Emotion::Joy, 0.9);
mood.set(Emotion::Arousal, 0.8);
let ex = exertion_from_mood(&mood);
assert!(ex > 0.3, "intense mood exertion: {ex}");
assert!(ex <= 1.0);
}
#[test]
fn test_apply_recovery_modifier() {
let mut e = EnergyState::new();
e.energy = Normalized01::new(0.5);
e.apply_recovery_modifier(1.5);
assert!(
e.energy.get() > 0.5,
"modifier > 1 should boost: {}",
e.energy
);
}
#[test]
fn test_apply_recovery_modifier_no_drain() {
let mut e = EnergyState::new();
e.energy = Normalized01::new(0.5);
let before = e.energy.get();
e.apply_recovery_modifier(0.5); assert!(
(e.energy.get() - before).abs() < f32::EPSILON,
"modifier < 1 should not drain"
);
}
#[test]
fn test_zero_tau_safe() {
let mut e = EnergyState::new();
e.fitness_tau = 0.0;
e.fatigue_tau = 0.0;
e.tick(0.5); assert!(e.fitness.is_finite());
assert!(e.fatigue.is_finite());
}
#[test]
fn test_negative_tau_safe() {
let mut e = EnergyState::new();
e.fitness_tau = -10.0;
e.fatigue_tau = -5.0;
e.tick(0.8);
assert!(e.fitness <= 5.0, "fitness clamped: {}", e.fitness);
assert!(e.fatigue <= 5.0, "fatigue clamped: {}", e.fatigue);
assert!(e.fitness.is_finite());
assert!(e.fatigue.is_finite());
}
#[test]
fn test_energy_level_display() {
assert_eq!(EnergyLevel::Depleted.to_string(), "depleted");
assert_eq!(EnergyLevel::Full.to_string(), "full");
}
#[test]
fn test_serde() {
let e = EnergyState::new();
let json = serde_json::to_string(&e).unwrap();
let e2: EnergyState = serde_json::from_str(&json).unwrap();
assert!((e2.energy.get() - e.energy.get()).abs() < f32::EPSILON);
}
#[cfg(feature = "traits")]
#[test]
fn test_energy_from_personality() {
let mut patient = crate::traits::PersonalityProfile::new("patient");
patient.set_trait(
crate::traits::TraitKind::Patience,
crate::traits::TraitLevel::Highest,
);
patient.set_trait(
crate::traits::TraitKind::Confidence,
crate::traits::TraitLevel::Highest,
);
let e = energy_from_personality(&patient);
assert!(e.recovery_rate > 0.03, "patient should recover faster");
let mut curious = crate::traits::PersonalityProfile::new("curious");
curious.set_trait(
crate::traits::TraitKind::Curiosity,
crate::traits::TraitLevel::Highest,
);
let e2 = energy_from_personality(&curious);
assert!(e2.drain_rate > 0.02, "curious should drain faster");
}
}