use std::collections::BTreeMap;
use std::fmt;
use serde::{Deserialize, Serialize};
use crate::clock::{WorldClock, WorldTick};
use crate::spatial::GeoHotspot;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum StateDomain {
Emergency,
Health,
Finance,
Trade,
Conflict,
Politics,
Weather,
Space,
Ocean,
Technology,
Personal,
Infrastructure,
Custom(String),
}
impl fmt::Display for StateDomain {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Custom(name) => write!(f, "{name}"),
other => fmt::Debug::fmt(other, f),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum Trend {
Rising,
Falling,
Stable,
Spike,
Crash,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StateLine {
pub domain: StateDomain,
pub activity: f32,
pub trend: Trend,
pub hotspots: Vec<GeoHotspot>,
pub last_updated: WorldTick,
#[serde(skip)]
pub activity_history: Vec<f32>,
}
impl StateLine {
pub fn new(domain: StateDomain) -> Self {
Self {
domain,
activity: 0.0,
trend: Trend::Stable,
hotspots: Vec::new(),
last_updated: WorldTick::zero(),
activity_history: Vec::new(),
}
}
pub fn update_activity(&mut self, sample: f32, tick: WorldTick) {
const ALPHA: f32 = 0.3;
self.activity = ALPHA * sample + (1.0 - ALPHA) * self.activity;
self.activity_history.push(self.activity);
if self.activity_history.len() > 60 {
self.activity_history.remove(0);
}
self.trend = self.detect_trend();
self.last_updated = tick;
}
fn detect_trend(&self) -> Trend {
let len = self.activity_history.len();
if len < 2 {
return Trend::Stable;
}
let window = len.min(3);
let start_idx = len - window;
let delta =
(self.activity_history[len - 1] - self.activity_history[start_idx]) / window as f32;
if delta > 0.15 {
Trend::Spike
} else if delta < -0.15 {
Trend::Crash
} else if delta > 0.03 {
Trend::Rising
} else if delta < -0.03 {
Trend::Falling
} else {
Trend::Stable
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WorldState {
pub clock: WorldClock,
pub state_lines: BTreeMap<StateDomain, StateLine>,
}
impl WorldState {
pub fn new(clock: WorldClock) -> Self {
let default_domains = [
StateDomain::Emergency,
StateDomain::Health,
StateDomain::Finance,
StateDomain::Trade,
StateDomain::Conflict,
StateDomain::Politics,
StateDomain::Weather,
StateDomain::Space,
StateDomain::Ocean,
StateDomain::Technology,
StateDomain::Personal,
StateDomain::Infrastructure,
];
let state_lines = default_domains
.into_iter()
.map(|d| {
let sl = StateLine::new(d.clone());
(d, sl)
})
.collect();
Self { clock, state_lines }
}
pub fn state_line_mut(&mut self, domain: &StateDomain) -> &mut StateLine {
self.state_lines
.entry(domain.clone())
.or_insert_with(|| StateLine::new(domain.clone()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn domain_display() {
assert_eq!(StateDomain::Finance.to_string(), "Finance");
assert_eq!(StateDomain::Custom("Crypto".into()).to_string(), "Crypto");
}
#[test]
fn initial_activity_zero() {
let sl = StateLine::new(StateDomain::Weather);
assert!((sl.activity - 0.0).abs() < f32::EPSILON);
}
#[test]
fn ema_smoothing() {
let mut sl = StateLine::new(StateDomain::Finance);
sl.update_activity(1.0, WorldTick(1));
assert!((sl.activity - 0.3).abs() < 1e-5, "got {}", sl.activity);
}
#[test]
fn spike_detection() {
let mut sl = StateLine::new(StateDomain::Finance);
for i in 0..10 {
sl.update_activity(0.1, WorldTick(i));
}
assert!(matches!(sl.trend, Trend::Stable));
sl.update_activity(1.0, WorldTick(10));
sl.update_activity(1.0, WorldTick(11));
assert_eq!(sl.trend, Trend::Spike);
}
#[test]
fn twelve_default_domains() {
let ws = WorldState::new(WorldClock::default());
assert_eq!(ws.state_lines.len(), 12);
}
#[test]
fn custom_domain_on_access() {
let mut ws = WorldState::new(WorldClock::default());
let domain = StateDomain::Custom("Crypto".into());
let sl = ws.state_line_mut(&domain);
assert_eq!(sl.domain, domain);
assert_eq!(ws.state_lines.len(), 13);
}
}