1use std::collections::BTreeMap;
2use std::fmt;
3
4use serde::{Deserialize, Serialize};
5
6use crate::clock::{WorldClock, WorldTick};
7use crate::spatial::GeoHotspot;
8
9#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
11pub enum StateDomain {
12 Emergency,
13 Health,
14 Finance,
15 Trade,
16 Conflict,
17 Politics,
18 Weather,
19 Space,
20 Ocean,
21 Technology,
22 Personal,
23 Infrastructure,
24 Custom(String),
25}
26
27impl fmt::Display for StateDomain {
28 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
29 match self {
30 Self::Custom(name) => write!(f, "{name}"),
31 other => fmt::Debug::fmt(other, f),
32 }
33 }
34}
35
36#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
38pub enum Trend {
39 Rising,
40 Falling,
41 Stable,
42 Spike,
43 Crash,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct StateLine {
49 pub domain: StateDomain,
51 pub activity: f32,
53 pub trend: Trend,
55 pub hotspots: Vec<GeoHotspot>,
57 pub last_updated: WorldTick,
59 #[serde(skip)]
61 pub activity_history: Vec<f32>,
62}
63
64impl StateLine {
65 pub fn new(domain: StateDomain) -> Self {
67 Self {
68 domain,
69 activity: 0.0,
70 trend: Trend::Stable,
71 hotspots: Vec::new(),
72 last_updated: WorldTick::zero(),
73 activity_history: Vec::new(),
74 }
75 }
76
77 pub fn update_activity(&mut self, sample: f32, tick: WorldTick) {
80 const ALPHA: f32 = 0.3;
81 self.activity = ALPHA * sample + (1.0 - ALPHA) * self.activity;
82 self.activity_history.push(self.activity);
83 if self.activity_history.len() > 60 {
84 self.activity_history.remove(0);
85 }
86 self.trend = self.detect_trend();
87 self.last_updated = tick;
88 }
89
90 fn detect_trend(&self) -> Trend {
92 let len = self.activity_history.len();
93 if len < 2 {
94 return Trend::Stable;
95 }
96
97 let window = len.min(3);
99 let start_idx = len - window;
100 let delta =
101 (self.activity_history[len - 1] - self.activity_history[start_idx]) / window as f32;
102
103 if delta > 0.15 {
104 Trend::Spike
105 } else if delta < -0.15 {
106 Trend::Crash
107 } else if delta > 0.03 {
108 Trend::Rising
109 } else if delta < -0.03 {
110 Trend::Falling
111 } else {
112 Trend::Stable
113 }
114 }
115}
116
117#[derive(Debug, Clone, Serialize, Deserialize)]
119pub struct WorldState {
120 pub clock: WorldClock,
122 pub state_lines: BTreeMap<StateDomain, StateLine>,
124}
125
126impl WorldState {
127 pub fn new(clock: WorldClock) -> Self {
129 let default_domains = [
130 StateDomain::Emergency,
131 StateDomain::Health,
132 StateDomain::Finance,
133 StateDomain::Trade,
134 StateDomain::Conflict,
135 StateDomain::Politics,
136 StateDomain::Weather,
137 StateDomain::Space,
138 StateDomain::Ocean,
139 StateDomain::Technology,
140 StateDomain::Personal,
141 StateDomain::Infrastructure,
142 ];
143
144 let state_lines = default_domains
145 .into_iter()
146 .map(|d| {
147 let sl = StateLine::new(d.clone());
148 (d, sl)
149 })
150 .collect();
151
152 Self { clock, state_lines }
153 }
154
155 pub fn state_line_mut(&mut self, domain: &StateDomain) -> &mut StateLine {
158 self.state_lines
159 .entry(domain.clone())
160 .or_insert_with(|| StateLine::new(domain.clone()))
161 }
162}
163
164#[cfg(test)]
165mod tests {
166 use super::*;
167
168 #[test]
169 fn domain_display() {
170 assert_eq!(StateDomain::Finance.to_string(), "Finance");
171 assert_eq!(StateDomain::Custom("Crypto".into()).to_string(), "Crypto");
172 }
173
174 #[test]
175 fn initial_activity_zero() {
176 let sl = StateLine::new(StateDomain::Weather);
177 assert!((sl.activity - 0.0).abs() < f32::EPSILON);
178 }
179
180 #[test]
181 fn ema_smoothing() {
182 let mut sl = StateLine::new(StateDomain::Finance);
183 sl.update_activity(1.0, WorldTick(1));
184 assert!((sl.activity - 0.3).abs() < 1e-5, "got {}", sl.activity);
186 }
187
188 #[test]
189 fn spike_detection() {
190 let mut sl = StateLine::new(StateDomain::Finance);
191 for i in 0..10 {
193 sl.update_activity(0.1, WorldTick(i));
194 }
195 assert!(matches!(sl.trend, Trend::Stable));
196 sl.update_activity(1.0, WorldTick(10));
200 sl.update_activity(1.0, WorldTick(11));
201 assert_eq!(sl.trend, Trend::Spike);
202 }
203
204 #[test]
205 fn twelve_default_domains() {
206 let ws = WorldState::new(WorldClock::default());
207 assert_eq!(ws.state_lines.len(), 12);
208 }
209
210 #[test]
211 fn custom_domain_on_access() {
212 let mut ws = WorldState::new(WorldClock::default());
213 let domain = StateDomain::Custom("Crypto".into());
214 let sl = ws.state_line_mut(&domain);
215 assert_eq!(sl.domain, domain);
216 assert_eq!(ws.state_lines.len(), 13);
217 }
218}