behaviorsim-rs 0.7.0

Domain-agnostic specification for modeling individual psychology and social dynamics
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
//! Developmental modifiers for event processing.
//!
//! This module applies developmental effects to event interpretation during
//! temporal state computation. Effects include:
//!
//! - Life stage plasticity modifiers (children more plastic than adults)
//! - Sensitive period amplification (attachment events amplified in childhood)
//! - Turning point effects (temporary plasticity increases)
//!
//! # Design
//!
//! All functions in this module are internal-only. Consumers interact with
//! developmental effects implicitly through `state_at()` - they don't call
//! these functions directly.
//!
//! # Theoretical Foundation
//!
//! Based on Erikson's psychosocial stages and personality development research:
//! - Plasticity decreases with age (personality crystallizes by ~25)
//! - Sensitive periods amplify specific event categories at specific life stages
//! - Turning points temporarily increase plasticity (major life transitions)

use crate::entity::Entity;
use crate::enums::{DevelopmentalCategory, LifeStage};
use crate::event::Event;
use crate::types::Timestamp;

/// Constants for plasticity computation.
const PLASTICITY_MAX: f64 = 2.0;
const PLASTICITY_MIN: f64 = 0.5;
const PLASTICITY_DECAY_RATE: f64 = 0.023;

/// Days per year for age conversion.
const DAYS_PER_YEAR: f64 = 365.25;

/// Applies developmental modifiers to event impact.
///
/// This function modifies the raw event impact based on:
/// 1. Age-based plasticity (younger = higher impact)
/// 2. Sensitive period multipliers (specific events at specific ages)
/// 3. Turning point boosts (recent major life changes)
///
/// # Arguments
///
/// * `entity` - The entity being affected
/// * `event` - The event being processed
/// * `event_impact` - The base impact of the event
/// * `current_age_days` - The entity's current age in days
/// * `current_timestamp` - The absolute timestamp for the current state
///
/// # Returns
///
/// The modified impact after applying developmental modifiers.
///
/// # Formula
///
/// ```text
/// modified_impact = event_impact
///                   * (plasticity + turning_point_boost)
///                   * sensitive_period_multiplier
///                   * life_stage_event_multiplier
/// ```
#[must_use]
pub(crate) fn apply_developmental_effects(
    entity: &Entity,
    event: &Event,
    event_impact: f64,
    current_age_days: u64,
    current_timestamp: Timestamp,
) -> f64 {
    // Get species for life stage and time scale calculations
    let species = entity.species();
    let time_scale = f64::from(species.time_scale());

    // Convert age to human-equivalent years for plasticity calculation
    let age_days_f64 = current_age_days as f64;
    let age_years = (age_days_f64 / DAYS_PER_YEAR) * time_scale;

    // Compute life stage at event time (not anchor time)
    // Use raw age in years for species-aware life stage lookup
    let raw_age_years = age_days_f64 / DAYS_PER_YEAR;
    let life_stage = LifeStage::from_age_years_for_species(species, raw_age_years);

    // Compute plasticity modifier
    let plasticity = get_plasticity_modifier(&life_stage, age_years);

    // Compute turning point boost
    let turning_point_boost = entity
        .context()
        .chronosystem()
        .turning_point_plasticity_boost(current_timestamp);

    // Compute sensitive period multiplier
    let category = DevelopmentalCategory::from(&event.event_type());
    let sensitive_multiplier = get_sensitive_period_multiplier(&life_stage, &category);

    // Apply all modifiers
    let effective_plasticity = plasticity + turning_point_boost;
    let stage_multiplier = f64::from(life_stage.event_impact_multiplier());
    event_impact * effective_plasticity * sensitive_multiplier * stage_multiplier
}

/// Returns the plasticity modifier based on age.
///
/// Plasticity follows a continuous decreasing curve from 2.0 at birth
/// to 0.5 at age 65+. This reflects research showing personality
/// crystallizes with age.
///
/// # Formula
///
/// ```text
/// plasticity = max(0.5, 2.0 - (age_years * 0.023))
/// ```
///
/// # Arguments
///
/// * `_life_stage` - The life stage (unused, included for API consistency)
/// * `age_years` - Human-equivalent age in years
///
/// # Returns
///
/// Plasticity modifier in range [0.5, 2.0].
#[must_use]
pub(crate) fn get_plasticity_modifier(_life_stage: &LifeStage, age_years: f64) -> f64 {
    // Clamp age to non-negative
    let age = age_years.max(0.0);

    // Linear decay from 2.0 at birth to 0.5 at ~65 years
    let plasticity = PLASTICITY_MAX - (age * PLASTICITY_DECAY_RATE);

    plasticity.max(PLASTICITY_MIN)
}

