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