1use serde::{Deserialize, Serialize};
12use std::collections::VecDeque;
13
14#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct AgentState {
17 pub id: String,
18 pub room_id: String,
19 pub vibe: f64,
20 pub confidence: f64,
21 pub phase: AgentPhase,
22 pub readings_seen: usize,
23 pub predictions_made: usize,
24 pub accuracy: f64,
25 pub energy: f64, }
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
29pub enum AgentPhase {
30 Gestating,
31 Forming,
32 Maturing,
33 Stable,
34 Dissolving,
35 Dissolved,
36}
37
38#[derive(Debug, Clone, Serialize, Deserialize)]
40pub struct CharacterAppearance {
41 pub name: String,
42 pub body_color: [f64; 3],
43 pub glow_intensity: f64,
44 pub scale: f64,
45 pub opacity: f64,
46 pub animation: CharacterAnimation,
47 pub accessories: Vec<String>,
48 pub expression: String,
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
52pub enum CharacterAnimation {
53 Idle,
54 Exploring,
55 Thinking,
56 Confident,
57 Celebrating,
58 Confused,
59 Fading,
60 Ghost,
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct CharacterAction {
66 pub agent_id: String,
67 pub kind: ActionKind,
68 pub target: Option<String>,
69 pub params: Vec<f64>,
70}
71
72#[derive(Debug, Clone, Serialize, Deserialize)]
73pub enum ActionKind {
74 Move,
75 Speak,
76 Build,
77 Observe,
78 Teach,
79 Celebrate,
80 Emote,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct AgentShell {
86 pub state: AgentState,
87 pub appearance: CharacterAppearance,
88 pub action_queue: VecDeque<CharacterAction>,
89 pub personality_traits: Vec<String>,
90}
91
92impl AgentShell {
93 pub fn new(id: &str, room_id: &str) -> Self {
94 let state = AgentState {
95 id: id.into(),
96 room_id: room_id.into(),
97 vibe: 0.0,
98 confidence: 0.0,
99 phase: AgentPhase::Gestating,
100 readings_seen: 0,
101 predictions_made: 0,
102 accuracy: 0.0,
103 energy: 0.5,
104 };
105 let appearance = CharacterAppearance {
106 name: id.into(),
107 body_color: [0.3, 0.3, 0.3],
108 glow_intensity: 0.0,
109 scale: 1.0,
110 opacity: 1.0,
111 animation: CharacterAnimation::Idle,
112 accessories: Vec::new(),
113 expression: "wondering".into(),
114 };
115 Self {
116 state,
117 appearance,
118 action_queue: VecDeque::new(),
119 personality_traits: vec!["curious".into()],
120 }
121 }
122
123 pub fn observe(&mut self, value: f64, confidence: f64) {
125 self.state.vibe = value;
126 self.state.confidence = confidence;
127 self.state.readings_seen += 1;
128 self.state.energy = (self.state.energy + 0.05).min(1.0);
129
130 if self.state.readings_seen == 1 {
132 self.state.phase = AgentPhase::Forming;
133 } else if self.state.readings_seen >= 5 && self.state.confidence > 0.5 {
134 self.state.phase = AgentPhase::Maturing;
135 } else if self.state.readings_seen >= 20 && self.state.accuracy > 0.8 {
136 self.state.phase = AgentPhase::Stable;
137 }
138
139 self.sync_appearance();
140
141 if self.state.readings_seen == 1 {
143 self.action_queue.push_back(CharacterAction {
144 agent_id: self.state.id.clone(),
145 kind: ActionKind::Observe,
146 target: Some(self.state.room_id.clone()),
147 params: vec![value],
148 });
149 }
150 }
151
152 pub fn predict(&mut self, predicted: f64, actual: f64) {
154 self.state.predictions_made += 1;
155 let error = (predicted - actual).abs();
156 self.state.accuracy = if self.state.predictions_made == 1 {
157 1.0 - error.min(1.0)
158 } else {
159 self.state.accuracy * 0.9 + (1.0 - error.min(1.0)) * 0.1
160 };
161
162 if error < 0.1 {
164 self.action_queue.push_back(CharacterAction {
165 agent_id: self.state.id.clone(),
166 kind: ActionKind::Celebrate,
167 target: None,
168 params: vec![error],
169 });
170 } else if error > 0.5 {
171 self.action_queue.push_back(CharacterAction {
172 agent_id: self.state.id.clone(),
173 kind: ActionKind::Emote,
174 target: None,
175 params: vec![error],
176 });
177 }
178
179 self.sync_appearance();
180 }
181
182 pub fn dissolve(&mut self) {
184 self.state.phase = AgentPhase::Dissolving;
185 self.sync_appearance();
186 self.action_queue.push_back(CharacterAction {
187 agent_id: self.state.id.clone(),
188 kind: ActionKind::Speak,
189 target: None,
190 params: vec![],
191 });
192 }
193
194 pub fn finish_dissolve(&mut self) {
196 self.state.phase = AgentPhase::Dissolved;
197 self.sync_appearance();
198 }
199
200 pub fn speak(&mut self, text: &str) {
202 self.action_queue.push_back(CharacterAction {
203 agent_id: self.state.id.clone(),
204 kind: ActionKind::Speak,
205 target: None,
206 params: vec![],
207 });
208 }
209
210 pub fn teach(&mut self, target_id: &str, knowledge: f64) {
212 self.action_queue.push_back(CharacterAction {
213 agent_id: self.state.id.clone(),
214 kind: ActionKind::Teach,
215 target: Some(target_id.into()),
216 params: vec![knowledge],
217 });
218 }
219
220 fn sync_appearance(&mut self) {
222 let t = ((self.state.vibe + 1.0) / 2.0).clamp(0.0, 1.0);
224 let hue = (1.0 - t) * 0.66; self.appearance.body_color = hue_to_rgb(hue);
226
227 self.appearance.glow_intensity = self.state.confidence;
229
230 self.appearance.scale = 0.5 + self.state.energy * 0.5;
232
233 self.appearance.animation = match self.state.phase {
235 AgentPhase::Gestating => CharacterAnimation::Idle,
236 AgentPhase::Forming => CharacterAnimation::Exploring,
237 AgentPhase::Maturing => CharacterAnimation::Thinking,
238 AgentPhase::Stable => CharacterAnimation::Confident,
239 AgentPhase::Dissolving => CharacterAnimation::Fading,
240 AgentPhase::Dissolved => CharacterAnimation::Ghost,
241 };
242
243 self.appearance.opacity = match self.state.phase {
245 AgentPhase::Dissolving => 0.3,
246 AgentPhase::Dissolved => 0.1,
247 _ => 1.0,
248 };
249
250 self.appearance.expression = match self.state.phase {
252 AgentPhase::Gestating => "wondering".into(),
253 AgentPhase::Forming => "curious".into(),
254 AgentPhase::Maturing => "focused".into(),
255 AgentPhase::Stable => "serene".into(),
256 AgentPhase::Dissolving => "peaceful".into(),
257 AgentPhase::Dissolved => "ethereal".into(),
258 };
259
260 self.appearance.accessories.clear();
262 if self.state.predictions_made > 10 {
263 self.appearance.accessories.push("thinking_cap".into());
264 }
265 if self.state.accuracy > 0.9 {
266 self.appearance.accessories.push("golden_badge".into());
267 }
268 if self.state.readings_seen > 100 {
269 self.appearance.accessories.push("explorer_backpack".into());
270 }
271 }
272
273 pub fn flush_actions(&mut self) -> Vec<CharacterAction> {
275 self.action_queue.drain(..).collect()
276 }
277
278 pub fn character_card(&self) -> String {
280 format!(
281 "{} [{}] — {} | vibe: {:.2} | conf: {:.0}% | acc: {:.0}% | {} | {}",
282 self.appearance.name,
283 self.state.room_id,
284 phase_display_name(self.state.phase),
285 self.state.vibe,
286 self.state.confidence * 100.0,
287 self.state.accuracy * 100.0,
288 format!("{:?}", self.appearance.animation).to_lowercase(),
289 self.appearance.expression,
290 )
291 }
292}
293
294fn phase_display_name(phase: AgentPhase) -> &'static str {
295 match phase {
296 AgentPhase::Gestating => "Newborn",
297 AgentPhase::Forming => "Explorer",
298 AgentPhase::Maturing => "Scholar",
299 AgentPhase::Stable => "Sage",
300 AgentPhase::Dissolving => "Transcendent",
301 AgentPhase::Dissolved => "Spirit",
302 }
303}
304
305fn hue_to_rgb(h: f64) -> [f64; 3] {
307 let h6 = h * 6.0;
308 let sector = h6 as usize % 6;
309 let f = h6 - (h6 as usize) as f64;
310 match sector {
311 0 => [1.0, f, 0.0],
312 1 => [1.0 - f, 1.0, 0.0],
313 2 => [0.0, 1.0, f],
314 3 => [0.0, 1.0 - f, 1.0],
315 4 => [f, 0.0, 1.0],
316 _ => [1.0, 0.0, 1.0 - f],
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn test_new_shell() {
326 let shell = AgentShell::new("agent-1", "room-kitchen");
327 assert_eq!(shell.state.id, "agent-1");
328 assert_eq!(shell.state.phase, AgentPhase::Gestating);
329 assert_eq!(shell.appearance.animation, CharacterAnimation::Idle);
330 }
331
332 #[test]
333 fn test_observe_transitions_to_forming() {
334 let mut shell = AgentShell::new("a1", "r1");
335 shell.observe(0.5, 0.8);
336 assert_eq!(shell.state.phase, AgentPhase::Forming);
337 assert_eq!(shell.state.readings_seen, 1);
338 }
339
340 #[test]
341 fn test_observe_increases_energy() {
342 let mut shell = AgentShell::new("a1", "r1");
343 let initial_energy = shell.state.energy;
344 shell.observe(0.5, 0.8);
345 assert!(shell.state.energy > initial_energy);
346 }
347
348 #[test]
349 fn test_predict_updates_accuracy() {
350 let mut shell = AgentShell::new("a1", "r1");
351 shell.observe(0.5, 0.8);
352 shell.predict(0.5, 0.5); assert!((shell.state.accuracy - 1.0).abs() < 0.01);
354 }
355
356 #[test]
357 fn test_predict_bad_accuracy() {
358 let mut shell = AgentShell::new("a1", "r1");
359 shell.observe(0.5, 0.8);
360 shell.predict(0.0, 1.0); assert!(shell.state.accuracy < 0.5);
362 }
363
364 #[test]
365 fn test_celebrate_on_good_prediction() {
366 let mut shell = AgentShell::new("a1", "r1");
367 shell.observe(0.5, 0.8);
368 shell.predict(0.5, 0.52); let actions = shell.flush_actions();
370 assert!(actions.iter().any(|a| matches!(a.kind, ActionKind::Celebrate)));
371 }
372
373 #[test]
374 fn test_emote_on_bad_prediction() {
375 let mut shell = AgentShell::new("a1", "r1");
376 shell.observe(0.5, 0.8);
377 shell.predict(0.0, 1.0); let actions = shell.flush_actions();
379 assert!(actions.iter().any(|a| matches!(a.kind, ActionKind::Emote)));
380 }
381
382 #[test]
383 fn test_dissolve_changes_animation() {
384 let mut shell = AgentShell::new("a1", "r1");
385 shell.dissolve();
386 assert_eq!(shell.appearance.animation, CharacterAnimation::Fading);
387 assert!((shell.appearance.opacity - 0.3).abs() < 1e-10);
388 }
389
390 #[test]
391 fn test_finish_dissolve_ghost() {
392 let mut shell = AgentShell::new("a1", "r1");
393 shell.dissolve();
394 shell.finish_dissolve();
395 assert_eq!(shell.state.phase, AgentPhase::Dissolved);
396 assert_eq!(shell.appearance.animation, CharacterAnimation::Ghost);
397 assert!((shell.appearance.opacity - 0.1).abs() < 1e-10);
398 }
399
400 #[test]
401 fn test_vibe_affects_color() {
402 let mut shell = AgentShell::new("a1", "r1");
403 shell.observe(-1.0, 0.5); let blue = shell.appearance.body_color;
405 shell.observe(1.0, 0.5); let red = shell.appearance.body_color;
407 assert!(red[0] > blue[0], "high vibe should be redder");
408 }
409
410 #[test]
411 fn test_confidence_affects_glow() {
412 let mut shell = AgentShell::new("a1", "r1");
413 shell.observe(0.5, 0.1);
414 let low_glow = shell.appearance.glow_intensity;
415 shell.observe(0.5, 0.9);
416 let high_glow = shell.appearance.glow_intensity;
417 assert!(high_glow > low_glow);
418 }
419
420 #[test]
421 fn test_accessories() {
422 let mut shell = AgentShell::new("a1", "r1");
423 for _ in 0..15 {
424 shell.observe(0.5, 0.8);
425 shell.predict(0.5, 0.5);
426 }
427 assert!(shell.appearance.accessories.contains(&"thinking_cap".into()));
428 }
429
430 #[test]
431 fn test_golden_badge() {
432 let mut shell = AgentShell::new("a1", "r1");
433 for _ in 0..30 {
435 shell.observe(0.5, 0.9);
436 shell.predict(0.5, 0.51); }
438 if shell.state.accuracy > 0.9 {
439 assert!(shell.appearance.accessories.contains(&"golden_badge".into()));
440 }
441 }
442
443 #[test]
444 fn test_explorer_backpack() {
445 let mut shell = AgentShell::new("a1", "r1");
446 for _ in 0..101 {
447 shell.observe(0.5, 0.5);
448 }
449 assert!(shell.appearance.accessories.contains(&"explorer_backpack".into()));
450 }
451
452 #[test]
453 fn test_flush_actions() {
454 let mut shell = AgentShell::new("a1", "r1");
455 shell.observe(0.5, 0.8);
456 assert!(!shell.flush_actions().is_empty());
457 assert!(shell.flush_actions().is_empty()); }
459
460 #[test]
461 fn test_character_card() {
462 let shell = AgentShell::new("Nova", "room-lab");
463 let card = shell.character_card();
464 assert!(card.contains("Nova"));
465 assert!(card.contains("room-lab"));
466 assert!(card.contains("Newborn"));
467 }
468
469 #[test]
470 fn test_speak_action() {
471 let mut shell = AgentShell::new("a1", "r1");
472 shell.speak("Hello world!");
473 let actions = shell.flush_actions();
474 assert!(actions.iter().any(|a| matches!(a.kind, ActionKind::Speak)));
475 }
476
477 #[test]
478 fn test_teach_action() {
479 let mut shell = AgentShell::new("a1", "r1");
480 shell.teach("agent-2", 0.8);
481 let actions = shell.flush_actions();
482 let teach = actions.iter().find(|a| matches!(a.kind, ActionKind::Teach)).unwrap();
483 assert_eq!(teach.target.as_deref(), Some("agent-2"));
484 }
485
486 #[test]
487 fn test_phase_expressions() {
488 let mut shell = AgentShell::new("a1", "r1");
489 assert_eq!(shell.appearance.expression, "wondering");
490 shell.observe(0.5, 0.8);
491 assert_eq!(shell.appearance.expression, "curious");
492 shell.dissolve();
493 assert_eq!(shell.appearance.expression, "peaceful");
494 }
495
496 #[test]
497 fn test_serialization() {
498 let mut shell = AgentShell::new("a1", "r1");
499 shell.observe(0.5, 0.8);
500 let json = serde_json::to_string(&shell).unwrap();
501 let restored: AgentShell = serde_json::from_str(&json).unwrap();
502 assert_eq!(restored.state.id, "a1");
503 assert_eq!(restored.state.readings_seen, 1);
504 }
505
506 #[test]
507 fn test_hue_to_rgb_bounds() {
508 for h in [0.0, 0.25, 0.5, 0.75, 1.0] {
509 let [r, g, b] = hue_to_rgb(h);
510 assert!(r >= 0.0 && r <= 1.0, "r={r}");
511 assert!(g >= 0.0 && g <= 1.0, "g={g}");
512 assert!(b >= 0.0 && b <= 1.0, "b={b}");
513 }
514 }
515
516 #[test]
517 fn test_maturing_phase() {
518 let mut shell = AgentShell::new("a1", "r1");
519 for _ in 0..6 {
520 shell.observe(0.5, 0.8);
521 }
522 assert!(matches!(shell.state.phase, AgentPhase::Maturing | AgentPhase::Stable));
524 }
525}