1#[derive(Clone, Debug, PartialEq)]
36#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
37pub struct Personality {
38 pub curiosity_drive: f32,
43 pub startle_sensitivity: f32,
48 pub recovery_speed: f32,
53}
54
55impl Personality {
56 pub fn new() -> Self {
58 Self {
59 curiosity_drive: 0.5,
60 startle_sensitivity: 0.5,
61 recovery_speed: 0.5,
62 }
63 }
64
65 pub fn modulate_coherence_gain(&self, base: f32) -> f32 {
69 base * (0.5 + self.recovery_speed)
70 }
71
72 pub fn modulate_startle_drop(&self, base: f32) -> f32 {
76 base * (0.5 + self.startle_sensitivity)
77 }
78}
79
80impl Default for Personality {
81 fn default() -> Self {
82 Self::new()
83 }
84}
85
86#[derive(Clone, Debug)]
99#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
100pub struct PhaseSpace {
101 pub coherence_high_enter: f32,
103 pub coherence_high_exit: f32,
105 pub tension_high_enter: f32,
107 pub tension_high_exit: f32,
109}
110
111impl PhaseSpace {
112 pub fn new() -> Self {
114 Self::default()
115 }
116}
117
118impl Default for PhaseSpace {
119 fn default() -> Self {
120 Self {
121 coherence_high_enter: 0.65,
122 coherence_high_exit: 0.55,
123 tension_high_enter: 0.45,
124 tension_high_exit: 0.35,
125 }
126 }
127}
128
129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
146#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
147pub enum SocialPhase {
148 ShyObserver,
150 StartledRetreat,
152 QuietlyBeloved,
154 ProtectiveGuardian,
156}
157
158impl SocialPhase {
159 pub fn classify(
166 effective_coherence: f32,
167 tension: f32,
168 prev: SocialPhase,
169 ps: &PhaseSpace,
170 ) -> SocialPhase {
171 let high_coherence = match prev {
172 SocialPhase::QuietlyBeloved | SocialPhase::ProtectiveGuardian => {
173 effective_coherence >= ps.coherence_high_exit
174 }
175 _ => effective_coherence >= ps.coherence_high_enter,
176 };
177
178 let high_tension = match prev {
179 SocialPhase::StartledRetreat | SocialPhase::ProtectiveGuardian => {
180 tension >= ps.tension_high_exit
181 }
182 _ => tension >= ps.tension_high_enter,
183 };
184
185 match (high_coherence, high_tension) {
186 (false, false) => SocialPhase::ShyObserver,
187 (false, true) => SocialPhase::StartledRetreat,
188 (true, false) => SocialPhase::QuietlyBeloved,
189 (true, true) => SocialPhase::ProtectiveGuardian,
190 }
191 }
192
193 pub fn expression_scale(&self) -> f32 {
199 permeability(0.5, 0.3, *self)
200 }
201
202 pub fn led_tint(&self) -> [u8; 3] {
204 match self {
205 SocialPhase::ShyObserver => [40, 40, 80], SocialPhase::StartledRetreat => [80, 20, 20], SocialPhase::QuietlyBeloved => [60, 120, 200], SocialPhase::ProtectiveGuardian => [200, 100, 0], }
210 }
211}
212
213pub fn permeability(effective_coherence: f32, _tension: f32, quadrant: SocialPhase) -> f32 {
230 match quadrant {
231 SocialPhase::ShyObserver => effective_coherence * 0.3,
232 SocialPhase::StartledRetreat => 0.1,
233 SocialPhase::QuietlyBeloved => 0.5 + effective_coherence * 0.5,
234 SocialPhase::ProtectiveGuardian => 0.4 + effective_coherence * 0.2,
235 }
236}
237
238#[derive(Clone, Copy, Debug, PartialEq, Eq)]
243#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
244pub enum NarrationDepth {
245 None,
247 Minimal,
249 Brief,
251 Full,
253 Deep,
255}
256
257impl NarrationDepth {
258 pub fn from_permeability(p: f32) -> Self {
260 if p < 0.2 {
261 NarrationDepth::None
262 } else if p < 0.4 {
263 NarrationDepth::Minimal
264 } else if p < 0.6 {
265 NarrationDepth::Brief
266 } else if p < 0.8 {
267 NarrationDepth::Full
268 } else {
269 NarrationDepth::Deep
270 }
271 }
272}
273
274#[cfg(test)]
277mod tests {
278 use super::*;
279
280 #[test]
283 fn test_personality_new_mid_range() {
284 let p = Personality::new();
285 assert!((p.curiosity_drive - 0.5).abs() < f32::EPSILON);
286 assert!((p.startle_sensitivity - 0.5).abs() < f32::EPSILON);
287 assert!((p.recovery_speed - 0.5).abs() < f32::EPSILON);
288 }
289
290 #[test]
291 fn test_personality_modulate_coherence_gain() {
292 let p = Personality { curiosity_drive: 0.5, startle_sensitivity: 0.5, recovery_speed: 0.9 };
293 let result = p.modulate_coherence_gain(0.02);
295 assert!((result - 0.02 * 1.4).abs() < f32::EPSILON, "got {}", result);
296 }
297
298 #[test]
299 fn test_personality_modulate_startle_drop() {
300 let p = Personality { curiosity_drive: 0.5, startle_sensitivity: 0.1, recovery_speed: 0.5 };
301 let result = p.modulate_startle_drop(0.05);
303 assert!((result - 0.05 * 0.6).abs() < f32::EPSILON, "got {}", result);
304 }
305
306 #[test]
309 fn test_phase_space_default_values() {
310 let ps = PhaseSpace::default();
311 assert!((ps.coherence_high_enter - 0.65).abs() < f32::EPSILON);
312 assert!((ps.coherence_high_exit - 0.55).abs() < f32::EPSILON);
313 assert!((ps.tension_high_enter - 0.45).abs() < f32::EPSILON);
314 assert!((ps.tension_high_exit - 0.35).abs() < f32::EPSILON);
315 }
316
317 #[test]
320 fn test_social_phase_shy_observer() {
321 let ps = PhaseSpace::default();
322 let phase = SocialPhase::classify(0.1, 0.1, SocialPhase::ShyObserver, &ps);
323 assert_eq!(phase, SocialPhase::ShyObserver);
324 }
325
326 #[test]
327 fn test_social_phase_quietly_beloved() {
328 let ps = PhaseSpace::default();
329 let phase = SocialPhase::classify(0.8, 0.1, SocialPhase::ShyObserver, &ps);
330 assert_eq!(phase, SocialPhase::QuietlyBeloved);
331 }
332
333 #[test]
334 fn test_social_phase_startled_retreat() {
335 let ps = PhaseSpace::default();
336 let phase = SocialPhase::classify(0.1, 0.7, SocialPhase::ShyObserver, &ps);
337 assert_eq!(phase, SocialPhase::StartledRetreat);
338 }
339
340 #[test]
341 fn test_social_phase_protective_guardian() {
342 let ps = PhaseSpace::default();
343 let phase = SocialPhase::classify(0.8, 0.7, SocialPhase::ShyObserver, &ps);
344 assert_eq!(phase, SocialPhase::ProtectiveGuardian);
345 }
346
347 #[test]
348 fn test_social_phase_hysteresis_coherence() {
349 let ps = PhaseSpace::default();
350
351 let phase = SocialPhase::classify(0.66, 0.1, SocialPhase::ShyObserver, &ps);
353 assert_eq!(phase, SocialPhase::QuietlyBeloved);
354
355 let phase = SocialPhase::classify(0.56, 0.1, SocialPhase::QuietlyBeloved, &ps);
357 assert_eq!(phase, SocialPhase::QuietlyBeloved);
358
359 let phase = SocialPhase::classify(0.54, 0.1, SocialPhase::QuietlyBeloved, &ps);
361 assert_eq!(phase, SocialPhase::ShyObserver);
362 }
363
364 #[test]
365 fn test_social_phase_hysteresis_tension() {
366 let ps = PhaseSpace::default();
367
368 let phase = SocialPhase::classify(0.1, 0.46, SocialPhase::ShyObserver, &ps);
370 assert_eq!(phase, SocialPhase::StartledRetreat);
371
372 let phase = SocialPhase::classify(0.1, 0.36, SocialPhase::StartledRetreat, &ps);
374 assert_eq!(phase, SocialPhase::StartledRetreat);
375
376 let phase = SocialPhase::classify(0.1, 0.34, SocialPhase::StartledRetreat, &ps);
378 assert_eq!(phase, SocialPhase::ShyObserver);
379 }
380
381 #[test]
382 fn test_custom_thresholds_stricter() {
383 let strict = PhaseSpace {
384 coherence_high_enter: 0.80,
385 coherence_high_exit: 0.70,
386 ..PhaseSpace::default()
387 };
388
389 let phase = SocialPhase::classify(0.70, 0.1, SocialPhase::ShyObserver, &strict);
391 assert_eq!(
392 phase,
393 SocialPhase::ShyObserver,
394 "coherence 0.70 should NOT enter QB with strict threshold 0.80"
395 );
396
397 let phase = SocialPhase::classify(0.85, 0.1, SocialPhase::ShyObserver, &strict);
399 assert_eq!(
400 phase,
401 SocialPhase::QuietlyBeloved,
402 "coherence 0.85 should enter QB with strict threshold 0.80"
403 );
404
405 let phase = SocialPhase::classify(0.75, 0.1, SocialPhase::QuietlyBeloved, &strict);
407 assert_eq!(
408 phase,
409 SocialPhase::QuietlyBeloved,
410 "coherence 0.75 should stay in QB (above exit 0.70)"
411 );
412
413 let phase = SocialPhase::classify(0.65, 0.1, SocialPhase::QuietlyBeloved, &strict);
415 assert_eq!(
416 phase,
417 SocialPhase::ShyObserver,
418 "coherence 0.65 should exit QB (below exit 0.70)"
419 );
420 }
421
422 #[test]
423 fn test_custom_thresholds_looser() {
424 let loose = PhaseSpace {
425 coherence_high_enter: 0.40,
426 coherence_high_exit: 0.30,
427 ..PhaseSpace::default()
428 };
429
430 let phase = SocialPhase::classify(0.42, 0.1, SocialPhase::ShyObserver, &loose);
432 assert_eq!(
433 phase,
434 SocialPhase::QuietlyBeloved,
435 "coherence 0.42 should enter QB with loose threshold 0.40"
436 );
437
438 let ps = PhaseSpace::default();
440 let phase = SocialPhase::classify(0.42, 0.1, SocialPhase::ShyObserver, &ps);
441 assert_eq!(
442 phase,
443 SocialPhase::ShyObserver,
444 "coherence 0.42 should NOT enter QB with default threshold 0.65"
445 );
446 }
447
448 #[test]
449 fn test_full_quadrant_sweep_with_default_thresholds() {
450 let ps = PhaseSpace::default();
451
452 let cases: &[(f32, f32, SocialPhase, SocialPhase)] = &[
453 (0.1, 0.1, SocialPhase::ShyObserver, SocialPhase::ShyObserver),
454 (0.8, 0.1, SocialPhase::ShyObserver, SocialPhase::QuietlyBeloved),
455 (0.1, 0.7, SocialPhase::ShyObserver, SocialPhase::StartledRetreat),
456 (0.8, 0.7, SocialPhase::ShyObserver, SocialPhase::ProtectiveGuardian),
457 (0.56, 0.1, SocialPhase::QuietlyBeloved, SocialPhase::QuietlyBeloved),
459 (0.54, 0.1, SocialPhase::QuietlyBeloved, SocialPhase::ShyObserver),
461 (0.1, 0.36, SocialPhase::StartledRetreat, SocialPhase::StartledRetreat),
463 (0.1, 0.34, SocialPhase::StartledRetreat, SocialPhase::ShyObserver),
465 ];
466
467 for &(coh, ten, prev, expected) in cases {
468 let result = SocialPhase::classify(coh, ten, prev, &ps);
469 assert_eq!(
470 result, expected,
471 "coh={} ten={} prev={:?}: got {:?}, expected {:?}",
472 coh, ten, prev, result, expected
473 );
474 }
475 }
476
477 #[test]
478 fn test_expression_scale_ordering() {
479 assert!(
480 SocialPhase::QuietlyBeloved.expression_scale()
481 > SocialPhase::ProtectiveGuardian.expression_scale()
482 );
483 assert!(
484 SocialPhase::ProtectiveGuardian.expression_scale()
485 > SocialPhase::ShyObserver.expression_scale()
486 );
487 assert!(
488 SocialPhase::ShyObserver.expression_scale()
489 > SocialPhase::StartledRetreat.expression_scale()
490 );
491 }
492
493 #[test]
494 fn test_led_tint_distinct() {
495 let so = SocialPhase::ShyObserver.led_tint();
496 let sr = SocialPhase::StartledRetreat.led_tint();
497 let qb = SocialPhase::QuietlyBeloved.led_tint();
498 let pg = SocialPhase::ProtectiveGuardian.led_tint();
499 assert_ne!(so, sr);
501 assert_ne!(so, qb);
502 assert_ne!(so, pg);
503 assert_ne!(sr, qb);
504 assert_ne!(sr, pg);
505 assert_ne!(qb, pg);
506 }
507
508 #[test]
511 fn test_permeability_shy_observer_range() {
512 let p_zero = permeability(0.0, 0.3, SocialPhase::ShyObserver);
513 assert!((p_zero - 0.0).abs() < f32::EPSILON, "got {}", p_zero);
514
515 let p_max = permeability(1.0, 0.3, SocialPhase::ShyObserver);
516 assert!((p_max - 0.3).abs() < f32::EPSILON, "got {}", p_max);
517
518 let p_mid = permeability(0.5, 0.3, SocialPhase::ShyObserver);
519 assert!((p_mid - 0.15).abs() < f32::EPSILON, "got {}", p_mid);
520 }
521
522 #[test]
523 fn test_permeability_startled_retreat_fixed() {
524 for coh in &[0.0_f32, 0.25, 0.5, 0.75, 1.0] {
525 for ten in &[0.0_f32, 0.5, 1.0] {
526 let p = permeability(*coh, *ten, SocialPhase::StartledRetreat);
527 assert!(
528 (p - 0.1).abs() < f32::EPSILON,
529 "SR should always be 0.1, got {} at coh={} ten={}",
530 p,
531 coh,
532 ten
533 );
534 }
535 }
536 }
537
538 #[test]
539 fn test_permeability_quietly_beloved_range() {
540 let p_zero = permeability(0.0, 0.3, SocialPhase::QuietlyBeloved);
541 assert!((p_zero - 0.5).abs() < f32::EPSILON, "got {}", p_zero);
542
543 let p_max = permeability(1.0, 0.3, SocialPhase::QuietlyBeloved);
544 assert!((p_max - 1.0).abs() < f32::EPSILON, "got {}", p_max);
545
546 let p_mid = permeability(0.5, 0.3, SocialPhase::QuietlyBeloved);
547 assert!((p_mid - 0.75).abs() < f32::EPSILON, "got {}", p_mid);
548 }
549
550 #[test]
551 fn test_permeability_protective_guardian_range() {
552 let p_zero = permeability(0.0, 0.3, SocialPhase::ProtectiveGuardian);
553 assert!((p_zero - 0.4).abs() < f32::EPSILON, "got {}", p_zero);
554
555 let p_max = permeability(1.0, 0.3, SocialPhase::ProtectiveGuardian);
556 assert!((p_max - 0.6).abs() < f32::EPSILON, "got {}", p_max);
557
558 let p_mid = permeability(0.5, 0.3, SocialPhase::ProtectiveGuardian);
559 assert!((p_mid - 0.5).abs() < f32::EPSILON, "got {}", p_mid);
560 }
561
562 #[test]
563 fn test_permeability_ordering() {
564 let coh = 0.7;
565 let ten = 0.3;
566 let qb = permeability(coh, ten, SocialPhase::QuietlyBeloved);
567 let pg = permeability(coh, ten, SocialPhase::ProtectiveGuardian);
568 let so = permeability(coh, ten, SocialPhase::ShyObserver);
569 let sr = permeability(coh, ten, SocialPhase::StartledRetreat);
570
571 assert!(qb > pg, "QB({}) should be > PG({})", qb, pg);
572 assert!(pg > so, "PG({}) should be > SO({})", pg, so);
573 assert!(so > sr, "SO({}) should be > SR({})", so, sr);
574 }
575
576 #[test]
577 fn test_expression_scale_matches_permeability() {
578 let qb = SocialPhase::QuietlyBeloved.expression_scale();
579 let pg = SocialPhase::ProtectiveGuardian.expression_scale();
580 let so = SocialPhase::ShyObserver.expression_scale();
581 let sr = SocialPhase::StartledRetreat.expression_scale();
582
583 assert!((qb - permeability(0.5, 0.3, SocialPhase::QuietlyBeloved)).abs() < f32::EPSILON);
584 assert!(
585 (pg - permeability(0.5, 0.3, SocialPhase::ProtectiveGuardian)).abs() < f32::EPSILON
586 );
587 assert!((so - permeability(0.5, 0.3, SocialPhase::ShyObserver)).abs() < f32::EPSILON);
588 assert!((sr - permeability(0.5, 0.3, SocialPhase::StartledRetreat)).abs() < f32::EPSILON);
589 }
590
591 #[test]
594 fn test_narration_depth_thresholds() {
595 assert_eq!(NarrationDepth::from_permeability(0.0), NarrationDepth::None);
596 assert_eq!(NarrationDepth::from_permeability(0.19), NarrationDepth::None);
597 assert_eq!(NarrationDepth::from_permeability(0.2), NarrationDepth::Minimal);
598 assert_eq!(NarrationDepth::from_permeability(0.39), NarrationDepth::Minimal);
599 assert_eq!(NarrationDepth::from_permeability(0.4), NarrationDepth::Brief);
600 assert_eq!(NarrationDepth::from_permeability(0.59), NarrationDepth::Brief);
601 assert_eq!(NarrationDepth::from_permeability(0.6), NarrationDepth::Full);
602 assert_eq!(NarrationDepth::from_permeability(0.79), NarrationDepth::Full);
603 assert_eq!(NarrationDepth::from_permeability(0.8), NarrationDepth::Deep);
604 assert_eq!(NarrationDepth::from_permeability(1.0), NarrationDepth::Deep);
605 }
606
607 #[test]
608 fn test_narration_depth_matches_quadrants() {
609 assert_eq!(
611 NarrationDepth::from_permeability(permeability(1.0, 0.3, SocialPhase::ShyObserver)),
612 NarrationDepth::Minimal
613 );
614 assert_eq!(
616 NarrationDepth::from_permeability(permeability(0.5, 0.5, SocialPhase::StartledRetreat)),
617 NarrationDepth::None
618 );
619 assert_eq!(
621 NarrationDepth::from_permeability(permeability(1.0, 0.1, SocialPhase::QuietlyBeloved)),
622 NarrationDepth::Deep
623 );
624 assert_eq!(
626 NarrationDepth::from_permeability(permeability(
627 0.5,
628 0.5,
629 SocialPhase::ProtectiveGuardian
630 )),
631 NarrationDepth::Brief
632 );
633 }
634}