use crate::types::Duration;
use serde::{Deserialize, Serialize};
const CHRONIC_HALF_LIFE_MULTIPLIER: u64 = 4;
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct StateValue {
base: f32,
delta: f32,
chronic_delta: f32,
decay_half_life: Option<Duration>,
min_bound: f32,
max_bound: f32,
feedback_loop_affected: bool,
}
impl StateValue {
#[must_use]
pub fn new(base: f32) -> Self {
StateValue {
base,
delta: 0.0,
chronic_delta: 0.0,
decay_half_life: Some(Duration::days(7)),
min_bound: 0.0,
max_bound: 1.0,
feedback_loop_affected: false,
}
}
#[must_use]
pub fn new_no_decay(base: f32) -> Self {
StateValue {
base,
delta: 0.0,
chronic_delta: 0.0,
decay_half_life: None,
min_bound: 0.0,
max_bound: 1.0,
feedback_loop_affected: false,
}
}
#[must_use]
pub fn with_bounds(mut self, min: f32, max: f32) -> Self {
self.min_bound = min;
self.max_bound = max;
self
}
pub fn set_bounds(&mut self, min: f32, max: f32) {
if min <= max {
self.min_bound = min;
self.max_bound = max;
} else {
self.min_bound = max;
self.max_bound = min;
}
}
#[must_use]
pub fn with_decay_half_life(mut self, half_life: Duration) -> Self {
self.decay_half_life = Some(half_life);
self
}
pub fn set_decay_half_life(&mut self, half_life: Duration) {
self.decay_half_life = Some(half_life);
}
#[must_use]
pub fn with_no_decay(mut self) -> Self {
self.decay_half_life = None;
self
}
#[must_use]
pub fn with_delta(mut self, delta: f32) -> Self {
self.delta = delta;
self.chronic_delta = 0.0;
self
}
#[must_use]
pub fn base(&self) -> f32 {
self.base
}
#[must_use]
pub fn delta(&self) -> f32 {
self.delta + self.chronic_delta
}
#[must_use]
pub fn decay_half_life(&self) -> Option<Duration> {
self.decay_half_life
}
#[must_use]
pub fn decays(&self) -> bool {
self.decay_half_life.is_some()
}
#[must_use]
pub fn is_feedback_loop_affected(&self) -> bool {
self.feedback_loop_affected
}
pub fn mark_feedback_loop_affected(&mut self) {
self.feedback_loop_affected = true;
}
pub fn clear_feedback_loop_affected(&mut self) {
self.feedback_loop_affected = false;
}
#[must_use]
pub fn effective(&self) -> f32 {
let total_delta = self.delta + self.chronic_delta;
(self.base + total_delta).clamp(self.min_bound, self.max_bound)
}
#[must_use]
pub fn effective_raw(&self) -> f32 {
self.base + self.delta + self.chronic_delta
}
pub fn set_base(&mut self, base: f32) {
self.base = base;
}
pub fn shift_base(&mut self, amount: f32) {
self.base += amount;
}
#[must_use]
pub fn chronic_delta(&self) -> f32 {
self.chronic_delta
}
pub fn add_delta(&mut self, amount: f32) {
self.delta += amount;
}
pub fn add_chronic_delta(&mut self, amount: f32) {
self.chronic_delta += amount;
}
pub fn set_delta(&mut self, delta: f32) {
self.delta = delta;
self.chronic_delta = 0.0;
}
pub fn apply_decay(&mut self, elapsed: Duration) {
let half_life = match self.decay_half_life {
Some(hl) => hl,
None => return,
};
if half_life.is_zero() || elapsed.is_zero() {
return;
}
let elapsed_seconds = elapsed.as_seconds() as f64;
let half_life_seconds = half_life.as_seconds() as f64;
let decay_factor = 0.5_f64.powf(elapsed_seconds / half_life_seconds);
self.delta *= decay_factor as f32;
let chronic_half_life = half_life * CHRONIC_HALF_LIFE_MULTIPLIER;
let chronic_half_life_seconds = chronic_half_life.as_seconds() as f64;
let chronic_decay_factor = 0.5_f64.powf(elapsed_seconds / chronic_half_life_seconds);
self.chronic_delta *= chronic_decay_factor as f32;
}
pub fn reset_delta(&mut self) {
self.delta = 0.0;
self.chronic_delta = 0.0;
}
}
impl Default for StateValue {
fn default() -> Self {
StateValue::new(0.5)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn current_returns_base_plus_delta() {
let value = StateValue::new(0.5).with_delta(0.3);
let effective = value.effective();
assert!((effective - 0.8).abs() < f32::EPSILON);
}
#[test]
fn current_clamps_to_max() {
let value = StateValue::new(0.8).with_delta(0.5);
let effective = value.effective();
assert!((effective - 1.0).abs() < f32::EPSILON);
assert!((value.effective_raw() - 1.3).abs() < f32::EPSILON);
}
#[test]
fn current_clamps_to_min() {
let value = StateValue::new(0.2).with_delta(-0.5);
let effective = value.effective();
assert!(effective.abs() < f32::EPSILON);
}
#[test]
fn add_delta_accumulates() {
let mut value = StateValue::new(0.5);
value.add_delta(0.1);
assert!((value.delta() - 0.1).abs() < f32::EPSILON);
value.add_delta(0.2);
assert!((value.delta() - 0.3).abs() < f32::EPSILON);
value.add_delta(-0.1);
assert!((value.delta() - 0.2).abs() < f32::EPSILON);
}
#[test]
fn add_chronic_delta_accumulates() {
let mut value = StateValue::new(0.5);
value.add_chronic_delta(0.3);
assert!((value.delta() - 0.3).abs() < f32::EPSILON);
value.add_chronic_delta(0.2);
assert!((value.delta() - 0.5).abs() < f32::EPSILON);
}
#[test]
fn chronic_delta_decays_slower_than_acute() {
let mut value = StateValue::new(0.5)
.with_decay_half_life(Duration::days(1))
.with_delta(0.4);
value.add_chronic_delta(0.4);
value.apply_decay(Duration::days(1));
let acute_expected = 0.4 * 0.5;
let chronic_decay = 0.5_f64.powf(1.0 / 4.0) as f32;
let chronic_expected = 0.4 * chronic_decay;
let expected_total = acute_expected + chronic_expected;
assert!((value.delta() - expected_total).abs() < 0.01);
}
#[test]
fn state_value_decay_is_species_independent() {
let value1 = StateValue::new(0.5)
.with_delta(0.4)
.with_decay_half_life(Duration::days(3));
let value2 = StateValue::new(0.5)
.with_delta(0.4)
.with_decay_half_life(Duration::days(3));
assert_eq!(value1.decay_half_life(), value2.decay_half_life());
assert_eq!(value1.decay_half_life().unwrap().as_days(), 3);
}
#[test]
fn decay_halves_delta_after_half_life() {
let mut value = StateValue::new(0.5)
.with_delta(0.4)
.with_decay_half_life(Duration::days(3));
value.apply_decay(Duration::days(3));
assert!((value.delta() - 0.2).abs() < 0.01);
}
#[test]
fn decay_quarter_after_two_half_lives() {
let mut value = StateValue::new(0.5)
.with_delta(0.4)
.with_decay_half_life(Duration::days(3));
value.apply_decay(Duration::days(6));
assert!((value.delta() - 0.1).abs() < 0.01);
}
#[test]
fn decay_with_zero_elapsed() {
let mut value = StateValue::new(0.5).with_delta(0.4);
let original_delta = value.delta();
value.apply_decay(Duration::zero());
assert!((value.delta() - original_delta).abs() < f32::EPSILON);
}
#[test]
fn decay_with_zero_half_life() {
let mut value = StateValue::new(0.5)
.with_delta(0.4)
.with_decay_half_life(Duration::zero());
let original_delta = value.delta();
value.apply_decay(Duration::days(1));
assert!((value.delta() - original_delta).abs() < f32::EPSILON);
}
#[test]
fn reset_delta() {
let mut value = StateValue::new(0.5).with_delta(0.4);
value.reset_delta();
assert!(value.delta().abs() < f32::EPSILON);
}
#[test]
fn set_base() {
let mut value = StateValue::new(0.5);
value.set_base(0.7);
assert!((value.base() - 0.7).abs() < f32::EPSILON);
}
#[test]
fn custom_bounds() {
let value = StateValue::new(0.0).with_bounds(-1.0, 1.0).with_delta(0.5);
assert!((value.effective() - 0.5).abs() < f32::EPSILON);
let negative = StateValue::new(0.0).with_bounds(-1.0, 1.0).with_delta(-0.5);
assert!((negative.effective() - (-0.5)).abs() < f32::EPSILON);
}
#[test]
fn set_bounds_updates_limits() {
let mut value = StateValue::new(0.0).with_bounds(-1.0, 1.0).with_delta(2.0);
assert!((value.effective() - 1.0).abs() < f32::EPSILON);
value.set_bounds(-0.2, 0.2);
assert!((value.effective() - 0.2).abs() < f32::EPSILON);
}
#[test]
fn set_bounds_swaps_when_min_exceeds_max() {
let mut value = StateValue::new(0.0).with_delta(0.5);
value.set_bounds(0.3, -0.3);
assert!((value.effective() - 0.3).abs() < f32::EPSILON);
}
#[test]
fn default_state_value() {
let value = StateValue::default();
assert!((value.base() - 0.5).abs() < f32::EPSILON);
assert!(value.delta().abs() < f32::EPSILON);
}
#[test]
fn clone_and_equality() {
let value1 = StateValue::new(0.5).with_delta(0.2);
let value2 = value1.clone();
assert_eq!(value1, value2);
}
#[test]
fn set_delta_directly() {
let mut value = StateValue::new(0.5);
value.set_delta(0.3);
assert!((value.delta() - 0.3).abs() < f32::EPSILON);
value.set_delta(-0.2);
assert!((value.delta() - (-0.2)).abs() < f32::EPSILON);
}
#[test]
fn effective_raw_not_clamped() {
let value = StateValue::new(0.9).with_delta(0.5);
assert!((value.effective() - 1.0).abs() < f32::EPSILON);
assert!((value.effective_raw() - 1.4).abs() < f32::EPSILON);
}
#[test]
fn decay_half_life_accessor() {
let value = StateValue::new(0.5).with_decay_half_life(Duration::days(5));
assert_eq!(value.decay_half_life().unwrap().as_days(), 5);
}
#[test]
fn no_decay_value_never_decays() {
let mut value = StateValue::new_no_decay(0.0).with_delta(0.5);
value.apply_decay(Duration::years(100));
assert!((value.delta() - 0.5).abs() < f32::EPSILON);
assert!(!value.decays());
assert!(value.decay_half_life().is_none());
}
#[test]
fn with_no_decay_removes_half_life() {
let value = StateValue::new(0.5)
.with_decay_half_life(Duration::days(7))
.with_no_decay();
assert!(value.decay_half_life().is_none());
assert!(!value.decays());
}
#[test]
fn decays_returns_true_for_decaying_value() {
let value = StateValue::new(0.5);
assert!(value.decays());
assert!(value.decay_half_life().is_some());
}
#[test]
fn negative_delta() {
let value = StateValue::new(0.5).with_delta(-0.2);
assert!((value.effective() - 0.3).abs() < f32::EPSILON);
}
#[test]
fn debug_format() {
let value = StateValue::new(0.5);
let debug = format!("{:?}", value);
assert!(debug.contains("StateValue"));
}
#[test]
fn base_accessor() {
let value = StateValue::new(0.7);
assert!((value.base() - 0.7).abs() < f32::EPSILON);
}
#[test]
fn new_value_not_feedback_affected() {
let value = StateValue::new(0.5);
assert!(!value.is_feedback_loop_affected());
}
#[test]
fn mark_feedback_loop_affected() {
let mut value = StateValue::new(0.5);
value.mark_feedback_loop_affected();
assert!(value.is_feedback_loop_affected());
}
#[test]
fn clear_feedback_loop_affected() {
let mut value = StateValue::new(0.5);
value.mark_feedback_loop_affected();
assert!(value.is_feedback_loop_affected());
value.clear_feedback_loop_affected();
assert!(!value.is_feedback_loop_affected());
}
#[test]
fn feedback_flag_preserved_in_clone() {
let mut value = StateValue::new(0.5);
value.mark_feedback_loop_affected();
let cloned = value.clone();
assert!(cloned.is_feedback_loop_affected());
}
#[test]
fn feedback_flag_in_equality() {
let mut value1 = StateValue::new(0.5);
let mut value2 = StateValue::new(0.5);
assert_eq!(value1, value2);
value1.mark_feedback_loop_affected();
assert_ne!(value1, value2);
value2.mark_feedback_loop_affected();
assert_eq!(value1, value2);
}
#[test]
fn no_decay_value_not_feedback_affected() {
let value = StateValue::new_no_decay(0.5);
assert!(!value.is_feedback_loop_affected());
}
#[test]
fn set_decay_half_life() {
let mut value = StateValue::new_no_decay(0.5);
assert!(value.decay_half_life().is_none());
value.set_decay_half_life(Duration::days(5));
assert_eq!(value.decay_half_life().unwrap().as_days(), 5);
assert!(value.decays());
}
}