/// Returns the sensitive period multiplier for a developmental category.
///
/// During sensitive periods, specific event categories have amplified effects.
/// For example, attachment events have 2.0x impact during childhood.
///
/// # Sensitive Periods
///
/// | Life Stage | Category | Multiplier |
/// |------------|----------|------------|
/// | Child | Attachment, Autonomy | 2.0x |
/// | Child | Initiative, Industry | 1.5x |
/// | Adolescent | Identity | 1.8x |
/// | YoungAdult | Intimacy | 1.3x |
/// | Adult | Generativity | 1.3x |
/// | MatureAdult | Generativity | 1.3x |
/// | Elder | Integrity | 1.2x |
///
/// # Arguments
///
/// * `life_stage` - The entity's current life stage
/// * `category` - The developmental category of the event
///
/// # Returns
///
/// Multiplier in range [1.0, 2.0]. Returns 1.0 if not in a sensitive period.
#[must_use]
pub(crate) fn get_sensitive_period_multiplier(
    life_stage: &LifeStage,
    category: &DevelopmentalCategory,
) -> f64 {
    match (life_stage, category) {
        // Child stage - attachment, autonomy, initiative, industry
        (LifeStage::Child, DevelopmentalCategory::Attachment) => 2.0,
        (LifeStage::Child, DevelopmentalCategory::Autonomy) => 2.0,
        (LifeStage::Child, DevelopmentalCategory::Initiative) => 1.5,
        (LifeStage::Child, DevelopmentalCategory::Industry) => 1.5,

        // Adolescent stage - identity
        (LifeStage::Adolescent, DevelopmentalCategory::Identity) => 1.8,

        // Young adult stage - intimacy
        (LifeStage::YoungAdult, DevelopmentalCategory::Intimacy) => 1.3,

        // Adult stage - generativity
        (LifeStage::Adult, DevelopmentalCategory::Generativity) => 1.3,

        // Mature adult stage - generativity continues
        (LifeStage::MatureAdult, DevelopmentalCategory::Generativity) => 1.3,

        // Elder stage - integrity
        (LifeStage::Elder, DevelopmentalCategory::Integrity) => 1.2,

        // All other combinations - no amplification
        _ => 1.0,
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::entity::EntityBuilder;
    use crate::enums::{EventType, Species};
    use crate::event::EventBuilder;
    use crate::types::{Duration, Timestamp};

    fn timestamp_for_days(days: u64) -> Timestamp {
        Timestamp::from_ymd_hms(2024, 1, 1, 0, 0, 0) + Duration::days(days)
    }

    // === Plasticity Tests ===

    #[test]
    fn child_event_impact_higher_than_adult() {
        let child_plasticity = get_plasticity_modifier(&LifeStage::Child, 5.0);
        let adult_plasticity = get_plasticity_modifier(&LifeStage::Adult, 40.0);
        assert!(child_plasticity > adult_plasticity);
    }

    #[test]
    fn personality_crystallizes_by_age_25() {
        let age_0_plasticity = get_plasticity_modifier(&LifeStage::Child, 0.0);
        let age_25_plasticity = get_plasticity_modifier(&LifeStage::YoungAdult, 25.0);

        // At age 0: 2.0
        assert!((age_0_plasticity - 2.0).abs() < f64::EPSILON);

        // At age 25: 2.0 - (25 * 0.023) = 2.0 - 0.575 = 1.425
        let expected_25 = 2.0 - (25.0 * 0.023);
        assert!((age_25_plasticity - expected_25).abs() < 0.001);

        // Significant reduction (more than 25%)
        let reduction = (age_0_plasticity - age_25_plasticity) / age_0_plasticity;
        assert!(reduction > 0.25);
    }

    #[test]
    fn plasticity_modifier_returns_expected_range() {
        let test_ages = [0.0, 5.0, 12.0, 18.0, 25.0, 40.0, 65.0, 80.0, 100.0];
        for age in test_ages {
            let plasticity = get_plasticity_modifier(&LifeStage::Adult, age);
            assert!(plasticity >= 0.5 && plasticity <= 2.0);
        }
    }

    #[test]
    fn plasticity_at_age_zero_is_maximum() {
        let plasticity = get_plasticity_modifier(&LifeStage::Child, 0.0);
        assert!((plasticity - 2.0).abs() < f64::EPSILON);
    }

    #[test]
    fn plasticity_at_age_65_is_minimum() {
        let plasticity = get_plasticity_modifier(&LifeStage::Elder, 65.0);
        // 2.0 - (65 * 0.023) = 2.0 - 1.495 = 0.505, but near the clamp
        assert!((plasticity - 0.505).abs() < 0.01);
    }

    #[test]
    fn plasticity_floors_at_minimum() {
        let plasticity_80 = get_plasticity_modifier(&LifeStage::Elder, 80.0);
        let plasticity_100 = get_plasticity_modifier(&LifeStage::Elder, 100.0);
        assert!((plasticity_80 - 0.5).abs() < f64::EPSILON);
        assert!((plasticity_100 - 0.5).abs() < f64::EPSILON);
    }

    #[test]
    fn plasticity_negative_age_clamps_to_zero() {
        let plasticity = get_plasticity_modifier(&LifeStage::Child, -5.0);
        assert!((plasticity - 2.0).abs() < f64::EPSILON);
    }

    // === Sensitive Period Tests ===

    #[test]
    fn attachment_event_amplified_in_childhood() {
        let multiplier =
            get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Attachment);
        assert!((multiplier - 2.0).abs() < f64::EPSILON);
    }

    #[test]
    fn identity_events_amplified_in_adolescence() {
        let multiplier = get_sensitive_period_multiplier(
            &LifeStage::Adolescent,
            &DevelopmentalCategory::Identity,
        );
        assert!((multiplier - 1.8).abs() < f64::EPSILON);
    }

    #[test]
    fn sensitive_period_returns_one_outside_period() {
        let adult_attachment =
            get_sensitive_period_multiplier(&LifeStage::Adult, &DevelopmentalCategory::Attachment);
        assert!((adult_attachment - 1.0).abs() < f64::EPSILON);

        let child_identity =
            get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Identity);
        assert!((child_identity - 1.0).abs() < f64::EPSILON);
    }

    #[test]
    fn autonomy_amplified_in_childhood() {
        let multiplier =
            get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Autonomy);
        assert!((multiplier - 2.0).abs() < f64::EPSILON);
    }

    #[test]
    fn initiative_amplified_in_childhood() {
        let multiplier =
            get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Initiative);
        assert!((multiplier - 1.5).abs() < f64::EPSILON);
    }

    #[test]
    fn industry_amplified_in_childhood() {
        let multiplier =
            get_sensitive_period_multiplier(&LifeStage::Child, &DevelopmentalCategory::Industry);
        assert!((multiplier - 1.5).abs() < f64::EPSILON);
    }

    #[test]
    fn intimacy_amplified_in_young_adult() {
        let multiplier = get_sensitive_period_multiplier(
            &LifeStage::YoungAdult,
            &DevelopmentalCategory::Intimacy,
        );
        assert!((multiplier - 1.3).abs() < f64::EPSILON);
    }

    #[test]
    fn generativity_amplified_in_adult() {
        let multiplier = get_sensitive_period_multiplier(
            &LifeStage::Adult,
            &DevelopmentalCategory::Generativity,
        );
        assert!((multiplier - 1.3).abs() < f64::EPSILON);
    }

    #[test]
    fn generativity_amplified_in_mature_adult() {
        let multiplier = get_sensitive_period_multiplier(
            &LifeStage::MatureAdult,
            &DevelopmentalCategory::Generativity,
        );
        assert!((multiplier - 1.3).abs() < f64::EPSILON);
    }

    #[test]
    fn integrity_amplified_in_elder() {
        let multiplier =
            get_sensitive_period_multiplier(&LifeStage::Elder, &DevelopmentalCategory::Integrity);
        assert!((multiplier - 1.2).abs() < f64::EPSILON);
    }

    #[test]
    fn neutral_category_returns_one() {
        for stage in LifeStage::all() {
            let multiplier =
                get_sensitive_period_multiplier(&stage, &DevelopmentalCategory::Neutral);
            assert!((multiplier - 1.0).abs() < f64::EPSILON);
        }
    }

    // === Species Scaling Tests ===

    #[test]
    fn dog_plasticity_uses_scaled_age() {
        let entity = EntityBuilder::new()
            .species(Species::Dog)
            .age(Duration::years(2))
            .build()
            .unwrap();

        assert_eq!(entity.life_stage(), LifeStage::YoungAdult);

        // Use EndRelationshipRomantic which maps to Intimacy category
        let event = EventBuilder::new(EventType::EndRelationshipRomantic).build().unwrap();
        let modified =
            apply_developmental_effects(&entity, &event, 1.0, 730, timestamp_for_days(730));

        // Dog at 2 years: time_scale ~6.67, human-equivalent ~13.3 years
        // Plasticity ~1.693, Intimacy category 1.3x in YoungAdult
        // Life stage multiplier 1.2x
        // Expected: 1.0 * 1.693 * 1.3 * 1.2 = ~2.64
        assert!(modified > 2.4 && modified < 2.9);
    }

    #[test]
    fn human_plasticity_uses_raw_age() {
        let entity = EntityBuilder::new()
            .species(Species::Human)
            .age(Duration::years(25))
            .build()
            .unwrap();

        let event = EventBuilder::new(EventType::ExperienceCombatMilitary).build().unwrap();
        let modified = apply_developmental_effects(
            &entity,
            &event,
            1.0,
            25 * 365,
            timestamp_for_days(25 * 365),
        );

        // Human at 25: time_scale 1.0, plasticity 1.425
        // Violence is Neutral category (1.0x), life stage multiplier 1.2x
        // Expected: 1.0 * 1.425 * 1.0 * 1.2 = 1.71
        assert!((modified - 1.71).abs() < 0.15);
    }

    // === Integration Tests ===

    #[test]
    fn apply_developmental_effects_modifies_event_impact() {
        let entity = EntityBuilder::new()
            .species(Species::Human)
            .age(Duration::years(8))
            .build()
            .unwrap();

        // Use ExperienceBetrayalTrust which maps to Attachment category
        let event = EventBuilder::new(EventType::ExperienceBetrayalTrust).build().unwrap();
        let base_impact = 0.5;
        let modified = apply_developmental_effects(
            &entity,
            &event,
            base_impact,
            8 * 365,
            timestamp_for_days(8 * 365),
        );

        // Child at age 8: plasticity ~1.816, Attachment category 2.0x
        // Life stage multiplier 2.0x
        // Expected: 0.5 * 1.816 * 2.0 * 2.0 = 3.632
        assert!(modified > base_impact);
        assert!((modified - 3.632).abs() < 0.2);
    }

    #[test]
    fn developmental_effects_pure_function() {
        let entity = EntityBuilder::new()
            .species(Species::Human)
            .age(Duration::years(30))
            .build()
            .unwrap();

        let event = EventBuilder::new(EventType::AchieveGoalMajor).build().unwrap();
        let original_age = entity.age();

        let result1 = apply_developmental_effects(
            &entity,
            &event,
            1.0,
            30 * 365,
            timestamp_for_days(30 * 365),
        );
        let result2 = apply_developmental_effects(
            &entity,
            &event,
            1.0,
            30 * 365,
            timestamp_for_days(30 * 365),
        );

        assert_eq!(entity.age(), original_age);
        assert!((result1 - result2).abs() < f64::EPSILON);
    }

    #[test]
    fn adolescent_identity_event_amplified() {
        let entity = EntityBuilder::new()
            .species(Species::Human)
            .age(Duration::years(15))
            .build()
            .unwrap();

        // Use DevelopIllnessChronic which maps to Identity category
        let event = EventBuilder::new(EventType::DevelopIllnessChronic).build().unwrap();
        let modified = apply_developmental_effects(
            &entity,
            &event,
            1.0,
            15 * 365,
            timestamp_for_days(15 * 365),
        );

        // Adolescent at 15: plasticity ~1.655, Identity category 1.8x
        // Life stage multiplier 1.5x
        // Expected: 1.0 * 1.655 * 1.8 * 1.5 = ~4.47
        assert!(modified > 4.0 && modified < 5.0);
    }

    #[test]
    fn elder_integrity_event_amplified() {
        let entity = EntityBuilder::new()
            .species(Species::Human)
            .age(Duration::years(75))
            .build()
            .unwrap();

        let event = EventBuilder::new(EventType::ExperienceAwarenessMortality).build().unwrap();
        let modified = apply_developmental_effects(
            &entity,
            &event,
            1.0,
            75 * 365,
            timestamp_for_days(75 * 365),
        );

        // Elder at 75: plasticity 0.5 (floor), Integrity category 1.2x
        // Life stage multiplier 0.8x
        // Expected: 1.0 * 0.5 * 1.2 * 0.8 = 0.48
        assert!((modified - 0.48).abs() < 0.1);
    }

    #[test]
    fn zero_impact_remains_zero() {
        let entity = EntityBuilder::new()
            .species(Species::Human)
            .age(Duration::years(10))
            .build()
            .unwrap();

        let event = EventBuilder::new(EventType::AchieveGoalMajor).build().unwrap();
        let modified = apply_developmental_effects(
            &entity,
            &event,
            0.0,
            10 * 365,
            timestamp_for_days(10 * 365),
        );
        assert!(modified.abs() < f64::EPSILON);
    }

    #[test]
    fn negative_impact_scales_correctly() {
        let entity = EntityBuilder::new()
            .species(Species::Human)
            .age(Duration::years(8))
            .build()
            .unwrap();

        // Use ExperienceBetrayalTrust which maps to Attachment category
        let event = EventBuilder::new(EventType::ExperienceBetrayalTrust).build().unwrap();
        let modified = apply_developmental_effects(
            &entity,
            &event,
            -0.5,
            8 * 365,
            timestamp_for_days(8 * 365),
        );

        // Child at 8: plasticity ~1.816, Attachment category 2.0x
        // Life stage multiplier 2.0x
        // Expected: -0.5 * 1.816 * 2.0 * 2.0 = -3.632
        assert!(modified < 0.0);
        assert!((modified - (-3.632)).abs() < 0.2);
    }
}